Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9db2d3a3fa |
@@ -1,21 +0,0 @@
|
|||||||
name: Publish Apps
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
sdk_version:
|
|
||||||
description: The SDK version that determines the path on the CDN
|
|
||||||
required: true
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: 'composite'
|
|
||||||
steps:
|
|
||||||
- name: 'Download cdn-files'
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: 'cdn-files'
|
|
||||||
path: cdn_files
|
|
||||||
- name: 'Install boto3'
|
|
||||||
shell: bash
|
|
||||||
run: pip install boto3
|
|
||||||
- name: 'Upload files'
|
|
||||||
shell: bash
|
|
||||||
run: python Buildscripts/CDN/upload-app-files.py cdn_files ${{ inputs.sdk_version }} ${{ env.CDN_ID }} ${{ env.CDN_TOKEN_NAME }} ${{ env.CDN_TOKEN_VALUE }}
|
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
name: Release Apps
|
name: Release Apps
|
||||||
|
|
||||||
outputs:
|
|
||||||
sdk_version:
|
|
||||||
description: 'Common SDK version shared by all bundled apps'
|
|
||||||
value: ${{ steps.release.outputs.sdk_version }}
|
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
@@ -16,11 +11,8 @@ runs:
|
|||||||
run: rsync -av downloaded_apps/*/*.app cdn_files/
|
run: rsync -av downloaded_apps/*/*.app cdn_files/
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: 'Create CDN release files'
|
- name: 'Create CDN release files'
|
||||||
id: release
|
run: python release.py cdn_files/
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
|
||||||
python release.py cdn_files/
|
|
||||||
echo "sdk_version=$(cat sdk_version.txt)" >> "$GITHUB_OUTPUT"
|
|
||||||
- name: 'Upload Artifact'
|
- name: 'Upload Artifact'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -12,12 +12,10 @@ jobs:
|
|||||||
Build:
|
Build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
uses: ./.github/actions/build-app
|
uses: ./.github/actions/build-app
|
||||||
with:
|
with:
|
||||||
@@ -25,26 +23,7 @@ jobs:
|
|||||||
Bundle:
|
Bundle:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [Build]
|
needs: [Build]
|
||||||
outputs:
|
|
||||||
sdk_version: ${{ steps.release.outputs.sdk_version }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
id: release
|
|
||||||
uses: ./.github/actions/release-apps
|
uses: ./.github/actions/release-apps
|
||||||
PublishApps:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [Bundle]
|
|
||||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: "Publish Apps"
|
|
||||||
env:
|
|
||||||
CDN_ID: ${{ secrets.CDN_ID }}
|
|
||||||
CDN_TOKEN_NAME: ${{ secrets.CDN_TOKEN_NAME }}
|
|
||||||
CDN_TOKEN_VALUE: ${{ secrets.CDN_TOKEN_VALUE }}
|
|
||||||
uses: ./.github/actions/publish-apps
|
|
||||||
with:
|
|
||||||
sdk_version: ${{ needs.Bundle.outputs.sdk_version }}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "Brainfuck.h"
|
#include "Brainfuck.h"
|
||||||
#include <tt_app.h>
|
#include <tt_app.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -399,11 +399,11 @@ void Brainfuck::onShow(AppHandle app, lv_obj_t* parent) {
|
|||||||
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
lv_obj_t* toolbar = 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);
|
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);
|
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_t* cont = lv_obj_create(parent);
|
||||||
lv_obj_set_width(cont, LV_PCT(100));
|
lv_obj_set_width(cont, LV_PCT(100));
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.brainfuck
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.6.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=6
|
[app]
|
||||||
app.name=Brainfuck interpreter
|
id=one.tactility.brainfuck
|
||||||
app.description=Brainfuck esoteric language interpreter
|
versionName=0.2.0
|
||||||
|
versionCode=2
|
||||||
|
name=Brainfuck interpreter
|
||||||
|
description=Brainfuck esoteric language interpreter
|
||||||
|
|||||||
@@ -7,14 +7,13 @@
|
|||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <tt_preferences.h>
|
#include <tt_preferences.h>
|
||||||
#include <esp_random.h>
|
#include <esp_random.h>
|
||||||
#include <tactility/device.h>
|
#include <tt_lvgl_keyboard.h>
|
||||||
#include <tactility/drivers/keyboard.h>
|
|
||||||
|
|
||||||
#include <lvgl/lvgl.h>
|
#include <tactility/lvgl_module.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
constexpr auto* TAG = "Breakout";
|
constexpr auto* TAG = "Breakout";
|
||||||
|
|
||||||
@@ -125,11 +124,12 @@ void Breakout::onShow(AppHandle appHandle, lv_obj_t* parent) {
|
|||||||
if (!sfxEngine) {
|
if (!sfxEngine) {
|
||||||
sfxEngine = new SfxEngine();
|
sfxEngine = new SfxEngine();
|
||||||
sfxEngine->start();
|
sfxEngine->start();
|
||||||
|
sfxEngine->applyVolumePreset(SfxEngine::VolumePreset::Quiet);
|
||||||
sfxEngine->setEnabled(soundEnabled);
|
sfxEngine->setEnabled(soundEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toolbar
|
// Toolbar
|
||||||
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Breakout");
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
|
||||||
|
|
||||||
// Score wrapper in toolbar
|
// Score wrapper in toolbar
|
||||||
lv_obj_t* scoreWrap = lv_obj_create(toolbar);
|
lv_obj_t* scoreWrap = lv_obj_create(toolbar);
|
||||||
@@ -1330,7 +1330,7 @@ void Breakout::updateMessage() {
|
|||||||
case GameState::Ready: {
|
case GameState::Ready: {
|
||||||
char buf[64];
|
char buf[64];
|
||||||
const char* input_hint = "Touch";
|
const char* input_hint = "Touch";
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
|
if (tt_lvgl_hardware_keyboard_is_available()) {
|
||||||
input_hint = "Space";
|
input_hint = "Space";
|
||||||
}
|
}
|
||||||
if (level > 1) {
|
if (level > 1) {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.breakout
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.7.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=7
|
[app]
|
||||||
app.name=Breakout
|
id=one.tactility.breakout
|
||||||
app.description=Classic brick-breaking arcade game
|
versionName=0.2.0
|
||||||
|
versionCode=2
|
||||||
|
name=Breakout
|
||||||
|
description=Classic brick-breaking arcade game
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <stack>
|
#include <stack>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ void Calculator::onShow(AppHandle appHandle, lv_obj_t* parent) {
|
|||||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
lv_obj_t* toolbar = 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_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.calculator
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.7.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=7
|
[app]
|
||||||
app.name=Calculator
|
id=one.tactility.calculator
|
||||||
|
versionName=0.3.0
|
||||||
|
versionCode=3
|
||||||
|
name=Calculator
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#include "Diceware.h"
|
#include "Diceware.h"
|
||||||
|
|
||||||
#include <tt_app_alertdialog.h>
|
#include <tt_app_alertdialog.h>
|
||||||
#include <tactility/filesystem/file_mutex.h>
|
#include <tt_lock.h>
|
||||||
#include <lvgl/lvgl.h>
|
#include <tt_lvgl.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
|
||||||
#include <esp_random.h>
|
#include <esp_random.h>
|
||||||
#include <esp_log.h>
|
#include <esp_log.h>
|
||||||
@@ -39,17 +39,18 @@ static std::string readWordAtLine(const AppHandle handle, const int lineIndex) {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FileMutex mutex;
|
auto lock = tt_lock_alloc_for_path(path);
|
||||||
file_mutex_get(&mutex, path);
|
|
||||||
std::string word;
|
std::string word;
|
||||||
file_mutex_lock(&mutex);
|
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
|
||||||
FILE* file = fopen(path, "r");
|
FILE* file = fopen(path, "r");
|
||||||
if (file != nullptr) {
|
if (file != nullptr) {
|
||||||
skipNewlines(file, lineIndex);
|
skipNewlines(file, lineIndex);
|
||||||
word = readWord(file);
|
word = readWord(file);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
} else { ESP_LOGE(TAG, "Failed to open %s", path); }
|
} else { ESP_LOGE(TAG, "Failed to open %s", path); }
|
||||||
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;
|
return word;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +87,9 @@ void Diceware::startJob(uint32_t jobWordCount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Diceware::onFinishJob(std::string result) {
|
void Diceware::onFinishJob(std::string result) {
|
||||||
lvgl_lock();
|
tt_lvgl_lock(tt::kernel::MAX_TICKS);
|
||||||
lv_label_set_text(resultLabel, result.c_str());
|
lv_label_set_text(resultLabel, result.c_str());
|
||||||
lvgl_unlock();
|
tt_lvgl_unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Diceware::onClickGenerate(lv_event_t* e) {
|
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_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
auto* toolbar = lvgl_toolbar_create(parent, "Diceware");
|
auto* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
|
||||||
lvgl_toolbar_add_text_button_action(toolbar, "?", onHelpClicked, nullptr);
|
tt_lvgl_toolbar_add_text_button_action(toolbar, "?", onHelpClicked, nullptr);
|
||||||
|
|
||||||
auto* wrapper = lv_obj_create(parent);
|
auto* wrapper = lv_obj_create(parent);
|
||||||
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.diceware
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.8.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=8
|
[app]
|
||||||
app.name=Diceware
|
id=one.tactility.diceware
|
||||||
|
versionName=0.3.0
|
||||||
|
versionCode=3
|
||||||
|
name=Diceware
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "EpubReader.h"
|
#include "EpubReader.h"
|
||||||
#include "HtmlStrip.h" // stripHtmlToText
|
#include "HtmlStrip.h" // stripHtmlToText
|
||||||
#include <tt_bundle.h>
|
#include <tt_bundle.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <tt_app_alertdialog.h>
|
#include <tt_app_alertdialog.h>
|
||||||
#include <tt_app_selectiondialog.h>
|
#include <tt_app_selectiondialog.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
@@ -239,7 +239,7 @@ void EpubReader::onShow(AppHandle app, lv_obj_t* parent) {
|
|||||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
toolbar_ = lvgl_toolbar_create(parent, "Epub Reader");
|
toolbar_ = tt_lvgl_toolbar_create_for_app(parent, app);
|
||||||
|
|
||||||
wrapperWidget_ = lv_obj_create(parent);
|
wrapperWidget_ = lv_obj_create(parent);
|
||||||
lv_obj_set_width(wrapperWidget_, LV_PCT(100));
|
lv_obj_set_width(wrapperWidget_, LV_PCT(100));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "EpubReader.h"
|
#include "EpubReader.h"
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <tactility/filesystem/file_mutex.h>
|
#include <tt_lock.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
#include <Tactility/kernel/Kernel.h>
|
#include <Tactility/kernel/Kernel.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
@@ -43,7 +43,7 @@ void EpubReader::spawnOpenTask(EpubReader* self, bool restore) {
|
|||||||
|
|
||||||
// Show a brief placeholder so old content doesn't linger during the open
|
// Show a brief placeholder so old content doesn't linger during the open
|
||||||
lv_obj_clean(self->wrapperWidget_);
|
lv_obj_clean(self->wrapperWidget_);
|
||||||
lvgl_toolbar_clear_actions(self->toolbar_);
|
tt_lvgl_toolbar_clear_actions(self->toolbar_);
|
||||||
lv_obj_t* lbl = lv_label_create(self->wrapperWidget_);
|
lv_obj_t* lbl = lv_label_create(self->wrapperWidget_);
|
||||||
lv_obj_set_style_pad_all(lbl, 8, 0);
|
lv_obj_set_style_pad_all(lbl, 8, 0);
|
||||||
lv_label_set_text(lbl, restore ? "Loading..." : "Opening...");
|
lv_label_set_text(lbl, restore ? "Loading..." : "Opening...");
|
||||||
@@ -115,9 +115,14 @@ void EpubReader::backgroundOpenTask(void* data) {
|
|||||||
|
|
||||||
// Acquire the filesystem lock before any SD card I/O - prevents concurrent
|
// Acquire the filesystem lock before any SD card I/O - prevents concurrent
|
||||||
// SDMMC access from the background and LVGL tasks (bus errors 0x107/0x108).
|
// SDMMC access from the background and LVGL tasks (bus errors 0x107/0x108).
|
||||||
struct FileMutex mutex;
|
auto lock = tt_lock_alloc_for_path(a->filePath.c_str());
|
||||||
file_mutex_get(&mutex, a->filePath.c_str());
|
if (!tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
|
||||||
file_mutex_lock(&mutex);
|
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)) {
|
if (isTextFile(a->filePath)) {
|
||||||
// Read the entire text file here (under the lock) so asyncOpenComplete
|
// Read the entire text file here (under the lock) so asyncOpenComplete
|
||||||
@@ -141,7 +146,8 @@ void EpubReader::backgroundOpenTask(void* data) {
|
|||||||
a->epub = EpubService::open(a->filePath);
|
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
|
// Signal the LVGL task that the work is done
|
||||||
lv_async_call(asyncOpenComplete, a);
|
lv_async_call(asyncOpenComplete, a);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "EpubReader.h"
|
#include "EpubReader.h"
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
@@ -37,20 +37,20 @@ static void setListBtnLongMode(lv_obj_t* btn, lv_label_long_mode_t mode) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void EpubReader::setReaderToolbarButtons() {
|
void EpubReader::setReaderToolbarButtons() {
|
||||||
lvgl_toolbar_clear_actions(toolbar_);
|
tt_lvgl_toolbar_clear_actions(toolbar_);
|
||||||
lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_PREV, onPrevPressed, this);
|
tt_lvgl_toolbar_add_text_button_action(toolbar_, LV_SYMBOL_PREV, onPrevPressed, this);
|
||||||
if (!textMode_) {
|
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);
|
tt_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_DIRECTORY, onBrowsePressed, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReader::setBrowserToolbarButtons() {
|
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
|
// Show "Use Folder" button when the current browse path isn't already the saved books folder
|
||||||
if (browsePath_ != booksPath_) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32s3,esp32p4
|
[target]
|
||||||
app.id=one.tactility.epubreader
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.5.0
|
platforms=esp32s3,esp32p4
|
||||||
app.version.code=5
|
[app]
|
||||||
app.name=Epub Reader
|
id=one.tactility.epubreader
|
||||||
app.description=Epub and text file reader. Requires PSRAM!
|
versionName=0.1.0
|
||||||
|
versionCode=1
|
||||||
|
name=Epub Reader
|
||||||
|
description=Epub and text file reader. Requires PSRAM!
|
||||||
|
|||||||
Binary file not shown.
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#include "EspNowBridge.h"
|
|
||||||
#include <TactilityCpp/App.h>
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
|
||||||
registerApp<EspNowBridge>();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
#include <Tactility/kernel/Kernel.h>
|
#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 <esp_log.h>
|
||||||
#include <driver/gpio.h>
|
#include <driver/gpio.h>
|
||||||
@@ -19,7 +20,7 @@ void Gpio::updatePinStates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Gpio::updatePinWidgets() {
|
void Gpio::updatePinWidgets() {
|
||||||
lvgl_lock();
|
tt_lvgl_lock(tt::kernel::MAX_TICKS);
|
||||||
for (int j = 0; j < pinStates.size(); ++j) {
|
for (int j = 0; j < pinStates.size(); ++j) {
|
||||||
int level = pinStates[j];
|
int level = pinStates[j];
|
||||||
lv_obj_t* label = pinWidgets[j];
|
lv_obj_t* label = pinWidgets[j];
|
||||||
@@ -34,7 +35,7 @@ void Gpio::updatePinWidgets() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lvgl_unlock();
|
tt_lvgl_unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_obj_t* Gpio::createGpioRowWrapper(lv_obj_t* parent) {
|
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_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
auto* toolbar = 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);
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
// Main content wrapper, enables scrolling content without scrolling the toolbar
|
// Main content wrapper, enables scrolling content without scrolling the toolbar
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.gpio
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.9.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=9
|
[app]
|
||||||
app.name=GPIO
|
id=one.tactility.gpio
|
||||||
|
versionName=0.4.0
|
||||||
|
versionCode=4
|
||||||
|
name=GPIO
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
#include "drivers/Colors.h"
|
#include "drivers/Colors.h"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <tactility/drivers/display.h>
|
#include <tt_hal_display.h>
|
||||||
|
|
||||||
class PixelBuffer {
|
class PixelBuffer {
|
||||||
uint16_t pixelWidth;
|
uint16_t pixelWidth;
|
||||||
uint16_t pixelHeight;
|
uint16_t pixelHeight;
|
||||||
enum DisplayColorFormat colorFormat;
|
ColorFormat colorFormat;
|
||||||
uint8_t* data;
|
uint8_t* data;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, enum DisplayColorFormat colorFormat) :
|
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, ColorFormat colorFormat) :
|
||||||
pixelWidth(pixelWidth),
|
pixelWidth(pixelWidth),
|
||||||
pixelHeight(pixelHeight),
|
pixelHeight(pixelHeight),
|
||||||
colorFormat(colorFormat)
|
colorFormat(colorFormat)
|
||||||
@@ -35,7 +35,7 @@ public:
|
|||||||
return pixelHeight;
|
return pixelHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DisplayColorFormat getColorFormat() const {
|
ColorFormat getColorFormat() const {
|
||||||
return colorFormat;
|
return colorFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,14 +58,14 @@ public:
|
|||||||
|
|
||||||
uint8_t getPixelSize() const {
|
uint8_t getPixelSize() const {
|
||||||
switch (colorFormat) {
|
switch (colorFormat) {
|
||||||
case DISPLAY_COLOR_FORMAT_MONOCHROME:
|
case COLOR_FORMAT_MONOCHROME:
|
||||||
return 1;
|
return 1;
|
||||||
case DISPLAY_COLOR_FORMAT_BGR565:
|
case COLOR_FORMAT_BGR565:
|
||||||
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
|
case COLOR_FORMAT_BGR565_SWAPPED:
|
||||||
case DISPLAY_COLOR_FORMAT_RGB565:
|
case COLOR_FORMAT_RGB565:
|
||||||
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
|
case COLOR_FORMAT_RGB565_SWAPPED:
|
||||||
return 2;
|
return 2;
|
||||||
case DISPLAY_COLOR_FORMAT_RGB888:
|
case COLOR_FORMAT_RGB888:
|
||||||
return 3;
|
return 3;
|
||||||
default:
|
default:
|
||||||
// TODO: Crash with error
|
// TODO: Crash with error
|
||||||
@@ -82,13 +82,13 @@ public:
|
|||||||
void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const {
|
void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const {
|
||||||
auto address = getPixelAddress(x, y);
|
auto address = getPixelAddress(x, y);
|
||||||
switch (colorFormat) {
|
switch (colorFormat) {
|
||||||
case DISPLAY_COLOR_FORMAT_MONOCHROME:
|
case COLOR_FORMAT_MONOCHROME:
|
||||||
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
|
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
|
||||||
break;
|
break;
|
||||||
case DISPLAY_COLOR_FORMAT_BGR565:
|
case COLOR_FORMAT_BGR565:
|
||||||
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||||
break;
|
break;
|
||||||
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: {
|
case COLOR_FORMAT_BGR565_SWAPPED: {
|
||||||
// TODO: Make proper conversion function
|
// TODO: Make proper conversion function
|
||||||
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||||
uint8_t temp = *address;
|
uint8_t temp = *address;
|
||||||
@@ -96,11 +96,11 @@ public:
|
|||||||
*(address + 1) = temp;
|
*(address + 1) = temp;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DISPLAY_COLOR_FORMAT_RGB565: {
|
case COLOR_FORMAT_RGB565: {
|
||||||
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: {
|
case COLOR_FORMAT_RGB565_SWAPPED: {
|
||||||
// TODO: Make proper conversion function
|
// TODO: Make proper conversion function
|
||||||
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||||
uint8_t temp = *address;
|
uint8_t temp = *address;
|
||||||
@@ -108,7 +108,7 @@ public:
|
|||||||
*(address + 1) = temp;
|
*(address + 1) = temp;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DISPLAY_COLOR_FORMAT_RGB888: {
|
case COLOR_FORMAT_RGB888: {
|
||||||
uint8_t pixel[3] = { r, g, b };
|
uint8_t pixel[3] = { r, g, b };
|
||||||
memcpy(address, pixel, 3);
|
memcpy(address, pixel, 3);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,47 +1,49 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <tactility/device.h>
|
#include <cassert>
|
||||||
#include <tactility/drivers/display.h>
|
#include <tt_hal_display.h>
|
||||||
#include <Tactility/kernel/Kernel.h>
|
#include <Tactility/kernel/Kernel.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for display_* device driver functions
|
* Wrapper for tt_hal_display_driver_*
|
||||||
*/
|
*/
|
||||||
class DisplayDriver {
|
class DisplayDriver {
|
||||||
|
|
||||||
struct Device* device;
|
DisplayDriverHandle handle = nullptr;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
explicit DisplayDriver(struct Device* device) : device(device) {
|
explicit DisplayDriver(DeviceId id) {
|
||||||
device_get(device);
|
assert(tt_hal_display_driver_supported(id));
|
||||||
|
handle = tt_hal_display_driver_alloc(id);
|
||||||
|
assert(handle != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
~DisplayDriver() {
|
~DisplayDriver() {
|
||||||
device_put(device);
|
tt_hal_display_driver_free(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const {
|
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 {
|
void unlock() const {
|
||||||
device_unlock(device);
|
tt_hal_display_driver_unlock(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getWidth() const {
|
uint16_t getWidth() const {
|
||||||
return display_get_resolution_x(device);
|
return tt_hal_display_driver_get_pixel_width(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getHeight() const {
|
uint16_t getHeight() const {
|
||||||
return display_get_resolution_y(device);
|
return tt_hal_display_driver_get_pixel_height(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DisplayColorFormat getColorFormat() const {
|
ColorFormat getColorFormat() const {
|
||||||
return display_get_color_format(device);
|
return tt_hal_display_driver_get_colorformat(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
|
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
|
||||||
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
|
#pragma once
|
||||||
|
|
||||||
#include <tactility/device.h>
|
#include <cassert>
|
||||||
#include <tactility/drivers/pointer.h>
|
#include <tt_hal_touch.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for pointer_* device driver functions
|
* Wrapper for tt_hal_touch_driver_*
|
||||||
*/
|
*/
|
||||||
class TouchDriver {
|
class TouchDriver {
|
||||||
|
|
||||||
struct Device* device;
|
TouchDriverHandle handle = nullptr;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
explicit TouchDriver(struct Device* device) : device(device) {
|
explicit TouchDriver(DeviceId id) {
|
||||||
device_get(device);
|
assert(tt_hal_touch_driver_supported(id));
|
||||||
|
handle = tt_hal_touch_driver_alloc(id);
|
||||||
|
assert(handle != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
~TouchDriver() {
|
~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 {
|
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.
|
return tt_hal_touch_driver_get_touched_points(handle, x, y, strength, count, maxCount);
|
||||||
pointer_read_data(device, 0);
|
|
||||||
return pointer_get_touched_points(device, x, y, strength, count, maxCount);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,44 +6,65 @@
|
|||||||
|
|
||||||
#include <tt_app.h>
|
#include <tt_app.h>
|
||||||
#include <tt_app_alertdialog.h>
|
#include <tt_app_alertdialog.h>
|
||||||
|
#include <tt_lvgl.h>
|
||||||
#include <tactility/device.h>
|
|
||||||
#include <tactility/drivers/display.h>
|
|
||||||
#include <tactility/drivers/pointer.h>
|
|
||||||
#include <lvgl/lvgl.h>
|
|
||||||
#include <lvgl/module.h>
|
|
||||||
#include <tactility/module.h>
|
|
||||||
|
|
||||||
constexpr auto TAG = "Main";
|
constexpr auto TAG = "Main";
|
||||||
|
|
||||||
static void onCreate(AppHandle appHandle, void* data) {
|
/** Find a DisplayDevice that supports the DisplayDriver interface */
|
||||||
struct Device* display_device;
|
static bool findUsableDisplay(DeviceId& deviceId) {
|
||||||
if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) {
|
uint16_t display_count = 0;
|
||||||
|
if (!tt_hal_device_find(DEVICE_TYPE_DISPLAY, &deviceId, &display_count, 1)) {
|
||||||
ESP_LOGE(TAG, "No display device found");
|
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_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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Device* touch_device;
|
DeviceId touch_id;
|
||||||
if (device_get_first_active_by_type(&POINTER_TYPE, &touch_device) != ERROR_NONE) {
|
if (!findUsableTouch(touch_id)) {
|
||||||
ESP_LOGE(TAG, "No touch device found");
|
|
||||||
device_put(display_device);
|
|
||||||
tt_app_stop();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop LVGL first (because it's currently using the drivers we want to use)
|
// Stop LVGL first (because it's currently using the drivers we want to use)
|
||||||
module_stop(&lvgl_module);
|
tt_lvgl_stop();
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Creating display driver");
|
ESP_LOGI(TAG, "Creating display driver");
|
||||||
auto display = new DisplayDriver(display_device);
|
auto display = new DisplayDriver(display_id);
|
||||||
device_put(display_device);
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Creating touch driver");
|
ESP_LOGI(TAG, "Creating touch driver");
|
||||||
auto touch = new TouchDriver(touch_device);
|
auto touch = new TouchDriver(touch_id);
|
||||||
device_put(touch_device);
|
|
||||||
|
|
||||||
// Run the main logic
|
// Run the main logic
|
||||||
ESP_LOGI(TAG, "Running application");
|
ESP_LOGI(TAG, "Running application");
|
||||||
@@ -61,9 +82,9 @@ static void onCreate(AppHandle appHandle, void* data) {
|
|||||||
|
|
||||||
static void onDestroy(AppHandle appHandle, void* data) {
|
static void onDestroy(AppHandle appHandle, void* data) {
|
||||||
// Restart LVGL to resume rendering of regular apps
|
// Restart LVGL to resume rendering of regular apps
|
||||||
if (!module_is_started(&lvgl_module)) {
|
if (!tt_lvgl_is_started()) {
|
||||||
ESP_LOGI(TAG, "Restarting LVGL");
|
ESP_LOGI(TAG, "Restarting LVGL");
|
||||||
module_start(&lvgl_module);
|
tt_lvgl_start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.graphicsdemo
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.7.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=7
|
[app]
|
||||||
app.name=Graphics Demo
|
id=one.tactility.graphicsdemo
|
||||||
|
versionName=0.3.0
|
||||||
|
versionCode=3
|
||||||
|
name=Graphics Demo
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#include <tt_app.h>
|
#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
|
* Note: LVGL and Tactility methods need to be exposed manually from TactilityC/Source/tt_init.cpp
|
||||||
* Only C is supported for now (C++ symbols fail to link)
|
* Only C is supported for now (C++ symbols fail to link)
|
||||||
*/
|
*/
|
||||||
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
||||||
lv_obj_t* toolbar = 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_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
lv_obj_t* label = lv_label_create(parent);
|
lv_obj_t* label = lv_label_create(parent);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.helloworld
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.7.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=7
|
[app]
|
||||||
app.name=Hello World
|
id=one.tactility.helloworld
|
||||||
|
versionName=0.3.0
|
||||||
|
versionCode=3
|
||||||
|
name=Hello World
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "TestUnitLcdGfx.h"
|
#include "TestUnitLcdGfx.h"
|
||||||
|
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <esp_log.h>
|
#include <esp_log.h>
|
||||||
|
|
||||||
constexpr auto* TAG = "M5UnitTest";
|
constexpr auto* TAG = "M5UnitTest";
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#include "TestListView.h"
|
#include "TestListView.h"
|
||||||
#include "M5UnitTest.h"
|
#include "M5UnitTest.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
void TestListView::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
void TestListView::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
||||||
app_ = app;
|
app_ = app;
|
||||||
|
|
||||||
lvgl_toolbar_create(parent, "M5 Unit Test");
|
tt_lvgl_toolbar_create_for_app(parent, handle);
|
||||||
|
|
||||||
list_ = lv_list_create(parent);
|
list_ = lv_list_create(parent);
|
||||||
lv_obj_set_width(list_, LV_PCT(100));
|
lv_obj_set_width(list_, LV_PCT(100));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include <lvgl/icons/shared.h>
|
#include <tactility/lvgl_icon_shared.h>
|
||||||
#include <tt_app.h>
|
#include <tt_app.h>
|
||||||
|
|
||||||
class M5UnitTest;
|
class M5UnitTest;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "GroveLookup.h"
|
#include "GroveLookup.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "GroveLookup.h"
|
#include "GroveLookup.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
void TestUnitByteButton::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
void TestUnitByteButton::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/drivers/uart_controller.h>
|
#include <tactility/drivers/uart_controller.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "TestUnitDualButton.h"
|
#include "TestUnitDualButton.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
static constexpr gpio_pin_t PIN_MIN = 0;
|
static constexpr gpio_pin_t PIN_MIN = 0;
|
||||||
static constexpr gpio_pin_t PIN_MAX = 57;
|
static constexpr gpio_pin_t PIN_MAX = 57;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "GroveLookup.h"
|
#include "GroveLookup.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "GroveLookup.h"
|
#include "GroveLookup.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
void TestUnitLcd::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
void TestUnitLcd::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
||||||
app_ = app;
|
app_ = app;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "GroveLookup.h"
|
#include "GroveLookup.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
#include <esp_timer.h>
|
#include <esp_timer.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "GroveLookup.h"
|
#include "GroveLookup.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
void TestUnitMidi::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
void TestUnitMidi::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
||||||
app_ = app;
|
app_ = app;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/drivers/i2c_controller.h>
|
#include <tactility/drivers/i2c_controller.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
void TestUnitPaHub::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
void TestUnitPaHub::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
||||||
app_ = app;
|
app_ = app;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
#include "GroveLookup.h"
|
#include "GroveLookup.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#include "GroveLookup.h"
|
#include "GroveLookup.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
void TestUnitScroll::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
void TestUnitScroll::onStart(lv_obj_t* parent, AppHandle handle, M5UnitTest* app) {
|
||||||
app_ = app;
|
app_ = app;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
#include "TestViewBase.h"
|
#include "TestViewBase.h"
|
||||||
#include "M5UnitTest.h"
|
#include "M5UnitTest.h"
|
||||||
#include "UiScale.h"
|
#include "UiScale.h"
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
lv_obj_t* TestViewBase::createToolbar(lv_obj_t* parent, AppHandle handle, const char* title) {
|
lv_obj_t* TestViewBase::createToolbar(lv_obj_t* parent, AppHandle handle, const char* title) {
|
||||||
lv_obj_t* toolbar = lvgl_toolbar_create(parent, title);
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, handle);
|
||||||
lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LEFT, onBackClicked, this);
|
tt_lvgl_toolbar_set_title(toolbar, title);
|
||||||
|
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LEFT, onBackClicked, this);
|
||||||
return toolbar;
|
return toolbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
// Device screen widths in default (portrait) orientation:
|
// Device screen widths in default (portrait) orientation:
|
||||||
// tiny < 200 : small OLEDs, custom breadboard devices
|
// tiny < 200 : small OLEDs, custom breadboard devices
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32s3,esp32p4
|
[target]
|
||||||
app.id=one.tactility.m5unittest
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.5.0
|
platforms=esp32s3,esp32p4
|
||||||
app.version.code=5
|
[app]
|
||||||
app.name=M5 Unit Test
|
id=one.tactility.m5unittest
|
||||||
|
versionName=0.1.0
|
||||||
|
versionCode=1
|
||||||
|
name=M5 Unit Test
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "Magic8Ball.h"
|
#include "Magic8Ball.h"
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <tactility/device.h>
|
#include <tt_lvgl_keyboard.h>
|
||||||
#include <tactility/drivers/keyboard.h>
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@ static const char* responses[] = {
|
|||||||
#define NUM_RESPONSES (sizeof(responses) / sizeof(responses[0]))
|
#define NUM_RESPONSES (sizeof(responses) / sizeof(responses[0]))
|
||||||
|
|
||||||
static const char* getInputHint() {
|
static const char* getInputHint() {
|
||||||
if (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 or Space to ask Q to exit";
|
||||||
}
|
}
|
||||||
return "Touch the ball to ask";
|
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);
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
/* Toolbar */
|
/* 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);
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
/* Main container */
|
/* 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);
|
lv_obj_add_event_cb(ballObj, onBallClick, LV_EVENT_CLICKED, this);
|
||||||
|
|
||||||
/* Keyboard support - no editing mode needed, just focus the ball */
|
/* Keyboard support - no editing mode needed, just focus the ball */
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
|
if (tt_lvgl_hardware_keyboard_is_available()) {
|
||||||
lv_group_t* grp = lv_group_get_default();
|
lv_group_t* grp = lv_group_get_default();
|
||||||
if (grp) {
|
if (grp) {
|
||||||
lv_group_add_obj(grp, ballObj);
|
lv_group_add_obj(grp, ballObj);
|
||||||
@@ -153,7 +152,7 @@ void Magic8Ball::onShow(AppHandle app, lv_obj_t* parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Magic8Ball::onHide(AppHandle app) {
|
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);
|
lv_group_remove_obj(ballObj);
|
||||||
}
|
}
|
||||||
answerLabel = nullptr;
|
answerLabel = nullptr;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.magic8ball
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.6.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=6
|
[app]
|
||||||
app.name=Magic 8-Ball
|
id=one.tactility.magic8ball
|
||||||
|
versionName=0.2.0
|
||||||
|
versionCode=2
|
||||||
|
name=Magic 8-Ball
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
#include <lvgl/lvgl.h>
|
#include <tt_lvgl.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
|
||||||
static const char* TAG = "MediaKeys";
|
static const char* TAG = "MediaKeys";
|
||||||
|
|
||||||
@@ -154,24 +154,24 @@ void MediaKeys::btEventCallback(struct Device* /*device*/, void* context, struct
|
|||||||
|
|
||||||
if (event.radio_state == BT_RADIO_STATE_ON) {
|
if (event.radio_state == BT_RADIO_STATE_ON) {
|
||||||
// Radio is now up - start HID (needs LVGL lock for UI update)
|
// Radio is now up - start HID (needs LVGL lock for UI update)
|
||||||
if (lvgl_try_lock(1000)) {
|
if (tt_lvgl_lock(1000)) {
|
||||||
// Re-check inside lock to avoid TOCTOU race with handleSwitchToggle(false)
|
// Re-check inside lock to avoid TOCTOU race with handleSwitchToggle(false)
|
||||||
if (self->_radioEnabling) {
|
if (self->_radioEnabling) {
|
||||||
self->startHid();
|
self->startHid();
|
||||||
}
|
}
|
||||||
lvgl_unlock();
|
tt_lvgl_unlock();
|
||||||
}
|
}
|
||||||
} else if (event.radio_state == BT_RADIO_STATE_OFF && self->_isEnabled) {
|
} else if (event.radio_state == BT_RADIO_STATE_OFF && self->_isEnabled) {
|
||||||
// Radio dropped while we were active - revert UI
|
// Radio dropped while we were active - revert UI
|
||||||
LOG_I(TAG, "BT radio turned off, disabling HID");
|
LOG_I(TAG, "BT radio turned off, disabling HID");
|
||||||
if (lvgl_try_lock(1000)) {
|
if (tt_lvgl_lock(1000)) {
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) self->exitKeyMode();
|
if (tt_lvgl_hardware_keyboard_is_available()) self->exitKeyMode();
|
||||||
self->_hidDevice = nullptr;
|
self->_hidDevice = nullptr;
|
||||||
self->_isEnabled = false;
|
self->_isEnabled = false;
|
||||||
self->_radioEnabling = false;
|
self->_radioEnabling = false;
|
||||||
if (self->_switchWidget) lv_obj_remove_state(self->_switchWidget, LV_STATE_CHECKED);
|
if (self->_switchWidget) lv_obj_remove_state(self->_switchWidget, LV_STATE_CHECKED);
|
||||||
if (self->_mainWrapper) lv_obj_add_flag(self->_mainWrapper, LV_OBJ_FLAG_HIDDEN);
|
if (self->_mainWrapper) lv_obj_add_flag(self->_mainWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
lvgl_unlock();
|
tt_lvgl_unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event.type == BT_EVENT_PROFILE_STATE_CHANGED && event.profile_state.profile == BT_PROFILE_HID_DEVICE) {
|
} else if (event.type == BT_EVENT_PROFILE_STATE_CHANGED && event.profile_state.profile == BT_PROFILE_HID_DEVICE) {
|
||||||
@@ -208,24 +208,7 @@ void MediaKeys::startHid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_mainWrapper) lv_obj_remove_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
|
if (_mainWrapper) lv_obj_remove_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) enterKeyMode();
|
if (tt_lvgl_hardware_keyboard_is_available()) enterKeyMode();
|
||||||
}
|
|
||||||
|
|
||||||
void MediaKeys::teardownBt() {
|
|
||||||
// Remove callback FIRST - stops any in-flight BT events from firing against
|
|
||||||
// our (possibly already freed) UI widget pointers after this returns.
|
|
||||||
if (_btDevice) bluetooth_remove_event_callback(_btDevice, btEventCallback);
|
|
||||||
// Do NOT call bluetooth_hid_device_stop here: it calls ble_gatts_reset() /
|
|
||||||
// ble_gatts_start() which corrupts NimBLE heap while the host task is still
|
|
||||||
// running. HID device is a persistent kernel device; hid_device_start() cleans
|
|
||||||
// up stale context on next use. Explicit stop is handled by handleSwitchToggle.
|
|
||||||
// Restore the radio/device to the state we found them in.
|
|
||||||
if (_btDevice && _radioWasOff) bluetooth_set_radio_enabled(_btDevice, false);
|
|
||||||
if (_btDevice && _deviceWasStarted) device_stop(_btDevice);
|
|
||||||
_btDevice = nullptr;
|
|
||||||
_hidDevice = nullptr;
|
|
||||||
_radioWasOff = false;
|
|
||||||
_deviceWasStarted = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaKeys::handleSwitchToggle(bool enabled) {
|
void MediaKeys::handleSwitchToggle(bool enabled) {
|
||||||
@@ -233,7 +216,7 @@ void MediaKeys::handleSwitchToggle(bool enabled) {
|
|||||||
_isEnabled = enabled;
|
_isEnabled = enabled;
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
_btDevice = device_find_first_by_type(&BLUETOOTH_TYPE);
|
_btDevice = bluetooth_find_first_ready_device();
|
||||||
if (!_btDevice) {
|
if (!_btDevice) {
|
||||||
LOG_E(TAG, "No Bluetooth device found");
|
LOG_E(TAG, "No Bluetooth device found");
|
||||||
_isEnabled = false;
|
_isEnabled = false;
|
||||||
@@ -241,19 +224,6 @@ void MediaKeys::handleSwitchToggle(bool enabled) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device may not be started yet (BT disabled in DTS by default to save memory).
|
|
||||||
if (!device_is_ready(_btDevice)) {
|
|
||||||
LOG_I(TAG, "BT device not started, starting now");
|
|
||||||
if (device_start(_btDevice) != ERROR_NONE) {
|
|
||||||
LOG_E(TAG, "Failed to start BT device");
|
|
||||||
_btDevice = nullptr;
|
|
||||||
_isEnabled = false;
|
|
||||||
if (_switchWidget) lv_obj_remove_state(_switchWidget, LV_STATE_CHECKED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_deviceWasStarted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bluetooth_set_device_name(_btDevice, "Tactility Media Keys");
|
bluetooth_set_device_name(_btDevice, "Tactility Media Keys");
|
||||||
|
|
||||||
// Register callback before enabling radio so we don't miss the state-change event.
|
// Register callback before enabling radio so we don't miss the state-change event.
|
||||||
@@ -276,11 +246,13 @@ void MediaKeys::handleSwitchToggle(bool enabled) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_radioEnabling = false;
|
_radioEnabling = false;
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) exitKeyMode();
|
if (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode();
|
||||||
// Explicit user toggle-off: stop HID cleanly (safe here since we're on the
|
|
||||||
// LVGL task and the user intentionally disabled, so no race with app teardown).
|
|
||||||
if (_hidDevice) bluetooth_hid_device_stop(_hidDevice);
|
if (_hidDevice) bluetooth_hid_device_stop(_hidDevice);
|
||||||
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);
|
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_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
lv_obj_t* toolbar = 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);
|
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);
|
lv_obj_add_event_cb(_switchWidget, onSwitchToggled, LV_EVENT_VALUE_CHANGED, this);
|
||||||
|
|
||||||
_mainWrapper = lv_obj_create(parent);
|
_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);
|
lv_obj_add_event_cb(_buttonMatrix, onButtonPressed, LV_EVENT_VALUE_CHANGED, this);
|
||||||
|
|
||||||
// Physical keyboard support: key events on the matrix (entered when BT enabled, Q/Esc exits)
|
// Physical keyboard support: key events on the matrix (entered when BT enabled, Q/Esc exits)
|
||||||
if (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);
|
lv_obj_add_event_cb(_buttonMatrix, onKeyEvent, LV_EVENT_KEY, this);
|
||||||
_keyHighlightTimer = lv_timer_create(onKeyHighlightTimer, 150, this);
|
_keyHighlightTimer = lv_timer_create(onKeyHighlightTimer, 150, this);
|
||||||
lv_timer_pause(_keyHighlightTimer);
|
lv_timer_pause(_keyHighlightTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(_mainWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
// Auto-enable if BT is already on (turned on via QuickPanel/Settings before opening app).
|
|
||||||
struct Device* btDev = device_find_first_by_type(&BLUETOOTH_TYPE);
|
|
||||||
if (btDev && device_is_ready(btDev)) {
|
|
||||||
enum BtRadioState radioState;
|
|
||||||
if (bluetooth_get_radio_state(btDev, &radioState) == ERROR_NONE && radioState == BT_RADIO_STATE_ON) {
|
|
||||||
lv_obj_add_state(_switchWidget, LV_STATE_CHECKED);
|
|
||||||
handleSwitchToggle(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MediaKeys::onHide(AppHandle /*appHandle*/) {
|
void MediaKeys::onHide(AppHandle /*appHandle*/) {
|
||||||
_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;
|
_isEnabled = false;
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) exitKeyMode();
|
_radioEnabling = false;
|
||||||
teardownBt();
|
_radioWasOff = false;
|
||||||
|
|
||||||
|
if (tt_lvgl_hardware_keyboard_is_available()) exitKeyMode();
|
||||||
if (_keyHighlightTimer) {
|
if (_keyHighlightTimer) {
|
||||||
lv_timer_delete(_keyHighlightTimer);
|
lv_timer_delete(_keyHighlightTimer);
|
||||||
_keyHighlightTimer = nullptr;
|
_keyHighlightTimer = nullptr;
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
#include <TactilityCpp/App.h>
|
#include <TactilityCpp/App.h>
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include <tactility/device.h>
|
|
||||||
#include <tactility/drivers/bluetooth.h>
|
#include <tactility/drivers/bluetooth.h>
|
||||||
#include <tactility/drivers/bluetooth_hid_device.h>
|
#include <tactility/drivers/bluetooth_hid_device.h>
|
||||||
#include <tactility/drivers/keyboard.h>
|
|
||||||
#include <tt_app.h>
|
#include <tt_app.h>
|
||||||
|
#include <tt_lvgl_keyboard.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
class MediaKeys final : public App {
|
class MediaKeys final : public App {
|
||||||
@@ -24,10 +23,9 @@ class MediaKeys final : public App {
|
|||||||
struct Device* _hidDevice = nullptr;
|
struct Device* _hidDevice = nullptr;
|
||||||
|
|
||||||
// State - accessed from both LVGL thread and BT callback thread
|
// State - accessed from both LVGL thread and BT callback thread
|
||||||
std::atomic<bool> _isEnabled {false};
|
std::atomic<bool> _isEnabled {false};
|
||||||
std::atomic<bool> _radioEnabling {false}; // true while waiting for radio to come ON
|
std::atomic<bool> _radioEnabling{false}; // true while waiting for radio to come ON
|
||||||
std::atomic<bool> _radioWasOff {false}; // true if we turned the radio on (restore on exit)
|
std::atomic<bool> _radioWasOff {false}; // true if MediaKeys turned the radio on (so we turn it off)
|
||||||
std::atomic<bool> _deviceWasStarted{false}; // true if we called device_start (restore on exit)
|
|
||||||
|
|
||||||
// Static event callbacks
|
// Static event callbacks
|
||||||
static void onSwitchToggled(lv_event_t* e);
|
static void onSwitchToggled(lv_event_t* e);
|
||||||
@@ -38,7 +36,6 @@ class MediaKeys final : public App {
|
|||||||
static void sendKeyTask(void* param);
|
static void sendKeyTask(void* param);
|
||||||
|
|
||||||
// Instance methods called by static callbacks
|
// Instance methods called by static callbacks
|
||||||
void teardownBt(); // remove callback + stop HID + restore radio/device state
|
|
||||||
void handleSwitchToggle(bool enabled);
|
void handleSwitchToggle(bool enabled);
|
||||||
void handleButtonPress(uint32_t buttonId);
|
void handleButtonPress(uint32_t buttonId);
|
||||||
void startHid(); // called once radio is confirmed ON
|
void startHid(); // called once radio is confirmed ON
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32s3,esp32p4
|
[target]
|
||||||
app.id=one.tactility.mediakeys
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.6.0
|
platforms=esp32s3,esp32p4
|
||||||
app.version.code=6
|
[app]
|
||||||
app.name=Media Keys
|
id=one.tactility.mediakeys
|
||||||
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.
|
versionName=0.1.0
|
||||||
|
versionCode=1
|
||||||
|
name=Media Keys
|
||||||
|
description=Bluetooth media keys. Touch or Physical Keyboard control\nB - previous, P - play/pause, N - next, M - mute, D - volume down, U - volume up.\nQ or ESC to exit focus.
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ if (DEFINED ENV{TACTILITY_SDK_PATH})
|
|||||||
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
else()
|
else()
|
||||||
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
project(EspNowBridge)
|
project(Mp3Player)
|
||||||
tactility_project(EspNowBridge)
|
tactility_project(Mp3Player)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
REQUIRES TactilitySDK
|
||||||
|
)
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
#include <tt_app.h>
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/drivers/i2s_controller.h>
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
#define MINIMP3_IMPLEMENTATION
|
||||||
|
#define MINIMP3_NO_SIMD
|
||||||
|
#include "minimp3.h"
|
||||||
|
|
||||||
|
#define TAG "Mp3Player"
|
||||||
|
#define MP3_INPUT_BUFFER_SIZE 16384
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
STATE_IDLE,
|
||||||
|
STATE_PLAYING,
|
||||||
|
STATE_PAUSED
|
||||||
|
} PlaybackState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
struct Device* i2s_dev;
|
||||||
|
char filepath[512];
|
||||||
|
PlaybackState state;
|
||||||
|
|
||||||
|
// UI elements
|
||||||
|
lv_obj_t* lbl_title;
|
||||||
|
lv_obj_t* lbl_status;
|
||||||
|
lv_obj_t* bar_progress;
|
||||||
|
lv_obj_t* btn_play_pause;
|
||||||
|
lv_obj_t* btn_stop;
|
||||||
|
|
||||||
|
// Playback state variables
|
||||||
|
FILE* file;
|
||||||
|
mp3dec_t decoder;
|
||||||
|
uint8_t* input_buf;
|
||||||
|
mp3d_sample_t* pcm_buf;
|
||||||
|
size_t buffered_bytes;
|
||||||
|
bool eof;
|
||||||
|
int volume;
|
||||||
|
|
||||||
|
int sample_rate;
|
||||||
|
int channels;
|
||||||
|
|
||||||
|
// Progress calculation
|
||||||
|
size_t file_size;
|
||||||
|
size_t bytes_read_total;
|
||||||
|
|
||||||
|
TaskHandle_t playback_task_handle;
|
||||||
|
} AppCtx;
|
||||||
|
|
||||||
|
static AppCtx g_ctx;
|
||||||
|
|
||||||
|
/* ─── Update UI ─── */
|
||||||
|
static void update_ui(AppCtx* ctx) {
|
||||||
|
if (!ctx->lbl_status) return;
|
||||||
|
|
||||||
|
switch (ctx->state) {
|
||||||
|
case STATE_PLAYING:
|
||||||
|
lv_label_set_text(ctx->lbl_status, "Playing...");
|
||||||
|
lv_label_set_text(lv_obj_get_child(ctx->btn_play_pause, 0), LV_SYMBOL_PAUSE " Pause");
|
||||||
|
lv_obj_clear_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||||
|
lv_obj_clear_state(ctx->btn_stop, LV_STATE_DISABLED);
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
lv_label_set_text(ctx->lbl_status, "Paused");
|
||||||
|
lv_label_set_text(lv_obj_get_child(ctx->btn_play_pause, 0), LV_SYMBOL_PLAY " Play");
|
||||||
|
lv_obj_clear_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||||
|
lv_obj_clear_state(ctx->btn_stop, LV_STATE_DISABLED);
|
||||||
|
break;
|
||||||
|
case STATE_IDLE:
|
||||||
|
default:
|
||||||
|
if (ctx->filepath[0] != '\0') {
|
||||||
|
lv_label_set_text(ctx->lbl_status, "Ready");
|
||||||
|
lv_obj_clear_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||||
|
} else {
|
||||||
|
lv_label_set_text(ctx->lbl_status, "No file loaded");
|
||||||
|
lv_obj_add_state(ctx->btn_play_pause, LV_STATE_DISABLED);
|
||||||
|
}
|
||||||
|
lv_label_set_text(lv_obj_get_child(ctx->btn_play_pause, 0), LV_SYMBOL_PLAY " Play");
|
||||||
|
lv_obj_add_state(ctx->btn_stop, LV_STATE_DISABLED);
|
||||||
|
lv_bar_set_value(ctx->bar_progress, 0, LV_ANIM_OFF);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Playback Task ─── */
|
||||||
|
static void mp3_playback_task(void* arg) {
|
||||||
|
AppCtx* ctx = (AppCtx*)arg;
|
||||||
|
|
||||||
|
ctx->file = fopen(ctx->filepath, "rb");
|
||||||
|
if (!ctx->file) {
|
||||||
|
ESP_LOGE(TAG, "Failed to open file: %s", ctx->filepath);
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
ctx->state = STATE_IDLE;
|
||||||
|
lv_label_set_text(ctx->lbl_status, "Error: File not found");
|
||||||
|
update_ui(ctx);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
ctx->playback_task_handle = NULL;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fseek(ctx->file, 0, SEEK_END);
|
||||||
|
ctx->file_size = ftell(ctx->file);
|
||||||
|
fseek(ctx->file, 0, SEEK_SET);
|
||||||
|
ctx->bytes_read_total = 0;
|
||||||
|
|
||||||
|
mp3dec_init(&ctx->decoder);
|
||||||
|
ctx->buffered_bytes = 0;
|
||||||
|
ctx->eof = false;
|
||||||
|
ctx->sample_rate = 0;
|
||||||
|
ctx->channels = 0;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Starting MP3 playback: %s (size: %d bytes)", ctx->filepath, ctx->file_size);
|
||||||
|
|
||||||
|
while (ctx->state != STATE_IDLE) {
|
||||||
|
if (ctx->state == STATE_PAUSED) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill input buffer
|
||||||
|
if (!ctx->eof && ctx->buffered_bytes < MP3_INPUT_BUFFER_SIZE) {
|
||||||
|
size_t to_read = MP3_INPUT_BUFFER_SIZE - ctx->buffered_bytes;
|
||||||
|
size_t read_bytes = fread(ctx->input_buf + ctx->buffered_bytes, 1, to_read, ctx->file);
|
||||||
|
ctx->buffered_bytes += read_bytes;
|
||||||
|
ctx->bytes_read_total += read_bytes;
|
||||||
|
if (read_bytes == 0) {
|
||||||
|
ctx->eof = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->buffered_bytes == 0 && ctx->eof) {
|
||||||
|
ESP_LOGI(TAG, "Reached EOF");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode one frame
|
||||||
|
mp3dec_frame_info_t info;
|
||||||
|
memset(&info, 0, sizeof(info));
|
||||||
|
int samples = mp3dec_decode_frame(&ctx->decoder, ctx->input_buf, (int)ctx->buffered_bytes, ctx->pcm_buf, &info);
|
||||||
|
|
||||||
|
if (info.frame_bytes <= 0) {
|
||||||
|
if (ctx->eof) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Move 1 byte forward to resync
|
||||||
|
memmove(ctx->input_buf, ctx->input_buf + 1, --ctx->buffered_bytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t consumed = (size_t)info.frame_bytes;
|
||||||
|
ctx->buffered_bytes -= consumed;
|
||||||
|
memmove(ctx->input_buf, ctx->input_buf + consumed, ctx->buffered_bytes);
|
||||||
|
|
||||||
|
if (samples > 0) {
|
||||||
|
// Configure I2S if format changed
|
||||||
|
if (ctx->sample_rate != info.hz || ctx->channels != info.channels) {
|
||||||
|
struct I2sConfig config = {
|
||||||
|
.communication_format = I2S_FORMAT_STAND_I2S,
|
||||||
|
.sample_rate = (uint32_t)info.hz,
|
||||||
|
.bits_per_sample = 16,
|
||||||
|
.channel_left = 0,
|
||||||
|
.channel_right = (info.channels == 2) ? 1 : I2S_CHANNEL_NONE
|
||||||
|
};
|
||||||
|
|
||||||
|
device_lock(ctx->i2s_dev);
|
||||||
|
error_t err = i2s_controller_set_config(ctx->i2s_dev, &config);
|
||||||
|
device_unlock(ctx->i2s_dev);
|
||||||
|
|
||||||
|
if (err != ERROR_NONE) {
|
||||||
|
ESP_LOGE(TAG, "Failed to set config: %d", err);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ctx->sample_rate = info.hz;
|
||||||
|
ctx->channels = info.channels;
|
||||||
|
ESP_LOGI(TAG, "I2S configured: %d Hz, %d channels", info.hz, info.channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust volume
|
||||||
|
int vol = ctx->volume;
|
||||||
|
int16_t* samples_ptr = (int16_t*)ctx->pcm_buf;
|
||||||
|
size_t sample_count = (size_t)samples * info.channels;
|
||||||
|
for (size_t i = 0; i < sample_count; ++i) {
|
||||||
|
int32_t scaled = (int32_t)samples_ptr[i] * vol / 100;
|
||||||
|
samples_ptr[i] = (int16_t)scaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play audio to I2S
|
||||||
|
size_t offset = 0;
|
||||||
|
size_t data_size = sample_count * sizeof(int16_t);
|
||||||
|
bool write_err = false;
|
||||||
|
while (offset < data_size && ctx->state == STATE_PLAYING) {
|
||||||
|
size_t written = 0;
|
||||||
|
error_t err = i2s_controller_write(ctx->i2s_dev, (uint8_t*)ctx->pcm_buf + offset, data_size - offset, &written, pdMS_TO_TICKS(250));
|
||||||
|
if (err != ERROR_NONE || written == 0) {
|
||||||
|
ESP_LOGE(TAG, "I2S write failed: %d", err);
|
||||||
|
write_err = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += written;
|
||||||
|
}
|
||||||
|
if (write_err) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress UI
|
||||||
|
if (ctx->file_size > 0) {
|
||||||
|
int pct = (int)((ctx->bytes_read_total - ctx->buffered_bytes) * 100 / ctx->file_size);
|
||||||
|
if (pct < 0) pct = 0;
|
||||||
|
if (pct > 100) pct = 100;
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
lv_bar_set_value(ctx->bar_progress, pct, LV_ANIM_OFF);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
taskYIELD();
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(ctx->file);
|
||||||
|
ctx->file = NULL;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Playback task finished");
|
||||||
|
|
||||||
|
tt_lvgl_lock(portMAX_DELAY);
|
||||||
|
ctx->state = STATE_IDLE;
|
||||||
|
update_ui(ctx);
|
||||||
|
tt_lvgl_unlock();
|
||||||
|
|
||||||
|
ctx->playback_task_handle = NULL;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Callbacks ─── */
|
||||||
|
static void on_play_pause_click(lv_event_t* e) {
|
||||||
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
|
if (ctx->filepath[0] == '\0') return;
|
||||||
|
|
||||||
|
if (ctx->state == STATE_IDLE) {
|
||||||
|
ctx->state = STATE_PLAYING;
|
||||||
|
update_ui(ctx);
|
||||||
|
xTaskCreate(mp3_playback_task, "mp3_play", 4096, ctx, 5, &ctx->playback_task_handle);
|
||||||
|
} else if (ctx->state == STATE_PLAYING) {
|
||||||
|
ctx->state = STATE_PAUSED;
|
||||||
|
update_ui(ctx);
|
||||||
|
} else if (ctx->state == STATE_PAUSED) {
|
||||||
|
ctx->state = STATE_PLAYING;
|
||||||
|
update_ui(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_stop_click(lv_event_t* e) {
|
||||||
|
AppCtx* ctx = (AppCtx*)lv_event_get_user_data(e);
|
||||||
|
if (ctx->state == STATE_IDLE) return;
|
||||||
|
|
||||||
|
ctx->state = STATE_IDLE;
|
||||||
|
update_ui(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── App Lifecycle ─── */
|
||||||
|
static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
|
||||||
|
memset(&g_ctx, 0, sizeof(g_ctx));
|
||||||
|
g_ctx.volume = 80;
|
||||||
|
|
||||||
|
// Allocate memory buffers
|
||||||
|
g_ctx.input_buf = (uint8_t*)malloc(MP3_INPUT_BUFFER_SIZE);
|
||||||
|
g_ctx.pcm_buf = (mp3d_sample_t*)malloc(MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t));
|
||||||
|
|
||||||
|
// Find device
|
||||||
|
g_ctx.i2s_dev = device_find_by_name("i2s0");
|
||||||
|
if (!g_ctx.i2s_dev) {
|
||||||
|
ESP_LOGE(TAG, "I2S device 'i2s0' not found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse launch parameters
|
||||||
|
BundleHandle bundle = tt_app_get_parameters(app);
|
||||||
|
if (bundle) {
|
||||||
|
char file_param[256] = {0};
|
||||||
|
if (tt_bundle_opt_string(bundle, "file", file_param, sizeof(file_param))) {
|
||||||
|
// Safely resolve SD card path
|
||||||
|
if (strncmp(file_param, "/sdcard", 7) == 0) {
|
||||||
|
strncpy(g_ctx.filepath, file_param, sizeof(g_ctx.filepath) - 1);
|
||||||
|
} else {
|
||||||
|
if (file_param[0] == '/') {
|
||||||
|
snprintf(g_ctx.filepath, sizeof(g_ctx.filepath), "/sdcard%s", file_param);
|
||||||
|
} else {
|
||||||
|
snprintf(g_ctx.filepath, sizeof(g_ctx.filepath), "/sdcard/%s", file_param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── UI Layout ───
|
||||||
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
|
||||||
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
|
// Player Card (rounded box)
|
||||||
|
lv_obj_t* card = lv_obj_create(parent);
|
||||||
|
lv_obj_set_size(card, lv_pct(90), lv_pct(70));
|
||||||
|
lv_obj_align(card, LV_ALIGN_CENTER, 0, 15);
|
||||||
|
lv_obj_set_style_radius(card, 15, 0);
|
||||||
|
lv_obj_set_style_bg_color(card, lv_color_hex(0x1E1E2E), 0);
|
||||||
|
lv_obj_set_style_border_color(card, lv_color_hex(0x313244), 0);
|
||||||
|
lv_obj_set_style_border_width(card, 2, 0);
|
||||||
|
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_all(card, 15, 0);
|
||||||
|
lv_obj_set_style_pad_gap(card, 15, 0);
|
||||||
|
|
||||||
|
// Audio Icon
|
||||||
|
lv_obj_t* icon = lv_label_create(card);
|
||||||
|
lv_label_set_text(icon, LV_SYMBOL_AUDIO);
|
||||||
|
lv_obj_set_style_text_color(icon, lv_color_hex(0x89B4FA), 0);
|
||||||
|
|
||||||
|
// Track Title
|
||||||
|
g_ctx.lbl_title = lv_label_create(card);
|
||||||
|
lv_obj_set_width(g_ctx.lbl_title, lv_pct(95));
|
||||||
|
lv_obj_set_style_text_align(g_ctx.lbl_title, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
|
lv_obj_set_style_text_color(g_ctx.lbl_title, lv_color_hex(0xCDD6F4), 0);
|
||||||
|
if (g_ctx.filepath[0] != '\0') {
|
||||||
|
const char* last_slash = strrchr(g_ctx.filepath, '/');
|
||||||
|
const char* filename = last_slash ? last_slash + 1 : g_ctx.filepath;
|
||||||
|
lv_label_set_text(g_ctx.lbl_title, filename);
|
||||||
|
lv_label_set_long_mode(g_ctx.lbl_title, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
||||||
|
} else {
|
||||||
|
lv_label_set_text(g_ctx.lbl_title, "No File Parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playback Status
|
||||||
|
g_ctx.lbl_status = lv_label_create(card);
|
||||||
|
lv_obj_set_style_text_color(g_ctx.lbl_status, lv_color_hex(0xA6ADC8), 0);
|
||||||
|
|
||||||
|
// Progress Bar
|
||||||
|
g_ctx.bar_progress = lv_bar_create(card);
|
||||||
|
lv_obj_set_size(g_ctx.bar_progress, lv_pct(85), 8);
|
||||||
|
lv_bar_set_range(g_ctx.bar_progress, 0, 100);
|
||||||
|
lv_bar_set_value(g_ctx.bar_progress, 0, LV_ANIM_OFF);
|
||||||
|
lv_obj_set_style_bg_color(g_ctx.bar_progress, lv_color_hex(0x45475A), LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_bg_color(g_ctx.bar_progress, lv_color_hex(0x89B4FA), LV_PART_INDICATOR);
|
||||||
|
|
||||||
|
// Controls Container
|
||||||
|
lv_obj_t* ctrl_box = lv_obj_create(card);
|
||||||
|
lv_obj_remove_style_all(ctrl_box);
|
||||||
|
lv_obj_set_size(ctrl_box, lv_pct(90), 45);
|
||||||
|
lv_obj_set_flex_flow(ctrl_box, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(ctrl_box, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
|
||||||
|
// Play/Pause Button
|
||||||
|
g_ctx.btn_play_pause = lv_btn_create(ctrl_box);
|
||||||
|
lv_obj_set_size(g_ctx.btn_play_pause, 100, 36);
|
||||||
|
lv_obj_set_style_radius(g_ctx.btn_play_pause, 18, 0);
|
||||||
|
lv_obj_set_style_bg_color(g_ctx.btn_play_pause, lv_color_hex(0x89B4FA), 0);
|
||||||
|
lv_obj_set_style_text_color(g_ctx.btn_play_pause, lv_color_hex(0x11111B), 0);
|
||||||
|
lv_obj_t* lbl_play = lv_label_create(g_ctx.btn_play_pause);
|
||||||
|
lv_label_set_text(lbl_play, LV_SYMBOL_PLAY " Play");
|
||||||
|
lv_obj_center(lbl_play);
|
||||||
|
lv_obj_add_event_cb(g_ctx.btn_play_pause, on_play_pause_click, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
|
// Stop Button
|
||||||
|
g_ctx.btn_stop = lv_btn_create(ctrl_box);
|
||||||
|
lv_obj_set_size(g_ctx.btn_stop, 100, 36);
|
||||||
|
lv_obj_set_style_radius(g_ctx.btn_stop, 18, 0);
|
||||||
|
lv_obj_set_style_bg_color(g_ctx.btn_stop, lv_color_hex(0xF38BA8), 0);
|
||||||
|
lv_obj_set_style_text_color(g_ctx.btn_stop, lv_color_hex(0x11111B), 0);
|
||||||
|
lv_obj_t* lbl_stop = lv_label_create(g_ctx.btn_stop);
|
||||||
|
lv_label_set_text(lbl_stop, LV_SYMBOL_STOP " Stop");
|
||||||
|
lv_obj_center(lbl_stop);
|
||||||
|
lv_obj_add_event_cb(g_ctx.btn_stop, on_stop_click, LV_EVENT_CLICKED, &g_ctx);
|
||||||
|
|
||||||
|
if (!g_ctx.i2s_dev) {
|
||||||
|
lv_label_set_text(g_ctx.lbl_status, "Error: I2S Not Found");
|
||||||
|
lv_obj_add_state(g_ctx.btn_play_pause, LV_STATE_DISABLED);
|
||||||
|
lv_obj_add_state(g_ctx.btn_stop, LV_STATE_DISABLED);
|
||||||
|
} else if (!g_ctx.input_buf || !g_ctx.pcm_buf) {
|
||||||
|
lv_label_set_text(g_ctx.lbl_status, "Error: Out of Memory");
|
||||||
|
lv_obj_add_state(g_ctx.btn_play_pause, LV_STATE_DISABLED);
|
||||||
|
lv_obj_add_state(g_ctx.btn_stop, LV_STATE_DISABLED);
|
||||||
|
} else {
|
||||||
|
g_ctx.state = STATE_IDLE;
|
||||||
|
update_ui(&g_ctx);
|
||||||
|
|
||||||
|
// Auto-play if a file parameter was provided
|
||||||
|
if (g_ctx.filepath[0] != '\0') {
|
||||||
|
g_ctx.state = STATE_PLAYING;
|
||||||
|
update_ui(&g_ctx);
|
||||||
|
xTaskCreate(mp3_playback_task, "mp3_play", 4096, &g_ctx, 5, &g_ctx.playback_task_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onHideApp(AppHandle app, void* data) {
|
||||||
|
if (g_ctx.state != STATE_IDLE) {
|
||||||
|
g_ctx.state = STATE_IDLE; // Signal task to stop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the task to finish self-deletion to prevent resource leaks
|
||||||
|
while (g_ctx.playback_task_handle != NULL) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_ctx.input_buf) {
|
||||||
|
free(g_ctx.input_buf);
|
||||||
|
g_ctx.input_buf = NULL;
|
||||||
|
}
|
||||||
|
if (g_ctx.pcm_buf) {
|
||||||
|
free(g_ctx.pcm_buf);
|
||||||
|
g_ctx.pcm_buf = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
tt_app_register((AppRegistration) {
|
||||||
|
.onShow = onShowApp,
|
||||||
|
.onHide = onHideApp
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
|||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.7.0-dev
|
||||||
|
platforms=esp32s3
|
||||||
|
[app]
|
||||||
|
id=one.tactility.mp3player
|
||||||
|
versionName=1.0.0
|
||||||
|
versionCode=1
|
||||||
|
name=MP3 Player
|
||||||
@@ -4,17 +4,17 @@
|
|||||||
#include "drivers/Colors.h"
|
#include "drivers/Colors.h"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <tactility/drivers/display.h>
|
#include <tt_hal_display.h>
|
||||||
|
|
||||||
class PixelBuffer {
|
class PixelBuffer {
|
||||||
uint16_t pixelWidth;
|
uint16_t pixelWidth;
|
||||||
uint16_t pixelHeight;
|
uint16_t pixelHeight;
|
||||||
enum DisplayColorFormat colorFormat;
|
ColorFormat colorFormat;
|
||||||
uint8_t* data;
|
uint8_t* data;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, enum DisplayColorFormat colorFormat) :
|
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, ColorFormat colorFormat) :
|
||||||
pixelWidth(pixelWidth),
|
pixelWidth(pixelWidth),
|
||||||
pixelHeight(pixelHeight),
|
pixelHeight(pixelHeight),
|
||||||
colorFormat(colorFormat)
|
colorFormat(colorFormat)
|
||||||
@@ -35,7 +35,7 @@ public:
|
|||||||
return pixelHeight;
|
return pixelHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DisplayColorFormat getColorFormat() const {
|
ColorFormat getColorFormat() const {
|
||||||
return colorFormat;
|
return colorFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,14 +58,14 @@ public:
|
|||||||
|
|
||||||
uint8_t getPixelSize() const {
|
uint8_t getPixelSize() const {
|
||||||
switch (colorFormat) {
|
switch (colorFormat) {
|
||||||
case DISPLAY_COLOR_FORMAT_MONOCHROME:
|
case COLOR_FORMAT_MONOCHROME:
|
||||||
return 1;
|
return 1;
|
||||||
case DISPLAY_COLOR_FORMAT_BGR565:
|
case COLOR_FORMAT_BGR565:
|
||||||
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED:
|
case COLOR_FORMAT_BGR565_SWAPPED:
|
||||||
case DISPLAY_COLOR_FORMAT_RGB565:
|
case COLOR_FORMAT_RGB565:
|
||||||
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED:
|
case COLOR_FORMAT_RGB565_SWAPPED:
|
||||||
return 2;
|
return 2;
|
||||||
case DISPLAY_COLOR_FORMAT_RGB888:
|
case COLOR_FORMAT_RGB888:
|
||||||
return 3;
|
return 3;
|
||||||
default:
|
default:
|
||||||
// TODO: Crash with error
|
// TODO: Crash with error
|
||||||
@@ -82,13 +82,13 @@ public:
|
|||||||
void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const {
|
void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const {
|
||||||
auto address = getPixelAddress(x, y);
|
auto address = getPixelAddress(x, y);
|
||||||
switch (colorFormat) {
|
switch (colorFormat) {
|
||||||
case DISPLAY_COLOR_FORMAT_MONOCHROME:
|
case COLOR_FORMAT_MONOCHROME:
|
||||||
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
|
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
|
||||||
break;
|
break;
|
||||||
case DISPLAY_COLOR_FORMAT_BGR565:
|
case COLOR_FORMAT_BGR565:
|
||||||
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||||
break;
|
break;
|
||||||
case DISPLAY_COLOR_FORMAT_BGR565_SWAPPED: {
|
case COLOR_FORMAT_BGR565_SWAPPED: {
|
||||||
// TODO: Make proper conversion function
|
// TODO: Make proper conversion function
|
||||||
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||||
uint8_t temp = *address;
|
uint8_t temp = *address;
|
||||||
@@ -96,11 +96,11 @@ public:
|
|||||||
*(address + 1) = temp;
|
*(address + 1) = temp;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DISPLAY_COLOR_FORMAT_RGB565: {
|
case COLOR_FORMAT_RGB565: {
|
||||||
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DISPLAY_COLOR_FORMAT_RGB565_SWAPPED: {
|
case COLOR_FORMAT_RGB565_SWAPPED: {
|
||||||
// TODO: Make proper conversion function
|
// TODO: Make proper conversion function
|
||||||
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||||
uint8_t temp = *address;
|
uint8_t temp = *address;
|
||||||
@@ -108,7 +108,7 @@ public:
|
|||||||
*(address + 1) = temp;
|
*(address + 1) = temp;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DISPLAY_COLOR_FORMAT_RGB888: {
|
case COLOR_FORMAT_RGB888: {
|
||||||
uint8_t pixel[3] = { r, g, b };
|
uint8_t pixel[3] = { r, g, b };
|
||||||
memcpy(address, pixel, 3);
|
memcpy(address, pixel, 3);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,47 +1,49 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <tactility/device.h>
|
#include <cassert>
|
||||||
#include <tactility/drivers/display.h>
|
#include <tt_hal_display.h>
|
||||||
#include <Tactility/kernel/Kernel.h>
|
#include <Tactility/kernel/Kernel.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for display_* device driver functions
|
* Wrapper for tt_hal_display_driver_*
|
||||||
*/
|
*/
|
||||||
class DisplayDriver {
|
class DisplayDriver {
|
||||||
|
|
||||||
struct Device* device;
|
DisplayDriverHandle handle = nullptr;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
explicit DisplayDriver(struct Device* device) : device(device) {
|
explicit DisplayDriver(DeviceId id) {
|
||||||
device_get(device);
|
assert(tt_hal_display_driver_supported(id));
|
||||||
|
handle = tt_hal_display_driver_alloc(id);
|
||||||
|
assert(handle != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
~DisplayDriver() {
|
~DisplayDriver() {
|
||||||
device_put(device);
|
tt_hal_display_driver_free(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const {
|
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 {
|
void unlock() const {
|
||||||
device_unlock(device);
|
tt_hal_display_driver_unlock(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getWidth() const {
|
uint16_t getWidth() const {
|
||||||
return display_get_resolution_x(device);
|
return tt_hal_display_driver_get_pixel_width(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t getHeight() const {
|
uint16_t getHeight() const {
|
||||||
return display_get_resolution_y(device);
|
return tt_hal_display_driver_get_pixel_height(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DisplayColorFormat getColorFormat() const {
|
ColorFormat getColorFormat() const {
|
||||||
return display_get_color_format(device);
|
return tt_hal_display_driver_get_colorformat(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
|
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
|
||||||
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
|
#pragma once
|
||||||
|
|
||||||
#include <tactility/device.h>
|
#include <cassert>
|
||||||
#include <tactility/drivers/pointer.h>
|
#include <tt_hal_touch.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for pointer_* device driver functions
|
* Wrapper for tt_hal_touch_driver_*
|
||||||
*/
|
*/
|
||||||
class TouchDriver {
|
class TouchDriver {
|
||||||
|
|
||||||
struct Device* device;
|
TouchDriverHandle handle = nullptr;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
explicit TouchDriver(struct Device* device) : device(device) {
|
explicit TouchDriver(DeviceId id) {
|
||||||
device_get(device);
|
assert(tt_hal_touch_driver_supported(id));
|
||||||
|
handle = tt_hal_touch_driver_alloc(id);
|
||||||
|
assert(handle != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
~TouchDriver() {
|
~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 {
|
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.
|
return tt_hal_touch_driver_get_touched_points(handle, x, y, strength, count, maxCount);
|
||||||
pointer_read_data(device, 0);
|
|
||||||
return pointer_get_touched_points(device, x, y, strength, count, maxCount);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,44 +6,65 @@
|
|||||||
|
|
||||||
#include <tt_app.h>
|
#include <tt_app.h>
|
||||||
#include <tt_app_alertdialog.h>
|
#include <tt_app_alertdialog.h>
|
||||||
|
#include <tt_lvgl.h>
|
||||||
#include <tactility/device.h>
|
|
||||||
#include <tactility/drivers/display.h>
|
|
||||||
#include <tactility/drivers/pointer.h>
|
|
||||||
#include <lvgl/lvgl.h>
|
|
||||||
#include <lvgl/module.h>
|
|
||||||
#include <tactility/module.h>
|
|
||||||
|
|
||||||
constexpr auto TAG = "Main";
|
constexpr auto TAG = "Main";
|
||||||
|
|
||||||
static void onCreate(AppHandle appHandle, void* data) {
|
/** Find a DisplayDevice that supports the DisplayDriver interface */
|
||||||
struct Device* display_device;
|
static bool findUsableDisplay(DeviceId& deviceId) {
|
||||||
if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) {
|
uint16_t display_count = 0;
|
||||||
|
if (!tt_hal_device_find(DEVICE_TYPE_DISPLAY, &deviceId, &display_count, 1)) {
|
||||||
ESP_LOGE(TAG, "No display device found");
|
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_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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Device* touch_device;
|
DeviceId touch_id;
|
||||||
if (device_get_first_active_by_type(&POINTER_TYPE, &touch_device) != ERROR_NONE) {
|
if (!findUsableTouch(touch_id)) {
|
||||||
ESP_LOGE(TAG, "No touch device found");
|
|
||||||
device_put(display_device);
|
|
||||||
tt_app_stop();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop LVGL first (because it's currently using the drivers we want to use)
|
// Stop LVGL first (because it's currently using the drivers we want to use)
|
||||||
module_stop(&lvgl_module);
|
tt_lvgl_stop();
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Creating display driver");
|
ESP_LOGI(TAG, "Creating display driver");
|
||||||
auto display = new DisplayDriver(display_device);
|
auto display = new DisplayDriver(display_id);
|
||||||
device_put(display_device);
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Creating touch driver");
|
ESP_LOGI(TAG, "Creating touch driver");
|
||||||
auto touch = new TouchDriver(touch_device);
|
auto touch = new TouchDriver(touch_id);
|
||||||
device_put(touch_device);
|
|
||||||
|
|
||||||
// Run the main logic
|
// Run the main logic
|
||||||
ESP_LOGI(TAG, "Running application");
|
ESP_LOGI(TAG, "Running application");
|
||||||
@@ -61,9 +82,9 @@ static void onCreate(AppHandle appHandle, void* data) {
|
|||||||
|
|
||||||
static void onDestroy(AppHandle appHandle, void* data) {
|
static void onDestroy(AppHandle appHandle, void* data) {
|
||||||
// Restart LVGL to resume rendering of regular apps
|
// Restart LVGL to resume rendering of regular apps
|
||||||
if (!module_is_started(&lvgl_module)) {
|
if (!tt_lvgl_is_started()) {
|
||||||
ESP_LOGI(TAG, "Restarting LVGL");
|
ESP_LOGI(TAG, "Restarting LVGL");
|
||||||
module_start(&lvgl_module);
|
tt_lvgl_start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.mystifydemo
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.8.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=8
|
[app]
|
||||||
app.name=Mystify Demo
|
id=one.tactility.mystifydemo
|
||||||
|
versionName=0.3.0
|
||||||
|
versionCode=3
|
||||||
|
name=Mystify Demo
|
||||||
|
|||||||
@@ -8,13 +8,12 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include <tt_app_alertdialog.h>
|
#include <tt_app_alertdialog.h>
|
||||||
|
#include <tt_lvgl.h>
|
||||||
#include <TactilityCpp/LvglLock.h>
|
#include <TactilityCpp/LvglLock.h>
|
||||||
#include <TactilityCpp/Preferences.h>
|
#include <TactilityCpp/Preferences.h>
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/drivers/uart_controller.h>
|
#include <tactility/drivers/uart_controller.h>
|
||||||
|
|
||||||
constexpr TickType_t LVGL_DEFAULT_LOCK_TIME = 500; // 500 ticks = 500 ms
|
|
||||||
|
|
||||||
class ConnectView final : public View {
|
class ConnectView final : public View {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -46,7 +45,7 @@ private:
|
|||||||
|
|
||||||
void onConnect() {
|
void onConnect() {
|
||||||
auto lock = lvglLock.asScopedLock();
|
auto lock = lvglLock.asScopedLock();
|
||||||
if (!lock.lock(LVGL_DEFAULT_LOCK_TIME)) {
|
if (!lock.lock(TT_LVGL_DEFAULT_LOCK_TIME)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
|
|
||||||
|
#include <tt_lvgl.h>
|
||||||
|
|
||||||
#include <Tactility/RecursiveMutex.h>
|
#include <Tactility/RecursiveMutex.h>
|
||||||
#include <Tactility/Thread.h>
|
#include <Tactility/Thread.h>
|
||||||
#include <TactilityCpp/LvglLock.h>
|
#include <TactilityCpp/LvglLock.h>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "SerialConsole.h"
|
#include "SerialConsole.h"
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
|
||||||
constexpr auto* TAG = "SerialMonitor";
|
constexpr auto* TAG = "SerialMonitor";
|
||||||
|
|
||||||
@@ -43,9 +43,9 @@ void SerialConsole::onShow(AppHandle appHandle, lv_obj_t* parent) {
|
|||||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
auto* toolbar = 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);
|
lv_obj_add_flag(disconnectButton, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
wrapperWidget = lv_obj_create(parent);
|
wrapperWidget = lv_obj_create(parent);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.serialconsole
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.9.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=9
|
[app]
|
||||||
app.name=Serial Console
|
id=one.tactility.serialconsole
|
||||||
|
versionName=0.4.0
|
||||||
|
versionCode=4
|
||||||
|
name=Serial Console
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
#include "Snake.h"
|
#include "Snake.h"
|
||||||
|
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <tt_app_alertdialog.h>
|
#include <tt_app_alertdialog.h>
|
||||||
#include <tt_app_selectiondialog.h>
|
#include <tt_app_selectiondialog.h>
|
||||||
#include <tt_preferences.h>
|
#include <tt_preferences.h>
|
||||||
|
|
||||||
#include <TactilityCpp/LvglLock.h>
|
#include <TactilityCpp/LvglLock.h>
|
||||||
|
|
||||||
#include <lvgl/lvgl.h>
|
#include <tactility/lvgl_module.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
|
|
||||||
constexpr auto* TAG = "Snake";
|
constexpr auto* TAG = "Snake";
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ void Snake::onShow(AppHandle appHandle, lv_obj_t* parent) {
|
|||||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
// Create toolbar
|
// Create toolbar
|
||||||
toolbar = lvgl_toolbar_create(parent, "Snake");
|
toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
|
||||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
// Create main wrapper
|
// Create main wrapper
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <tactility/device.h>
|
#include <tt_lvgl_keyboard.h>
|
||||||
#include <tactility/drivers/keyboard.h>
|
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
static void game_play_event(lv_event_t* e);
|
static void game_play_event(lv_event_t* e);
|
||||||
@@ -37,7 +36,7 @@ static void delete_event(lv_event_t* e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore edit mode and remove from group before cleanup
|
// Restore edit mode and remove from group before cleanup
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
|
if (tt_lvgl_hardware_keyboard_is_available()) {
|
||||||
lv_group_t* group = lv_group_get_default();
|
lv_group_t* group = lv_group_get_default();
|
||||||
if (group) lv_group_set_editing(group, false);
|
if (group) lv_group_set_editing(group, false);
|
||||||
lv_group_remove_obj(game->container);
|
lv_group_remove_obj(game->container);
|
||||||
@@ -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);
|
lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL);
|
||||||
|
|
||||||
// Set up keyboard focus if available
|
// Set up keyboard focus if available
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
|
if (tt_lvgl_hardware_keyboard_is_available()) {
|
||||||
lv_group_t* group = lv_group_get_default();
|
lv_group_t* group = lv_group_get_default();
|
||||||
if (group) {
|
if (group) {
|
||||||
lv_group_add_obj(group, game->container);
|
lv_group_add_obj(group, game->container);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.snake
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.10.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=10
|
[app]
|
||||||
app.name=Snake
|
id=one.tactility.snake
|
||||||
app.description=Classic Snake game
|
versionName=0.5.0
|
||||||
|
versionCode=5
|
||||||
|
name=Snake
|
||||||
|
description=Classic Snake game
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
#include "TamaTac.h"
|
#include "TamaTac.h"
|
||||||
#include "SpriteData.h"
|
#include "SpriteData.h"
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <tt_app_alertdialog.h>
|
#include <tt_app_alertdialog.h>
|
||||||
#include <Tactility/kernel/Kernel.h>
|
#include <Tactility/kernel/Kernel.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
@@ -57,6 +57,7 @@ void TamaTac::onShow(AppHandle context, lv_obj_t* parent) {
|
|||||||
if (sfxEngine == nullptr) {
|
if (sfxEngine == nullptr) {
|
||||||
sfxEngine = new SfxEngine();
|
sfxEngine = new SfxEngine();
|
||||||
sfxEngine->start();
|
sfxEngine->start();
|
||||||
|
sfxEngine->applyVolumePreset(SfxEngine::VolumePreset::Normal);
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
bool soundEnabled;
|
bool soundEnabled;
|
||||||
@@ -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_all(parent, 0, 0);
|
||||||
lv_obj_set_style_pad_row(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);
|
menuButton = tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onMenuClicked, this);
|
||||||
lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onCleanClicked, this);
|
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onCleanClicked, this);
|
||||||
lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_REFRESH, onResetClicked, this);
|
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_REFRESH, onResetClicked, this);
|
||||||
|
|
||||||
wrapperWidget = lv_obj_create(parent);
|
wrapperWidget = lv_obj_create(parent);
|
||||||
lv_obj_set_width(wrapperWidget, LV_PCT(100));
|
lv_obj_set_width(wrapperWidget, LV_PCT(100));
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.tamatac
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.5.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=5
|
[app]
|
||||||
app.name=TamaTac
|
id=one.tactility.tamatac
|
||||||
app.description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
|
versionName=0.1.0
|
||||||
|
versionCode=1
|
||||||
|
name=TamaTac
|
||||||
|
description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
#include "TodoList.h"
|
#include "TodoList.h"
|
||||||
#include <tt_app.h>
|
#include <tt_app.h>
|
||||||
#include <tactility/filesystem/file_mutex.h>
|
#include <tt_lock.h>
|
||||||
#include <Tactility/kernel/Kernel.h>
|
#include <Tactility/kernel/Kernel.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <lvgl/lvgl.h>
|
#include <tt_lvgl_keyboard.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_module.h>
|
||||||
|
#include <tactility/lvgl_fonts.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -59,48 +60,52 @@ void TodoList::saveTodos() {
|
|||||||
char savePath[256];
|
char savePath[256];
|
||||||
if (!getSaveFilePath(savePath, sizeof(savePath))) return;
|
if (!getSaveFilePath(savePath, sizeof(savePath))) return;
|
||||||
|
|
||||||
struct FileMutex mutex;
|
auto lock = tt_lock_alloc_for_path(savePath);
|
||||||
file_mutex_get(&mutex, savePath);
|
if (!lock) return;
|
||||||
file_mutex_lock(&mutex);
|
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
|
||||||
FILE* f = fopen(savePath, "w");
|
FILE* f = fopen(savePath, "w");
|
||||||
if (f) {
|
if (f) {
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
fprintf(f, "%c %s\n", items[i].done ? '+' : '-', items[i].text);
|
fprintf(f, "%c %s\n", items[i].done ? '+' : '-', items[i].text);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
}
|
}
|
||||||
fclose(f);
|
tt_lock_release(lock);
|
||||||
}
|
}
|
||||||
file_mutex_unlock(&mutex);
|
tt_lock_free(lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TodoList::loadTodos() {
|
void TodoList::loadTodos() {
|
||||||
char savePath[256];
|
char savePath[256];
|
||||||
if (!getSaveFilePath(savePath, sizeof(savePath))) return;
|
if (!getSaveFilePath(savePath, sizeof(savePath))) return;
|
||||||
|
|
||||||
struct FileMutex mutex;
|
auto lock = tt_lock_alloc_for_path(savePath);
|
||||||
file_mutex_get(&mutex, savePath);
|
if (!lock) return;
|
||||||
|
|
||||||
file_mutex_lock(&mutex);
|
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
|
||||||
count = 0;
|
count = 0;
|
||||||
FILE* f = fopen(savePath, "r");
|
FILE* f = fopen(savePath, "r");
|
||||||
if (f) {
|
if (f) {
|
||||||
char line[MAX_TEXT_LEN + 4];
|
char line[MAX_TEXT_LEN + 4];
|
||||||
while (count < MAX_TODOS && fgets(line, sizeof(line), f)) {
|
while (count < MAX_TODOS && fgets(line, sizeof(line), f)) {
|
||||||
size_t len = strlen(line);
|
size_t len = strlen(line);
|
||||||
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
|
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
|
||||||
line[--len] = '\0';
|
line[--len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len < 3 || line[1] != ' ') continue;
|
||||||
|
|
||||||
|
TodoItem* item = &items[count];
|
||||||
|
item->done = (line[0] == '+');
|
||||||
|
strncpy(item->text, &line[2], MAX_TEXT_LEN - 1);
|
||||||
|
item->text[MAX_TEXT_LEN - 1] = '\0';
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
|
fclose(f);
|
||||||
if (len < 3 || line[1] != ' ') continue;
|
|
||||||
|
|
||||||
TodoItem* item = &items[count];
|
|
||||||
item->done = (line[0] == '+');
|
|
||||||
strncpy(item->text, &line[2], MAX_TEXT_LEN - 1);
|
|
||||||
item->text[MAX_TEXT_LEN - 1] = '\0';
|
|
||||||
count++;
|
|
||||||
}
|
}
|
||||||
fclose(f);
|
tt_lock_release(lock);
|
||||||
}
|
}
|
||||||
file_mutex_unlock(&mutex);
|
tt_lock_free(lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── UI Helpers ───────────────────────────────────────────────────── */
|
/* ── 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);
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
/* Toolbar */
|
/* 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_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
lv_obj_t* countWrapper = lv_obj_create(toolbar);
|
lv_obj_t* countWrapper = lv_obj_create(toolbar);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.todolist
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.7.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=7
|
[app]
|
||||||
app.name=Todo List
|
id=one.tactility.todolist
|
||||||
app.description=Simple task list manager
|
versionName=0.2.0
|
||||||
|
versionCode=2
|
||||||
|
name=Todo List
|
||||||
|
description=Simple task list manager
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
#include "TwoEleven.h"
|
#include "TwoEleven.h"
|
||||||
|
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <tt_lvgl_toolbar.h>
|
||||||
#include <tt_app_alertdialog.h>
|
#include <tt_app_alertdialog.h>
|
||||||
#include <tt_app_selectiondialog.h>
|
#include <tt_app_selectiondialog.h>
|
||||||
#include <tt_preferences.h>
|
#include <tt_preferences.h>
|
||||||
#include <lvgl/lvgl.h>
|
#include <tactility/lvgl_module.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <tactility/lvgl_fonts.h>
|
||||||
#include <TactilityCpp/LvglLock.h>
|
#include <TactilityCpp/LvglLock.h>
|
||||||
|
|
||||||
constexpr auto* TAG = "TwoEleven";
|
constexpr auto* TAG = "TwoEleven";
|
||||||
@@ -257,7 +257,7 @@ void TwoEleven::onShow(AppHandle appHandle, lv_obj_t* parent) {
|
|||||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
// Create toolbar
|
// Create toolbar
|
||||||
toolbar = lvgl_toolbar_create(parent, "2048");
|
toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
|
||||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
// Create main wrapper
|
// Create main wrapper
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
#include "TwoElevenHelpers.h"
|
#include "TwoElevenHelpers.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <tactility/device.h>
|
#include <tt_lvgl_keyboard.h>
|
||||||
#include <tactility/drivers/keyboard.h>
|
|
||||||
|
|
||||||
static void game_play_event(lv_event_t * e);
|
static void game_play_event(lv_event_t * e);
|
||||||
static void btnm_event_cb(lv_event_t * e);
|
static void btnm_event_cb(lv_event_t * e);
|
||||||
@@ -19,7 +18,7 @@ static void delete_event(lv_event_t * e)
|
|||||||
twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(obj);
|
twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(obj);
|
||||||
if (game_2048) {
|
if (game_2048) {
|
||||||
// Restore edit mode and remove from group before cleanup
|
// Restore edit mode and remove from group before cleanup
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
|
if (tt_lvgl_hardware_keyboard_is_available()) {
|
||||||
lv_group_t* group = lv_group_get_default();
|
lv_group_t* group = lv_group_get_default();
|
||||||
if (group) lv_group_set_editing(group, false);
|
if (group) lv_group_set_editing(group, false);
|
||||||
lv_group_remove_obj(game_2048->btnm);
|
lv_group_remove_obj(game_2048->btnm);
|
||||||
@@ -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(game_2048->btnm, btnm_event_cb, LV_EVENT_DRAW_TASK_ADDED, NULL);
|
||||||
lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL);
|
lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL);
|
||||||
|
|
||||||
if (device_has_active_by_type(&KEYBOARD_TYPE)) {
|
if (tt_lvgl_hardware_keyboard_is_available()) {
|
||||||
lv_group_t* group = lv_group_get_default();
|
lv_group_t* group = lv_group_get_default();
|
||||||
if (group) {
|
if (group) {
|
||||||
lv_group_add_obj(group, game_2048->btnm);
|
lv_group_add_obj(group, game_2048->btnm);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
manifest.version=0.2
|
[manifest]
|
||||||
target.sdk=0.8.0-dev
|
version=0.1
|
||||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
[target]
|
||||||
app.id=one.tactility.twoeleven
|
sdk=0.7.0-dev
|
||||||
app.version.name=0.9.0
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
app.version.code=9
|
[app]
|
||||||
app.name=2048
|
id=one.tactility.twoeleven
|
||||||
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).
|
versionName=0.4.0
|
||||||
|
versionCode=4
|
||||||
|
name=2048
|
||||||
|
description=A fun, customizable 2048 sliding tile game for tactility!\nSlide tiles to combine numbers and reach 2048.\nChoose grid sizes: 3x3 (easy), 4x4 (classic), 5x5, or 6x6 (expert).
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import boto3
|
|
||||||
|
|
||||||
SHELL_COLOR_RED = "\033[91m"
|
|
||||||
SHELL_COLOR_ORANGE = "\033[93m"
|
|
||||||
SHELL_COLOR_RESET = "\033[m"
|
|
||||||
|
|
||||||
def print_warning(message):
|
|
||||||
print(f"{SHELL_COLOR_ORANGE}WARNING: {message}{SHELL_COLOR_RESET}")
|
|
||||||
|
|
||||||
def print_error(message):
|
|
||||||
print(f"{SHELL_COLOR_RED}ERROR: {message}{SHELL_COLOR_RESET}")
|
|
||||||
|
|
||||||
def print_help():
|
|
||||||
print("Usage: python upload-app-files.py [path] [sdkVersion] [cloudflareAccountId] [cloudflareTokenName] [cloudflareTokenValue]")
|
|
||||||
print("")
|
|
||||||
print("Options:")
|
|
||||||
print(" --index-only Upload only apps.json")
|
|
||||||
|
|
||||||
def exit_with_error(message):
|
|
||||||
print_error(message)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def main(path: str, sdk_version: str, cloudflare_account_id, cloudflare_token_name: str, cloudflare_token_value: str, index_only: bool):
|
|
||||||
if not os.path.exists(path):
|
|
||||||
exit_with_error(f"Path not found: {path}")
|
|
||||||
s3 = boto3.client(
|
|
||||||
service_name="s3",
|
|
||||||
endpoint_url=f"https://{cloudflare_account_id}.r2.cloudflarestorage.com",
|
|
||||||
aws_access_key_id=cloudflare_token_name,
|
|
||||||
aws_secret_access_key=cloudflare_token_value,
|
|
||||||
region_name="auto"
|
|
||||||
)
|
|
||||||
files_to_upload = os.listdir(path)
|
|
||||||
if index_only:
|
|
||||||
files_to_upload = [f for f in files_to_upload if f == 'apps.json']
|
|
||||||
else:
|
|
||||||
# Ensure apps.json is uploaded last so it never references files that
|
|
||||||
# haven't finished uploading yet.
|
|
||||||
files_to_upload.sort(key=lambda f: f == 'apps.json')
|
|
||||||
counter = 1
|
|
||||||
total = len(files_to_upload)
|
|
||||||
for file_name in files_to_upload:
|
|
||||||
object_path = f"apps/{sdk_version}/{file_name}"
|
|
||||||
print(f"[{counter}/{total}] Uploading {file_name} to {object_path}")
|
|
||||||
file_path = os.path.join(path, file_name)
|
|
||||||
try:
|
|
||||||
s3.upload_file(file_path, "tactility", object_path)
|
|
||||||
except Exception as e:
|
|
||||||
exit_with_error(f"Failed to upload {file_name}: {str(e)}")
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Tactility CDN Apps Uploader")
|
|
||||||
if "--help" in sys.argv:
|
|
||||||
print_help()
|
|
||||||
sys.exit()
|
|
||||||
# Argument validation
|
|
||||||
if len(sys.argv) < 6:
|
|
||||||
print_help()
|
|
||||||
sys.exit(1)
|
|
||||||
main(
|
|
||||||
path=sys.argv[1],
|
|
||||||
sdk_version=sys.argv[2],
|
|
||||||
cloudflare_account_id=sys.argv[3],
|
|
||||||
cloudflare_token_name=sys.argv[4],
|
|
||||||
cloudflare_token_value=sys.argv[5],
|
|
||||||
index_only="--index-only" in sys.argv
|
|
||||||
)
|
|
||||||
@@ -10,15 +10,13 @@ UnitDualButton::~UnitDualButton() {
|
|||||||
bool UnitDualButton::begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB) {
|
bool UnitDualButton::begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB) {
|
||||||
if (!controller) return false;
|
if (!controller) return false;
|
||||||
|
|
||||||
gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_UP;
|
descA_ = gpio_descriptor_acquire(controller, pinA, GPIO_OWNER_GPIO);
|
||||||
|
|
||||||
descA_ = gpio_descriptor_acquire(controller, pinA, flags, GPIO_OWNER_GPIO);
|
|
||||||
if (!descA_) {
|
if (!descA_) {
|
||||||
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinA);
|
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinA);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
descB_ = gpio_descriptor_acquire(controller, pinB, flags, GPIO_OWNER_GPIO);
|
descB_ = gpio_descriptor_acquire(controller, pinB, GPIO_OWNER_GPIO);
|
||||||
if (!descB_) {
|
if (!descB_) {
|
||||||
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinB);
|
ESP_LOGW(TAG, "Failed to acquire pin %d", (int)pinB);
|
||||||
gpio_descriptor_release(descA_);
|
gpio_descriptor_release(descA_);
|
||||||
@@ -26,6 +24,20 @@ bool UnitDualButton::begin(Device* controller, gpio_pin_t pinA, gpio_pin_t pinB)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_UP;
|
||||||
|
if (gpio_descriptor_set_flags(descA_, flags) != ERROR_NONE) {
|
||||||
|
ESP_LOGW(TAG, "Failed to configure pin %d flags", (int)pinA);
|
||||||
|
gpio_descriptor_release(descA_); gpio_descriptor_release(descB_);
|
||||||
|
descA_ = descB_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (gpio_descriptor_set_flags(descB_, flags) != ERROR_NONE) {
|
||||||
|
ESP_LOGW(TAG, "Failed to configure pin %d flags", (int)pinB);
|
||||||
|
gpio_descriptor_release(descA_); gpio_descriptor_release(descB_);
|
||||||
|
descA_ = descB_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ready_ = true;
|
ready_ = true;
|
||||||
ESP_LOGI(TAG, "DualButton ready on pins %d/%d", (int)pinA, (int)pinB);
|
ESP_LOGI(TAG, "DualButton ready on pins %d/%d", (int)pinA, (int)pinB);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/drivers/audio_stream.h>
|
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
@@ -162,6 +161,12 @@ public:
|
|||||||
// Settings
|
// Settings
|
||||||
void setEnabled(bool enabled) { enabled_ = enabled; }
|
void setEnabled(bool enabled) { enabled_ = enabled; }
|
||||||
bool isEnabled() const { return enabled_; }
|
bool isEnabled() const { return enabled_; }
|
||||||
|
void setVolume(float vol) { masterVolume_ = (vol < 0) ? 0 : (vol > 1.0f) ? 1.0f : vol; }
|
||||||
|
float getVolume() const { return masterVolume_; }
|
||||||
|
|
||||||
|
// Volume presets (consistent with SoundEngine naming)
|
||||||
|
enum class VolumePreset { Quiet, Normal, Loud };
|
||||||
|
void applyVolumePreset(VolumePreset preset);
|
||||||
|
|
||||||
// Polyphonic gate (consistent with SoundEngine)
|
// Polyphonic gate (consistent with SoundEngine)
|
||||||
void setPolyphonicGateEnabled(bool enabled) { polyphonicGateEnabled_ = enabled; }
|
void setPolyphonicGateEnabled(bool enabled) { polyphonicGateEnabled_ = enabled; }
|
||||||
@@ -272,21 +277,14 @@ private:
|
|||||||
// State
|
// State
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
Device* audioStreamDevice_ = nullptr;
|
Device* i2sDevice_ = nullptr;
|
||||||
AudioStreamHandle audioStreamHandle_ = nullptr;
|
|
||||||
TaskHandle_t task_ = nullptr;
|
TaskHandle_t task_ = nullptr;
|
||||||
SemaphoreHandle_t stopSemaphore_ = nullptr; // Signaled when audio task exits
|
SemaphoreHandle_t stopSemaphore_ = nullptr; // Signaled when audio task exits
|
||||||
QueueHandle_t msgQueue_ = nullptr;
|
QueueHandle_t msgQueue_ = nullptr;
|
||||||
|
|
||||||
volatile bool running_ = false;
|
volatile bool running_ = false;
|
||||||
volatile bool enabled_ = true;
|
volatile bool enabled_ = true;
|
||||||
|
volatile float masterVolume_ = 0.5f;
|
||||||
// Cached system output volume (0..1), refreshed periodically from the shared
|
|
||||||
// audio_stream device so the preset acts as a relative multiplier on top of it
|
|
||||||
// rather than an absolute level (a "Quiet" preset should sound quiet relative
|
|
||||||
// to whatever the user has the system volume set to, not in absolute terms).
|
|
||||||
float systemVolumeMix_ = 1.0f;
|
|
||||||
int systemVolumePollCounter_ = 0;
|
|
||||||
|
|
||||||
// Polyphonic gate
|
// Polyphonic gate
|
||||||
volatile bool polyphonicGateEnabled_ = true;
|
volatile bool polyphonicGateEnabled_ = true;
|
||||||
|
|||||||
@@ -35,8 +35,11 @@ if (!engine->start()) {
|
|||||||
ESP_LOGE(TAG, "Failed to start SfxEngine");
|
ESP_LOGE(TAG, "Failed to start SfxEngine");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
engine->applyVolumePreset(SfxEngine::VolumePreset::Normal);
|
||||||
|
|
||||||
engine->play(SfxId::Coin); // Predefined SFX
|
engine->play(SfxId::Coin); // Predefined SFX
|
||||||
engine->playNote(0, 60, 200); // Manual: voice 0, C4, 200ms
|
engine->playNote(0, 60, 200); // Manual: voice 0, C4, 200ms
|
||||||
|
engine->setVolume(0.7f); // Volume control
|
||||||
|
|
||||||
engine->stop();
|
engine->stop();
|
||||||
delete engine;
|
delete engine;
|
||||||
@@ -84,11 +87,9 @@ idf_component_register(
|
|||||||
- `void stopVoice(voice)` - Stop specific voice
|
- `void stopVoice(voice)` - Stop specific voice
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
- `void setVolume(float)` - Master volume (0.0-1.0, exponential curve)
|
||||||
- `void setEnabled(bool)` - Mute/unmute
|
- `void setEnabled(bool)` - Mute/unmute
|
||||||
|
- `void applyVolumePreset(VolumePreset)` - Apply Quiet/Normal/Loud preset (configures volume, gate, normalization)
|
||||||
Loudness is controlled by the system output volume (set via the audio_stream device / Settings UI),
|
|
||||||
not by SfxEngine itself -- a fixed app-side gain on top of hardware attenuation gets swamped at low
|
|
||||||
system volumes, so there's no separate volume control here.
|
|
||||||
|
|
||||||
### Mixing (consistent with SoundEngine)
|
### Mixing (consistent with SoundEngine)
|
||||||
- `void setPolyphonicGateEnabled(bool)` - Soft gate when multiple voices clip (default: on)
|
- `void setPolyphonicGateEnabled(bool)` - Soft gate when multiple voices clip (default: on)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "SfxEngine.h"
|
#include "SfxEngine.h"
|
||||||
#include "SfxDefinitions.h"
|
#include "SfxDefinitions.h"
|
||||||
|
|
||||||
#include <tactility/drivers/audio_stream.h>
|
#include <tactility/drivers/i2s_controller.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include "esp_log.h"
|
#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)
|
// Apply polyphonic soft gate (proportional reduction when clipping threatened)
|
||||||
mix = applyPolyphonicGate(mix, activeVoices);
|
mix = applyPolyphonicGate(mix, activeVoices);
|
||||||
|
|
||||||
|
// Apply master volume (exponential curve for perceptual linearity)
|
||||||
|
float volCurve = masterVolume_ * masterVolume_;
|
||||||
|
mix *= volCurve;
|
||||||
|
|
||||||
// Apply auto-normalization (consistent volume across different SFX)
|
// Apply auto-normalization (consistent volume across different SFX)
|
||||||
mix = applyAutoNormalization(mix);
|
mix = applyAutoNormalization(mix);
|
||||||
|
|
||||||
// Brick-wall limiter (final safety net before soft clip)
|
// Brick-wall limiter (final safety net before soft clip)
|
||||||
mix = applyBrickWallLimiter(mix);
|
mix = applyBrickWallLimiter(mix);
|
||||||
|
|
||||||
// The shared system output volume (esp_codec_dev hardware attenuation, set via
|
|
||||||
// the Settings UI / audio_stream_set_volume) is the sole loudness control here --
|
|
||||||
// a fixed app-side gain multiplier stacked on top of it just gets swamped at low
|
|
||||||
// system-volume levels, making any such control feel like it does nothing.
|
|
||||||
mix *= systemVolumeMix_;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cubic soft clip
|
// Cubic soft clip
|
||||||
@@ -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)
|
// Fill audio buffer (member buffer to avoid stack pressure)
|
||||||
self->fillStereoBuffer(self->audioBuffer_, BUFFER_SAMPLES);
|
self->fillStereoBuffer(self->audioBuffer_, BUFFER_SAMPLES);
|
||||||
|
|
||||||
// Write to the audio stream (resampled to the codec's native rate transparently)
|
// Write to I2S
|
||||||
error_t error = audio_stream_write(self->audioStreamHandle_, self->audioBuffer_,
|
error_t error = i2s_controller_write(self->i2sDevice_, self->audioBuffer_,
|
||||||
sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(100));
|
sizeof(self->audioBuffer_), &written, pdMS_TO_TICKS(100));
|
||||||
if (error != ERROR_NONE) {
|
if (error != ERROR_NONE) {
|
||||||
ESP_LOGE(TAG, "Audio stream write error");
|
ESP_LOGE(TAG, "I2S write error");
|
||||||
self->running_ = false;
|
self->running_ = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -491,7 +475,7 @@ void SfxEngine::audioTaskFunc(void* param) {
|
|||||||
|
|
||||||
// Flush silence
|
// Flush silence
|
||||||
memset(self->audioBuffer_, 0, sizeof(self->audioBuffer_));
|
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");
|
ESP_LOGI(TAG, "Audio task exiting");
|
||||||
|
|
||||||
@@ -510,31 +494,33 @@ void SfxEngine::audioTaskFunc(void* param) {
|
|||||||
bool SfxEngine::start() {
|
bool SfxEngine::start() {
|
||||||
if (running_) return true;
|
if (running_) return true;
|
||||||
|
|
||||||
// Find audio stream device
|
// Find I2S device
|
||||||
audioStreamDevice_ = nullptr;
|
i2sDevice_ = nullptr;
|
||||||
device_for_each_of_type(&AUDIO_STREAM_TYPE, &audioStreamDevice_, [](Device* device, void* context) {
|
device_for_each_of_type(&I2S_CONTROLLER_TYPE, &i2sDevice_, [](Device* device, void* context) {
|
||||||
if (!device_is_ready(device)) return true;
|
if (!device_is_ready(device)) return true;
|
||||||
Device** devicePtr = static_cast<Device**>(context);
|
Device** devicePtr = static_cast<Device**>(context);
|
||||||
*devicePtr = device;
|
*devicePtr = device;
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (audioStreamDevice_ == nullptr) {
|
if (i2sDevice_ == nullptr) {
|
||||||
ESP_LOGW(TAG, "No audio stream device found");
|
ESP_LOGW(TAG, "No I2S device found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open output stream (the kernel resamples to the codec's native rate transparently)
|
// Configure I2S
|
||||||
AudioStreamConfig config = {
|
I2sConfig config = {
|
||||||
|
.communication_format = I2S_FORMAT_STAND_I2S,
|
||||||
.sample_rate = SAMPLE_RATE,
|
.sample_rate = SAMPLE_RATE,
|
||||||
.bits_per_sample = 16,
|
.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) {
|
if (error != ERROR_NONE) {
|
||||||
ESP_LOGE(TAG, "Failed to open audio output stream: %s", error_to_string(error));
|
ESP_LOGE(TAG, "Failed to configure I2S: %s", error_to_string(error));
|
||||||
audioStreamDevice_ = nullptr;
|
i2sDevice_ = nullptr;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,14 +528,12 @@ bool SfxEngine::start() {
|
|||||||
msgQueue_ = xQueueCreate(8, sizeof(QueueMsg));
|
msgQueue_ = xQueueCreate(8, sizeof(QueueMsg));
|
||||||
if (msgQueue_ == nullptr) {
|
if (msgQueue_ == nullptr) {
|
||||||
ESP_LOGE(TAG, "Failed to create message queue");
|
ESP_LOGE(TAG, "Failed to create message queue");
|
||||||
audio_stream_close(audioStreamHandle_);
|
i2s_controller_reset(i2sDevice_);
|
||||||
audioStreamHandle_ = nullptr;
|
i2sDevice_ = nullptr;
|
||||||
audioStreamDevice_ = nullptr;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start audio task
|
// Start audio task
|
||||||
systemVolumePollCounter_ = 0; // poll the system volume immediately on the first iteration
|
|
||||||
running_ = true;
|
running_ = true;
|
||||||
BaseType_t result = xTaskCreate(audioTaskFunc, "sfxeng", 4096, this, 5, &task_);
|
BaseType_t result = xTaskCreate(audioTaskFunc, "sfxeng", 4096, this, 5, &task_);
|
||||||
if (result != pdPASS) {
|
if (result != pdPASS) {
|
||||||
@@ -557,9 +541,8 @@ bool SfxEngine::start() {
|
|||||||
running_ = false;
|
running_ = false;
|
||||||
vQueueDelete(msgQueue_);
|
vQueueDelete(msgQueue_);
|
||||||
msgQueue_ = nullptr;
|
msgQueue_ = nullptr;
|
||||||
audio_stream_close(audioStreamHandle_);
|
i2s_controller_reset(i2sDevice_);
|
||||||
audioStreamHandle_ = nullptr;
|
i2sDevice_ = nullptr;
|
||||||
audioStreamDevice_ = nullptr;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,22 +551,19 @@ bool SfxEngine::start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SfxEngine::stop() {
|
void SfxEngine::stop() {
|
||||||
// Guard on msgQueue_ (the resource marker), not running_ - the audio task can clear
|
if (!running_) return;
|
||||||
// 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_) {
|
// Create semaphore for deterministic shutdown
|
||||||
// Only wait on the semaphore if the task might still be alive to signal it - if
|
stopSemaphore_ = xSemaphoreCreateBinary();
|
||||||
// running_ is already false, the task already exited (and self-deleted) on its own.
|
running_ = false;
|
||||||
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));
|
xSemaphoreTake(stopSemaphore_, pdMS_TO_TICKS(500));
|
||||||
}
|
}
|
||||||
|
task_ = nullptr;
|
||||||
}
|
}
|
||||||
task_ = nullptr;
|
|
||||||
|
|
||||||
if (stopSemaphore_ != nullptr) {
|
if (stopSemaphore_ != nullptr) {
|
||||||
vSemaphoreDelete(stopSemaphore_);
|
vSemaphoreDelete(stopSemaphore_);
|
||||||
@@ -595,15 +575,43 @@ void SfxEngine::stop() {
|
|||||||
msgQueue_ = nullptr;
|
msgQueue_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioStreamHandle_ != nullptr) {
|
if (i2sDevice_ != nullptr) {
|
||||||
audio_stream_close(audioStreamHandle_);
|
i2s_controller_reset(i2sDevice_);
|
||||||
audioStreamHandle_ = nullptr;
|
i2sDevice_ = nullptr;
|
||||||
}
|
}
|
||||||
audioStreamDevice_ = nullptr;
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "SfxEngine stopped");
|
ESP_LOGI(TAG, "SfxEngine stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SfxEngine::applyVolumePreset(VolumePreset preset) {
|
||||||
|
switch (preset) {
|
||||||
|
case VolumePreset::Quiet:
|
||||||
|
masterVolume_ = 0.3f;
|
||||||
|
autoNormalize_ = true;
|
||||||
|
targetRms_ = 0.25f;
|
||||||
|
polyphonicGateEnabled_ = true;
|
||||||
|
softGateThreshold_ = 0.90f;
|
||||||
|
ESP_LOGI(TAG, "Applied Quiet preset");
|
||||||
|
break;
|
||||||
|
case VolumePreset::Normal:
|
||||||
|
masterVolume_ = 0.5f;
|
||||||
|
autoNormalize_ = true;
|
||||||
|
targetRms_ = 0.35f;
|
||||||
|
polyphonicGateEnabled_ = true;
|
||||||
|
softGateThreshold_ = 0.95f;
|
||||||
|
ESP_LOGI(TAG, "Applied Normal preset");
|
||||||
|
break;
|
||||||
|
case VolumePreset::Loud:
|
||||||
|
masterVolume_ = 0.75f;
|
||||||
|
autoNormalize_ = true;
|
||||||
|
targetRms_ = 0.45f;
|
||||||
|
polyphonicGateEnabled_ = true;
|
||||||
|
softGateThreshold_ = 0.98f;
|
||||||
|
ESP_LOGI(TAG, "Applied Loud preset");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SfxEngine::play(SfxId sound) {
|
void SfxEngine::play(SfxId sound) {
|
||||||
if (!running_ || msgQueue_ == nullptr) return;
|
if (!running_ || msgQueue_ == nullptr) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/Lock.h>
|
#include <Tactility/Lock.h>
|
||||||
#include <lvgl/lvgl.h>
|
#include <tt_lvgl.h>
|
||||||
|
|
||||||
class LvglLock final : public tt::Lock {
|
class LvglLock final : public tt::Lock {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
using tt::Lock::lock;
|
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const override {
|
||||||
|
return tt_lvgl_lock(timeout);
|
||||||
bool lock(TickType_t timeout) const override {
|
|
||||||
return lvgl_try_lock(timeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void unlock() const override {
|
void unlock() const override {
|
||||||
lvgl_unlock();
|
tt_lvgl_unlock();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+40
-55
@@ -1,23 +1,14 @@
|
|||||||
import json
|
import json
|
||||||
import subprocess
|
|
||||||
import tarfile
|
import tarfile
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import configparser
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, UTC
|
|
||||||
|
|
||||||
def read_properties_file(path):
|
def read_properties_file(path):
|
||||||
properties = {}
|
config = configparser.RawConfigParser()
|
||||||
with open(path, "r") as file:
|
config.read(path)
|
||||||
for line in file:
|
return config
|
||||||
stripped = line.strip()
|
|
||||||
if not stripped or stripped.startswith("#"):
|
|
||||||
continue
|
|
||||||
key, sep, value = line.partition("=")
|
|
||||||
if not sep:
|
|
||||||
continue
|
|
||||||
properties[key.strip()] = value.strip()
|
|
||||||
return properties
|
|
||||||
|
|
||||||
def get_manifest(appPath):
|
def get_manifest(appPath):
|
||||||
"""Extract only the file named 'manifest.properties' from the given tar/tar.gz
|
"""Extract only the file named 'manifest.properties' from the given tar/tar.gz
|
||||||
@@ -71,49 +62,51 @@ def get_manifest(appPath):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_versioned_file_name(manifest):
|
def get_versioned_file_name(manifest):
|
||||||
app_id = manifest["app.id"]
|
app_id = manifest["app"]["id"]
|
||||||
version_code = manifest["app.version.code"]
|
version_code = manifest["app"]["versionCode"]
|
||||||
return f"{app_id}-{version_code}.app"
|
return f"{app_id}-{version_code}.app"
|
||||||
|
|
||||||
def get_os_version(manifest):
|
def get_os_version(manifest):
|
||||||
sdk = manifest["target.sdk"]
|
sdk = manifest["target"]["sdk"]
|
||||||
# Remove trailing hyphen suffix if present
|
# Remove trailing hyphen suffix if present
|
||||||
if "-" in sdk:
|
if "-" in sdk:
|
||||||
return sdk.rsplit("-", 1)[0].strip()
|
return sdk.rsplit("-", 1)[0].strip()
|
||||||
else:
|
else:
|
||||||
return sdk
|
return sdk
|
||||||
|
|
||||||
def check_and_get_sdk_version(manifest_map):
|
|
||||||
"""Ensure all apps target the same (simplified) SDK version and return it."""
|
|
||||||
versions = {get_os_version(manifest) for manifest in manifest_map.values()}
|
|
||||||
if len(versions) != 1:
|
|
||||||
print(f"ERROR: Apps target multiple SDK versions: {sorted(versions)}. All apps must target the same SDK version.")
|
|
||||||
sys.exit(1)
|
|
||||||
return next(iter(versions))
|
|
||||||
|
|
||||||
def get_git_commit_hash():
|
|
||||||
return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()
|
|
||||||
|
|
||||||
def manifest_config_to_flat_json(manifest):
|
def manifest_config_to_flat_json(manifest):
|
||||||
"""Convert a flat (V2) manifest dict into a flat JSON-like dict.
|
"""Convert a ConfigParser manifest into a flat JSON-like dict.
|
||||||
|
|
||||||
Expected keys:
|
Expected sections/keys (case-insensitive for keys):
|
||||||
app.id -> appId
|
- [app]
|
||||||
app.version.name -> appVersionName
|
id -> appId
|
||||||
app.version.code -> appVersionCode (int)
|
versionName -> appVersionName
|
||||||
app.name -> appName
|
versionCode -> appVersionCode (int)
|
||||||
app.description -> appDescription (optional; default "")
|
name -> appName
|
||||||
target.sdk -> targetSdk
|
description -> appDescription (optional; default "")
|
||||||
target.platforms -> targetPlatforms (comma-separated list)
|
- [target]
|
||||||
|
sdk -> targetSdk
|
||||||
|
platforms -> targetPlatforms (comma-separated list)
|
||||||
|
|
||||||
Unknown/missing values fall back to sensible defaults per requirements.
|
Unknown/missing values fall back to sensible defaults per requirements.
|
||||||
"""
|
"""
|
||||||
|
def get_opt(section, option, default=None):
|
||||||
|
if not manifest.has_section(section):
|
||||||
|
return default
|
||||||
|
# try exact option then lowercase (RawConfigParser lowercases by default)
|
||||||
|
if manifest.has_option(section, option):
|
||||||
|
return manifest.get(section, option)
|
||||||
|
low = option.lower()
|
||||||
|
if manifest.has_option(section, low):
|
||||||
|
return manifest.get(section, low)
|
||||||
|
return default
|
||||||
|
|
||||||
# Map values
|
# Map values
|
||||||
app_id = manifest.get("app.id", "")
|
app_id = get_opt("app", "id", "")
|
||||||
app_version_name = manifest.get("app.version.name", "")
|
app_version_name = get_opt("app", "versionName", "")
|
||||||
app_version_code_raw = manifest.get("app.version.code", "0")
|
app_version_code_raw = get_opt("app", "versionCode", "0")
|
||||||
app_name = manifest.get("app.name", "")
|
app_name = get_opt("app", "name", "")
|
||||||
app_description = manifest.get("app.description", "") or ""
|
app_description = get_opt("app", "description", "") or ""
|
||||||
|
|
||||||
# Coerce version code to int safely
|
# Coerce version code to int safely
|
||||||
try:
|
try:
|
||||||
@@ -121,8 +114,8 @@ def manifest_config_to_flat_json(manifest):
|
|||||||
except Exception:
|
except Exception:
|
||||||
app_version_code = 0
|
app_version_code = 0
|
||||||
|
|
||||||
target_sdk = manifest.get("target.sdk", "")
|
target_sdk = get_opt("target", "sdk", "")
|
||||||
platforms_raw = manifest.get("target.platforms", "")
|
platforms_raw = get_opt("target", "platforms", "")
|
||||||
target_platforms = [p.strip() for p in str(platforms_raw).split(",") if p.strip()] if platforms_raw is not None else []
|
target_platforms = [p.strip() for p in str(platforms_raw).split(",") if p.strip()] if platforms_raw is not None else []
|
||||||
|
|
||||||
filename = get_versioned_file_name(manifest)
|
filename = get_versioned_file_name(manifest)
|
||||||
@@ -149,23 +142,15 @@ if __name__ == "__main__":
|
|||||||
sys.exit()
|
sys.exit()
|
||||||
app_directory = sys.argv[1]
|
app_directory = sys.argv[1]
|
||||||
manifest_map = {}
|
manifest_map = {}
|
||||||
|
output_json = {
|
||||||
|
"apps": []
|
||||||
|
}
|
||||||
any_manifest = None
|
any_manifest = None
|
||||||
if os.path.exists(app_directory):
|
if os.path.exists(app_directory):
|
||||||
for file in os.listdir(app_directory):
|
for file in os.listdir(app_directory):
|
||||||
if file.endswith(".app"):
|
if file.endswith(".app"):
|
||||||
file_path = os.path.join(app_directory, file)
|
file_path = os.path.join(app_directory, file)
|
||||||
manifest_map[file_path] = get_manifest(file_path)
|
manifest_map[file_path] = get_manifest(file_path)
|
||||||
# All bundled apps must target the same SDK version; this becomes the CDN path segment
|
|
||||||
sdk_version = check_and_get_sdk_version(manifest_map)
|
|
||||||
with open("sdk_version.txt", "w") as f:
|
|
||||||
f.write(sdk_version)
|
|
||||||
print(f"SDK version: {sdk_version}")
|
|
||||||
output_json = {
|
|
||||||
"sdkVersion": sdk_version,
|
|
||||||
"created": datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
||||||
"gitCommit": get_git_commit_hash(),
|
|
||||||
"apps": []
|
|
||||||
}
|
|
||||||
# Rename files and collect manifest data into output json object
|
# Rename files and collect manifest data into output json object
|
||||||
for file_path in manifest_map.keys():
|
for file_path in manifest_map.keys():
|
||||||
print(f"Processing {file_path}: {manifest_map[file_path]}")
|
print(f"Processing {file_path}: {manifest_map[file_path]}")
|
||||||
@@ -179,4 +164,4 @@ if __name__ == "__main__":
|
|||||||
any_manifest = manifest
|
any_manifest = manifest
|
||||||
# Write JSON
|
# Write JSON
|
||||||
output_json_path = os.path.join(app_directory, "apps.json")
|
output_json_path = os.path.join(app_directory, "apps.json")
|
||||||
write_json(output_json_path, output_json)
|
write_json(output_json_path, output_json)
|
||||||
+36
-23
@@ -1,3 +1,4 @@
|
|||||||
|
import configparser
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -12,7 +13,7 @@ import tarfile
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
ttbuild_path = ".tactility"
|
ttbuild_path = ".tactility"
|
||||||
ttbuild_version = "4.1.0"
|
ttbuild_version = "3.5.1"
|
||||||
ttbuild_cdn = "https://cdn.tactilityproject.org"
|
ttbuild_cdn = "https://cdn.tactilityproject.org"
|
||||||
ttbuild_sdk_json_validity = 3600 # seconds
|
ttbuild_sdk_json_validity = 3600 # seconds
|
||||||
ttport = 6666
|
ttport = 6666
|
||||||
@@ -105,17 +106,9 @@ def get_url(ip, path):
|
|||||||
return f"http://{ip}:{ttport}{path}"
|
return f"http://{ip}:{ttport}{path}"
|
||||||
|
|
||||||
def read_properties_file(path):
|
def read_properties_file(path):
|
||||||
properties = {}
|
config = configparser.RawConfigParser()
|
||||||
with open(path, "r") as file:
|
config.read(path)
|
||||||
for line in file:
|
return config
|
||||||
stripped = line.strip()
|
|
||||||
if not stripped or stripped.startswith("#"):
|
|
||||||
continue
|
|
||||||
key, sep, value = line.partition("=")
|
|
||||||
if not sep:
|
|
||||||
continue
|
|
||||||
properties[key.strip()] = value.strip()
|
|
||||||
return properties
|
|
||||||
|
|
||||||
#endregion Core
|
#endregion Core
|
||||||
|
|
||||||
@@ -192,7 +185,7 @@ def fetch_sdkconfig_files(platform_targets):
|
|||||||
for platform in platform_targets:
|
for platform in platform_targets:
|
||||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||||
target_path = os.path.join(ttbuild_path, sdkconfig_filename)
|
target_path = os.path.join(ttbuild_path, sdkconfig_filename)
|
||||||
if not download_file(f"{ttbuild_cdn}/sdk/{sdkconfig_filename}", target_path):
|
if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path):
|
||||||
exit_with_error(f"Failed to download sdkconfig file for {platform}")
|
exit_with_error(f"Failed to download sdkconfig file for {platform}")
|
||||||
|
|
||||||
#endregion SDK helpers
|
#endregion SDK helpers
|
||||||
@@ -238,12 +231,32 @@ def read_manifest():
|
|||||||
return read_properties_file("manifest.properties")
|
return read_properties_file("manifest.properties")
|
||||||
|
|
||||||
def validate_manifest(manifest):
|
def validate_manifest(manifest):
|
||||||
for key in ("manifest.version", "target.sdk", "target.platforms", "app.id", "app.version.name", "app.version.code", "app.name"):
|
# [manifest]
|
||||||
if key not in manifest:
|
if not "manifest" in manifest:
|
||||||
exit_with_error(f"Invalid manifest format: {key} not found")
|
exit_with_error("Invalid manifest format: [manifest] not found")
|
||||||
|
if not "version" in manifest["manifest"]:
|
||||||
|
exit_with_error("Invalid manifest format: [manifest] version not found")
|
||||||
|
# [target]
|
||||||
|
if not "target" in manifest:
|
||||||
|
exit_with_error("Invalid manifest format: [target] not found")
|
||||||
|
if not "sdk" in manifest["target"]:
|
||||||
|
exit_with_error("Invalid manifest format: [target] sdk not found")
|
||||||
|
if not "platforms" in manifest["target"]:
|
||||||
|
exit_with_error("Invalid manifest format: [target] platforms not found")
|
||||||
|
# [app]
|
||||||
|
if not "app" in manifest:
|
||||||
|
exit_with_error("Invalid manifest format: [app] not found")
|
||||||
|
if not "id" in manifest["app"]:
|
||||||
|
exit_with_error("Invalid manifest format: [app] id not found")
|
||||||
|
if not "versionName" in manifest["app"]:
|
||||||
|
exit_with_error("Invalid manifest format: [app] versionName not found")
|
||||||
|
if not "versionCode" in manifest["app"]:
|
||||||
|
exit_with_error("Invalid manifest format: [app] versionCode not found")
|
||||||
|
if not "name" in manifest["app"]:
|
||||||
|
exit_with_error("Invalid manifest format: [app] name not found")
|
||||||
|
|
||||||
def is_valid_manifest_platform(manifest, platform):
|
def is_valid_manifest_platform(manifest, platform):
|
||||||
manifest_platforms = manifest["target.platforms"].split(",")
|
manifest_platforms = manifest["target"]["platforms"].split(",")
|
||||||
return platform in manifest_platforms
|
return platform in manifest_platforms
|
||||||
|
|
||||||
def validate_manifest_platform(manifest, platform):
|
def validate_manifest_platform(manifest, platform):
|
||||||
@@ -252,7 +265,7 @@ def validate_manifest_platform(manifest, platform):
|
|||||||
|
|
||||||
def get_manifest_target_platforms(manifest, requested_platform):
|
def get_manifest_target_platforms(manifest, requested_platform):
|
||||||
if requested_platform == "" or requested_platform is None:
|
if requested_platform == "" or requested_platform is None:
|
||||||
return manifest["target.platforms"].split(",")
|
return manifest["target"]["platforms"].split(",")
|
||||||
else:
|
else:
|
||||||
validate_manifest_platform(manifest, requested_platform)
|
validate_manifest_platform(manifest, requested_platform)
|
||||||
return [requested_platform]
|
return [requested_platform]
|
||||||
@@ -499,7 +512,7 @@ def build_action(manifest, platform_arg, skip_build):
|
|||||||
if use_local_sdk:
|
if use_local_sdk:
|
||||||
global local_base_path
|
global local_base_path
|
||||||
local_base_path = os.environ.get("TACTILITY_SDK_PATH")
|
local_base_path = os.environ.get("TACTILITY_SDK_PATH")
|
||||||
validate_local_sdks(platforms_to_build, manifest["target.sdk"])
|
validate_local_sdks(platforms_to_build, manifest["target"]["sdk"])
|
||||||
|
|
||||||
if should_fetch_sdkconfig_files(platforms_to_build):
|
if should_fetch_sdkconfig_files(platforms_to_build):
|
||||||
fetch_sdkconfig_files(platforms_to_build)
|
fetch_sdkconfig_files(platforms_to_build)
|
||||||
@@ -508,7 +521,7 @@ def build_action(manifest, platform_arg, skip_build):
|
|||||||
sdk_json = read_sdk_json()
|
sdk_json = read_sdk_json()
|
||||||
validate_self(sdk_json)
|
validate_self(sdk_json)
|
||||||
# Build
|
# Build
|
||||||
sdk_version = manifest["target.sdk"]
|
sdk_version = manifest["target"]["sdk"]
|
||||||
if not use_local_sdk:
|
if not use_local_sdk:
|
||||||
if not sdk_download_all(sdk_version, platforms_to_build):
|
if not sdk_download_all(sdk_version, platforms_to_build):
|
||||||
exit_with_error("Failed to download one or more SDKs")
|
exit_with_error("Failed to download one or more SDKs")
|
||||||
@@ -557,7 +570,7 @@ def get_device_info(ip):
|
|||||||
print_status_error(f"Device info request failed: {e}")
|
print_status_error(f"Device info request failed: {e}")
|
||||||
|
|
||||||
def run_action(manifest, ip):
|
def run_action(manifest, ip):
|
||||||
app_id = manifest["app.id"]
|
app_id = manifest["app"]["id"]
|
||||||
print_status_busy("Running")
|
print_status_busy("Running")
|
||||||
url = get_url(ip, "/app/run")
|
url = get_url(ip, "/app/run")
|
||||||
params = {'id': app_id}
|
params = {'id': app_id}
|
||||||
@@ -601,7 +614,7 @@ def install_action(ip, platforms):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def uninstall_action(manifest, ip):
|
def uninstall_action(manifest, ip):
|
||||||
app_id = manifest["app.id"]
|
app_id = manifest["app"]["id"]
|
||||||
print_status_busy("Uninstalling")
|
print_status_busy("Uninstalling")
|
||||||
url = get_url(ip, "/app/uninstall")
|
url = get_url(ip, "/app/uninstall")
|
||||||
params = {'id': app_id}
|
params = {'id': app_id}
|
||||||
@@ -657,7 +670,7 @@ if __name__ == "__main__":
|
|||||||
exit_with_error("manifest.properties not found")
|
exit_with_error("manifest.properties not found")
|
||||||
manifest = read_manifest()
|
manifest = read_manifest()
|
||||||
validate_manifest(manifest)
|
validate_manifest(manifest)
|
||||||
all_platform_targets = manifest["target.platforms"].split(",")
|
all_platform_targets = manifest["target"]["platforms"].split(",")
|
||||||
# Update SDK cache (tool.json)
|
# Update SDK cache (tool.json)
|
||||||
if not use_local_sdk and should_update_tool_json() and not update_tool_json():
|
if not use_local_sdk and should_update_tool_json() and not update_tool_json():
|
||||||
exit_with_error("Failed to retrieve SDK info")
|
exit_with_error("Failed to retrieve SDK info")
|
||||||
|
|||||||
Reference in New Issue
Block a user