1 Commits

Author SHA1 Message Date
Adolfo 2795dddc12 feat(RobotArm): cute MCP arm controller with 2x tall sliders, sequencer colored blocks, smooth move_all_joints
- 6 joints vertical sliders 22x72 (2x tall), 3 cols, pastel cute cards, home-centered ranges shoulder max 70
- open/close inverted fix (0=open 180=close)
- sequencer bottom not fixed, colored blocks 22x22 no info, current highlight white 3px, larger buttons 38x32 Play 56x32
- smooth concurrent moves via move_all_joints tool (tools/list discovered) not one-by-one
- scroll fixes: clearflag SCROLLABLE on cards/grid/hdr/brow/seqbar, root LV_DIR_VER only (no horiz scroll)
- safe for device .129: custom my_htons avoids missing lwip_htons, 52U 0 missing symbols, 14K ELF
- live reading from 192.168.68.103/api/mcp get_arm_state + move_joint
- enjoyed by son :3
2026-07-17 11:19:07 -04:00
78 changed files with 949 additions and 1335 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
Build:
strategy:
matrix:
app_name: [Brainfuck, Breakout, Calculator, Diceware, EpubReader, EspNowBridge, GPIO, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, MediaKeys, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven]
app_name: [Brainfuck, Breakout, Calculator, Diceware, EpubReader, GPIO, GraphicsDemo, HelloWorld, M5UnitTest, Magic8Ball, MediaKeys, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+4 -4
View File
@@ -1,6 +1,6 @@
#include "Brainfuck.h"
#include <tt_app.h>
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
#include <dirent.h>
#include <stdio.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_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Brainfuck interpreter");
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
clrBtn = lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearClicked, nullptr);
clrBtn = tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearClicked, nullptr);
lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onExamplesClicked, nullptr);
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onExamplesClicked, nullptr);
lv_obj_t* cont = lv_obj_create(parent);
lv_obj_set_width(cont, LV_PCT(100));
+2 -2
View File
@@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.brainfuck
app.version.name=0.6.0
app.version.code=6
app.version.name=0.5.0
app.version.code=5
app.name=Brainfuck interpreter
app.description=Brainfuck esoteric language interpreter
+7 -7
View File
@@ -7,14 +7,13 @@
#include <cstdio>
#include <cmath>
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
#include <tt_preferences.h>
#include <esp_random.h>
#include <tactility/device.h>
#include <tactility/drivers/keyboard.h>
#include <tt_lvgl_keyboard.h>
#include <lvgl/lvgl.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_module.h>
#include <tactility/lvgl_fonts.h>
constexpr auto* TAG = "Breakout";
@@ -125,11 +124,12 @@ void Breakout::onShow(AppHandle appHandle, lv_obj_t* parent) {
if (!sfxEngine) {
sfxEngine = new SfxEngine();
sfxEngine->start();
sfxEngine->applyVolumePreset(SfxEngine::VolumePreset::Quiet);
sfxEngine->setEnabled(soundEnabled);
}
// Toolbar
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Breakout");
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
// Score wrapper in toolbar
lv_obj_t* scoreWrap = lv_obj_create(toolbar);
@@ -1330,7 +1330,7 @@ void Breakout::updateMessage() {
case GameState::Ready: {
char buf[64];
const char* input_hint = "Touch";
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
if (tt_lvgl_hardware_keyboard_is_available()) {
input_hint = "Space";
}
if (level > 1) {
+2 -2
View File
@@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.breakout
app.version.name=0.7.0
app.version.code=7
app.version.name=0.5.0
app.version.code=5
app.name=Breakout
app.description=Classic brick-breaking arcade game
+2 -2
View File
@@ -2,7 +2,7 @@
#include <cstdio>
#include <ctype.h>
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
#include <stack>
#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_style_pad_row(parent, 0, LV_STATE_DEFAULT);
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Calculator");
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* wrapper = lv_obj_create(parent);
+2 -2
View File
@@ -2,6 +2,6 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.calculator
app.version.name=0.7.0
app.version.code=7
app.version.name=0.6.0
app.version.code=6
app.name=Calculator
+12 -11
View File
@@ -1,9 +1,9 @@
#include "Diceware.h"
#include <tt_app_alertdialog.h>
#include <tactility/filesystem/file_mutex.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/toolbar.h>
#include <tt_lock.h>
#include <tt_lvgl.h>
#include <tt_lvgl_toolbar.h>
#include <esp_random.h>
#include <esp_log.h>
@@ -39,17 +39,18 @@ static std::string readWordAtLine(const AppHandle handle, const int lineIndex) {
return "";
}
struct FileMutex mutex;
file_mutex_get(&mutex, path);
auto lock = tt_lock_alloc_for_path(path);
std::string word;
file_mutex_lock(&mutex);
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
FILE* file = fopen(path, "r");
if (file != nullptr) {
skipNewlines(file, lineIndex);
word = readWord(file);
fclose(file);
} else { ESP_LOGE(TAG, "Failed to open %s", path); }
file_mutex_unlock(&mutex);
tt_lock_release(lock);
} else { ESP_LOGE(TAG, "Failed to acquire lock for %s", path); }
tt_lock_free(lock);
return word;
}
@@ -86,9 +87,9 @@ void Diceware::startJob(uint32_t jobWordCount) {
}
void Diceware::onFinishJob(std::string result) {
lvgl_lock();
tt_lvgl_lock(tt::kernel::MAX_TICKS);
lv_label_set_text(resultLabel, result.c_str());
lvgl_unlock();
tt_lvgl_unlock();
}
void Diceware::onClickGenerate(lv_event_t* e) {
@@ -122,8 +123,8 @@ void Diceware::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = lvgl_toolbar_create(parent, "Diceware");
lvgl_toolbar_add_text_button_action(toolbar, "?", onHelpClicked, nullptr);
auto* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
tt_lvgl_toolbar_add_text_button_action(toolbar, "?", onHelpClicked, nullptr);
auto* wrapper = lv_obj_create(parent);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
+2 -2
View File
@@ -2,6 +2,6 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.diceware
app.version.name=0.8.0
app.version.code=8
app.version.name=0.6.0
app.version.code=6
app.name=Diceware
+2 -2
View File
@@ -1,7 +1,7 @@
#include "EpubReader.h"
#include "HtmlStrip.h" // stripHtmlToText
#include <tt_bundle.h>
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
#include <tt_app_alertdialog.h>
#include <tt_app_selectiondialog.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_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
toolbar_ = lvgl_toolbar_create(parent, "Epub Reader");
toolbar_ = tt_lvgl_toolbar_create_for_app(parent, app);
wrapperWidget_ = lv_obj_create(parent);
lv_obj_set_width(wrapperWidget_, LV_PCT(100));
@@ -1,6 +1,6 @@
#include "EpubReader.h"
#include <lvgl/widgets/toolbar.h>
#include <tactility/filesystem/file_mutex.h>
#include <tt_lvgl_toolbar.h>
#include <tt_lock.h>
#include <tactility/log.h>
#include <Tactility/kernel/Kernel.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
lv_obj_clean(self->wrapperWidget_);
lvgl_toolbar_clear_actions(self->toolbar_);
tt_lvgl_toolbar_clear_actions(self->toolbar_);
lv_obj_t* lbl = lv_label_create(self->wrapperWidget_);
lv_obj_set_style_pad_all(lbl, 8, 0);
lv_label_set_text(lbl, restore ? "Loading..." : "Opening...");
@@ -115,9 +115,14 @@ void EpubReader::backgroundOpenTask(void* data) {
// Acquire the filesystem lock before any SD card I/O - prevents concurrent
// SDMMC access from the background and LVGL tasks (bus errors 0x107/0x108).
struct FileMutex mutex;
file_mutex_get(&mutex, a->filePath.c_str());
file_mutex_lock(&mutex);
auto lock = tt_lock_alloc_for_path(a->filePath.c_str());
if (!tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
LOG_E(TAG, "FS lock timed out, skipping open: %s", a->filePath.c_str());
tt_lock_free(lock);
lv_async_call(asyncOpenComplete, a);
vTaskDelete(nullptr);
return;
}
if (isTextFile(a->filePath)) {
// Read the entire text file here (under the lock) so asyncOpenComplete
@@ -141,7 +146,8 @@ void EpubReader::backgroundOpenTask(void* data) {
a->epub = EpubService::open(a->filePath);
}
file_mutex_unlock(&mutex);
tt_lock_release(lock);
tt_lock_free(lock);
// Signal the LVGL task that the work is done
lv_async_call(asyncOpenComplete, a);
+8 -8
View File
@@ -1,5 +1,5 @@
#include "EpubReader.h"
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
#include <tactility/log.h>
#include <esp_heap_caps.h>
#include <dirent.h>
@@ -37,20 +37,20 @@ static void setListBtnLongMode(lv_obj_t* btn, lv_label_long_mode_t mode) {
// ---------------------------------------------------------------------------
void EpubReader::setReaderToolbarButtons() {
lvgl_toolbar_clear_actions(toolbar_);
lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_PREV, onPrevPressed, this);
tt_lvgl_toolbar_clear_actions(toolbar_);
tt_lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_PREV, onPrevPressed, this);
if (!textMode_) {
lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_LIST, onTocPressed, this);
tt_lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_LIST, onTocPressed, this);
}
lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_NEXT, onNextPressed, this);
lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_DIRECTORY, onBrowsePressed, this);
tt_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);
}
void EpubReader::setBrowserToolbarButtons() {
lvgl_toolbar_clear_actions(toolbar_);
tt_lvgl_toolbar_clear_actions(toolbar_);
// Show "Use Folder" button when the current browse path isn't already the saved books folder
if (browsePath_ != booksPath_) {
lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_DIRECTORY, onSetBooksFolder, this);
tt_lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_DIRECTORY, onSetBooksFolder, this);
}
}
+2 -2
View File
@@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32s3,esp32p4
app.id=one.tactility.epubreader
app.version.name=0.5.0
app.version.code=5
app.version.name=0.4.0
app.version.code=4
app.name=Epub Reader
app.description=Epub and text file reader. Requires PSRAM!
Binary file not shown.
-11
View File
@@ -1,11 +0,0 @@
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
)
@@ -1,740 +0,0 @@
#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;
}
}
}
@@ -1,108 +0,0 @@
#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
@@ -1,11 +0,0 @@
#include "EspNowBridge.h"
#include <TactilityCpp/App.h>
extern "C" {
int main(int argc, char* argv[]) {
registerApp<EspNowBridge>();
return 0;
}
}
-8
View File
@@ -1,8 +0,0 @@
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.
+6 -5
View File
@@ -2,9 +2,10 @@
#include <Tactility/kernel/Kernel.h>
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl.h>
#include <tt_lvgl_toolbar.h>
#include <lvgl/lvgl.h>
#include <tactility/lvgl_module.h>
#include <esp_log.h>
#include <driver/gpio.h>
@@ -19,7 +20,7 @@ void Gpio::updatePinStates() {
}
void Gpio::updatePinWidgets() {
lvgl_lock();
tt_lvgl_lock(tt::kernel::MAX_TICKS);
for (int j = 0; j < pinStates.size(); ++j) {
int level = pinStates[j];
lv_obj_t* label = pinWidgets[j];
@@ -34,7 +35,7 @@ void Gpio::updatePinWidgets() {
}
}
}
lvgl_unlock();
tt_lvgl_unlock();
}
lv_obj_t* Gpio::createGpioRowWrapper(lv_obj_t* parent) {
@@ -78,7 +79,7 @@ void Gpio::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = lvgl_toolbar_create(parent, "GPIO");
auto* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
// Main content wrapper, enables scrolling content without scrolling the toolbar
+2 -2
View File
@@ -2,6 +2,6 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.gpio
app.version.name=0.9.0
app.version.code=9
app.version.name=0.7.0
app.version.code=7
app.name=GPIO
+16 -16
View File
@@ -4,17 +4,17 @@
#include "drivers/Colors.h"
#include <cstring>
#include <tactility/drivers/display.h>
#include <tt_hal_display.h>
class PixelBuffer {
uint16_t pixelWidth;
uint16_t pixelHeight;
enum DisplayColorFormat colorFormat;
ColorFormat colorFormat;
uint8_t* data;
public:
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, enum DisplayColorFormat colorFormat) :
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, ColorFormat colorFormat) :
pixelWidth(pixelWidth),
pixelHeight(pixelHeight),
colorFormat(colorFormat)
@@ -35,7 +35,7 @@ public:
return pixelHeight;
}
enum DisplayColorFormat getColorFormat() const {
ColorFormat getColorFormat() const {
return colorFormat;
}
@@ -58,14 +58,14 @@ public:
uint8_t getPixelSize() const {
switch (colorFormat) {
case DISPLAY_COLOR_FORMAT_MONOCHROME:
case COLOR_FORMAT_MONOCHROME:
return 1;
case DISPLAY_COLOR_FORMAT_BGR565:
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
case DISPLAY_COLOR_FORMAT_RGB565:
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
case COLOR_FORMAT_BGR565:
case COLOR_FORMAT_BGR565_SWAPPED:
case COLOR_FORMAT_RGB565:
case COLOR_FORMAT_RGB565_SWAPPED:
return 2;
case DISPLAY_COLOR_FORMAT_RGB888:
case COLOR_FORMAT_RGB888:
return 3;
default:
// 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 {
auto address = getPixelAddress(x, y);
switch (colorFormat) {
case DISPLAY_COLOR_FORMAT_MONOCHROME:
case COLOR_FORMAT_MONOCHROME:
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
break;
case DISPLAY_COLOR_FORMAT_BGR565:
case COLOR_FORMAT_BGR565:
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
break;
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: {
case COLOR_FORMAT_BGR565_SWAPPED: {
// TODO: Make proper conversion function
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address;
@@ -96,11 +96,11 @@ public:
*(address + 1) = temp;
break;
}
case DISPLAY_COLOR_FORMAT_RGB565: {
case COLOR_FORMAT_RGB565: {
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
break;
}
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: {
case COLOR_FORMAT_RGB565_SWAPPED: {
// TODO: Make proper conversion function
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address;
@@ -108,7 +108,7 @@ public:
*(address + 1) = temp;
break;
}
case DISPLAY_COLOR_FORMAT_RGB888: {
case COLOR_FORMAT_RGB888: {
uint8_t pixel[3] = { r, g, b };
memcpy(address, pixel, 3);
break;
@@ -1,47 +1,49 @@
#pragma once
#include <tactility/device.h>
#include <tactility/drivers/display.h>
#include <cassert>
#include <tt_hal_display.h>
#include <Tactility/kernel/Kernel.h>
/**
* Wrapper for display_* device driver functions
* Wrapper for tt_hal_display_driver_*
*/
class DisplayDriver {
struct Device* device;
DisplayDriverHandle handle = nullptr;
public:
explicit DisplayDriver(struct Device* device) : device(device) {
device_get(device);
explicit DisplayDriver(DeviceId id) {
assert(tt_hal_display_driver_supported(id));
handle = tt_hal_display_driver_alloc(id);
assert(handle != nullptr);
}
~DisplayDriver() {
device_put(device);
tt_hal_display_driver_free(handle);
}
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const {
return device_try_lock(device, timeout);
return tt_hal_display_driver_lock(handle, timeout);
}
void unlock() const {
device_unlock(device);
tt_hal_display_driver_unlock(handle);
}
uint16_t getWidth() const {
return display_get_resolution_x(device);
return tt_hal_display_driver_get_pixel_width(handle);
}
uint16_t getHeight() const {
return display_get_resolution_y(device);
return tt_hal_display_driver_get_pixel_height(handle);
}
enum DisplayColorFormat getColorFormat() const {
return display_get_color_format(device);
ColorFormat getColorFormat() const {
return tt_hal_display_driver_get_colorformat(handle);
}
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
display_draw_bitmap(device, xStart, yStart, xEnd, yEnd, pixelData);
tt_hal_display_driver_draw_bitmap(handle, xStart, yStart, xEnd, yEnd, pixelData);
}
};
@@ -1,28 +1,28 @@
#pragma once
#include <tactility/device.h>
#include <tactility/drivers/pointer.h>
#include <cassert>
#include <tt_hal_touch.h>
/**
* Wrapper for pointer_* device driver functions
* Wrapper for tt_hal_touch_driver_*
*/
class TouchDriver {
struct Device* device;
TouchDriverHandle handle = nullptr;
public:
explicit TouchDriver(struct Device* device) : device(device) {
device_get(device);
explicit TouchDriver(DeviceId id) {
assert(tt_hal_touch_driver_supported(id));
handle = tt_hal_touch_driver_alloc(id);
assert(handle != nullptr);
}
~TouchDriver() {
device_put(device);
tt_hal_touch_driver_free(handle);
}
bool getTouchedPoints(uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* count, uint8_t maxCount) const {
// 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);
return tt_hal_touch_driver_get_touched_points(handle, x, y, strength, count, maxCount);
}
};
+44 -23
View File
@@ -6,44 +6,65 @@
#include <tt_app.h>
#include <tt_app_alertdialog.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>
#include <tt_lvgl.h>
constexpr auto TAG = "Main";
static void onCreate(AppHandle appHandle, void* data) {
struct Device* display_device;
if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) {
/** 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) {
DeviceId display_id;
if (!findUsableDisplay(display_id)) {
tt_app_stop();
tt_app_alertdialog_start("Error", "No display device was found.", nullptr, 0);
tt_app_alertdialog_start("Error", "The display doesn't support the required features.", nullptr, 0);
return;
}
struct Device* touch_device;
if (device_get_first_active_by_type(&POINTER_TYPE, &touch_device) != ERROR_NONE) {
ESP_LOGE(TAG, "No touch device found");
device_put(display_device);
DeviceId touch_id;
if (!findUsableTouch(touch_id)) {
tt_app_stop();
tt_app_alertdialog_start("Error", "No touch device was found.", nullptr, 0);
tt_app_alertdialog_start("Error", "The touch driver doesn't support the required features.", nullptr, 0);
return;
}
// Stop LVGL first (because it's currently using the drivers we want to use)
module_stop(&lvgl_module);
tt_lvgl_stop();
ESP_LOGI(TAG, "Creating display driver");
auto display = new DisplayDriver(display_device);
device_put(display_device);
auto display = new DisplayDriver(display_id);
ESP_LOGI(TAG, "Creating touch driver");
auto touch = new TouchDriver(touch_device);
device_put(touch_device);
auto touch = new TouchDriver(touch_id);
// Run the main logic
ESP_LOGI(TAG, "Running application");
@@ -61,9 +82,9 @@ static void onCreate(AppHandle appHandle, void* data) {
static void onDestroy(AppHandle appHandle, void* data) {
// Restart LVGL to resume rendering of regular apps
if (!module_is_started(&lvgl_module)) {
if (!tt_lvgl_is_started()) {
ESP_LOGI(TAG, "Restarting LVGL");
module_start(&lvgl_module);
tt_lvgl_start();
}
}
+2 -2
View File
@@ -2,6 +2,6 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.graphicsdemo
app.version.name=0.7.0
app.version.code=7
app.version.name=0.6.0
app.version.code=6
app.name=Graphics Demo
+2 -2
View File
@@ -1,12 +1,12 @@
#include <tt_app.h>
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
/**
* 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)
*/
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Hello World");
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* label = lv_label_create(parent);
+2 -2
View File
@@ -2,6 +2,6 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.helloworld
app.version.name=0.7.0
app.version.code=7
app.version.name=0.6.0
app.version.code=6
app.name=Hello World
@@ -14,6 +14,7 @@
#include "TestUnitLcdGfx.h"
#include <tactility/device.h>
#include <tt_lvgl_toolbar.h>
#include <esp_log.h>
constexpr auto* TAG = "M5UnitTest";
+3 -3
View File
@@ -1,13 +1,13 @@
#include "TestListView.h"
#include "M5UnitTest.h"
#include "UiScale.h"
#include <lvgl/widgets/toolbar.h>
#include <lvgl/fonts.h>
#include <tt_lvgl_toolbar.h>
#include <tactility/lvgl_fonts.h>
void TestListView::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
lvgl_toolbar_create(parent, "M5 Unit Test");
tt_lvgl_toolbar_create_for_app(parent, handle);
list_ = lv_list_create(parent);
lv_obj_set_width(list_, LV_PCT(100));
+1 -1
View File
@@ -2,7 +2,7 @@
#include <array>
#include <lvgl.h>
#include <lvgl/icons/shared.h>
#include <tactility/lvgl_icon_shared.h>
#include <tt_app.h>
class M5UnitTest;
@@ -2,7 +2,7 @@
#include "GroveLookup.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
#include <cstring>
@@ -2,7 +2,7 @@
#include "GroveLookup.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
#include <cstring>
void TestUnitByteButton::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
@@ -3,7 +3,7 @@
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/drivers/uart_controller.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
#include <cstring>
// ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
#include "TestUnitDualButton.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
static constexpr gpio_pin_t PIN_MIN = 0;
static constexpr gpio_pin_t PIN_MAX = 57;
@@ -2,7 +2,7 @@
#include "GroveLookup.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
#include <algorithm>
#include <cmath>
+1 -1
View File
@@ -2,7 +2,7 @@
#include "GroveLookup.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
void TestUnitLcd::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
@@ -2,7 +2,7 @@
#include "GroveLookup.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
#include <esp_timer.h>
#include <cstring>
#include <cstdio>
+1 -1
View File
@@ -2,7 +2,7 @@
#include "GroveLookup.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
void TestUnitMidi::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
@@ -3,7 +3,7 @@
#include "UiScale.h"
#include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
void TestUnitPaHub::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
@@ -2,7 +2,8 @@
#include "GroveLookup.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
#include <tt_lvgl_toolbar.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
@@ -2,7 +2,7 @@
#include "GroveLookup.h"
#include "UiScale.h"
#include <tactility/device.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
void TestUnitScroll::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
app_ = app;
+5 -4
View File
@@ -1,12 +1,13 @@
#include "TestViewBase.h"
#include "M5UnitTest.h"
#include "UiScale.h"
#include <lvgl/widgets/toolbar.h>
#include <lvgl/fonts.h>
#include <tt_lvgl_toolbar.h>
#include <tactility/lvgl_fonts.h>
lv_obj_t* TestViewBase::createToolbar(lv_obj_t* parent, AppHandle handle, const char* title) {
lv_obj_t* toolbar = lvgl_toolbar_create(parent, title);
lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LEFT, onBackClicked, this);
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, handle);
tt_lvgl_toolbar_set_title(toolbar, title);
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LEFT, onBackClicked, this);
return toolbar;
}
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#include <lvgl.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_fonts.h>
// Device screen widths in default (portrait) orientation:
// tiny < 200 : small OLEDs, custom breadboard devices
+2 -2
View File
@@ -2,6 +2,6 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32s3,esp32p4
app.id=one.tactility.m5unittest
app.version.name=0.5.0
app.version.code=5
app.version.name=0.4.0
app.version.code=4
app.name=M5 Unit Test
+6 -7
View File
@@ -1,7 +1,6 @@
#include "Magic8Ball.h"
#include <lvgl/widgets/toolbar.h>
#include <tactility/device.h>
#include <tactility/drivers/keyboard.h>
#include <tt_lvgl_toolbar.h>
#include <tt_lvgl_keyboard.h>
#include <stdlib.h>
#include <time.h>
@@ -36,7 +35,7 @@ static const char* responses[] = {
#define NUM_RESPONSES (sizeof(responses) / sizeof(responses[0]))
static const char* getInputHint() {
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
if (tt_lvgl_hardware_keyboard_is_available()) {
return "Touch or Space to ask Q to exit";
}
return "Touch the ball to ask";
@@ -93,7 +92,7 @@ void Magic8Ball::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
/* Toolbar */
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Magic 8-Ball");
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
/* Main container */
@@ -142,7 +141,7 @@ void Magic8Ball::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_add_event_cb(ballObj, onBallClick, LV_EVENT_CLICKED, this);
/* Keyboard support - no editing mode needed, just focus the ball */
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
if (tt_lvgl_hardware_keyboard_is_available()) {
lv_group_t* grp = lv_group_get_default();
if (grp) {
lv_group_add_obj(grp, ballObj);
@@ -153,7 +152,7 @@ void Magic8Ball::onShow(AppHandle app, lv_obj_t* parent) {
}
void Magic8Ball::onHide(AppHandle app) {
if (device_has_active_by_type(&KEYBOARD_TYPE) && ballObj) {
if (tt_lvgl_hardware_keyboard_is_available() && ballObj) {
lv_group_remove_obj(ballObj);
}
answerLabel = nullptr;
+2 -2
View File
@@ -2,6 +2,6 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.magic8ball
app.version.name=0.6.0
app.version.code=6
app.version.name=0.5.0
app.version.code=5
app.name=Magic 8-Ball
+28 -60
View File
@@ -4,9 +4,9 @@
#include <esp_heap_caps.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <lvgl/fonts.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/toolbar.h>
#include <tactility/lvgl_fonts.h>
#include <tt_lvgl.h>
#include <tt_lvgl_toolbar.h>
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) {
// Radio is now up - start HID (needs LVGL lock for UI update)
if (lvgl_try_lock(1000)) {
if (tt_lvgl_lock(1000)) {
// Re-check inside lock to avoid TOCTOU race with handleSwitchToggle(false)
if (self->_radioEnabling) {
self->startHid();
}
lvgl_unlock();
tt_lvgl_unlock();
}
} else if (event.radio_state == BT_RADIO_STATE_OFF && self->_isEnabled) {
// Radio dropped while we were active - revert UI
LOG_I(TAG, "BT radio turned off, disabling HID");
if (lvgl_try_lock(1000)) {
if (device_has_active_by_type(&KEYBOARD_TYPE)) self->exitKeyMode();
if (tt_lvgl_lock(1000)) {
if (tt_lvgl_hardware_keyboard_is_available()) self->exitKeyMode();
self->_hidDevice = nullptr;
self->_isEnabled = false;
self->_radioEnabling = false;
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);
lvgl_unlock();
tt_lvgl_unlock();
}
}
} else if (event.type == BT_EVENT_PROFILE_STATE_CHANGED && event.profile_state.profile == BT_PROFILE_HID_DEVICE) {
@@ -208,24 +208,7 @@ void MediaKeys::startHid() {
}
if (_mainWrapper) lv_obj_remove_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
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;
if (tt_lvgl_hardware_keyboard_is_available()) enterKeyMode();
}
void MediaKeys::handleSwitchToggle(bool enabled) {
@@ -233,7 +216,7 @@ void MediaKeys::handleSwitchToggle(bool enabled) {
_isEnabled = enabled;
if (enabled) {
_btDevice = device_find_first_by_type(&BLUETOOTH_TYPE);
_btDevice = bluetooth_find_first_ready_device();
if (!_btDevice) {
LOG_E(TAG, "No Bluetooth device found");
_isEnabled = false;
@@ -241,19 +224,6 @@ void MediaKeys::handleSwitchToggle(bool enabled) {
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");
// Register callback before enabling radio so we don't miss the state-change event.
@@ -276,11 +246,13 @@ void MediaKeys::handleSwitchToggle(bool enabled) {
}
} else {
_radioEnabling = false;
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 (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode();
if (_hidDevice) bluetooth_hid_device_stop(_hidDevice);
teardownBt();
if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback);
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);
}
}
@@ -307,10 +279,10 @@ void MediaKeys::onShow(AppHandle appHandle, lv_obj_t* parent) {
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, "Media Keys");
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
_switchWidget = lvgl_toolbar_add_switch_action(toolbar);
_switchWidget = tt_lvgl_toolbar_add_switch_action(toolbar);
lv_obj_add_event_cb(_switchWidget, onSwitchToggled, LV_EVENT_VALUE_CHANGED, this);
_mainWrapper = lv_obj_create(parent);
@@ -342,30 +314,26 @@ void MediaKeys::onShow(AppHandle appHandle, lv_obj_t* parent) {
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)
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
if (tt_lvgl_hardware_keyboard_is_available()) {
lv_obj_add_event_cb(_buttonMatrix, onKeyEvent, LV_EVENT_KEY, this);
_keyHighlightTimer = lv_timer_create(onKeyHighlightTimer, 150, this);
lv_timer_pause(_keyHighlightTimer);
}
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*/) {
_radioEnabling = false;
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;
if (device_has_active_by_type(&KEYBOARD_TYPE)) exitKeyMode();
teardownBt();
_radioEnabling = false;
_radioWasOff = false;
if (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode();
if (_keyHighlightTimer) {
lv_timer_delete(_keyHighlightTimer);
_keyHighlightTimer = nullptr;
+3 -6
View File
@@ -2,11 +2,10 @@
#include <TactilityCpp/App.h>
#include <lvgl.h>
#include <tactility/device.h>
#include <tactility/drivers/bluetooth.h>
#include <tactility/drivers/bluetooth_hid_device.h>
#include <tactility/drivers/keyboard.h>
#include <tt_app.h>
#include <tt_lvgl_keyboard.h>
#include <atomic>
class MediaKeys final : public App {
@@ -25,9 +24,8 @@ class MediaKeys final : public App {
// State - accessed from both LVGL thread and BT callback thread
std::atomic<bool> _isEnabled {false};
std::atomic<bool> _radioEnabling {false}; // true while waiting for radio to come ON
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)
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)
// Static event callbacks
static void onSwitchToggled(lv_event_t* e);
@@ -38,7 +36,6 @@ class MediaKeys final : public App {
static void sendKeyTask(void* param);
// Instance methods called by static callbacks
void teardownBt(); // remove callback + stop HID + restore radio/device state
void handleSwitchToggle(bool enabled);
void handleButtonPress(uint32_t buttonId);
void startHid(); // called once radio is confirmed ON
+2 -2
View File
@@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32s3,esp32p4
app.id=one.tactility.mediakeys
app.version.name=0.6.0
app.version.code=6
app.version.name=0.4.0
app.version.code=4
app.name=Media Keys
app.description=Bluetooth media keys. Touch or Physical Keyboard control\nB - previous, P - play/pause, N - next, M - mute, D - volume down, U - volume up.\nQ or ESC to exit focus.
+16 -16
View File
@@ -4,17 +4,17 @@
#include "drivers/Colors.h"
#include <cstring>
#include <tactility/drivers/display.h>
#include <tt_hal_display.h>
class PixelBuffer {
uint16_t pixelWidth;
uint16_t pixelHeight;
enum DisplayColorFormat colorFormat;
ColorFormat colorFormat;
uint8_t* data;
public:
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, enum DisplayColorFormat colorFormat) :
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, ColorFormat colorFormat) :
pixelWidth(pixelWidth),
pixelHeight(pixelHeight),
colorFormat(colorFormat)
@@ -35,7 +35,7 @@ public:
return pixelHeight;
}
enum DisplayColorFormat getColorFormat() const {
ColorFormat getColorFormat() const {
return colorFormat;
}
@@ -58,14 +58,14 @@ public:
uint8_t getPixelSize() const {
switch (colorFormat) {
case DISPLAY_COLOR_FORMAT_MONOCHROME:
case COLOR_FORMAT_MONOCHROME:
return 1;
case DISPLAY_COLOR_FORMAT_BGR565:
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
case DISPLAY_COLOR_FORMAT_RGB565:
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
case COLOR_FORMAT_BGR565:
case COLOR_FORMAT_BGR565_SWAPPED:
case COLOR_FORMAT_RGB565:
case COLOR_FORMAT_RGB565_SWAPPED:
return 2;
case DISPLAY_COLOR_FORMAT_RGB888:
case COLOR_FORMAT_RGB888:
return 3;
default:
// 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 {
auto address = getPixelAddress(x, y);
switch (colorFormat) {
case DISPLAY_COLOR_FORMAT_MONOCHROME:
case COLOR_FORMAT_MONOCHROME:
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
break;
case DISPLAY_COLOR_FORMAT_BGR565:
case COLOR_FORMAT_BGR565:
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
break;
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: {
case COLOR_FORMAT_BGR565_SWAPPED: {
// TODO: Make proper conversion function
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address;
@@ -96,11 +96,11 @@ public:
*(address + 1) = temp;
break;
}
case DISPLAY_COLOR_FORMAT_RGB565: {
case COLOR_FORMAT_RGB565: {
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
break;
}
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: {
case COLOR_FORMAT_RGB565_SWAPPED: {
// TODO: Make proper conversion function
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address;
@@ -108,7 +108,7 @@ public:
*(address + 1) = temp;
break;
}
case DISPLAY_COLOR_FORMAT_RGB888: {
case COLOR_FORMAT_RGB888: {
uint8_t pixel[3] = { r, g, b };
memcpy(address, pixel, 3);
break;
@@ -1,47 +1,49 @@
#pragma once
#include <tactility/device.h>
#include <tactility/drivers/display.h>
#include <cassert>
#include <tt_hal_display.h>
#include <Tactility/kernel/Kernel.h>
/**
* Wrapper for display_* device driver functions
* Wrapper for tt_hal_display_driver_*
*/
class DisplayDriver {
struct Device* device;
DisplayDriverHandle handle = nullptr;
public:
explicit DisplayDriver(struct Device* device) : device(device) {
device_get(device);
explicit DisplayDriver(DeviceId id) {
assert(tt_hal_display_driver_supported(id));
handle = tt_hal_display_driver_alloc(id);
assert(handle != nullptr);
}
~DisplayDriver() {
device_put(device);
tt_hal_display_driver_free(handle);
}
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const {
return device_try_lock(device, timeout);
return tt_hal_display_driver_lock(handle, timeout);
}
void unlock() const {
device_unlock(device);
tt_hal_display_driver_unlock(handle);
}
uint16_t getWidth() const {
return display_get_resolution_x(device);
return tt_hal_display_driver_get_pixel_width(handle);
}
uint16_t getHeight() const {
return display_get_resolution_y(device);
return tt_hal_display_driver_get_pixel_height(handle);
}
enum DisplayColorFormat getColorFormat() const {
return display_get_color_format(device);
ColorFormat getColorFormat() const {
return tt_hal_display_driver_get_colorformat(handle);
}
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
display_draw_bitmap(device, xStart, yStart, xEnd, yEnd, pixelData);
tt_hal_display_driver_draw_bitmap(handle, xStart, yStart, xEnd, yEnd, pixelData);
}
};
@@ -1,28 +1,28 @@
#pragma once
#include <tactility/device.h>
#include <tactility/drivers/pointer.h>
#include <cassert>
#include <tt_hal_touch.h>
/**
* Wrapper for pointer_* device driver functions
* Wrapper for tt_hal_touch_driver_*
*/
class TouchDriver {
struct Device* device;
TouchDriverHandle handle = nullptr;
public:
explicit TouchDriver(struct Device* device) : device(device) {
device_get(device);
explicit TouchDriver(DeviceId id) {
assert(tt_hal_touch_driver_supported(id));
handle = tt_hal_touch_driver_alloc(id);
assert(handle != nullptr);
}
~TouchDriver() {
device_put(device);
tt_hal_touch_driver_free(handle);
}
bool getTouchedPoints(uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* count, uint8_t maxCount) const {
// 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);
return tt_hal_touch_driver_get_touched_points(handle, x, y, strength, count, maxCount);
}
};
+44 -23
View File
@@ -6,44 +6,65 @@
#include <tt_app.h>
#include <tt_app_alertdialog.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>
#include <tt_lvgl.h>
constexpr auto TAG = "Main";
static void onCreate(AppHandle appHandle, void* data) {
struct Device* display_device;
if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) {
/** 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) {
DeviceId display_id;
if (!findUsableDisplay(display_id)) {
tt_app_stop();
tt_app_alertdialog_start("Error", "No display device was found.", nullptr, 0);
tt_app_alertdialog_start("Error", "The display doesn't support the required features.", nullptr, 0);
return;
}
struct Device* touch_device;
if (device_get_first_active_by_type(&POINTER_TYPE, &touch_device) != ERROR_NONE) {
ESP_LOGE(TAG, "No touch device found");
device_put(display_device);
DeviceId touch_id;
if (!findUsableTouch(touch_id)) {
tt_app_stop();
tt_app_alertdialog_start("Error", "No touch device was found.", nullptr, 0);
tt_app_alertdialog_start("Error", "The touch driver doesn't support the required features.", nullptr, 0);
return;
}
// Stop LVGL first (because it's currently using the drivers we want to use)
module_stop(&lvgl_module);
tt_lvgl_stop();
ESP_LOGI(TAG, "Creating display driver");
auto display = new DisplayDriver(display_device);
device_put(display_device);
auto display = new DisplayDriver(display_id);
ESP_LOGI(TAG, "Creating touch driver");
auto touch = new TouchDriver(touch_device);
device_put(touch_device);
auto touch = new TouchDriver(touch_id);
// Run the main logic
ESP_LOGI(TAG, "Running application");
@@ -61,9 +82,9 @@ static void onCreate(AppHandle appHandle, void* data) {
static void onDestroy(AppHandle appHandle, void* data) {
// Restart LVGL to resume rendering of regular apps
if (!module_is_started(&lvgl_module)) {
if (!tt_lvgl_is_started()) {
ESP_LOGI(TAG, "Restarting LVGL");
module_start(&lvgl_module);
tt_lvgl_start();
}
}
+2 -2
View File
@@ -2,6 +2,6 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.mystifydemo
app.version.name=0.8.0
app.version.code=8
app.version.name=0.6.0
app.version.code=6
app.name=Mystify Demo
@@ -6,11 +6,11 @@ if (DEFINED ENV{TACTILITY_SDK_PATH})
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
else()
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
endif()
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
project(EspNowBridge)
tactility_project(EspNowBridge)
project(RobotArm)
tactility_project(RobotArm)
+6
View File
@@ -0,0 +1,6 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
idf_component_register(
SRCS ${SOURCE_FILES}
REQUIRES TactilitySDK lwip
)
target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-format-truncation -Wno-unused-but-set-variable -Wno-unused-function)
+429
View File
@@ -0,0 +1,429 @@
#include <tt_app.h>
#include <tt_lvgl_toolbar.h>
#include <tactility/lvgl_fonts.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <lwip/sockets.h>
#include <lwip/inet.h>
#include "esp_log.h"
#define TAG "RobotArm"
#define ARM_HOST "192.168.68.103"
#define ARM_PORT 80
#define ARM_PATH "/api/mcp"
#define NUM_JOINTS 6
#define SEQ_MAX 16
typedef struct {
const char* name;
const char* cute;
uint32_t bg;
uint32_t accent;
int home, rmin, rmax;
int value, last_sent;
} Joint;
static Joint joints[NUM_JOINTS] = {
{"base","Base",0xFFD6E0,0xFF8FA8,70,0,180,70,-1},
{"shoulder","Shldr",0xD6E8FF,0x8FB6FF,40,0,70,40,-1},
{"elbow","Elbow",0xFFE8C5,0xFFB86A,20,0,120,20,-1},
{"pitch","Pitch",0xD5F0D5,0x88D488,90,30,150,90,-1},
{"roll","Roll",0xE8D5FF,0xB088FF,90,30,150,90,-1},
{"gripper","Grip",0xFFF0B3,0xFFD060,90,0,180,90,-1},
};
typedef struct { int v[NUM_JOINTS]; } Frame;
static Frame seq[SEQ_MAX];
static int seq_len = 0, seq_idx = -1;
static bool seq_playing = false;
static int seq_play_pos = 0;
typedef struct {
AppHandle app;
lv_obj_t* status;
lv_obj_t* sliders[6];
lv_obj_t* vals[6];
lv_obj_t* seq_label;
lv_obj_t* play_label;
lv_obj_t* blocks[SEQ_MAX];
int pend[6];
bool has[6];
int ticks[6];
lv_timer_t* poll;
lv_timer_t* seq_timer;
} Ctx;
static Ctx* g = NULL;
static uint16_t my_htons(uint16_t v){ return (v<<8)|(v>>8); }
static int http_post(const char* host,int port,const char* path,const char* body,char* out,size_t olen){
int fd=lwip_socket(AF_INET,SOCK_STREAM,0);
if(fd<0) return -1;
struct sockaddr_in s; memset(&s,0,sizeof(s));
s.sin_family=AF_INET; s.sin_port=my_htons(port); s.sin_addr.s_addr=ipaddr_addr(host);
struct timeval tv={5,0};
lwip_setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv));
lwip_setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv));
if(lwip_connect(fd,(struct sockaddr*)&s,sizeof(s))<0){close(fd);return -2;}
char hdr[256];
int bl=strlen(body);
int hl=snprintf(hdr,sizeof(hdr),"POST %s HTTP/1.1\r\nHost: %s:%d\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",path,host,port,bl);
if(lwip_send(fd,hdr,hl,0)<0){close(fd);return -3;}
if(lwip_send(fd,body,bl,0)<0){close(fd);return -3;}
int tot=0;
while(tot<(int)olen-1){int r=lwip_recv(fd,out+tot,olen-1-tot,0); if(r<=0) break; tot+=r;}
out[tot]='\0'; close(fd);
char* bp=strstr(out,"\r\n\r\n"); if(bp){bp+=4; memmove(out,bp,strlen(bp)+1);}
return tot>0?0:-4;
}
static bool jget(const char* js,const char* key,int* out){
const char* p=strstr(js,key);
if(!p) return false;
p+=strlen(key);
while(*p && *p!=':'){
p++;
if(!*p) return false;
}
p++;
while(*p && (*p==' '||*p=='\t'||*p=='"'||*p=='\\')) p++;
int sign=1;
if(*p=='-'){sign=-1;p++;}
float v=0,frac=0.1f;
bool dot=false,got=false;
while(*p){
if(*p>='0'&&*p<='9'){got=true; if(!dot) v=v*10+(*p-'0'); else {v+=(*p-'0')*frac; frac*=0.1f;}}
else if(*p=='.'&&!dot) dot=true;
else break;
p++;
}
if(!got) return false;
*out=(int)(v*sign+0.5f);
return true;
}
static bool parse_state(const char* r,int* b,int* s,int* e,int* p,int* ro,int* gr){
int v; bool ok=true;
if(jget(r,"base",&v)) *b=v; else ok=false;
if(jget(r,"shoulder",&v)) *s=v; else ok=false;
if(jget(r,"elbow",&v)) *e=v; else ok=false;
if(jget(r,"pitch",&v)) *p=v; else ok=false;
if(jget(r,"roll",&v)) *ro=v; else ok=false;
if(jget(r,"gripper",&v)) *gr=v; else ok=false;
return ok;
}
static bool rpc_get(int* b,int* s,int* e,int* p,int* ro,int* gr){
const char* body="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"get_arm_state\",\"arguments\":{}}}";
char resp[2048]; memset(resp,0,sizeof(resp));
if(http_post(ARM_HOST,ARM_PORT,ARM_PATH,body,resp,sizeof(resp))!=0) return false;
return parse_state(resp,b,s,e,p,ro,gr);
}
static bool rpc_move(const char* j,int a){
char body[300]; snprintf(body,sizeof(body),
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"move_joint\",\"arguments\":{\"joint\":\"%s\",\"angle\":%d,\"duration\":0.5}}}",
j,a);
char r[1024]; memset(r,0,sizeof(r));
return http_post(ARM_HOST,ARM_PORT,ARM_PATH,body,r,sizeof(r))==0;
}
static bool rpc_move_all(int b,int s,int e,int p,int ro,int gr,float dur){
char body[420]; snprintf(body,sizeof(body),
"{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"move_all_joints\",\"arguments\":{\"base\":%d,\"shoulder\":%d,\"elbow\":%d,\"pitch\":%d,\"roll\":%d,\"gripper\":%d,\"duration\":%.1f}}}",
b,s,e,p,ro,gr,dur);
char r[1024]; memset(r,0,sizeof(r));
int rc=http_post(ARM_HOST,ARM_PORT,ARM_PATH,body,r,sizeof(r));
ESP_LOGI(TAG,"move_all %d %d %d rc=%d",b,s,e,rc);
return rc==0;
}
static bool rpc_home(void){
const char* b="{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"home_arm\",\"arguments\":{\"duration\":1.0}}}";
char r[512]; memset(r,0,sizeof(r)); return http_post(ARM_HOST,ARM_PORT,ARM_PATH,b,r,sizeof(r))==0;
}
static void set_status(const char* t){ if(g&&g->status) lv_label_set_text(g->status,t); }
static void apply_ui(void){
if(!g) return;
for(int i=0;i<NUM_JOINTS;i++){
if(g->sliders[i]) lv_slider_set_value(g->sliders[i],joints[i].value,LV_ANIM_OFF);
if(g->vals[i]){char buf[8]; snprintf(buf,sizeof(buf),"%d",joints[i].value); lv_label_set_text(g->vals[i],buf);}
}
}
static void refresh_arm(void){
set_status("Reading .103...");
int b,s,e,p,ro,gr;
if(rpc_get(&b,&s,&e,&p,&ro,&gr)){
joints[0].value=b; joints[1].value=s; joints[2].value=e;
joints[3].value=p; joints[4].value=ro; joints[5].value=gr;
for(int i=0;i<NUM_JOINTS;i++){joints[i].last_sent=joints[i].value; if(g){g->pend[i]=joints[i].value; g->has[i]=false; g->ticks[i]=0;}}
apply_ui();
char buf[32]; snprintf(buf,sizeof(buf),"B%d S%d E%d",b,s,e); set_status(buf);
} else set_status("No .103");
}
static void del_ref_cb(lv_timer_t* t){ lv_timer_delete(t); refresh_arm(); }
static void init_cb(lv_timer_t* t){ lv_timer_delete(t); refresh_arm(); }
static void poll_cb(lv_timer_t* t){
(void)t; if(!g) return;
for(int i=0;i<NUM_JOINTS;i++){
if(!g->has[i]) continue;
g->ticks[i]++; if(g->ticks[i]<3) continue;
g->has[i]=false; g->ticks[i]=0;
int tgt=g->pend[i]; if(joints[i].last_sent==tgt) continue;
joints[i].last_sent=tgt; joints[i].value=tgt;
char buf[20]; snprintf(buf,sizeof(buf),"%s %d",joints[i].cute,tgt); set_status(buf);
bool ok=rpc_move(joints[i].name,tgt);
snprintf(buf,sizeof(buf),"%s %d %s",joints[i].cute,tgt,ok?"ok":"fail"); set_status(buf);
break;
}
}
static void slider_cb(lv_event_t* e){
int idx=(int)(intptr_t)lv_event_get_user_data(e);
int v=(int)lv_slider_get_value(lv_event_get_target(e));
joints[idx].value=v;
if(g){
if(g->vals[idx]){char b[8]; snprintf(b,sizeof(b),"%d",v); lv_label_set_text(g->vals[idx],b);}
g->pend[idx]=v; g->has[idx]=true; g->ticks[idx]=0;
}
}
/* ── sequencer ── */
static void update_seq_ui(void){
if(!g) return;
if(g->seq_label){
if(seq_len==0) lv_label_set_text(g->seq_label,"empty");
else {char b[16]; snprintf(b,sizeof(b),"%d/%d", (seq_idx>=0?seq_idx+1:seq_len), seq_len); lv_label_set_text(g->seq_label,b);}
}
for(int i=0;i<SEQ_MAX;i++){
if(!g->blocks[i]) continue;
if(i<seq_len){
lv_obj_set_style_bg_opa(g->blocks[i],LV_OPA_COVER,0);
if(i==seq_idx){
lv_obj_set_style_border_width(g->blocks[i],3,0);
lv_obj_set_style_border_color(g->blocks[i],lv_color_hex(0xFFFFFF),0);
} else {
lv_obj_set_style_border_width(g->blocks[i],2,0);
lv_obj_set_style_border_color(g->blocks[i],lv_color_hex(0x3A3A3A),0);
}
} else {
lv_obj_set_style_bg_opa(g->blocks[i],LV_OPA_30,0);
lv_obj_set_style_border_width(g->blocks[i],0,0);
}
}
}
static void load_frame(int fidx){
if(fidx<0||fidx>=seq_len) return;
for(int i=0;i<NUM_JOINTS;i++){joints[i].value=seq[fidx].v[i]; joints[i].last_sent=joints[i].value; if(g){g->pend[i]=joints[i].value; g->has[i]=false;}}
apply_ui(); seq_idx=fidx; update_seq_ui();
char b[20]; snprintf(b,sizeof(b),"Frame %d",fidx+1); set_status(b);
}
static void seq_add_cb(void){ if(seq_len>=SEQ_MAX){set_status("Seq full"); return;} for(int i=0;i<NUM_JOINTS;i++) seq[seq_len].v[i]=joints[i].value; seq_len++; seq_idx=seq_len-1; update_seq_ui(); char b[16]; snprintf(b,sizeof(b),"+ %d",seq_len); set_status(b); }
static void seq_rem_cb(void){
if(seq_len==0||seq_idx<0){set_status("Nothing"); return;}
int rem=seq_idx;
for(int f=rem;f<seq_len-1;f++) seq[f]=seq[f+1];
seq_len--; if(seq_len==0){seq_idx=-1; update_seq_ui(); set_status("- empty"); return;}
if(seq_idx>=seq_len) seq_idx=seq_len-1;
load_frame(seq_idx);
}
static void seq_prev_cb(void){ if(seq_len==0) return; int n=(seq_idx<=0)?seq_len-1:seq_idx-1; load_frame(n); }
static void seq_next_cb(void){ if(seq_len==0) return; int n=(seq_idx>=seq_len-1)?0:seq_idx+1; load_frame(n); }
static void seq_timer_cb(lv_timer_t* t){
(void)t; if(!seq_playing||seq_len==0) return;
int f=seq_play_pos%seq_len;
int* v=seq[f].v;
if(!rpc_move_all(v[0],v[1],v[2],v[3],v[4],v[5],0.8f)){
set_status("Seq fail"); seq_playing=false;
if(g&&g->play_label) lv_label_set_text(g->play_label,"Play");
return;
}
for(int i=0;i<NUM_JOINTS;i++){joints[i].value=v[i]; joints[i].last_sent=v[i]; if(g){g->pend[i]=v[i]; g->has[i]=false;}}
apply_ui(); seq_idx=f; update_seq_ui();
char b[20]; snprintf(b,sizeof(b),"Play %d/%d",f+1,seq_len); set_status(b);
seq_play_pos++; if(seq_play_pos>=seq_len) seq_play_pos=0;
}
static void seq_play_cb(lv_event_t* e){
(void)e; if(seq_len==0){set_status("No frames"); return;}
seq_playing=!seq_playing;
if(g&&g->play_label) lv_label_set_text(g->play_label, seq_playing?"Pause":"Play");
if(seq_playing){seq_play_pos=(seq_idx>=0?seq_idx:0); set_status("Playing");} else set_status("Paused");
}
static void seq_add_ev(lv_event_t* e){(void)e; seq_add_cb();}
static void seq_rem_ev(lv_event_t* e){(void)e; seq_rem_cb();}
static void seq_prev_ev(lv_event_t* e){(void)e; seq_prev_cb();}
static void seq_next_ev(lv_event_t* e){(void)e; seq_next_cb();}
static void block_click_cb(lv_event_t* e){int idx=(int)(intptr_t)lv_event_get_user_data(e); if(idx<0||idx>=seq_len) return; load_frame(idx);}
static void home_cb(lv_event_t* e){(void)e; set_status("Homing..."); if(rpc_home()){lv_timer_create(del_ref_cb,1200,NULL); set_status("Homed :3");} else set_status("Fail");}
static void read_cb(lv_event_t* e){(void)e; refresh_arm();}
static void open_cb(lv_event_t* e){(void)e; joints[5].value=0; apply_ui(); rpc_move("gripper",0); set_status("Open");}
static void close_cb(lv_event_t* e){(void)e; joints[5].value=180; apply_ui(); rpc_move("gripper",180); set_status("Close");}
static void onShow(AppHandle app,void* data,lv_obj_t* parent){
(void)data;
Ctx* c=(Ctx*)calloc(1,sizeof(Ctx)); if(!c) return;
c->app=app; g=c;
for(int i=0;i<NUM_JOINTS;i++){joints[i].last_sent=-1; c->pend[i]=joints[i].value; c->has[i]=false;}
lv_obj_t* tb=tt_lvgl_toolbar_create_for_app(parent,app); lv_obj_align(tb,LV_ALIGN_TOP_MID,0,0);
lv_obj_t* root=lv_obj_create(parent);
lv_obj_set_size(root,LV_PCT(100),LV_PCT(100));
lv_obj_set_style_pad_all(root,2,0); lv_obj_set_style_pad_top(root,34,0);
lv_obj_set_style_border_width(root,0,0);
lv_obj_set_style_bg_color(root,lv_color_hex(0xFFF8F0),0);
lv_obj_set_style_bg_opa(root,LV_OPA_COVER,0);
lv_obj_set_scroll_dir(root, LV_DIR_VER);
lv_obj_t* hdr=lv_obj_create(root); lv_obj_set_size(hdr,LV_PCT(100),16);
lv_obj_set_style_border_width(hdr,0,0); lv_obj_set_style_bg_opa(hdr,LV_OPA_TRANSP,0);
lv_obj_set_style_pad_all(hdr,0,0); lv_obj_set_pos(hdr,0,0);
lv_obj_clear_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* title=lv_label_create(hdr); lv_label_set_text(title,"Robot Arm :3");
lv_obj_set_style_text_font(title,lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(title,lv_color_hex(0x3D2B5A),0); lv_obj_set_pos(title,2,0);
c->status=lv_label_create(hdr); lv_label_set_text(c->status,"Conn .103...");
lv_obj_set_style_text_font(c->status,lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(c->status,lv_color_hex(0xA08090),0); lv_obj_set_pos(c->status,90,0);
lv_obj_t* brow=lv_obj_create(root); lv_obj_set_size(brow,LV_PCT(100),20);
lv_obj_set_style_border_width(brow,0,0); lv_obj_set_style_bg_opa(brow,LV_OPA_TRANSP,0);
lv_obj_set_style_pad_all(brow,0,0); lv_obj_set_pos(brow,0,16);
lv_obj_clear_flag(brow, LV_OBJ_FLAG_SCROLLABLE);
struct { const char* t; uint32_t col; lv_event_cb_t cb; } btns[]={
{"Home",0xFFD6E0,home_cb},{"Read",0xD6E8FF,read_cb},{"Open",0xD5F0D5,open_cb},{"Close",0xFFE8C5,close_cb},};
for(int i=0;i<4;i++){
lv_obj_t* b=lv_btn_create(brow); lv_obj_set_size(b,56,18);
lv_obj_set_style_bg_color(b,lv_color_hex(btns[i].col),0); lv_obj_set_style_radius(b,8,0);
lv_obj_set_pos(b,i*62+2,0);
lv_obj_t* l=lv_label_create(b); lv_label_set_text(l,btns[i].t);
lv_obj_set_style_text_font(l,lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(l,lv_color_hex(0x4A356A),0); lv_obj_center(l);
lv_obj_add_event_cb(b,btns[i].cb,LV_EVENT_CLICKED,NULL);
}
/* ── large nice vertical sliders: 3 cols, 2x height 22x72 per request ── */
lv_obj_t* grid=lv_obj_create(root);
lv_obj_set_size(grid,LV_PCT(100),196);
lv_obj_set_style_border_width(grid,0,0); lv_obj_set_style_bg_opa(grid,LV_OPA_TRANSP,0);
lv_obj_set_style_pad_all(grid,2,0); lv_obj_set_pos(grid,0,36);
lv_obj_clear_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
for(int i=0;i<NUM_JOINTS;i++){
int col=i%3, row=i/3;
lv_obj_t* card=lv_obj_create(grid);
lv_obj_set_size(card,102,96);
lv_obj_set_pos(card,col*104+2,row*98);
lv_obj_set_style_bg_color(card,lv_color_hex(joints[i].bg),0);
lv_obj_set_style_bg_opa(card,LV_OPA_90,0);
lv_obj_set_style_radius(card,12,0);
lv_obj_set_style_border_width(card,0,0);
lv_obj_set_style_pad_all(card,3,0);
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* name=lv_label_create(card);
lv_label_set_text(name,joints[i].cute);
lv_obj_set_style_text_font(name,lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(name,lv_color_hex(0x4A356A),0); lv_obj_set_pos(name,2,0);
c->vals[i]=lv_label_create(card);
char vb[8]; snprintf(vb,sizeof(vb),"%d",joints[i].value);
lv_label_set_text(c->vals[i],vb);
lv_obj_set_style_text_font(c->vals[i],lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(c->vals[i],lv_color_hex(0xC04060),0); lv_obj_set_pos(c->vals[i],40,0);
lv_obj_t* rlbl=lv_label_create(card);
char rb[12]; snprintf(rb,sizeof(rb),"%d-%d",joints[i].rmin,joints[i].rmax);
lv_label_set_text(rlbl,rb);
lv_obj_set_style_text_font(rlbl,lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(rlbl,lv_color_hex(0xA090A0),0); lv_obj_set_pos(rlbl,58,0);
lv_obj_t* sl=lv_slider_create(card);
c->sliders[i]=sl;
lv_obj_set_size(sl,22,72);
lv_obj_set_pos(sl,40,20);
lv_slider_set_range(sl,joints[i].rmin,joints[i].rmax);
lv_slider_set_value(sl,joints[i].value,LV_ANIM_OFF);
lv_obj_set_style_bg_color(sl,lv_color_hex(0xFFFFFF),LV_PART_MAIN);
lv_obj_set_style_bg_opa(sl,LV_OPA_80,LV_PART_MAIN);
lv_obj_set_style_radius(sl,10,LV_PART_MAIN);
lv_obj_set_style_bg_color(sl,lv_color_hex(joints[i].accent),LV_PART_INDICATOR);
lv_obj_set_style_radius(sl,10,LV_PART_INDICATOR);
lv_obj_set_style_bg_color(sl,lv_color_hex(0xFFFFFF),LV_PART_KNOB);
lv_obj_set_style_border_color(sl,lv_color_hex(joints[i].accent),LV_PART_KNOB);
lv_obj_set_style_border_width(sl,2,LV_PART_KNOB);
lv_obj_set_style_radius(sl,12,LV_PART_KNOB);
lv_obj_set_style_pad_all(sl,3,LV_PART_KNOB);
lv_obj_add_event_cb(sl,slider_cb,LV_EVENT_VALUE_CHANGED,(void*)(intptr_t)i);
}
/* ── sequencer at bottom, NOT fixed — scrollable with content ──
visual: colored blocks, no info, current highlighted, larger buttons */
lv_obj_t* seqbar=lv_obj_create(root);
lv_obj_set_size(seqbar,316,96);
lv_obj_set_pos(seqbar,2,232);
lv_obj_clear_flag(seqbar, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_color(seqbar,lv_color_hex(0xF0E8FF),0);
lv_obj_set_style_bg_opa(seqbar,LV_OPA_90,0);
lv_obj_set_style_radius(seqbar,12,0);
lv_obj_set_style_border_width(seqbar,1,0);
lv_obj_set_style_border_color(seqbar,lv_color_hex(0xD0C0E0),0);
lv_obj_set_style_pad_all(seqbar,4,0);
lv_obj_t* seq_t=lv_label_create(seqbar); lv_label_set_text(seq_t,"Seq");
lv_obj_set_style_text_font(seq_t,lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(seq_t,lv_color_hex(0x3D2B5A),0); lv_obj_set_pos(seq_t,2,0);
c->seq_label=lv_label_create(seqbar); lv_label_set_text(c->seq_label,"empty");
lv_obj_set_style_text_font(c->seq_label,lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(c->seq_label,lv_color_hex(0x6A5A7A),0); lv_obj_set_pos(c->seq_label,30,0);
struct { const char* t; uint32_t col; lv_event_cb_t cb; int play; } sbtns[]={
{"-",0xFFB7B7,seq_rem_ev,0},{"<",0xD6E8FF,seq_prev_ev,0},
{"Play",0xC5F5C5,seq_play_cb,1},{">",0xD6E8FF,seq_next_ev,0},{"+",0xFFE8A0,seq_add_ev,0},};
for(int i=0;i<5;i++){
lv_obj_t* b=lv_btn_create(seqbar);
lv_obj_set_size(b,(i==2)?56:38,32);
lv_obj_set_style_bg_color(b,lv_color_hex(sbtns[i].col),0);
lv_obj_set_style_radius(b,8,0);
int xs[5]={64,110,158,220,260};
lv_obj_set_pos(b,xs[i],0);
lv_obj_t* l=lv_label_create(b); if(sbtns[i].play) c->play_label=l;
lv_label_set_text(l,sbtns[i].t);
lv_obj_set_style_text_font(l,lvgl_get_text_font(FONT_SIZE_SMALL),0);
lv_obj_set_style_text_color(l,lv_color_hex(0x2A2A5A),0); lv_obj_center(l);
lv_obj_add_event_cb(b,sbtns[i].cb,LV_EVENT_CLICKED,NULL);
}
uint32_t blk_cols[16]={
0xFF8FA8,0x8FB6FF,0xFFB86A,0x88D488,0xB088FF,0xFFD060,0xFF7AA2,0x7AC8FF,
0xFDBA74,0x86EFAC,0xA78BFA,0xFDE68A,0xFCA5A5,0x93C5FD,0xBEF264,0xFDA4AF
};
for(int i=0;i<SEQ_MAX;i++){
lv_obj_t* blk=lv_btn_create(seqbar);
lv_obj_set_size(blk,22,22);
lv_obj_set_pos(blk,(i%8)*24+2,(i/8)*24+38);
lv_obj_set_style_bg_color(blk,lv_color_hex(blk_cols[i]),0);
lv_obj_set_style_radius(blk,6,0);
lv_obj_set_style_border_width(blk,0,0);
lv_obj_set_style_bg_opa(blk,LV_OPA_30,0);
c->blocks[i]=blk;
lv_obj_add_event_cb(blk,block_click_cb,LV_EVENT_CLICKED,(void*)(intptr_t)i);
}
update_seq_ui();
c->poll=lv_timer_create(poll_cb,100,NULL);
c->seq_timer=lv_timer_create(seq_timer_cb,1300,NULL);
lv_timer_create(init_cb,600,NULL);
}
static void onHide(AppHandle app,void* data){
(void)app;(void)data;
if(g){
if(g->poll) lv_timer_delete(g->poll);
if(g->seq_timer) lv_timer_delete(g->seq_timer);
free(g); g=NULL;
}
seq_playing=false;
}
int main(int argc,char* argv[]){(void)argc;(void)argv; tt_app_register((AppRegistration){.onShow=onShow,.onHide=onHide}); return 0;}
+13
View File
@@ -0,0 +1,13 @@
[manifest]
version=0.1
[target]
sdk=0.8.0-dev
platforms=esp32s3
[app]
id=one.tactility.robotarm
versionName=0.1.0
versionCode=1
name=Robot Arm
description=Cute controller for MCP robot arm
+2 -3
View File
@@ -8,13 +8,12 @@
#include <functional>
#include <tt_app_alertdialog.h>
#include <tt_lvgl.h>
#include <TactilityCpp/LvglLock.h>
#include <TactilityCpp/Preferences.h>
#include <tactility/device.h>
#include <tactility/drivers/uart_controller.h>
constexpr TickType_t LVGL_DEFAULT_LOCK_TIME = 500; // 500 ticks = 500 ms
class ConnectView final : public View {
public:
@@ -46,7 +45,7 @@ private:
void onConnect() {
auto lock = lvglLock.asScopedLock();
if (!lock.lock(LVGL_DEFAULT_LOCK_TIME)) {
if (!lock.lock(TT_LVGL_DEFAULT_LOCK_TIME)) {
return;
}
@@ -7,6 +7,8 @@
#include <sstream>
#include <lvgl.h>
#include <tt_lvgl.h>
#include <Tactility/RecursiveMutex.h>
#include <Tactility/Thread.h>
#include <TactilityCpp/LvglLock.h>
@@ -1,5 +1,5 @@
#include "SerialConsole.h"
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
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_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = lvgl_toolbar_create(parent, "Serial Console");
auto* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
disconnectButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_POWER, onDisconnectPressed, this);
disconnectButton = tt_lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_POWER, onDisconnectPressed, this);
lv_obj_add_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN);
wrapperWidget = lv_obj_create(parent);
+2 -2
View File
@@ -2,6 +2,6 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.serialconsole
app.version.name=0.9.0
app.version.code=9
app.version.name=0.7.0
app.version.code=7
app.name=Serial Console
+4 -4
View File
@@ -5,15 +5,15 @@
#include "Snake.h"
#include <inttypes.h>
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
#include <tt_app_alertdialog.h>
#include <tt_app_selectiondialog.h>
#include <tt_preferences.h>
#include <TactilityCpp/LvglLock.h>
#include <lvgl/lvgl.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_module.h>
#include <tactility/lvgl_fonts.h>
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);
// Create toolbar
toolbar = lvgl_toolbar_create(parent, "Snake");
toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
// Create main wrapper
+3 -4
View File
@@ -7,8 +7,7 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <tactility/device.h>
#include <tactility/drivers/keyboard.h>
#include <tt_lvgl_keyboard.h>
// Forward declarations
static void game_play_event(lv_event_t* e);
@@ -37,7 +36,7 @@ static void delete_event(lv_event_t* e) {
}
// Restore edit mode and remove from group before cleanup
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
if (tt_lvgl_hardware_keyboard_is_available()) {
lv_group_t* group = lv_group_get_default();
if (group) lv_group_set_editing(group, false);
lv_group_remove_obj(game->container);
@@ -398,7 +397,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);
// Set up keyboard focus if available
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
if (tt_lvgl_hardware_keyboard_is_available()) {
lv_group_t* group = lv_group_get_default();
if (group) {
lv_group_add_obj(group, game->container);
+2 -2
View File
@@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.snake
app.version.name=0.10.0
app.version.code=10
app.version.name=0.8.0
app.version.code=8
app.name=Snake
app.description=Classic Snake game
+6 -5
View File
@@ -5,7 +5,7 @@
#include "TamaTac.h"
#include "SpriteData.h"
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
#include <tt_app_alertdialog.h>
#include <Tactility/kernel/Kernel.h>
#include <freertos/FreeRTOS.h>
@@ -57,6 +57,7 @@ void TamaTac::onShow(AppHandle context, lv_obj_t* parent) {
if (sfxEngine == nullptr) {
sfxEngine = new SfxEngine();
sfxEngine->start();
sfxEngine->applyVolumePreset(SfxEngine::VolumePreset::Normal);
// Load settings
bool soundEnabled;
@@ -86,11 +87,11 @@ void TamaTac::onShow(AppHandle context, lv_obj_t* parent) {
lv_obj_set_style_pad_all(parent, 0, 0);
lv_obj_set_style_pad_row(parent, 0, 0);
toolbar = lvgl_toolbar_create(parent, "TamaTac");
toolbar = tt_lvgl_toolbar_create_for_app(parent, context);
menuButton = lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onMenuClicked, this);
lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onCleanClicked, this);
lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_REFRESH, onResetClicked, this);
menuButton = tt_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);
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_REFRESH, onResetClicked, this);
wrapperWidget = lv_obj_create(parent);
lv_obj_set_width(wrapperWidget, LV_PCT(100));
+2 -2
View File
@@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.tamatac
app.version.name=0.5.0
app.version.code=5
app.version.name=0.4.0
app.version.code=4
app.name=TamaTac
app.description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
+18 -13
View File
@@ -1,10 +1,11 @@
#include "TodoList.h"
#include <tt_app.h>
#include <tactility/filesystem/file_mutex.h>
#include <tt_lock.h>
#include <Tactility/kernel/Kernel.h>
#include <lvgl/widgets/toolbar.h>
#include <lvgl/lvgl.h>
#include <lvgl/fonts.h>
#include <tt_lvgl_toolbar.h>
#include <tt_lvgl_keyboard.h>
#include <tactility/lvgl_module.h>
#include <tactility/lvgl_fonts.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -59,9 +60,9 @@ void TodoList::saveTodos() {
char savePath[256];
if (!getSaveFilePath(savePath, sizeof(savePath))) return;
struct FileMutex mutex;
file_mutex_get(&mutex, savePath);
file_mutex_lock(&mutex);
auto lock = tt_lock_alloc_for_path(savePath);
if (!lock) return;
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
FILE* f = fopen(savePath, "w");
if (f) {
for (int i = 0; i < count; i++) {
@@ -69,17 +70,19 @@ void TodoList::saveTodos() {
}
fclose(f);
}
file_mutex_unlock(&mutex);
tt_lock_release(lock);
}
tt_lock_free(lock);
}
void TodoList::loadTodos() {
char savePath[256];
if (!getSaveFilePath(savePath, sizeof(savePath))) return;
struct FileMutex mutex;
file_mutex_get(&mutex, savePath);
auto lock = tt_lock_alloc_for_path(savePath);
if (!lock) return;
file_mutex_lock(&mutex);
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
count = 0;
FILE* f = fopen(savePath, "r");
if (f) {
@@ -100,7 +103,9 @@ void TodoList::loadTodos() {
}
fclose(f);
}
file_mutex_unlock(&mutex);
tt_lock_release(lock);
}
tt_lock_free(lock);
}
/* ── UI Helpers ───────────────────────────────────────────────────── */
@@ -272,7 +277,7 @@ void TodoList::onShow(AppHandle app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
/* Toolbar */
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Todo List");
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* countWrapper = lv_obj_create(toolbar);
+2 -2
View File
@@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.todolist
app.version.name=0.7.0
app.version.code=7
app.version.name=0.5.0
app.version.code=5
app.name=Todo List
app.description=Simple task list manager
+4 -4
View File
@@ -5,12 +5,12 @@
#include "TwoEleven.h"
#include <inttypes.h>
#include <lvgl/widgets/toolbar.h>
#include <tt_lvgl_toolbar.h>
#include <tt_app_alertdialog.h>
#include <tt_app_selectiondialog.h>
#include <tt_preferences.h>
#include <lvgl/lvgl.h>
#include <lvgl/fonts.h>
#include <tactility/lvgl_module.h>
#include <tactility/lvgl_fonts.h>
#include <TactilityCpp/LvglLock.h>
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);
// Create toolbar
toolbar = lvgl_toolbar_create(parent, "2048");
toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
// Create main wrapper
+3 -4
View File
@@ -3,8 +3,7 @@
#include "TwoElevenHelpers.h"
#include <stdlib.h>
#include <string.h>
#include <tactility/device.h>
#include <tactility/drivers/keyboard.h>
#include <tt_lvgl_keyboard.h>
static void game_play_event(lv_event_t * e);
static void btnm_event_cb(lv_event_t * e);
@@ -19,7 +18,7 @@ static void delete_event(lv_event_t * e)
twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(obj);
if (game_2048) {
// Restore edit mode and remove from group before cleanup
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
if (tt_lvgl_hardware_keyboard_is_available()) {
lv_group_t* group = lv_group_get_default();
if (group) lv_group_set_editing(group, false);
lv_group_remove_obj(game_2048->btnm);
@@ -141,7 +140,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(obj, delete_event, LV_EVENT_DELETE, NULL);
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
if (tt_lvgl_hardware_keyboard_is_available()) {
lv_group_t* group = lv_group_get_default();
if (group) {
lv_group_add_obj(group, game_2048->btnm);
+2 -2
View File
@@ -2,7 +2,7 @@ manifest.version=0.2
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4
app.id=one.tactility.twoeleven
app.version.name=0.9.0
app.version.code=9
app.version.name=0.7.0
app.version.code=7
app.name=2048
app.description=A fun, customizable 2048 sliding tile game for tactility!\nSlide tiles to combine numbers and reach 2048.\nChoose grid sizes: 3x3 (easy), 4x4 (classic), 5x5, or 6x6 (expert).
@@ -10,15 +10,13 @@ UnitDualButton::~UnitDualButton() {
bool UnitDualButton::begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB) {
if (!controller) return false;
gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_UP;
descA_ = gpio_descriptor_acquire(controller, pinA, flags, GPIO_OWNER_GPIO);
descA_ = gpio_descriptor_acquire(controller, pinA, GPIO_OWNER_GPIO);
if (!descA_) {
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinA);
return false;
}
descB_ = gpio_descriptor_acquire(controller, pinB, flags, GPIO_OWNER_GPIO);
descB_ = gpio_descriptor_acquire(controller, pinB, GPIO_OWNER_GPIO);
if (!descB_) {
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinB);
gpio_descriptor_release(descA_);
@@ -26,6 +24,20 @@ bool UnitDualButton::begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB)
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;
ESP_LOGI(TAG, "DualButton ready on pins %d/%d", (int)pinA, (int)pinB);
return true;
+8 -10
View File
@@ -26,7 +26,6 @@
#include <cstdint>
#include <tactility/device.h>
#include <tactility/drivers/audio_stream.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
@@ -162,6 +161,12 @@ public:
// Settings
void setEnabled(bool enabled) { enabled_ = 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)
void setPolyphonicGateEnabled(bool enabled) { polyphonicGateEnabled_ = enabled; }
@@ -272,21 +277,14 @@ private:
// State
//--------------------------------------------------------------------------
Device* audioStreamDevice_ = nullptr;
AudioStreamHandle audioStreamHandle_ = nullptr;
Device* i2sDevice_ = nullptr;
TaskHandle_t task_ = nullptr;
SemaphoreHandle_t stopSemaphore_ = nullptr; // Signaled when audio task exits
QueueHandle_t msgQueue_ = nullptr;
volatile bool running_ = false;
volatile bool enabled_ = true;
// 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;
volatile float masterVolume_ = 0.5f;
// Polyphonic gate
volatile bool polyphonicGateEnabled_ = true;
+5 -4
View File
@@ -35,8 +35,11 @@ if (!engine->start()) {
ESP_LOGE(TAG, "Failed to start SfxEngine");
return;
}
engine->applyVolumePreset(SfxEngine::VolumePreset::Normal);
engine->play(SfxId::Coin); // Predefined SFX
engine->playNote(0, 60, 200); // Manual: voice 0, C4, 200ms
engine->setVolume(0.7f); // Volume control
engine->stop();
delete engine;
@@ -84,11 +87,9 @@ idf_component_register(
- `void stopVoice(voice)` - Stop specific voice
### Settings
- `void setVolume(float)` - Master volume (0.0-1.0, exponential curve)
- `void setEnabled(bool)` - Mute/unmute
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.
- `void applyVolumePreset(VolumePreset)` - Apply Quiet/Normal/Loud preset (configures volume, gate, normalization)
### Mixing (consistent with SoundEngine)
- `void setPolyphonicGateEnabled(bool)` - Soft gate when multiple voices clip (default: on)
+64 -56
View File
@@ -9,7 +9,7 @@
#include "SfxEngine.h"
#include "SfxDefinitions.h"
#include <tactility/drivers/audio_stream.h>
#include <tactility/drivers/i2s_controller.h>
#include <cmath>
#include <cstring>
#include "esp_log.h"
@@ -320,17 +320,15 @@ void SfxEngine::fillStereoBuffer(int16_t* buf, int samples) {
// Apply polyphonic soft gate (proportional reduction when clipping threatened)
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)
mix = applyAutoNormalization(mix);
// Brick-wall limiter (final safety net before soft clip)
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
@@ -462,28 +460,14 @@ 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)
self->fillStereoBuffer(self->audioBuffer_, BUFFER_SAMPLES);
// Write to the audio stream (resampled to the codec's native rate transparently)
error_t error = audio_stream_write(self->audioStreamHandle_, self->audioBuffer_,
// Write to I2S
error_t error = i2s_controller_write(self->i2sDevice_, self->audioBuffer_,
sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(100));
if (error != ERROR_NONE) {
ESP_LOGE(TAG, "Audio stream write error");
ESP_LOGE(TAG, "I2S write error");
self->running_ = false;
break;
}
@@ -491,7 +475,7 @@ void SfxEngine::audioTaskFunc(void* param) {
// Flush silence
memset(self->audioBuffer_, 0, sizeof(self->audioBuffer_));
audio_stream_write(self->audioStreamHandle_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(50));
i2s_controller_write(self->i2sDevice_, self->audioBuffer_, sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(50));
ESP_LOGI(TAG, "Audio task exiting");
@@ -510,31 +494,33 @@ void SfxEngine::audioTaskFunc(void* param) {
bool SfxEngine::start() {
if (running_) return true;
// Find audio stream device
audioStreamDevice_ = nullptr;
device_for_each_of_type(&AUDIO_STREAM_TYPE, &audioStreamDevice_, [](Device* device, void* context) {
// Find I2S device
i2sDevice_ = nullptr;
device_for_each_of_type(&I2S_CONTROLLER_TYPE, &i2sDevice_, [](Device* device, void* context) {
if (!device_is_ready(device)) return true;
Device** devicePtr = static_cast<Device**>(context);
*devicePtr = device;
return false;
});
if (audioStreamDevice_ == nullptr) {
ESP_LOGW(TAG, "No audio stream device found");
if (i2sDevice_ == nullptr) {
ESP_LOGW(TAG, "No I2S device found");
return false;
}
// Open output stream (the kernel resamples to the codec's native rate transparently)
AudioStreamConfig config = {
// Configure I2S
I2sConfig config = {
.communication_format = I2S_FORMAT_STAND_I2S,
.sample_rate = SAMPLE_RATE,
.bits_per_sample = 16,
.channels = 2
.channel_left = 0,
.channel_right = 0
};
error_t error = audio_stream_open_output(audioStreamDevice_, &config, &audioStreamHandle_);
error_t error = i2s_controller_set_config(i2sDevice_, &config);
if (error != ERROR_NONE) {
ESP_LOGE(TAG, "Failed to open audio output stream: %s", error_to_string(error));
audioStreamDevice_ = nullptr;
ESP_LOGE(TAG, "Failed to configure I2S: %s", error_to_string(error));
i2sDevice_ = nullptr;
return false;
}
@@ -542,14 +528,12 @@ bool SfxEngine::start() {
msgQueue_ = xQueueCreate(8, sizeof(QueueMsg));
if (msgQueue_ == nullptr) {
ESP_LOGE(TAG, "Failed to create message queue");
audio_stream_close(audioStreamHandle_);
audioStreamHandle_ = nullptr;
audioStreamDevice_ = nullptr;
i2s_controller_reset(i2sDevice_);
i2sDevice_ = nullptr;
return false;
}
// Start audio task
systemVolumePollCounter_ = 0; // poll the system volume immediately on the first iteration
running_ = true;
BaseType_t result = xTaskCreate(audioTaskFunc, "sfxeng", 4096, this, 5, &task_);
if (result != pdPASS) {
@@ -557,9 +541,8 @@ bool SfxEngine::start() {
running_ = false;
vQueueDelete(msgQueue_);
msgQueue_ = nullptr;
audio_stream_close(audioStreamHandle_);
audioStreamHandle_ = nullptr;
audioStreamDevice_ = nullptr;
i2s_controller_reset(i2sDevice_);
i2sDevice_ = nullptr;
return false;
}
@@ -568,22 +551,19 @@ bool SfxEngine::start() {
}
void SfxEngine::stop() {
// 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;
if (!running_) return;
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.
// Create semaphore for deterministic shutdown
stopSemaphore_ = xSemaphoreCreateBinary();
running_ = false;
if (task_ != nullptr && stopSemaphore_ != nullptr) {
if (task_ != nullptr) {
// Wait for audio task to signal completion (up to 500ms)
if (stopSemaphore_ != nullptr) {
xSemaphoreTake(stopSemaphore_, pdMS_TO_TICKS(500));
}
}
task_ = nullptr;
}
if (stopSemaphore_ != nullptr) {
vSemaphoreDelete(stopSemaphore_);
@@ -595,15 +575,43 @@ void SfxEngine::stop() {
msgQueue_ = nullptr;
}
if (audioStreamHandle_ != nullptr) {
audio_stream_close(audioStreamHandle_);
audioStreamHandle_ = nullptr;
if (i2sDevice_ != nullptr) {
i2s_controller_reset(i2sDevice_);
i2sDevice_ = nullptr;
}
audioStreamDevice_ = nullptr;
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) {
if (!running_ || msgQueue_ == nullptr) return;
@@ -1,20 +1,19 @@
#pragma once
#include <Tactility/Lock.h>
#include <lvgl/lvgl.h>
#include <tt_lvgl.h>
class LvglLock final : public tt::Lock {
public:
using tt::Lock::lock;
bool lock(TickType_t timeout) const override {
return lvgl_try_lock(timeout);
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const override {
return tt_lvgl_lock(timeout);
}
void unlock() const override {
lvgl_unlock();
tt_lvgl_unlock();
}
};