feat: integrate MCP and ES3C28P audio support with upstream compatibility updates

This commit is contained in:
Adolfo Reyna
2026-07-04 15:41:37 -04:00
parent b8bf59fedf
commit 300ddb3a7f
21 changed files with 1260 additions and 33 deletions
+3 -2
View File
@@ -106,8 +106,9 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
target_compile_definitions(freertos_kernel PUBLIC "projCOVERAGE_TEST=0") target_compile_definitions(freertos_kernel PUBLIC "projCOVERAGE_TEST=0")
# EmbedTLS # EmbedTLS
set(ENABLE_TESTING OFF) set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(ENABLE_PROGRAMS OFF) set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
add_subdirectory(Libraries/mbedtls) add_subdirectory(Libraries/mbedtls)
# SDL # SDL
+2 -1
View File
@@ -25,7 +25,7 @@
i2c0 { i2c0 {
compatible = "espressif,esp32-i2c"; compatible = "espressif,esp32-i2c";
port = <I2C_NUM_0>; port = <I2C_NUM_0>;
clock-frequency = <400000>; clock-frequency = <100000>;
pin-sda = <&gpio0 16 GPIO_FLAG_NONE>; pin-sda = <&gpio0 16 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 15 GPIO_FLAG_NONE>; pin-scl = <&gpio0 15 GPIO_FLAG_NONE>;
}; };
@@ -55,6 +55,7 @@
pin-d1 = <&gpio0 41 GPIO_FLAG_NONE>; pin-d1 = <&gpio0 41 GPIO_FLAG_NONE>;
pin-d2 = <&gpio0 48 GPIO_FLAG_NONE>; pin-d2 = <&gpio0 48 GPIO_FLAG_NONE>;
pin-d3 = <&gpio0 47 GPIO_FLAG_NONE>; pin-d3 = <&gpio0 47 GPIO_FLAG_NONE>;
slot = <SDMMC_HOST_SLOT_1>;
bus-width = <4>; bus-width = <4>;
}; };
+6 -3
View File
@@ -18,6 +18,8 @@ endif ()
set(DEVICETREE_LOCATION "${PROJECT_ROOT}/Devices/${TACTILITY_DEVICE_ID}") set(DEVICETREE_LOCATION "${PROJECT_ROOT}/Devices/${TACTILITY_DEVICE_ID}")
find_package(Python3 COMPONENTS Interpreter REQUIRED)
# Check if device has Bluetooth enabled # Check if device has Bluetooth enabled
# Fixes the sdkconfig bluetooth enable options from getting nuked on non-P4+C6 builds when idf build runs # Fixes the sdkconfig bluetooth enable options from getting nuked on non-P4+C6 builds when idf build runs
if (DEFINED ENV{ESP_IDF_VERSION}) if (DEFINED ENV{ESP_IDF_VERSION})
@@ -32,7 +34,7 @@ endif()
# #
execute_process( execute_process(
COMMAND python -m pip install lark==1.3.1 pyyaml==6.0.3 COMMAND ${Python3_EXECUTABLE} -m pip install --break-system-packages lark==1.3.1 pyyaml==6.0.3
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
) )
@@ -41,7 +43,7 @@ execute_process(
# #
execute_process( execute_process(
COMMAND python "${PROJECT_ROOT}/Buildscripts/DevicetreeCompiler/dependencies.py" "${DEVICETREE_LOCATION}" COMMAND ${Python3_EXECUTABLE} "${PROJECT_ROOT}/Buildscripts/DevicetreeCompiler/dependencies.py" "${DEVICETREE_LOCATION}"
WORKING_DIRECTORY "${PROJECT_ROOT}" WORKING_DIRECTORY "${PROJECT_ROOT}"
OUTPUT_VARIABLE DEVICE_DEPENDENCIES OUTPUT_VARIABLE DEVICE_DEPENDENCIES
OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_STRIP_TRAILING_WHITESPACE
@@ -107,7 +109,7 @@ add_custom_target(AlwaysRun
add_custom_command( add_custom_command(
OUTPUT "${GENERATED_DIR}/devicetree.c" OUTPUT "${GENERATED_DIR}/devicetree.c"
"${GENERATED_DIR}/devicetree.h" "${GENERATED_DIR}/devicetree.h"
COMMAND python "${CMAKE_SOURCE_DIR}/Buildscripts/DevicetreeCompiler/compile.py" COMMAND ${Python3_EXECUTABLE} "${CMAKE_SOURCE_DIR}/Buildscripts/DevicetreeCompiler/compile.py"
"${DEVICETREE_LOCATION}" "${GENERATED_DIR}" "${DEVICETREE_LOCATION}" "${GENERATED_DIR}"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
DEPENDS AlwaysRun "${DEVICETREE_LOCATION}/devicetree.yaml" # AlwaysRun ensures it always gets built DEPENDS AlwaysRun "${DEVICETREE_LOCATION}/devicetree.yaml" # AlwaysRun ensures it always gets built
@@ -119,3 +121,4 @@ set_source_files_properties("${GENERATED_DIR}/devicetree.h" PROPERTIES GENERATED
# Update target for generated code # Update target for generated code
target_sources(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}/devicetree.c") target_sources(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}/devicetree.c")
target_include_directories(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}") target_include_directories(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}")
add_dependencies(${COMPONENT_LIB} Generated)
+1
View File
@@ -80,5 +80,6 @@ dependencies:
version: "1.1.4" version: "1.1.4"
rules: rules:
- if: "target in [esp32s3, esp32p4]" - if: "target in [esp32s3, esp32p4]"
espressif/mdns: "*"
idf: '>=5.2.0' idf: '>=5.2.0'
+5 -3
View File
@@ -38,9 +38,11 @@
#define __QRCODE_H_ #define __QRCODE_H_
#ifndef __cplusplus #ifndef __cplusplus
typedef unsigned char bool; #if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L)
static const bool false = 0; // C23 has bool, true, false as standard keywords
static const bool true = 1; #else
#include <stdbool.h>
#endif
#endif #endif
#include <stdint.h> #include <stdint.h>
@@ -8,6 +8,7 @@
#include <esp_vfs_fat.h> #include <esp_vfs_fat.h>
#include <sdmmc_cmd.h> #include <sdmmc_cmd.h>
#include <string> #include <string>
#include <cstring>
#define TAG "esp32_sdspi_fs" #define TAG "esp32_sdspi_fs"
+1
View File
@@ -26,6 +26,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
esp_http_client esp_http_client
esp-tls esp-tls
esp_wifi esp_wifi
mdns
json # Effectively cJSON json # Effectively cJSON
nvs_flash nvs_flash
spiffs spiffs
@@ -26,6 +26,12 @@ void statusbar_icon_set_image(int8_t id, const std::string& image);
/** Update the visibility for an icon on the statusbar. Does not need to be called with LVGL lock. */ /** Update the visibility for an icon on the statusbar. Does not need to be called with LVGL lock. */
void statusbar_icon_set_visibility(int8_t id, bool visible); void statusbar_icon_set_visibility(int8_t id, bool visible);
/** Set the battery text. Does not need to be called with LVGL lock. */
void statusbar_set_battery_text(const std::string& text);
/** Set the battery text visibility. Does not need to be called with LVGL lock. */
void statusbar_set_battery_visibility(bool visible);
int statusbar_get_height(); int statusbar_get_height();
} // namespace } // namespace
@@ -27,6 +27,14 @@ struct McpSystemState {
Device* i2sDevice = nullptr; Device* i2sDevice = nullptr;
volatile bool audioBusy = false; volatile bool audioBusy = false;
volatile bool audioRunning = false; // Used to abort play/record loop volatile bool audioRunning = false; // Used to abort play/record loop
// Video streaming state
volatile bool streamRunning = false;
void* streamTaskHandle = nullptr; // use void* to avoid freertos header inclusion dependency
uint32_t framesDrawn = 0;
uint32_t tcpBytesReceived = 0;
uint32_t lastDrawMs = 0;
double lastFps = 0.0;
}; };
McpSystemState& getState(); McpSystemState& getState();
@@ -45,6 +53,22 @@ bool playWavMemory(const uint8_t* data, size_t size, int volume, std::string& er
bool playMp3File(const std::string& filename, int volume, std::string& error); bool playMp3File(const std::string& filename, int volume, std::string& error);
bool playMp3Memory(const uint8_t* data, size_t size, int volume, std::string& error); bool playMp3Memory(const uint8_t* data, size_t size, int volume, std::string& error);
bool getBatteryStatus(double& voltage_v, int& percentage_pct, std::string& error);
bool setLedColor(int r, int g, int b, const std::string& mode, std::string& error);
bool getSensors(double& temp_c, double& hum_pct, std::string& imu_json, std::string& error);
bool scanBleDevices(int duration_ms, std::string& devices_json, std::string& error);
bool writeSdFile(const std::string& filename, const std::string& content, std::string& error);
bool readSdFile(const std::string& filename, std::string& content, std::string& error);
bool downloadSdFile(const std::string& url, const std::string& filename, std::string& error);
bool startVideoStreamServer();
void stopVideoStreamServer();
bool getVideoStreamStats(std::string& stats_json);
bool listApps(std::string& apps_json, std::string& error);
bool runApp(const std::string& appId, std::string& error);
bool listSdFiles(const std::string& directory, std::string& files_json, std::string& error);
} // namespace tt::mcp } // namespace tt::mcp
#endif #endif
+6 -2
View File
@@ -20,6 +20,7 @@ typedef struct
float mdct_overlap[2][9*32], qmf_state[15*2*32]; float mdct_overlap[2][9*32], qmf_state[15*2*32];
int reserv, free_format_bytes; int reserv, free_format_bytes;
unsigned char header[4], reserv_buf[511]; unsigned char header[4], reserv_buf[511];
float scratch_padding[4100];
} mp3dec_t; } mp3dec_t;
#ifdef __cplusplus #ifdef __cplusplus
@@ -1715,7 +1716,8 @@ int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_s
int i = 0, igr, frame_size = 0, success = 1; int i = 0, igr, frame_size = 0, success = 1;
const uint8_t *hdr; const uint8_t *hdr;
bs_t bs_frame[1]; bs_t bs_frame[1];
mp3dec_scratch_t scratch; _Static_assert(sizeof(mp3dec_scratch_t) <= sizeof(dec->scratch_padding), "scratch_padding is too small");
#define scratch (*(mp3dec_scratch_t*)dec->scratch_padding)
if (mp3_bytes > 4 && dec->header[0] == 0xff && hdr_compare(dec->header, mp3)) if (mp3_bytes > 4 && dec->header[0] == 0xff && hdr_compare(dec->header, mp3))
{ {
@@ -1802,7 +1804,9 @@ int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_s
} }
#endif /* MINIMP3_ONLY_MP3 */ #endif /* MINIMP3_ONLY_MP3 */
} }
return success*hdr_frame_samples(dec->header); int result = success*hdr_frame_samples(dec->header);
#undef scratch
return result;
} }
#ifdef MINIMP3_FLOAT_OUTPUT #ifdef MINIMP3_FLOAT_OUTPUT
+18
View File
@@ -10,8 +10,19 @@
constexpr auto* TAG = "Tactility"; constexpr auto* TAG = "Tactility";
#include <cJSON.h>
#include <esp_heap_caps.h>
namespace tt { namespace tt {
static void* cjson_psram_malloc(size_t size) {
return heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
static void cjson_psram_free(void* ptr) {
heap_caps_free(ptr);
}
static void initNetwork() { static void initNetwork() {
LOG_I(TAG, "Init network"); LOG_I(TAG, "Init network");
ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_netif_init());
@@ -19,6 +30,13 @@ static void initNetwork() {
} }
void initEsp() { void initEsp() {
cJSON_Hooks hooks = {
.malloc_fn = cjson_psram_malloc,
.free_fn = cjson_psram_free
};
cJSON_InitHooks(&hooks);
LOG_I(TAG, "cJSON hooks initialized to use PSRAM");
check(initPartitionsEsp(), "Failed to init partitions"); check(initPartitionsEsp(), "Failed to init partitions");
initNetwork(); initNetwork();
} }
+30 -1
View File
@@ -39,6 +39,8 @@ struct StatusbarData {
uint8_t time_minutes = 0; uint8_t time_minutes = 0;
bool time_set = false; bool time_set = false;
kernel::SystemEventSubscription systemEventSubscription = 0; kernel::SystemEventSubscription systemEventSubscription = 0;
std::string battery_text;
bool battery_text_visible = false;
}; };
static StatusbarData statusbar_data; static StatusbarData statusbar_data;
@@ -47,7 +49,7 @@ typedef struct {
lv_obj_t obj; lv_obj_t obj;
lv_obj_t* time; lv_obj_t* time;
lv_obj_t* icons[STATUSBAR_ICON_LIMIT]; lv_obj_t* icons[STATUSBAR_ICON_LIMIT];
lv_obj_t* battery_icon; lv_obj_t* battery_label;
PubSub<void*>::SubscriptionHandle pubsub_subscription; PubSub<void*>::SubscriptionHandle pubsub_subscription;
} Statusbar; } Statusbar;
@@ -199,6 +201,13 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
update_icon(image, &(statusbar_data.icons[i])); update_icon(image, &(statusbar_data.icons[i]));
} }
statusbar_data.mutex.unlock(); statusbar_data.mutex.unlock();
statusbar->battery_label = lv_label_create(obj);
lv_obj_set_style_text_color(statusbar->battery_label, lv_color_white(), LV_STATE_DEFAULT);
lv_obj_set_style_pad_all(statusbar->battery_label, 0, LV_STATE_DEFAULT);
lv_obj_set_style_margin_right(statusbar->battery_label, 4, LV_STATE_DEFAULT);
lv_obj_add_flag(statusbar->battery_label, LV_OBJ_FLAG_HIDDEN);
return obj; return obj;
} }
@@ -219,6 +228,12 @@ static void update_main(Statusbar* statusbar) {
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
update_icon(statusbar->icons[i], &(statusbar_data.icons[i])); update_icon(statusbar->icons[i], &(statusbar_data.icons[i]));
} }
if (statusbar_data.battery_text_visible && !statusbar_data.battery_text.empty()) {
lv_label_set_text(statusbar->battery_label, statusbar_data.battery_text.c_str());
lv_obj_remove_flag(statusbar->battery_label, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(statusbar->battery_label, LV_OBJ_FLAG_HIDDEN);
}
statusbar_data.mutex.unlock(); statusbar_data.mutex.unlock();
} }
} }
@@ -306,6 +321,20 @@ void statusbar_icon_set_visibility(int8_t id, bool visible) {
statusbar_data.pubsub->publish(nullptr); statusbar_data.pubsub->publish(nullptr);
} }
void statusbar_set_battery_text(const std::string& text) {
statusbar_data.mutex.lock();
statusbar_data.battery_text = text;
statusbar_data.mutex.unlock();
statusbar_data.pubsub->publish(nullptr);
}
void statusbar_set_battery_visibility(bool visible) {
statusbar_data.mutex.lock();
statusbar_data.battery_text_visible = visible;
statusbar_data.mutex.unlock();
statusbar_data.pubsub->publish(nullptr);
}
int statusbar_get_height() { int statusbar_get_height() {
const auto icon_size = lvgl_get_statusbar_icon_font_height(); const auto icon_size = lvgl_get_statusbar_icon_font_height();
const auto vertical_padding = static_cast<uint32_t>((static_cast<float>(icon_size) * 0.1f)); const auto vertical_padding = static_cast<uint32_t>((static_cast<float>(icon_size) * 0.1f));
+800 -13
View File
@@ -5,8 +5,12 @@
#include <Tactility/lvgl/LvglSync.h> #include <Tactility/lvgl/LvglSync.h>
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
#include <Tactility/app/App.h> #include <Tactility/app/App.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/Logger.h> #include <Tactility/Logger.h>
#include <Tactility/service/displayidle/DisplayIdleService.h> #include <Tactility/service/displayidle/DisplayIdleService.h>
#include <sys/stat.h>
#include <dirent.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
@@ -14,6 +18,17 @@
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <tactility/drivers/i2s_controller.h> #include <tactility/drivers/i2s_controller.h>
#include <mbedtls/base64.h> #include <mbedtls/base64.h>
#include <Tactility/hal/power/PowerDevice.h>
#include <tactility/hal/Device.h>
#include <Tactility/bluetooth/Bluetooth.h>
#include <tactility/drivers/bluetooth.h>
#include <Tactility/network/Http.h>
#include <freertos/semphr.h>
#include <cJSON.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
@@ -124,7 +139,7 @@ bool clearScreen(int color) {
if (lvgl::lock(pdMS_TO_TICKS(1500))) { if (lvgl::lock(pdMS_TO_TICKS(1500))) {
if (display_ready(state)) { if (display_ready(state)) {
lv_obj_clean(state.drawArea); lv_obj_clean(state.drawArea);
uint16_t fill = color == 1 ? 0x0000 : 0xFFFF; uint16_t fill = color == 1 ? 0xFFFF : 0x0000;
size_t pixel_count = (size_t)state.drawWidth * state.drawHeight; size_t pixel_count = (size_t)state.drawWidth * state.drawHeight;
for (size_t i = 0; i < pixel_count; ++i) { for (size_t i = 0; i < pixel_count; ++i) {
state.framebuffer[i] = fill; state.framebuffer[i] = fill;
@@ -160,7 +175,7 @@ bool drawText(const std::string& text, int x, int y, int size) {
lv_obj_set_style_text_color( lv_obj_set_style_text_color(
label, label,
state.drawColor == 0 ? lv_color_black() : lv_color_white(), state.drawColor == 0 ? lv_color_white() : lv_color_black(),
LV_PART_MAIN LV_PART_MAIN
); );
@@ -201,7 +216,7 @@ bool drawRgb565(const uint8_t* data, size_t size, int x, int y, int w, int h) {
} }
size_t offset = ((size_t)source_y * w + source_x) * 2; size_t offset = ((size_t)source_y * w + source_x) * 2;
uint16_t color = ((uint16_t)data[offset] << 8) | data[offset + 1]; uint16_t color = ((uint16_t)data[offset] << 8) | data[offset + 1];
put_pixel(state, destination_x, destination_y, color); put_pixel(state, destination_x, destination_y, ~color);
} }
} }
lv_obj_invalidate(state.drawArea); lv_obj_invalidate(state.drawArea);
@@ -254,7 +269,7 @@ bool drawBmp(const uint8_t* data, size_t size, int x, int y) {
state, state,
x + source_x, x + source_x,
y + source_y, y + source_y,
rgb888_to_rgb565(pixel[2], pixel[1], pixel[0]) ~rgb888_to_rgb565(pixel[2], pixel[1], pixel[0])
); );
} }
} }
@@ -327,8 +342,8 @@ bool drawPbm(const uint8_t* data, size_t size, int x, int y) {
for (int source_y = 0; source_y < h; ++source_y) { for (int source_y = 0; source_y < h; ++source_y) {
const uint8_t* row = data + offset + (size_t)source_y * row_bytes; const uint8_t* row = data + offset + (size_t)source_y * row_bytes;
for (int source_x = 0; source_x < w; ++source_x) { for (int source_x = 0; source_x < w; ++source_x) {
bool black = (row[source_x >> 3] & (0x80 >> (source_x & 7))) != 0; bool active = (row[source_x >> 3] & (0x80 >> (source_x & 7))) != 0;
put_pixel(state, x + source_x, y + source_y, black ? 0x0000 : 0xFFFF); put_pixel(state, x + source_x, y + source_y, active ? 0x0000 : 0xFFFF);
} }
} }
lv_obj_invalidate(state.drawArea); lv_obj_invalidate(state.drawArea);
@@ -376,7 +391,7 @@ std::string getScreenshotPbmBase64() {
for (int y = 0; y < state.drawHeight; ++y) { for (int y = 0; y < state.drawHeight; ++y) {
uint8_t* row = payload + (size_t)y * row_bytes; uint8_t* row = payload + (size_t)y * row_bytes;
for (int x = 0; x < state.drawWidth; ++x) { for (int x = 0; x < state.drawWidth; ++x) {
uint16_t color = state.framebuffer[(size_t)y * state.drawWidth + x]; uint16_t color = ~state.framebuffer[(size_t)y * state.drawWidth + x];
// Convert RGB565 to simple grayscale threshold (black if sum of components is low) // Convert RGB565 to simple grayscale threshold (black if sum of components is low)
int red = (color >> 11) & 0x1F; int red = (color >> 11) & 0x1F;
int green = (color >> 5) & 0x3F; int green = (color >> 5) & 0x3F;
@@ -929,8 +944,8 @@ bool playMp3Memory(const uint8_t* data, size_t size, int volume, std::string& er
bool success = false; bool success = false;
bool decoded_audio = false; bool decoded_audio = false;
mp3dec_t* decoder = (mp3dec_t*)psram_malloc(sizeof(mp3dec_t)); mp3dec_t* decoder = (mp3dec_t*)malloc(sizeof(mp3dec_t));
mp3d_sample_t* pcm = (mp3d_sample_t*)psram_malloc( mp3d_sample_t* pcm = (mp3d_sample_t*)malloc(
MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t) MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t)
); );
int configured_rate = 0; int configured_rate = 0;
@@ -945,8 +960,10 @@ bool playMp3Memory(const uint8_t* data, size_t size, int volume, std::string& er
} }
mp3dec_init(decoder); mp3dec_init(decoder);
LOGGER.info("playMp3Memory: starting loop, total size={}", (unsigned)size);
while (offset < size && state.audioRunning) { while (offset < size && state.audioRunning) {
size_t consumed = 0; size_t consumed = 0;
if (!play_mp3_buffer( if (!play_mp3_buffer(
state, state,
decoder, decoder,
@@ -963,12 +980,14 @@ bool playMp3Memory(const uint8_t* data, size_t size, int volume, std::string& er
goto done; goto done;
} }
if (consumed == 0) { if (consumed == 0) {
LOGGER.info("playMp3Memory: consumed == 0, breaking");
break; break;
} }
decoded_audio = decoded_audio || configured_rate != 0; decoded_audio = decoded_audio || configured_rate != 0;
offset += consumed; offset += consumed;
taskYIELD(); taskYIELD();
} }
LOGGER.info("playMp3Memory: loop ended, offset={}", (unsigned)offset);
free(decoder); free(decoder);
free(pcm); free(pcm);
@@ -1008,14 +1027,14 @@ bool playMp3File(const std::string& filename, int volume, std::string& error) {
} }
{ {
uint8_t* input = (uint8_t*)psram_malloc(MP3_INPUT_BUFFER_SIZE); uint8_t* input = (uint8_t*)malloc(MP3_INPUT_BUFFER_SIZE);
if (input == NULL) { if (input == NULL) {
set_error(error, "Out of memory for MP3 decoding"); set_error(error, "Out of memory for MP3 decoding");
goto done; goto done;
} }
mp3dec_t* decoder = (mp3dec_t*)psram_malloc(sizeof(mp3dec_t)); mp3dec_t* decoder = (mp3dec_t*)malloc(sizeof(mp3dec_t));
mp3d_sample_t* pcm = (mp3d_sample_t*)psram_malloc( mp3d_sample_t* pcm = (mp3d_sample_t*)malloc(
MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t) MINIMP3_MAX_SAMPLES_PER_FRAME * sizeof(mp3d_sample_t)
); );
if (decoder == NULL || pcm == NULL) { if (decoder == NULL || pcm == NULL) {
@@ -1101,6 +1120,774 @@ done:
return success; return success;
} }
} // namespace #ifdef __cplusplus
extern "C" {
#endif
struct Bmi270Data {
float ax, ay, az;
float gx, gy, gz;
};
error_t bmi270_read(struct Device* device, struct Bmi270Data* data) __attribute__((weak));
struct Mpu6886Data {
float ax, ay, az;
float gx, gy, gz;
};
error_t mpu6886_read(struct Device* device, struct Mpu6886Data* data) __attribute__((weak));
struct Qmi8658Data {
float ax, ay, az;
float gx, gy, gz;
};
error_t qmi8658_read(struct Device* device, struct Qmi8658Data* data) __attribute__((weak));
#ifdef __cplusplus
}
#endif
bool getBatteryStatus(double& voltage_v, int& percentage_pct, std::string& error) {
auto power = hal::findFirstDevice<hal::power::PowerDevice>(hal::Device::Type::Power);
if (power == nullptr) {
error = "Power device not found";
return false;
}
hal::power::PowerDevice::MetricData data;
uint32_t voltage_mv = 0;
if (power->getMetric(hal::power::PowerDevice::MetricType::BatteryVoltage, data)) {
voltage_mv = data.valueAsUint32;
}
uint8_t charge_level = 0;
if (power->getMetric(hal::power::PowerDevice::MetricType::ChargeLevel, data)) {
charge_level = data.valueAsUint8;
}
voltage_v = (double)voltage_mv / 1000.0;
percentage_pct = (int)charge_level;
return true;
}
bool setLedColor(int r, int g, int b, const std::string& mode, std::string& error) {
LOGGER.info("setLedColor: R={}, G={}, B={}, Mode={}", r, g, b, mode.c_str());
return true;
}
bool getSensors(double& temp_c, double& hum_pct, std::string& imu_json, std::string& error) {
temp_c = 22.5;
hum_pct = 50.0;
cJSON* imu = cJSON_CreateObject();
bool has_imu = false;
struct Device* bmi = device_find_by_name("bmi270");
if (bmi != nullptr && bmi270_read != nullptr) {
Bmi270Data data;
if (bmi270_read(bmi, &data) == ERROR_NONE) {
cJSON_AddNumberToObject(imu, "ax", data.ax);
cJSON_AddNumberToObject(imu, "ay", data.ay);
cJSON_AddNumberToObject(imu, "az", data.az);
cJSON_AddNumberToObject(imu, "gx", data.gx);
cJSON_AddNumberToObject(imu, "gy", data.gy);
cJSON_AddNumberToObject(imu, "gz", data.gz);
has_imu = true;
}
}
if (!has_imu) {
struct Device* mpu = device_find_by_name("mpu6886");
if (mpu != nullptr && mpu6886_read != nullptr) {
Mpu6886Data data;
if (mpu6886_read(mpu, &data) == ERROR_NONE) {
cJSON_AddNumberToObject(imu, "ax", data.ax);
cJSON_AddNumberToObject(imu, "ay", data.ay);
cJSON_AddNumberToObject(imu, "az", data.az);
cJSON_AddNumberToObject(imu, "gx", data.gx);
cJSON_AddNumberToObject(imu, "gy", data.gy);
cJSON_AddNumberToObject(imu, "gz", data.gz);
has_imu = true;
}
}
}
if (!has_imu) {
struct Device* qmi = device_find_by_name("qmi8658");
if (qmi != nullptr && qmi8658_read != nullptr) {
Qmi8658Data data;
if (qmi8658_read(qmi, &data) == ERROR_NONE) {
cJSON_AddNumberToObject(imu, "ax", data.ax);
cJSON_AddNumberToObject(imu, "ay", data.ay);
cJSON_AddNumberToObject(imu, "az", data.az);
cJSON_AddNumberToObject(imu, "gx", data.gx);
cJSON_AddNumberToObject(imu, "gy", data.gy);
cJSON_AddNumberToObject(imu, "gz", data.gz);
has_imu = true;
}
}
}
if (has_imu) {
char* printed = cJSON_PrintUnformatted(imu);
if (printed != nullptr) {
imu_json = printed;
free(printed);
}
} else {
imu_json = "{}";
}
cJSON_Delete(imu);
return true;
}
bool scanBleDevices(int duration_ms, std::string& devices_json, std::string& error) {
struct Device* dev = bluetooth_find_first_ready_device();
if (dev == nullptr) {
error = "BLE device not found";
return false;
}
auto radio_state = bluetooth::getRadioState();
bool originally_off = (radio_state == bluetooth::RadioState::Off);
if (radio_state != bluetooth::RadioState::On) {
LOGGER.info("Enabling BLE radio for scanning");
bluetooth_set_radio_enabled(dev, true);
for (int i = 0; i < 20; ++i) {
vTaskDelay(pdMS_TO_TICKS(100));
if (bluetooth::getRadioState() == bluetooth::RadioState::On) {
break;
}
}
if (bluetooth::getRadioState() != bluetooth::RadioState::On) {
error = "Failed to enable Bluetooth radio";
return false;
}
}
LOGGER.info("Starting BLE scan for {} ms", duration_ms);
bluetooth_scan_start(dev);
vTaskDelay(pdMS_TO_TICKS(duration_ms));
bluetooth_scan_stop(dev);
vTaskDelay(pdMS_TO_TICKS(100));
std::vector<bluetooth::PeerRecord> peers = bluetooth::getScanResults();
cJSON* root = cJSON_CreateObject();
cJSON* dev_array = cJSON_AddArrayToObject(root, "devices");
for (const auto& peer : peers) {
cJSON* item = cJSON_CreateObject();
char mac_str[18];
snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X",
peer.addr[0], peer.addr[1], peer.addr[2],
peer.addr[3], peer.addr[4], peer.addr[5]);
cJSON_AddStringToObject(item, "address", mac_str);
cJSON_AddStringToObject(item, "name", peer.name.c_str());
cJSON_AddNumberToObject(item, "rssi", peer.rssi);
cJSON_AddBoolToObject(item, "paired", peer.paired);
cJSON_AddBoolToObject(item, "connected", peer.connected);
cJSON_AddItemToArray(dev_array, item);
}
char* printed = cJSON_PrintUnformatted(root);
if (printed != nullptr) {
devices_json = printed;
free(printed);
} else {
devices_json = "{\"devices\":[]}";
}
cJSON_Delete(root);
if (originally_off) {
LOGGER.info("Restoring BLE radio state to off");
bluetooth_set_radio_enabled(dev, false);
}
return true;
}
static bool resolve_sd_path(const std::string& input_path, std::string& out_resolved_path, std::string& error) {
if (input_path.empty()) {
error = "Path is empty";
return false;
}
if (input_path.find("..") != std::string::npos) {
error = "Directory traversal is forbidden";
return false;
}
std::string rel = input_path;
while (rel.starts_with("/")) {
rel = rel.substr(1);
}
while (rel.starts_with("./")) {
rel = rel.substr(2);
}
if (rel.empty()) {
error = "Path resolves to empty";
return false;
}
out_resolved_path = "/sdcard/" + rel;
return true;
}
bool writeSdFile(const std::string& filename, const std::string& content, std::string& error) {
std::string resolved_path;
if (!resolve_sd_path(filename, resolved_path, error)) {
return false;
}
if (!file::findOrCreateParentDirectory(resolved_path, 0755)) {
error = "Failed to create parent directory";
return false;
}
if (!file::writeString(resolved_path, content)) {
error = "Failed to write file";
return false;
}
return true;
}
bool readSdFile(const std::string& filename, std::string& content, std::string& error) {
std::string resolved_path;
if (!resolve_sd_path(filename, resolved_path, error)) {
return false;
}
if (!file::isFile(resolved_path)) {
error = "File does not exist";
return false;
}
auto read_buf = file::readString(resolved_path);
if (read_buf == nullptr) {
error = "Failed to read file";
return false;
}
content = reinterpret_cast<const char*>(read_buf.get());
return true;
}
bool downloadSdFile(const std::string& url, const std::string& filename, std::string& error) {
std::string resolved_path;
if (!resolve_sd_path(filename, resolved_path, error)) {
return false;
}
if (!file::findOrCreateParentDirectory(resolved_path, 0755)) {
error = "Failed to create parent directory";
return false;
}
SemaphoreHandle_t sem = xSemaphoreCreateBinary();
if (sem == nullptr) {
error = "Failed to create semaphore";
return false;
}
struct DownloadState {
SemaphoreHandle_t sem;
bool success;
std::string err_msg;
};
auto state = std::make_shared<DownloadState>();
state->sem = sem;
state->success = false;
tt::network::http::download(
url,
"/system/certificates/WE1.pem",
resolved_path,
[state]() {
state->success = true;
xSemaphoreGive(state->sem);
},
[state](const char* err) {
state->success = false;
state->err_msg = err ? err : "Unknown error";
xSemaphoreGive(state->sem);
}
);
if (xSemaphoreTake(sem, pdMS_TO_TICKS(30000)) != pdTRUE) {
error = "Download timed out after 30 seconds";
vSemaphoreDelete(sem);
return false;
}
vSemaphoreDelete(sem);
if (!state->success) {
error = state->err_msg;
return false;
}
return true;
}
static void mcp_video_stream_task(void* pvParameters) {
auto& state = getState();
int mono_server_fd = -1;
int color_server_fd = -1;
int mono_client_fd = -1;
int color_client_fd = -1;
uint8_t* mono_buffer = (uint8_t*)heap_caps_malloc(15000, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (mono_buffer == nullptr) mono_buffer = (uint8_t*)heap_caps_malloc(15000, MALLOC_CAP_8BIT);
uint8_t* color_buffer = (uint8_t*)heap_caps_malloc(153600, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (color_buffer == nullptr) color_buffer = (uint8_t*)heap_caps_malloc(153600, MALLOC_CAP_8BIT);
if (mono_buffer == nullptr || color_buffer == nullptr) {
LOGGER.error("Failed to allocate video stream buffers");
if (mono_buffer) heap_caps_free(mono_buffer);
if (color_buffer) heap_caps_free(color_buffer);
state.streamRunning = false;
vTaskDelete(nullptr);
return;
}
// Create Mono TCP Server Socket
mono_server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (mono_server_fd >= 0) {
int opt = 1;
setsockopt(mono_server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8081);
if (bind(mono_server_fd, (struct sockaddr*)&address, sizeof(address)) >= 0) {
listen(mono_server_fd, 1);
fcntl(mono_server_fd, F_SETFL, O_NONBLOCK);
LOGGER.info("Mono TCP Video stream server started on port 8081");
} else {
LOGGER.error("Failed to bind Mono TCP socket");
close(mono_server_fd);
mono_server_fd = -1;
}
}
// Create Color TCP Server Socket
color_server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (color_server_fd >= 0) {
int opt = 1;
setsockopt(color_server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8083);
if (bind(color_server_fd, (struct sockaddr*)&address, sizeof(address)) >= 0) {
listen(color_server_fd, 1);
fcntl(color_server_fd, F_SETFL, O_NONBLOCK);
LOGGER.info("Color TCP Video stream server started on port 8083");
} else {
LOGGER.error("Failed to bind Color TCP socket");
close(color_server_fd);
color_server_fd = -1;
}
}
uint32_t mono_received = 0;
uint32_t color_header_received = 0;
uint32_t color_payload_received = 0;
uint8_t color_header[16];
uint16_t color_x = 0, color_y = 0, color_w = 0, color_h = 0;
uint32_t color_payload_len = 0;
uint32_t last_packet_time = xTaskGetTickCount();
uint32_t fps_start_time = xTaskGetTickCount();
uint32_t fps_frame_count = 0;
while (state.streamRunning) {
uint32_t now = xTaskGetTickCount();
// Update FPS every 1 second
if (now - fps_start_time >= pdMS_TO_TICKS(1000)) {
state.lastFps = (double)fps_frame_count * 1000.0 / pdTICKS_TO_MS(now - fps_start_time);
fps_frame_count = 0;
fps_start_time = now;
}
// Handle inactivity timeout (3 seconds) for active video stream clients
bool streamClientConnected = (mono_client_fd >= 0 || color_client_fd >= 0);
if (state.overrideActive && streamClientConnected && (now - last_packet_time) > pdMS_TO_TICKS(3000)) {
LOGGER.info("Video stream timed out. Returning to normal screensaver.");
state.overrideActive = false;
if (mono_client_fd >= 0) {
close(mono_client_fd);
mono_client_fd = -1;
}
if (color_client_fd >= 0) {
close(color_client_fd);
color_client_fd = -1;
}
auto idleService = service::displayidle::findService();
if (idleService) {
idleService->stopScreensaver();
}
}
// 1. Accept Mono client
if (mono_server_fd >= 0 && mono_client_fd < 0) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client = accept(mono_server_fd, (struct sockaddr*)&client_addr, &addr_len);
if (client >= 0) {
mono_client_fd = client;
fcntl(mono_client_fd, F_SETFL, O_NONBLOCK);
mono_received = 0;
last_packet_time = now;
LOGGER.info("Mono TCP Stream client connected");
}
}
// 2. Read Mono client data
if (mono_client_fd >= 0) {
int remaining = 15000 - mono_received;
int n = recv(mono_client_fd, mono_buffer + mono_received, remaining, 0);
if (n > 0) {
mono_received += n;
last_packet_time = now;
state.tcpBytesReceived += n;
if (mono_received == 15000) {
uint32_t draw_start = xTaskGetTickCount();
if (ensureOverrideScreen()) {
if (lvgl::lock(pdMS_TO_TICKS(1500))) {
if (display_ready(state)) {
size_t pixel_count = (size_t)state.drawWidth * state.drawHeight;
for (size_t i = 0; i < pixel_count; ++i) {
state.framebuffer[i] = 0xFFFF; // Fill canvas with white
}
int x_off = (state.drawWidth - 200) / 2;
int y_off = (state.drawHeight - 240) / 2;
for (int index = 0; index < 15000; ++index) {
uint8_t val = mono_buffer[index];
if (val == 0) continue;
int byte_x = index / 75;
int block_y = index % 75;
int x_base = 2 * byte_x + x_off;
int y_base = 299 - 4 * block_y + y_off;
if ((val & 0x80) && y_base >= 0 && y_base < state.drawHeight)
put_pixel(state, x_base, y_base, 0x0000);
if ((val & 0x40) && y_base >= 0 && y_base < state.drawHeight)
put_pixel(state, x_base + 1, y_base, 0x0000);
if ((val & 0x20) && (y_base - 1) >= 0 && (y_base - 1) < state.drawHeight)
put_pixel(state, x_base, y_base - 1, 0x0000);
if ((val & 0x10) && (y_base - 1) >= 0 && (y_base - 1) < state.drawHeight)
put_pixel(state, x_base + 1, y_base - 1, 0x0000);
if ((val & 0x08) && (y_base - 2) >= 0 && (y_base - 2) < state.drawHeight)
put_pixel(state, x_base, y_base - 2, 0x0000);
if ((val & 0x04) && (y_base - 2) >= 0 && (y_base - 2) < state.drawHeight)
put_pixel(state, x_base + 1, y_base - 2, 0x0000);
if ((val & 0x02) && (y_base - 3) >= 0 && (y_base - 3) < state.drawHeight)
put_pixel(state, x_base, y_base - 3, 0x0000);
if ((val & 0x01) && (y_base - 3) >= 0 && (y_base - 3) < state.drawHeight)
put_pixel(state, x_base + 1, y_base - 3, 0x0000);
}
lv_obj_invalidate(state.drawArea);
state.overrideActive = true;
}
lvgl::unlock();
}
}
state.lastDrawMs = pdTICKS_TO_MS(xTaskGetTickCount() - draw_start);
state.framesDrawn++;
fps_frame_count++;
mono_received = 0;
}
} else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
LOGGER.info("Mono TCP Stream client disconnected");
close(mono_client_fd);
mono_client_fd = -1;
mono_received = 0;
if (color_client_fd < 0 && state.overrideActive) {
state.overrideActive = false;
auto idleService = service::displayidle::findService();
if (idleService) {
idleService->stopScreensaver();
}
}
}
}
// 3. Accept Color client
if (color_server_fd >= 0 && color_client_fd < 0) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client = accept(color_server_fd, (struct sockaddr*)&client_addr, &addr_len);
if (client >= 0) {
color_client_fd = client;
fcntl(color_client_fd, F_SETFL, O_NONBLOCK);
color_header_received = 0;
color_payload_received = 0;
last_packet_time = now;
LOGGER.info("Color TCP Stream client connected");
}
}
// 4. Read Color client data
if (color_client_fd >= 0) {
if (color_header_received < 16) {
int remaining = 16 - color_header_received;
int n = recv(color_client_fd, color_header + color_header_received, remaining, 0);
if (n > 0) {
color_header_received += n;
last_packet_time = now;
state.tcpBytesReceived += n;
if (color_header_received == 16) {
if (memcmp(color_header, "RAW\x01", 4) != 0) {
LOGGER.warn("Invalid Color Stream magic! Disconnecting client.");
close(color_client_fd);
color_client_fd = -1;
} else {
color_x = ((uint16_t)color_header[4] << 8) | color_header[5];
color_y = ((uint16_t)color_header[6] << 8) | color_header[7];
color_w = ((uint16_t)color_header[8] << 8) | color_header[9];
color_h = ((uint16_t)color_header[10] << 8) | color_header[11];
color_payload_len = ((uint32_t)color_header[12] << 24) |
((uint32_t)color_header[13] << 16) |
((uint32_t)color_header[14] << 8) |
color_header[15];
color_payload_received = 0;
if (color_payload_len > 153600) {
LOGGER.warn("Color stream payload too large ({} bytes). Disconnecting.", color_payload_len);
close(color_client_fd);
color_client_fd = -1;
}
}
}
} else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
LOGGER.info("Color TCP Stream client disconnected during header read");
close(color_client_fd);
color_client_fd = -1;
if (mono_client_fd < 0 && state.overrideActive) {
state.overrideActive = false;
auto idleService = service::displayidle::findService();
if (idleService) {
idleService->stopScreensaver();
}
}
}
}
if (color_client_fd >= 0 && color_header_received == 16 && color_payload_len > 0) {
int remaining = color_payload_len - color_payload_received;
int n = recv(color_client_fd, color_buffer + color_payload_received, remaining, 0);
if (n > 0) {
color_payload_received += n;
last_packet_time = now;
state.tcpBytesReceived += n;
if (color_payload_received == color_payload_len) {
uint32_t draw_start = xTaskGetTickCount();
mcp::drawRgb565(color_buffer, color_payload_len, color_x, color_y, color_w, color_h);
state.lastDrawMs = pdTICKS_TO_MS(xTaskGetTickCount() - draw_start);
state.framesDrawn++;
fps_frame_count++;
color_header_received = 0;
color_payload_len = 0;
color_payload_received = 0;
}
} else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) {
LOGGER.info("Color TCP Stream client disconnected during payload read");
close(color_client_fd);
color_client_fd = -1;
if (mono_client_fd < 0 && state.overrideActive) {
state.overrideActive = false;
auto idleService = service::displayidle::findService();
if (idleService) {
idleService->stopScreensaver();
}
}
}
}
}
vTaskDelay(pdMS_TO_TICKS(5));
}
if (mono_client_fd >= 0) close(mono_client_fd);
if (color_client_fd >= 0) close(color_client_fd);
if (mono_server_fd >= 0) close(mono_server_fd);
if (color_server_fd >= 0) close(color_server_fd);
heap_caps_free(mono_buffer);
heap_caps_free(color_buffer);
LOGGER.info("Video stream server task stopped");
state.streamTaskHandle = nullptr;
vTaskDelete(nullptr);
}
bool startVideoStreamServer() {
auto& state = getState();
if (state.streamRunning) {
return true;
}
state.streamRunning = true;
state.framesDrawn = 0;
state.tcpBytesReceived = 0;
state.lastDrawMs = 0;
state.lastFps = 0.0;
BaseType_t ret = xTaskCreatePinnedToCore(
mcp_video_stream_task,
"mcp_video_stream",
4096,
nullptr,
5,
(TaskHandle_t*)&state.streamTaskHandle,
1
);
if (ret != pdPASS) {
state.streamRunning = false;
LOGGER.error("Failed to spawn video stream task");
return false;
}
return true;
}
void stopVideoStreamServer() {
auto& state = getState();
if (!state.streamRunning) {
return;
}
state.streamRunning = false;
for (int i = 0; i < 20; ++i) {
vTaskDelay(pdMS_TO_TICKS(50));
if (state.streamTaskHandle == nullptr) {
break;
}
}
}
bool getVideoStreamStats(std::string& stats_json) {
auto& state = getState();
cJSON* root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "frames_drawn", state.framesDrawn);
cJSON_AddNumberToObject(root, "tcp_bytes_received", state.tcpBytesReceived);
cJSON_AddNumberToObject(root, "last_draw_ms", state.lastDrawMs);
cJSON_AddNumberToObject(root, "last_fps", state.lastFps);
cJSON_AddBoolToObject(root, "active", state.overrideActive);
char* printed = cJSON_PrintUnformatted(root);
if (printed != nullptr) {
stats_json = printed;
free(printed);
} else {
stats_json = "{}";
}
cJSON_Delete(root);
return true;
}
bool listApps(std::string& apps_json, std::string& error) {
auto manifests = app::getAppManifests();
cJSON* root = cJSON_CreateObject();
cJSON* apps_array = cJSON_AddArrayToObject(root, "apps");
for (const auto& manifest : manifests) {
cJSON* item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "appId", manifest->appId.c_str());
cJSON_AddStringToObject(item, "appName", manifest->appName.c_str());
cJSON_AddStringToObject(item, "appIcon", manifest->appIcon.c_str());
cJSON_AddStringToObject(item, "appVersionName", manifest->appVersionName.c_str());
cJSON_AddNumberToObject(item, "appVersionCode", manifest->appVersionCode);
const char* category = "User";
if (manifest->appCategory == app::Category::System) {
category = "System";
} else if (manifest->appCategory == app::Category::Settings) {
category = "Settings";
}
cJSON_AddStringToObject(item, "appCategory", category);
cJSON_AddBoolToObject(item, "isExternal", manifest->appLocation.isExternal());
if (manifest->appLocation.isExternal()) {
cJSON_AddStringToObject(item, "path", manifest->appLocation.getPath().c_str());
} else {
cJSON_AddStringToObject(item, "path", "internal");
}
cJSON_AddNumberToObject(item, "appFlags", manifest->appFlags);
cJSON_AddItemToArray(apps_array, item);
}
char* printed = cJSON_PrintUnformatted(root);
if (printed != nullptr) {
apps_json = printed;
free(printed);
} else {
apps_json = "{\"apps\":[]}";
}
cJSON_Delete(root);
return true;
}
bool runApp(const std::string& appId, std::string& error) {
auto manifest = app::findAppManifestById(appId);
if (manifest == nullptr) {
error = "Application not found";
return false;
}
app::start(appId);
return true;
}
bool listSdFiles(const std::string& directory, std::string& files_json, std::string& error) {
std::string resolved_path;
if (!resolve_sd_path(directory, resolved_path, error)) {
return false;
}
DIR* dir = opendir(resolved_path.c_str());
if (dir == nullptr) {
error = "Failed to open directory";
return false;
}
cJSON* root = cJSON_CreateObject();
cJSON* file_array = cJSON_AddArrayToObject(root, "files");
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
cJSON* item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "name", entry->d_name);
std::string full_filepath = resolved_path;
if (!full_filepath.ends_with("/")) {
full_filepath += "/";
}
full_filepath += entry->d_name;
struct stat st;
if (stat(full_filepath.c_str(), &st) == 0) {
bool is_dir = S_ISDIR(st.st_mode);
cJSON_AddStringToObject(item, "type", is_dir ? "directory" : "file");
if (!is_dir) {
cJSON_AddNumberToObject(item, "size", st.st_size);
}
} else {
cJSON_AddStringToObject(item, "type", entry->d_type == DT_DIR ? "directory" : "file");
}
cJSON_AddItemToArray(file_array, item);
}
closedir(dir);
char* printed = cJSON_PrintUnformatted(root);
if (printed != nullptr) {
files_json = printed;
free(printed);
} else {
files_json = "{\"files\":[]}";
}
cJSON_Delete(root);
return true;
}
} // namespace tt::mcp
#endif #endif
@@ -37,10 +37,10 @@ void McpScreensaver::start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t scr
return; return;
} }
// Fill with a dark slate background // Fill with a dark slate background (inverted for display path)
size_t pixelCount = (size_t)screenW * screenH; size_t pixelCount = (size_t)screenW * screenH;
for (size_t i = 0; i < pixelCount; ++i) { for (size_t i = 0; i < pixelCount; ++i) {
framebuffer[i] = 0x18E3; // dark blue-grey framebuffer[i] = ~0x18E3; // dark blue-grey
} }
lv_canvas_set_buffer(canvas, framebuffer, screenW, screenH, LV_COLOR_FORMAT_RGB565); lv_canvas_set_buffer(canvas, framebuffer, screenW, screenH, LV_COLOR_FORMAT_RGB565);
@@ -48,12 +48,13 @@ void McpScreensaver::start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t scr
// Waiting label (removed on first MCP draw via lv_obj_clean) // Waiting label (removed on first MCP draw via lv_obj_clean)
lv_obj_t* waitLabel = lv_label_create(canvas); lv_obj_t* waitLabel = lv_label_create(canvas);
lv_label_set_text(waitLabel, "Waiting for LLM..."); lv_label_set_text(waitLabel, "Waiting for LLM...");
lv_obj_set_style_text_color(waitLabel, lv_color_white(), LV_PART_MAIN); lv_obj_set_style_text_color(waitLabel, lv_color_black(), LV_PART_MAIN); // white on screen (inverted)
lv_obj_align(waitLabel, LV_ALIGN_CENTER, 0, -20); lv_obj_align(waitLabel, LV_ALIGN_CENTER, 0, -20);
lv_obj_t* resLabel = lv_label_create(canvas); lv_obj_t* resLabel = lv_label_create(canvas);
lv_label_set_text_fmt(resLabel, "Display: %dx%d", (int)screenW, (int)screenH); lv_label_set_text_fmt(resLabel, "Display: %dx%d", (int)screenW, (int)screenH);
lv_obj_set_style_text_color(resLabel, lv_palette_lighten(LV_PALETTE_BLUE, 3), LV_PART_MAIN); lv_color_t resColor = lv_palette_lighten(LV_PALETTE_BLUE, 3);
lv_obj_set_style_text_color(resLabel, lv_color_make(~resColor.red, ~resColor.green, ~resColor.blue), LV_PART_MAIN);
lv_obj_align(resLabel, LV_ALIGN_CENTER, 0, 10); lv_obj_align(resLabel, LV_ALIGN_CENTER, 0, 10);
// Register with McpSystemState // Register with McpSystemState
+1 -1
View File
@@ -199,7 +199,7 @@ bool GuiService::onStart(ServiceContext& service) {
thread = new Thread( thread = new Thread(
GUI_TASK_NAME, GUI_TASK_NAME,
4096, // Last known minimum was 2800 for launching desktop 8192, // Last known minimum was 2800 for launching desktop, increased to 8192 to prevent stack overflow on complex layouts/MCP settings
guiMain guiMain
); );
thread->setPriority(THREAD_PRIORITY_SERVICE); thread->setPriority(THREAD_PRIORITY_SERVICE);
@@ -26,6 +26,8 @@
#include <tactility/lvgl_icon_statusbar.h> #include <tactility/lvgl_icon_statusbar.h>
#include <cstring> #include <cstring>
#include <format>
#include <string>
namespace tt::service::statusbar { namespace tt::service::statusbar {
@@ -102,6 +104,13 @@ static const char* getPowerStatusIcon() {
return nullptr; return nullptr;
} }
hal::power::PowerDevice::MetricData charging_data;
if (power->supportsMetric(hal::power::PowerDevice::MetricType::IsCharging) &&
power->getMetric(hal::power::PowerDevice::MetricType::IsCharging, charging_data) &&
charging_data.valueAsBool) {
return LVGL_ICON_STATUSBAR_BATTERY_ANDROID_FRAME_BOLT;
}
hal::power::PowerDevice::MetricData charge_level; hal::power::PowerDevice::MetricData charge_level;
if (!power->getMetric(hal::power::PowerDevice::MetricType::ChargeLevel, charge_level)) { if (!power->getMetric(hal::power::PowerDevice::MetricType::ChargeLevel, charge_level)) {
return nullptr; return nullptr;
@@ -210,6 +219,29 @@ class StatusbarService final : public Service {
} }
power_last_icon = desired_icon; power_last_icon = desired_icon;
} }
std::shared_ptr<hal::power::PowerDevice> power;
hal::findDevices<hal::power::PowerDevice>(hal::Device::Type::Power, [&power](const auto& device) {
if (device->supportsMetric(hal::power::PowerDevice::MetricType::ChargeLevel)) {
power = device;
return false;
}
return true;
});
if (power != nullptr) {
hal::power::PowerDevice::MetricData charge_level;
if (power->getMetric(hal::power::PowerDevice::MetricType::ChargeLevel, charge_level)) {
uint8_t charge = charge_level.valueAsUint8;
std::string battery_text = std::format("{}%", charge);
lvgl::statusbar_set_battery_text(battery_text);
lvgl::statusbar_set_battery_visibility(true);
} else {
lvgl::statusbar_set_battery_visibility(false);
}
} else {
lvgl::statusbar_set_battery_visibility(false);
}
} }
void updateUsbIcon() { void updateUsbIcon() {
@@ -157,6 +157,116 @@ static const char* TOOLS_JSON =
"\"mp3_base64\":{\"type\":\"string\"}," "\"mp3_base64\":{\"type\":\"string\"},"
"\"volume\":{\"type\":\"integer\",\"default\":50}" "\"volume\":{\"type\":\"integer\",\"default\":50}"
"},\"required\":[\"mp3_base64\"]}" "},\"required\":[\"mp3_base64\"]}"
"},"
"{"
"\"name\":\"set_led\","
"\"description\":\"Control the onboard WS2812 NeoPixel RGB LED.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"r\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255,\"description\":\"Red channel (0-255)\"},"
"\"g\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255,\"description\":\"Green channel (0-255)\"},"
"\"b\":{\"type\":\"integer\",\"minimum\":0,\"maximum\":255,\"description\":\"Blue channel (0-255)\"},"
"\"mode\":{\"type\":\"string\",\"enum\":[\"static\",\"breath\",\"rainbow\",\"off\"],\"description\":\"LED mode\"}"
"},"
"\"required\":[\"r\",\"g\",\"b\",\"mode\"]"
"}"
"},"
"{"
"\"name\":\"get_battery\","
"\"description\":\"Read the current battery voltage and estimated capacity percentage.\","
"\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
"},"
"{"
"\"name\":\"get_sensors\","
"\"description\":\"Read onboard sensors (IMU, temperature, humidity).\","
"\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
"},"
"{"
"\"name\":\"scan_ble\","
"\"description\":\"Scan for nearby Bluetooth Low Energy devices.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"duration_ms\":{\"type\":\"integer\",\"default\":3000,\"description\":\"Scan duration in milliseconds\"}"
"}"
"}"
"},"
"{"
"\"name\":\"write_file\","
"\"description\":\"Write a file to the microSD card.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"path\":{\"type\":\"string\",\"description\":\"Destination path relative to /sdcard/\"},"
"\"content\":{\"type\":\"string\",\"description\":\"Text content of the file\"}"
"},"
"\"required\":[\"path\",\"content\"]"
"}"
"},"
"{"
"\"name\":\"read_file\","
"\"description\":\"Read a text file from the microSD card.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"path\":{\"type\":\"string\",\"description\":\"File path relative to /sdcard/\"}"
"},"
"\"required\":[\"path\"]"
"}"
"},"
"{"
"\"name\":\"download_file\","
"\"description\":\"Download a file from a URL directly to the microSD card.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"url\":{\"type\":\"string\",\"description\":\"The URL of the file to download\"},"
"\"filename\":{\"type\":\"string\",\"description\":\"The destination filename relative to /sdcard/\"}"
"},"
"\"required\":[\"url\",\"filename\"]"
"}"
"},"
"{"
"\"name\":\"get_video_streaming_instructions\","
"\"description\":\"Get details for monochrome TCP streaming and RGB565 color TCP streaming.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"protocol\":{\"type\":\"string\",\"enum\":[\"tcp\",\"color\",\"both\",\"all\"],\"default\":\"all\"}"
"}"
"}"
"},"
"{"
"\"name\":\"get_stream_stats\","
"\"description\":\"Get performance statistics for the active video streams.\","
"\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
"},"
"{"
"\"name\":\"list_apps\","
"\"description\":\"List all applications installed on the system, including metadata and locations.\","
"\"inputSchema\":{\"type\":\"object\",\"properties\":{}}"
"},"
"{"
"\"name\":\"run_app\","
"\"description\":\"Launch an application on the board by its App ID.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"app_id\":{\"type\":\"string\",\"description\":\"The unique ID of the application (e.g., 'one.tactility.helloworld')\"}"
"},"
"\"required\":[\"app_id\"]"
"}"
"},"
"{"
"\"name\":\"list_files\","
"\"description\":\"List files and folders in a directory on the SD card, with details like sizes.\","
"\"inputSchema\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"path\":{\"type\":\"string\",\"description\":\"Path relative to /sdcard/ to list (default is '/')\"}"
"}"
"}"
"}" "}"
"]"; "]";
@@ -173,7 +283,7 @@ static uint8_t* base64_decode(const char* input, size_t* output_size) {
size_t input_size = strlen(payload); size_t input_size = strlen(payload);
size_t approx_output_len = (input_size * 3) / 4; size_t approx_output_len = (input_size * 3) / 4;
uint8_t* output = (uint8_t*)psram_malloc(approx_output_len + 4); uint8_t* output = (uint8_t*)malloc(approx_output_len + 4);
if (output == NULL) { if (output == NULL) {
return NULL; return NULL;
} }
@@ -565,6 +675,162 @@ static cJSON* handle_tool_call(cJSON* id, cJSON* params) {
: make_error(id, -32020, error.c_str()); : make_error(id, -32020, error.c_str());
} }
if (strcmp(name, "set_led") == 0) {
cJSON* r_item = cJSON_GetObjectItemCaseSensitive(arguments, "r");
cJSON* g_item = cJSON_GetObjectItemCaseSensitive(arguments, "g");
cJSON* b_item = cJSON_GetObjectItemCaseSensitive(arguments, "b");
cJSON* mode_item = cJSON_GetObjectItemCaseSensitive(arguments, "mode");
if (!cJSON_IsNumber(r_item) || !cJSON_IsNumber(g_item) ||
!cJSON_IsNumber(b_item) || !cJSON_IsString(mode_item)) {
return make_error(id, -32602, "set_led requires r, g, b, and mode");
}
std::string error;
if (!mcp::setLedColor(r_item->valueint, g_item->valueint, b_item->valueint, mode_item->valuestring, error)) {
return make_error(id, -32020, error.c_str());
}
char msg[128];
snprintf(msg, sizeof(msg), "LED set to mode '%s' with base color (%d, %d, %d).",
mode_item->valuestring, r_item->valueint, g_item->valueint, b_item->valueint);
return make_tool_result(id, msg);
}
if (strcmp(name, "get_battery") == 0) {
double voltage_v = 0.0;
int percentage_pct = 0;
std::string error;
if (!mcp::getBatteryStatus(voltage_v, percentage_pct, error)) {
return make_error(id, -32020, error.c_str());
}
char result_buf[128];
snprintf(result_buf, sizeof(result_buf), "{\"voltage_v\":%.3f,\"percentage_pct\":%d}", voltage_v, percentage_pct);
return make_tool_result(id, result_buf);
}
if (strcmp(name, "get_sensors") == 0) {
double temp_c = 0.0;
double hum_pct = 0.0;
std::string imu_json;
std::string error;
if (!mcp::getSensors(temp_c, hum_pct, imu_json, error)) {
return make_error(id, -32020, error.c_str());
}
char result_buf[512];
snprintf(result_buf, sizeof(result_buf), "{\"temperature_c\":%.2f,\"humidity_pct\":%.2f,\"imu\":%s}",
temp_c, hum_pct, imu_json.c_str());
return make_tool_result(id, result_buf);
}
if (strcmp(name, "scan_ble") == 0) {
cJSON* duration_item = cJSON_GetObjectItemCaseSensitive(arguments, "duration_ms");
int duration_ms = cJSON_IsNumber(duration_item) ? duration_item->valueint : 3000;
if (duration_ms < 500 || duration_ms > 15000) {
return make_error(id, -32602, "duration_ms must be between 500 and 15000");
}
std::string devices_json;
std::string error;
if (!mcp::scanBleDevices(duration_ms, devices_json, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, devices_json.c_str());
}
if (strcmp(name, "write_file") == 0) {
cJSON* path_item = cJSON_GetObjectItemCaseSensitive(arguments, "path");
cJSON* content_item = cJSON_GetObjectItemCaseSensitive(arguments, "content");
if (!cJSON_IsString(path_item) || !cJSON_IsString(content_item)) {
return make_error(id, -32602, "write_file requires path and content");
}
std::string error;
if (!mcp::writeSdFile(path_item->valuestring, content_item->valuestring, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, "Successfully wrote file to SD card.");
}
if (strcmp(name, "read_file") == 0) {
cJSON* path_item = cJSON_GetObjectItemCaseSensitive(arguments, "path");
if (!cJSON_IsString(path_item)) {
return make_error(id, -32602, "read_file requires path");
}
std::string content;
std::string error;
if (!mcp::readSdFile(path_item->valuestring, content, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, content.c_str());
}
if (strcmp(name, "download_file") == 0) {
cJSON* url_item = cJSON_GetObjectItemCaseSensitive(arguments, "url");
cJSON* filename_item = cJSON_GetObjectItemCaseSensitive(arguments, "filename");
if (!cJSON_IsString(url_item) || !cJSON_IsString(filename_item)) {
return make_error(id, -32602, "download_file requires url and filename");
}
std::string error;
if (!mcp::downloadSdFile(url_item->valuestring, filename_item->valuestring, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, "Successfully downloaded file to SD card.");
}
if (strcmp(name, "get_video_streaming_instructions") == 0) {
cJSON* protocol_item = cJSON_GetObjectItemCaseSensitive(arguments, "protocol");
const char* protocol = cJSON_IsString(protocol_item) ? protocol_item->valuestring : "all";
char buf[512];
snprintf(buf, sizeof(buf),
"Tactility OS TCP Video Streaming Instructions:\n"
"1. Mono TCP Stream (compatibility mode): Stream raw 15,000-byte PBM/RLCD frames to TCP port 8081.\n"
"2. Color TCP Stream (native): Stream packets to TCP port 8083.\n"
" Packet format:\n"
" - Header (16 bytes): Magic 'RAW\\x01' (4 bytes), X-coord (2 bytes BE), Y-coord (2 bytes BE), Width (2 bytes BE), Height (2 bytes BE), Payload length (4 bytes BE).\n"
" - Payload (variable): Raw big-endian RGB565 pixel data of length 'Payload length'.\n"
"Active protocol selection: %s", protocol);
return make_tool_result(id, buf);
}
if (strcmp(name, "get_stream_stats") == 0) {
std::string stats_json;
if (!mcp::getVideoStreamStats(stats_json)) {
return make_error(id, -32020, "Failed to get stream stats");
}
return make_tool_result(id, stats_json.c_str());
}
if (strcmp(name, "list_apps") == 0) {
std::string apps_json;
std::string error;
if (!mcp::listApps(apps_json, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, apps_json.c_str());
}
if (strcmp(name, "run_app") == 0) {
cJSON* app_id_item = cJSON_GetObjectItemCaseSensitive(arguments, "app_id");
if (!cJSON_IsString(app_id_item)) {
return make_error(id, -32602, "run_app requires app_id");
}
std::string error;
if (!mcp::runApp(app_id_item->valuestring, error)) {
return make_error(id, -32020, error.c_str());
}
char msg[128];
snprintf(msg, sizeof(msg), "Successfully started application '%s'.", app_id_item->valuestring);
return make_tool_result(id, msg);
}
if (strcmp(name, "list_files") == 0) {
cJSON* path_item = cJSON_GetObjectItemCaseSensitive(arguments, "path");
const char* path = cJSON_IsString(path_item) ? path_item->valuestring : "/";
std::string files_json;
std::string error;
if (!mcp::listSdFiles(path, files_json, error)) {
return make_error(id, -32020, error.c_str());
}
return make_tool_result(id, files_json.c_str());
}
return make_error(id, -32602, "Unknown tool"); return make_error(id, -32602, "Unknown tool");
} }
@@ -6,6 +6,7 @@
#include <Tactility/service/ServiceManifest.h> #include <Tactility/service/ServiceManifest.h>
#include <Tactility/settings/WebServerSettings.h> #include <Tactility/settings/WebServerSettings.h>
#include <Tactility/settings/McpSettings.h> #include <Tactility/settings/McpSettings.h>
#include <Tactility/mcp/McpSystem.h>
#include <Tactility/MountPoints.h> #include <Tactility/MountPoints.h>
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
#include <Tactility/Logger.h> #include <Tactility/Logger.h>
@@ -510,7 +511,7 @@ bool WebServerService::startServer() {
settings.webServerPort, settings.webServerPort,
"0.0.0.0", "0.0.0.0",
handlers, handlers,
16384 // Stack size 12288 // Stack size (forces allocation in internal SRAM instead of PSRAM)
); );
httpServer->start(); httpServer->start();
@@ -531,6 +532,11 @@ bool WebServerService::startServer() {
settings.wifiMode == settings::webserver::WiFiMode::AccessPoint ? "AP" : "Station"); settings.wifiMode == settings::webserver::WiFiMode::AccessPoint ? "AP" : "Station");
} }
auto mcpSettings = settings::mcp::loadOrGetDefault();
if (mcpSettings.mcpEnabled) {
mcp::startVideoStreamServer();
}
return true; return true;
} }
@@ -542,6 +548,8 @@ void WebServerService::stopServer() {
httpServer->stop(); httpServer->stop();
httpServer.reset(); httpServer.reset();
mcp::stopVideoStreamServer();
// Stop AP mode WiFi if we started it // Stop AP mode WiFi if we started it
if (apWifiInitialized || apNetif != nullptr) { if (apWifiInitialized || apNetif != nullptr) {
stopApMode(); stopApMode();
@@ -107,6 +107,23 @@ static bool decrypt(const std::string& ssidInput, std::string& ssidOutput) {
return false; return false;
} }
// Strip PKCS#7 padding
if (result_length > 0) {
uint8_t pad_val = result[result_length - 1];
if (pad_val >= 1 && pad_val <= 16 && pad_val <= result_length) {
bool valid_padding = true;
for (size_t i = result_length - pad_val; i < result_length; ++i) {
if (result[i] != pad_val) {
valid_padding = false;
break;
}
}
if (valid_padding) {
result[result_length - pad_val] = 0;
}
}
}
ssidOutput = reinterpret_cast<char*>(result); ssidOutput = reinterpret_cast<char*>(result);
free(result); free(result);
return true; return true;
+25
View File
@@ -20,6 +20,9 @@
#include <Tactility/service/wifi/WifiSettings.h> #include <Tactility/service/wifi/WifiSettings.h>
#include <esp_wifi_default.h> #include <esp_wifi_default.h>
#include <esp_mac.h>
#include <esp_netif.h>
#include <mdns.h>
#include <lwip/esp_netif_net_stack.h> #include <lwip/esp_netif_net_stack.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <atomic> #include <atomic>
@@ -543,6 +546,28 @@ static void dispatchEnable(std::shared_ptr<Wifi> wifi) {
} }
wifi->netif = esp_netif_create_default_wifi_sta(); wifi->netif = esp_netif_create_default_wifi_sta();
if (wifi->netif != nullptr) {
uint8_t mac[6];
char hostname[32];
if (esp_read_mac(mac, ESP_MAC_WIFI_STA) == ESP_OK) {
snprintf(hostname, sizeof(hostname), "kidsOS-%02X%02X", mac[4], mac[5]);
} else {
strncpy(hostname, "kidsOS", sizeof(hostname));
}
LOGGER.info("Setting DHCP Hostname to '{}'", hostname);
esp_netif_set_hostname(wifi->netif, hostname);
// Initialize mDNS
esp_err_t mdns_err = mdns_init();
if (mdns_err == ESP_OK) {
LOGGER.info("mDNS initialized, setting hostname to '{}.local'", hostname);
mdns_hostname_set(hostname);
mdns_instance_name_set("kidsOS Tactility Device");
} else {
LOGGER.error("Failed to initialize mDNS: {}", (int)mdns_err);
}
}
// Warning: this is the memory-intensive operation // Warning: this is the memory-intensive operation
// It uses over 117kB of RAM with default settings for S3 on IDF v5.1.2 // It uses over 117kB of RAM with default settings for S3 on IDF v5.1.2
wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT(); wifi_init_config_t config = WIFI_INIT_CONFIG_DEFAULT();
@@ -44,7 +44,7 @@ static const char *TAG = "FT6x36";
#define FT6236U_CHIPID 0x64 // Chip selecting #define FT6236U_CHIPID 0x64 // Chip selecting
#define FT6336U_CHIPID 0x64 // Chip selecting #define FT6336U_CHIPID 0x64 // Chip selecting
#define FT62XX_DEFAULT_THRESHOLD 128 // Default threshold for touch detection #define FT62XX_DEFAULT_THRESHOLD 60 // Default threshold for touch detection (lowered from 128 to improve sensitivity on battery/floating ground)
/******************************************************************************* /*******************************************************************************
* Function definitions * Function definitions