Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2795dddc12 | |||
| 8721a653e7 | |||
| 694b68de1a | |||
| 71bf2631f4 | |||
| 4ab2377970 | |||
| 8a0f9ef4e4 |
@@ -0,0 +1,21 @@
|
||||
name: Publish Apps
|
||||
|
||||
inputs:
|
||||
sdk_version:
|
||||
description: The SDK version that determines the path on the CDN
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 'Download cdn-files'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: 'cdn-files'
|
||||
path: cdn_files
|
||||
- name: 'Install boto3'
|
||||
shell: bash
|
||||
run: pip install boto3
|
||||
- name: 'Upload files'
|
||||
shell: bash
|
||||
run: python Buildscripts/CDN/upload-app-files.py cdn_files ${{ inputs.sdk_version }} ${{ env.CDN_ID }} ${{ env.CDN_TOKEN_NAME }} ${{ env.CDN_TOKEN_VALUE }}
|
||||
@@ -1,5 +1,10 @@
|
||||
name: Release Apps
|
||||
|
||||
outputs:
|
||||
sdk_version:
|
||||
description: 'Common SDK version shared by all bundled apps'
|
||||
value: ${{ steps.release.outputs.sdk_version }}
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -11,8 +16,11 @@ runs:
|
||||
run: rsync -av downloaded_apps/*/*.app cdn_files/
|
||||
shell: bash
|
||||
- name: 'Create CDN release files'
|
||||
run: python release.py cdn_files/
|
||||
id: release
|
||||
shell: bash
|
||||
run: |
|
||||
python release.py cdn_files/
|
||||
echo "sdk_version=$(cat sdk_version.txt)" >> "$GITHUB_OUTPUT"
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -16,6 +16,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Build"
|
||||
uses: ./.github/actions/build-app
|
||||
with:
|
||||
@@ -23,7 +25,26 @@ jobs:
|
||||
Bundle:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [Build]
|
||||
outputs:
|
||||
sdk_version: ${{ steps.release.outputs.sdk_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Build"
|
||||
id: release
|
||||
uses: ./.github/actions/release-apps
|
||||
PublishApps:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [Bundle]
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Publish Apps"
|
||||
env:
|
||||
CDN_ID: ${{ secrets.CDN_ID }}
|
||||
CDN_TOKEN_NAME: ${{ secrets.CDN_TOKEN_NAME }}
|
||||
CDN_TOKEN_VALUE: ${{ secrets.CDN_TOKEN_VALUE }}
|
||||
uses: ./.github/actions/publish-apps
|
||||
with:
|
||||
sdk_version: ${{ needs.Bundle.outputs.sdk_version }}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.brainfuck
|
||||
versionName=0.2.0
|
||||
versionCode=2
|
||||
name=Brainfuck interpreter
|
||||
description=Brainfuck esoteric language interpreter
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.brainfuck
|
||||
app.version.name=0.5.0
|
||||
app.version.code=5
|
||||
app.name=Brainfuck interpreter
|
||||
app.description=Brainfuck esoteric language interpreter
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.breakout
|
||||
versionName=0.2.0
|
||||
versionCode=2
|
||||
name=Breakout
|
||||
description=Classic brick-breaking arcade game
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.breakout
|
||||
app.version.name=0.5.0
|
||||
app.version.code=5
|
||||
app.name=Breakout
|
||||
app.description=Classic brick-breaking arcade game
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.calculator
|
||||
versionName=0.3.0
|
||||
versionCode=3
|
||||
name=Calculator
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.calculator
|
||||
app.version.name=0.6.0
|
||||
app.version.code=6
|
||||
app.name=Calculator
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.diceware
|
||||
versionName=0.3.0
|
||||
versionCode=3
|
||||
name=Diceware
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.diceware
|
||||
app.version.name=0.6.0
|
||||
app.version.code=6
|
||||
app.name=Diceware
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32s3,esp32p4
|
||||
[app]
|
||||
id=one.tactility.epubreader
|
||||
versionName=0.1.0
|
||||
versionCode=1
|
||||
name=Epub Reader
|
||||
description=Epub and text file reader. Requires PSRAM!
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32s3,esp32p4
|
||||
app.id=one.tactility.epubreader
|
||||
app.version.name=0.4.0
|
||||
app.version.code=4
|
||||
app.name=Epub Reader
|
||||
app.description=Epub and text file reader. Requires PSRAM!
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.gpio
|
||||
versionName=0.4.0
|
||||
versionCode=4
|
||||
name=GPIO
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.gpio
|
||||
app.version.name=0.7.0
|
||||
app.version.code=7
|
||||
app.name=GPIO
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.graphicsdemo
|
||||
versionName=0.3.0
|
||||
versionCode=3
|
||||
name=Graphics Demo
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.graphicsdemo
|
||||
app.version.name=0.6.0
|
||||
app.version.code=6
|
||||
app.name=Graphics Demo
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.helloworld
|
||||
versionName=0.3.0
|
||||
versionCode=3
|
||||
name=Hello World
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.helloworld
|
||||
app.version.name=0.6.0
|
||||
app.version.code=6
|
||||
app.name=Hello World
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32s3,esp32p4
|
||||
[app]
|
||||
id=one.tactility.m5unittest
|
||||
versionName=0.1.0
|
||||
versionCode=1
|
||||
name=M5 Unit Test
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32s3,esp32p4
|
||||
app.id=one.tactility.m5unittest
|
||||
app.version.name=0.4.0
|
||||
app.version.code=4
|
||||
app.name=M5 Unit Test
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.magic8ball
|
||||
versionName=0.2.0
|
||||
versionCode=2
|
||||
name=Magic 8-Ball
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.magic8ball
|
||||
app.version.name=0.5.0
|
||||
app.version.code=5
|
||||
app.name=Magic 8-Ball
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32s3,esp32p4
|
||||
[app]
|
||||
id=one.tactility.mediakeys
|
||||
versionName=0.1.0
|
||||
versionCode=1
|
||||
name=Media Keys
|
||||
description=Bluetooth media keys. Touch or Physical Keyboard control\nB - previous, P - play/pause, N - next, M - mute, D - volume down, U - volume up.\nQ or ESC to exit focus.
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32s3,esp32p4
|
||||
app.id=one.tactility.mediakeys
|
||||
app.version.name=0.4.0
|
||||
app.version.code=4
|
||||
app.name=Media Keys
|
||||
app.description=Bluetooth media keys. Touch or Physical Keyboard control\nB - previous, P - play/pause, N - next, M - mute, D - volume down, U - volume up.\nQ or ESC to exit focus.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
REQUIRES TactilitySDK
|
||||
)
|
||||
@@ -1,427 +0,0 @@
|
||||
#include <tt_app.h>
|
||||
#include <tt_lvgl.h>
|
||||
#include <tt_lvgl_toolbar.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#define MINIMP3_IMPLEMENTATION
|
||||
#define MINIMP3_NO_SIMD
|
||||
#include "minimp3.h"
|
||||
|
||||
#define TAG "Mp3Player"
|
||||
#define MP3_INPUT_BUFFER_SIZE 16384
|
||||
|
||||
typedef enum {
|
||||
STATE_IDLE,
|
||||
STATE_PLAYING,
|
||||
STATE_PAUSED
|
||||
} PlaybackState;
|
||||
|
||||
typedef struct {
|
||||
struct Device* i2s_dev;
|
||||
char filepath[512];
|
||||
PlaybackState state;
|
||||
|
||||
// UI elements
|
||||
lv_obj_t* lbl_title;
|
||||
lv_obj_t* lbl_status;
|
||||
lv_obj_t* bar_progress;
|
||||
lv_obj_t* btn_play_pause;
|
||||
lv_obj_t* btn_stop;
|
||||
|
||||
// Playback state variables
|
||||
FILE* file;
|
||||
mp3dec_t decoder;
|
||||
uint8_t* input_buf;
|
||||
mp3d_sample_t* pcm_buf;
|
||||
size_t buffered_bytes;
|
||||
bool eof;
|
||||
int volume;
|
||||
|
||||
int sample_rate;
|
||||
int channels;
|
||||
|
||||
// Progress calculation
|
||||
size_t file_size;
|
||||
size_t bytes_read_total;
|
||||
|
||||
TaskHandle_t playback_task_handle;
|
||||
} AppCtx;
|
||||
|
||||
static AppCtx g_ctx;
|
||||
|
||||
/* ─── Update UI ─── */
|
||||
static void update_ui(AppCtx* ctx) {
|
||||
if (!ctx->lbl_status) return;
|
||||
|
||||
switch (ctx->state) {
|
||||
case STATE_PLAYING:
|
||||
lv_label_set_text(ctx->lbl_status, "Playing...");
|
||||
lv_label_set_text(lv_obj_get_child(ctx->btn_play_pause, 0), LV_SYMBOL_PAUSE " Pause");
|
||||
lv_obj_clear_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||
lv_obj_clear_state(ctx->btn_stop, LV_STATE_DISABLED);
|
||||
break;
|
||||
case STATE_PAUSED:
|
||||
lv_label_set_text(ctx->lbl_status, "Paused");
|
||||
lv_label_set_text(lv_obj_get_child(ctx->btn_play_pause, 0), LV_SYMBOL_PLAY " Play");
|
||||
lv_obj_clear_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||
lv_obj_clear_state(ctx->btn_stop, LV_STATE_DISABLED);
|
||||
break;
|
||||
case STATE_IDLE:
|
||||
default:
|
||||
if (ctx->filepath[0] != '\0') {
|
||||
lv_label_set_text(ctx->lbl_status, "Ready");
|
||||
lv_obj_clear_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_label_set_text(ctx->lbl_status, "No file loaded");
|
||||
lv_obj_add_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||
}
|
||||
lv_label_set_text(lv_obj_get_child(ctx->btn_play_pause, 0), LV_SYMBOL_PLAY " Play");
|
||||
lv_obj_add_state(ctx->btn_stop, LV_STATE_DISABLED);
|
||||
lv_bar_set_value(ctx->bar_progress, 0, LV_ANIM_OFF);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Playback Task ─── */
|
||||
static void mp3_playback_task(void* arg) {
|
||||
AppCtx* ctx = (AppCtx*)arg;
|
||||
|
||||
ctx->file = fopen(ctx->filepath, "rb");
|
||||
if (!ctx->file) {
|
||||
ESP_LOGE(TAG, "Failed to open file: %s", ctx->filepath);
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
ctx->state = STATE_IDLE;
|
||||
lv_label_set_text(ctx->lbl_status, "Error: File not found");
|
||||
update_ui(ctx);
|
||||
tt_lvgl_unlock();
|
||||
ctx->playback_task_handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
fseek(ctx->file, 0, SEEK_END);
|
||||
ctx->file_size = ftell(ctx->file);
|
||||
fseek(ctx->file, 0, SEEK_SET);
|
||||
ctx->bytes_read_total = 0;
|
||||
|
||||
mp3dec_init(&ctx->decoder);
|
||||
ctx->buffered_bytes = 0;
|
||||
ctx->eof = false;
|
||||
ctx->sample_rate = 0;
|
||||
ctx->channels = 0;
|
||||
|
||||
ESP_LOGI(TAG, "Starting MP3 playback: %s (size: %d bytes)", ctx->filepath, ctx->file_size);
|
||||
|
||||
while (ctx->state != STATE_IDLE) {
|
||||
if (ctx->state == STATE_PAUSED) {
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fill input buffer
|
||||
if (!ctx->eof && ctx->buffered_bytes < MP3_INPUT_BUFFER_SIZE) {
|
||||
size_t to_read = MP3_INPUT_BUFFER_SIZE - ctx->buffered_bytes;
|
||||
size_t read_bytes = fread(ctx->input_buf + ctx->buffered_bytes, 1, to_read, ctx->file);
|
||||
ctx->buffered_bytes += read_bytes;
|
||||
ctx->bytes_read_total += read_bytes;
|
||||
if (read_bytes == 0) {
|
||||
ctx->eof = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx->buffered_bytes == 0 && ctx->eof) {
|
||||
ESP_LOGI(TAG, "Reached EOF");
|
||||
break;
|
||||
}
|
||||
|
||||
// Decode one frame
|
||||
mp3dec_frame_info_t info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
int samples = mp3dec_decode_frame(&ctx->decoder, ctx->input_buf, (int)ctx->buffered_bytes, ctx->pcm_buf, &info);
|
||||
|
||||
if (info.frame_bytes <= 0) {
|
||||
if (ctx->eof) {
|
||||
break;
|
||||
}
|
||||
// Move 1 byte forward to resync
|
||||
memmove(ctx->input_buf, ctx->input_buf + 1, --ctx->buffered_bytes);
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t consumed = (size_t)info.frame_bytes;
|
||||
ctx->buffered_bytes -= consumed;
|
||||
memmove(ctx->input_buf, ctx->input_buf + consumed, ctx->buffered_bytes);
|
||||
|
||||
if (samples > 0) {
|
||||
// Configure I2S if format changed
|
||||
if (ctx->sample_rate != info.hz || ctx->channels != info.channels) {
|
||||
struct I2sConfig config = {
|
||||
.communication_format = I2S_FORMAT_STAND_I2S,
|
||||
.sample_rate = (uint32_t)info.hz,
|
||||
.bits_per_sample = 16,
|
||||
.channel_left = 0,
|
||||
.channel_right = (info.channels == 2) ? 1 : I2S_CHANNEL_NONE
|
||||
};
|
||||
|
||||
device_lock(ctx->i2s_dev);
|
||||
error_t err = i2s_controller_set_config(ctx->i2s_dev, &config);
|
||||
device_unlock(ctx->i2s_dev);
|
||||
|
||||
if (err != ERROR_NONE) {
|
||||
ESP_LOGE(TAG, "Failed to set config: %d", err);
|
||||
break;
|
||||
}
|
||||
ctx->sample_rate = info.hz;
|
||||
ctx->channels = info.channels;
|
||||
ESP_LOGI(TAG, "I2S configured: %d Hz, %d channels", info.hz, info.channels);
|
||||
}
|
||||
|
||||
// Adjust volume
|
||||
int vol = ctx->volume;
|
||||
int16_t* samples_ptr = (int16_t*)ctx->pcm_buf;
|
||||
size_t sample_count = (size_t)samples * info.channels;
|
||||
for (size_t i = 0; i < sample_count; ++i) {
|
||||
int32_t scaled = (int32_t)samples_ptr[i] * vol / 100;
|
||||
samples_ptr[i] = (int16_t)scaled;
|
||||
}
|
||||
|
||||
// Play audio to I2S
|
||||
size_t offset = 0;
|
||||
size_t data_size = sample_count * sizeof(int16_t);
|
||||
bool write_err = false;
|
||||
while (offset < data_size && ctx->state == STATE_PLAYING) {
|
||||
size_t written = 0;
|
||||
error_t err = i2s_controller_write(ctx->i2s_dev, (uint8_t*)ctx->pcm_buf + offset, data_size - offset, &written, pdMS_TO_TICKS(250));
|
||||
if (err != ERROR_NONE || written == 0) {
|
||||
ESP_LOGE(TAG, "I2S write failed: %d", err);
|
||||
write_err = true;
|
||||
break;
|
||||
}
|
||||
offset += written;
|
||||
}
|
||||
if (write_err) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress UI
|
||||
if (ctx->file_size > 0) {
|
||||
int pct = (int)((ctx->bytes_read_total - ctx->buffered_bytes) * 100 / ctx->file_size);
|
||||
if (pct < 0) pct = 0;
|
||||
if (pct > 100) pct = 100;
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF);
|
||||
tt_lvgl_unlock();
|
||||
}
|
||||
|
||||
taskYIELD();
|
||||
}
|
||||
|
||||
fclose(ctx->file);
|
||||
ctx->file = NULL;
|
||||
|
||||
ESP_LOGI(TAG, "Playback task finished");
|
||||
|
||||
tt_lvgl_lock(portMAX_DELAY);
|
||||
ctx->state = STATE_IDLE;
|
||||
update_ui(ctx);
|
||||
tt_lvgl_unlock();
|
||||
|
||||
ctx->playback_task_handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ─── Callbacks ─── */
|
||||
static void on_play_pause_click(lv_event_t* e) {
|
||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||
if (ctx->filepath[0] == '\0') return;
|
||||
|
||||
if (ctx->state == STATE_IDLE) {
|
||||
ctx->state = STATE_PLAYING;
|
||||
update_ui(ctx);
|
||||
xTaskCreate(mp3_playback_task, "mp3_play", 4096, ctx, 5, &ctx->playback_task_handle);
|
||||
} else if (ctx->state == STATE_PLAYING) {
|
||||
ctx->state = STATE_PAUSED;
|
||||
update_ui(ctx);
|
||||
} else if (ctx->state == STATE_PAUSED) {
|
||||
ctx->state = STATE_PLAYING;
|
||||
update_ui(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_stop_click(lv_event_t* e) {
|
||||
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||
if (ctx->state == STATE_IDLE) return;
|
||||
|
||||
ctx->state = STATE_IDLE;
|
||||
update_ui(ctx);
|
||||
}
|
||||
|
||||
/* ─── App Lifecycle ─── */
|
||||
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
memset(&g_ctx, 0, sizeof(g_ctx));
|
||||
g_ctx.volume = 80;
|
||||
|
||||
// Allocate memory buffers
|
||||
g_ctx.input_buf = (uint8_t*)malloc(MP3_INPUT_BUFFER_SIZE);
|
||||
g_ctx.pcm_buf = (mp3d_sample_t*)malloc(MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t));
|
||||
|
||||
// Find device
|
||||
g_ctx.i2s_dev = device_find_by_name("i2s0");
|
||||
if (!g_ctx.i2s_dev) {
|
||||
ESP_LOGE(TAG, "I2S device 'i2s0' not found!");
|
||||
}
|
||||
|
||||
// Parse launch parameters
|
||||
BundleHandle bundle = tt_app_get_parameters(app);
|
||||
if (bundle) {
|
||||
char file_param[256] = {0};
|
||||
if (tt_bundle_opt_string(bundle, "file", file_param, sizeof(file_param))) {
|
||||
// Safely resolve SD card path
|
||||
if (strncmp(file_param, "/sdcard", 7) == 0) {
|
||||
strncpy(g_ctx.filepath, file_param, sizeof(g_ctx.filepath) - 1);
|
||||
} else {
|
||||
if (file_param[0] == '/') {
|
||||
snprintf(g_ctx.filepath, sizeof(g_ctx.filepath), "/sdcard%s", file_param);
|
||||
} else {
|
||||
snprintf(g_ctx.filepath, sizeof(g_ctx.filepath), "/sdcard/%s", file_param);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── UI Layout ───
|
||||
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
// Player Card (rounded box)
|
||||
lv_obj_t* card = lv_obj_create(parent);
|
||||
lv_obj_set_size(card, lv_pct(90), lv_pct(70));
|
||||
lv_obj_align(card, LV_ALIGN_CENTER, 0, 15);
|
||||
lv_obj_set_style_radius(card, 15, 0);
|
||||
lv_obj_set_style_bg_color(card, lv_color_hex(0x1E1E2E), 0);
|
||||
lv_obj_set_style_border_color(card, lv_color_hex(0x313244), 0);
|
||||
lv_obj_set_style_border_width(card, 2, 0);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(card, 15, 0);
|
||||
lv_obj_set_style_pad_gap(card, 15, 0);
|
||||
|
||||
// Audio Icon
|
||||
lv_obj_t* icon = lv_label_create(card);
|
||||
lv_label_set_text(icon, LV_SYMBOL_AUDIO);
|
||||
lv_obj_set_style_text_color(icon, lv_color_hex(0x89B4FA), 0);
|
||||
|
||||
// Track Title
|
||||
g_ctx.lbl_title = lv_label_create(card);
|
||||
lv_obj_set_width(g_ctx.lbl_title, lv_pct(95));
|
||||
lv_obj_set_style_text_align(g_ctx.lbl_title, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(g_ctx.lbl_title, lv_color_hex(0xCDD6F4), 0);
|
||||
if (g_ctx.filepath[0] != '\0') {
|
||||
const char* last_slash = strrchr(g_ctx.filepath, '/');
|
||||
const char* filename = last_slash ? last_slash + 1 : g_ctx.filepath;
|
||||
lv_label_set_text(g_ctx.lbl_title, filename);
|
||||
lv_label_set_long_mode(g_ctx.lbl_title, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||
} else {
|
||||
lv_label_set_text(g_ctx.lbl_title, "No File Parameter");
|
||||
}
|
||||
|
||||
// Playback Status
|
||||
g_ctx.lbl_status = lv_label_create(card);
|
||||
lv_obj_set_style_text_color(g_ctx.lbl_status, lv_color_hex(0xA6ADC8), 0);
|
||||
|
||||
// Progress Bar
|
||||
g_ctx.bar_progress = lv_bar_create(card);
|
||||
lv_obj_set_size(g_ctx.bar_progress, lv_pct(85), 8);
|
||||
lv_bar_set_range(g_ctx.bar_progress, 0, 100);
|
||||
lv_bar_set_value(g_ctx.bar_progress, 0, LV_ANIM_OFF);
|
||||
lv_obj_set_style_bg_color(g_ctx.bar_progress, lv_color_hex(0x45475A), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(g_ctx.bar_progress, lv_color_hex(0x89B4FA), LV_PART_INDICATOR);
|
||||
|
||||
// Controls Container
|
||||
lv_obj_t* ctrl_box = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(ctrl_box);
|
||||
lv_obj_set_size(ctrl_box, lv_pct(90), 45);
|
||||
lv_obj_set_flex_flow(ctrl_box, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(ctrl_box, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
// Play/Pause Button
|
||||
g_ctx.btn_play_pause = lv_btn_create(ctrl_box);
|
||||
lv_obj_set_size(g_ctx.btn_play_pause, 100, 36);
|
||||
lv_obj_set_style_radius(g_ctx.btn_play_pause, 18, 0);
|
||||
lv_obj_set_style_bg_color(g_ctx.btn_play_pause, lv_color_hex(0x89B4FA), 0);
|
||||
lv_obj_set_style_text_color(g_ctx.btn_play_pause, lv_color_hex(0x11111B), 0);
|
||||
lv_obj_t* lbl_play = lv_label_create(g_ctx.btn_play_pause);
|
||||
lv_label_set_text(lbl_play, LV_SYMBOL_PLAY " Play");
|
||||
lv_obj_center(lbl_play);
|
||||
lv_obj_add_event_cb(g_ctx.btn_play_pause, on_play_pause_click, LV_EVENT_CLICKED, &g_ctx);
|
||||
|
||||
// Stop Button
|
||||
g_ctx.btn_stop = lv_btn_create(ctrl_box);
|
||||
lv_obj_set_size(g_ctx.btn_stop, 100, 36);
|
||||
lv_obj_set_style_radius(g_ctx.btn_stop, 18, 0);
|
||||
lv_obj_set_style_bg_color(g_ctx.btn_stop, lv_color_hex(0xF38BA8), 0);
|
||||
lv_obj_set_style_text_color(g_ctx.btn_stop, lv_color_hex(0x11111B), 0);
|
||||
lv_obj_t* lbl_stop = lv_label_create(g_ctx.btn_stop);
|
||||
lv_label_set_text(lbl_stop, LV_SYMBOL_STOP " Stop");
|
||||
lv_obj_center(lbl_stop);
|
||||
lv_obj_add_event_cb(g_ctx.btn_stop, on_stop_click, LV_EVENT_CLICKED, &g_ctx);
|
||||
|
||||
if (!g_ctx.i2s_dev) {
|
||||
lv_label_set_text(g_ctx.lbl_status, "Error: I2S Not Found");
|
||||
lv_obj_add_state(g_ctx.btn_play_pause, LV_STATE_DISABLED);
|
||||
lv_obj_add_state(g_ctx.btn_stop, LV_STATE_DISABLED);
|
||||
} else if (!g_ctx.input_buf || !g_ctx.pcm_buf) {
|
||||
lv_label_set_text(g_ctx.lbl_status, "Error: Out of Memory");
|
||||
lv_obj_add_state(g_ctx.btn_play_pause, LV_STATE_DISABLED);
|
||||
lv_obj_add_state(g_ctx.btn_stop, LV_STATE_DISABLED);
|
||||
} else {
|
||||
g_ctx.state = STATE_IDLE;
|
||||
update_ui(&g_ctx);
|
||||
|
||||
// Auto-play if a file parameter was provided
|
||||
if (g_ctx.filepath[0] != '\0') {
|
||||
g_ctx.state = STATE_PLAYING;
|
||||
update_ui(&g_ctx);
|
||||
xTaskCreate(mp3_playback_task, "mp3_play", 4096, &g_ctx, 5, &g_ctx.playback_task_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void onHideApp(AppHandle app, void* data) {
|
||||
if (g_ctx.state != STATE_IDLE) {
|
||||
g_ctx.state = STATE_IDLE; // Signal task to stop
|
||||
}
|
||||
|
||||
// Wait for the task to finish self-deletion to prevent resource leaks
|
||||
while (g_ctx.playback_task_handle != NULL) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
if (g_ctx.input_buf) {
|
||||
free(g_ctx.input_buf);
|
||||
g_ctx.input_buf = NULL;
|
||||
}
|
||||
if (g_ctx.pcm_buf) {
|
||||
free(g_ctx.pcm_buf);
|
||||
g_ctx.pcm_buf = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
tt_app_register((AppRegistration) {
|
||||
.onShow = onShowApp,
|
||||
.onHide = onHideApp
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32s3
|
||||
[app]
|
||||
id=one.tactility.mp3player
|
||||
versionName=1.0.0
|
||||
versionCode=1
|
||||
name=MP3 Player
|
||||
@@ -1,10 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.mystifydemo
|
||||
versionName=0.3.0
|
||||
versionCode=3
|
||||
name=Mystify Demo
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.mystifydemo
|
||||
app.version.name=0.6.0
|
||||
app.version.code=6
|
||||
app.name=Mystify Demo
|
||||
|
||||
@@ -12,5 +12,5 @@ endif()
|
||||
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||
|
||||
project(Mp3Player)
|
||||
tactility_project(Mp3Player)
|
||||
project(RobotArm)
|
||||
tactility_project(RobotArm)
|
||||
@@ -0,0 +1,6 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
REQUIRES TactilitySDK lwip
|
||||
)
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-format-truncation -Wno-unused-but-set-variable -Wno-unused-function)
|
||||
@@ -0,0 +1,429 @@
|
||||
#include <tt_app.h>
|
||||
#include <tt_lvgl_toolbar.h>
|
||||
#include <tactility/lvgl_fonts.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <lwip/sockets.h>
|
||||
#include <lwip/inet.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
#define TAG "RobotArm"
|
||||
#define ARM_HOST "192.168.68.103"
|
||||
#define ARM_PORT 80
|
||||
#define ARM_PATH "/api/mcp"
|
||||
#define NUM_JOINTS 6
|
||||
#define SEQ_MAX 16
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
const char* cute;
|
||||
uint32_t bg;
|
||||
uint32_t accent;
|
||||
int home, rmin, rmax;
|
||||
int value, last_sent;
|
||||
} Joint;
|
||||
|
||||
static Joint joints[NUM_JOINTS] = {
|
||||
{"base","Base",0xFFD6E0,0xFF8FA8,70,0,180,70,-1},
|
||||
{"shoulder","Shldr",0xD6E8FF,0x8FB6FF,40,0,70,40,-1},
|
||||
{"elbow","Elbow",0xFFE8C5,0xFFB86A,20,0,120,20,-1},
|
||||
{"pitch","Pitch",0xD5F0D5,0x88D488,90,30,150,90,-1},
|
||||
{"roll","Roll",0xE8D5FF,0xB088FF,90,30,150,90,-1},
|
||||
{"gripper","Grip",0xFFF0B3,0xFFD060,90,0,180,90,-1},
|
||||
};
|
||||
|
||||
typedef struct { int v[NUM_JOINTS]; } Frame;
|
||||
static Frame seq[SEQ_MAX];
|
||||
static int seq_len = 0, seq_idx = -1;
|
||||
static bool seq_playing = false;
|
||||
static int seq_play_pos = 0;
|
||||
|
||||
typedef struct {
|
||||
AppHandle app;
|
||||
lv_obj_t* status;
|
||||
lv_obj_t* sliders[6];
|
||||
lv_obj_t* vals[6];
|
||||
lv_obj_t* seq_label;
|
||||
lv_obj_t* play_label;
|
||||
lv_obj_t* blocks[SEQ_MAX];
|
||||
int pend[6];
|
||||
bool has[6];
|
||||
int ticks[6];
|
||||
lv_timer_t* poll;
|
||||
lv_timer_t* seq_timer;
|
||||
} Ctx;
|
||||
static Ctx* g = NULL;
|
||||
|
||||
static uint16_t my_htons(uint16_t v){ return (v<<8)|(v>>8); }
|
||||
|
||||
static int http_post(const char* host,int port,const char* path,const char* body,char* out,size_t olen){
|
||||
int fd=lwip_socket(AF_INET,SOCK_STREAM,0);
|
||||
if(fd<0) return -1;
|
||||
struct sockaddr_in s; memset(&s,0,sizeof(s));
|
||||
s.sin_family=AF_INET; s.sin_port=my_htons(port); s.sin_addr.s_addr=ipaddr_addr(host);
|
||||
struct timeval tv={5,0};
|
||||
lwip_setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv));
|
||||
lwip_setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv));
|
||||
if(lwip_connect(fd,(struct sockaddr*)&s,sizeof(s))<0){close(fd);return -2;}
|
||||
char hdr[256];
|
||||
int bl=strlen(body);
|
||||
int hl=snprintf(hdr,sizeof(hdr),"POST %s HTTP/1.1\r\nHost: %s:%d\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",path,host,port,bl);
|
||||
if(lwip_send(fd,hdr,hl,0)<0){close(fd);return -3;}
|
||||
if(lwip_send(fd,body,bl,0)<0){close(fd);return -3;}
|
||||
int tot=0;
|
||||
while(tot<(int)olen-1){int r=lwip_recv(fd,out+tot,olen-1-tot,0); if(r<=0) break; tot+=r;}
|
||||
out[tot]='\0'; close(fd);
|
||||
char* bp=strstr(out,"\r\n\r\n"); if(bp){bp+=4; memmove(out,bp,strlen(bp)+1);}
|
||||
return tot>0?0:-4;
|
||||
}
|
||||
|
||||
static bool jget(const char* js,const char* key,int* out){
|
||||
const char* p=strstr(js,key);
|
||||
if(!p) return false;
|
||||
p+=strlen(key);
|
||||
while(*p && *p!=':'){
|
||||
p++;
|
||||
if(!*p) return false;
|
||||
}
|
||||
p++;
|
||||
while(*p && (*p==' '||*p=='\t'||*p=='"'||*p=='\\')) p++;
|
||||
int sign=1;
|
||||
if(*p=='-'){sign=-1;p++;}
|
||||
float v=0,frac=0.1f;
|
||||
bool dot=false,got=false;
|
||||
while(*p){
|
||||
if(*p>='0'&&*p<='9'){got=true; if(!dot) v=v*10+(*p-'0'); else {v+=(*p-'0')*frac; frac*=0.1f;}}
|
||||
else if(*p=='.'&&!dot) dot=true;
|
||||
else break;
|
||||
p++;
|
||||
}
|
||||
if(!got) return false;
|
||||
*out=(int)(v*sign+0.5f);
|
||||
return true;
|
||||
}
|
||||
static bool parse_state(const char* r,int* b,int* s,int* e,int* p,int* ro,int* gr){
|
||||
int v; bool ok=true;
|
||||
if(jget(r,"base",&v)) *b=v; else ok=false;
|
||||
if(jget(r,"shoulder",&v)) *s=v; else ok=false;
|
||||
if(jget(r,"elbow",&v)) *e=v; else ok=false;
|
||||
if(jget(r,"pitch",&v)) *p=v; else ok=false;
|
||||
if(jget(r,"roll",&v)) *ro=v; else ok=false;
|
||||
if(jget(r,"gripper",&v)) *gr=v; else ok=false;
|
||||
return ok;
|
||||
}
|
||||
static bool rpc_get(int* b,int* s,int* e,int* p,int* ro,int* gr){
|
||||
const char* body="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"get_arm_state\",\"arguments\":{}}}";
|
||||
char resp[2048]; memset(resp,0,sizeof(resp));
|
||||
if(http_post(ARM_HOST,ARM_PORT,ARM_PATH,body,resp,sizeof(resp))!=0) return false;
|
||||
return parse_state(resp,b,s,e,p,ro,gr);
|
||||
}
|
||||
static bool rpc_move(const char* j,int a){
|
||||
char body[300]; snprintf(body,sizeof(body),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"move_joint\",\"arguments\":{\"joint\":\"%s\",\"angle\":%d,\"duration\":0.5}}}",
|
||||
j,a);
|
||||
char r[1024]; memset(r,0,sizeof(r));
|
||||
return http_post(ARM_HOST,ARM_PORT,ARM_PATH,body,r,sizeof(r))==0;
|
||||
}
|
||||
static bool rpc_move_all(int b,int s,int e,int p,int ro,int gr,float dur){
|
||||
char body[420]; snprintf(body,sizeof(body),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"move_all_joints\",\"arguments\":{\"base\":%d,\"shoulder\":%d,\"elbow\":%d,\"pitch\":%d,\"roll\":%d,\"gripper\":%d,\"duration\":%.1f}}}",
|
||||
b,s,e,p,ro,gr,dur);
|
||||
char r[1024]; memset(r,0,sizeof(r));
|
||||
int rc=http_post(ARM_HOST,ARM_PORT,ARM_PATH,body,r,sizeof(r));
|
||||
ESP_LOGI(TAG,"move_all %d %d %d rc=%d",b,s,e,rc);
|
||||
return rc==0;
|
||||
}
|
||||
static bool rpc_home(void){
|
||||
const char* b="{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"home_arm\",\"arguments\":{\"duration\":1.0}}}";
|
||||
char r[512]; memset(r,0,sizeof(r)); return http_post(ARM_HOST,ARM_PORT,ARM_PATH,b,r,sizeof(r))==0;
|
||||
}
|
||||
|
||||
static void set_status(const char* t){ if(g&&g->status) lv_label_set_text(g->status,t); }
|
||||
static void apply_ui(void){
|
||||
if(!g) return;
|
||||
for(int i=0;i<NUM_JOINTS;i++){
|
||||
if(g->sliders[i]) lv_slider_set_value(g->sliders[i],joints[i].value,LV_ANIM_OFF);
|
||||
if(g->vals[i]){char buf[8]; snprintf(buf,sizeof(buf),"%d",joints[i].value); lv_label_set_text(g->vals[i],buf);}
|
||||
}
|
||||
}
|
||||
static void refresh_arm(void){
|
||||
set_status("Reading .103...");
|
||||
int b,s,e,p,ro,gr;
|
||||
if(rpc_get(&b,&s,&e,&p,&ro,&gr)){
|
||||
joints[0].value=b; joints[1].value=s; joints[2].value=e;
|
||||
joints[3].value=p; joints[4].value=ro; joints[5].value=gr;
|
||||
for(int i=0;i<NUM_JOINTS;i++){joints[i].last_sent=joints[i].value; if(g){g->pend[i]=joints[i].value; g->has[i]=false; g->ticks[i]=0;}}
|
||||
apply_ui();
|
||||
char buf[32]; snprintf(buf,sizeof(buf),"B%d S%d E%d",b,s,e); set_status(buf);
|
||||
} else set_status("No .103");
|
||||
}
|
||||
static void del_ref_cb(lv_timer_t* t){ lv_timer_delete(t); refresh_arm(); }
|
||||
static void init_cb(lv_timer_t* t){ lv_timer_delete(t); refresh_arm(); }
|
||||
static void poll_cb(lv_timer_t* t){
|
||||
(void)t; if(!g) return;
|
||||
for(int i=0;i<NUM_JOINTS;i++){
|
||||
if(!g->has[i]) continue;
|
||||
g->ticks[i]++; if(g->ticks[i]<3) continue;
|
||||
g->has[i]=false; g->ticks[i]=0;
|
||||
int tgt=g->pend[i]; if(joints[i].last_sent==tgt) continue;
|
||||
joints[i].last_sent=tgt; joints[i].value=tgt;
|
||||
char buf[20]; snprintf(buf,sizeof(buf),"%s %d",joints[i].cute,tgt); set_status(buf);
|
||||
bool ok=rpc_move(joints[i].name,tgt);
|
||||
snprintf(buf,sizeof(buf),"%s %d %s",joints[i].cute,tgt,ok?"ok":"fail"); set_status(buf);
|
||||
break;
|
||||
}
|
||||
}
|
||||
static void slider_cb(lv_event_t* e){
|
||||
int idx=(int)(intptr_t)lv_event_get_user_data(e);
|
||||
int v=(int)lv_slider_get_value(lv_event_get_target(e));
|
||||
joints[idx].value=v;
|
||||
if(g){
|
||||
if(g->vals[idx]){char b[8]; snprintf(b,sizeof(b),"%d",v); lv_label_set_text(g->vals[idx],b);}
|
||||
g->pend[idx]=v; g->has[idx]=true; g->ticks[idx]=0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── sequencer ── */
|
||||
static void update_seq_ui(void){
|
||||
if(!g) return;
|
||||
if(g->seq_label){
|
||||
if(seq_len==0) lv_label_set_text(g->seq_label,"empty");
|
||||
else {char b[16]; snprintf(b,sizeof(b),"%d/%d", (seq_idx>=0?seq_idx+1:seq_len), seq_len); lv_label_set_text(g->seq_label,b);}
|
||||
}
|
||||
for(int i=0;i<SEQ_MAX;i++){
|
||||
if(!g->blocks[i]) continue;
|
||||
if(i<seq_len){
|
||||
lv_obj_set_style_bg_opa(g->blocks[i],LV_OPA_COVER,0);
|
||||
if(i==seq_idx){
|
||||
lv_obj_set_style_border_width(g->blocks[i],3,0);
|
||||
lv_obj_set_style_border_color(g->blocks[i],lv_color_hex(0xFFFFFF),0);
|
||||
} else {
|
||||
lv_obj_set_style_border_width(g->blocks[i],2,0);
|
||||
lv_obj_set_style_border_color(g->blocks[i],lv_color_hex(0x3A3A3A),0);
|
||||
}
|
||||
} else {
|
||||
lv_obj_set_style_bg_opa(g->blocks[i],LV_OPA_30,0);
|
||||
lv_obj_set_style_border_width(g->blocks[i],0,0);
|
||||
}
|
||||
}
|
||||
}
|
||||
static void load_frame(int fidx){
|
||||
if(fidx<0||fidx>=seq_len) return;
|
||||
for(int i=0;i<NUM_JOINTS;i++){joints[i].value=seq[fidx].v[i]; joints[i].last_sent=joints[i].value; if(g){g->pend[i]=joints[i].value; g->has[i]=false;}}
|
||||
apply_ui(); seq_idx=fidx; update_seq_ui();
|
||||
char b[20]; snprintf(b,sizeof(b),"Frame %d",fidx+1); set_status(b);
|
||||
}
|
||||
static void seq_add_cb(void){ if(seq_len>=SEQ_MAX){set_status("Seq full"); return;} for(int i=0;i<NUM_JOINTS;i++) seq[seq_len].v[i]=joints[i].value; seq_len++; seq_idx=seq_len-1; update_seq_ui(); char b[16]; snprintf(b,sizeof(b),"+ %d",seq_len); set_status(b); }
|
||||
static void seq_rem_cb(void){
|
||||
if(seq_len==0||seq_idx<0){set_status("Nothing"); return;}
|
||||
int rem=seq_idx;
|
||||
for(int f=rem;f<seq_len-1;f++) seq[f]=seq[f+1];
|
||||
seq_len--; if(seq_len==0){seq_idx=-1; update_seq_ui(); set_status("- empty"); return;}
|
||||
if(seq_idx>=seq_len) seq_idx=seq_len-1;
|
||||
load_frame(seq_idx);
|
||||
}
|
||||
static void seq_prev_cb(void){ if(seq_len==0) return; int n=(seq_idx<=0)?seq_len-1:seq_idx-1; load_frame(n); }
|
||||
static void seq_next_cb(void){ if(seq_len==0) return; int n=(seq_idx>=seq_len-1)?0:seq_idx+1; load_frame(n); }
|
||||
|
||||
static void seq_timer_cb(lv_timer_t* t){
|
||||
(void)t; if(!seq_playing||seq_len==0) return;
|
||||
int f=seq_play_pos%seq_len;
|
||||
int* v=seq[f].v;
|
||||
if(!rpc_move_all(v[0],v[1],v[2],v[3],v[4],v[5],0.8f)){
|
||||
set_status("Seq fail"); seq_playing=false;
|
||||
if(g&&g->play_label) lv_label_set_text(g->play_label,"Play");
|
||||
return;
|
||||
}
|
||||
for(int i=0;i<NUM_JOINTS;i++){joints[i].value=v[i]; joints[i].last_sent=v[i]; if(g){g->pend[i]=v[i]; g->has[i]=false;}}
|
||||
apply_ui(); seq_idx=f; update_seq_ui();
|
||||
char b[20]; snprintf(b,sizeof(b),"Play %d/%d",f+1,seq_len); set_status(b);
|
||||
seq_play_pos++; if(seq_play_pos>=seq_len) seq_play_pos=0;
|
||||
}
|
||||
static void seq_play_cb(lv_event_t* e){
|
||||
(void)e; if(seq_len==0){set_status("No frames"); return;}
|
||||
seq_playing=!seq_playing;
|
||||
if(g&&g->play_label) lv_label_set_text(g->play_label, seq_playing?"Pause":"Play");
|
||||
if(seq_playing){seq_play_pos=(seq_idx>=0?seq_idx:0); set_status("Playing");} else set_status("Paused");
|
||||
}
|
||||
static void seq_add_ev(lv_event_t* e){(void)e; seq_add_cb();}
|
||||
static void seq_rem_ev(lv_event_t* e){(void)e; seq_rem_cb();}
|
||||
static void seq_prev_ev(lv_event_t* e){(void)e; seq_prev_cb();}
|
||||
static void seq_next_ev(lv_event_t* e){(void)e; seq_next_cb();}
|
||||
static void block_click_cb(lv_event_t* e){int idx=(int)(intptr_t)lv_event_get_user_data(e); if(idx<0||idx>=seq_len) return; load_frame(idx);}
|
||||
|
||||
static void home_cb(lv_event_t* e){(void)e; set_status("Homing..."); if(rpc_home()){lv_timer_create(del_ref_cb,1200,NULL); set_status("Homed :3");} else set_status("Fail");}
|
||||
static void read_cb(lv_event_t* e){(void)e; refresh_arm();}
|
||||
static void open_cb(lv_event_t* e){(void)e; joints[5].value=0; apply_ui(); rpc_move("gripper",0); set_status("Open");}
|
||||
static void close_cb(lv_event_t* e){(void)e; joints[5].value=180; apply_ui(); rpc_move("gripper",180); set_status("Close");}
|
||||
|
||||
static void onShow(AppHandle app,void* data,lv_obj_t* parent){
|
||||
(void)data;
|
||||
Ctx* c=(Ctx*)calloc(1,sizeof(Ctx)); if(!c) return;
|
||||
c->app=app; g=c;
|
||||
for(int i=0;i<NUM_JOINTS;i++){joints[i].last_sent=-1; c->pend[i]=joints[i].value; c->has[i]=false;}
|
||||
|
||||
lv_obj_t* tb=tt_lvgl_toolbar_create_for_app(parent,app); lv_obj_align(tb,LV_ALIGN_TOP_MID,0,0);
|
||||
|
||||
lv_obj_t* root=lv_obj_create(parent);
|
||||
lv_obj_set_size(root,LV_PCT(100),LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(root,2,0); lv_obj_set_style_pad_top(root,34,0);
|
||||
lv_obj_set_style_border_width(root,0,0);
|
||||
lv_obj_set_style_bg_color(root,lv_color_hex(0xFFF8F0),0);
|
||||
lv_obj_set_style_bg_opa(root,LV_OPA_COVER,0);
|
||||
lv_obj_set_scroll_dir(root, LV_DIR_VER);
|
||||
|
||||
lv_obj_t* hdr=lv_obj_create(root); lv_obj_set_size(hdr,LV_PCT(100),16);
|
||||
lv_obj_set_style_border_width(hdr,0,0); lv_obj_set_style_bg_opa(hdr,LV_OPA_TRANSP,0);
|
||||
lv_obj_set_style_pad_all(hdr,0,0); lv_obj_set_pos(hdr,0,0);
|
||||
lv_obj_clear_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_t* title=lv_label_create(hdr); lv_label_set_text(title,"Robot Arm :3");
|
||||
lv_obj_set_style_text_font(title,lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||
lv_obj_set_style_text_color(title,lv_color_hex(0x3D2B5A),0); lv_obj_set_pos(title,2,0);
|
||||
c->status=lv_label_create(hdr); lv_label_set_text(c->status,"Conn .103...");
|
||||
lv_obj_set_style_text_font(c->status,lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||
lv_obj_set_style_text_color(c->status,lv_color_hex(0xA08090),0); lv_obj_set_pos(c->status,90,0);
|
||||
|
||||
lv_obj_t* brow=lv_obj_create(root); lv_obj_set_size(brow,LV_PCT(100),20);
|
||||
lv_obj_set_style_border_width(brow,0,0); lv_obj_set_style_bg_opa(brow,LV_OPA_TRANSP,0);
|
||||
lv_obj_set_style_pad_all(brow,0,0); lv_obj_set_pos(brow,0,16);
|
||||
lv_obj_clear_flag(brow, LV_OBJ_FLAG_SCROLLABLE);
|
||||
struct { const char* t; uint32_t col; lv_event_cb_t cb; } btns[]={
|
||||
{"Home",0xFFD6E0,home_cb},{"Read",0xD6E8FF,read_cb},{"Open",0xD5F0D5,open_cb},{"Close",0xFFE8C5,close_cb},};
|
||||
for(int i=0;i<4;i++){
|
||||
lv_obj_t* b=lv_btn_create(brow); lv_obj_set_size(b,56,18);
|
||||
lv_obj_set_style_bg_color(b,lv_color_hex(btns[i].col),0); lv_obj_set_style_radius(b,8,0);
|
||||
lv_obj_set_pos(b,i*62+2,0);
|
||||
lv_obj_t* l=lv_label_create(b); lv_label_set_text(l,btns[i].t);
|
||||
lv_obj_set_style_text_font(l,lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||
lv_obj_set_style_text_color(l,lv_color_hex(0x4A356A),0); lv_obj_center(l);
|
||||
lv_obj_add_event_cb(b,btns[i].cb,LV_EVENT_CLICKED,NULL);
|
||||
}
|
||||
|
||||
/* ── large nice vertical sliders: 3 cols, 2x height 22x72 per request ── */
|
||||
lv_obj_t* grid=lv_obj_create(root);
|
||||
lv_obj_set_size(grid,LV_PCT(100),196);
|
||||
lv_obj_set_style_border_width(grid,0,0); lv_obj_set_style_bg_opa(grid,LV_OPA_TRANSP,0);
|
||||
lv_obj_set_style_pad_all(grid,2,0); lv_obj_set_pos(grid,0,36);
|
||||
lv_obj_clear_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
for(int i=0;i<NUM_JOINTS;i++){
|
||||
int col=i%3, row=i/3;
|
||||
lv_obj_t* card=lv_obj_create(grid);
|
||||
lv_obj_set_size(card,102,96);
|
||||
lv_obj_set_pos(card,col*104+2,row*98);
|
||||
lv_obj_set_style_bg_color(card,lv_color_hex(joints[i].bg),0);
|
||||
lv_obj_set_style_bg_opa(card,LV_OPA_90,0);
|
||||
lv_obj_set_style_radius(card,12,0);
|
||||
lv_obj_set_style_border_width(card,0,0);
|
||||
lv_obj_set_style_pad_all(card,3,0);
|
||||
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lv_obj_t* name=lv_label_create(card);
|
||||
lv_label_set_text(name,joints[i].cute);
|
||||
lv_obj_set_style_text_font(name,lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||
lv_obj_set_style_text_color(name,lv_color_hex(0x4A356A),0); lv_obj_set_pos(name,2,0);
|
||||
|
||||
c->vals[i]=lv_label_create(card);
|
||||
char vb[8]; snprintf(vb,sizeof(vb),"%d",joints[i].value);
|
||||
lv_label_set_text(c->vals[i],vb);
|
||||
lv_obj_set_style_text_font(c->vals[i],lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||
lv_obj_set_style_text_color(c->vals[i],lv_color_hex(0xC04060),0); lv_obj_set_pos(c->vals[i],40,0);
|
||||
|
||||
lv_obj_t* rlbl=lv_label_create(card);
|
||||
char rb[12]; snprintf(rb,sizeof(rb),"%d-%d",joints[i].rmin,joints[i].rmax);
|
||||
lv_label_set_text(rlbl,rb);
|
||||
lv_obj_set_style_text_font(rlbl,lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||
lv_obj_set_style_text_color(rlbl,lv_color_hex(0xA090A0),0); lv_obj_set_pos(rlbl,58,0);
|
||||
|
||||
lv_obj_t* sl=lv_slider_create(card);
|
||||
c->sliders[i]=sl;
|
||||
lv_obj_set_size(sl,22,72);
|
||||
lv_obj_set_pos(sl,40,20);
|
||||
lv_slider_set_range(sl,joints[i].rmin,joints[i].rmax);
|
||||
lv_slider_set_value(sl,joints[i].value,LV_ANIM_OFF);
|
||||
lv_obj_set_style_bg_color(sl,lv_color_hex(0xFFFFFF),LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(sl,LV_OPA_80,LV_PART_MAIN);
|
||||
lv_obj_set_style_radius(sl,10,LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(sl,lv_color_hex(joints[i].accent),LV_PART_INDICATOR);
|
||||
lv_obj_set_style_radius(sl,10,LV_PART_INDICATOR);
|
||||
lv_obj_set_style_bg_color(sl,lv_color_hex(0xFFFFFF),LV_PART_KNOB);
|
||||
lv_obj_set_style_border_color(sl,lv_color_hex(joints[i].accent),LV_PART_KNOB);
|
||||
lv_obj_set_style_border_width(sl,2,LV_PART_KNOB);
|
||||
lv_obj_set_style_radius(sl,12,LV_PART_KNOB);
|
||||
lv_obj_set_style_pad_all(sl,3,LV_PART_KNOB);
|
||||
lv_obj_add_event_cb(sl,slider_cb,LV_EVENT_VALUE_CHANGED,(void*)(intptr_t)i);
|
||||
}
|
||||
|
||||
/* ── sequencer at bottom, NOT fixed — scrollable with content ──
|
||||
visual: colored blocks, no info, current highlighted, larger buttons */
|
||||
lv_obj_t* seqbar=lv_obj_create(root);
|
||||
lv_obj_set_size(seqbar,316,96);
|
||||
lv_obj_set_pos(seqbar,2,232);
|
||||
lv_obj_clear_flag(seqbar, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_style_bg_color(seqbar,lv_color_hex(0xF0E8FF),0);
|
||||
lv_obj_set_style_bg_opa(seqbar,LV_OPA_90,0);
|
||||
lv_obj_set_style_radius(seqbar,12,0);
|
||||
lv_obj_set_style_border_width(seqbar,1,0);
|
||||
lv_obj_set_style_border_color(seqbar,lv_color_hex(0xD0C0E0),0);
|
||||
lv_obj_set_style_pad_all(seqbar,4,0);
|
||||
|
||||
lv_obj_t* seq_t=lv_label_create(seqbar); lv_label_set_text(seq_t,"Seq");
|
||||
lv_obj_set_style_text_font(seq_t,lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||
lv_obj_set_style_text_color(seq_t,lv_color_hex(0x3D2B5A),0); lv_obj_set_pos(seq_t,2,0);
|
||||
|
||||
c->seq_label=lv_label_create(seqbar); lv_label_set_text(c->seq_label,"empty");
|
||||
lv_obj_set_style_text_font(c->seq_label,lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||
lv_obj_set_style_text_color(c->seq_label,lv_color_hex(0x6A5A7A),0); lv_obj_set_pos(c->seq_label,30,0);
|
||||
|
||||
struct { const char* t; uint32_t col; lv_event_cb_t cb; int play; } sbtns[]={
|
||||
{"-",0xFFB7B7,seq_rem_ev,0},{"<",0xD6E8FF,seq_prev_ev,0},
|
||||
{"Play",0xC5F5C5,seq_play_cb,1},{">",0xD6E8FF,seq_next_ev,0},{"+",0xFFE8A0,seq_add_ev,0},};
|
||||
for(int i=0;i<5;i++){
|
||||
lv_obj_t* b=lv_btn_create(seqbar);
|
||||
lv_obj_set_size(b,(i==2)?56:38,32);
|
||||
lv_obj_set_style_bg_color(b,lv_color_hex(sbtns[i].col),0);
|
||||
lv_obj_set_style_radius(b,8,0);
|
||||
int xs[5]={64,110,158,220,260};
|
||||
lv_obj_set_pos(b,xs[i],0);
|
||||
lv_obj_t* l=lv_label_create(b); if(sbtns[i].play) c->play_label=l;
|
||||
lv_label_set_text(l,sbtns[i].t);
|
||||
lv_obj_set_style_text_font(l,lvgl_get_text_font(FONT_SIZE_SMALL),0);
|
||||
lv_obj_set_style_text_color(l,lv_color_hex(0x2A2A5A),0); lv_obj_center(l);
|
||||
lv_obj_add_event_cb(b,sbtns[i].cb,LV_EVENT_CLICKED,NULL);
|
||||
}
|
||||
|
||||
uint32_t blk_cols[16]={
|
||||
0xFF8FA8,0x8FB6FF,0xFFB86A,0x88D488,0xB088FF,0xFFD060,0xFF7AA2,0x7AC8FF,
|
||||
0xFDBA74,0x86EFAC,0xA78BFA,0xFDE68A,0xFCA5A5,0x93C5FD,0xBEF264,0xFDA4AF
|
||||
};
|
||||
for(int i=0;i<SEQ_MAX;i++){
|
||||
lv_obj_t* blk=lv_btn_create(seqbar);
|
||||
lv_obj_set_size(blk,22,22);
|
||||
lv_obj_set_pos(blk,(i%8)*24+2,(i/8)*24+38);
|
||||
lv_obj_set_style_bg_color(blk,lv_color_hex(blk_cols[i]),0);
|
||||
lv_obj_set_style_radius(blk,6,0);
|
||||
lv_obj_set_style_border_width(blk,0,0);
|
||||
lv_obj_set_style_bg_opa(blk,LV_OPA_30,0);
|
||||
c->blocks[i]=blk;
|
||||
lv_obj_add_event_cb(blk,block_click_cb,LV_EVENT_CLICKED,(void*)(intptr_t)i);
|
||||
}
|
||||
update_seq_ui();
|
||||
|
||||
c->poll=lv_timer_create(poll_cb,100,NULL);
|
||||
c->seq_timer=lv_timer_create(seq_timer_cb,1300,NULL);
|
||||
lv_timer_create(init_cb,600,NULL);
|
||||
}
|
||||
|
||||
static void onHide(AppHandle app,void* data){
|
||||
(void)app;(void)data;
|
||||
if(g){
|
||||
if(g->poll) lv_timer_delete(g->poll);
|
||||
if(g->seq_timer) lv_timer_delete(g->seq_timer);
|
||||
free(g); g=NULL;
|
||||
}
|
||||
seq_playing=false;
|
||||
}
|
||||
int main(int argc,char* argv[]){(void)argc;(void)argv; tt_app_register((AppRegistration){.onShow=onShow,.onHide=onHide}); return 0;}
|
||||
@@ -0,0 +1,13 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
|
||||
[target]
|
||||
sdk=0.8.0-dev
|
||||
platforms=esp32s3
|
||||
|
||||
[app]
|
||||
id=one.tactility.robotarm
|
||||
versionName=0.1.0
|
||||
versionCode=1
|
||||
name=Robot Arm
|
||||
description=Cute controller for MCP robot arm
|
||||
@@ -1,10 +1,7 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.serialconsole
|
||||
versionName=0.4.0
|
||||
versionCode=4
|
||||
name=Serial Console
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.serialconsole
|
||||
app.version.name=0.7.0
|
||||
app.version.code=7
|
||||
app.name=Serial Console
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.snake
|
||||
versionName=0.5.0
|
||||
versionCode=5
|
||||
name=Snake
|
||||
description=Classic Snake game
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.snake
|
||||
app.version.name=0.8.0
|
||||
app.version.code=8
|
||||
app.name=Snake
|
||||
app.description=Classic Snake game
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.tamatac
|
||||
versionName=0.1.0
|
||||
versionCode=1
|
||||
name=TamaTac
|
||||
description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.tamatac
|
||||
app.version.name=0.4.0
|
||||
app.version.code=4
|
||||
app.name=TamaTac
|
||||
app.description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.todolist
|
||||
versionName=0.2.0
|
||||
versionCode=2
|
||||
name=Todo List
|
||||
description=Simple task list manager
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.todolist
|
||||
app.version.name=0.5.0
|
||||
app.version.code=5
|
||||
app.name=Todo List
|
||||
app.description=Simple task list manager
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.7.0-dev
|
||||
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
[app]
|
||||
id=one.tactility.twoeleven
|
||||
versionName=0.4.0
|
||||
versionCode=4
|
||||
name=2048
|
||||
description=A fun, customizable 2048 sliding tile game for tactility!\nSlide tiles to combine numbers and reach 2048.\nChoose grid sizes: 3x3 (easy), 4x4 (classic), 5x5, or 6x6 (expert).
|
||||
manifest.version=0.2
|
||||
target.sdk=0.8.0-dev
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.twoeleven
|
||||
app.version.name=0.7.0
|
||||
app.version.code=7
|
||||
app.name=2048
|
||||
app.description=A fun, customizable 2048 sliding tile game for tactility!\nSlide tiles to combine numbers and reach 2048.\nChoose grid sizes: 3x3 (easy), 4x4 (classic), 5x5, or 6x6 (expert).
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
import sys
|
||||
import boto3
|
||||
|
||||
SHELL_COLOR_RED = "\033[91m"
|
||||
SHELL_COLOR_ORANGE = "\033[93m"
|
||||
SHELL_COLOR_RESET = "\033[m"
|
||||
|
||||
def print_warning(message):
|
||||
print(f"{SHELL_COLOR_ORANGE}WARNING: {message}{SHELL_COLOR_RESET}")
|
||||
|
||||
def print_error(message):
|
||||
print(f"{SHELL_COLOR_RED}ERROR: {message}{SHELL_COLOR_RESET}")
|
||||
|
||||
def print_help():
|
||||
print("Usage: python upload-app-files.py [path] [sdkVersion] [cloudflareAccountId] [cloudflareTokenName] [cloudflareTokenValue]")
|
||||
print("")
|
||||
print("Options:")
|
||||
print(" --index-only Upload only apps.json")
|
||||
|
||||
def exit_with_error(message):
|
||||
print_error(message)
|
||||
sys.exit(1)
|
||||
|
||||
def main(path: str, sdk_version: str, cloudflare_account_id, cloudflare_token_name: str, cloudflare_token_value: str, index_only: bool):
|
||||
if not os.path.exists(path):
|
||||
exit_with_error(f"Path not found: {path}")
|
||||
s3 = boto3.client(
|
||||
service_name="s3",
|
||||
endpoint_url=f"https://{cloudflare_account_id}.r2.cloudflarestorage.com",
|
||||
aws_access_key_id=cloudflare_token_name,
|
||||
aws_secret_access_key=cloudflare_token_value,
|
||||
region_name="auto"
|
||||
)
|
||||
files_to_upload = os.listdir(path)
|
||||
if index_only:
|
||||
files_to_upload = [f for f in files_to_upload if f == 'apps.json']
|
||||
else:
|
||||
# Ensure apps.json is uploaded last so it never references files that
|
||||
# haven't finished uploading yet.
|
||||
files_to_upload.sort(key=lambda f: f == 'apps.json')
|
||||
counter = 1
|
||||
total = len(files_to_upload)
|
||||
for file_name in files_to_upload:
|
||||
object_path = f"apps/{sdk_version}/{file_name}"
|
||||
print(f"[{counter}/{total}] Uploading {file_name} to {object_path}")
|
||||
file_path = os.path.join(path, file_name)
|
||||
try:
|
||||
s3.upload_file(file_path, "tactility", object_path)
|
||||
except Exception as e:
|
||||
exit_with_error(f"Failed to upload {file_name}: {str(e)}")
|
||||
counter += 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Tactility CDN Apps Uploader")
|
||||
if "--help" in sys.argv:
|
||||
print_help()
|
||||
sys.exit()
|
||||
# Argument validation
|
||||
if len(sys.argv) < 6:
|
||||
print_help()
|
||||
sys.exit(1)
|
||||
main(
|
||||
path=sys.argv[1],
|
||||
sdk_version=sys.argv[2],
|
||||
cloudflare_account_id=sys.argv[3],
|
||||
cloudflare_token_name=sys.argv[4],
|
||||
cloudflare_token_value=sys.argv[5],
|
||||
index_only="--index-only" in sys.argv
|
||||
)
|
||||
+56
-41
@@ -1,14 +1,23 @@
|
||||
import json
|
||||
import subprocess
|
||||
import tarfile
|
||||
import os
|
||||
import tempfile
|
||||
import configparser
|
||||
import sys
|
||||
from datetime import datetime, UTC
|
||||
|
||||
def read_properties_file(path):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(path)
|
||||
return config
|
||||
properties = {}
|
||||
with open(path, "r") as file:
|
||||
for line in file:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
key, sep, value = line.partition("=")
|
||||
if not sep:
|
||||
continue
|
||||
properties[key.strip()] = value.strip()
|
||||
return properties
|
||||
|
||||
def get_manifest(appPath):
|
||||
"""Extract only the file named 'manifest.properties' from the given tar/tar.gz
|
||||
@@ -62,51 +71,49 @@ def get_manifest(appPath):
|
||||
return None
|
||||
|
||||
def get_versioned_file_name(manifest):
|
||||
app_id = manifest["app"]["id"]
|
||||
version_code = manifest["app"]["versionCode"]
|
||||
app_id = manifest["app.id"]
|
||||
version_code = manifest["app.version.code"]
|
||||
return f"{app_id}-{version_code}.app"
|
||||
|
||||
def get_os_version(manifest):
|
||||
sdk = manifest["target"]["sdk"]
|
||||
sdk = manifest["target.sdk"]
|
||||
# Remove trailing hyphen suffix if present
|
||||
if "-" in sdk:
|
||||
return sdk.rsplit("-", 1)[0].strip()
|
||||
else:
|
||||
return sdk
|
||||
|
||||
def manifest_config_to_flat_json(manifest):
|
||||
"""Convert a ConfigParser manifest into a flat JSON-like dict.
|
||||
def check_and_get_sdk_version(manifest_map):
|
||||
"""Ensure all apps target the same (simplified) SDK version and return it."""
|
||||
versions = {get_os_version(manifest) for manifest in manifest_map.values()}
|
||||
if len(versions) != 1:
|
||||
print(f"ERROR: Apps target multiple SDK versions: {sorted(versions)}. All apps must target the same SDK version.")
|
||||
sys.exit(1)
|
||||
return next(iter(versions))
|
||||
|
||||
Expected sections/keys (case-insensitive for keys):
|
||||
- [app]
|
||||
id -> appId
|
||||
versionName -> appVersionName
|
||||
versionCode -> appVersionCode (int)
|
||||
name -> appName
|
||||
description -> appDescription (optional; default "")
|
||||
- [target]
|
||||
sdk -> targetSdk
|
||||
platforms -> targetPlatforms (comma-separated list)
|
||||
def get_git_commit_hash():
|
||||
return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()
|
||||
|
||||
def manifest_config_to_flat_json(manifest):
|
||||
"""Convert a flat (V2) manifest dict into a flat JSON-like dict.
|
||||
|
||||
Expected keys:
|
||||
app.id -> appId
|
||||
app.version.name -> appVersionName
|
||||
app.version.code -> appVersionCode (int)
|
||||
app.name -> appName
|
||||
app.description -> appDescription (optional; default "")
|
||||
target.sdk -> targetSdk
|
||||
target.platforms -> targetPlatforms (comma-separated list)
|
||||
|
||||
Unknown/missing values fall back to sensible defaults per requirements.
|
||||
"""
|
||||
def get_opt(section, option, default=None):
|
||||
if not manifest.has_section(section):
|
||||
return default
|
||||
# try exact option then lowercase (RawConfigParser lowercases by default)
|
||||
if manifest.has_option(section, option):
|
||||
return manifest.get(section, option)
|
||||
low = option.lower()
|
||||
if manifest.has_option(section, low):
|
||||
return manifest.get(section, low)
|
||||
return default
|
||||
|
||||
# Map values
|
||||
app_id = get_opt("app", "id", "")
|
||||
app_version_name = get_opt("app", "versionName", "")
|
||||
app_version_code_raw = get_opt("app", "versionCode", "0")
|
||||
app_name = get_opt("app", "name", "")
|
||||
app_description = get_opt("app", "description", "") or ""
|
||||
app_id = manifest.get("app.id", "")
|
||||
app_version_name = manifest.get("app.version.name", "")
|
||||
app_version_code_raw = manifest.get("app.version.code", "0")
|
||||
app_name = manifest.get("app.name", "")
|
||||
app_description = manifest.get("app.description", "") or ""
|
||||
|
||||
# Coerce version code to int safely
|
||||
try:
|
||||
@@ -114,8 +121,8 @@ def manifest_config_to_flat_json(manifest):
|
||||
except Exception:
|
||||
app_version_code = 0
|
||||
|
||||
target_sdk = get_opt("target", "sdk", "")
|
||||
platforms_raw = get_opt("target", "platforms", "")
|
||||
target_sdk = manifest.get("target.sdk", "")
|
||||
platforms_raw = manifest.get("target.platforms", "")
|
||||
target_platforms = [p.strip() for p in str(platforms_raw).split(",") if p.strip()] if platforms_raw is not None else []
|
||||
|
||||
filename = get_versioned_file_name(manifest)
|
||||
@@ -142,15 +149,23 @@ if __name__ == "__main__":
|
||||
sys.exit()
|
||||
app_directory = sys.argv[1]
|
||||
manifest_map = {}
|
||||
output_json = {
|
||||
"apps": []
|
||||
}
|
||||
any_manifest = None
|
||||
if os.path.exists(app_directory):
|
||||
for file in os.listdir(app_directory):
|
||||
if file.endswith(".app"):
|
||||
file_path = os.path.join(app_directory, file)
|
||||
manifest_map[file_path] = get_manifest(file_path)
|
||||
# All bundled apps must target the same SDK version; this becomes the CDN path segment
|
||||
sdk_version = check_and_get_sdk_version(manifest_map)
|
||||
with open("sdk_version.txt", "w") as f:
|
||||
f.write(sdk_version)
|
||||
print(f"SDK version: {sdk_version}")
|
||||
output_json = {
|
||||
"sdkVersion": sdk_version,
|
||||
"created": datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
||||
"gitCommit": get_git_commit_hash(),
|
||||
"apps": []
|
||||
}
|
||||
# Rename files and collect manifest data into output json object
|
||||
for file_path in manifest_map.keys():
|
||||
print(f"Processing {file_path}: {manifest_map[file_path]}")
|
||||
@@ -164,4 +179,4 @@ if __name__ == "__main__":
|
||||
any_manifest = manifest
|
||||
# Write JSON
|
||||
output_json_path = os.path.join(app_directory, "apps.json")
|
||||
write_json(output_json_path, output_json)
|
||||
write_json(output_json_path, output_json)
|
||||
|
||||
+23
-36
@@ -1,4 +1,3 @@
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -13,7 +12,7 @@ import tarfile
|
||||
from urllib.parse import urlparse
|
||||
|
||||
ttbuild_path = ".tactility"
|
||||
ttbuild_version = "3.5.1"
|
||||
ttbuild_version = "4.1.0"
|
||||
ttbuild_cdn = "https://cdn.tactilityproject.org"
|
||||
ttbuild_sdk_json_validity = 3600 # seconds
|
||||
ttport = 6666
|
||||
@@ -106,9 +105,17 @@ def get_url(ip, path):
|
||||
return f"http://{ip}:{ttport}{path}"
|
||||
|
||||
def read_properties_file(path):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(path)
|
||||
return config
|
||||
properties = {}
|
||||
with open(path, "r") as file:
|
||||
for line in file:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
key, sep, value = line.partition("=")
|
||||
if not sep:
|
||||
continue
|
||||
properties[key.strip()] = value.strip()
|
||||
return properties
|
||||
|
||||
#endregion Core
|
||||
|
||||
@@ -185,7 +192,7 @@ def fetch_sdkconfig_files(platform_targets):
|
||||
for platform in platform_targets:
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
target_path = os.path.join(ttbuild_path, sdkconfig_filename)
|
||||
if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path):
|
||||
if not download_file(f"{ttbuild_cdn}/sdk/{sdkconfig_filename}", target_path):
|
||||
exit_with_error(f"Failed to download sdkconfig file for {platform}")
|
||||
|
||||
#endregion SDK helpers
|
||||
@@ -231,32 +238,12 @@ def read_manifest():
|
||||
return read_properties_file("manifest.properties")
|
||||
|
||||
def validate_manifest(manifest):
|
||||
# [manifest]
|
||||
if not "manifest" in manifest:
|
||||
exit_with_error("Invalid manifest format: [manifest] not found")
|
||||
if not "version" in manifest["manifest"]:
|
||||
exit_with_error("Invalid manifest format: [manifest] version not found")
|
||||
# [target]
|
||||
if not "target" in manifest:
|
||||
exit_with_error("Invalid manifest format: [target] not found")
|
||||
if not "sdk" in manifest["target"]:
|
||||
exit_with_error("Invalid manifest format: [target] sdk not found")
|
||||
if not "platforms" in manifest["target"]:
|
||||
exit_with_error("Invalid manifest format: [target] platforms not found")
|
||||
# [app]
|
||||
if not "app" in manifest:
|
||||
exit_with_error("Invalid manifest format: [app] not found")
|
||||
if not "id" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] id not found")
|
||||
if not "versionName" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] versionName not found")
|
||||
if not "versionCode" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] versionCode not found")
|
||||
if not "name" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] name not found")
|
||||
for key in ("manifest.version", "target.sdk", "target.platforms", "app.id", "app.version.name", "app.version.code", "app.name"):
|
||||
if key not in manifest:
|
||||
exit_with_error(f"Invalid manifest format: {key} not found")
|
||||
|
||||
def is_valid_manifest_platform(manifest, platform):
|
||||
manifest_platforms = manifest["target"]["platforms"].split(",")
|
||||
manifest_platforms = manifest["target.platforms"].split(",")
|
||||
return platform in manifest_platforms
|
||||
|
||||
def validate_manifest_platform(manifest, platform):
|
||||
@@ -265,7 +252,7 @@ def validate_manifest_platform(manifest, platform):
|
||||
|
||||
def get_manifest_target_platforms(manifest, requested_platform):
|
||||
if requested_platform == "" or requested_platform is None:
|
||||
return manifest["target"]["platforms"].split(",")
|
||||
return manifest["target.platforms"].split(",")
|
||||
else:
|
||||
validate_manifest_platform(manifest, requested_platform)
|
||||
return [requested_platform]
|
||||
@@ -512,7 +499,7 @@ def build_action(manifest, platform_arg, skip_build):
|
||||
if use_local_sdk:
|
||||
global local_base_path
|
||||
local_base_path = os.environ.get("TACTILITY_SDK_PATH")
|
||||
validate_local_sdks(platforms_to_build, manifest["target"]["sdk"])
|
||||
validate_local_sdks(platforms_to_build, manifest["target.sdk"])
|
||||
|
||||
if should_fetch_sdkconfig_files(platforms_to_build):
|
||||
fetch_sdkconfig_files(platforms_to_build)
|
||||
@@ -521,7 +508,7 @@ def build_action(manifest, platform_arg, skip_build):
|
||||
sdk_json = read_sdk_json()
|
||||
validate_self(sdk_json)
|
||||
# Build
|
||||
sdk_version = manifest["target"]["sdk"]
|
||||
sdk_version = manifest["target.sdk"]
|
||||
if not use_local_sdk:
|
||||
if not sdk_download_all(sdk_version, platforms_to_build):
|
||||
exit_with_error("Failed to download one or more SDKs")
|
||||
@@ -570,7 +557,7 @@ def get_device_info(ip):
|
||||
print_status_error(f"Device info request failed: {e}")
|
||||
|
||||
def run_action(manifest, ip):
|
||||
app_id = manifest["app"]["id"]
|
||||
app_id = manifest["app.id"]
|
||||
print_status_busy("Running")
|
||||
url = get_url(ip, "/app/run")
|
||||
params = {'id': app_id}
|
||||
@@ -614,7 +601,7 @@ def install_action(ip, platforms):
|
||||
return False
|
||||
|
||||
def uninstall_action(manifest, ip):
|
||||
app_id = manifest["app"]["id"]
|
||||
app_id = manifest["app.id"]
|
||||
print_status_busy("Uninstalling")
|
||||
url = get_url(ip, "/app/uninstall")
|
||||
params = {'id': app_id}
|
||||
@@ -670,7 +657,7 @@ if __name__ == "__main__":
|
||||
exit_with_error("manifest.properties not found")
|
||||
manifest = read_manifest()
|
||||
validate_manifest(manifest)
|
||||
all_platform_targets = manifest["target"]["platforms"].split(",")
|
||||
all_platform_targets = manifest["target.platforms"].split(",")
|
||||
# Update SDK cache (tool.json)
|
||||
if not use_local_sdk and should_update_tool_json() and not update_tool_json():
|
||||
exit_with_error("Failed to retrieve SDK info")
|
||||
|
||||
Reference in New Issue
Block a user