8 Commits

Author SHA1 Message Date
Ken Van Hoeylandt b1e7eb999a Update apps for SDK updates (#38)
The updates mainly relate to lvgl-module changes.
2026-07-26 17:20:01 +02:00
Ken Van Hoeylandt 4f635d38e3 Updates for removal of deprecated HAL (#37)
Update all apps to replace deprecated LVGL code and to use the new kernel APIs.
2026-07-26 12:47:03 +02:00
Shadowtrance 25b966a340 Add EspNowBridge + updates for Audio System (#36) 2026-07-19 21:52:39 +02: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
84 changed files with 1628 additions and 3035 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 name: Release Apps
outputs:
sdk_version:
description: 'Common SDK version shared by all bundled apps'
value: ${{ steps.release.outputs.sdk_version }}
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
@@ -11,8 +16,11 @@ runs:
run: rsync -av downloaded_apps/*/*.app cdn_files/ run: rsync -av downloaded_apps/*/*.app cdn_files/
shell: bash shell: bash
- name: 'Create CDN release files' - name: 'Create CDN release files'
run: python release.py cdn_files/ id: release
shell: bash shell: bash
run: |
python release.py cdn_files/
echo "sdk_version=$(cat sdk_version.txt)" >> "$GITHUB_OUTPUT"
- name: 'Upload Artifact' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
+22 -1
View File
@@ -12,10 +12,12 @@ jobs:
Build: Build:
strategy: strategy:
matrix: matrix:
app_name: [Brainfuck, Breakout, Calculator, Diceware, EpubReader, GPIO, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, MediaKeys, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven] app_name: [Brainfuck, Breakout, Calculator, Diceware, EpubReader, EspNowBridge, GPIO, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, MediaKeys, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Build" - name: "Build"
uses: ./.github/actions/build-app uses: ./.github/actions/build-app
with: with:
@@ -23,7 +25,26 @@ jobs:
Bundle: Bundle:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [Build] needs: [Build]
outputs:
sdk_version: ${{ steps.release.outputs.sdk_version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Build" - name: "Build"
id: release
uses: ./.github/actions/release-apps 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 }}
+4 -4
View File
@@ -1,6 +1,6 @@
#include "Brainfuck.h" #include "Brainfuck.h"
#include <tt_app.h> #include <tt_app.h>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <dirent.h> #include <dirent.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -399,11 +399,11 @@ void Brainfuck::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Brainfuck interpreter");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
clrBtn = tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearClicked, nullptr); clrBtn = lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearClicked, nullptr);
lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onExamplesClicked, nullptr); lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onExamplesClicked, nullptr);
lv_obj_t* cont = lv_obj_create(parent); lv_obj_t* cont = lv_obj_create(parent);
lv_obj_set_width(cont, LV_PCT(100)); lv_obj_set_width(cont, LV_PCT(100));
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.brainfuck
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.6.0
[app] app.version.code=6
id=one.tactility.brainfuck app.name=Brainfuck interpreter
versionName=0.2.0 app.description=Brainfuck esoteric language interpreter
versionCode=2
name=Brainfuck interpreter
description=Brainfuck esoteric language interpreter
+7 -7
View File
@@ -7,13 +7,14 @@
#include <cstdio> #include <cstdio>
#include <cmath> #include <cmath>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tt_preferences.h> #include <tt_preferences.h>
#include <esp_random.h> #include <esp_random.h>
#include <tt_lvgl_keyboard.h> #include <tactility/device.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/lvgl_module.h> #include <lvgl/lvgl.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
constexpr auto* TAG = "Breakout"; constexpr auto* TAG = "Breakout";
@@ -124,12 +125,11 @@ void Breakout::onShow(AppHandle appHandle, lv_obj_t* parent) {
if (!sfxEngine) { if (!sfxEngine) {
sfxEngine = new SfxEngine(); sfxEngine = new SfxEngine();
sfxEngine->start(); sfxEngine->start();
sfxEngine->applyVolumePreset(SfxEngine::VolumePreset::Quiet);
sfxEngine->setEnabled(soundEnabled); sfxEngine->setEnabled(soundEnabled);
} }
// Toolbar // Toolbar
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Breakout");
// Score wrapper in toolbar // Score wrapper in toolbar
lv_obj_t* scoreWrap = lv_obj_create(toolbar); lv_obj_t* scoreWrap = lv_obj_create(toolbar);
@@ -1330,7 +1330,7 @@ void Breakout::updateMessage() {
case GameState::Ready: { case GameState::Ready: {
char buf[64]; char buf[64];
const char* input_hint = "Touch"; const char* input_hint = "Touch";
if (tt_lvgl_hardware_keyboard_is_available()) { if (device_has_active_by_type(&KEYBOARD_TYPE)) {
input_hint = "Space"; input_hint = "Space";
} }
if (level > 1) { if (level > 1) {
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.breakout
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.7.0
[app] app.version.code=7
id=one.tactility.breakout app.name=Breakout
versionName=0.2.0 app.description=Classic brick-breaking arcade game
versionCode=2
name=Breakout
description=Classic brick-breaking arcade game
+2 -2
View File
@@ -2,7 +2,7 @@
#include <cstdio> #include <cstdio>
#include <ctype.h> #include <ctype.h>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <stack> #include <stack>
#include <cstring> #include <cstring>
@@ -148,7 +148,7 @@ void Calculator::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Calculator");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* wrapper = lv_obj_create(parent); lv_obj_t* wrapper = lv_obj_create(parent);
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.calculator
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.7.0
[app] app.version.code=7
id=one.tactility.calculator app.name=Calculator
versionName=0.3.0
versionCode=3
name=Calculator
+11 -12
View File
@@ -1,9 +1,9 @@
#include "Diceware.h" #include "Diceware.h"
#include <tt_app_alertdialog.h> #include <tt_app_alertdialog.h>
#include <tt_lock.h> #include <tactility/filesystem/file_mutex.h>
#include <tt_lvgl.h> #include <lvgl/lvgl.h>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <esp_random.h> #include <esp_random.h>
#include <esp_log.h> #include <esp_log.h>
@@ -39,18 +39,17 @@ static std::string readWordAtLine(const AppHandle handle, const int lineIndex) {
return ""; return "";
} }
auto lock = tt_lock_alloc_for_path(path); struct FileMutex mutex;
file_mutex_get(&mutex, path);
std::string word; std::string word;
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) { file_mutex_lock(&mutex);
FILE* file = fopen(path, "r"); FILE* file = fopen(path, "r");
if (file != nullptr) { if (file != nullptr) {
skipNewlines(file, lineIndex); skipNewlines(file, lineIndex);
word = readWord(file); word = readWord(file);
fclose(file); fclose(file);
} else { ESP_LOGE(TAG, "Failed to open %s", path); } } else { ESP_LOGE(TAG, "Failed to open %s", path); }
tt_lock_release(lock); file_mutex_unlock(&mutex);
} else { ESP_LOGE(TAG, "Failed to acquire lock for %s", path); }
tt_lock_free(lock);
return word; return word;
} }
@@ -87,9 +86,9 @@ void Diceware::startJob(uint32_t jobWordCount) {
} }
void Diceware::onFinishJob(std::string result) { void Diceware::onFinishJob(std::string result) {
tt_lvgl_lock(tt::kernel::MAX_TICKS); lvgl_lock();
lv_label_set_text(resultLabel, result.c_str()); lv_label_set_text(resultLabel, result.c_str());
tt_lvgl_unlock(); lvgl_unlock();
} }
void Diceware::onClickGenerate(lv_event_t* e) { void Diceware::onClickGenerate(lv_event_t* e) {
@@ -123,8 +122,8 @@ void Diceware::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); auto* toolbar = lvgl_toolbar_create(parent, "Diceware");
tt_lvgl_toolbar_add_text_button_action(toolbar, "?", onHelpClicked, nullptr); lvgl_toolbar_add_text_button_action(toolbar, "?", onHelpClicked, nullptr);
auto* wrapper = lv_obj_create(parent); auto* wrapper = lv_obj_create(parent);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.diceware
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.8.0
[app] app.version.code=8
id=one.tactility.diceware app.name=Diceware
versionName=0.3.0
versionCode=3
name=Diceware
+2 -2
View File
@@ -1,7 +1,7 @@
#include "EpubReader.h" #include "EpubReader.h"
#include "HtmlStrip.h" // stripHtmlToText #include "HtmlStrip.h" // stripHtmlToText
#include <tt_bundle.h> #include <tt_bundle.h>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tt_app_alertdialog.h> #include <tt_app_alertdialog.h>
#include <tt_app_selectiondialog.h> #include <tt_app_selectiondialog.h>
#include <tactility/log.h> #include <tactility/log.h>
@@ -239,7 +239,7 @@ void EpubReader::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
toolbar_ = tt_lvgl_toolbar_create_for_app(parent, app); toolbar_ = lvgl_toolbar_create(parent, "Epub Reader");
wrapperWidget_ = lv_obj_create(parent); wrapperWidget_ = lv_obj_create(parent);
lv_obj_set_width(wrapperWidget_, LV_PCT(100)); lv_obj_set_width(wrapperWidget_, LV_PCT(100));
@@ -1,6 +1,6 @@
#include "EpubReader.h" #include "EpubReader.h"
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tt_lock.h> #include <tactility/filesystem/file_mutex.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <Tactility/kernel/Kernel.h> #include <Tactility/kernel/Kernel.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
@@ -43,7 +43,7 @@ void EpubReader::spawnOpenTask(EpubReader* self, bool restore) {
// Show a brief placeholder so old content doesn't linger during the open // Show a brief placeholder so old content doesn't linger during the open
lv_obj_clean(self->wrapperWidget_); lv_obj_clean(self->wrapperWidget_);
tt_lvgl_toolbar_clear_actions(self->toolbar_); lvgl_toolbar_clear_actions(self->toolbar_);
lv_obj_t* lbl = lv_label_create(self->wrapperWidget_); lv_obj_t* lbl = lv_label_create(self->wrapperWidget_);
lv_obj_set_style_pad_all(lbl, 8, 0); lv_obj_set_style_pad_all(lbl, 8, 0);
lv_label_set_text(lbl, restore ? "Loading..." : "Opening..."); lv_label_set_text(lbl, restore ? "Loading..." : "Opening...");
@@ -115,14 +115,9 @@ void EpubReader::backgroundOpenTask(void* data) {
// Acquire the filesystem lock before any SD card I/O - prevents concurrent // Acquire the filesystem lock before any SD card I/O - prevents concurrent
// SDMMC access from the background and LVGL tasks (bus errors 0x107/0x108). // SDMMC access from the background and LVGL tasks (bus errors 0x107/0x108).
auto lock = tt_lock_alloc_for_path(a->filePath.c_str()); struct FileMutex mutex;
if (!tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) { file_mutex_get(&mutex, a->filePath.c_str());
LOG_E(TAG, "FS lock timed out, skipping open: %s", a->filePath.c_str()); file_mutex_lock(&mutex);
tt_lock_free(lock);
lv_async_call(asyncOpenComplete, a);
vTaskDelete(nullptr);
return;
}
if (isTextFile(a->filePath)) { if (isTextFile(a->filePath)) {
// Read the entire text file here (under the lock) so asyncOpenComplete // Read the entire text file here (under the lock) so asyncOpenComplete
@@ -146,8 +141,7 @@ void EpubReader::backgroundOpenTask(void* data) {
a->epub = EpubService::open(a->filePath); a->epub = EpubService::open(a->filePath);
} }
tt_lock_release(lock); file_mutex_unlock(&mutex);
tt_lock_free(lock);
// Signal the LVGL task that the work is done // Signal the LVGL task that the work is done
lv_async_call(asyncOpenComplete, a); lv_async_call(asyncOpenComplete, a);
+8 -8
View File
@@ -1,5 +1,5 @@
#include "EpubReader.h" #include "EpubReader.h"
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <dirent.h> #include <dirent.h>
@@ -37,20 +37,20 @@ static void setListBtnLongMode(lv_obj_t* btn, lv_label_long_mode_t mode) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void EpubReader::setReaderToolbarButtons() { void EpubReader::setReaderToolbarButtons() {
tt_lvgl_toolbar_clear_actions(toolbar_); lvgl_toolbar_clear_actions(toolbar_);
tt_lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_PREV, onPrevPressed, this); lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_PREV, onPrevPressed, this);
if (!textMode_) { if (!textMode_) {
tt_lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_LIST, onTocPressed, this); lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_LIST, onTocPressed, this);
} }
tt_lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_NEXT, onNextPressed, this); lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_NEXT, onNextPressed, this);
tt_lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_DIRECTORY, onBrowsePressed, this); lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_DIRECTORY, onBrowsePressed, this);
} }
void EpubReader::setBrowserToolbarButtons() { void EpubReader::setBrowserToolbarButtons() {
tt_lvgl_toolbar_clear_actions(toolbar_); lvgl_toolbar_clear_actions(toolbar_);
// Show "Use Folder" button when the current browse path isn't already the saved books folder // Show "Use Folder" button when the current browse path isn't already the saved books folder
if (browsePath_ != booksPath_) { if (browsePath_ != booksPath_) {
tt_lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_DIRECTORY, onSetBooksFolder, this); lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_DIRECTORY, onSetBooksFolder, this);
} }
} }
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32s3,esp32p4
sdk=0.7.0-dev app.id=one.tactility.epubreader
platforms=esp32s3,esp32p4 app.version.name=0.5.0
[app] app.version.code=5
id=one.tactility.epubreader app.name=Epub Reader
versionName=0.1.0 app.description=Epub and text file reader. Requires PSRAM!
versionCode=1
name=Epub Reader
description=Epub and text file reader. Requires PSRAM!
@@ -6,11 +6,11 @@ if (DEFINED ENV{TACTILITY_SDK_PATH})
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
else() else()
set(TACTILITY_SDK_PATH "../../release/TactilitySDK") set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}") message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
endif() endif()
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
project(Mp3Player) project(EspNowBridge)
tactility_project(Mp3Player) tactility_project(EspNowBridge)
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
file(GLOB_RECURSE SOURCE_FILES
Source/*.c*
)
idf_component_register(
SRCS ${SOURCE_FILES}
# Library headers must be included directly,
# because all regular dependencies get stripped by elf_loader's cmake script
INCLUDE_DIRS ../../../Libraries/TactilityCpp/Include
REQUIRES TactilitySDK bootloader_support esp_app_format
)
@@ -0,0 +1,740 @@
#include "EspNowBridge.h"
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/wifi_auto_scan.h>
#include <tactility/firmware/firmware.h>
#include <tt_app.h>
#include <tt_app_fileselection.h>
#include <tt_bundle.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/toolbar.h>
#include <esp_app_desc.h>
#include <esp_app_format.h>
#include <esp_system.h>
#include <tactility/log.h>
constexpr TickType_t LVGL_DEFAULT_LOCK_TIME = 500; // 500 ticks = 500 ms
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <cstring>
static constexpr auto* TAG = "EspNowBridge";
static constexpr size_t CHUNK_SIZE = 1500;
static constexpr uint32_t TRANSPORT_WAIT_TIMEOUT_MS = 5000;
static constexpr uint32_t UPDATE_TASK_STACK_SIZE = 8192;
AutoScanPauseGuard::AutoScanPauseGuard() { wifi_auto_scan_set_paused(true); }
AutoScanPauseGuard::~AutoScanPauseGuard() { wifi_auto_scan_set_paused(false); }
// Binary partition table format (gen_esp32part.py STRUCT_FORMAT '<2sBBLL16sL'): a flat array of
// 32-byte little-endian records starting at flash offset PARTITION_TABLE_OFFSET, terminated by
// an all-0xFF entry or an MD5-checksum record (magic 0xEBEB). Not exposed as a C header by
// ESP-IDF (only the Python generator knows the format) - this is a hand-ported minimal reader,
// just enough to locate the app partition inside a merged/factory bin.
static constexpr size_t PARTITION_TABLE_OFFSET = 0x8000;
static constexpr size_t PARTITION_TABLE_MAX_ENTRIES = 128; // covers the largest partition table IDF supports (0x1000 / 32)
static constexpr uint16_t PARTITION_ENTRY_MAGIC = 0x50AA; // little-endian bytes 0xAA, 0x50
static constexpr uint16_t PARTITION_MD5_MAGIC = 0xEBEB;
static constexpr uint8_t PARTITION_TYPE_APP = 0x00;
static constexpr uint8_t PARTITION_SUBTYPE_FACTORY = 0x00;
static constexpr uint8_t PARTITION_SUBTYPE_OTA_0 = 0x10;
struct __attribute__((packed)) PartitionEntry {
uint16_t magic;
uint8_t type;
uint8_t subtype;
uint32_t offset;
uint32_t size;
char name[16];
uint32_t flags;
};
static_assert(sizeof(PartitionEntry) == 32, "partition table entry must be 32 bytes");
/**
* Scans the partition table embedded in a merged/factory bin (at PARTITION_TABLE_OFFSET) for
* the app partition to flash: prefers "factory" if present, otherwise the first OTA slot
* (ota_0) - matches what a real M5Stack ESP-Hosted factory image contains.
* @return true if an app partition was found, with appOffset/appSize set to its location
* within the file (these are the same as the absolute flash offsets the merged bin preserves).
*/
static bool findAppPartitionInMergedBin(FILE* file, size_t& appOffset, size_t& appSize) {
if (fseek(file, static_cast<long>(PARTITION_TABLE_OFFSET), SEEK_SET) != 0) {
return false;
}
bool foundFactory = false;
bool foundOta0 = false;
size_t factoryOffset = 0, factorySize = 0;
size_t ota0Offset = 0, ota0Size = 0;
for (size_t i = 0; i < PARTITION_TABLE_MAX_ENTRIES; i++) {
PartitionEntry entry;
if (fread(&entry, 1, sizeof(entry), file) != sizeof(entry)) {
break;
}
if (entry.magic == PARTITION_MD5_MAGIC) {
break;
}
if (entry.magic != PARTITION_ENTRY_MAGIC) {
break;
}
if (entry.type == PARTITION_TYPE_APP) {
if (entry.subtype == PARTITION_SUBTYPE_FACTORY) {
foundFactory = true;
factoryOffset = entry.offset;
factorySize = entry.size;
} else if (entry.subtype == PARTITION_SUBTYPE_OTA_0 && !foundOta0) {
foundOta0 = true;
ota0Offset = entry.offset;
ota0Size = entry.size;
}
}
}
if (foundFactory) {
appOffset = factoryOffset;
appSize = factorySize;
return true;
}
if (foundOta0) {
appOffset = ota0Offset;
appSize = ota0Size;
return true;
}
return false;
}
/**
* Validates the app image at the given file offset and extracts its version string. The actual
* transfer size used for the OTA loop is just the real remaining file size from appOffset (see
* performUpdate) - hand-computing the image's "logical" size from segment headers + checksum/
* hash padding drifts a bit short of the real length, so we just use the file size instead.
*/
static bool parseImageHeader(FILE* file, size_t appOffset, char* versionOut, size_t versionOutLen, std::string* errorOut = nullptr) {
esp_image_header_t imageHeader;
if (fseek(file, static_cast<long>(appOffset), SEEK_SET) != 0 ||
fread(&imageHeader, 1, sizeof(imageHeader), file) != sizeof(imageHeader)) {
if (errorOut != nullptr) {
*errorOut = "Failed to read image header";
}
return false;
}
if (imageHeader.magic != ESP_IMAGE_HEADER_MAGIC) {
if (errorOut != nullptr) {
*errorOut = "Selected file is not a valid firmware image (bad magic)";
}
return false;
}
// Fail fast on a wrong-chip image (e.g. an ESP32 or S3 binary picked by mistake) before
// streaming the whole file over the paced, slow bridge link - esp_hosted_slave_ota_end()
// would eventually catch this too, but only after the entire transfer already completed.
if (imageHeader.chip_id != ESP_CHIP_ID_ESP32C6) {
if (errorOut != nullptr) {
char buf[96];
snprintf(buf, sizeof(buf), "Wrong chip: image targets chip id %u, expected ESP32-C6",
(unsigned)imageHeader.chip_id);
*errorOut = buf;
}
return false;
}
esp_image_segment_header_t segmentHeader;
size_t firstSegmentOffset = appOffset + sizeof(imageHeader);
if (fseek(file, static_cast<long>(firstSegmentOffset), SEEK_SET) != 0 ||
fread(&segmentHeader, 1, sizeof(segmentHeader), file) != sizeof(segmentHeader)) {
if (errorOut != nullptr) {
*errorOut = "Failed to read first segment header";
}
return false;
}
esp_app_desc_t appDesc;
size_t appDescOffset = appOffset + sizeof(imageHeader) + sizeof(segmentHeader);
if (fseek(file, static_cast<long>(appDescOffset), SEEK_SET) == 0 && fread(&appDesc, 1, sizeof(appDesc), file) == sizeof(appDesc)) {
strncpy(versionOut, appDesc.version, versionOutLen - 1);
versionOut[versionOutLen - 1] = '\0';
} else {
strncpy(versionOut, "unknown", versionOutLen - 1);
versionOut[versionOutLen - 1] = '\0';
}
return true;
}
static bool getCurrentVersionString(const FirmwareOps* ops, void* ctx, char* versionOut, size_t versionOutLen) {
FirmwareInfo info = {};
if (ops == nullptr || ops->get_info(ctx, &info) != ERROR_NONE) {
return false;
}
if (info.name[0] != '\0') {
snprintf(versionOut, versionOutLen, "%u.%u.%u (%s)",
(unsigned)info.fw_major, (unsigned)info.fw_minor, (unsigned)info.fw_patch, info.name);
} else {
snprintf(versionOut, versionOutLen, "%u.%u.%u",
(unsigned)info.fw_major, (unsigned)info.fw_minor, (unsigned)info.fw_patch);
}
return true;
}
/** Only slave firmware >= v2.6.0 implements esp_hosted_slave_ota_activate() - older slaves
* reject/lack the RPC entirely. Matches upstream's host_performs_slave_ota example. */
static bool activateSupported(uint32_t major, uint32_t minor) {
return (major > 2) || (major == 2 && minor > 5);
}
std::atomic<EspNowBridge*> EspNowBridge::liveInstance_{nullptr};
void EspNowBridge::onCreate(AppHandle app) {
appHandle_ = app;
taskDoneSemaphore_ = xSemaphoreCreateBinary();
liveInstance_ = this;
}
void EspNowBridge::onDestroy(AppHandle /*app*/) {
// Clear liveInstance_ first so any task still running bails out at its next liveInstance_
// check instead of continuing to touch this instance's members.
liveInstance_ = nullptr;
// Wait for any outstanding background task (OTA update, transport-wait) to actually finish -
// the app framework frees this instance shortly after onDestroy() returns, so a task that
// outlives it would dereference freed memory.
while (outstandingTasks_.load() > 0) {
if (taskDoneSemaphore_ != nullptr) {
xSemaphoreTake(taskDoneSemaphore_, pdMS_TO_TICKS(1000));
}
}
if (taskDoneSemaphore_ != nullptr) {
vSemaphoreDelete(taskDoneSemaphore_);
taskDoneSemaphore_ = nullptr;
}
}
void EspNowBridge::refreshCurrentVersion() {
char versionStr[32];
if (getCurrentVersionString(firmwareOps_, firmwareCtx_, versionStr, sizeof(versionStr))) {
lv_label_set_text_fmt(currentVersionLabel_, "Co-processor firmware: %s", versionStr);
} else {
lv_label_set_text(currentVersionLabel_, "Co-processor firmware: unknown (link not up)");
}
}
bool EspNowBridge::isWifiRadioOn() {
if (wifiDevice_ == nullptr) {
return false;
}
WifiRadioState radioState = WIFI_RADIO_STATE_OFF;
if (wifi_get_radio_state(wifiDevice_, &radioState) != ERROR_NONE) {
return false;
}
// ON with any station state (disconnected/pending/connected) is fine - the ESP-NOW bridge
// just needs the radio + esp_hosted transport up, not a completed AP connection.
return radioState == WIFI_RADIO_STATE_ON;
}
void EspNowBridge::refreshWifiPrompt() {
if (isWifiRadioOn()) {
lv_obj_add_flag(enableWifiButton_, LV_OBJ_FLAG_HIDDEN);
setUpdateButtonsDisabled(false);
} else {
lv_obj_clear_flag(enableWifiButton_, LV_OBJ_FLAG_HIDDEN);
setUpdateButtonsDisabled(true);
}
}
void EspNowBridge::setUpdateButtonsDisabled(bool disabled) {
if (disabled) {
lv_obj_add_state(updateButton_, LV_STATE_DISABLED);
lv_obj_add_state(updateBundledButton_, LV_STATE_DISABLED);
} else {
lv_obj_clear_state(updateButton_, LV_STATE_DISABLED);
lv_obj_clear_state(updateBundledButton_, LV_STATE_DISABLED);
}
}
void EspNowBridge::setStatus(const std::string& text) {
lv_label_set_text(statusLabel_, text.c_str());
}
void EspNowBridge::setProgress(int percent) {
lv_bar_set_value(progressBar_, percent, LV_ANIM_OFF);
}
namespace {
struct UiDispatchPayload {
EspNowBridge* instance;
void (*work)(EspNowBridge&, void*);
void* context;
void (*freeContext)(void*);
};
}
void EspNowBridge::dispatchToUi(void (*work)(EspNowBridge&, void*), void* context, void (*freeContext)(void*)) {
auto* payload = new UiDispatchPayload{this, work, context, freeContext};
// lv_async_call() itself is an LVGL operation and must be lock-guarded when called from a
// non-LVGL task (see lvgl_lock()'s doc comment) - the OTA worker task calls dispatchToUi()
// repeatedly during the transfer, and without this lock most of those calls were silently
// racing LVGL's own task and getting lost (only the very last status update, right before
// esp_restart(), happened to land - everything else stayed stuck at "Waiting for
// co-processor link...").
bool locked = lvgl_try_lock(LVGL_DEFAULT_LOCK_TIME);
if (!locked) {
// Without the lock, lv_async_call() itself would be touching LVGL's internal timer list
// unguarded - and if it happened to still enqueue successfully, the callback below would
// later fire against `payload` after we've already freed it here. Drop the update instead.
if (freeContext != nullptr) {
freeContext(context);
}
delete payload;
return;
}
lv_result_t result = lv_async_call([](void* userData) {
auto* payload = static_cast<UiDispatchPayload*>(userData);
if (EspNowBridge::liveInstance_.load() == payload->instance && payload->instance->isShown_.load()) {
payload->work(*payload->instance, payload->context);
}
if (payload->freeContext != nullptr) {
payload->freeContext(payload->context);
}
delete payload;
}, payload);
lvgl_unlock();
if (result != LV_RESULT_OK) {
if (freeContext != nullptr) {
freeContext(context);
}
delete payload;
}
}
namespace {
void workSetStatus(EspNowBridge& app, void* context) {
app.setStatus(*static_cast<std::string*>(context));
}
void freeString(void* context) { delete static_cast<std::string*>(context); }
void workSetProgress(EspNowBridge& app, void* context) {
app.setProgress(*static_cast<int*>(context));
}
void freeInt(void* context) { delete static_cast<int*>(context); }
} // namespace
void EspNowBridge::performUpdate(const std::string& filePath) {
dispatchToUi([](EspNowBridge& app, void*) {
app.setUpdateButtonsDisabled(true);
app.setProgress(0);
app.setStatus("Waiting for co-processor link...");
}, nullptr, nullptr);
if (firmwareOps_ == nullptr) {
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("This WiFi device has no updatable co-processor");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
if (!firmwareOps_->wait_ready(firmwareCtx_, TRANSPORT_WAIT_TIMEOUT_MS)) {
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("Co-processor link not available - update cancelled");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
FILE* file = fopen(filePath.c_str(), "rb");
if (file == nullptr) {
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("Failed to open selected file");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
fseek(file, 0, SEEK_END);
long fileSizeSigned = ftell(file);
if (fileSizeSigned <= 0) {
fclose(file);
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("Failed to determine file size");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
size_t fileSize = static_cast<size_t>(fileSizeSigned);
// Support both a plain app image (starting with the app image header at offset 0) and a
// merged/factory bin (e.g. M5Stack's official ESP-Hosted factory image) - detected by whether
// a valid partition table is found at PARTITION_TABLE_OFFSET.
size_t appOffset = 0;
size_t partitionSize = 0;
bool isMergedBin = findAppPartitionInMergedBin(file, appOffset, partitionSize);
if (isMergedBin && appOffset >= fileSize) {
fclose(file);
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("Merged bin's app partition is outside the file - selected file looks truncated");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
char newVersion[32];
std::string parseError;
if (!parseImageHeader(file, appOffset, newVersion, sizeof(newVersion), &parseError)) {
fclose(file);
dispatchToUi(workSetStatus, new std::string(parseError), freeString);
dispatchToUi([](EspNowBridge& app, void*) {
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
// Merged bins pad the app partition to its declared size; a plain app image is exactly as
// long as the app itself. Transfer whichever is smaller.
size_t remainingInFile = fileSize - appOffset;
size_t firmwareSize = isMergedBin ? std::min(partitionSize, remainingInFile) : remainingInFile;
std::string versionStr(newVersion);
{
char buf[64];
snprintf(buf, sizeof(buf), "Pushing firmware %s...", versionStr.c_str());
dispatchToUi(workSetStatus, new std::string(buf), freeString);
}
// Held on the app instance (not a local variable) so it outlives this function - see
// heldAutoScanPauseGuard_'s declaration for why. Released when the host actually restarts
// (moot, since esp_restart() doesn't return) or if the update fails early below.
heldAutoScanPauseGuard_.emplace();
FirmwareUpdateRequest updateRequest = {};
updateRequest.image_size = firmwareSize;
FirmwareUpdateHandle* handle = nullptr;
if (firmwareOps_->begin(firmwareCtx_, &updateRequest, &handle) != ERROR_NONE) {
fclose(file);
heldAutoScanPauseGuard_.reset();
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("Failed to start OTA on co-processor");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
if (fseek(file, static_cast<long>(appOffset), SEEK_SET) != 0) {
fclose(file);
firmwareOps_->abort(handle);
heldAutoScanPauseGuard_.reset();
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("Failed to seek to firmware start");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
uint8_t chunk[CHUNK_SIZE];
size_t sent = 0;
bool writeFailed = false;
int lastReportedPercent = -1;
while (sent < firmwareSize) {
size_t toRead = (firmwareSize - sent > CHUNK_SIZE) ? CHUNK_SIZE : (firmwareSize - sent);
size_t actuallyRead = fread(chunk, 1, toRead, file);
if (actuallyRead != toRead) {
LOG_E(TAG, "Failed to read file at offset %zu", sent);
writeFailed = true;
break;
}
if (firmwareOps_->write(handle, chunk, actuallyRead) != ERROR_NONE) {
LOG_E(TAG, "firmwareOps_->write() failed at offset %zu", sent);
writeFailed = true;
break;
}
// Pace the transfer - esp_hosted's SDIO driver only retries a write twice with no
// backoff before giving up and restarting the host. Back-to-back chunk writes with zero
// gap were observed to saturate the bus enough to trigger a genuine SDIO timeout
// mid-transfer, not just around the post-activate reboot.
vTaskDelay(pdMS_TO_TICKS(5));
sent += actuallyRead;
// Only touch LVGL every couple of percent, not every 1500-byte chunk - frequent
// display-bus activity during the transfer was implicated in SDIO transport crashes
// under sustained OTA write load.
int percent = (int)((sent * 100) / firmwareSize);
if (percent != lastReportedPercent) {
dispatchToUi(workSetProgress, new int(percent), freeInt);
lastReportedPercent = percent;
}
}
fclose(file);
if (writeFailed) {
firmwareOps_->abort(handle);
heldAutoScanPauseGuard_.reset();
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("Update failed while transferring firmware");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
if (firmwareOps_->finish(handle) != ERROR_NONE) {
heldAutoScanPauseGuard_.reset();
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("Failed to finalize OTA on co-processor");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
// Check the *currently running* (pre-update) slave version - the new image isn't running
// yet - and skip straight to the required host restart for older slaves.
FirmwareInfo runningInfo = {};
bool canActivate = firmwareOps_->get_info(firmwareCtx_, &runningInfo) == ERROR_NONE
&& activateSupported(runningInfo.fw_major, runningInfo.fw_minor);
if (canActivate) {
if (firmwareOps_->activate(firmwareCtx_) != ERROR_NONE) {
heldAutoScanPauseGuard_.reset();
dispatchToUi([](EspNowBridge& app, void*) {
app.setStatus("Failed to activate new firmware - co-processor still running old firmware");
app.setUpdateButtonsDisabled(false);
}, nullptr, nullptr);
return;
}
}
// heldAutoScanPauseGuard_ is deliberately left held (never explicitly released) - the host
// restarts itself immediately below, and there's no safe window to resume normal WiFi
// activity before that.
{
char buf[80];
if (canActivate) {
snprintf(buf, sizeof(buf), "Firmware %s activated - restarting...", versionStr.c_str());
} else {
snprintf(buf, sizeof(buf), "Firmware %s pushed - restarting to apply...", versionStr.c_str());
}
dispatchToUi(workSetStatus, new std::string(buf), freeString);
}
// Give the status message above a moment to actually be seen before the restart cuts the
// display, then restart.
vTaskDelay(pdMS_TO_TICKS(1500));
esp_restart();
}
void EspNowBridge::updateTaskEntry(void* arg) {
auto* self = static_cast<EspNowBridge*>(arg);
self->performUpdate(self->pendingUpdateFilePath_);
self->updateTask_ = nullptr;
if (self->outstandingTasks_.fetch_sub(1) == 1 && self->taskDoneSemaphore_ != nullptr) {
xSemaphoreGive(self->taskDoneSemaphore_);
}
vTaskDelete(nullptr);
}
void EspNowBridge::startUpdateTask(const std::string& filePath) {
if (updateTask_ != nullptr) {
return;
}
pendingUpdateFilePath_ = filePath;
outstandingTasks_.fetch_add(1);
if (xTaskCreate(updateTaskEntry, "espnow_bridge_ota", UPDATE_TASK_STACK_SIZE / sizeof(StackType_t), this, tskIDLE_PRIORITY + 1, &updateTask_) != pdPASS) {
outstandingTasks_.fetch_sub(1);
}
}
void EspNowBridge::onUpdateButtonClicked(lv_event_t* /*event*/) {
auto* self = liveInstance_.load();
if (self == nullptr || !self->isWifiRadioOn()) {
return;
}
self->pickFileLaunchId_ = tt_app_fileselection_start_for_existing_file();
}
// Name of the slave bridge firmware bundled in this app's assets/ folder
// lets users flash the known-good bridge firmware without needing to source/copy a
// .bin onto the SD card themselves. The SD-card picker (onUpdateButtonClicked above) stays
// available too, for factory-image downgrades or custom builds.
static constexpr auto* BUNDLED_FIRMWARE_ASSET_NAME = "espnow_bridge_slave_c6.bin";
void EspNowBridge::onUpdateBundledButtonClicked(lv_event_t* /*event*/) {
auto* self = liveInstance_.load();
if (self == nullptr || !self->isWifiRadioOn()) {
return;
}
char assetPath[256] = {};
size_t assetPathSize = sizeof(assetPath);
tt_app_get_assets_child_path(self->appHandle_, BUNDLED_FIRMWARE_ASSET_NAME, assetPath, &assetPathSize);
if (assetPath[0] == '\0') {
LOG_E(TAG, "Failed to resolve bundled firmware asset path");
return;
}
self->startUpdateTask(assetPath);
}
void EspNowBridge::onEnableWifiButtonClicked(lv_event_t* /*event*/) {
auto* self = liveInstance_.load();
if (self == nullptr || self->wifiDevice_ == nullptr) {
return;
}
device_start(self->wifiDevice_);
// start_device() allocates a fresh driver context (Platforms/platform-esp32's
// esp32_wifi.cpp), which wipes any event callback registered before the device was started -
// re-register now that it's actually running. Also refresh once directly rather than relying
// solely on the next WifiEvent, so the "WiFi on" prompt updates immediately even though the
// co-processor firmware version below isn't available yet.
wifi_add_event_callback(self->wifiDevice_, self, onWifiEvent);
self->refreshWifiPrompt();
self->refreshCurrentVersion();
// The co-processor RPC transport isn't up the instant device_start() returns - it comes up
// asynchronously (~1-2s later) - so firmwareOps_->get_info() above reliably fails right after
// enabling WiFi. Nothing else reliably re-triggers a version refresh once the transport
// actually comes up (WifiEvent only covers radio/station state, not transport readiness), so
// wait for it explicitly on a background task and refresh once it's ready.
if (self->firmwareOps_ != nullptr) {
self->outstandingTasks_.fetch_add(1);
if (xTaskCreate(waitForTransportTaskEntry, "espnow_bridge_wait", 4096 / sizeof(StackType_t), self, tskIDLE_PRIORITY + 1, nullptr) != pdPASS) {
self->outstandingTasks_.fetch_sub(1);
}
}
}
void EspNowBridge::waitForTransportTaskEntry(void* arg) {
auto* self = static_cast<EspNowBridge*>(arg);
constexpr uint32_t WAIT_TIMEOUT_MS = 10000;
// liveInstance_ must be checked before touching any member of self - if onDestroy() already
// ran, `self` may be freed, and dereferencing self->firmwareOps_ first would be a
// use-after-free even just to read the pointer.
if (liveInstance_.load() == self && self->firmwareOps_ != nullptr
&& self->firmwareOps_->wait_ready(self->firmwareCtx_, WAIT_TIMEOUT_MS)
&& liveInstance_.load() == self) {
self->dispatchToUi([](EspNowBridge& app, void*) {
app.refreshCurrentVersion();
}, nullptr, nullptr);
}
if (self->outstandingTasks_.fetch_sub(1) == 1 && self->taskDoneSemaphore_ != nullptr) {
xSemaphoreGive(self->taskDoneSemaphore_);
}
vTaskDelete(nullptr);
}
void EspNowBridge::onWifiEvent(Device* /*device*/, void* callbackContext, WifiEvent /*event*/) {
auto* self = static_cast<EspNowBridge*>(callbackContext);
if (liveInstance_.load() != self) {
return;
}
self->dispatchToUi([](EspNowBridge& app, void*) {
app.refreshWifiPrompt();
app.refreshCurrentVersion();
}, nullptr, nullptr);
}
void EspNowBridge::onShow(AppHandle app, lv_obj_t* parent) {
isShown_ = true;
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "ESP-NOW Bridge");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
auto* wrapper = lv_obj_create(parent);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(wrapper, 8, LV_STATE_DEFAULT);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
currentVersionLabel_ = lv_label_create(wrapper);
lv_obj_set_style_pad_bottom(currentVersionLabel_, 12, LV_STATE_DEFAULT);
enableWifiButton_ = lv_button_create(wrapper);
lv_obj_add_event_cb(enableWifiButton_, onEnableWifiButtonClicked, LV_EVENT_CLICKED, nullptr);
auto* enableWifiButtonLabel = lv_label_create(enableWifiButton_);
lv_label_set_text(enableWifiButtonLabel, "Enable WiFi (required for co-processor link)");
lv_obj_set_style_pad_bottom(enableWifiButton_, 12, LV_STATE_DEFAULT);
updateBundledButton_ = lv_button_create(wrapper);
lv_obj_add_event_cb(updateBundledButton_, onUpdateBundledButtonClicked, LV_EVENT_CLICKED, nullptr);
auto* updateBundledButtonLabel = lv_label_create(updateBundledButton_);
lv_label_set_text(updateBundledButtonLabel, "Update to bundled firmware");
lv_obj_set_style_pad_bottom(updateBundledButton_, 12, LV_STATE_DEFAULT);
updateButton_ = lv_button_create(wrapper);
lv_obj_add_event_cb(updateButton_, onUpdateButtonClicked, LV_EVENT_CLICKED, nullptr);
auto* updateButtonLabel = lv_label_create(updateButton_);
lv_label_set_text(updateButtonLabel, "Update from SD card...");
lv_obj_set_style_pad_bottom(updateButton_, 12, LV_STATE_DEFAULT);
progressBar_ = lv_bar_create(wrapper);
lv_obj_set_size(progressBar_, LV_PCT(100), LV_PCT(6));
lv_bar_set_range(progressBar_, 0, 100);
lv_bar_set_value(progressBar_, 0, LV_ANIM_OFF);
statusLabel_ = lv_label_create(wrapper);
lv_label_set_text(statusLabel_, "Ready");
wifiDevice_ = wifi_find_first_registered_device();
if (wifiDevice_ != nullptr) {
wifi_add_event_callback(wifiDevice_, this, onWifiEvent);
if (wifi_get_firmware_ops(wifiDevice_, &firmwareOps_, &firmwareCtx_) != ERROR_NONE) {
firmwareOps_ = nullptr;
firmwareCtx_ = nullptr;
}
}
refreshCurrentVersion();
refreshWifiPrompt();
// If an SD-card file was picked before this onShow() ran (FileSelection tears down and
// rebuilds this app's whole widget tree), perform the update now that widgets are valid
// again. The bundled-firmware button doesn't go through this path - it calls
// startUpdateTask() directly since there's no separate app launch/result round trip involved.
if (!pendingUpdateFilePath_.empty()) {
std::string path = std::move(pendingUpdateFilePath_);
pendingUpdateFilePath_.clear();
startUpdateTask(path);
}
}
void EspNowBridge::onHide(AppHandle /*app*/) {
isShown_ = false;
if (wifiDevice_ != nullptr) {
wifi_remove_event_callback(wifiDevice_, onWifiEvent);
wifiDevice_ = nullptr;
}
}
void EspNowBridge::onResult(AppHandle /*app*/, void* /*data*/, AppLaunchId launchId, AppResult result, BundleHandle resultData) {
if (launchId != pickFileLaunchId_) {
return;
}
pickFileLaunchId_ = 0;
if (result == APP_RESULT_OK && resultData != nullptr) {
char pathBuf[256] = {};
if (tt_app_fileselection_get_result_path(resultData, pathBuf, sizeof(pathBuf))) {
pendingUpdateFilePath_ = pathBuf;
}
}
}
@@ -0,0 +1,108 @@
#pragma once
#include <TactilityCpp/App.h>
#include <atomic>
#include <optional>
#include <string>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <lvgl.h>
#include <tactility/drivers/wifi.h>
/** RAII guard: pauses WifiService's background auto-connect scan for the guard's lifetime. See
* tactility/wifi_auto_scan.h - belt-and-suspenders measure, not sufficient on its own (see the
* REBOOT comment in EspNowBridge.cpp). */
class AutoScanPauseGuard {
public:
AutoScanPauseGuard();
~AutoScanPauseGuard();
AutoScanPauseGuard(const AutoScanPauseGuard&) = delete;
AutoScanPauseGuard& operator=(const AutoScanPauseGuard&) = delete;
};
class EspNowBridge final : public App {
public:
EspNowBridge() = default;
EspNowBridge(const EspNowBridge&) = delete;
EspNowBridge& operator=(const EspNowBridge&) = delete;
void onCreate(AppHandle app) override;
void onDestroy(AppHandle app) override;
void onShow(AppHandle app, lv_obj_t* parent) override;
void onHide(AppHandle app) override;
void onResult(AppHandle app, void* data, AppLaunchId launchId, AppResult result, BundleHandle resultData) override;
// Public so the free-function dispatchToUi() work callbacks in EspNowBridge.cpp (which run
// outside any member-function's lexical scope, unlike the inline lambdas in performUpdate())
// can call them.
void setStatus(const std::string& text);
void setProgress(int percent);
private:
AppHandle appHandle_ = nullptr;
AppLaunchId pickFileLaunchId_ = 0;
std::string pendingUpdateFilePath_;
Device* wifiDevice_ = nullptr;
// Resolved once in onShow() via wifi_get_firmware_ops() - null on a WiFi device with no
// updatable co-processor (e.g. a native, non-hosted chip). All OTA/version-query calls go
// through this generic interface, not any esp_hosted-specific API directly.
const FirmwareOps* firmwareOps_ = nullptr;
void* firmwareCtx_ = nullptr;
// Set once in onShow(), false once onHide() tears the widget tree down - checked (via
// dispatchToUi(), below) before touching any lv_obj_t*, since the OTA worker task and the
// WiFi-event callback can both outlive a hide/app-switch.
std::atomic<bool> isShown_{false};
// Only one EspNowBridge instance is ever live at a time (app loader owns a single instance
// per running app), so a single static "is this instance still current" pointer, guarded by
// an atomic, substitutes for the internal app's shared_ptr-based lifetime guard - the OTA
// worker task and dispatchToUi()'s lv_async_call closures check liveInstance_ == this before
// touching any member, instead of holding a shared_ptr to keep `this` alive.
static std::atomic<EspNowBridge*> liveInstance_;
TaskHandle_t updateTask_ = nullptr;
// Number of background tasks (updateTaskEntry, waitForTransportTaskEntry) currently running
// against this instance's members. onDestroy() must wait for this to hit 0 before returning -
// the app framework frees this instance shortly after onDestroy() returns (see Loader.cpp),
// so any task still touching `this` past that point is a use-after-free.
std::atomic<int> outstandingTasks_{0};
SemaphoreHandle_t taskDoneSemaphore_ = nullptr;
// Outlives performUpdate() deliberately, so auto-scan stays paused across the async gap
// between performUpdate() returning and the automatic restart - see performUpdate().
std::optional<AutoScanPauseGuard> heldAutoScanPauseGuard_;
lv_obj_t* currentVersionLabel_ = nullptr;
lv_obj_t* statusLabel_ = nullptr;
lv_obj_t* progressBar_ = nullptr;
lv_obj_t* updateButton_ = nullptr;
lv_obj_t* updateBundledButton_ = nullptr;
lv_obj_t* enableWifiButton_ = nullptr;
void refreshCurrentVersion();
bool isWifiRadioOn();
void refreshWifiPrompt();
/** Enables/disables both update-trigger buttons together - only one performUpdate() can run
* at a time (see updateTask_), regardless of which button started it. */
void setUpdateButtonsDisabled(bool disabled);
/** Marshal a UI-touching closure onto the LVGL task. Only ever invoked if liveInstance_ is
* still this instance (checked at dispatch time and again right before running, on the LVGL
* task) and isShown_ is true (this app's widget tree exists). */
void dispatchToUi(void (*work)(EspNowBridge&, void*), void* context, void (*freeContext)(void*));
void performUpdate(const std::string& filePath);
void startUpdateTask(const std::string& filePath);
static void updateTaskEntry(void* arg);
static void onUpdateButtonClicked(lv_event_t* event);
static void onUpdateBundledButtonClicked(lv_event_t* event);
static void onEnableWifiButtonClicked(lv_event_t* event);
static void onWifiEvent(Device* device, void* callbackContext, WifiEvent event);
static void waitForTransportTaskEntry(void* arg);
};
+11
View File
@@ -0,0 +1,11 @@
#include "EspNowBridge.h"
#include <TactilityCpp/App.h>
extern "C" {
int main(int argc, char* argv[]) {
registerApp<EspNowBridge>();
return 0;
}
}
+8
View File
@@ -0,0 +1,8 @@
manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32p4
app.id=one.tactility.espnowbridge
app.version.name=0.3.0
app.version.code=3
app.name=ESP-NOW Bridge
app.description=Companion app for updating P4 device C6 co-processor firmware to enable ESP-NOW bridge support.
+5 -6
View File
@@ -2,10 +2,9 @@
#include <Tactility/kernel/Kernel.h> #include <Tactility/kernel/Kernel.h>
#include <tt_lvgl.h> #include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
#include <tactility/lvgl_module.h> #include <lvgl/lvgl.h>
#include <esp_log.h> #include <esp_log.h>
#include <driver/gpio.h> #include <driver/gpio.h>
@@ -20,7 +19,7 @@ void Gpio::updatePinStates() {
} }
void Gpio::updatePinWidgets() { void Gpio::updatePinWidgets() {
tt_lvgl_lock(tt::kernel::MAX_TICKS); lvgl_lock();
for (int j = 0; j < pinStates.size(); ++j) { for (int j = 0; j < pinStates.size(); ++j) {
int level = pinStates[j]; int level = pinStates[j];
lv_obj_t* label = pinWidgets[j]; lv_obj_t* label = pinWidgets[j];
@@ -35,7 +34,7 @@ void Gpio::updatePinWidgets() {
} }
} }
} }
tt_lvgl_unlock(); lvgl_unlock();
} }
lv_obj_t* Gpio::createGpioRowWrapper(lv_obj_t* parent) { lv_obj_t* Gpio::createGpioRowWrapper(lv_obj_t* parent) {
@@ -79,7 +78,7 @@ void Gpio::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = tt_lvgl_toolbar_create_for_app(parent, app); auto* toolbar = lvgl_toolbar_create(parent, "GPIO");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
// Main content wrapper, enables scrolling content without scrolling the toolbar // Main content wrapper, enables scrolling content without scrolling the toolbar
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.gpio
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.9.0
[app] app.version.code=9
id=one.tactility.gpio app.name=GPIO
versionName=0.4.0
versionCode=4
name=GPIO
+16 -16
View File
@@ -4,17 +4,17 @@
#include "drivers/Colors.h" #include "drivers/Colors.h"
#include <cstring> #include <cstring>
#include <tt_hal_display.h> #include <tactility/drivers/display.h>
class PixelBuffer { class PixelBuffer {
uint16_t pixelWidth; uint16_t pixelWidth;
uint16_t pixelHeight; uint16_t pixelHeight;
ColorFormat colorFormat; enum DisplayColorFormat colorFormat;
uint8_t* data; uint8_t* data;
public: public:
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, ColorFormat colorFormat) : PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, enum DisplayColorFormat colorFormat) :
pixelWidth(pixelWidth), pixelWidth(pixelWidth),
pixelHeight(pixelHeight), pixelHeight(pixelHeight),
colorFormat(colorFormat) colorFormat(colorFormat)
@@ -35,7 +35,7 @@ public:
return pixelHeight; return pixelHeight;
} }
ColorFormat getColorFormat() const { enum DisplayColorFormat getColorFormat() const {
return colorFormat; return colorFormat;
} }
@@ -58,14 +58,14 @@ public:
uint8_t getPixelSize() const { uint8_t getPixelSize() const {
switch (colorFormat) { switch (colorFormat) {
case COLOR_FORMAT_MONOCHROME: case DISPLAY_COLOR_FORMAT_MONOCHROME:
return 1; return 1;
case COLOR_FORMAT_BGR565: case DISPLAY_COLOR_FORMAT_BGR565:
case COLOR_FORMAT_BGR565_SWAPPED: case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
case COLOR_FORMAT_RGB565: case DISPLAY_COLOR_FORMAT_RGB565:
case COLOR_FORMAT_RGB565_SWAPPED: case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
return 2; return 2;
case COLOR_FORMAT_RGB888: case DISPLAY_COLOR_FORMAT_RGB888:
return 3; return 3;
default: default:
// TODO: Crash with error // TODO: Crash with error
@@ -82,13 +82,13 @@ public:
void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const { void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const {
auto address = getPixelAddress(x, y); auto address = getPixelAddress(x, y);
switch (colorFormat) { switch (colorFormat) {
case COLOR_FORMAT_MONOCHROME: case DISPLAY_COLOR_FORMAT_MONOCHROME:
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3); *address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
break; break;
case COLOR_FORMAT_BGR565: case DISPLAY_COLOR_FORMAT_BGR565:
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address)); Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
break; break;
case COLOR_FORMAT_BGR565_SWAPPED: { case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: {
// TODO: Make proper conversion function // TODO: Make proper conversion function
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address)); Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address; uint8_t temp = *address;
@@ -96,11 +96,11 @@ public:
*(address + 1) = temp; *(address + 1) = temp;
break; break;
} }
case COLOR_FORMAT_RGB565: { case DISPLAY_COLOR_FORMAT_RGB565: {
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address)); Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
break; break;
} }
case COLOR_FORMAT_RGB565_SWAPPED: { case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: {
// TODO: Make proper conversion function // TODO: Make proper conversion function
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address)); Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address; uint8_t temp = *address;
@@ -108,7 +108,7 @@ public:
*(address + 1) = temp; *(address + 1) = temp;
break; break;
} }
case COLOR_FORMAT_RGB888: { case DISPLAY_COLOR_FORMAT_RGB888: {
uint8_t pixel[3] = { r, g, b }; uint8_t pixel[3] = { r, g, b };
memcpy(address, pixel, 3); memcpy(address, pixel, 3);
break; break;
@@ -1,49 +1,47 @@
#pragma once #pragma once
#include <cassert> #include <tactility/device.h>
#include <tt_hal_display.h> #include <tactility/drivers/display.h>
#include <Tactility/kernel/Kernel.h> #include <Tactility/kernel/Kernel.h>
/** /**
* Wrapper for tt_hal_display_driver_* * Wrapper for display_* device driver functions
*/ */
class DisplayDriver { class DisplayDriver {
DisplayDriverHandle handle = nullptr; struct Device* device;
public: public:
explicit DisplayDriver(DeviceId id) { explicit DisplayDriver(struct Device* device) : device(device) {
assert(tt_hal_display_driver_supported(id)); device_get(device);
handle = tt_hal_display_driver_alloc(id);
assert(handle != nullptr);
} }
~DisplayDriver() { ~DisplayDriver() {
tt_hal_display_driver_free(handle); device_put(device);
} }
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const { bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const {
return tt_hal_display_driver_lock(handle, timeout); return device_try_lock(device, timeout);
} }
void unlock() const { void unlock() const {
tt_hal_display_driver_unlock(handle); device_unlock(device);
} }
uint16_t getWidth() const { uint16_t getWidth() const {
return tt_hal_display_driver_get_pixel_width(handle); return display_get_resolution_x(device);
} }
uint16_t getHeight() const { uint16_t getHeight() const {
return tt_hal_display_driver_get_pixel_height(handle); return display_get_resolution_y(device);
} }
ColorFormat getColorFormat() const { enum DisplayColorFormat getColorFormat() const {
return tt_hal_display_driver_get_colorformat(handle); return display_get_color_format(device);
} }
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const { void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
tt_hal_display_driver_draw_bitmap(handle, xStart, yStart, xEnd, yEnd, pixelData); display_draw_bitmap(device, xStart, yStart, xEnd, yEnd, pixelData);
} }
}; };
@@ -1,28 +1,28 @@
#pragma once #pragma once
#include <cassert> #include <tactility/device.h>
#include <tt_hal_touch.h> #include <tactility/drivers/pointer.h>
/** /**
* Wrapper for tt_hal_touch_driver_* * Wrapper for pointer_* device driver functions
*/ */
class TouchDriver { class TouchDriver {
TouchDriverHandle handle = nullptr; struct Device* device;
public: public:
explicit TouchDriver(DeviceId id) { explicit TouchDriver(struct Device* device) : device(device) {
assert(tt_hal_touch_driver_supported(id)); device_get(device);
handle = tt_hal_touch_driver_alloc(id);
assert(handle != nullptr);
} }
~TouchDriver() { ~TouchDriver() {
tt_hal_touch_driver_free(handle); device_put(device);
} }
bool getTouchedPoints(uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* count, uint8_t maxCount) const { bool getTouchedPoints(uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* count, uint8_t maxCount) const {
return tt_hal_touch_driver_get_touched_points(handle, x, y, strength, count, maxCount); // Poll without blocking: perform one read attempt, then report whatever is cached.
pointer_read_data(device, 0);
return pointer_get_touched_points(device, x, y, strength, count, maxCount);
} }
}; };
+23 -44
View File
@@ -6,65 +6,44 @@
#include <tt_app.h> #include <tt_app.h>
#include <tt_app_alertdialog.h> #include <tt_app_alertdialog.h>
#include <tt_lvgl.h>
#include <tactility/device.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/pointer.h>
#include <lvgl/lvgl.h>
#include <lvgl/module.h>
#include <tactility/module.h>
constexpr auto TAG = "Main"; constexpr auto TAG = "Main";
/** Find a DisplayDevice that supports the DisplayDriver interface */
static bool findUsableDisplay(DeviceId& deviceId) {
uint16_t display_count = 0;
if (!tt_hal_device_find(DEVICE_TYPE_DISPLAY, &deviceId, &display_count, 1)) {
ESP_LOGE(TAG, "No display device found");
return false;
}
if (!tt_hal_display_driver_supported(deviceId)) {
ESP_LOGE(TAG, "Display doesn't support driver mode");
return false;
}
return true;
}
/** Find a TouchDevice that supports the TouchDriver interface */
static bool findUsableTouch(DeviceId& deviceId) {
uint16_t touch_count = 0;
if (!tt_hal_device_find(DEVICE_TYPE_TOUCH, &deviceId, &touch_count, 1)) {
ESP_LOGE(TAG, "No touch device found");
return false;
}
if (!tt_hal_touch_driver_supported(deviceId)) {
ESP_LOGE(TAG, "Touch doesn't support driver mode");
return false;
}
return true;
}
static void onCreate(AppHandle appHandle, void* data) { static void onCreate(AppHandle appHandle, void* data) {
DeviceId display_id; struct Device* display_device;
if (!findUsableDisplay(display_id)) { if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) {
ESP_LOGE(TAG, "No display device found");
tt_app_stop(); tt_app_stop();
tt_app_alertdialog_start("Error", "The display doesn't support the required features.", nullptr, 0); tt_app_alertdialog_start("Error", "No display device was found.", nullptr, 0);
return; return;
} }
DeviceId touch_id; struct Device* touch_device;
if (!findUsableTouch(touch_id)) { if (device_get_first_active_by_type(&POINTER_TYPE, &touch_device) != ERROR_NONE) {
ESP_LOGE(TAG, "No touch device found");
device_put(display_device);
tt_app_stop(); tt_app_stop();
tt_app_alertdialog_start("Error", "The touch driver doesn't support the required features.", nullptr, 0); tt_app_alertdialog_start("Error", "No touch device was found.", nullptr, 0);
return; return;
} }
// Stop LVGL first (because it's currently using the drivers we want to use) // Stop LVGL first (because it's currently using the drivers we want to use)
tt_lvgl_stop(); module_stop(&lvgl_module);
ESP_LOGI(TAG, "Creating display driver"); ESP_LOGI(TAG, "Creating display driver");
auto display = new DisplayDriver(display_id); auto display = new DisplayDriver(display_device);
device_put(display_device);
ESP_LOGI(TAG, "Creating touch driver"); ESP_LOGI(TAG, "Creating touch driver");
auto touch = new TouchDriver(touch_id); auto touch = new TouchDriver(touch_device);
device_put(touch_device);
// Run the main logic // Run the main logic
ESP_LOGI(TAG, "Running application"); ESP_LOGI(TAG, "Running application");
@@ -82,9 +61,9 @@ static void onCreate(AppHandle appHandle, void* data) {
static void onDestroy(AppHandle appHandle, void* data) { static void onDestroy(AppHandle appHandle, void* data) {
// Restart LVGL to resume rendering of regular apps // Restart LVGL to resume rendering of regular apps
if (!tt_lvgl_is_started()) { if (!module_is_started(&lvgl_module)) {
ESP_LOGI(TAG, "Restarting LVGL"); ESP_LOGI(TAG, "Restarting LVGL");
tt_lvgl_start(); module_start(&lvgl_module);
} }
} }
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.graphicsdemo
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.7.0
[app] app.version.code=7
id=one.tactility.graphicsdemo app.name=Graphics Demo
versionName=0.3.0
versionCode=3
name=Graphics Demo
+2 -2
View File
@@ -1,12 +1,12 @@
#include <tt_app.h> #include <tt_app.h>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
/** /**
* Note: LVGL and Tactility methods need to be exposed manually from TactilityC/Source/tt_init.cpp * Note: LVGL and Tactility methods need to be exposed manually from TactilityC/Source/tt_init.cpp
* Only C is supported for now (C++ symbols fail to link) * Only C is supported for now (C++ symbols fail to link)
*/ */
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) { static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Hello World");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* label = lv_label_create(parent); lv_obj_t* label = lv_label_create(parent);
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.helloworld
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.7.0
[app] app.version.code=7
id=one.tactility.helloworld app.name=Hello World
versionName=0.3.0
versionCode=3
name=Hello World
@@ -14,7 +14,6 @@
#include "TestUnitLcdGfx.h" #include "TestUnitLcdGfx.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tt_lvgl_toolbar.h>
#include <esp_log.h> #include <esp_log.h>
constexpr auto* TAG = "M5UnitTest"; constexpr auto* TAG = "M5UnitTest";
+3 -3
View File
@@ -1,13 +1,13 @@
#include "TestListView.h" #include "TestListView.h"
#include "M5UnitTest.h" #include "M5UnitTest.h"
#include "UiScale.h" #include "UiScale.h"
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
void TestListView::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) { void TestListView::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app; app_ = app;
tt_lvgl_toolbar_create_for_app(parent, handle); lvgl_toolbar_create(parent, "M5 Unit Test");
list_ = lv_list_create(parent); list_ = lv_list_create(parent);
lv_obj_set_width(list_, LV_PCT(100)); lv_obj_set_width(list_, LV_PCT(100));
+1 -1
View File
@@ -2,7 +2,7 @@
#include <array> #include <array>
#include <lvgl.h> #include <lvgl.h>
#include <tactility/lvgl_icon_shared.h> #include <lvgl/icons/shared.h>
#include <tt_app.h> #include <tt_app.h>
class M5UnitTest; class M5UnitTest;
@@ -2,7 +2,7 @@
#include "GroveLookup.h" #include "GroveLookup.h"
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
#include <cstring> #include <cstring>
@@ -2,7 +2,7 @@
#include "GroveLookup.h" #include "GroveLookup.h"
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
#include <cstring> #include <cstring>
void TestUnitByteButton::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) { void TestUnitByteButton::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
@@ -3,7 +3,7 @@
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/uart_controller.h> #include <tactility/drivers/uart_controller.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
#include <cstring> #include <cstring>
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
#include "TestUnitDualButton.h" #include "TestUnitDualButton.h"
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
static constexpr gpio_pin_t PIN_MIN = 0; static constexpr gpio_pin_t PIN_MIN = 0;
static constexpr gpio_pin_t PIN_MAX = 57; static constexpr gpio_pin_t PIN_MAX = 57;
@@ -2,7 +2,7 @@
#include "GroveLookup.h" #include "GroveLookup.h"
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
+1 -1
View File
@@ -2,7 +2,7 @@
#include "GroveLookup.h" #include "GroveLookup.h"
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
void TestUnitLcd::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) { void TestUnitLcd::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app; app_ = app;
@@ -2,7 +2,7 @@
#include "GroveLookup.h" #include "GroveLookup.h"
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
#include <esp_timer.h> #include <esp_timer.h>
#include <cstring> #include <cstring>
#include <cstdio> #include <cstdio>
+1 -1
View File
@@ -2,7 +2,7 @@
#include "GroveLookup.h" #include "GroveLookup.h"
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
void TestUnitMidi::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) { void TestUnitMidi::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app; app_ = app;
@@ -3,7 +3,7 @@
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h> #include <tactility/drivers/i2c_controller.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
void TestUnitPaHub::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) { void TestUnitPaHub::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app; app_ = app;
@@ -2,8 +2,7 @@
#include "GroveLookup.h" #include "GroveLookup.h"
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
#include <tt_lvgl_toolbar.h>
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
@@ -2,7 +2,7 @@
#include "GroveLookup.h" #include "GroveLookup.h"
#include "UiScale.h" #include "UiScale.h"
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
void TestUnitScroll::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) { void TestUnitScroll::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app; app_ = app;
+4 -5
View File
@@ -1,13 +1,12 @@
#include "TestViewBase.h" #include "TestViewBase.h"
#include "M5UnitTest.h" #include "M5UnitTest.h"
#include "UiScale.h" #include "UiScale.h"
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
lv_obj_t* TestViewBase::createToolbar(lv_obj_t* parent, AppHandle handle, const char* title) { lv_obj_t* TestViewBase::createToolbar(lv_obj_t* parent, AppHandle handle, const char* title) {
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, handle); lv_obj_t* toolbar = lvgl_toolbar_create(parent, title);
tt_lvgl_toolbar_set_title(toolbar, title); lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LEFT, onBackClicked, this);
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LEFT, onBackClicked, this);
return toolbar; return toolbar;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <lvgl.h> #include <lvgl.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
// Device screen widths in default (portrait) orientation: // Device screen widths in default (portrait) orientation:
// tiny < 200 : small OLEDs, custom breadboard devices // tiny < 200 : small OLEDs, custom breadboard devices
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32s3,esp32p4
sdk=0.7.0-dev app.id=one.tactility.m5unittest
platforms=esp32s3,esp32p4 app.version.name=0.5.0
[app] app.version.code=5
id=one.tactility.m5unittest app.name=M5 Unit Test
versionName=0.1.0
versionCode=1
name=M5 Unit Test
+7 -6
View File
@@ -1,6 +1,7 @@
#include "Magic8Ball.h" #include "Magic8Ball.h"
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_keyboard.h> #include <tactility/device.h>
#include <tactility/drivers/keyboard.h>
#include <stdlib.h> #include <stdlib.h>
#include <time.h> #include <time.h>
@@ -35,7 +36,7 @@ static const char* responses[] = {
#define NUM_RESPONSES (sizeof(responses) / sizeof(responses[0])) #define NUM_RESPONSES (sizeof(responses) / sizeof(responses[0]))
static const char* getInputHint() { static const char* getInputHint() {
if (tt_lvgl_hardware_keyboard_is_available()) { if (device_has_active_by_type(&KEYBOARD_TYPE)) {
return "Touch or Space to ask Q to exit"; return "Touch or Space to ask Q to exit";
} }
return "Touch the ball to ask"; return "Touch the ball to ask";
@@ -92,7 +93,7 @@ void Magic8Ball::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
/* Toolbar */ /* Toolbar */
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Magic 8-Ball");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
/* Main container */ /* Main container */
@@ -141,7 +142,7 @@ void Magic8Ball::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_add_event_cb(ballObj, onBallClick, LV_EVENT_CLICKED, this); lv_obj_add_event_cb(ballObj, onBallClick, LV_EVENT_CLICKED, this);
/* Keyboard support - no editing mode needed, just focus the ball */ /* Keyboard support - no editing mode needed, just focus the ball */
if (tt_lvgl_hardware_keyboard_is_available()) { if (device_has_active_by_type(&KEYBOARD_TYPE)) {
lv_group_t* grp = lv_group_get_default(); lv_group_t* grp = lv_group_get_default();
if (grp) { if (grp) {
lv_group_add_obj(grp, ballObj); lv_group_add_obj(grp, ballObj);
@@ -152,7 +153,7 @@ void Magic8Ball::onShow(AppHandle app, lv_obj_t* parent) {
} }
void Magic8Ball::onHide(AppHandle app) { void Magic8Ball::onHide(AppHandle app) {
if (tt_lvgl_hardware_keyboard_is_available() && ballObj) { if (device_has_active_by_type(&KEYBOARD_TYPE) && ballObj) {
lv_group_remove_obj(ballObj); lv_group_remove_obj(ballObj);
} }
answerLabel = nullptr; answerLabel = nullptr;
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.magic8ball
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.6.0
[app] app.version.code=6
id=one.tactility.magic8ball app.name=Magic 8-Ball
versionName=0.2.0
versionCode=2
name=Magic 8-Ball
+60 -28
View File
@@ -4,9 +4,9 @@
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
#include <tt_lvgl.h> #include <lvgl/lvgl.h>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
static const char* TAG = "MediaKeys"; static const char* TAG = "MediaKeys";
@@ -154,24 +154,24 @@ void MediaKeys::btEventCallback(struct Device* /*device*/, void* context, struct
if (event.radio_state == BT_RADIO_STATE_ON) { if (event.radio_state == BT_RADIO_STATE_ON) {
// Radio is now up - start HID (needs LVGL lock for UI update) // Radio is now up - start HID (needs LVGL lock for UI update)
if (tt_lvgl_lock(1000)) { if (lvgl_try_lock(1000)) {
// Re-check inside lock to avoid TOCTOU race with handleSwitchToggle(false) // Re-check inside lock to avoid TOCTOU race with handleSwitchToggle(false)
if (self->_radioEnabling) { if (self->_radioEnabling) {
self->startHid(); self->startHid();
} }
tt_lvgl_unlock(); lvgl_unlock();
} }
} else if (event.radio_state == BT_RADIO_STATE_OFF && self->_isEnabled) { } else if (event.radio_state == BT_RADIO_STATE_OFF && self->_isEnabled) {
// Radio dropped while we were active - revert UI // Radio dropped while we were active - revert UI
LOG_I(TAG, "BT radio turned off, disabling HID"); LOG_I(TAG, "BT radio turned off, disabling HID");
if (tt_lvgl_lock(1000)) { if (lvgl_try_lock(1000)) {
if (tt_lvgl_hardware_keyboard_is_available()) self->exitKeyMode(); if (device_has_active_by_type(&KEYBOARD_TYPE)) self->exitKeyMode();
self->_hidDevice = nullptr; self->_hidDevice = nullptr;
self->_isEnabled = false; self->_isEnabled = false;
self->_radioEnabling = false; self->_radioEnabling = false;
if (self->_switchWidget) lv_obj_remove_state(self->_switchWidget, LV_STATE_CHECKED); if (self->_switchWidget) lv_obj_remove_state(self->_switchWidget, LV_STATE_CHECKED);
if (self->_mainWrapper) lv_obj_add_flag(self->_mainWrapper, LV_OBJ_FLAG_HIDDEN); if (self->_mainWrapper) lv_obj_add_flag(self->_mainWrapper, LV_OBJ_FLAG_HIDDEN);
tt_lvgl_unlock(); lvgl_unlock();
} }
} }
} else if (event.type == BT_EVENT_PROFILE_STATE_CHANGED && event.profile_state.profile == BT_PROFILE_HID_DEVICE) { } else if (event.type == BT_EVENT_PROFILE_STATE_CHANGED && event.profile_state.profile == BT_PROFILE_HID_DEVICE) {
@@ -208,7 +208,24 @@ void MediaKeys::startHid() {
} }
if (_mainWrapper) lv_obj_remove_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN); if (_mainWrapper) lv_obj_remove_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
if (tt_lvgl_hardware_keyboard_is_available()) enterKeyMode(); if (device_has_active_by_type(&KEYBOARD_TYPE)) enterKeyMode();
}
void MediaKeys::teardownBt() {
// Remove callback FIRST - stops any in-flight BT events from firing against
// our (possibly already freed) UI widget pointers after this returns.
if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback);
// Do NOT call bluetooth_hid_device_stop here: it calls ble_gatts_reset() /
// ble_gatts_start() which corrupts NimBLE heap while the host task is still
// running. HID device is a persistent kernel device; hid_device_start() cleans
// up stale context on next use. Explicit stop is handled by handleSwitchToggle.
// Restore the radio/device to the state we found them in.
if (_btDevice && _radioWasOff) bluetooth_set_radio_enabled(_btDevice, false);
if (_btDevice && _deviceWasStarted) device_stop(_btDevice);
_btDevice = nullptr;
_hidDevice = nullptr;
_radioWasOff = false;
_deviceWasStarted = false;
} }
void MediaKeys::handleSwitchToggle(bool enabled) { void MediaKeys::handleSwitchToggle(bool enabled) {
@@ -216,7 +233,7 @@ void MediaKeys::handleSwitchToggle(bool enabled) {
_isEnabled = enabled; _isEnabled = enabled;
if (enabled) { if (enabled) {
_btDevice = bluetooth_find_first_ready_device(); _btDevice = device_find_first_by_type(&BLUETOOTH_TYPE);
if (!_btDevice) { if (!_btDevice) {
LOG_E(TAG, "No Bluetooth device found"); LOG_E(TAG, "No Bluetooth device found");
_isEnabled = false; _isEnabled = false;
@@ -224,6 +241,19 @@ void MediaKeys::handleSwitchToggle(bool enabled) {
return; return;
} }
// Device may not be started yet (BT disabled in DTS by default to save memory).
if (!device_is_ready(_btDevice)) {
LOG_I(TAG, "BT device not started, starting now");
if (device_start(_btDevice) != ERROR_NONE) {
LOG_E(TAG, "Failed to start BT device");
_btDevice = nullptr;
_isEnabled = false;
if (_switchWidget) lv_obj_remove_state(_switchWidget, LV_STATE_CHECKED);
return;
}
_deviceWasStarted = true;
}
bluetooth_set_device_name(_btDevice, "Tactility Media Keys"); bluetooth_set_device_name(_btDevice, "Tactility Media Keys");
// Register callback before enabling radio so we don't miss the state-change event. // Register callback before enabling radio so we don't miss the state-change event.
@@ -246,13 +276,11 @@ void MediaKeys::handleSwitchToggle(bool enabled) {
} }
} else { } else {
_radioEnabling = false; _radioEnabling = false;
if (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode(); if (device_has_active_by_type(&KEYBOARD_TYPE)) exitKeyMode();
// Explicit user toggle-off: stop HID cleanly (safe here since we're on the
// LVGL task and the user intentionally disabled, so no race with app teardown).
if (_hidDevice) bluetooth_hid_device_stop(_hidDevice); if (_hidDevice) bluetooth_hid_device_stop(_hidDevice);
if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback); teardownBt();
if (_btDevice && _radioWasOff) bluetooth_set_radio_enabled(_btDevice, false);
_radioWasOff = false;
_btDevice = nullptr;
_hidDevice = nullptr;
if (_mainWrapper) lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN); if (_mainWrapper) lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
} }
} }
@@ -279,10 +307,10 @@ void MediaKeys::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Media Keys");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
_switchWidget = tt_lvgl_toolbar_add_switch_action(toolbar); _switchWidget = lvgl_toolbar_add_switch_action(toolbar);
lv_obj_add_event_cb(_switchWidget, onSwitchToggled, LV_EVENT_VALUE_CHANGED, this); lv_obj_add_event_cb(_switchWidget, onSwitchToggled, LV_EVENT_VALUE_CHANGED, this);
_mainWrapper = lv_obj_create(parent); _mainWrapper = lv_obj_create(parent);
@@ -314,26 +342,30 @@ void MediaKeys::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_add_event_cb(_buttonMatrix, onButtonPressed, LV_EVENT_VALUE_CHANGED, this); lv_obj_add_event_cb(_buttonMatrix, onButtonPressed, LV_EVENT_VALUE_CHANGED, this);
// Physical keyboard support: key events on the matrix (entered when BT enabled, Q/Esc exits) // Physical keyboard support: key events on the matrix (entered when BT enabled, Q/Esc exits)
if (tt_lvgl_hardware_keyboard_is_available()) { if (device_has_active_by_type(&KEYBOARD_TYPE)) {
lv_obj_add_event_cb(_buttonMatrix, onKeyEvent, LV_EVENT_KEY, this); lv_obj_add_event_cb(_buttonMatrix, onKeyEvent, LV_EVENT_KEY, this);
_keyHighlightTimer = lv_timer_create(onKeyHighlightTimer, 150, this); _keyHighlightTimer = lv_timer_create(onKeyHighlightTimer, 150, this);
lv_timer_pause(_keyHighlightTimer); lv_timer_pause(_keyHighlightTimer);
} }
lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
// Auto-enable if BT is already on (turned on via QuickPanel/Settings before opening app).
struct Device* btDev = device_find_first_by_type(&BLUETOOTH_TYPE);
if (btDev && device_is_ready(btDev)) {
enum BtRadioState radioState;
if (bluetooth_get_radio_state(btDev, &radioState) == ERROR_NONE && radioState == BT_RADIO_STATE_ON) {
lv_obj_add_state(_switchWidget, LV_STATE_CHECKED);
handleSwitchToggle(true);
}
}
} }
void MediaKeys::onHide(AppHandle /*appHandle*/) { void MediaKeys::onHide(AppHandle /*appHandle*/) {
if (_hidDevice) bluetooth_hid_device_stop(_hidDevice);
if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback);
if (_btDevice && _radioWasOff) bluetooth_set_radio_enabled(_btDevice, false);
_btDevice = nullptr;
_hidDevice = nullptr;
_isEnabled = false;
_radioEnabling = false; _radioEnabling = false;
_radioWasOff = false; _isEnabled = false;
if (device_has_active_by_type(&KEYBOARD_TYPE)) exitKeyMode();
if (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode(); teardownBt();
if (_keyHighlightTimer) { if (_keyHighlightTimer) {
lv_timer_delete(_keyHighlightTimer); lv_timer_delete(_keyHighlightTimer);
_keyHighlightTimer = nullptr; _keyHighlightTimer = nullptr;
+6 -3
View File
@@ -2,10 +2,11 @@
#include <TactilityCpp/App.h> #include <TactilityCpp/App.h>
#include <lvgl.h> #include <lvgl.h>
#include <tactility/device.h>
#include <tactility/drivers/bluetooth.h> #include <tactility/drivers/bluetooth.h>
#include <tactility/drivers/bluetooth_hid_device.h> #include <tactility/drivers/bluetooth_hid_device.h>
#include <tactility/drivers/keyboard.h>
#include <tt_app.h> #include <tt_app.h>
#include <tt_lvgl_keyboard.h>
#include <atomic> #include <atomic>
class MediaKeys final : public App { class MediaKeys final : public App {
@@ -24,8 +25,9 @@ class MediaKeys final : public App {
// State - accessed from both LVGL thread and BT callback thread // State - accessed from both LVGL thread and BT callback thread
std::atomic<bool> _isEnabled {false}; std::atomic<bool> _isEnabled {false};
std::atomic<bool> _radioEnabling{false}; // true while waiting for radio to come ON std::atomic<bool> _radioEnabling {false}; // true while waiting for radio to come ON
std::atomic<bool> _radioWasOff {false}; // true if MediaKeys turned the radio on (so we turn it off) std::atomic<bool> _radioWasOff {false}; // true if we turned the radio on (restore on exit)
std::atomic<bool> _deviceWasStarted{false}; // true if we called device_start (restore on exit)
// Static event callbacks // Static event callbacks
static void onSwitchToggled(lv_event_t* e); static void onSwitchToggled(lv_event_t* e);
@@ -36,6 +38,7 @@ class MediaKeys final : public App {
static void sendKeyTask(void* param); static void sendKeyTask(void* param);
// Instance methods called by static callbacks // Instance methods called by static callbacks
void teardownBt(); // remove callback + stop HID + restore radio/device state
void handleSwitchToggle(bool enabled); void handleSwitchToggle(bool enabled);
void handleButtonPress(uint32_t buttonId); void handleButtonPress(uint32_t buttonId);
void startHid(); // called once radio is confirmed ON void startHid(); // called once radio is confirmed ON
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32s3,esp32p4
sdk=0.7.0-dev app.id=one.tactility.mediakeys
platforms=esp32s3,esp32p4 app.version.name=0.6.0
[app] app.version.code=6
id=one.tactility.mediakeys app.name=Media Keys
versionName=0.1.0 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.
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.
-6
View File
@@ -1,6 +0,0 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
idf_component_register(
SRCS ${SOURCE_FILES}
REQUIRES TactilitySDK
)
-427
View File
@@ -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
-10
View File
@@ -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
+16 -16
View File
@@ -4,17 +4,17 @@
#include "drivers/Colors.h" #include "drivers/Colors.h"
#include <cstring> #include <cstring>
#include <tt_hal_display.h> #include <tactility/drivers/display.h>
class PixelBuffer { class PixelBuffer {
uint16_t pixelWidth; uint16_t pixelWidth;
uint16_t pixelHeight; uint16_t pixelHeight;
ColorFormat colorFormat; enum DisplayColorFormat colorFormat;
uint8_t* data; uint8_t* data;
public: public:
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, ColorFormat colorFormat) : PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, enum DisplayColorFormat colorFormat) :
pixelWidth(pixelWidth), pixelWidth(pixelWidth),
pixelHeight(pixelHeight), pixelHeight(pixelHeight),
colorFormat(colorFormat) colorFormat(colorFormat)
@@ -35,7 +35,7 @@ public:
return pixelHeight; return pixelHeight;
} }
ColorFormat getColorFormat() const { enum DisplayColorFormat getColorFormat() const {
return colorFormat; return colorFormat;
} }
@@ -58,14 +58,14 @@ public:
uint8_t getPixelSize() const { uint8_t getPixelSize() const {
switch (colorFormat) { switch (colorFormat) {
case COLOR_FORMAT_MONOCHROME: case DISPLAY_COLOR_FORMAT_MONOCHROME:
return 1; return 1;
case COLOR_FORMAT_BGR565: case DISPLAY_COLOR_FORMAT_BGR565:
case COLOR_FORMAT_BGR565_SWAPPED: case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
case COLOR_FORMAT_RGB565: case DISPLAY_COLOR_FORMAT_RGB565:
case COLOR_FORMAT_RGB565_SWAPPED: case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
return 2; return 2;
case COLOR_FORMAT_RGB888: case DISPLAY_COLOR_FORMAT_RGB888:
return 3; return 3;
default: default:
// TODO: Crash with error // TODO: Crash with error
@@ -82,13 +82,13 @@ public:
void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const { void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const {
auto address = getPixelAddress(x, y); auto address = getPixelAddress(x, y);
switch (colorFormat) { switch (colorFormat) {
case COLOR_FORMAT_MONOCHROME: case DISPLAY_COLOR_FORMAT_MONOCHROME:
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3); *address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
break; break;
case COLOR_FORMAT_BGR565: case DISPLAY_COLOR_FORMAT_BGR565:
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address)); Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
break; break;
case COLOR_FORMAT_BGR565_SWAPPED: { case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: {
// TODO: Make proper conversion function // TODO: Make proper conversion function
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address)); Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address; uint8_t temp = *address;
@@ -96,11 +96,11 @@ public:
*(address + 1) = temp; *(address + 1) = temp;
break; break;
} }
case COLOR_FORMAT_RGB565: { case DISPLAY_COLOR_FORMAT_RGB565: {
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address)); Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
break; break;
} }
case COLOR_FORMAT_RGB565_SWAPPED: { case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: {
// TODO: Make proper conversion function // TODO: Make proper conversion function
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address)); Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address; uint8_t temp = *address;
@@ -108,7 +108,7 @@ public:
*(address + 1) = temp; *(address + 1) = temp;
break; break;
} }
case COLOR_FORMAT_RGB888: { case DISPLAY_COLOR_FORMAT_RGB888: {
uint8_t pixel[3] = { r, g, b }; uint8_t pixel[3] = { r, g, b };
memcpy(address, pixel, 3); memcpy(address, pixel, 3);
break; break;
@@ -1,49 +1,47 @@
#pragma once #pragma once
#include <cassert> #include <tactility/device.h>
#include <tt_hal_display.h> #include <tactility/drivers/display.h>
#include <Tactility/kernel/Kernel.h> #include <Tactility/kernel/Kernel.h>
/** /**
* Wrapper for tt_hal_display_driver_* * Wrapper for display_* device driver functions
*/ */
class DisplayDriver { class DisplayDriver {
DisplayDriverHandle handle = nullptr; struct Device* device;
public: public:
explicit DisplayDriver(DeviceId id) { explicit DisplayDriver(struct Device* device) : device(device) {
assert(tt_hal_display_driver_supported(id)); device_get(device);
handle = tt_hal_display_driver_alloc(id);
assert(handle != nullptr);
} }
~DisplayDriver() { ~DisplayDriver() {
tt_hal_display_driver_free(handle); device_put(device);
} }
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const { bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const {
return tt_hal_display_driver_lock(handle, timeout); return device_try_lock(device, timeout);
} }
void unlock() const { void unlock() const {
tt_hal_display_driver_unlock(handle); device_unlock(device);
} }
uint16_t getWidth() const { uint16_t getWidth() const {
return tt_hal_display_driver_get_pixel_width(handle); return display_get_resolution_x(device);
} }
uint16_t getHeight() const { uint16_t getHeight() const {
return tt_hal_display_driver_get_pixel_height(handle); return display_get_resolution_y(device);
} }
ColorFormat getColorFormat() const { enum DisplayColorFormat getColorFormat() const {
return tt_hal_display_driver_get_colorformat(handle); return display_get_color_format(device);
} }
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const { void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
tt_hal_display_driver_draw_bitmap(handle, xStart, yStart, xEnd, yEnd, pixelData); display_draw_bitmap(device, xStart, yStart, xEnd, yEnd, pixelData);
} }
}; };
@@ -1,28 +1,28 @@
#pragma once #pragma once
#include <cassert> #include <tactility/device.h>
#include <tt_hal_touch.h> #include <tactility/drivers/pointer.h>
/** /**
* Wrapper for tt_hal_touch_driver_* * Wrapper for pointer_* device driver functions
*/ */
class TouchDriver { class TouchDriver {
TouchDriverHandle handle = nullptr; struct Device* device;
public: public:
explicit TouchDriver(DeviceId id) { explicit TouchDriver(struct Device* device) : device(device) {
assert(tt_hal_touch_driver_supported(id)); device_get(device);
handle = tt_hal_touch_driver_alloc(id);
assert(handle != nullptr);
} }
~TouchDriver() { ~TouchDriver() {
tt_hal_touch_driver_free(handle); device_put(device);
} }
bool getTouchedPoints(uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* count, uint8_t maxCount) const { bool getTouchedPoints(uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* count, uint8_t maxCount) const {
return tt_hal_touch_driver_get_touched_points(handle, x, y, strength, count, maxCount); // Poll without blocking: perform one read attempt, then report whatever is cached.
pointer_read_data(device, 0);
return pointer_get_touched_points(device, x, y, strength, count, maxCount);
} }
}; };
+23 -44
View File
@@ -6,65 +6,44 @@
#include <tt_app.h> #include <tt_app.h>
#include <tt_app_alertdialog.h> #include <tt_app_alertdialog.h>
#include <tt_lvgl.h>
#include <tactility/device.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/pointer.h>
#include <lvgl/lvgl.h>
#include <lvgl/module.h>
#include <tactility/module.h>
constexpr auto TAG = "Main"; constexpr auto TAG = "Main";
/** Find a DisplayDevice that supports the DisplayDriver interface */
static bool findUsableDisplay(DeviceId& deviceId) {
uint16_t display_count = 0;
if (!tt_hal_device_find(DEVICE_TYPE_DISPLAY, &deviceId, &display_count, 1)) {
ESP_LOGE(TAG, "No display device found");
return false;
}
if (!tt_hal_display_driver_supported(deviceId)) {
ESP_LOGE(TAG, "Display doesn't support driver mode");
return false;
}
return true;
}
/** Find a TouchDevice that supports the TouchDriver interface */
static bool findUsableTouch(DeviceId& deviceId) {
uint16_t touch_count = 0;
if (!tt_hal_device_find(DEVICE_TYPE_TOUCH, &deviceId, &touch_count, 1)) {
ESP_LOGE(TAG, "No touch device found");
return false;
}
if (!tt_hal_touch_driver_supported(deviceId)) {
ESP_LOGE(TAG, "Touch doesn't support driver mode");
return false;
}
return true;
}
static void onCreate(AppHandle appHandle, void* data) { static void onCreate(AppHandle appHandle, void* data) {
DeviceId display_id; struct Device* display_device;
if (!findUsableDisplay(display_id)) { if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) {
ESP_LOGE(TAG, "No display device found");
tt_app_stop(); tt_app_stop();
tt_app_alertdialog_start("Error", "The display doesn't support the required features.", nullptr, 0); tt_app_alertdialog_start("Error", "No display device was found.", nullptr, 0);
return; return;
} }
DeviceId touch_id; struct Device* touch_device;
if (!findUsableTouch(touch_id)) { if (device_get_first_active_by_type(&POINTER_TYPE, &touch_device) != ERROR_NONE) {
ESP_LOGE(TAG, "No touch device found");
device_put(display_device);
tt_app_stop(); tt_app_stop();
tt_app_alertdialog_start("Error", "The touch driver doesn't support the required features.", nullptr, 0); tt_app_alertdialog_start("Error", "No touch device was found.", nullptr, 0);
return; return;
} }
// Stop LVGL first (because it's currently using the drivers we want to use) // Stop LVGL first (because it's currently using the drivers we want to use)
tt_lvgl_stop(); module_stop(&lvgl_module);
ESP_LOGI(TAG, "Creating display driver"); ESP_LOGI(TAG, "Creating display driver");
auto display = new DisplayDriver(display_id); auto display = new DisplayDriver(display_device);
device_put(display_device);
ESP_LOGI(TAG, "Creating touch driver"); ESP_LOGI(TAG, "Creating touch driver");
auto touch = new TouchDriver(touch_id); auto touch = new TouchDriver(touch_device);
device_put(touch_device);
// Run the main logic // Run the main logic
ESP_LOGI(TAG, "Running application"); ESP_LOGI(TAG, "Running application");
@@ -82,9 +61,9 @@ static void onCreate(AppHandle appHandle, void* data) {
static void onDestroy(AppHandle appHandle, void* data) { static void onDestroy(AppHandle appHandle, void* data) {
// Restart LVGL to resume rendering of regular apps // Restart LVGL to resume rendering of regular apps
if (!tt_lvgl_is_started()) { if (!module_is_started(&lvgl_module)) {
ESP_LOGI(TAG, "Restarting LVGL"); ESP_LOGI(TAG, "Restarting LVGL");
tt_lvgl_start(); module_start(&lvgl_module);
} }
} }
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.mystifydemo
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.8.0
[app] app.version.code=8
id=one.tactility.mystifydemo app.name=Mystify Demo
versionName=0.3.0
versionCode=3
name=Mystify Demo
+3 -2
View File
@@ -8,12 +8,13 @@
#include <functional> #include <functional>
#include <tt_app_alertdialog.h> #include <tt_app_alertdialog.h>
#include <tt_lvgl.h>
#include <TactilityCpp/LvglLock.h> #include <TactilityCpp/LvglLock.h>
#include <TactilityCpp/Preferences.h> #include <TactilityCpp/Preferences.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/uart_controller.h> #include <tactility/drivers/uart_controller.h>
constexpr TickType_t LVGL_DEFAULT_LOCK_TIME = 500; // 500 ticks = 500 ms
class ConnectView final : public View { class ConnectView final : public View {
public: public:
@@ -45,7 +46,7 @@ private:
void onConnect() { void onConnect() {
auto lock = lvglLock.asScopedLock(); auto lock = lvglLock.asScopedLock();
if (!lock.lock(TT_LVGL_DEFAULT_LOCK_TIME)) { if (!lock.lock(LVGL_DEFAULT_LOCK_TIME)) {
return; return;
} }
@@ -7,8 +7,6 @@
#include <sstream> #include <sstream>
#include <lvgl.h> #include <lvgl.h>
#include <tt_lvgl.h>
#include <Tactility/RecursiveMutex.h> #include <Tactility/RecursiveMutex.h>
#include <Tactility/Thread.h> #include <Tactility/Thread.h>
#include <TactilityCpp/LvglLock.h> #include <TactilityCpp/LvglLock.h>
@@ -1,5 +1,5 @@
#include "SerialConsole.h" #include "SerialConsole.h"
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
constexpr auto* TAG = "SerialMonitor"; constexpr auto* TAG = "SerialMonitor";
@@ -43,9 +43,9 @@ void SerialConsole::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); auto* toolbar = lvgl_toolbar_create(parent, "Serial Console");
disconnectButton = tt_lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_POWER, onDisconnectPressed, this); disconnectButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_POWER, onDisconnectPressed, this);
lv_obj_add_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN);
wrapperWidget = lv_obj_create(parent); wrapperWidget = lv_obj_create(parent);
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.serialconsole
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.9.0
[app] app.version.code=9
id=one.tactility.serialconsole app.name=Serial Console
versionName=0.4.0
versionCode=4
name=Serial Console
+4 -4
View File
@@ -5,15 +5,15 @@
#include "Snake.h" #include "Snake.h"
#include <inttypes.h> #include <inttypes.h>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tt_app_alertdialog.h> #include <tt_app_alertdialog.h>
#include <tt_app_selectiondialog.h> #include <tt_app_selectiondialog.h>
#include <tt_preferences.h> #include <tt_preferences.h>
#include <TactilityCpp/LvglLock.h> #include <TactilityCpp/LvglLock.h>
#include <tactility/lvgl_module.h> #include <lvgl/lvgl.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
constexpr auto* TAG = "Snake"; constexpr auto* TAG = "Snake";
@@ -245,7 +245,7 @@ void Snake::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
// Create toolbar // Create toolbar
toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); toolbar = lvgl_toolbar_create(parent, "Snake");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
// Create main wrapper // Create main wrapper
+4 -3
View File
@@ -7,7 +7,8 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include <tt_lvgl_keyboard.h> #include <tactility/device.h>
#include <tactility/drivers/keyboard.h>
// Forward declarations // Forward declarations
static void game_play_event(lv_event_t* e); static void game_play_event(lv_event_t* e);
@@ -36,7 +37,7 @@ static void delete_event(lv_event_t* e) {
} }
// Restore edit mode and remove from group before cleanup // Restore edit mode and remove from group before cleanup
if (tt_lvgl_hardware_keyboard_is_available()) { if (device_has_active_by_type(&KEYBOARD_TYPE)) {
lv_group_t* group = lv_group_get_default(); lv_group_t* group = lv_group_get_default();
if (group) lv_group_set_editing(group, false); if (group) lv_group_set_editing(group, false);
lv_group_remove_obj(game->container); lv_group_remove_obj(game->container);
@@ -397,7 +398,7 @@ lv_obj_t* snake_create(lv_obj_t* parent, uint16_t cell_size, bool wall_collision
lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL); lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL);
// Set up keyboard focus if available // Set up keyboard focus if available
if (tt_lvgl_hardware_keyboard_is_available()) { if (device_has_active_by_type(&KEYBOARD_TYPE)) {
lv_group_t* group = lv_group_get_default(); lv_group_t* group = lv_group_get_default();
if (group) { if (group) {
lv_group_add_obj(group, game->container); lv_group_add_obj(group, game->container);
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.snake
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.10.0
[app] app.version.code=10
id=one.tactility.snake app.name=Snake
versionName=0.5.0 app.description=Classic Snake game
versionCode=5
name=Snake
description=Classic Snake game
+5 -6
View File
@@ -5,7 +5,7 @@
#include "TamaTac.h" #include "TamaTac.h"
#include "SpriteData.h" #include "SpriteData.h"
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tt_app_alertdialog.h> #include <tt_app_alertdialog.h>
#include <Tactility/kernel/Kernel.h> #include <Tactility/kernel/Kernel.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
@@ -57,7 +57,6 @@ void TamaTac::onShow(AppHandle context, lv_obj_t* parent) {
if (sfxEngine == nullptr) { if (sfxEngine == nullptr) {
sfxEngine = new SfxEngine(); sfxEngine = new SfxEngine();
sfxEngine->start(); sfxEngine->start();
sfxEngine->applyVolumePreset(SfxEngine::VolumePreset::Normal);
// Load settings // Load settings
bool soundEnabled; bool soundEnabled;
@@ -87,11 +86,11 @@ void TamaTac::onShow(AppHandle context, lv_obj_t* parent) {
lv_obj_set_style_pad_all(parent, 0, 0); lv_obj_set_style_pad_all(parent, 0, 0);
lv_obj_set_style_pad_row(parent, 0, 0); lv_obj_set_style_pad_row(parent, 0, 0);
toolbar = tt_lvgl_toolbar_create_for_app(parent, context); toolbar = lvgl_toolbar_create(parent, "TamaTac");
menuButton = tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onMenuClicked, this); menuButton = lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onMenuClicked, this);
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onCleanClicked, this); lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onCleanClicked, this);
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_REFRESH, onResetClicked, this); lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_REFRESH, onResetClicked, this);
wrapperWidget = lv_obj_create(parent); wrapperWidget = lv_obj_create(parent);
lv_obj_set_width(wrapperWidget, LV_PCT(100)); lv_obj_set_width(wrapperWidget, LV_PCT(100));
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.tamatac
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.5.0
[app] app.version.code=5
id=one.tactility.tamatac app.name=TamaTac
versionName=0.1.0 app.description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
versionCode=1
name=TamaTac
description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
+13 -18
View File
@@ -1,11 +1,10 @@
#include "TodoList.h" #include "TodoList.h"
#include <tt_app.h> #include <tt_app.h>
#include <tt_lock.h> #include <tactility/filesystem/file_mutex.h>
#include <Tactility/kernel/Kernel.h> #include <Tactility/kernel/Kernel.h>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_keyboard.h> #include <lvgl/lvgl.h>
#include <tactility/lvgl_module.h> #include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -60,9 +59,9 @@ void TodoList::saveTodos() {
char savePath[256]; char savePath[256];
if (!getSaveFilePath(savePath, sizeof(savePath))) return; if (!getSaveFilePath(savePath, sizeof(savePath))) return;
auto lock = tt_lock_alloc_for_path(savePath); struct FileMutex mutex;
if (!lock) return; file_mutex_get(&mutex, savePath);
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) { file_mutex_lock(&mutex);
FILE* f = fopen(savePath, "w"); FILE* f = fopen(savePath, "w");
if (f) { if (f) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -70,19 +69,17 @@ void TodoList::saveTodos() {
} }
fclose(f); fclose(f);
} }
tt_lock_release(lock); file_mutex_unlock(&mutex);
}
tt_lock_free(lock);
} }
void TodoList::loadTodos() { void TodoList::loadTodos() {
char savePath[256]; char savePath[256];
if (!getSaveFilePath(savePath, sizeof(savePath))) return; if (!getSaveFilePath(savePath, sizeof(savePath))) return;
auto lock = tt_lock_alloc_for_path(savePath); struct FileMutex mutex;
if (!lock) return; file_mutex_get(&mutex, savePath);
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) { file_mutex_lock(&mutex);
count = 0; count = 0;
FILE* f = fopen(savePath, "r"); FILE* f = fopen(savePath, "r");
if (f) { if (f) {
@@ -103,9 +100,7 @@ void TodoList::loadTodos() {
} }
fclose(f); fclose(f);
} }
tt_lock_release(lock); file_mutex_unlock(&mutex);
}
tt_lock_free(lock);
} }
/* ── UI Helpers ───────────────────────────────────────────────────── */ /* ── UI Helpers ───────────────────────────────────────────────────── */
@@ -277,7 +272,7 @@ void TodoList::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
/* Toolbar */ /* Toolbar */
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app); lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Todo List");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* countWrapper = lv_obj_create(toolbar); lv_obj_t* countWrapper = lv_obj_create(toolbar);
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.todolist
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.7.0
[app] app.version.code=7
id=one.tactility.todolist app.name=Todo List
versionName=0.2.0 app.description=Simple task list manager
versionCode=2
name=Todo List
description=Simple task list manager
+4 -4
View File
@@ -5,12 +5,12 @@
#include "TwoEleven.h" #include "TwoEleven.h"
#include <inttypes.h> #include <inttypes.h>
#include <tt_lvgl_toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tt_app_alertdialog.h> #include <tt_app_alertdialog.h>
#include <tt_app_selectiondialog.h> #include <tt_app_selectiondialog.h>
#include <tt_preferences.h> #include <tt_preferences.h>
#include <tactility/lvgl_module.h> #include <lvgl/lvgl.h>
#include <tactility/lvgl_fonts.h> #include <lvgl/fonts.h>
#include <TactilityCpp/LvglLock.h> #include <TactilityCpp/LvglLock.h>
constexpr auto* TAG = "TwoEleven"; constexpr auto* TAG = "TwoEleven";
@@ -257,7 +257,7 @@ void TwoEleven::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
// Create toolbar // Create toolbar
toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); toolbar = lvgl_toolbar_create(parent, "2048");
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
// Create main wrapper // Create main wrapper
+4 -3
View File
@@ -3,7 +3,8 @@
#include "TwoElevenHelpers.h" #include "TwoElevenHelpers.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <tt_lvgl_keyboard.h> #include <tactility/device.h>
#include <tactility/drivers/keyboard.h>
static void game_play_event(lv_event_t * e); static void game_play_event(lv_event_t * e);
static void btnm_event_cb(lv_event_t * e); static void btnm_event_cb(lv_event_t * e);
@@ -18,7 +19,7 @@ static void delete_event(lv_event_t * e)
twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(obj); twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(obj);
if (game_2048) { if (game_2048) {
// Restore edit mode and remove from group before cleanup // Restore edit mode and remove from group before cleanup
if (tt_lvgl_hardware_keyboard_is_available()) { if (device_has_active_by_type(&KEYBOARD_TYPE)) {
lv_group_t* group = lv_group_get_default(); lv_group_t* group = lv_group_get_default();
if (group) lv_group_set_editing(group, false); if (group) lv_group_set_editing(group, false);
lv_group_remove_obj(game_2048->btnm); lv_group_remove_obj(game_2048->btnm);
@@ -140,7 +141,7 @@ lv_obj_t * twoeleven_create(lv_obj_t * parent, uint16_t matrix_size)
lv_obj_add_event_cb(game_2048->btnm, btnm_event_cb, LV_EVENT_DRAW_TASK_ADDED, NULL); lv_obj_add_event_cb(game_2048->btnm, btnm_event_cb, LV_EVENT_DRAW_TASK_ADDED, NULL);
lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL); lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL);
if (tt_lvgl_hardware_keyboard_is_available()) { if (device_has_active_by_type(&KEYBOARD_TYPE)) {
lv_group_t* group = lv_group_get_default(); lv_group_t* group = lv_group_get_default();
if (group) { if (group) {
lv_group_add_obj(group, game_2048->btnm); lv_group_add_obj(group, game_2048->btnm);
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.twoeleven
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.9.0
[app] app.version.code=9
id=one.tactility.twoeleven app.name=2048
versionName=0.4.0 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).
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).
+70
View File
@@ -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
)
@@ -10,13 +10,15 @@ UnitDualButton::~UnitDualButton() {
bool UnitDualButton::begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB) { bool UnitDualButton::begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB) {
if (!controller) return false; if (!controller) return false;
descA_ = gpio_descriptor_acquire(controller, pinA, GPIO_OWNER_GPIO); gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_UP;
descA_ = gpio_descriptor_acquire(controller, pinA, flags, GPIO_OWNER_GPIO);
if (!descA_) { if (!descA_) {
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinA); ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinA);
return false; return false;
} }
descB_ = gpio_descriptor_acquire(controller, pinB, GPIO_OWNER_GPIO); descB_ = gpio_descriptor_acquire(controller, pinB, flags, GPIO_OWNER_GPIO);
if (!descB_) { if (!descB_) {
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinB); ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinB);
gpio_descriptor_release(descA_); gpio_descriptor_release(descA_);
@@ -24,20 +26,6 @@ bool UnitDualButton::begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB)
return false; return false;
} }
gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_UP;
if (gpio_descriptor_set_flags(descA_, flags) != ERROR_NONE) {
ESP_LOGW(TAG, "Failed to configure pin %d flags", (int)pinA);
gpio_descriptor_release(descA_); gpio_descriptor_release(descB_);
descA_ = descB_ = nullptr;
return false;
}
if (gpio_descriptor_set_flags(descB_, flags) != ERROR_NONE) {
ESP_LOGW(TAG, "Failed to configure pin %d flags", (int)pinB);
gpio_descriptor_release(descA_); gpio_descriptor_release(descB_);
descA_ = descB_ = nullptr;
return false;
}
ready_ = true; ready_ = true;
ESP_LOGI(TAG, "DualButton ready on pins %d/%d", (int)pinA, (int)pinB); ESP_LOGI(TAG, "DualButton ready on pins %d/%d", (int)pinA, (int)pinB);
return true; return true;
+10 -8
View File
@@ -26,6 +26,7 @@
#include <cstdint> #include <cstdint>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/audio_stream.h>
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/queue.h" #include "freertos/queue.h"
@@ -161,12 +162,6 @@ public:
// Settings // Settings
void setEnabled(bool enabled) { enabled_ = enabled; } void setEnabled(bool enabled) { enabled_ = enabled; }
bool isEnabled() const { return enabled_; } bool isEnabled() const { return enabled_; }
void setVolume(float vol) { masterVolume_ = (vol < 0) ? 0 : (vol > 1.0f) ? 1.0f : vol; }
float getVolume() const { return masterVolume_; }
// Volume presets (consistent with SoundEngine naming)
enum class VolumePreset { Quiet, Normal, Loud };
void applyVolumePreset(VolumePreset preset);
// Polyphonic gate (consistent with SoundEngine) // Polyphonic gate (consistent with SoundEngine)
void setPolyphonicGateEnabled(bool enabled) { polyphonicGateEnabled_ = enabled; } void setPolyphonicGateEnabled(bool enabled) { polyphonicGateEnabled_ = enabled; }
@@ -277,14 +272,21 @@ private:
// State // State
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
Device* i2sDevice_ = nullptr; Device* audioStreamDevice_ = nullptr;
AudioStreamHandle audioStreamHandle_ = nullptr;
TaskHandle_t task_ = nullptr; TaskHandle_t task_ = nullptr;
SemaphoreHandle_t stopSemaphore_ = nullptr; // Signaled when audio task exits SemaphoreHandle_t stopSemaphore_ = nullptr; // Signaled when audio task exits
QueueHandle_t msgQueue_ = nullptr; QueueHandle_t msgQueue_ = nullptr;
volatile bool running_ = false; volatile bool running_ = false;
volatile bool enabled_ = true; volatile bool enabled_ = true;
volatile float masterVolume_ = 0.5f;
// Cached system output volume (0..1), refreshed periodically from the shared
// audio_stream device so the preset acts as a relative multiplier on top of it
// rather than an absolute level (a "Quiet" preset should sound quiet relative
// to whatever the user has the system volume set to, not in absolute terms).
float systemVolumeMix_ = 1.0f;
int systemVolumePollCounter_ = 0;
// Polyphonic gate // Polyphonic gate
volatile bool polyphonicGateEnabled_ = true; volatile bool polyphonicGateEnabled_ = true;
+4 -5
View File
@@ -35,11 +35,8 @@ if (!engine->start()) {
ESP_LOGE(TAG, "Failed to start SfxEngine"); ESP_LOGE(TAG, "Failed to start SfxEngine");
return; return;
} }
engine->applyVolumePreset(SfxEngine::VolumePreset::Normal);
engine->play(SfxId::Coin); // Predefined SFX engine->play(SfxId::Coin); // Predefined SFX
engine->playNote(0, 60, 200); // Manual: voice 0, C4, 200ms engine->playNote(0, 60, 200); // Manual: voice 0, C4, 200ms
engine->setVolume(0.7f); // Volume control
engine->stop(); engine->stop();
delete engine; delete engine;
@@ -87,9 +84,11 @@ idf_component_register(
- `void stopVoice(voice)` - Stop specific voice - `void stopVoice(voice)` - Stop specific voice
### Settings ### Settings
- `void setVolume(float)` - Master volume (0.0-1.0, exponential curve)
- `void setEnabled(bool)` - Mute/unmute - `void setEnabled(bool)` - Mute/unmute
- `void applyVolumePreset(VolumePreset)` - Apply Quiet/Normal/Loud preset (configures volume, gate, normalization)
Loudness is controlled by the system output volume (set via the audio_stream device / Settings UI),
not by SfxEngine itself -- a fixed app-side gain on top of hardware attenuation gets swamped at low
system volumes, so there's no separate volume control here.
### Mixing (consistent with SoundEngine) ### Mixing (consistent with SoundEngine)
- `void setPolyphonicGateEnabled(bool)` - Soft gate when multiple voices clip (default: on) - `void setPolyphonicGateEnabled(bool)` - Soft gate when multiple voices clip (default: on)
+56 -64
View File
@@ -9,7 +9,7 @@
#include "SfxEngine.h" #include "SfxEngine.h"
#include "SfxDefinitions.h" #include "SfxDefinitions.h"
#include <tactility/drivers/i2s_controller.h> #include <tactility/drivers/audio_stream.h>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#include "esp_log.h" #include "esp_log.h"
@@ -320,15 +320,17 @@ void SfxEngine::fillStereoBuffer(int16_t* buf, int samples) {
// Apply polyphonic soft gate (proportional reduction when clipping threatened) // Apply polyphonic soft gate (proportional reduction when clipping threatened)
mix = applyPolyphonicGate(mix, activeVoices); mix = applyPolyphonicGate(mix, activeVoices);
// Apply master volume (exponential curve for perceptual linearity)
float volCurve = masterVolume_ * masterVolume_;
mix *= volCurve;
// Apply auto-normalization (consistent volume across different SFX) // Apply auto-normalization (consistent volume across different SFX)
mix = applyAutoNormalization(mix); mix = applyAutoNormalization(mix);
// Brick-wall limiter (final safety net before soft clip) // Brick-wall limiter (final safety net before soft clip)
mix = applyBrickWallLimiter(mix); mix = applyBrickWallLimiter(mix);
// The shared system output volume (esp_codec_dev hardware attenuation, set via
// the Settings UI / audio_stream_set_volume) is the sole loudness control here --
// a fixed app-side gain multiplier stacked on top of it just gets swamped at low
// system-volume levels, making any such control feel like it does nothing.
mix *= systemVolumeMix_;
} }
// Cubic soft clip // Cubic soft clip
@@ -460,14 +462,28 @@ void SfxEngine::audioTaskFunc(void* param) {
} }
} }
// Periodically refresh the cached system output volume/enabled state (cheap pass-
// through to the shared kernel device; polled rather than read per-sample since it
// changes rarely and audio_stream_get_volume may take a lock).
if (self->systemVolumePollCounter_-- <= 0) {
self->systemVolumePollCounter_ = 32; // ~0.5s at 256 samples / 16kHz
float systemVolumePercent = 100.0f;
bool systemOutputEnabled = true;
audio_stream_get_volume(self->audioStreamDevice_, AUDIO_CODEC_DIR_OUTPUT, &systemVolumePercent);
audio_stream_get_enabled(self->audioStreamDevice_, AUDIO_CODEC_DIR_OUTPUT, &systemOutputEnabled);
self->systemVolumeMix_ = systemOutputEnabled ? (systemVolumePercent / 100.0f) : 0.0f;
}
// Fill audio buffer (member buffer to avoid stack pressure) // Fill audio buffer (member buffer to avoid stack pressure)
self->fillStereoBuffer(self->audioBuffer_, BUFFER_SAMPLES); self->fillStereoBuffer(self->audioBuffer_, BUFFER_SAMPLES);
// Write to I2S // Write to the audio stream (resampled to the codec's native rate transparently)
error_t error = i2s_controller_write(self->i2sDevice_, self->audioBuffer_, error_t error = audio_stream_write(self->audioStreamHandle_, self->audioBuffer_,
sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(100)); sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(100));
if (error != ERROR_NONE) { if (error != ERROR_NONE) {
ESP_LOGE(TAG, "I2S write error"); ESP_LOGE(TAG, "Audio stream write error");
self->running_ = false; self->running_ = false;
break; break;
} }
@@ -475,7 +491,7 @@ void SfxEngine::audioTaskFunc(void* param) {
// Flush silence // Flush silence
memset(self->audioBuffer_, 0, sizeof(self->audioBuffer_)); memset(self->audioBuffer_, 0, sizeof(self->audioBuffer_));
i2s_controller_write(self->i2sDevice_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(50)); audio_stream_write(self->audioStreamHandle_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(50));
ESP_LOGI(TAG, "Audio task exiting"); ESP_LOGI(TAG, "Audio task exiting");
@@ -494,33 +510,31 @@ void SfxEngine::audioTaskFunc(void* param) {
bool SfxEngine::start() { bool SfxEngine::start() {
if (running_) return true; if (running_) return true;
// Find I2S device // Find audio stream device
i2sDevice_ = nullptr; audioStreamDevice_ = nullptr;
device_for_each_of_type(&I2S_CONTROLLER_TYPE, &i2sDevice_, [](Device* device, void* context) { device_for_each_of_type(&AUDIO_STREAM_TYPE, &audioStreamDevice_, [](Device* device, void* context) {
if (!device_is_ready(device)) return true; if (!device_is_ready(device)) return true;
Device** devicePtr = static_cast<Device**>(context); Device** devicePtr = static_cast<Device**>(context);
*devicePtr = device; *devicePtr = device;
return false; return false;
}); });
if (i2sDevice_ == nullptr) { if (audioStreamDevice_ == nullptr) {
ESP_LOGW(TAG, "No I2S device found"); ESP_LOGW(TAG, "No audio stream device found");
return false; return false;
} }
// Configure I2S // Open output stream (the kernel resamples to the codec's native rate transparently)
I2sConfig config = { AudioStreamConfig config = {
.communication_format = I2S_FORMAT_STAND_I2S,
.sample_rate = SAMPLE_RATE, .sample_rate = SAMPLE_RATE,
.bits_per_sample = 16, .bits_per_sample = 16,
.channel_left = 0, .channels = 2
.channel_right = 0
}; };
error_t error = i2s_controller_set_config(i2sDevice_, &config); error_t error = audio_stream_open_output(audioStreamDevice_, &config, &audioStreamHandle_);
if (error != ERROR_NONE) { if (error != ERROR_NONE) {
ESP_LOGE(TAG, "Failed to configure I2S: %s", error_to_string(error)); ESP_LOGE(TAG, "Failed to open audio output stream: %s", error_to_string(error));
i2sDevice_ = nullptr; audioStreamDevice_ = nullptr;
return false; return false;
} }
@@ -528,12 +542,14 @@ bool SfxEngine::start() {
msgQueue_ = xQueueCreate(8, sizeof(QueueMsg)); msgQueue_ = xQueueCreate(8, sizeof(QueueMsg));
if (msgQueue_ == nullptr) { if (msgQueue_ == nullptr) {
ESP_LOGE(TAG, "Failed to create message queue"); ESP_LOGE(TAG, "Failed to create message queue");
i2s_controller_reset(i2sDevice_); audio_stream_close(audioStreamHandle_);
i2sDevice_ = nullptr; audioStreamHandle_ = nullptr;
audioStreamDevice_ = nullptr;
return false; return false;
} }
// Start audio task // Start audio task
systemVolumePollCounter_ = 0; // poll the system volume immediately on the first iteration
running_ = true; running_ = true;
BaseType_t result = xTaskCreate(audioTaskFunc, "sfxeng", 4096, this, 5, &task_); BaseType_t result = xTaskCreate(audioTaskFunc, "sfxeng", 4096, this, 5, &task_);
if (result != pdPASS) { if (result != pdPASS) {
@@ -541,8 +557,9 @@ bool SfxEngine::start() {
running_ = false; running_ = false;
vQueueDelete(msgQueue_); vQueueDelete(msgQueue_);
msgQueue_ = nullptr; msgQueue_ = nullptr;
i2s_controller_reset(i2sDevice_); audio_stream_close(audioStreamHandle_);
i2sDevice_ = nullptr; audioStreamHandle_ = nullptr;
audioStreamDevice_ = nullptr;
return false; return false;
} }
@@ -551,19 +568,22 @@ bool SfxEngine::start() {
} }
void SfxEngine::stop() { void SfxEngine::stop() {
if (!running_) return; // Guard on msgQueue_ (the resource marker), not running_ - the audio task can clear
// running_ itself on a write error and self-delete before stop() is ever called, which
// would otherwise make this early-return and leak audioStreamHandle_/msgQueue_.
if (msgQueue_ == nullptr && audioStreamHandle_ == nullptr) return;
// Create semaphore for deterministic shutdown if (running_) {
// Only wait on the semaphore if the task might still be alive to signal it - if
// running_ is already false, the task already exited (and self-deleted) on its own.
stopSemaphore_ = xSemaphoreCreateBinary(); stopSemaphore_ = xSemaphoreCreateBinary();
running_ = false; running_ = false;
if (task_ != nullptr) { if (task_ != nullptr && stopSemaphore_ != nullptr) {
// Wait for audio task to signal completion (up to 500ms)
if (stopSemaphore_ != nullptr) {
xSemaphoreTake(stopSemaphore_, pdMS_TO_TICKS(500)); xSemaphoreTake(stopSemaphore_, pdMS_TO_TICKS(500));
} }
task_ = nullptr;
} }
task_ = nullptr;
if (stopSemaphore_ != nullptr) { if (stopSemaphore_ != nullptr) {
vSemaphoreDelete(stopSemaphore_); vSemaphoreDelete(stopSemaphore_);
@@ -575,43 +595,15 @@ void SfxEngine::stop() {
msgQueue_ = nullptr; msgQueue_ = nullptr;
} }
if (i2sDevice_ != nullptr) { if (audioStreamHandle_ != nullptr) {
i2s_controller_reset(i2sDevice_); audio_stream_close(audioStreamHandle_);
i2sDevice_ = nullptr; audioStreamHandle_ = nullptr;
} }
audioStreamDevice_ = nullptr;
ESP_LOGI(TAG, "SfxEngine stopped"); ESP_LOGI(TAG, "SfxEngine stopped");
} }
void SfxEngine::applyVolumePreset(VolumePreset preset) {
switch (preset) {
case VolumePreset::Quiet:
masterVolume_ = 0.3f;
autoNormalize_ = true;
targetRms_ = 0.25f;
polyphonicGateEnabled_ = true;
softGateThreshold_ = 0.90f;
ESP_LOGI(TAG, "Applied Quiet preset");
break;
case VolumePreset::Normal:
masterVolume_ = 0.5f;
autoNormalize_ = true;
targetRms_ = 0.35f;
polyphonicGateEnabled_ = true;
softGateThreshold_ = 0.95f;
ESP_LOGI(TAG, "Applied Normal preset");
break;
case VolumePreset::Loud:
masterVolume_ = 0.75f;
autoNormalize_ = true;
targetRms_ = 0.45f;
polyphonicGateEnabled_ = true;
softGateThreshold_ = 0.98f;
ESP_LOGI(TAG, "Applied Loud preset");
break;
}
}
void SfxEngine::play(SfxId sound) { void SfxEngine::play(SfxId sound) {
if (!running_ || msgQueue_ == nullptr) return; if (!running_ || msgQueue_ == nullptr) return;
@@ -1,19 +1,20 @@
#pragma once #pragma once
#include <Tactility/Lock.h> #include <Tactility/Lock.h>
#include <tt_lvgl.h> #include <lvgl/lvgl.h>
class LvglLock final : public tt::Lock { class LvglLock final : public tt::Lock {
public: public:
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const override { using tt::Lock::lock;
return tt_lvgl_lock(timeout);
bool lock(TickType_t timeout) const override {
return lvgl_try_lock(timeout);
} }
void unlock() const override { void unlock() const override {
tt_lvgl_unlock(); lvgl_unlock();
} }
}; };
+55 -40
View File
@@ -1,14 +1,23 @@
import json import json
import subprocess
import tarfile import tarfile
import os import os
import tempfile import tempfile
import configparser
import sys import sys
from datetime import datetime, UTC
def read_properties_file(path): def read_properties_file(path):
config = configparser.RawConfigParser() properties = {}
config.read(path) with open(path, "r") as file:
return config 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): def get_manifest(appPath):
"""Extract only the file named 'manifest.properties' from the given tar/tar.gz """Extract only the file named 'manifest.properties' from the given tar/tar.gz
@@ -62,51 +71,49 @@ def get_manifest(appPath):
return None return None
def get_versioned_file_name(manifest): def get_versioned_file_name(manifest):
app_id = manifest["app"]["id"] app_id = manifest["app.id"]
version_code = manifest["app"]["versionCode"] version_code = manifest["app.version.code"]
return f"{app_id}-{version_code}.app" return f"{app_id}-{version_code}.app"
def get_os_version(manifest): def get_os_version(manifest):
sdk = manifest["target"]["sdk"] sdk = manifest["target.sdk"]
# Remove trailing hyphen suffix if present # Remove trailing hyphen suffix if present
if "-" in sdk: if "-" in sdk:
return sdk.rsplit("-", 1)[0].strip() return sdk.rsplit("-", 1)[0].strip()
else: else:
return sdk return sdk
def manifest_config_to_flat_json(manifest): def check_and_get_sdk_version(manifest_map):
"""Convert a ConfigParser manifest into a flat JSON-like dict. """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): def get_git_commit_hash():
- [app] return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()
id -> appId
versionName -> appVersionName def manifest_config_to_flat_json(manifest):
versionCode -> appVersionCode (int) """Convert a flat (V2) manifest dict into a flat JSON-like dict.
name -> appName
description -> appDescription (optional; default "") Expected keys:
- [target] app.id -> appId
sdk -> targetSdk app.version.name -> appVersionName
platforms -> targetPlatforms (comma-separated list) 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. 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 # Map values
app_id = get_opt("app", "id", "") app_id = manifest.get("app.id", "")
app_version_name = get_opt("app", "versionName", "") app_version_name = manifest.get("app.version.name", "")
app_version_code_raw = get_opt("app", "versionCode", "0") app_version_code_raw = manifest.get("app.version.code", "0")
app_name = get_opt("app", "name", "") app_name = manifest.get("app.name", "")
app_description = get_opt("app", "description", "") or "" app_description = manifest.get("app.description", "") or ""
# Coerce version code to int safely # Coerce version code to int safely
try: try:
@@ -114,8 +121,8 @@ def manifest_config_to_flat_json(manifest):
except Exception: except Exception:
app_version_code = 0 app_version_code = 0
target_sdk = get_opt("target", "sdk", "") target_sdk = manifest.get("target.sdk", "")
platforms_raw = get_opt("target", "platforms", "") 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 [] 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) filename = get_versioned_file_name(manifest)
@@ -142,15 +149,23 @@ if __name__ == "__main__":
sys.exit() sys.exit()
app_directory = sys.argv[1] app_directory = sys.argv[1]
manifest_map = {} manifest_map = {}
output_json = {
"apps": []
}
any_manifest = None any_manifest = None
if os.path.exists(app_directory): if os.path.exists(app_directory):
for file in os.listdir(app_directory): for file in os.listdir(app_directory):
if file.endswith(".app"): if file.endswith(".app"):
file_path = os.path.join(app_directory, file) file_path = os.path.join(app_directory, file)
manifest_map[file_path] = get_manifest(file_path) 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 # Rename files and collect manifest data into output json object
for file_path in manifest_map.keys(): for file_path in manifest_map.keys():
print(f"Processing {file_path}: {manifest_map[file_path]}") print(f"Processing {file_path}: {manifest_map[file_path]}")
+23 -36
View File
@@ -1,4 +1,3 @@
import configparser
import json import json
import os import os
import re import re
@@ -13,7 +12,7 @@ import tarfile
from urllib.parse import urlparse from urllib.parse import urlparse
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "3.5.1" ttbuild_version = "4.1.0"
ttbuild_cdn = "https://cdn.tactilityproject.org" ttbuild_cdn = "https://cdn.tactilityproject.org"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@@ -106,9 +105,17 @@ def get_url(ip, path):
return f"http://{ip}:{ttport}{path}" return f"http://{ip}:{ttport}{path}"
def read_properties_file(path): def read_properties_file(path):
config = configparser.RawConfigParser() properties = {}
config.read(path) with open(path, "r") as file:
return config 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 #endregion Core
@@ -185,7 +192,7 @@ def fetch_sdkconfig_files(platform_targets):
for platform in platform_targets: for platform in platform_targets:
sdkconfig_filename = f"sdkconfig.app.{platform}" sdkconfig_filename = f"sdkconfig.app.{platform}"
target_path = os.path.join(ttbuild_path, sdkconfig_filename) 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}") exit_with_error(f"Failed to download sdkconfig file for {platform}")
#endregion SDK helpers #endregion SDK helpers
@@ -231,32 +238,12 @@ def read_manifest():
return read_properties_file("manifest.properties") return read_properties_file("manifest.properties")
def validate_manifest(manifest): def validate_manifest(manifest):
# [manifest] for key in ("manifest.version", "target.sdk", "target.platforms", "app.id", "app.version.name", "app.version.code", "app.name"):
if not "manifest" in manifest: if key not in manifest:
exit_with_error("Invalid manifest format: [manifest] not found") exit_with_error(f"Invalid manifest format: {key} 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")
def is_valid_manifest_platform(manifest, platform): def is_valid_manifest_platform(manifest, platform):
manifest_platforms = manifest["target"]["platforms"].split(",") manifest_platforms = manifest["target.platforms"].split(",")
return platform in manifest_platforms return platform in manifest_platforms
def validate_manifest_platform(manifest, platform): def validate_manifest_platform(manifest, platform):
@@ -265,7 +252,7 @@ def validate_manifest_platform(manifest, platform):
def get_manifest_target_platforms(manifest, requested_platform): def get_manifest_target_platforms(manifest, requested_platform):
if requested_platform == "" or requested_platform is None: if requested_platform == "" or requested_platform is None:
return manifest["target"]["platforms"].split(",") return manifest["target.platforms"].split(",")
else: else:
validate_manifest_platform(manifest, requested_platform) validate_manifest_platform(manifest, requested_platform)
return [requested_platform] return [requested_platform]
@@ -512,7 +499,7 @@ def build_action(manifest, platform_arg, skip_build):
if use_local_sdk: if use_local_sdk:
global local_base_path global local_base_path
local_base_path = os.environ.get("TACTILITY_SDK_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): if should_fetch_sdkconfig_files(platforms_to_build):
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() sdk_json = read_sdk_json()
validate_self(sdk_json) validate_self(sdk_json)
# Build # Build
sdk_version = manifest["target"]["sdk"] sdk_version = manifest["target.sdk"]
if not use_local_sdk: if not use_local_sdk:
if not sdk_download_all(sdk_version, platforms_to_build): if not sdk_download_all(sdk_version, platforms_to_build):
exit_with_error("Failed to download one or more SDKs") 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}") print_status_error(f"Device info request failed: {e}")
def run_action(manifest, ip): def run_action(manifest, ip):
app_id = manifest["app"]["id"] app_id = manifest["app.id"]
print_status_busy("Running") print_status_busy("Running")
url = get_url(ip, "/app/run") url = get_url(ip, "/app/run")
params = {'id': app_id} params = {'id': app_id}
@@ -614,7 +601,7 @@ def install_action(ip, platforms):
return False return False
def uninstall_action(manifest, ip): def uninstall_action(manifest, ip):
app_id = manifest["app"]["id"] app_id = manifest["app.id"]
print_status_busy("Uninstalling") print_status_busy("Uninstalling")
url = get_url(ip, "/app/uninstall") url = get_url(ip, "/app/uninstall")
params = {'id': app_id} params = {'id': app_id}
@@ -670,7 +657,7 @@ if __name__ == "__main__":
exit_with_error("manifest.properties not found") exit_with_error("manifest.properties not found")
manifest = read_manifest() manifest = read_manifest()
validate_manifest(manifest) validate_manifest(manifest)
all_platform_targets = manifest["target"]["platforms"].split(",") all_platform_targets = manifest["target.platforms"].split(",")
# Update SDK cache (tool.json) # Update SDK cache (tool.json)
if not use_local_sdk and should_update_tool_json() and not update_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") exit_with_error("Failed to retrieve SDK info")