6 Commits

Author SHA1 Message Date
Adolfo 2795dddc12 feat(RobotArm): cute MCP arm controller with 2x tall sliders, sequencer colored blocks, smooth move_all_joints
- 6 joints vertical sliders 22x72 (2x tall), 3 cols, pastel cute cards, home-centered ranges shoulder max 70
- open/close inverted fix (0=open 180=close)
- sequencer bottom not fixed, colored blocks 22x22 no info, current highlight white 3px, larger buttons 38x32 Play 56x32
- smooth concurrent moves via move_all_joints tool (tools/list discovered) not one-by-one
- scroll fixes: clearflag SCROLLABLE on cards/grid/hdr/brow/seqbar, root LV_DIR_VER only (no horiz scroll)
- safe for device .129: custom my_htons avoids missing lwip_htons, 52U 0 missing symbols, 14K ELF
- live reading from 192.168.68.103/api/mcp get_arm_state + move_joint
- enjoyed by son :3
2026-07-17 11:19:07 -04:00
Ken Van Hoeylandt 8721a653e7 Tool 4.1.0 2026-07-03 23:17:09 +02:00
Ken Van Hoeylandt 694b68de1a Fixes for app publishing (#35) 2026-07-03 22:30:14 +02:00
Ken Van Hoeylandt 71bf2631f4 Implement CDN uploading (#34) 2026-07-03 21:41:18 +02:00
Ken Van Hoeylandt 4ab2377970 Manifest format update (#33) 2026-07-03 00:04:29 +02:00
Ken Van Hoeylandt 8a0f9ef4e4 SDK 0.7.0 & bump app versions (#32) 2026-07-02 19:31:34 +02:00
214 changed files with 780 additions and 21147 deletions
+21
View File
@@ -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 }}
+9 -1
View File
@@ -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:
+22 -1
View File
@@ -12,10 +12,12 @@ jobs:
Build:
strategy:
matrix:
app_name: [Brainfuck, Breakout, BookPlayer, Calculator, Diceware, EpubReader, GPIO, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, MediaKeys, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven]
app_name: [Brainfuck, Breakout, Calculator, Diceware, EpubReader, GPIO, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, MediaKeys, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven]
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 }}
-6
View File
@@ -1,6 +0,0 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
idf_component_register(
SRCS ${SOURCE_FILES}
REQUIRES TactilitySDK
)
-337
View File
@@ -1,337 +0,0 @@
/**
* Audio Test - Record & Playback
*
* Records from the ES8311 microphone via I2S and plays it back through
* the FM8002E speaker amplifier. Uses the Tactility kernel I2S controller API.
*
* UI: Two buttons - Record and Play
* Record: holds recording for up to ~3 seconds (16kHz 16-bit mono ≈ 96KB)
* Play: plays back the last recorded buffer
*/
#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 "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#define TAG "AudioTest"
/* ─── Audio params ─── */
#define SAMPLE_RATE 16000
#define BITS_PER_SAMPLE 16
#define RECORD_SECONDS 3
/* 16-bit mono → 2 bytes per sample */
#define AUDIO_BUF_SIZE (SAMPLE_RATE * (BITS_PER_SAMPLE / 8) * RECORD_SECONDS)
/* Chunk size for read/write calls (512 samples = 1024 bytes at 16-bit) */
#define CHUNK_SAMPLES 512
#define CHUNK_BYTES (CHUNK_SAMPLES * (BITS_PER_SAMPLE / 8))
/* ─── App state ─── */
typedef enum {
STATE_IDLE,
STATE_RECORDING,
STATE_PLAYING
} AudioState;
typedef struct {
struct Device* i2s_dev;
uint8_t* audio_buf;
size_t recorded_bytes;
AudioState state;
lv_obj_t* btn_record;
lv_obj_t* btn_play;
lv_obj_t* lbl_status;
lv_obj_t* bar_progress;
TaskHandle_t task_handle;
} AppCtx;
/* ─── Forward decls ─── */
static void record_task(void* arg);
static void play_task(void* arg);
static void update_ui(AppCtx* ctx);
/* ─── Callbacks ─── */
static void on_record_click(lv_event_t* e) {
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
if (ctx->state != STATE_IDLE) return;
if (ctx->i2s_dev == NULL) return;
ctx->state = STATE_RECORDING;
ctx->recorded_bytes = 0;
update_ui(ctx);
xTaskCreate(record_task, "audio_rec", 4096, ctx, 5, &ctx->task_handle);
}
static void on_play_click(lv_event_t* e) {
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
if (ctx->state != STATE_IDLE) return;
if (ctx->recorded_bytes == 0) return;
if (ctx->i2s_dev == NULL) return;
ctx->state = STATE_PLAYING;
update_ui(ctx);
xTaskCreate(play_task, "audio_play", 4096, ctx, 5, &ctx->task_handle);
}
/* ─── Update UI labels/buttons from any context ─── */
static void update_ui(AppCtx* ctx) {
if (ctx->lbl_status == NULL) return;
switch (ctx->state) {
case STATE_RECORDING:
lv_label_set_text(ctx->lbl_status, "🔴 Recording...");
lv_obj_add_state(ctx->btn_record, LV_STATE_DISABLED);
lv_obj_add_state(ctx->btn_play, LV_STATE_DISABLED);
break;
case STATE_PLAYING:
lv_label_set_text(ctx->lbl_status, "🔊 Playing...");
lv_obj_add_state(ctx->btn_record, LV_STATE_DISABLED);
lv_obj_add_state(ctx->btn_play, LV_STATE_DISABLED);
break;
case STATE_IDLE:
default:
if (ctx->recorded_bytes > 0) {
char buf[64];
snprintf(buf, sizeof(buf), "Ready (%u bytes recorded)", (unsigned)ctx->recorded_bytes);
lv_label_set_text(ctx->lbl_status, buf);
} else {
lv_label_set_text(ctx->lbl_status, "Press Record to capture audio");
}
lv_obj_clear_state(ctx->btn_record, LV_STATE_DISABLED);
if (ctx->recorded_bytes > 0) {
lv_obj_clear_state(ctx->btn_play, LV_STATE_DISABLED);
} else {
lv_obj_add_state(ctx->btn_play, LV_STATE_DISABLED);
}
break;
}
}
/* ─── Record task ─── */
static void record_task(void* arg) {
AppCtx* ctx = (AppCtx*)arg;
size_t total = 0;
size_t bytes_read = 0;
/* Configure I2S for recording */
struct I2sConfig cfg = {
.communication_format = I2S_FORMAT_STAND_I2S,
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channel_left = 0,
.channel_right = I2S_CHANNEL_NONE
};
device_lock(ctx->i2s_dev);
error_t err = i2s_controller_set_config(ctx->i2s_dev, &cfg);
device_unlock(ctx->i2s_dev);
if (err != ERROR_NONE) {
ESP_LOGE(TAG, "Failed to set I2S config for recording: %d", err);
tt_lvgl_lock(portMAX_DELAY);
ctx->state = STATE_IDLE;
update_ui(ctx);
tt_lvgl_unlock();
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "Recording started (max %d bytes)", AUDIO_BUF_SIZE);
while (total < AUDIO_BUF_SIZE && ctx->state == STATE_RECORDING) {
size_t remaining = AUDIO_BUF_SIZE - total;
size_t to_read = (remaining < CHUNK_BYTES) ? remaining : CHUNK_BYTES;
err = i2s_controller_read(ctx->i2s_dev,
ctx->audio_buf + total,
to_read,
&bytes_read,
pdMS_TO_TICKS(1000));
if (err != ERROR_NONE) {
ESP_LOGE(TAG, "I2S read error: %d", err);
break;
}
total += bytes_read;
/* Update progress bar */
int pct = (int)((total * 100) / AUDIO_BUF_SIZE);
tt_lvgl_lock(portMAX_DELAY);
lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF);
tt_lvgl_unlock();
}
ctx->recorded_bytes = total;
ctx->state = STATE_IDLE;
ctx->task_handle = NULL;
ESP_LOGI(TAG, "Recording complete: %u bytes", (unsigned)total);
tt_lvgl_lock(portMAX_DELAY);
lv_bar_set_value(ctx->bar_progress, 0, LV_ANIM_OFF);
update_ui(ctx);
tt_lvgl_unlock();
vTaskDelete(NULL);
}
/* ─── Play task ─── */
static void play_task(void* arg) {
AppCtx* ctx = (AppCtx*)arg;
size_t total = 0;
size_t bytes_written = 0;
/* Configure I2S for playback */
struct I2sConfig cfg = {
.communication_format = I2S_FORMAT_STAND_I2S,
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channel_left = 0,
.channel_right = I2S_CHANNEL_NONE
};
device_lock(ctx->i2s_dev);
error_t err = i2s_controller_set_config(ctx->i2s_dev, &cfg);
device_unlock(ctx->i2s_dev);
if (err != ERROR_NONE) {
ESP_LOGE(TAG, "Failed to set I2S config for playback: %d", err);
tt_lvgl_lock(portMAX_DELAY);
ctx->state = STATE_IDLE;
update_ui(ctx);
tt_lvgl_unlock();
vTaskDelete(NULL);
return;
}
ESP_LOGI(TAG, "Playback started (%u bytes)", (unsigned)ctx->recorded_bytes);
while (total < ctx->recorded_bytes && ctx->state == STATE_PLAYING) {
size_t remaining = ctx->recorded_bytes - total;
size_t to_write = (remaining < CHUNK_BYTES) ? remaining : CHUNK_BYTES;
err = i2s_controller_write(ctx->i2s_dev,
ctx->audio_buf + total,
to_write,
&bytes_written,
pdMS_TO_TICKS(1000));
if (err != ERROR_NONE) {
ESP_LOGE(TAG, "I2S write error: %d", err);
break;
}
total += bytes_written;
/* Update progress bar */
int pct = (int)((total * 100) / ctx->recorded_bytes);
tt_lvgl_lock(portMAX_DELAY);
lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF);
tt_lvgl_unlock();
}
ctx->state = STATE_IDLE;
ctx->task_handle = NULL;
ESP_LOGI(TAG, "Playback complete");
tt_lvgl_lock(portMAX_DELAY);
lv_bar_set_value(ctx->bar_progress, 0, LV_ANIM_OFF);
update_ui(ctx);
tt_lvgl_unlock();
vTaskDelete(NULL);
}
/* ─── App lifecycle ─── */
static AppCtx g_ctx;
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
memset(&g_ctx, 0, sizeof(g_ctx));
/* Allocate audio buffer */
g_ctx.audio_buf = (uint8_t*)malloc(AUDIO_BUF_SIZE);
if (g_ctx.audio_buf == NULL) {
ESP_LOGE(TAG, "Failed to allocate audio buffer (%d bytes)", AUDIO_BUF_SIZE);
}
/* Find I2S device */
g_ctx.i2s_dev = device_find_by_name("i2s0");
if (g_ctx.i2s_dev == NULL) {
ESP_LOGE(TAG, "I2S device 'i2s0' not found!");
} else {
ESP_LOGI(TAG, "Found I2S device: %s", g_ctx.i2s_dev->name);
}
/* ─── UI ─── */
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
/* Status label */
g_ctx.lbl_status = lv_label_create(parent);
lv_label_set_text(g_ctx.lbl_status, "Initializing...");
lv_obj_set_width(g_ctx.lbl_status, lv_pct(90));
lv_label_set_long_mode(g_ctx.lbl_status, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_align(g_ctx.lbl_status, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(g_ctx.lbl_status, LV_ALIGN_TOP_MID, 0, 50);
/* Progress bar */
g_ctx.bar_progress = lv_bar_create(parent);
lv_obj_set_size(g_ctx.bar_progress, lv_pct(80), 10);
lv_bar_set_range(g_ctx.bar_progress, 0, 100);
lv_bar_set_value(g_ctx.bar_progress, 0, LV_ANIM_OFF);
lv_obj_align(g_ctx.bar_progress, LV_ALIGN_CENTER, 0, -10);
/* Button container */
lv_obj_t* btn_container = lv_obj_create(parent);
lv_obj_remove_style_all(btn_container);
lv_obj_set_size(btn_container, lv_pct(90), 50);
lv_obj_set_flex_flow(btn_container, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(btn_container, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_align(btn_container, LV_ALIGN_CENTER, 0, 30);
/* Record button */
g_ctx.btn_record = lv_btn_create(btn_container);
lv_obj_set_size(g_ctx.btn_record, 100, 40);
lv_obj_t* lbl_rec = lv_label_create(g_ctx.btn_record);
lv_label_set_text(lbl_rec, LV_SYMBOL_AUDIO " Record");
lv_obj_center(lbl_rec);
lv_obj_add_event_cb(g_ctx.btn_record, on_record_click, LV_EVENT_CLICKED, &g_ctx);
/* Play button */
g_ctx.btn_play = lv_btn_create(btn_container);
lv_obj_set_size(g_ctx.btn_play, 100, 40);
lv_obj_t* lbl_play = lv_label_create(g_ctx.btn_play);
lv_label_set_text(lbl_play, LV_SYMBOL_PLAY " Play");
lv_obj_center(lbl_play);
lv_obj_add_event_cb(g_ctx.btn_play, on_play_click, LV_EVENT_CLICKED, &g_ctx);
/* Check initialization status */
if (g_ctx.i2s_dev == NULL) {
lv_label_set_text(g_ctx.lbl_status, "ERROR: I2S device not found");
lv_obj_add_state(g_ctx.btn_record, LV_STATE_DISABLED);
lv_obj_add_state(g_ctx.btn_play, LV_STATE_DISABLED);
} else if (g_ctx.audio_buf == NULL) {
lv_label_set_text(g_ctx.lbl_status, "ERROR: Out of memory");
lv_obj_add_state(g_ctx.btn_record, LV_STATE_DISABLED);
lv_obj_add_state(g_ctx.btn_play, LV_STATE_DISABLED);
} else {
g_ctx.state = STATE_IDLE;
update_ui(&g_ctx);
}
}
int main(int argc, char* argv[]) {
tt_app_register((AppRegistration) {
.onShow = onShowApp
});
return 0;
}
-10
View File
@@ -1,10 +0,0 @@
[manifest]
version=0.1
[target]
sdk=0.7.0-dev
platforms=esp32s3
[app]
id=one.tactility.audiotest
versionName=0.1.0
versionCode=1
name=Audio Test
-16
View File
@@ -1,16 +0,0 @@
cmake_minimum_required(VERSION 3.20)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
if (DEFINED ENV{TACTILITY_SDK_PATH})
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
else()
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
endif()
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
project(BibleVerse)
tactility_project(BibleVerse)
-84
View File
@@ -1,84 +0,0 @@
# BibleVerse for Tactility
Single-verse bible reader. Shows one verse fullscreen, auto-advances every minute. Tap to toggle controls.
## UX
- **Default view (no chrome)**: immersive context — just the verse text + reference, centered, large font. Like BookPlayer image-only view.
- **Tap**: shows header (current ref + position) and footer controls:
- Close X, Prev, Pause/Play (auto-advance), Next, Highlight (star), Audio memo placeholder.
- Highlighted verses saved to `favorites.txt` in app user data. Long-term: record voice memo, read aloud TTS.
## Bible data format (split per book)
We fetch public domain bibles from https://github.com/sajeevavahini/bibles (Zefania XML).
Offline conversion:
```
python3 tools/convert_bible.py --src "World English Bible.xml" --dst assets/bible
```
Result structure:
```
assets/bible/
books.json # {"books":[{"bnumber":1,"bname":"Genesis","verses":1533,...}], total_verses, translation, version}
01.bin / 01.idx # Genesis
02.bin / 02.idx # Exodus
...
66.bin / 66.idx
```
- `.bin`: concatenated NUL-terminated UTF8 verse texts. No parsing needed, low RAM.
- `.idx`: header 10 bytes: magic 'BIBK', uint16 version, uint32 count. Then per entry 8 bytes: uint32 offset, uint16 cnum, uint16 vnum.
This split makes memory footprint tiny for ESP32:
- Only one book loaded at a time (max ~215KB for Psalms, typical 12-100KB).
- Index ~19KB max for Psalms, typically <5KB.
## SD card fallback
App also scans:
- `/sdcard/bibles/` and `/sdcard/bible/`
If `books.json` not found in assets, it will try to use SD folder. This lets users drop extra translations offline.
## Settings persistence
Same pattern as EpubReader / BookPlayer (but those two didn't persist in BookPlayer, EpubReader does):
Using Tactility app user data paths:
- `tt_app_get_user_data_path()` -> e.g. `/sdcard/user/app/one.tactility.bibleverse`
- `progress.txt`: stores global verse index + b/c/v for debug
```
1234
1 1 1
0 0
```
- `favorites.txt`: list of global indices, one per line.
This lives on SD card (or internal data partition), survives app reinstalls.
## Build (color screen device, esp32s3)
Same as BookPlayer — external app, compiled for esp32s3 target (covers CYD-2432s028r, m5stack-cores3, etc).
```bash
# source IDF (example)
source ~/esp/esp-idf/export.sh
export TACTILITY_SDK_PATH=/path/to/TactilitySDK # or use --local-sdk
python3 tactility.py Apps/BibleVerse build esp32s3 --local-sdk
```
Output: `build/BibleVerse.app` installable via Tactility file manager or `tactility.py ... install <ip>`
## Future extensions
- Voice memo: record to `/sdcard/bible_memos/<global>.wav` using same I2S code as BookPlayer
- TTS read aloud: use esp tts or pre-generated mp3 like BookPlayer pages
- Book picker UI (like BookPlayer picker but for bible books)
- Highlight color, search, jump to reference dialog
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More