Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68766cdbed | |||
| 75178652e2 | |||
| ff3eeacaf7 | |||
| 80bd1c9f20 | |||
| f94cc160cf | |||
| 84cd73e503 | |||
| 6076497997 | |||
| 213ad7ee61 | |||
| 1a96307de2 | |||
| c7dc66e04b | |||
| 15aad3f585 | |||
| 9a307e522d | |||
| c3abe79c90 | |||
| 05ce4335ff | |||
| ec2a6c0b88 | |||
| 0966cd45a4 | |||
| e3eb3fd415 | |||
| c4adaef0a5 | |||
| 057daf49a5 | |||
| d70f474f02 |
@@ -0,0 +1,4 @@
|
|||||||
|
idf_component_register(
|
||||||
|
SRCS "source/module.cpp"
|
||||||
|
REQUIRES TactilityKernel
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
general.vendor=CYD
|
||||||
|
general.name=ES3C28P
|
||||||
|
|
||||||
|
apps.launcherAppId=Launcher
|
||||||
|
|
||||||
|
hardware.target=ESP32S3
|
||||||
|
hardware.flashSize=16MB
|
||||||
|
hardware.spiRam=true
|
||||||
|
hardware.spiRamMode=OCT
|
||||||
|
hardware.spiRamSpeed=120M
|
||||||
|
hardware.esptoolFlashFreq=120M
|
||||||
|
hardware.tinyUsb=true
|
||||||
|
hardware.bluetooth=true
|
||||||
|
|
||||||
|
display.size=2.8"
|
||||||
|
display.shape=rectangle
|
||||||
|
display.dpi=143
|
||||||
|
|
||||||
|
lvgl.colorDepth=16
|
||||||
|
|
||||||
|
storage.userDataLocation=SD
|
||||||
|
|
||||||
|
dependencies.useDeprecatedHal=false
|
||||||
|
|
||||||
|
sdkconfig.CONFIG_LV_CACHE_DEF_SIZE=524288
|
||||||
|
sdkconfig.CONFIG_LV_IMAGE_HEADER_CACHE_DEF_CNT=16
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
dependencies:
|
||||||
|
- Platforms/platform-esp32
|
||||||
|
- Drivers/ili9341-module
|
||||||
|
- Drivers/ft5x06-module
|
||||||
|
- Drivers/es8311-module
|
||||||
|
- Drivers/audio-stream-module
|
||||||
|
dts: es3c28p.dts
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/dts-v1/;
|
||||||
|
|
||||||
|
#include <tactility/bindings/root.h>
|
||||||
|
#include <tactility/bindings/esp32_adc_oneshot.h>
|
||||||
|
#include <tactility/bindings/esp32_ble.h>
|
||||||
|
#include <tactility/bindings/esp32_gpio.h>
|
||||||
|
#include <tactility/bindings/esp32_i2c.h>
|
||||||
|
#include <tactility/bindings/esp32_i2s.h>
|
||||||
|
#include <tactility/bindings/esp32_pwm_ledc.h>
|
||||||
|
#include <tactility/bindings/esp32_sdmmc.h>
|
||||||
|
#include <tactility/bindings/esp32_spi.h>
|
||||||
|
#include <tactility/bindings/esp32_wifi_pinned.h>
|
||||||
|
#include <tactility/bindings/battery_sense.h>
|
||||||
|
#include <tactility/bindings/gpio_hog.h>
|
||||||
|
#include <tactility/bindings/pwm_backlight.h>
|
||||||
|
#include <bindings/ili9341.h>
|
||||||
|
#include <bindings/ft5x06.h>
|
||||||
|
#include <bindings/es8311.h>
|
||||||
|
|
||||||
|
/ {
|
||||||
|
compatible = "root";
|
||||||
|
model = "LCDWIKI ES3C28P";
|
||||||
|
|
||||||
|
wifi0 {
|
||||||
|
compatible = "espressif,esp32-wifi-pinned";
|
||||||
|
status = "disabled";
|
||||||
|
};
|
||||||
|
|
||||||
|
ble0 {
|
||||||
|
compatible = "espressif,esp32-ble";
|
||||||
|
status = "disabled";
|
||||||
|
};
|
||||||
|
|
||||||
|
gpio0 {
|
||||||
|
compatible = "espressif,esp32-gpio";
|
||||||
|
gpio-count = <49>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* FM8002E speaker amp enable, active-low on GPIO1 */
|
||||||
|
amp_enable {
|
||||||
|
compatible = "gpio-hog";
|
||||||
|
pin = <&gpio0 1 GPIO_FLAG_NONE>;
|
||||||
|
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
|
||||||
|
};
|
||||||
|
|
||||||
|
adc0 {
|
||||||
|
compatible = "espressif,esp32-adc-oneshot";
|
||||||
|
unit-id = <ADC_UNIT_1>;
|
||||||
|
clk-src = <ADC_RTC_CLK_SRC_DEFAULT>;
|
||||||
|
channels = <ADC_CHANNEL_8 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* BAT+ through 200K/200K divider into GPIO9 ADC1_CH8 */
|
||||||
|
battery-sense {
|
||||||
|
compatible = "battery-sense";
|
||||||
|
io-channel = <&adc0 0>;
|
||||||
|
reference-voltage-mv = <3300>;
|
||||||
|
multiplier = <2000>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* i2s0 must be before i2c0 so codec can reference it */
|
||||||
|
i2s0 {
|
||||||
|
compatible = "espressif,esp32-i2s";
|
||||||
|
port = <I2S_NUM_0>;
|
||||||
|
pin-bclk = <&gpio0 5 GPIO_FLAG_NONE>;
|
||||||
|
pin-ws = <&gpio0 7 GPIO_FLAG_NONE>;
|
||||||
|
pin-data-out = <&gpio0 8 GPIO_FLAG_NONE>;
|
||||||
|
pin-data-in = <&gpio0 6 GPIO_FLAG_NONE>;
|
||||||
|
pin-mclk = <&gpio0 4 GPIO_FLAG_NONE>;
|
||||||
|
};
|
||||||
|
|
||||||
|
i2c0 {
|
||||||
|
compatible = "espressif,esp32-i2c";
|
||||||
|
port = <I2C_NUM_0>;
|
||||||
|
clock-frequency = <100000>;
|
||||||
|
pin-sda = <&gpio0 16 GPIO_FLAG_NONE>;
|
||||||
|
pin-scl = <&gpio0 15 GPIO_FLAG_NONE>;
|
||||||
|
|
||||||
|
ft5x06@38 {
|
||||||
|
compatible = "focaltech,ft5x06";
|
||||||
|
reg = <0x38>;
|
||||||
|
x-max = <240>;
|
||||||
|
y-max = <320>;
|
||||||
|
swap-xy;
|
||||||
|
mirror-x;
|
||||||
|
pin-reset = <&gpio0 18 GPIO_FLAG_NONE>;
|
||||||
|
pin-interrupt = <&gpio0 17 GPIO_FLAG_NONE>;
|
||||||
|
};
|
||||||
|
|
||||||
|
es8311: es8311@18 {
|
||||||
|
compatible = "everest,es8311";
|
||||||
|
reg = <0x18>;
|
||||||
|
i2s = <&i2s0>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
display_backlight_pwm {
|
||||||
|
compatible = "espressif,esp32-pwm-ledc";
|
||||||
|
pin = <&gpio0 45 GPIO_FLAG_NONE>;
|
||||||
|
period-ns = <500000>;
|
||||||
|
ledc-timer = <0>;
|
||||||
|
ledc-channel = <0>;
|
||||||
|
};
|
||||||
|
|
||||||
|
display_backlight {
|
||||||
|
compatible = "pwm-backlight";
|
||||||
|
status = "disabled";
|
||||||
|
pwm = <&display_backlight_pwm>;
|
||||||
|
};
|
||||||
|
|
||||||
|
spi0 {
|
||||||
|
compatible = "espressif,esp32-spi";
|
||||||
|
host = <SPI2_HOST>;
|
||||||
|
pin-mosi = <&gpio0 11 GPIO_FLAG_NONE>;
|
||||||
|
pin-sclk = <&gpio0 12 GPIO_FLAG_NONE>;
|
||||||
|
cs-gpios = <&gpio0 10 GPIO_FLAG_NONE>;
|
||||||
|
|
||||||
|
display@0 {
|
||||||
|
compatible = "ilitek,ili9341";
|
||||||
|
horizontal-resolution = <320>;
|
||||||
|
vertical-resolution = <240>;
|
||||||
|
swap-xy;
|
||||||
|
invert-color;
|
||||||
|
bgr-order;
|
||||||
|
pixel-clock-hz = <40000000>;
|
||||||
|
pin-dc = <&gpio0 46 GPIO_FLAG_NONE>;
|
||||||
|
backlight = <&display_backlight>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sdmmc0 {
|
||||||
|
compatible = "espressif,esp32-sdmmc";
|
||||||
|
pin-clk = <&gpio0 38 GPIO_FLAG_NONE>;
|
||||||
|
pin-cmd = <&gpio0 40 GPIO_FLAG_NONE>;
|
||||||
|
pin-d0 = <&gpio0 39 GPIO_FLAG_NONE>;
|
||||||
|
pin-d1 = <&gpio0 41 GPIO_FLAG_NONE>;
|
||||||
|
pin-d2 = <&gpio0 48 GPIO_FLAG_NONE>;
|
||||||
|
pin-d3 = <&gpio0 47 GPIO_FLAG_NONE>;
|
||||||
|
slot = <SDMMC_HOST_SLOT_1>;
|
||||||
|
bus-width = <4>;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#include <tactility/module.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
static error_t start() {
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static error_t stop() {
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Module es3c28p_module = {
|
||||||
|
.name = "es3c28p",
|
||||||
|
.start = start,
|
||||||
|
.stop = stop,
|
||||||
|
.symbols = nullptr,
|
||||||
|
.internal = nullptr
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Force CMake reload to detect new files module.cpp and Configuration.cpp
|
||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
INCLUDE_DIRS "Source"
|
||||||
|
REQUIRES Tactility ButtonControl ST7305 PwmBacklight driver
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#include "devices/Display.h"
|
||||||
|
|
||||||
|
#include <Tactility/hal/Configuration.h>
|
||||||
|
#include <ButtonControl.h>
|
||||||
|
#include <driver/gpio.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
static constexpr auto* TAG = "WaveshareRLCD";
|
||||||
|
|
||||||
|
using namespace tt::hal;
|
||||||
|
|
||||||
|
static DeviceVector createDevices() {
|
||||||
|
return {
|
||||||
|
createDisplay(),
|
||||||
|
ButtonControl::createTwoButtonControl(GPIO_NUM_18, GPIO_NUM_0) // KEY=18 cycles, BOOT=0 selects
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool initBoot() {
|
||||||
|
// Amp enable GPIO46 active HIGH - keep LOW during boot to avoid pop,
|
||||||
|
// then HIGH after delay so speaker works for MCP / Mp3Player
|
||||||
|
gpio_config_t io_conf = {};
|
||||||
|
io_conf.intr_type = GPIO_INTR_DISABLE;
|
||||||
|
io_conf.mode = GPIO_MODE_OUTPUT;
|
||||||
|
io_conf.pin_bit_mask = (1ULL << GPIO_NUM_46);
|
||||||
|
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||||
|
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
|
||||||
|
gpio_config(&io_conf);
|
||||||
|
gpio_set_level(GPIO_NUM_46, 0);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
gpio_set_level(GPIO_NUM_46, 1);
|
||||||
|
LOG_I(TAG, "Speaker amp GPIO46 enabled");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern const Configuration hardwareConfiguration = {
|
||||||
|
.initBoot = initBoot,
|
||||||
|
.createDevices = createDevices
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#include "Display.h"
|
||||||
|
#include <St7305Display.h>
|
||||||
|
|
||||||
|
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
|
||||||
|
auto configuration = std::make_unique<St7305Display::Configuration>(
|
||||||
|
SPI2_HOST,
|
||||||
|
GPIO_NUM_40, // CS
|
||||||
|
GPIO_NUM_5, // DC
|
||||||
|
400, // Width
|
||||||
|
300, // Height
|
||||||
|
nullptr, // Touch device
|
||||||
|
false, // swapXY
|
||||||
|
false, // mirrorX
|
||||||
|
false, // mirrorY
|
||||||
|
true, // invertColor - initial true to match existing inverted look, user can toggle via Settings->Display
|
||||||
|
0 // bufferSize (0 = default, which is full screen = 120,000 pixels)
|
||||||
|
);
|
||||||
|
|
||||||
|
configuration->resetPin = GPIO_NUM_41;
|
||||||
|
|
||||||
|
auto display = std::make_shared<St7305Display>(std::move(configuration));
|
||||||
|
return std::reinterpret_pointer_cast<tt::hal::display::DisplayDevice>(display);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Tactility/hal/display/DisplayDevice.h>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#include <tactility/module.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
static error_t start() {
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static error_t stop() {
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Module waveshare_esp32_s3_rlcd_module = {
|
||||||
|
.name = "waveshare-esp32-s3-rlcd",
|
||||||
|
.start = start,
|
||||||
|
.stop = stop,
|
||||||
|
.symbols = nullptr,
|
||||||
|
.internal = nullptr
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
general.vendor=WaveShare
|
||||||
|
general.name=ESP32-S3-RLCD-4.2
|
||||||
|
general.incubating=true
|
||||||
|
|
||||||
|
apps.launcherAppId=Launcher
|
||||||
|
# apps.autoStartAppId=ApWebServer
|
||||||
|
|
||||||
|
hardware.target=ESP32S3
|
||||||
|
hardware.flashSize=16MB
|
||||||
|
hardware.spiRam=true
|
||||||
|
hardware.spiRamMode=OCT
|
||||||
|
hardware.spiRamSpeed=120M
|
||||||
|
hardware.tinyUsb=true
|
||||||
|
hardware.esptoolFlashFreq=120M
|
||||||
|
hardware.bluetooth=true
|
||||||
|
|
||||||
|
storage.userDataLocation=SD
|
||||||
|
|
||||||
|
display.size=4.2"
|
||||||
|
display.shape=rectangle
|
||||||
|
display.dpi=120
|
||||||
|
|
||||||
|
lvgl.colorDepth=16
|
||||||
|
lvgl.uiDensity=compact
|
||||||
|
lvgl.fontSize=20
|
||||||
|
lvgl.theme=DefaultDark
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
dependencies:
|
||||||
|
- Platforms/platform-esp32
|
||||||
|
dts: waveshare,esp32-s3-rlcd.dts
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/dts-v1/;
|
||||||
|
|
||||||
|
#include <tactility/bindings/root.h>
|
||||||
|
#include <tactility/bindings/esp32_ble.h>
|
||||||
|
#include <tactility/bindings/esp32_gpio.h>
|
||||||
|
#include <tactility/bindings/esp32_i2c_master.h>
|
||||||
|
#include <tactility/bindings/esp32_sdmmc.h>
|
||||||
|
#include <tactility/bindings/esp32_spi.h>
|
||||||
|
#include <tactility/bindings/esp32_i2s.h>
|
||||||
|
#include <tactility/bindings/esp32_uart.h>
|
||||||
|
#include <tactility/bindings/esp32_wifi_pinned.h>
|
||||||
|
#include <tactility/bindings/display_placeholder.h>
|
||||||
|
|
||||||
|
/ {
|
||||||
|
compatible = "root";
|
||||||
|
model = "Waveshare ESP32-S3-RLCD-4.2";
|
||||||
|
|
||||||
|
ble0 {
|
||||||
|
compatible = "espressif,esp32-ble";
|
||||||
|
status = "disabled";
|
||||||
|
};
|
||||||
|
|
||||||
|
gpio0 {
|
||||||
|
compatible = "espressif,esp32-gpio";
|
||||||
|
gpio-count = <49>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Audio: ES7210 mic + speaker via I2S0
|
||||||
|
// Pins verified from working MicroPython board_config.py for WAVESHARE_RLCD:
|
||||||
|
// mclk=16, bclk=9, ws=45, tx=8 (DAC out), rx=10 (ES7210 mic in)
|
||||||
|
// NOTE: Display DC is GPIO5, so I2S must NOT use GPIO5 to avoid bus corruption
|
||||||
|
i2s0 {
|
||||||
|
compatible = "espressif,esp32-i2s";
|
||||||
|
port = <I2S_NUM_0>;
|
||||||
|
pin-bclk = <&gpio0 9 GPIO_FLAG_NONE>;
|
||||||
|
pin-ws = <&gpio0 45 GPIO_FLAG_NONE>;
|
||||||
|
pin-data-out = <&gpio0 8 GPIO_FLAG_NONE>;
|
||||||
|
pin-data-in = <&gpio0 10 GPIO_FLAG_NONE>;
|
||||||
|
pin-mclk = <&gpio0 16 GPIO_FLAG_NONE>;
|
||||||
|
};
|
||||||
|
|
||||||
|
i2c0 {
|
||||||
|
compatible = "espressif,esp32-i2c-master";
|
||||||
|
port = <I2C_NUM_0>;
|
||||||
|
clock-frequency = <400000>;
|
||||||
|
pin-sda = <&gpio0 13 GPIO_FLAG_NONE>;
|
||||||
|
pin-scl = <&gpio0 14 GPIO_FLAG_NONE>;
|
||||||
|
};
|
||||||
|
|
||||||
|
spi0 {
|
||||||
|
compatible = "espressif,esp32-spi";
|
||||||
|
host = <SPI2_HOST>;
|
||||||
|
cs-gpios = <&gpio0 40 GPIO_FLAG_NONE>;
|
||||||
|
pin-mosi = <&gpio0 12 GPIO_FLAG_NONE>;
|
||||||
|
pin-sclk = <&gpio0 11 GPIO_FLAG_NONE>;
|
||||||
|
|
||||||
|
display {
|
||||||
|
compatible = "display-placeholder";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sdmmc0 {
|
||||||
|
compatible = "espressif,esp32-sdmmc";
|
||||||
|
pin-clk = <&gpio0 38 GPIO_FLAG_NONE>;
|
||||||
|
pin-cmd = <&gpio0 21 GPIO_FLAG_NONE>;
|
||||||
|
pin-d0 = <&gpio0 39 GPIO_FLAG_NONE>;
|
||||||
|
pin-d3 = <&gpio0 17 GPIO_FLAG_NONE>;
|
||||||
|
slot = <SDMMC_HOST_SLOT_1>;
|
||||||
|
bus-width = <1>;
|
||||||
|
pullups;
|
||||||
|
};
|
||||||
|
|
||||||
|
wifi0 {
|
||||||
|
compatible = "espressif,esp32-wifi-pinned";
|
||||||
|
status = "disabled";
|
||||||
|
};
|
||||||
|
|
||||||
|
uart0 {
|
||||||
|
compatible = "espressif,esp32-uart";
|
||||||
|
port = <UART_NUM_0>;
|
||||||
|
pin-tx = <&gpio0 43 GPIO_FLAG_NONE>;
|
||||||
|
pin-rx = <&gpio0 44 GPIO_FLAG_NONE>;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
idf_component_register(
|
||||||
|
SRC_DIRS "Source"
|
||||||
|
INCLUDE_DIRS "Source"
|
||||||
|
REQUIRES Tactility driver EspLcdCompat
|
||||||
|
)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#include "St7305Display.h"
|
||||||
|
#include "esp_lcd_st7305.h"
|
||||||
|
|
||||||
|
#include <tactility/log.h>
|
||||||
|
#include <esp_lcd_panel_ops.h>
|
||||||
|
#include <esp_lvgl_port.h>
|
||||||
|
|
||||||
|
static const char* TAG = "ST7305";
|
||||||
|
|
||||||
|
bool St7305Display::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) {
|
||||||
|
LOG_I(TAG, "Starting ST7305 SPI panel IO creation");
|
||||||
|
|
||||||
|
const esp_lcd_panel_io_spi_config_t panel_io_config = {
|
||||||
|
.cs_gpio_num = configuration->csPin,
|
||||||
|
.dc_gpio_num = configuration->dcPin,
|
||||||
|
.spi_mode = 0,
|
||||||
|
.pclk_hz = configuration->pixelClockFrequency,
|
||||||
|
.trans_queue_depth = configuration->transactionQueueDepth,
|
||||||
|
.on_color_trans_done = nullptr,
|
||||||
|
.user_ctx = nullptr,
|
||||||
|
.lcd_cmd_bits = 8,
|
||||||
|
.lcd_param_bits = 8,
|
||||||
|
.flags = {
|
||||||
|
.dc_high_on_cmd = 0,
|
||||||
|
.dc_low_on_data = 0,
|
||||||
|
.dc_low_on_param = 0,
|
||||||
|
.octal_mode = 0,
|
||||||
|
.quad_mode = 0,
|
||||||
|
.sio_mode = 0,
|
||||||
|
.lsb_first = 0,
|
||||||
|
.cs_high_active = 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (esp_lcd_new_panel_io_spi(configuration->spiHostDevice, &panel_io_config, &outHandle) != ESP_OK) {
|
||||||
|
LOG_E(TAG, "Failed to create panel SPI IO");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool St7305Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) {
|
||||||
|
const esp_lcd_panel_dev_config_t panel_config = {
|
||||||
|
.reset_gpio_num = configuration->resetPin,
|
||||||
|
.color_space = ESP_LCD_COLOR_SPACE_MONOCHROME,
|
||||||
|
.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
|
||||||
|
.bits_per_pixel = 1,
|
||||||
|
.flags = {
|
||||||
|
.reset_active_high = false
|
||||||
|
},
|
||||||
|
.vendor_config = nullptr
|
||||||
|
};
|
||||||
|
|
||||||
|
if (esp_lcd_new_panel_st7305(ioHandle, &panel_config, &panelHandle) != ESP_OK) {
|
||||||
|
LOG_E(TAG, "Failed to create st7305 panel");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ST7305 needs extra delay after reset before init — prevents freeze on fast boot
|
||||||
|
if (esp_lcd_panel_reset(panelHandle) != ESP_OK) {
|
||||||
|
LOG_E(TAG, "Failed to reset st7305 panel");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(150));
|
||||||
|
|
||||||
|
if (esp_lcd_panel_init(panelHandle) != ESP_OK) {
|
||||||
|
LOG_E(TAG, "Failed to init st7305 panel");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
|
||||||
|
if (configuration->invertColor) {
|
||||||
|
if (esp_lcd_panel_invert_color(panelHandle, true) != ESP_OK) {
|
||||||
|
LOG_W(TAG, "Failed to apply initial invertColor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
lvgl_port_display_cfg_t St7305Display::getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) {
|
||||||
|
return lvgl_port_display_cfg_t {
|
||||||
|
.io_handle = ioHandle,
|
||||||
|
.panel_handle = panelHandle,
|
||||||
|
.control_handle = nullptr,
|
||||||
|
.buffer_size = configuration->bufferSize,
|
||||||
|
.double_buffer = false, // Monochrome displays usually use single buffering to save RAM
|
||||||
|
.trans_size = 0,
|
||||||
|
.hres = configuration->horizontalResolution,
|
||||||
|
.vres = configuration->verticalResolution,
|
||||||
|
.monochrome = true, // Enables esp_lvgl_port monochrome converter
|
||||||
|
.rotation = {
|
||||||
|
.swap_xy = false,
|
||||||
|
.mirror_x = true,
|
||||||
|
.mirror_y = false,
|
||||||
|
},
|
||||||
|
.color_format = LV_COLOR_FORMAT_RGB565, // Must be RGB565 for monochrome mode to trigger converter
|
||||||
|
.flags = {
|
||||||
|
.buff_dma = false,
|
||||||
|
.buff_spiram = false,
|
||||||
|
.sw_rotate = false,
|
||||||
|
.swap_bytes = false,
|
||||||
|
.full_refresh = true, // We want full refresh to rewrite the converted block format to ST7305
|
||||||
|
.direct_mode = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <EspLcdDisplay.h>
|
||||||
|
#include <Tactility/hal/display/DisplayDevice.h>
|
||||||
|
|
||||||
|
#include <driver/gpio.h>
|
||||||
|
#include <driver/spi_common.h>
|
||||||
|
#include <esp_lcd_panel_io.h>
|
||||||
|
#include <esp_lcd_types.h>
|
||||||
|
#include <functional>
|
||||||
|
#include <lvgl.h>
|
||||||
|
|
||||||
|
class St7305Display final : public EspLcdDisplay {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
class Configuration {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
Configuration(
|
||||||
|
spi_host_device_t spiHostDevice,
|
||||||
|
gpio_num_t csPin,
|
||||||
|
gpio_num_t dcPin,
|
||||||
|
unsigned int horizontalResolution,
|
||||||
|
unsigned int verticalResolution,
|
||||||
|
std::shared_ptr<tt::hal::touch::TouchDevice> touch = nullptr,
|
||||||
|
bool swapXY = false,
|
||||||
|
bool mirrorX = false,
|
||||||
|
bool mirrorY = false,
|
||||||
|
bool invertColor = false,
|
||||||
|
uint32_t bufferSize = 0
|
||||||
|
) : spiHostDevice(spiHostDevice),
|
||||||
|
csPin(csPin),
|
||||||
|
dcPin(dcPin),
|
||||||
|
horizontalResolution(horizontalResolution),
|
||||||
|
verticalResolution(verticalResolution),
|
||||||
|
swapXY(swapXY),
|
||||||
|
mirrorX(mirrorX),
|
||||||
|
mirrorY(mirrorY),
|
||||||
|
invertColor(invertColor),
|
||||||
|
bufferSize(bufferSize),
|
||||||
|
touch(std::move(touch))
|
||||||
|
{
|
||||||
|
if (this->bufferSize == 0) {
|
||||||
|
// For monochrome display, full pixel count is expected for buffer size
|
||||||
|
this->bufferSize = horizontalResolution * verticalResolution;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spi_host_device_t spiHostDevice;
|
||||||
|
gpio_num_t csPin;
|
||||||
|
gpio_num_t dcPin;
|
||||||
|
gpio_num_t resetPin = GPIO_NUM_NC;
|
||||||
|
unsigned int pixelClockFrequency = 10'000'000; // 10MHz SPI clock for ST7305
|
||||||
|
size_t transactionQueueDepth = 10;
|
||||||
|
unsigned int horizontalResolution;
|
||||||
|
unsigned int verticalResolution;
|
||||||
|
bool swapXY = false;
|
||||||
|
bool mirrorX = false;
|
||||||
|
bool mirrorY = false;
|
||||||
|
bool invertColor = false;
|
||||||
|
uint32_t bufferSize = 0;
|
||||||
|
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
|
||||||
|
std::function<void(uint8_t)> _Nullable backlightDutyFunction = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
std::unique_ptr<Configuration> configuration;
|
||||||
|
|
||||||
|
bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) override;
|
||||||
|
|
||||||
|
bool createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) override;
|
||||||
|
|
||||||
|
lvgl_port_display_cfg_t getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
explicit St7305Display(std::unique_ptr<Configuration> inConfiguration) :
|
||||||
|
configuration(std::move(inConfiguration))
|
||||||
|
{}
|
||||||
|
|
||||||
|
std::string getName() const override { return "ST7305"; }
|
||||||
|
|
||||||
|
std::string getDescription() const override { return "ST7305 monochrome reflective LCD display"; }
|
||||||
|
|
||||||
|
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable getTouchDevice() override { return configuration->touch; }
|
||||||
|
|
||||||
|
void setBacklightDuty(uint8_t backlightDuty) override {
|
||||||
|
if (configuration->backlightDutyFunction != nullptr) {
|
||||||
|
configuration->backlightDutyFunction(backlightDuty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool supportsBacklightDuty() const override { return configuration->backlightDutyFunction != nullptr; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
#include "esp_lcd_st7305.h"
|
||||||
|
#include "soc/soc_caps.h"
|
||||||
|
#include "esp_check.h"
|
||||||
|
#include "esp_lcd_types.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/cdefs.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_lcd_panel_interface.h"
|
||||||
|
#include "esp_lcd_panel_io.h"
|
||||||
|
#include "esp_lcd_panel_vendor.h"
|
||||||
|
#include "esp_lcd_panel_ops.h"
|
||||||
|
#include "esp_lcd_panel_commands.h"
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
static const char *TAG = "st7305";
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_del(esp_lcd_panel_t *panel);
|
||||||
|
static esp_err_t panel_st7305_reset(esp_lcd_panel_t *panel);
|
||||||
|
static esp_err_t panel_st7305_init(esp_lcd_panel_t *panel);
|
||||||
|
static esp_err_t panel_st7305_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);
|
||||||
|
static esp_err_t panel_st7305_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
|
||||||
|
static esp_err_t panel_st7305_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
|
||||||
|
static esp_err_t panel_st7305_swap_xy(esp_lcd_panel_t *panel, bool swap_axes);
|
||||||
|
static esp_err_t panel_st7305_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap);
|
||||||
|
static esp_err_t panel_st7305_disp_on_off(esp_lcd_panel_t *panel, bool off);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
esp_lcd_panel_t base;
|
||||||
|
esp_lcd_panel_io_handle_t io;
|
||||||
|
int reset_gpio_num;
|
||||||
|
bool reset_level;
|
||||||
|
int x_gap;
|
||||||
|
int y_gap;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
uint8_t rotation;
|
||||||
|
uint8_t madctl_val;
|
||||||
|
const st7305_lcd_init_cmd_t *init_cmds;
|
||||||
|
uint16_t init_cmds_size;
|
||||||
|
uint8_t *draw_buffer;
|
||||||
|
} st7305_panel_t;
|
||||||
|
|
||||||
|
static const st7305_lcd_init_cmd_t st7305_init_cmds[] = {
|
||||||
|
{0xD6, (uint8_t[]){0x17, 0x02}, 2, 0}, // NVM Load Control
|
||||||
|
{0xD1, (uint8_t[]){0x01}, 1, 0}, // Booster Enable
|
||||||
|
{0xC0, (uint8_t[]){0x11, 0x04}, 2, 0}, // Gate Voltage Setting
|
||||||
|
{0xC1, (uint8_t[]){0x41, 0x41, 0x41, 0x41}, 4, 0}, // VSHP Setting
|
||||||
|
{0xC2, (uint8_t[]){0x19, 0x19, 0x19, 0x19}, 4, 0}, // VSLP Setting
|
||||||
|
{0xC4, (uint8_t[]){0x41, 0x41, 0x41, 0x41}, 4, 0}, // VSHN Setting
|
||||||
|
{0xC5, (uint8_t[]){0x19, 0x13, 0x19, 0x19}, 4, 0}, // VSLN Setting
|
||||||
|
{0xD8, (uint8_t[]){0xA6, 0xE9}, 2, 0}, // OSC Setting
|
||||||
|
{0xB2, (uint8_t[]){0x05}, 1, 0}, // Frame Rate Control
|
||||||
|
{0xB3, (uint8_t[]){0xE5, 0xF6, 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45}, 10, 0}, // Gate EQ HPM
|
||||||
|
{0xB4, (uint8_t[]){0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45}, 8, 0}, // Gate EQ LPM
|
||||||
|
{0x62, (uint8_t[]){0x32, 0x03, 0x1F}, 3, 0}, // Gate Timing Control
|
||||||
|
{0xB7, (uint8_t[]){0x13}, 1, 0}, // Source EQ Enable
|
||||||
|
{0xB0, (uint8_t[]){0x64}, 1, 0}, // Gate Line Setting: 300 line (0x64 = 100 * 3)
|
||||||
|
{0x11, NULL, 0, 200}, // Sleep out
|
||||||
|
{0xC9, (uint8_t[]){0x00}, 1, 0}, // Source Voltage Select
|
||||||
|
{0x36, (uint8_t[]){0x48}, 1, 0}, // Memory Data Access Control (MX=1, DO=1)
|
||||||
|
{0x3A, (uint8_t[]){0x11}, 1, 0}, // Data Format Select: 1bpp
|
||||||
|
{0xB9, (uint8_t[]){0x20}, 1, 0}, // Gamma Mode Setting
|
||||||
|
{0xB8, (uint8_t[]){0x29}, 1, 0}, // Panel Setting
|
||||||
|
{0x21, NULL, 0, 0}, // Display Inversion On
|
||||||
|
{0x2A, (uint8_t[]){0x12, 0x2A}, 2, 0}, // Column Address Setting
|
||||||
|
{0x2B, (uint8_t[]){0x00, 0xC7}, 2, 0}, // Row Address Setting
|
||||||
|
{0x35, (uint8_t[]){0x00}, 1, 0}, // TE Line
|
||||||
|
{0xD0, (uint8_t[]){0xFF}, 1, 0}, // Auto power down ON
|
||||||
|
{0x38, NULL, 0, 0}, // High Power Mode ON
|
||||||
|
{0x29, NULL, 0, 100}, // Display ON
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t esp_lcd_new_panel_st7305(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config,
|
||||||
|
esp_lcd_panel_handle_t *ret_panel)
|
||||||
|
{
|
||||||
|
esp_err_t ret = ESP_OK;
|
||||||
|
st7305_panel_t *st7305 = NULL;
|
||||||
|
gpio_config_t io_conf = { 0 };
|
||||||
|
|
||||||
|
ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
|
||||||
|
st7305 = (st7305_panel_t *)calloc(1, sizeof(st7305_panel_t));
|
||||||
|
ESP_GOTO_ON_FALSE(st7305, ESP_ERR_NO_MEM, err, TAG, "no mem for st7305 panel");
|
||||||
|
|
||||||
|
if (panel_dev_config->reset_gpio_num >= 0) {
|
||||||
|
io_conf.mode = GPIO_MODE_OUTPUT;
|
||||||
|
io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num;
|
||||||
|
ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
|
||||||
|
}
|
||||||
|
st7305->width = ST7305_WIDTH;
|
||||||
|
st7305->height = ST7305_HEIGHT;
|
||||||
|
st7305->madctl_val = 0x48; // MX=1, DO=1
|
||||||
|
st7305->rotation = 0;
|
||||||
|
|
||||||
|
st7305->io = io;
|
||||||
|
st7305->reset_gpio_num = panel_dev_config->reset_gpio_num;
|
||||||
|
st7305->reset_level = panel_dev_config->flags.reset_active_high;
|
||||||
|
if (panel_dev_config->vendor_config) {
|
||||||
|
st7305->init_cmds = ((st7305_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds;
|
||||||
|
st7305->init_cmds_size = ((st7305_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size;
|
||||||
|
} else {
|
||||||
|
st7305->init_cmds = st7305_init_cmds;
|
||||||
|
st7305->init_cmds_size = sizeof(st7305_init_cmds) / sizeof(st7305_lcd_init_cmd_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
st7305->draw_buffer = heap_caps_malloc(15000, MALLOC_CAP_DMA);
|
||||||
|
ESP_GOTO_ON_FALSE(st7305->draw_buffer, ESP_ERR_NO_MEM, err, TAG, "no mem for st7305 draw buffer");
|
||||||
|
memset(st7305->draw_buffer, 0, 15000);
|
||||||
|
st7305->base.del = panel_st7305_del;
|
||||||
|
st7305->base.reset = panel_st7305_reset;
|
||||||
|
st7305->base.init = panel_st7305_init;
|
||||||
|
st7305->base.draw_bitmap = panel_st7305_draw_bitmap;
|
||||||
|
st7305->base.invert_color = panel_st7305_invert_color;
|
||||||
|
st7305->base.set_gap = panel_st7305_set_gap;
|
||||||
|
st7305->base.mirror = panel_st7305_mirror;
|
||||||
|
st7305->base.swap_xy = panel_st7305_swap_xy;
|
||||||
|
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||||
|
st7305->base.disp_off = panel_st7305_disp_on_off;
|
||||||
|
#else
|
||||||
|
st7305->base.disp_on_off = panel_st7305_disp_on_off;
|
||||||
|
#endif
|
||||||
|
*ret_panel = &(st7305->base);
|
||||||
|
ESP_LOGD(TAG, "new st7305 panel @%p", st7305);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
|
||||||
|
err:
|
||||||
|
if (st7305) {
|
||||||
|
if (panel_dev_config->reset_gpio_num >= 0) {
|
||||||
|
gpio_reset_pin(panel_dev_config->reset_gpio_num);
|
||||||
|
}
|
||||||
|
if (st7305->draw_buffer) {
|
||||||
|
free(st7305->draw_buffer);
|
||||||
|
}
|
||||||
|
free(st7305);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_del(esp_lcd_panel_t *panel)
|
||||||
|
{
|
||||||
|
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||||
|
if (st7305->reset_gpio_num >= 0) {
|
||||||
|
gpio_reset_pin(st7305->reset_gpio_num);
|
||||||
|
}
|
||||||
|
if (st7305->draw_buffer) {
|
||||||
|
free(st7305->draw_buffer);
|
||||||
|
}
|
||||||
|
free(st7305);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_reset(esp_lcd_panel_t *panel)
|
||||||
|
{
|
||||||
|
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||||
|
if (st7305->reset_gpio_num >= 0) {
|
||||||
|
gpio_set_level(st7305->reset_gpio_num, !st7305->reset_level);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
gpio_set_level(st7305->reset_gpio_num, st7305->reset_level);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(20));
|
||||||
|
gpio_set_level(st7305->reset_gpio_num, !st7305->reset_level);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_init(esp_lcd_panel_t *panel)
|
||||||
|
{
|
||||||
|
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||||
|
esp_lcd_panel_io_handle_t io = st7305->io;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < st7305->init_cmds_size; i++) {
|
||||||
|
if (st7305->init_cmds[i].data_bytes > 0) {
|
||||||
|
esp_lcd_panel_io_tx_param(io, st7305->init_cmds[i].cmd,
|
||||||
|
st7305->init_cmds[i].data,
|
||||||
|
st7305->init_cmds[i].data_bytes);
|
||||||
|
} else {
|
||||||
|
esp_lcd_panel_io_tx_param(io, st7305->init_cmds[i].cmd, NULL, 0);
|
||||||
|
}
|
||||||
|
if (st7305->init_cmds[i].delay_ms > 0) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(st7305->init_cmds[i].delay_ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly clear display RAM to white
|
||||||
|
if (st7305->draw_buffer) {
|
||||||
|
memset(st7305->draw_buffer, 0xFF, 15000); // 0xFF is White
|
||||||
|
uint8_t caset[] = {0x12, 0x2A};
|
||||||
|
uint8_t raset[] = {0x00, 0xC7};
|
||||||
|
esp_lcd_panel_io_tx_param(io, ST7305_CMD_CASET, caset, sizeof(caset));
|
||||||
|
esp_lcd_panel_io_tx_param(io, ST7305_CMD_RASET, raset, sizeof(raset));
|
||||||
|
esp_lcd_panel_io_tx_color(io, ST7305_CMD_RAMWR, st7305->draw_buffer, 15000);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)
|
||||||
|
{
|
||||||
|
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||||
|
esp_lcd_panel_io_handle_t io = st7305->io;
|
||||||
|
|
||||||
|
const uint8_t *src = (const uint8_t *)color_data;
|
||||||
|
|
||||||
|
// Convert from vertical-page format (SSD1306-style) to ST7305 landscape 2x4 block format
|
||||||
|
for (int y = 0; y < 300; y++) {
|
||||||
|
int inv_y = 299 - y;
|
||||||
|
int block_y = inv_y >> 2;
|
||||||
|
int local_y = inv_y & 3;
|
||||||
|
|
||||||
|
int src_y_div_8 = y >> 3;
|
||||||
|
int src_y_mod_8 = y & 7;
|
||||||
|
uint8_t src_bit_mask = 1 << src_y_mod_8;
|
||||||
|
|
||||||
|
for (int x = 0; x < 400; x++) {
|
||||||
|
int byte_x = x >> 1;
|
||||||
|
int local_x = x & 1;
|
||||||
|
|
||||||
|
int dest_byte_idx = byte_x * 75 + block_y;
|
||||||
|
int dest_bit_pos = 7 - ((local_y << 1) | local_x);
|
||||||
|
|
||||||
|
int src_byte_idx = 400 * src_y_div_8 + x;
|
||||||
|
|
||||||
|
// Read standard vertical page byte bit
|
||||||
|
bool is_pixel_set = (src[src_byte_idx] & src_bit_mask) != 0;
|
||||||
|
|
||||||
|
// Invert the pixel logic if required, standard:
|
||||||
|
// esp_lvgl_port monochrome transform clears the bit (0) for light/chroma colors
|
||||||
|
// and sets the bit (1) for dark/black.
|
||||||
|
// In ST7305 display RAM, White/Light is 1, Black/Dark is 0.
|
||||||
|
// So we write: 1 (White) if is_pixel_set is false (light), and 0 (Black) if is_pixel_set is true (dark).
|
||||||
|
if (!is_pixel_set) {
|
||||||
|
st7305->draw_buffer[dest_byte_idx] |= (1 << dest_bit_pos);
|
||||||
|
} else {
|
||||||
|
st7305->draw_buffer[dest_byte_idx] &= ~(1 << dest_bit_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t caset[] = {0x12, 0x2A};
|
||||||
|
uint8_t raset[] = {0x00, 0xC7};
|
||||||
|
|
||||||
|
esp_lcd_panel_io_tx_param(io, ST7305_CMD_CASET, caset, sizeof(caset));
|
||||||
|
esp_lcd_panel_io_tx_param(io, ST7305_CMD_RASET, raset, sizeof(raset));
|
||||||
|
esp_lcd_panel_io_tx_color(io, ST7305_CMD_RAMWR, st7305->draw_buffer, 15000);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
|
||||||
|
{
|
||||||
|
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||||
|
return esp_lcd_panel_io_tx_param(st7305->io, invert_color_data ? ST7305_CMD_INVON : ST7305_CMD_INVOFF, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
|
||||||
|
{
|
||||||
|
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||||
|
esp_lcd_panel_io_handle_t io = st7305->io;
|
||||||
|
if (mirror_x) {
|
||||||
|
st7305->madctl_val |= ST7305_MADCTL_MX;
|
||||||
|
} else {
|
||||||
|
st7305->madctl_val &= ~ST7305_MADCTL_MX;
|
||||||
|
}
|
||||||
|
if (mirror_y) {
|
||||||
|
st7305->madctl_val |= ST7305_MADCTL_MY;
|
||||||
|
} else {
|
||||||
|
st7305->madctl_val &= ~ST7305_MADCTL_MY;
|
||||||
|
}
|
||||||
|
return esp_lcd_panel_io_tx_param(io, ST7305_CMD_MADCTL, &st7305->madctl_val, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_swap_xy(esp_lcd_panel_t *panel, bool swap_axes)
|
||||||
|
{
|
||||||
|
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||||
|
esp_lcd_panel_io_handle_t io = st7305->io;
|
||||||
|
if (swap_axes) {
|
||||||
|
st7305->madctl_val |= ST7305_MADCTL_MV;
|
||||||
|
} else {
|
||||||
|
st7305->madctl_val &= ~ST7305_MADCTL_MV;
|
||||||
|
}
|
||||||
|
return esp_lcd_panel_io_tx_param(io, ST7305_CMD_MADCTL, (uint8_t[]) { st7305->madctl_val }, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap)
|
||||||
|
{
|
||||||
|
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||||
|
st7305->x_gap = x_gap;
|
||||||
|
st7305->y_gap = y_gap;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t panel_st7305_disp_on_off(esp_lcd_panel_t *panel, bool off)
|
||||||
|
{
|
||||||
|
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||||
|
return esp_lcd_panel_io_tx_param(st7305->io, off ? ST7305_CMD_DISPOFF : ST7305_CMD_DISPON, NULL, 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "esp_lcd_types.h"
|
||||||
|
#include "esp_lcd_panel_vendor.h"
|
||||||
|
#include "sdkconfig.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int cmd; /*<! The specific LCD command */
|
||||||
|
const void *data; /*<! Buffer that holds the command specific data */
|
||||||
|
size_t data_bytes; /*<! Size of `data` in memory, in bytes */
|
||||||
|
unsigned int delay_ms; /*<! Delay in milliseconds after this command */
|
||||||
|
} st7305_lcd_init_cmd_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const st7305_lcd_init_cmd_t *init_cmds;
|
||||||
|
uint16_t init_cmds_size;
|
||||||
|
} st7305_vendor_config_t;
|
||||||
|
|
||||||
|
#define ST7305_WIDTH 400
|
||||||
|
#define ST7305_HEIGHT 300
|
||||||
|
|
||||||
|
#define ST7305_CMD_NOP 0x00
|
||||||
|
#define ST7305_CMD_SWRESET 0x01
|
||||||
|
#define ST7305_CMD_SLPOUT 0x11
|
||||||
|
#define ST7305_CMD_NORON 0x13
|
||||||
|
#define ST7305_CMD_INVOFF 0x20
|
||||||
|
#define ST7305_CMD_INVON 0x21
|
||||||
|
#define ST7305_CMD_DISPOFF 0x28
|
||||||
|
#define ST7305_CMD_DISPON 0x29
|
||||||
|
#define ST7305_CMD_CASET 0x2A
|
||||||
|
#define ST7305_CMD_RASET 0x2B
|
||||||
|
#define ST7305_CMD_RAMWR 0x2C
|
||||||
|
#define ST7305_CMD_MADCTL 0x36
|
||||||
|
|
||||||
|
#define ST7305_MADCTL_MY 0x80
|
||||||
|
#define ST7305_MADCTL_MX 0x40
|
||||||
|
#define ST7305_MADCTL_MV 0x20
|
||||||
|
#define ST7305_MADCTL_ML 0x10
|
||||||
|
|
||||||
|
esp_err_t esp_lcd_new_panel_st7305(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -553,9 +553,14 @@ error_t close_stream(AudioStreamHandle handle_base) {
|
|||||||
Device* codec = is_input ? data->input_codec : data->output_codec;
|
Device* codec = is_input ? data->input_codec : data->output_codec;
|
||||||
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output;
|
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output;
|
||||||
|
|
||||||
|
// Determine if underlying codec is shared (BOTH codec used for both directions)
|
||||||
|
// In that case we must NOT close the codec if the other direction is still active.
|
||||||
|
Device* other_codec = is_input ? data->output_codec : data->input_codec;
|
||||||
|
AudioStreamHandleImpl** other_slot = is_input ? &data->open_output : &data->open_input;
|
||||||
|
bool codec_shared = (codec != nullptr && other_codec != nullptr && codec == other_codec);
|
||||||
|
|
||||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||||
if (handle->closing) {
|
if (handle->closing) {
|
||||||
// Already being closed by another caller (e.g. concurrent set_enabled + app close).
|
|
||||||
xSemaphoreGive(data->mutex);
|
xSemaphoreGive(data->mutex);
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
@@ -563,14 +568,16 @@ error_t close_stream(AudioStreamHandle handle_base) {
|
|||||||
if (*slot == handle) {
|
if (*slot == handle) {
|
||||||
*slot = nullptr;
|
*slot = nullptr;
|
||||||
}
|
}
|
||||||
|
bool other_still_open = (other_slot != nullptr && *other_slot != nullptr && *other_slot != reinterpret_cast<AudioStreamHandleImpl*>(1));
|
||||||
bool must_drain = (handle->busy_count > 0);
|
bool must_drain = (handle->busy_count > 0);
|
||||||
|
bool should_close_codec = !codec_shared || !other_still_open;
|
||||||
xSemaphoreGive(data->mutex);
|
xSemaphoreGive(data->mutex);
|
||||||
|
|
||||||
if (must_drain && handle->drain_semaphore != nullptr) {
|
if (must_drain && handle->drain_semaphore != nullptr) {
|
||||||
xSemaphoreTake(handle->drain_semaphore, portMAX_DELAY);
|
xSemaphoreTake(handle->drain_semaphore, portMAX_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (codec != nullptr) {
|
if (should_close_codec && codec != nullptr) {
|
||||||
audio_codec_close(codec);
|
audio_codec_close(codec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,16 +53,36 @@ error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (data->is_open) {
|
if (data->is_open) {
|
||||||
// open_direction == BOTH already serves INPUT-only or OUTPUT-only requests on the
|
// ES8311 is configured for WORK_MODE_BOTH, so an already-open device
|
||||||
// same sample settings -- only an exact direction mismatch (e.g. requesting BOTH
|
// can serve the opposite direction without reopening, provided sample
|
||||||
// while opened for INPUT only) needs a reopen.
|
// settings match. Promote open_direction to BOTH when we see a
|
||||||
bool direction_compatible = data->open_direction == config->direction
|
// complementary request.
|
||||||
|| data->open_direction == AUDIO_CODEC_DIR_BOTH;
|
bool is_complementary = (data->open_direction == AUDIO_CODEC_DIR_OUTPUT && config->direction == AUDIO_CODEC_DIR_INPUT)
|
||||||
|
|| (data->open_direction == AUDIO_CODEC_DIR_INPUT && config->direction == AUDIO_CODEC_DIR_OUTPUT);
|
||||||
|
bool direction_compatible = (data->open_direction == config->direction)
|
||||||
|
|| (data->open_direction == AUDIO_CODEC_DIR_BOTH)
|
||||||
|
|| (config->direction == AUDIO_CODEC_DIR_BOTH)
|
||||||
|
|| is_complementary;
|
||||||
bool same_config = direction_compatible
|
bool same_config = direction_compatible
|
||||||
&& data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
|
&& data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
|
||||||
&& data->open_sample_info.channel == sample_info.channel
|
&& data->open_sample_info.channel == sample_info.channel
|
||||||
&& data->open_sample_info.sample_rate == sample_info.sample_rate;
|
&& data->open_sample_info.sample_rate == sample_info.sample_rate;
|
||||||
return same_config ? ERROR_NONE : ERROR_RESOURCE;
|
if (same_config) {
|
||||||
|
// If we opened OUTPUT then INPUT (or vice versa), mark as BOTH
|
||||||
|
if (is_complementary) {
|
||||||
|
data->open_direction = AUDIO_CODEC_DIR_BOTH;
|
||||||
|
}
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
// Different sample config for opposite direction - ES8311 can only have one
|
||||||
|
// sample rate at a time (native 44100 resampled via audio-stream), so if
|
||||||
|
// codec rates differ we must fail. But if both sides use native 44100 (audio-stream
|
||||||
|
// always opens codec with native rate), we allow it.
|
||||||
|
if (direction_compatible) {
|
||||||
|
// Allow if both use same native rate path (audio-stream opens with codec's native)
|
||||||
|
return ERROR_RESOURCE;
|
||||||
|
}
|
||||||
|
return ERROR_RESOURCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
||||||
|
|||||||
@@ -84,5 +84,6 @@ dependencies:
|
|||||||
version: "1.1.4"
|
version: "1.1.4"
|
||||||
rules:
|
rules:
|
||||||
- if: "target in [esp32s3, esp32p4]"
|
- if: "target in [esp32s3, esp32p4]"
|
||||||
|
espressif/mdns: "1.3.2"
|
||||||
idf: '5.5.2'
|
idf: '5.5.2'
|
||||||
|
|
||||||
|
|||||||
@@ -199,12 +199,52 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(lv_buttonmatrix_set_button_width),
|
DEFINE_MODULE_SYMBOL(lv_buttonmatrix_set_button_width),
|
||||||
DEFINE_MODULE_SYMBOL(lv_buttonmatrix_set_selected_button),
|
DEFINE_MODULE_SYMBOL(lv_buttonmatrix_set_selected_button),
|
||||||
DEFINE_MODULE_SYMBOL(lv_buttonmatrix_clear_button_ctrl),
|
DEFINE_MODULE_SYMBOL(lv_buttonmatrix_clear_button_ctrl),
|
||||||
// lv_canvas
|
// lv_canvas - full API for emulator framebuffer use-cases (GameBoy 160x144)
|
||||||
DEFINE_MODULE_SYMBOL(lv_canvas_create),
|
DEFINE_MODULE_SYMBOL(lv_canvas_create),
|
||||||
DEFINE_MODULE_SYMBOL(lv_canvas_fill_bg),
|
|
||||||
DEFINE_MODULE_SYMBOL(lv_canvas_set_draw_buf),
|
|
||||||
DEFINE_MODULE_SYMBOL(lv_canvas_set_buffer),
|
DEFINE_MODULE_SYMBOL(lv_canvas_set_buffer),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_set_draw_buf),
|
||||||
DEFINE_MODULE_SYMBOL(lv_canvas_set_px),
|
DEFINE_MODULE_SYMBOL(lv_canvas_set_px),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_set_palette),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_get_draw_buf),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_get_px),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_get_image),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_get_buf),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_copy_buf),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_fill_bg),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_init_layer),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_finish_layer),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_canvas_buf_size),
|
||||||
|
// lv_draw_buf - LVGL9 buffers (was lv_img_buf)
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_create),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_destroy),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_init),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_dup),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_copy),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_goto_xy),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_clear),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_width_to_stride),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_align),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_set_palette),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_from_image),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_to_image),
|
||||||
|
// LVGL cache invalidation - critical for raw framebuffer apps (GB emulator) where buffer mutated directly
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_invalidate_cache),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_buf_flush_cache),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_image_cache_drop),
|
||||||
|
// lv_draw layer helpers
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_layer_create),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_draw_layer_alloc_buf),
|
||||||
|
// lv_image transform/scale - critical for GB 2x/3x scaling (image as canvas)
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_image_set_scale),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_image_set_scale_x),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_image_set_scale_y),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_image_set_rotation),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_image_set_pivot),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_image_set_offset_x),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_image_set_offset_y),
|
||||||
|
// generic obj transform styles (fallback scaling path) - pivot_x/y already exported above, only add scale
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_scale_x),
|
||||||
|
DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_scale_y),
|
||||||
// lv_label
|
// lv_label
|
||||||
DEFINE_MODULE_SYMBOL(lv_label_create),
|
DEFINE_MODULE_SYMBOL(lv_label_create),
|
||||||
DEFINE_MODULE_SYMBOL(lv_label_cut_text),
|
DEFINE_MODULE_SYMBOL(lv_label_cut_text),
|
||||||
@@ -431,7 +471,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(lv_draw_task_get_draw_dsc),
|
DEFINE_MODULE_SYMBOL(lv_draw_task_get_draw_dsc),
|
||||||
DEFINE_MODULE_SYMBOL(lv_draw_task_get_label_dsc),
|
DEFINE_MODULE_SYMBOL(lv_draw_task_get_label_dsc),
|
||||||
DEFINE_MODULE_SYMBOL(lv_draw_task_get_fill_dsc),
|
DEFINE_MODULE_SYMBOL(lv_draw_task_get_fill_dsc),
|
||||||
DEFINE_MODULE_SYMBOL(lv_draw_buf_create),
|
// lv_draw_buf_create moved to canvas block (duplicate removed)
|
||||||
// lv_image
|
// lv_image
|
||||||
DEFINE_MODULE_SYMBOL(lv_image_create),
|
DEFINE_MODULE_SYMBOL(lv_image_create),
|
||||||
DEFINE_MODULE_SYMBOL(lv_image_set_src),
|
DEFINE_MODULE_SYMBOL(lv_image_set_src),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ idf_component_register(
|
|||||||
SRCS ${SOURCES}
|
SRCS ${SOURCES}
|
||||||
INCLUDE_DIRS "include/"
|
INCLUDE_DIRS "include/"
|
||||||
PRIV_INCLUDE_DIRS "private/"
|
PRIV_INCLUDE_DIRS "private/"
|
||||||
REQUIRES TactilityKernel driver esp_adc esp_driver_i2c esp_lcd vfs fatfs esp_wifi esp_netif esp_event
|
REQUIRES TactilityKernel driver esp_adc esp_driver_i2c esp_lcd vfs fatfs esp_wifi esp_netif esp_event mdns
|
||||||
)
|
)
|
||||||
|
|
||||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
#if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
#if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
||||||
|
|
||||||
#include <esp_event.h>
|
#include <esp_event.h>
|
||||||
|
#include <esp_mac.h>
|
||||||
#include <esp_netif.h>
|
#include <esp_netif.h>
|
||||||
#include <esp_wifi.h>
|
#include <esp_wifi.h>
|
||||||
#include <esp_wifi_default.h>
|
#include <esp_wifi_default.h>
|
||||||
|
#include <mdns.h>
|
||||||
|
|
||||||
#include <tactility/concurrent/mutex.h>
|
#include <tactility/concurrent/mutex.h>
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
@@ -151,6 +153,31 @@ error_t bring_up_wifi(Esp32WifiCtx* ctx) {
|
|||||||
return ERROR_RESOURCE;
|
return ERROR_RESOURCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mDNS + hostname: kidsOS-XXXX from MAC, for Bonjour discovery
|
||||||
|
{
|
||||||
|
uint8_t mac[6];
|
||||||
|
char hostname[32];
|
||||||
|
if (esp_wifi_get_mac(WIFI_IF_STA, mac) == ESP_OK) {
|
||||||
|
snprintf(hostname, sizeof(hostname), "kidsOS-%02X%02X", mac[4], mac[5]);
|
||||||
|
} else {
|
||||||
|
strncpy(hostname, "kidsOS", sizeof(hostname));
|
||||||
|
}
|
||||||
|
LOG_I(TAG, "Setting DHCP hostname to '%s'", hostname);
|
||||||
|
esp_netif_set_hostname(ctx->netif, hostname);
|
||||||
|
|
||||||
|
esp_err_t mdns_err = mdns_init();
|
||||||
|
if (mdns_err == ESP_OK) {
|
||||||
|
LOG_I(TAG, "mDNS initialized, hostname '%s.local'", hostname);
|
||||||
|
mdns_hostname_set(hostname);
|
||||||
|
mdns_instance_name_set("kidsOS Tactility Device");
|
||||||
|
mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0);
|
||||||
|
mdns_service_add(NULL, "_tactility", "_tcp", 6666, NULL, 0);
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "mDNS init failed: %s", esp_err_to_name(mdns_err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Warning: this is the memory-intensive operation. It uses over 100kB of
|
// Warning: this is the memory-intensive operation. It uses over 100kB of
|
||||||
// RAM with default settings.
|
// RAM with default settings.
|
||||||
wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT();
|
wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT();
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
|
|||||||
vfs
|
vfs
|
||||||
fatfs
|
fatfs
|
||||||
lwip
|
lwip
|
||||||
|
mdns
|
||||||
spi_flash
|
spi_flash
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ struct PairedDevice {
|
|||||||
bool autoConnect = false;
|
bool autoConnect = false;
|
||||||
/** Profile used to pair (BtProfileId value). Defaults to BT_PROFILE_SPP=2. */
|
/** Profile used to pair (BtProfileId value). Defaults to BT_PROFILE_SPP=2. */
|
||||||
int profileId = 2;
|
int profileId = 2;
|
||||||
|
/** BLE address type (0=PUBLIC, 1=RANDOM, etc). Defaults to PUBLIC for backward compat. */
|
||||||
|
uint8_t addrType = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string addrToHex(const std::array<uint8_t, 6>& addr);
|
std::string addrToHex(const std::array<uint8_t, 6>& addr);
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace tt::network::mdns {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A discovered mDNS service instance.
|
||||||
|
* Example: instanceName="kidsOS-AB12", serviceType="_http", proto="_tcp",
|
||||||
|
* hostname="kidsOS-AB12", port=80, addresses=["192.168.1.42"]
|
||||||
|
*/
|
||||||
|
struct Service {
|
||||||
|
std::string instanceName; ///< Instance name (e.g. "ESP32-WebServer")
|
||||||
|
std::string serviceType; ///< Service type (e.g. "_http", "_tactility")
|
||||||
|
std::string proto; ///< Protocol (e.g. "_tcp", "_udp")
|
||||||
|
std::string hostname; ///< Hostname without .local (e.g. "kidsOS-AB12")
|
||||||
|
uint16_t port = 0; ///< Service port
|
||||||
|
std::vector<std::string> addresses; ///< All resolved IP addresses (v4 and v6)
|
||||||
|
std::string primaryAddress; ///< First IPv4 address, or first address if no v4
|
||||||
|
uint32_t ttl = 0; ///< Time to live
|
||||||
|
std::map<std::string, std::string> txtRecords; ///< TXT key-value pairs
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if mDNS subsystem is initialized and ready for queries.
|
||||||
|
* On simulator/POSIX it returns false until a platform implementation is present.
|
||||||
|
*/
|
||||||
|
bool isAvailable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browse for mDNS services.
|
||||||
|
*
|
||||||
|
* This wraps `mdns_query_ptr(serviceType, proto, timeout, maxResults, ...)`.
|
||||||
|
* It blocks for up to timeoutMs while collecting results.
|
||||||
|
*
|
||||||
|
* @param serviceType e.g. "_http", "_tactility", "_arduino"
|
||||||
|
* @param proto e.g. "_tcp", "_udp" (include leading underscore)
|
||||||
|
* @param timeoutMs how long to wait for answers (e.g. 3000)
|
||||||
|
* @param maxResults maximum number of results to collect (e.g. 20)
|
||||||
|
* @param outResults filled with discovered services
|
||||||
|
* @return true on success (may still be 0 results), false if mDNS not running or error
|
||||||
|
*/
|
||||||
|
bool browse(const std::string& serviceType, const std::string& proto, uint32_t timeoutMs, size_t maxResults, std::vector<Service>& outResults);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browse with sensible defaults: 3s timeout, 20 max results.
|
||||||
|
*/
|
||||||
|
inline bool browse(const std::string& serviceType, const std::string& proto, std::vector<Service>& outResults) {
|
||||||
|
return browse(serviceType, proto, 3000, 20, outResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a hostname (e.g. "kidsOS-AB12" or "kidsOS-AB12.local") to an IPv4 address string.
|
||||||
|
*
|
||||||
|
* @param hostname hostname to resolve, ".local" suffix is optional and stripped
|
||||||
|
* @param timeoutMs time to wait
|
||||||
|
* @param outIp resolved IP (e.g. "192.168.1.42")
|
||||||
|
* @return true if resolved
|
||||||
|
*/
|
||||||
|
bool resolveHostname(const std::string& hostname, uint32_t timeoutMs, std::string& outIp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve with 2s default timeout.
|
||||||
|
*/
|
||||||
|
inline bool resolveHostname(const std::string& hostname, std::string& outIp) {
|
||||||
|
return resolveHostname(hostname, 2000, outIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tt::network::mdns
|
||||||
@@ -1,44 +1,33 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <src/display/lv_display.h>
|
#include <src/display/lv_display.h>
|
||||||
|
|
||||||
namespace tt::settings::display {
|
namespace tt::settings::display {
|
||||||
|
|
||||||
enum class Orientation {
|
enum class Orientation {
|
||||||
// In order of rotation (to make it easier to convert to LVGL rotation)
|
|
||||||
Landscape,
|
Landscape,
|
||||||
Portrait,
|
Portrait,
|
||||||
LandscapeFlipped,
|
LandscapeFlipped,
|
||||||
PortraitFlipped,
|
PortraitFlipped,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class ScreensaverType {
|
enum class ScreensaverType {
|
||||||
None, // Just black screen
|
None,
|
||||||
BouncingBalls,
|
BouncingBalls,
|
||||||
Mystify,
|
Mystify,
|
||||||
MatrixRain,
|
MatrixRain,
|
||||||
StackChan,
|
StackChan,
|
||||||
Count // Sentinel for bounds checking - must be last
|
McpScreen,
|
||||||
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DisplaySettings {
|
struct DisplaySettings {
|
||||||
Orientation orientation;
|
Orientation orientation;
|
||||||
uint8_t gammaCurve;
|
uint8_t gammaCurve;
|
||||||
uint8_t backlightDuty;
|
uint8_t backlightDuty;
|
||||||
bool backlightTimeoutEnabled;
|
bool backlightTimeoutEnabled;
|
||||||
uint32_t backlightTimeoutMs; // 0 = Never
|
uint32_t backlightTimeoutMs;
|
||||||
ScreensaverType screensaverType = ScreensaverType::BouncingBalls;
|
ScreensaverType screensaverType = ScreensaverType::BouncingBalls;
|
||||||
|
bool disableScreensaverWhenCharging = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Compares default settings with the function parameter to return the difference */
|
|
||||||
lv_display_rotation_t toLvglDisplayRotation(Orientation orientation);
|
lv_display_rotation_t toLvglDisplayRotation(Orientation orientation);
|
||||||
|
|
||||||
bool load(DisplaySettings& settings);
|
bool load(DisplaySettings& settings);
|
||||||
|
|
||||||
DisplaySettings loadOrGetDefault();
|
DisplaySettings loadOrGetDefault();
|
||||||
|
|
||||||
DisplaySettings getDefault();
|
DisplaySettings getDefault();
|
||||||
|
|
||||||
bool save(const DisplaySettings& settings);
|
bool save(const DisplaySettings& settings);
|
||||||
|
}
|
||||||
} // namespace
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace tt::settings::mcp {
|
||||||
|
|
||||||
|
struct McpSettings {
|
||||||
|
bool mcpEnabled = false; // Enable MCP server endpoints on system web server
|
||||||
|
};
|
||||||
|
|
||||||
|
bool load(McpSettings& settings);
|
||||||
|
McpSettings getDefault();
|
||||||
|
McpSettings loadOrGetDefault();
|
||||||
|
bool save(const McpSettings& settings);
|
||||||
|
|
||||||
|
} // namespace tt::settings::mcp
|
||||||
@@ -14,6 +14,9 @@ bool isValidAppVersionName(const std::string& version);
|
|||||||
bool isValidAppVersionCode(const std::string& version);
|
bool isValidAppVersionCode(const std::string& version);
|
||||||
bool isValidName(const std::string& name);
|
bool isValidName(const std::string& name);
|
||||||
|
|
||||||
|
/** Parses a comma-separated flags string (e.g. "HideStatusBar,Hidden") into appFlags bitmask. */
|
||||||
|
uint16_t parseAppFlagsString(const std::string& raw);
|
||||||
|
|
||||||
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map. */
|
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map. */
|
||||||
bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest& manifest);
|
bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest& manifest);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace tt::app::files {
|
namespace tt::app::files {
|
||||||
|
|
||||||
bool isSupportedAppFile(const std::string& filename);
|
bool isSupportedAppFile(const std::string& filename);
|
||||||
bool isSupportedImageFile(const std::string& filename);
|
bool isSupportedImageFile(const std::string& filename);
|
||||||
bool isSupportedTextFile(const std::string& filename);
|
bool isSupportedTextFile(const std::string& filename);
|
||||||
|
bool isSupportedAudioFile(const std::string& filename);
|
||||||
|
bool isSupportedGameBoyFile(const std::string& filename);
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/drivers/audio_stream.h>
|
||||||
|
|
||||||
|
namespace tt::mcp {
|
||||||
|
|
||||||
|
struct McpSystemState {
|
||||||
|
std::mutex mutex;
|
||||||
|
bool overrideActive = false;
|
||||||
|
|
||||||
|
// UI elements when McpOverrideApp is active
|
||||||
|
lv_obj_t* drawArea = nullptr;
|
||||||
|
uint16_t* framebuffer = nullptr;
|
||||||
|
size_t framebufferSize = 0;
|
||||||
|
uint16_t displayWidth = 320;
|
||||||
|
uint16_t displayHeight = 240;
|
||||||
|
uint16_t drawWidth = 320;
|
||||||
|
uint16_t drawHeight = 240;
|
||||||
|
int drawColor = 1; // 0 = white, 1 = black
|
||||||
|
|
||||||
|
// Audio device status
|
||||||
|
Device* i2sDevice = nullptr;
|
||||||
|
Device* audioStreamDevice = nullptr;
|
||||||
|
AudioStreamHandle audioHandle = nullptr;
|
||||||
|
volatile bool audioBusy = false;
|
||||||
|
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();
|
||||||
|
|
||||||
|
bool clearScreen(int color);
|
||||||
|
bool drawText(const std::string& text, int x, int y, int size);
|
||||||
|
bool drawRgb565(const uint8_t* data, size_t size, int x, int y, int w, int h);
|
||||||
|
bool drawBmp(const uint8_t* data, size_t size, int x, int y);
|
||||||
|
bool drawPbm(const uint8_t* data, size_t size, int x, int y);
|
||||||
|
std::string getScreenshotPbmBase64();
|
||||||
|
|
||||||
|
bool playTone(int frequency, int durationMs, int volume, std::string& error);
|
||||||
|
bool recordVoice(int durationSec, const std::string& filename, size_t& recordedBytes, std::string& error);
|
||||||
|
bool playAudioFile(const std::string& filename, int volume, std::string& error);
|
||||||
|
bool playWavMemory(const uint8_t* data, size_t size, 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 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
|
||||||
|
|
||||||
|
#endif
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,7 @@ class DisplayIdleService final : public Service {
|
|||||||
bool backlightOff = false;
|
bool backlightOff = false;
|
||||||
|
|
||||||
static void stopScreensaverCb(lv_event_t* e);
|
static void stopScreensaverCb(lv_event_t* e);
|
||||||
|
void stopScreensaverLocked();
|
||||||
|
|
||||||
/** @pre Caller must hold LVGL lock */
|
/** @pre Caller must hold LVGL lock */
|
||||||
void activateScreensaver();
|
void activateScreensaver();
|
||||||
@@ -63,6 +64,7 @@ public:
|
|||||||
* arbitrary threads while the timer is running.
|
* arbitrary threads while the timer is running.
|
||||||
*/
|
*/
|
||||||
void stopScreensaver();
|
void stopScreensaver();
|
||||||
|
void startMcpScreensaver();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the screensaver is currently active.
|
* Check if the screensaver is currently active.
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ private:
|
|||||||
static esp_err_t handleApiAppsInstall(httpd_req_t* request);
|
static esp_err_t handleApiAppsInstall(httpd_req_t* request);
|
||||||
static esp_err_t handleApiWifi(httpd_req_t* request);
|
static esp_err_t handleApiWifi(httpd_req_t* request);
|
||||||
static esp_err_t handleApiScreenshot(httpd_req_t* request);
|
static esp_err_t handleApiScreenshot(httpd_req_t* request);
|
||||||
|
static esp_err_t handleApiMcp(httpd_req_t* request);
|
||||||
|
static esp_err_t handleApiScreenRaw(httpd_req_t* request);
|
||||||
|
|
||||||
// Dynamic asset serving
|
// Dynamic asset serving
|
||||||
static esp_err_t handleAssets(httpd_req_t* request);
|
static esp_err_t handleAssets(httpd_req_t* request);
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ namespace app {
|
|||||||
namespace apwebserver { extern const AppManifest manifest; }
|
namespace apwebserver { extern const AppManifest manifest; }
|
||||||
namespace crashdiagnostics { extern const AppManifest manifest; }
|
namespace crashdiagnostics { extern const AppManifest manifest; }
|
||||||
namespace webserversettings { extern const AppManifest manifest; }
|
namespace webserversettings { extern const AppManifest manifest; }
|
||||||
|
namespace mcpsettings { extern const AppManifest manifest; }
|
||||||
#if CONFIG_TT_TDECK_WORKAROUND == 1
|
#if CONFIG_TT_TDECK_WORKAROUND == 1
|
||||||
namespace keyboardsettings { extern const AppManifest manifest; } // T-Deck only for now
|
namespace keyboardsettings { extern const AppManifest manifest; } // T-Deck only for now
|
||||||
namespace trackballsettings { extern const AppManifest manifest; } // T-Deck only for now
|
namespace trackballsettings { extern const AppManifest manifest; } // T-Deck only for now
|
||||||
@@ -213,6 +214,8 @@ static void registerInternalApps() {
|
|||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
addAppManifest(app::apwebserver::manifest);
|
addAppManifest(app::apwebserver::manifest);
|
||||||
addAppManifest(app::webserversettings::manifest);
|
addAppManifest(app::webserversettings::manifest);
|
||||||
|
addAppManifest(app::mcpsettings::manifest);
|
||||||
|
// mcpoverride internal only via McpScreensaver, not shown in launcher
|
||||||
addAppManifest(app::crashdiagnostics::manifest);
|
addAppManifest(app::crashdiagnostics::manifest);
|
||||||
addAppManifest(app::development::manifest);
|
addAppManifest(app::development::manifest);
|
||||||
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
||||||
|
|||||||
@@ -56,6 +56,34 @@ bool isValidName(const std::string& name) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint16_t parseAppFlagsString(const std::string& raw) {
|
||||||
|
uint16_t flags = AppManifest::Flags::None;
|
||||||
|
if (raw.empty()) {
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parts = string::split(raw, ",");
|
||||||
|
for (auto& part : parts) {
|
||||||
|
std::string trimmed = string::trim(part, " \t\r\n");
|
||||||
|
if (trimmed.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string lower = string::lowercase(trimmed);
|
||||||
|
|
||||||
|
if (lower == "hidestatusbar" || lower == "hide_statusbar" || lower == "hide-statusbar" || lower == "hide_status_bar") {
|
||||||
|
flags |= AppManifest::Flags::HideStatusBar;
|
||||||
|
} else if (lower == "hidden") {
|
||||||
|
flags |= AppManifest::Flags::Hidden;
|
||||||
|
} else if (lower == "none" || lower == "0" || lower == "") {
|
||||||
|
// keep as none, no additional flag
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "Unknown app flag \"%s\" - ignoring", trimmed.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
/** The V1 format's first line is always the literal "[manifest]" section header; V2 files are flat from the first line onward. */
|
/** The V1 format's first line is always the literal "[manifest]" section header; V2 files are flat from the first line onward. */
|
||||||
static bool detectIsV1Format(const std::string& filePath) {
|
static bool detectIsV1Format(const std::string& filePath) {
|
||||||
std::string first_line;
|
std::string first_line;
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest&
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional: [app]flags - e.g. "HideStatusBar" or "HideStatusBar,Hidden"
|
||||||
|
auto flags_it = map.find("[app]flags");
|
||||||
|
if (flags_it != map.end()) {
|
||||||
|
manifest.appFlags = parseAppFlagsString(flags_it->second);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ bool parseManifestV2(const std::map<std::string, std::string>& map, AppManifest&
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional: app.flags - e.g. "HideStatusBar" or "HideStatusBar,Hidden"
|
||||||
|
auto flags_it = map.find("app.flags");
|
||||||
|
if (flags_it != map.end()) {
|
||||||
|
manifest.appFlags = parseAppFlagsString(flags_it->second);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class HalDisplayApp final : public App {
|
|||||||
lv_obj_t* timeoutSwitch = nullptr;
|
lv_obj_t* timeoutSwitch = nullptr;
|
||||||
lv_obj_t* timeoutDropdown = nullptr;
|
lv_obj_t* timeoutDropdown = nullptr;
|
||||||
lv_obj_t* screensaverDropdown = nullptr;
|
lv_obj_t* screensaverDropdown = nullptr;
|
||||||
|
lv_obj_t* disableWhenChargingWrapper = nullptr;
|
||||||
|
lv_obj_t* disableWhenChargingSwitch = nullptr;
|
||||||
|
|
||||||
static void onBacklightSliderEvent(lv_event_t* event) {
|
static void onBacklightSliderEvent(lv_event_t* event) {
|
||||||
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||||
@@ -72,6 +74,14 @@ class HalDisplayApp final : public App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void onDisableWhenChargingChanged(lv_event_t* event) {
|
||||||
|
auto* app = static_cast<HalDisplayApp*>(lv_event_get_user_data(event));
|
||||||
|
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||||
|
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
||||||
|
app->displaySettings.disableScreensaverWhenCharging = enabled;
|
||||||
|
app->displaySettingsUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
static void onTimeoutSwitch(lv_event_t* event) {
|
static void onTimeoutSwitch(lv_event_t* event) {
|
||||||
auto* app = static_cast<HalDisplayApp*>(lv_event_get_user_data(event));
|
auto* app = static_cast<HalDisplayApp*>(lv_event_get_user_data(event));
|
||||||
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||||
@@ -84,11 +94,17 @@ class HalDisplayApp final : public App {
|
|||||||
if (app->screensaverDropdown) {
|
if (app->screensaverDropdown) {
|
||||||
lv_obj_clear_state(app->screensaverDropdown, LV_STATE_DISABLED);
|
lv_obj_clear_state(app->screensaverDropdown, LV_STATE_DISABLED);
|
||||||
}
|
}
|
||||||
|
if (app->disableWhenChargingWrapper) {
|
||||||
|
lv_obj_clear_state(app->disableWhenChargingWrapper, LV_STATE_DISABLED);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lv_obj_add_state(app->timeoutDropdown, LV_STATE_DISABLED);
|
lv_obj_add_state(app->timeoutDropdown, LV_STATE_DISABLED);
|
||||||
if (app->screensaverDropdown) {
|
if (app->screensaverDropdown) {
|
||||||
lv_obj_add_state(app->screensaverDropdown, LV_STATE_DISABLED);
|
lv_obj_add_state(app->screensaverDropdown, LV_STATE_DISABLED);
|
||||||
}
|
}
|
||||||
|
if (app->disableWhenChargingWrapper) {
|
||||||
|
lv_obj_add_state(app->disableWhenChargingWrapper, LV_STATE_DISABLED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +294,23 @@ public:
|
|||||||
if (!displaySettings.backlightTimeoutEnabled) {
|
if (!displaySettings.backlightTimeoutEnabled) {
|
||||||
lv_obj_add_state(screensaverDropdown, LV_STATE_DISABLED);
|
lv_obj_add_state(screensaverDropdown, LV_STATE_DISABLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disableWhenChargingWrapper = lv_obj_create(main_wrapper);
|
||||||
|
lv_obj_set_size(disableWhenChargingWrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_pad_all(disableWhenChargingWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(disableWhenChargingWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
auto* charging_label = lv_label_create(disableWhenChargingWrapper);
|
||||||
|
lv_label_set_text(charging_label, "Disable on charging");
|
||||||
|
lv_obj_align(charging_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||||
|
disableWhenChargingSwitch = lv_switch_create(disableWhenChargingWrapper);
|
||||||
|
if (displaySettings.disableScreensaverWhenCharging) {
|
||||||
|
lv_obj_add_state(disableWhenChargingSwitch, LV_STATE_CHECKED);
|
||||||
|
}
|
||||||
|
lv_obj_align(disableWhenChargingSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||||
|
lv_obj_add_event_cb(disableWhenChargingSwitch, onDisableWhenChargingChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||||
|
if (!displaySettings.backlightTimeoutEnabled) {
|
||||||
|
lv_obj_add_state(disableWhenChargingWrapper, LV_STATE_DISABLED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
#include <Tactility/StringUtils.h>
|
#include <Tactility/StringUtils.h>
|
||||||
#include <Tactility/TactilityCore.h>
|
#include <Tactility/TactilityCore.h>
|
||||||
|
|
||||||
namespace tt::app::files {
|
namespace tt::app::files {
|
||||||
|
|
||||||
constexpr auto* TAG = "Files";
|
constexpr auto* TAG = "Files";
|
||||||
|
|
||||||
bool isSupportedAppFile(const std::string& filename) {
|
bool isSupportedAppFile(const std::string& filename) {
|
||||||
return filename.ends_with(".app");
|
return filename.ends_with(".app");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSupportedImageFile(const std::string& filename) {
|
bool isSupportedImageFile(const std::string& filename) {
|
||||||
// Currently only the PNG library is built into Tactility
|
|
||||||
return string::lowercase(filename).ends_with(".png");
|
return string::lowercase(filename).ends_with(".png");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSupportedTextFile(const std::string& filename) {
|
bool isSupportedTextFile(const std::string& filename) {
|
||||||
std::string filename_lower = string::lowercase(filename);
|
std::string l = string::lowercase(filename);
|
||||||
return filename_lower.ends_with(".txt") ||
|
return l.ends_with(".txt") || l.ends_with(".ini") || l.ends_with(".json") || l.ends_with(".yaml") || l.ends_with(".yml") ||
|
||||||
filename_lower.ends_with(".ini") ||
|
l.ends_with(".lua") || l.ends_with(".js") || l.ends_with(".properties");
|
||||||
filename_lower.ends_with(".json") ||
|
|
||||||
filename_lower.ends_with(".yaml") ||
|
|
||||||
filename_lower.ends_with(".yml") ||
|
|
||||||
filename_lower.ends_with(".lua") ||
|
|
||||||
filename_lower.ends_with(".js") ||
|
|
||||||
filename_lower.ends_with(".properties");
|
|
||||||
}
|
}
|
||||||
|
bool isSupportedAudioFile(const std::string& filename) {
|
||||||
} // namespace tt::app::filebrowser
|
std::string l = string::lowercase(filename);
|
||||||
|
return l.ends_with(".mp3") || l.ends_with(".wav") || l.ends_with(".ogg") || l.ends_with(".flac");
|
||||||
|
}
|
||||||
|
bool isSupportedGameBoyFile(const std::string& filename) {
|
||||||
|
std::string l = string::lowercase(filename);
|
||||||
|
return l.ends_with(".gb") || l.ends_with(".gbc") || l.ends_with(".sgb");
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <Tactility/app/files/SupportedFiles.h>
|
#include <Tactility/app/files/SupportedFiles.h>
|
||||||
|
#include <Tactility/Bundle.h>
|
||||||
#include <Tactility/app/files/View.h>
|
#include <Tactility/app/files/View.h>
|
||||||
|
|
||||||
#include <Tactility/Platform.h>
|
#include <Tactility/Platform.h>
|
||||||
@@ -228,9 +229,26 @@ void View::viewFile(const std::string& path, const std::string& filename) {
|
|||||||
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
||||||
notes::start(processed_filepath);
|
notes::start(processed_filepath);
|
||||||
} else {
|
} else {
|
||||||
// Remove forward slash, because we need a relative path
|
|
||||||
notes::start(processed_filepath.substr(1));
|
notes::start(processed_filepath.substr(1));
|
||||||
}
|
}
|
||||||
|
} else if (isSupportedAudioFile(filename)) {
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
auto bundle = std::make_shared<Bundle>();
|
||||||
|
bundle->putString("file", processed_filepath);
|
||||||
|
auto loader = service::loader::findLoaderService();
|
||||||
|
if (loader) {
|
||||||
|
loader->start("one.tactility.mp3player", bundle);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
} else if (isSupportedGameBoyFile(filename)) {
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
auto bundle = std::make_shared<Bundle>();
|
||||||
|
bundle->putString("file", processed_filepath);
|
||||||
|
auto loader = service::loader::findLoaderService();
|
||||||
|
if (loader) {
|
||||||
|
loader->start("one.tactility.gameboy", bundle);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
LOG_W(TAG, "Opening files of this type is not supported");
|
LOG_W(TAG, "Opening files of this type is not supported");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
|
||||||
|
#include <Tactility/Tactility.h>
|
||||||
|
#include <Tactility/mcp/McpSystem.h>
|
||||||
|
#include <Tactility/lvgl/Toolbar.h>
|
||||||
|
#include <Tactility/lvgl/LvglSync.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
|
|
||||||
|
constexpr auto* TAG = "McpOverrideApp";
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <tactility/lvgl_icon_shared.h>
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
|
||||||
|
namespace tt::app::mcpoverride {
|
||||||
|
|
||||||
|
|
||||||
|
class McpOverrideApp final : public App {
|
||||||
|
|
||||||
|
public:
|
||||||
|
void onCreate(AppContext& app) override {
|
||||||
|
// Prepare global state
|
||||||
|
auto& state = mcp::getState();
|
||||||
|
state.overrideActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||||
|
LOG_I(TAG, "onShow: Starting MCP Override display canvas");
|
||||||
|
auto& state = mcp::getState();
|
||||||
|
|
||||||
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_all(parent, 0, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_pad_row(parent, 0, LV_PART_MAIN);
|
||||||
|
|
||||||
|
// Standard toolbar so the user can navigate back
|
||||||
|
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
|
||||||
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
|
lv_obj_t* title_label = lv_label_create(toolbar);
|
||||||
|
lv_label_set_text(title_label, "MCP Override Screen");
|
||||||
|
|
||||||
|
// Create drawing canvas
|
||||||
|
state.drawArea = lv_canvas_create(parent);
|
||||||
|
lv_obj_set_width(state.drawArea, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(state.drawArea, 1);
|
||||||
|
lv_obj_set_style_radius(state.drawArea, 0, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_border_width(state.drawArea, 0, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_pad_all(state.drawArea, 0, LV_PART_MAIN);
|
||||||
|
lv_obj_remove_flag(state.drawArea, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
// Get display metrics
|
||||||
|
lv_display_t* display = lv_obj_get_display(parent);
|
||||||
|
state.displayWidth = lv_display_get_horizontal_resolution(display);
|
||||||
|
state.displayHeight = lv_display_get_vertical_resolution(display);
|
||||||
|
|
||||||
|
lv_obj_update_layout(parent);
|
||||||
|
state.drawWidth = lv_obj_get_content_width(state.drawArea);
|
||||||
|
state.drawHeight = lv_obj_get_content_height(state.drawArea);
|
||||||
|
|
||||||
|
// Allocate framebuffer
|
||||||
|
size_t required_size = (size_t)state.drawWidth * state.drawHeight * sizeof(uint16_t);
|
||||||
|
if (state.framebuffer == nullptr || state.framebufferSize != required_size) {
|
||||||
|
if (state.framebuffer != nullptr) {
|
||||||
|
heap_caps_free(state.framebuffer);
|
||||||
|
state.framebuffer = nullptr;
|
||||||
|
}
|
||||||
|
state.framebuffer = (uint16_t*)heap_caps_malloc(required_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if (state.framebuffer == nullptr) {
|
||||||
|
state.framebuffer = (uint16_t*)heap_caps_malloc(required_size, MALLOC_CAP_8BIT);
|
||||||
|
}
|
||||||
|
state.framebufferSize = state.framebuffer == nullptr ? 0 : required_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.framebuffer == nullptr) {
|
||||||
|
LOG_E(TAG, "Failed to allocate %u bytes framebuffer", (unsigned)required_size);
|
||||||
|
lv_obj_t* error = lv_label_create(state.drawArea);
|
||||||
|
lv_label_set_text(error, "Framebuffer allocation failed");
|
||||||
|
lv_obj_center(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_canvas_set_buffer(
|
||||||
|
state.drawArea,
|
||||||
|
state.framebuffer,
|
||||||
|
state.drawWidth,
|
||||||
|
state.drawHeight,
|
||||||
|
LV_COLOR_FORMAT_RGB565
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize welcome/waiting screen if LLM hasn't written anything yet
|
||||||
|
if (!state.overrideActive) {
|
||||||
|
// Fill with a nice dark blue/slate color
|
||||||
|
for (size_t i = 0; i < (size_t)state.drawWidth * state.drawHeight; ++i) {
|
||||||
|
state.framebuffer[i] = 0x18E3;
|
||||||
|
}
|
||||||
|
state.drawColor = 1;
|
||||||
|
|
||||||
|
lv_obj_t* welcome_label = lv_label_create(state.drawArea);
|
||||||
|
lv_label_set_text(welcome_label, "Waiting for LLM...");
|
||||||
|
lv_obj_set_style_text_color(welcome_label, lv_color_white(), LV_PART_MAIN);
|
||||||
|
lv_obj_align(welcome_label, LV_ALIGN_CENTER, 0, -20);
|
||||||
|
|
||||||
|
lv_obj_t* desc_label = lv_label_create(state.drawArea);
|
||||||
|
lv_label_set_text_fmt(desc_label, "Display Resolution: %ux%u", state.drawWidth, state.drawHeight);
|
||||||
|
lv_obj_set_style_text_color(desc_label, lv_palette_lighten(LV_PALETTE_BLUE, 3), LV_PART_MAIN);
|
||||||
|
lv_obj_align(desc_label, LV_ALIGN_CENTER, 0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onHide(AppContext& app) override {
|
||||||
|
LOG_I(TAG, "onHide: Tearing down MCP Override canvas");
|
||||||
|
auto& state = mcp::getState();
|
||||||
|
state.drawArea = nullptr;
|
||||||
|
if (state.framebuffer != nullptr) {
|
||||||
|
heap_caps_free(state.framebuffer);
|
||||||
|
state.framebuffer = nullptr;
|
||||||
|
state.framebufferSize = 0;
|
||||||
|
}
|
||||||
|
state.overrideActive = false;
|
||||||
|
|
||||||
|
// Stop any running tone or recording to prevent stuck state
|
||||||
|
state.audioRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDestroy(AppContext& app) override {
|
||||||
|
onHide(app);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const AppManifest manifest = {
|
||||||
|
.appId = "one.tactility.mcpscreen", // Keep the original appId for compatibility
|
||||||
|
.appName = "MCP Override Screen",
|
||||||
|
.appIcon = LVGL_ICON_SHARED_TOOLBAR,
|
||||||
|
.appCategory = Category::System,
|
||||||
|
.createApp = create<McpOverrideApp>
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#endif // ESP_PLATFORM
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
|
||||||
|
#include <Tactility/Tactility.h>
|
||||||
|
#include <Tactility/settings/McpSettings.h>
|
||||||
|
#include <Tactility/settings/WebServerSettings.h>
|
||||||
|
#include <Tactility/service/webserver/WebServerService.h>
|
||||||
|
#include <Tactility/lvgl/Toolbar.h>
|
||||||
|
#include <Tactility/lvgl/LvglSync.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
|
|
||||||
|
constexpr auto* TAG = "McpSettingsApp";
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <tactility/lvgl_icon_shared.h>
|
||||||
|
|
||||||
|
#include <esp_netif.h>
|
||||||
|
#include <esp_wifi.h>
|
||||||
|
|
||||||
|
namespace tt::app::mcpsettings {
|
||||||
|
|
||||||
|
|
||||||
|
class McpSettingsApp final : public App {
|
||||||
|
|
||||||
|
settings::mcp::McpSettings mcpSettings;
|
||||||
|
settings::mcp::McpSettings originalSettings;
|
||||||
|
settings::webserver::WebServerSettings wsSettings;
|
||||||
|
bool updated = false;
|
||||||
|
|
||||||
|
lv_obj_t* switchMcpEnabled = nullptr;
|
||||||
|
lv_obj_t* labelUrlValue = nullptr;
|
||||||
|
|
||||||
|
static void onMcpEnabledSwitch(lv_event_t* e) {
|
||||||
|
auto* app = static_cast<McpSettingsApp*>(lv_event_get_user_data(e));
|
||||||
|
bool enabled = lv_obj_has_state(app->switchMcpEnabled, LV_STATE_CHECKED);
|
||||||
|
getMainDispatcher().dispatch([app, enabled] {
|
||||||
|
app->mcpSettings.mcpEnabled = enabled;
|
||||||
|
app->updated = true;
|
||||||
|
if (lvgl::lock(100)) {
|
||||||
|
app->updateUrlDisplay();
|
||||||
|
lvgl::unlock();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateUrlDisplay() {
|
||||||
|
if (!labelUrlValue) return;
|
||||||
|
|
||||||
|
if (!mcpSettings.mcpEnabled) {
|
||||||
|
lv_label_set_text(labelUrlValue, "Disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string url = "http://";
|
||||||
|
bool ip_added = false;
|
||||||
|
|
||||||
|
// Try getting station IP first (we are connected to home Wi-Fi)
|
||||||
|
esp_netif_t* sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||||
|
if (sta_netif != nullptr) {
|
||||||
|
esp_netif_ip_info_t ip_info;
|
||||||
|
if (esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) {
|
||||||
|
char ip_str[16];
|
||||||
|
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
|
||||||
|
url += ip_str;
|
||||||
|
ip_added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no station IP, check if the AP interface has a valid IP address
|
||||||
|
if (!ip_added) {
|
||||||
|
esp_netif_t* ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
||||||
|
if (ap_netif != nullptr) {
|
||||||
|
esp_netif_ip_info_t ip_info;
|
||||||
|
if (esp_netif_get_ip_info(ap_netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) {
|
||||||
|
char ip_str[16];
|
||||||
|
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
|
||||||
|
url += ip_str;
|
||||||
|
ip_added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if no active IP address is detected on either interface
|
||||||
|
if (!ip_added) {
|
||||||
|
if (wsSettings.wifiMode == settings::webserver::WiFiMode::AccessPoint) {
|
||||||
|
url += "192.168.4.1";
|
||||||
|
} else {
|
||||||
|
url = "Connecting...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.starts_with("http://")) {
|
||||||
|
if (wsSettings.webServerPort != 80) {
|
||||||
|
url += ":" + std::to_string(wsSettings.webServerPort);
|
||||||
|
}
|
||||||
|
url += "/api/mcp";
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_label_set_text(labelUrlValue, url.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
void onCreate(AppContext& app) override {
|
||||||
|
mcpSettings = settings::mcp::loadOrGetDefault();
|
||||||
|
originalSettings = mcpSettings;
|
||||||
|
wsSettings = settings::webserver::loadOrGetDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
|
||||||
|
|
||||||
|
// MCP Enable toggle on toolbar
|
||||||
|
switchMcpEnabled = lvgl::toolbar_add_switch_action(toolbar);
|
||||||
|
if (mcpSettings.mcpEnabled) {
|
||||||
|
lv_obj_add_state(switchMcpEnabled, LV_STATE_CHECKED);
|
||||||
|
}
|
||||||
|
lv_obj_add_event_cb(switchMcpEnabled, onMcpEnabledSwitch, LV_EVENT_VALUE_CHANGED, this);
|
||||||
|
|
||||||
|
auto* main_wrapper = lv_obj_create(parent);
|
||||||
|
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||||
|
|
||||||
|
// URL Display
|
||||||
|
auto* url_wrapper = lv_obj_create(main_wrapper);
|
||||||
|
lv_obj_set_size(url_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_pad_all(url_wrapper, 10, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(url_wrapper, 1, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_flex_flow(url_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_flex_cross_place(url_wrapper, LV_FLEX_ALIGN_START, 0);
|
||||||
|
|
||||||
|
auto* url_title = lv_label_create(url_wrapper);
|
||||||
|
lv_label_set_text(url_title, "MCP Endpoint URL:");
|
||||||
|
|
||||||
|
labelUrlValue = lv_label_create(url_wrapper);
|
||||||
|
if (lv_display_get_color_format(lv_obj_get_display(parent)) == LV_COLOR_FORMAT_L8) {
|
||||||
|
lv_obj_set_style_text_color(labelUrlValue, lv_theme_get_color_secondary(labelUrlValue), LV_PART_MAIN);
|
||||||
|
} else {
|
||||||
|
lv_obj_set_style_text_color(labelUrlValue, lv_palette_main(LV_PALETTE_BLUE), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUrlDisplay();
|
||||||
|
|
||||||
|
// Info / Documentation text
|
||||||
|
auto* info_label = lv_label_create(main_wrapper);
|
||||||
|
lv_label_set_long_mode(info_label, LV_LABEL_LONG_WRAP);
|
||||||
|
lv_obj_set_width(info_label, LV_PCT(95));
|
||||||
|
if (lv_display_get_color_format(lv_obj_get_display(parent)) != LV_COLOR_FORMAT_L8) {
|
||||||
|
lv_obj_set_style_text_color(info_label, lv_palette_main(LV_PALETTE_GREY), 0);
|
||||||
|
}
|
||||||
|
lv_label_set_text(info_label,
|
||||||
|
"MCP (Model Context Protocol) Screen service allows LLMs to interact with the device "
|
||||||
|
"screen, audio, and tools directly.\n\n"
|
||||||
|
"Endpoints:\n"
|
||||||
|
"- POST /api/mcp (JSON-RPC tools)\n"
|
||||||
|
"- POST /api/screen/raw (big-endian RGB565 writes)\n\n"
|
||||||
|
"To show the LLM canvas, select 'MCP Screen' in Settings -> Display -> Screensaver. "
|
||||||
|
"The canvas also pops up automatically when an LLM sends a draw command.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void onHide(AppContext& app) override {
|
||||||
|
if (updated) {
|
||||||
|
const auto copy = mcpSettings;
|
||||||
|
const bool mcpStateChanged = (copy.mcpEnabled != originalSettings.mcpEnabled);
|
||||||
|
|
||||||
|
getMainDispatcher().dispatch([copy, mcpStateChanged]{
|
||||||
|
// Save to properties file
|
||||||
|
if (!settings::mcp::save(copy)) {
|
||||||
|
LOG_W(TAG, "Failed to persist MCP settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish WebServerSettingsChanged event so the HTTP server restarts/refreshes if needed
|
||||||
|
service::webserver::getPubsub()->publish(service::webserver::WebServerEvent::WebServerSettingsChanged);
|
||||||
|
|
||||||
|
if (mcpStateChanged) {
|
||||||
|
LOG_I(TAG, "MCP server state changed to %s", copy.mcpEnabled ? "enabled" : "disabled");
|
||||||
|
service::webserver::setWebServerEnabled(copy.mcpEnabled);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const AppManifest manifest = {
|
||||||
|
.appId = "McpSettings",
|
||||||
|
.appName = "MCP Screen",
|
||||||
|
.appIcon = LVGL_ICON_SHARED_SETTINGS,
|
||||||
|
.appCategory = Category::Settings,
|
||||||
|
.createApp = create<McpSettingsApp>
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#endif // ESP_PLATFORM
|
||||||
@@ -115,6 +115,15 @@ static void bt_event_bridge(Device*, void* /*context*/, BtEvent event) {
|
|||||||
case BT_RADIO_STATE_ON:
|
case BT_RADIO_STATE_ON:
|
||||||
getMainDispatcher().dispatch([] {
|
getMainDispatcher().dispatch([] {
|
||||||
auto peers = settings::loadAll();
|
auto peers = settings::loadAll();
|
||||||
|
LOG_I(TAG, "RADIO ON: loaded %d paired peers", (int)peers.size());
|
||||||
|
for (const auto& p : peers) {
|
||||||
|
LOG_I(TAG, " - peer %s name='%s' profile=%d auto=%d type=%d",
|
||||||
|
settings::addrToHex(p.addr).c_str(),
|
||||||
|
p.name.c_str(),
|
||||||
|
p.profileId,
|
||||||
|
(int)p.autoConnect,
|
||||||
|
(int)p.addrType);
|
||||||
|
}
|
||||||
bool has_hid_host_auto = false;
|
bool has_hid_host_auto = false;
|
||||||
bool has_hid_device_auto = false;
|
bool has_hid_device_auto = false;
|
||||||
for (const auto& p : peers) {
|
for (const auto& p : peers) {
|
||||||
@@ -122,28 +131,36 @@ static void bt_event_bridge(Device*, void* /*context*/, BtEvent event) {
|
|||||||
if (p.profileId == BT_PROFILE_HID_HOST) has_hid_host_auto = true;
|
if (p.profileId == BT_PROFILE_HID_HOST) has_hid_host_auto = true;
|
||||||
if (p.profileId == BT_PROFILE_HID_DEVICE) has_hid_device_auto = true;
|
if (p.profileId == BT_PROFILE_HID_DEVICE) has_hid_device_auto = true;
|
||||||
}
|
}
|
||||||
|
LOG_I(TAG, "RADIO ON: has_hid_host_auto=%d has_hid_device_auto=%d spp=%d midi=%d",
|
||||||
|
(int)has_hid_host_auto, (int)has_hid_device_auto,
|
||||||
|
(int)settings::shouldSppAutoStart(), (int)settings::shouldMidiAutoStart());
|
||||||
|
// Start all auto-connect/auto-start roles that are configured.
|
||||||
|
// Scanning (central) and advertising (peripheral) can run concurrently
|
||||||
|
// on ESP32 NimBLE, so we no longer use exclusive else-if.
|
||||||
if (has_hid_host_auto) {
|
if (has_hid_host_auto) {
|
||||||
LOG_I(TAG, "HID host auto-connect peer found — starting scan");
|
LOG_I(TAG, "HID host auto-connect peer found — starting scan");
|
||||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||||
bluetooth_scan_start(dev);
|
bluetooth_scan_start(dev);
|
||||||
}
|
}
|
||||||
} else if (has_hid_device_auto) {
|
}
|
||||||
|
if (has_hid_device_auto) {
|
||||||
LOG_I(TAG, "HID device auto-start (bonded peer found)");
|
LOG_I(TAG, "HID device auto-start (bonded peer found)");
|
||||||
if (Device* dev = bluetooth_hid_device_get_device()) {
|
if (Device* dev = bluetooth_hid_device_get_device()) {
|
||||||
bluetooth_hid_device_start(dev, BT_HID_DEVICE_MODE_KEYBOARD);
|
bluetooth_hid_device_start(dev, BT_HID_DEVICE_MODE_KEYBOARD);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
if (settings::shouldSppAutoStart()) {
|
// SPP and MIDI are peripheral servers; start them if their global
|
||||||
LOG_I(TAG, "Auto-starting SPP server");
|
// auto-start flags are set, regardless of HID roles.
|
||||||
if (Device* dev = bluetooth_serial_get_device()) {
|
if (settings::shouldSppAutoStart()) {
|
||||||
bluetooth_serial_start(dev);
|
LOG_I(TAG, "Auto-starting SPP server");
|
||||||
}
|
if (Device* dev = bluetooth_serial_get_device()) {
|
||||||
|
bluetooth_serial_start(dev);
|
||||||
}
|
}
|
||||||
if (settings::shouldMidiAutoStart()) {
|
}
|
||||||
LOG_I(TAG, "Auto-starting MIDI server");
|
if (settings::shouldMidiAutoStart()) {
|
||||||
if (Device* dev = bluetooth_midi_get_device()) {
|
LOG_I(TAG, "Auto-starting MIDI server");
|
||||||
bluetooth_midi_start(dev);
|
if (Device* dev = bluetooth_midi_get_device()) {
|
||||||
}
|
bluetooth_midi_start(dev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -176,6 +193,9 @@ static void bt_event_bridge(Device*, void* /*context*/, BtEvent event) {
|
|||||||
int profile_copy = event.pair_result.profile;
|
int profile_copy = event.pair_result.profile;
|
||||||
memcpy(addr_buf, event.pair_result.addr, 6);
|
memcpy(addr_buf, event.pair_result.addr, 6);
|
||||||
getMainDispatcher().dispatch([addr_buf, profile_copy]() mutable {
|
getMainDispatcher().dispatch([addr_buf, profile_copy]() mutable {
|
||||||
|
// Ensure Bluetooth auto-enables on boot after successful pairing,
|
||||||
|
// so previously connected devices reconnect after a restart.
|
||||||
|
settings::setEnableOnBoot(true);
|
||||||
std::array<uint8_t, 6> peer_addr;
|
std::array<uint8_t, 6> peer_addr;
|
||||||
memcpy(peer_addr.data(), addr_buf, 6);
|
memcpy(peer_addr.data(), addr_buf, 6);
|
||||||
const auto hex = settings::addrToHex(peer_addr);
|
const auto hex = settings::addrToHex(peer_addr);
|
||||||
@@ -185,6 +205,13 @@ static void bt_event_bridge(Device*, void* /*context*/, BtEvent event) {
|
|||||||
dev.name = "";
|
dev.name = "";
|
||||||
dev.autoConnect = true;
|
dev.autoConnect = true;
|
||||||
dev.profileId = profile_copy;
|
dev.profileId = profile_copy;
|
||||||
|
// Try to preserve addrType from scan cache if available
|
||||||
|
uint8_t cached_type = 0;
|
||||||
|
if (getCachedScanAddrType(peer_addr.data(), &cached_type)) {
|
||||||
|
dev.addrType = cached_type;
|
||||||
|
} else {
|
||||||
|
dev.addrType = 0;
|
||||||
|
}
|
||||||
if (settings::save(dev)) {
|
if (settings::save(dev)) {
|
||||||
LOG_I(TAG, "Saved paired peer %s (profile=%d)", hex.c_str(), profile_copy);
|
LOG_I(TAG, "Saved paired peer %s (profile=%d)", hex.c_str(), profile_copy);
|
||||||
}
|
}
|
||||||
@@ -290,6 +317,11 @@ bool start(Device* dev) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist enable-on-boot so that paired devices auto-reconnect after a restart.
|
||||||
|
// The settings file is written from the main task to avoid blocking the NimBLE host
|
||||||
|
// task, but the dispatcher may run immediately, so we also set it here.
|
||||||
|
settings::setEnableOnBoot(true);
|
||||||
|
|
||||||
LOG_I(TAG, "BT enabled");
|
LOG_I(TAG, "BT enabled");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -414,6 +446,9 @@ void unpair(const std::array<uint8_t, 6>& addr) {
|
|||||||
|
|
||||||
void connect(const std::array<uint8_t, 6>& addr, int profileId) {
|
void connect(const std::array<uint8_t, 6>& addr, int profileId) {
|
||||||
LOG_I(TAG, "connect(profile=%d)", profileId);
|
LOG_I(TAG, "connect(profile=%d)", profileId);
|
||||||
|
// Ensure BT restarts in the same mode after reboot, so previously connected
|
||||||
|
// devices can be re-found (scan) or reconnected to (advertising).
|
||||||
|
settings::setEnableOnBoot(true);
|
||||||
if (profileId == BT_PROFILE_HID_HOST) {
|
if (profileId == BT_PROFILE_HID_HOST) {
|
||||||
hidHostConnect(addr);
|
hidHostConnect(addr);
|
||||||
} else if (profileId == BT_PROFILE_HID_DEVICE) {
|
} else if (profileId == BT_PROFILE_HID_DEVICE) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <Tactility/bluetooth/Bluetooth.h>
|
#include <Tactility/bluetooth/Bluetooth.h>
|
||||||
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
|
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
|
||||||
#include <Tactility/bluetooth/BluetoothPrivate.h>
|
#include <Tactility/bluetooth/BluetoothPrivate.h>
|
||||||
|
#include <Tactility/bluetooth/BluetoothSettings.h>
|
||||||
|
|
||||||
#include <Tactility/Assets.h>
|
#include <Tactility/Assets.h>
|
||||||
#include <Tactility/Tactility.h>
|
#include <Tactility/Tactility.h>
|
||||||
@@ -480,6 +481,8 @@ static void hidHostSubscribeNext(HidHostCtx& ctx) {
|
|||||||
|
|
||||||
auto peer_addr = ctx.peerAddr;
|
auto peer_addr = ctx.peerAddr;
|
||||||
getMainDispatcher().dispatch([peer_addr] {
|
getMainDispatcher().dispatch([peer_addr] {
|
||||||
|
// Ensure BT stays on after reboot so this keyboard can be re-found.
|
||||||
|
settings::setEnableOnBoot(true);
|
||||||
// Find name from cached scan results
|
// Find name from cached scan results
|
||||||
std::string name;
|
std::string name;
|
||||||
{
|
{
|
||||||
@@ -488,17 +491,34 @@ static void hidHostSubscribeNext(HidHostCtx& ctx) {
|
|||||||
if (r.addr == peer_addr) { name = r.name; break; }
|
if (r.addr == peer_addr) { name = r.name; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
uint8_t cached_type = 0;
|
||||||
|
bool has_cached_type = getCachedScanAddrType(peer_addr.data(), &cached_type);
|
||||||
settings::PairedDevice device;
|
settings::PairedDevice device;
|
||||||
device.addr = peer_addr;
|
device.addr = peer_addr;
|
||||||
device.profileId = BT_PROFILE_HID_HOST;
|
device.profileId = BT_PROFILE_HID_HOST;
|
||||||
device.autoConnect = true;
|
device.autoConnect = true;
|
||||||
|
device.addrType = has_cached_type ? cached_type : 0;
|
||||||
const auto addr_hex = settings::addrToHex(peer_addr);
|
const auto addr_hex = settings::addrToHex(peer_addr);
|
||||||
|
LOG_I(TAG, "HID host ready: saving device %s name='%s' cached_type=%d has_cached=%d",
|
||||||
|
addr_hex.c_str(), name.c_str(), (int)cached_type, (int)has_cached_type);
|
||||||
settings::PairedDevice existing;
|
settings::PairedDevice existing;
|
||||||
if (settings::load(addr_hex, existing)) {
|
if (settings::load(addr_hex, existing)) {
|
||||||
|
LOG_I(TAG, "Existing file found for %s, preserving autoConnect=%d", addr_hex.c_str(), (int)existing.autoConnect);
|
||||||
device.autoConnect = existing.autoConnect;
|
device.autoConnect = existing.autoConnect;
|
||||||
|
// Preserve existing addrType if we don't have a cached one
|
||||||
|
if (!has_cached_type) {
|
||||||
|
device.addrType = existing.addrType;
|
||||||
|
}
|
||||||
|
// Preserve stored name if scan didn't provide one
|
||||||
|
if (name.empty() && !existing.name.empty()) {
|
||||||
|
name = existing.name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_I(TAG, "No existing file for %s, creating new", addr_hex.c_str());
|
||||||
}
|
}
|
||||||
device.name = name;
|
device.name = name;
|
||||||
settings::save(device);
|
bool saved = settings::save(device);
|
||||||
|
LOG_I(TAG, "Save result for %s: %d", addr_hex.c_str(), (int)saved);
|
||||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||||
BtEvent e = {};
|
BtEvent e = {};
|
||||||
e.type = BT_EVENT_PROFILE_STATE_CHANGED;
|
e.type = BT_EVENT_PROFILE_STATE_CHANGED;
|
||||||
@@ -794,13 +814,22 @@ void hidHostConnect(const std::array<uint8_t, 6>& addr) {
|
|||||||
// Notify driver that a HID host central connection is starting.
|
// Notify driver that a HID host central connection is starting.
|
||||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) bluetooth_set_hid_host_active(dev, true);
|
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) bluetooth_set_hid_host_active(dev, true);
|
||||||
|
|
||||||
// Look up the addr_type from the cached scan results.
|
// Look up the addr_type from the cached scan results, or from persisted storage.
|
||||||
|
// For RPA devices we may have cached RPA type, but we want to connect using identity.
|
||||||
|
// Prefer cached scan type for direct scan-match, fallback to stored file's addrType.
|
||||||
ble_addr_t ble_addr = {};
|
ble_addr_t ble_addr = {};
|
||||||
ble_addr.type = BLE_ADDR_PUBLIC;
|
ble_addr.type = BLE_ADDR_PUBLIC;
|
||||||
std::memcpy(ble_addr.val, addr.data(), 6);
|
std::memcpy(ble_addr.val, addr.data(), 6);
|
||||||
uint8_t addr_type = 0;
|
uint8_t addr_type = 0;
|
||||||
if (getCachedScanAddrType(addr.data(), &addr_type)) {
|
if (getCachedScanAddrType(addr.data(), &addr_type)) {
|
||||||
ble_addr.type = addr_type;
|
ble_addr.type = addr_type;
|
||||||
|
} else {
|
||||||
|
// Try persisted addrType if available
|
||||||
|
const auto hex = settings::addrToHex(addr);
|
||||||
|
settings::PairedDevice stored;
|
||||||
|
if (settings::load(hex, stored)) {
|
||||||
|
ble_addr.type = stored.addrType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t own_addr_type;
|
uint8_t own_addr_type;
|
||||||
@@ -847,34 +876,58 @@ bool hidHostGetConnectedPeer(std::array<uint8_t, 6>& addr_out) {
|
|||||||
void autoConnectHidHost() {
|
void autoConnectHidHost() {
|
||||||
if (hidHostIsConnected()) return;
|
if (hidHostIsConnected()) return;
|
||||||
|
|
||||||
// Connect to the first saved HID host peer that appeared in the last scan.
|
// Gather all stored peers that want auto-connect as HID host (central).
|
||||||
// cacheScanAddr() is populated during scanning so addr_type is available for ble_gap_connect.
|
auto all_peers = settings::loadAll();
|
||||||
|
std::vector<settings::PairedDevice> auto_peers;
|
||||||
|
for (const auto& p : all_peers) {
|
||||||
|
if (p.autoConnect && p.profileId == BT_PROFILE_HID_HOST) {
|
||||||
|
auto_peers.push_back(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (auto_peers.empty()) return;
|
||||||
|
|
||||||
auto scan = getScanResults();
|
auto scan = getScanResults();
|
||||||
|
|
||||||
|
// 1. Direct address match (public address devices, most keyboards).
|
||||||
|
// cacheScanAddr() is populated during scanning so addr_type is available for ble_gap_connect.
|
||||||
for (const auto& r : scan) {
|
for (const auto& r : scan) {
|
||||||
settings::PairedDevice stored;
|
settings::PairedDevice stored;
|
||||||
if (settings::load(settings::addrToHex(r.addr), stored) &&
|
if (settings::load(settings::addrToHex(r.addr), stored) &&
|
||||||
stored.autoConnect &&
|
stored.autoConnect &&
|
||||||
stored.profileId == BT_PROFILE_HID_HOST) {
|
stored.profileId == BT_PROFILE_HID_HOST) {
|
||||||
LOG_I(TAG, "Auto-connecting HID host to %s", settings::addrToHex(r.addr).c_str());
|
LOG_I(TAG, "Auto-connecting HID host to %s (direct match)", settings::addrToHex(r.addr).c_str());
|
||||||
hidHostConnect(r.addr);
|
hidHostConnect(r.addr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device not in the last scan. If we have an autoConnect HID host peer, restart
|
// 2. RPA / name fallback: some peripherals use Resolvable Private Addresses.
|
||||||
// scanning so we keep checking until the device powers back on.
|
// Their advertised address (RPA) does not equal the stored identity address,
|
||||||
auto peers = settings::loadAll();
|
// so hex-lookup fails. If we see a scan result whose name matches a stored
|
||||||
for (const auto& peer : peers) {
|
// auto-connect peer, attempt a direct connection to the stored identity address.
|
||||||
if (peer.autoConnect && peer.profileId == BT_PROFILE_HID_HOST) {
|
// The controller's resolving list (populated from NVS IRK) will resolve the RPA.
|
||||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
for (const auto& r : scan) {
|
||||||
if (!bluetooth_is_scanning(dev)) {
|
if (r.name.empty()) continue;
|
||||||
LOG_I(TAG, "Auto-connect HID host: device not in scan, retrying scan");
|
for (const auto& stored : auto_peers) {
|
||||||
bluetooth_scan_start(dev);
|
if (!stored.name.empty() && stored.name == r.name) {
|
||||||
}
|
LOG_I(TAG, "Auto-connecting HID host to %s via name match '%s' (RPA handling: scan=%s stored=%s)",
|
||||||
|
settings::addrToHex(stored.addr).c_str(),
|
||||||
|
r.name.c_str(),
|
||||||
|
settings::addrToHex(r.addr).c_str(),
|
||||||
|
settings::addrToHex(stored.addr).c_str());
|
||||||
|
hidHostConnect(stored.addr);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Direct connect fallback: if device not in scan (or uses RPA without name in adv),
|
||||||
|
// try to connect directly to the first stored auto peer. ble_gap_connect() will
|
||||||
|
// internally scan and use the resolving list to match RPA to identity.
|
||||||
|
// This also covers the case where the keyboard is powered off and later on.
|
||||||
|
LOG_I(TAG, "Auto-connect HID host: %d auto peer(s) not in scan, trying direct connect to %s",
|
||||||
|
(int)auto_peers.size(), settings::addrToHex(auto_peers[0].addr).c_str());
|
||||||
|
hidHostConnect(auto_peers[0].addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace tt::bluetooth
|
} // namespace tt::bluetooth
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ constexpr auto* TAG = "BluetoothPairedDevice";
|
|||||||
|
|
||||||
// Use the same directory as the old service for backward compatibility.
|
// Use the same directory as the old service for backward compatibility.
|
||||||
constexpr auto* DEVICE_SETTINGS_FORMAT = "{}/{}.device.properties";
|
constexpr auto* DEVICE_SETTINGS_FORMAT = "{}/{}.device.properties";
|
||||||
constexpr auto* KEY_NAME = "name";
|
constexpr auto* KEY_NAME = "name";
|
||||||
constexpr auto* KEY_ADDR = "addr";
|
constexpr auto* KEY_ADDR = "addr";
|
||||||
constexpr auto* KEY_AUTO_CONNECT = "autoConnect";
|
constexpr auto* KEY_AUTO_CONNECT = "autoConnect";
|
||||||
constexpr auto* KEY_PROFILE_ID = "profileId";
|
constexpr auto* KEY_PROFILE_ID = "profileId";
|
||||||
|
constexpr auto* KEY_ADDR_TYPE = "addrType";
|
||||||
|
|
||||||
static std::string getSettingsFilePath() {
|
static std::string getSettingsFilePath() {
|
||||||
return getUserDataPath() + "/service/bluetooth";
|
return getUserDataPath() + "/service/bluetooth";
|
||||||
@@ -78,8 +79,23 @@ bool load(const std::string& addr_hex, PairedDevice& device) {
|
|||||||
device.autoConnect = !map.contains(KEY_AUTO_CONNECT) || (map[KEY_AUTO_CONNECT] == "true");
|
device.autoConnect = !map.contains(KEY_AUTO_CONNECT) || (map[KEY_AUTO_CONNECT] == "true");
|
||||||
|
|
||||||
if (map.contains(KEY_PROFILE_ID)) {
|
if (map.contains(KEY_PROFILE_ID)) {
|
||||||
// TODO: Handle incorrect parsing input
|
char* endPtr = nullptr;
|
||||||
device.profileId = std::stoi(map[KEY_PROFILE_ID]);
|
long val = std::strtol(map[KEY_PROFILE_ID].c_str(), &endPtr, 10);
|
||||||
|
if (endPtr != map[KEY_PROFILE_ID].c_str()) {
|
||||||
|
device.profileId = static_cast<int>(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.contains(KEY_ADDR_TYPE)) {
|
||||||
|
char* endPtr = nullptr;
|
||||||
|
long val = std::strtol(map[KEY_ADDR_TYPE].c_str(), &endPtr, 10);
|
||||||
|
if (endPtr != map[KEY_ADDR_TYPE].c_str() && val >= 0 && val <= 255) {
|
||||||
|
device.addrType = static_cast<uint8_t>(val);
|
||||||
|
} else {
|
||||||
|
device.addrType = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
device.addrType = 0; // backward compat: assume PUBLIC
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -91,12 +107,17 @@ bool save(const PairedDevice& device) {
|
|||||||
map[KEY_ADDR] = addr_hex;
|
map[KEY_ADDR] = addr_hex;
|
||||||
map[KEY_AUTO_CONNECT] = device.autoConnect ? "true" : "false";
|
map[KEY_AUTO_CONNECT] = device.autoConnect ? "true" : "false";
|
||||||
map[KEY_PROFILE_ID] = std::to_string(device.profileId);
|
map[KEY_PROFILE_ID] = std::to_string(device.profileId);
|
||||||
|
map[KEY_ADDR_TYPE] = std::to_string(device.addrType);
|
||||||
auto file_path = getFilePath(addr_hex);
|
auto file_path = getFilePath(addr_hex);
|
||||||
|
LOG_I(TAG, "Saving device file %s profile=%d auto=%d type=%d",
|
||||||
|
file_path.c_str(), device.profileId, (int)device.autoConnect, (int)device.addrType);
|
||||||
if (!file::findOrCreateParentDirectory(file_path, 0755)) {
|
if (!file::findOrCreateParentDirectory(file_path, 0755)) {
|
||||||
LOG_E(TAG, "Failed to create parent dir for %s", file_path.c_str());
|
LOG_E(TAG, "Failed to create parent dir for %s", file_path.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return file::savePropertiesFile(file_path, map);
|
bool result = file::savePropertiesFile(file_path, map);
|
||||||
|
LOG_I(TAG, "SavePropertiesFile result for %s: %d", file_path.c_str(), (int)result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool remove(const std::string& addr_hex) {
|
bool remove(const std::string& addr_hex) {
|
||||||
@@ -108,6 +129,7 @@ bool remove(const std::string& addr_hex) {
|
|||||||
std::vector<PairedDevice> loadAll() {
|
std::vector<PairedDevice> loadAll() {
|
||||||
std::vector<dirent> entries;
|
std::vector<dirent> entries;
|
||||||
if (!file::isDirectory(getSettingsFilePath())) {
|
if (!file::isDirectory(getSettingsFilePath())) {
|
||||||
|
LOG_I(TAG, "loadAll: directory %s does not exist", getSettingsFilePath().c_str());
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
file::scandir(getSettingsFilePath(), entries, [](const dirent* entry) -> int {
|
file::scandir(getSettingsFilePath(), entries, [](const dirent* entry) -> int {
|
||||||
@@ -116,6 +138,7 @@ std::vector<PairedDevice> loadAll() {
|
|||||||
return name.ends_with(".device.properties") ? 0 : -1;
|
return name.ends_with(".device.properties") ? 0 : -1;
|
||||||
}, nullptr);
|
}, nullptr);
|
||||||
|
|
||||||
|
LOG_I(TAG, "loadAll: found %d entries in %s", (int)entries.size(), getSettingsFilePath().c_str());
|
||||||
std::vector<PairedDevice> result;
|
std::vector<PairedDevice> result;
|
||||||
result.reserve(entries.size());
|
result.reserve(entries.size());
|
||||||
for (const auto& entry : entries) {
|
for (const auto& entry : entries) {
|
||||||
@@ -126,6 +149,8 @@ std::vector<PairedDevice> loadAll() {
|
|||||||
PairedDevice device;
|
PairedDevice device;
|
||||||
if (load(addr_hex, device)) {
|
if (load(addr_hex, device)) {
|
||||||
result.push_back(std::move(device));
|
result.push_back(std::move(device));
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "loadAll: failed to load %s", filename.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,172 @@
|
|||||||
|
#include <Tactility/network/Mdns.h>
|
||||||
|
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
|
||||||
|
#include <tactility/log.h>
|
||||||
|
#include <esp_wifi.h>
|
||||||
|
#include <esp_netif.h>
|
||||||
|
#include <lwip/ip4_addr.h>
|
||||||
|
#if CONFIG_LWIP_IPV6
|
||||||
|
#include <lwip/ip6_addr.h>
|
||||||
|
#endif
|
||||||
|
#include <mdns.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace tt::network::mdns {
|
||||||
|
|
||||||
|
constexpr auto* TAG = "Mdns";
|
||||||
|
|
||||||
|
static std::string ipAddrToString(const esp_ip_addr_t& ip) {
|
||||||
|
char buf[64];
|
||||||
|
if (ip.type == ESP_IPADDR_TYPE_V4) {
|
||||||
|
esp_ip4addr_ntoa(&ip.u_addr.ip4, buf, sizeof(buf));
|
||||||
|
} else {
|
||||||
|
#if CONFIG_LWIP_IPV6
|
||||||
|
ip6addr_ntoa_r(reinterpret_cast<const ip6_addr_t*>(&ip.u_addr.ip6), buf, sizeof(buf));
|
||||||
|
#else
|
||||||
|
snprintf(buf, sizeof(buf), "IPv6(not enabled)");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string normalizeHostname(std::string host) {
|
||||||
|
const std::string suffix = ".local";
|
||||||
|
if (host.size() > suffix.size() && host.compare(host.size() - suffix.size(), suffix.size(), suffix) == 0) {
|
||||||
|
host.erase(host.size() - suffix.size());
|
||||||
|
}
|
||||||
|
if (!host.empty() && host.back() == '.') {
|
||||||
|
host.pop_back();
|
||||||
|
}
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAvailable() {
|
||||||
|
char buf[64];
|
||||||
|
return mdns_hostname_get(buf) == ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Service convertResult(const mdns_result_t* r) {
|
||||||
|
Service s;
|
||||||
|
if (r->instance_name) s.instanceName = r->instance_name;
|
||||||
|
if (r->service_type) s.serviceType = r->service_type;
|
||||||
|
if (r->proto) s.proto = r->proto;
|
||||||
|
if (r->hostname) s.hostname = r->hostname;
|
||||||
|
s.port = r->port;
|
||||||
|
s.ttl = r->ttl;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < r->txt_count; i++) {
|
||||||
|
if (r->txt[i].key) {
|
||||||
|
std::string key = r->txt[i].key;
|
||||||
|
std::string value;
|
||||||
|
if (r->txt[i].value && r->txt_value_len) {
|
||||||
|
value = std::string(r->txt[i].value, r->txt_value_len[i]);
|
||||||
|
} else if (r->txt[i].value) {
|
||||||
|
value = r->txt[i].value;
|
||||||
|
}
|
||||||
|
s.txtRecords[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string firstV4;
|
||||||
|
for (mdns_ip_addr_t* a = r->addr; a != nullptr; a = a->next) {
|
||||||
|
std::string ipStr = ipAddrToString(a->addr);
|
||||||
|
if (!ipStr.empty()) {
|
||||||
|
s.addresses.push_back(ipStr);
|
||||||
|
if (firstV4.empty() && a->addr.type == ESP_IPADDR_TYPE_V4) {
|
||||||
|
firstV4 = ipStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!firstV4.empty()) {
|
||||||
|
s.primaryAddress = firstV4;
|
||||||
|
} else if (!s.addresses.empty()) {
|
||||||
|
s.primaryAddress = s.addresses.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool browse(const std::string& serviceType, const std::string& proto, uint32_t timeoutMs, size_t maxResults, std::vector<Service>& outResults) {
|
||||||
|
if (serviceType.empty() || proto.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outResults.clear();
|
||||||
|
|
||||||
|
mdns_result_t* results = nullptr;
|
||||||
|
esp_err_t err = mdns_query_ptr(serviceType.c_str(), proto.c_str(), timeoutMs, maxResults, &results);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
if (err == ESP_ERR_INVALID_STATE) {
|
||||||
|
LOG_W(TAG, "browse: mDNS not running");
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "browse %s.%s failed: %s", serviceType.c_str(), proto.c_str(), esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (mdns_result_t* r = results; r != nullptr; r = r->next) {
|
||||||
|
outResults.push_back(convertResult(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
mdns_query_results_free(results);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool resolveHostname(const std::string& hostname, uint32_t timeoutMs, std::string& outIp) {
|
||||||
|
std::string normalized = normalizeHostname(hostname);
|
||||||
|
if (normalized.empty()) return false;
|
||||||
|
|
||||||
|
outIp.clear();
|
||||||
|
|
||||||
|
esp_ip4_addr_t addr;
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
|
||||||
|
esp_err_t err = mdns_query_a(normalized.c_str(), timeoutMs, &addr);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
char buf[32];
|
||||||
|
esp_ip4addr_ntoa(&addr, buf, sizeof(buf));
|
||||||
|
outIp = buf;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if CONFIG_LWIP_IPV6
|
||||||
|
esp_ip6_addr_t addr6;
|
||||||
|
memset(&addr6, 0, sizeof(addr6));
|
||||||
|
err = mdns_query_aaaa(normalized.c_str(), timeoutMs, &addr6);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
char buf[64];
|
||||||
|
ip6addr_ntoa_r(reinterpret_cast<const ip6_addr_t*>(&addr6), buf, sizeof(buf));
|
||||||
|
outIp = buf;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tt::network::mdns
|
||||||
|
|
||||||
|
#else // !ESP_PLATFORM — POSIX simulator stub
|
||||||
|
|
||||||
|
namespace tt::network::mdns {
|
||||||
|
|
||||||
|
bool isAvailable() { return false; }
|
||||||
|
|
||||||
|
bool browse(const std::string& serviceType, const std::string& proto, uint32_t timeoutMs, size_t maxResults, std::vector<Service>& outResults) {
|
||||||
|
(void)serviceType; (void)proto; (void)timeoutMs; (void)maxResults;
|
||||||
|
outResults.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool resolveHostname(const std::string& hostname, uint32_t timeoutMs, std::string& outIp) {
|
||||||
|
(void)hostname; (void)timeoutMs;
|
||||||
|
outIp.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tt::network::mdns
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
|
#include <tactility/log.h>
|
||||||
|
|
||||||
#include <Tactility/service/displayidle/DisplayIdleService.h>
|
#include <Tactility/service/displayidle/DisplayIdleService.h>
|
||||||
|
|
||||||
@@ -7,14 +8,16 @@
|
|||||||
#include "MatrixRainScreensaver.h"
|
#include "MatrixRainScreensaver.h"
|
||||||
#include "MystifyScreensaver.h"
|
#include "MystifyScreensaver.h"
|
||||||
#include "StackChanScreensaver.h"
|
#include "StackChanScreensaver.h"
|
||||||
|
#include "McpScreensaver.h"
|
||||||
|
|
||||||
#include <tactility/log.h>
|
|
||||||
#include <Tactility/CoreDefines.h>
|
#include <Tactility/CoreDefines.h>
|
||||||
#include <Tactility/hal/display/DisplayDevice.h>
|
#include <Tactility/hal/display/DisplayDevice.h>
|
||||||
|
#include <Tactility/hal/power/PowerDevice.h>
|
||||||
#include <Tactility/lvgl/LvglSync.h>
|
#include <Tactility/lvgl/LvglSync.h>
|
||||||
#include <Tactility/service/ServiceContext.h>
|
#include <Tactility/service/ServiceContext.h>
|
||||||
#include <Tactility/service/ServiceManifest.h>
|
#include <Tactility/service/ServiceManifest.h>
|
||||||
#include <Tactility/service/ServiceRegistration.h>
|
#include <Tactility/service/ServiceRegistration.h>
|
||||||
|
#include <Tactility/mcp/McpSystem.h>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
|
||||||
@@ -28,19 +31,28 @@ static std::shared_ptr<hal::display::DisplayDevice> getDisplay() {
|
|||||||
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
|
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayIdleService::stopScreensaverCb(lv_event_t* e) {
|
static bool isDeviceCharging() {
|
||||||
auto* self = static_cast<DisplayIdleService*>(lv_event_get_user_data(e));
|
bool charging = false;
|
||||||
lv_event_stop_bubbling(e);
|
hal::findDevices<hal::power::PowerDevice>(hal::Device::Type::Power, [&charging](const auto& power) {
|
||||||
self->stopScreensaverRequested.store(true, std::memory_order_release);
|
if (!power->supportsMetric(hal::power::PowerDevice::MetricType::IsCharging)) {
|
||||||
lv_display_trigger_activity(nullptr);
|
return true; // continue
|
||||||
|
}
|
||||||
|
hal::power::PowerDevice::MetricData data;
|
||||||
|
if (power->getMetric(hal::power::PowerDevice::MetricType::IsCharging, data) && data.valueAsBool) {
|
||||||
|
charging = true;
|
||||||
|
return false; // stop iter
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return charging;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayIdleService::stopScreensaver() {
|
void DisplayIdleService::stopScreensaverCb(lv_event_t* e) {
|
||||||
if (!lvgl::lock(100)) {
|
auto* self = static_cast<DisplayIdleService*>(lv_event_get_user_data(e));
|
||||||
// Lock failed - keep flag set to retry on next tick
|
self->stopScreensaverLocked();
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
void DisplayIdleService::stopScreensaverLocked() {
|
||||||
const auto restoreDuty = cachedDisplaySettings.backlightDuty;
|
const auto restoreDuty = cachedDisplaySettings.backlightDuty;
|
||||||
const bool wasDimmed = displayDimmed;
|
const bool wasDimmed = displayDimmed;
|
||||||
|
|
||||||
@@ -52,7 +64,6 @@ void DisplayIdleService::stopScreensaver() {
|
|||||||
lv_obj_delete(screensaverOverlay);
|
lv_obj_delete(screensaverOverlay);
|
||||||
screensaverOverlay = nullptr;
|
screensaverOverlay = nullptr;
|
||||||
}
|
}
|
||||||
lvgl::unlock();
|
|
||||||
stopScreensaverRequested.store(false, std::memory_order_relaxed);
|
stopScreensaverRequested.store(false, std::memory_order_relaxed);
|
||||||
|
|
||||||
// Reset auto-off state
|
// Reset auto-off state
|
||||||
@@ -67,6 +78,15 @@ void DisplayIdleService::stopScreensaver() {
|
|||||||
displayDimmed = wasDimmed ? false : displayDimmed;
|
displayDimmed = wasDimmed ? false : displayDimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DisplayIdleService::stopScreensaver() {
|
||||||
|
if (!lvgl::lock(100)) {
|
||||||
|
// Lock failed - keep flag set to retry on next tick
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopScreensaverLocked();
|
||||||
|
lvgl::unlock();
|
||||||
|
}
|
||||||
|
|
||||||
void DisplayIdleService::activateScreensaver() {
|
void DisplayIdleService::activateScreensaver() {
|
||||||
lv_obj_t* top = lv_layer_top();
|
lv_obj_t* top = lv_layer_top();
|
||||||
|
|
||||||
@@ -103,6 +123,9 @@ void DisplayIdleService::activateScreensaver() {
|
|||||||
case settings::display::ScreensaverType::StackChan:
|
case settings::display::ScreensaverType::StackChan:
|
||||||
screensaver = std::make_unique<StackChanScreensaver>();
|
screensaver = std::make_unique<StackChanScreensaver>();
|
||||||
break;
|
break;
|
||||||
|
case settings::display::ScreensaverType::McpScreen:
|
||||||
|
screensaver = std::make_unique<McpScreensaver>();
|
||||||
|
break;
|
||||||
case settings::display::ScreensaverType::None:
|
case settings::display::ScreensaverType::None:
|
||||||
default:
|
default:
|
||||||
// Just black screen, no animated screensaver
|
// Just black screen, no animated screensaver
|
||||||
@@ -124,6 +147,16 @@ void DisplayIdleService::updateScreensaver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DisplayIdleService::tick() {
|
void DisplayIdleService::tick() {
|
||||||
|
// Check if MCP override is active — must not be auto-stopped by idle logic
|
||||||
|
// READ OUTSIDE LVGL lock to avoid lock inversion:
|
||||||
|
// MCP video task locks mutex -> LVGL, we must NOT do LVGL -> mutex
|
||||||
|
bool isMcpActive = false;
|
||||||
|
{
|
||||||
|
auto& st = mcp::getState();
|
||||||
|
std::lock_guard<std::mutex> lk(st.mutex);
|
||||||
|
isMcpActive = (st.drawArea != nullptr) || st.overrideActive;
|
||||||
|
}
|
||||||
|
|
||||||
if (!lvgl::lock(100)) {
|
if (!lvgl::lock(100)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -138,11 +171,10 @@ void DisplayIdleService::tick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t inactive_ms = 0;
|
uint32_t inactive_ms = 0;
|
||||||
|
|
||||||
inactive_ms = lv_display_get_inactive_time(nullptr);
|
inactive_ms = lv_display_get_inactive_time(nullptr);
|
||||||
|
|
||||||
// Only update if not stopping (prevents lag on touch)
|
// Only update if not stopping (prevents lag on touch) — skip for MCP (no animation)
|
||||||
if (displayDimmed && screensaverOverlay && !stopScreensaverRequested.load(std::memory_order_acquire)) {
|
if (displayDimmed && screensaverOverlay && !stopScreensaverRequested.load(std::memory_order_acquire) && !isMcpActive) {
|
||||||
// Check if screensaver should auto-off after 5 minutes
|
// Check if screensaver should auto-off after 5 minutes
|
||||||
if (!backlightOff) {
|
if (!backlightOff) {
|
||||||
screensaverActiveCounter++;
|
screensaverActiveCounter++;
|
||||||
@@ -172,25 +204,48 @@ void DisplayIdleService::tick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto display = getDisplay();
|
auto display = getDisplay();
|
||||||
if (display != nullptr && display->supportsBacklightDuty()) {
|
bool supportsBacklight = display != nullptr && display->supportsBacklightDuty();
|
||||||
if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) {
|
|
||||||
if (displayDimmed) {
|
if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) {
|
||||||
|
// Timeout disabled (Never): ensure we restore if we were dimmed, regardless of display type
|
||||||
|
if (displayDimmed && !isMcpActive) {
|
||||||
|
if (supportsBacklight && display != nullptr) {
|
||||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||||
displayDimmed = false;
|
|
||||||
}
|
}
|
||||||
} else {
|
displayDimmed = false;
|
||||||
if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) {
|
}
|
||||||
|
} else if (supportsBacklight) {
|
||||||
|
// For backlight-capable displays: full idle handling
|
||||||
|
bool charging_blocks = cachedDisplaySettings.disableScreensaverWhenCharging && isDeviceCharging();
|
||||||
|
|
||||||
|
if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) {
|
||||||
|
if (charging_blocks) {
|
||||||
|
// Skip screensaver while charging
|
||||||
|
} else {
|
||||||
if (!lvgl::lock(100)) {
|
if (!lvgl::lock(100)) {
|
||||||
return; // Retry on next tick
|
return; // Retry on next tick
|
||||||
}
|
}
|
||||||
activateScreensaver();
|
activateScreensaver();
|
||||||
lvgl::unlock();
|
lvgl::unlock();
|
||||||
// Turn off backlight for "None" screensaver (just black screen)
|
|
||||||
if (cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
|
if (cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
|
||||||
display->setBacklightDuty(0);
|
if (display != nullptr) {
|
||||||
|
display->setBacklightDuty(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
displayDimmed = true;
|
displayDimmed = true;
|
||||||
} else if (displayDimmed && (inactive_ms < kWakeActivityThresholdMs)) {
|
}
|
||||||
|
} else if (displayDimmed && !isMcpActive) {
|
||||||
|
if (inactive_ms < kWakeActivityThresholdMs) {
|
||||||
|
stopScreensaver();
|
||||||
|
} else if (charging_blocks) {
|
||||||
|
stopScreensaver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For monochrome/RLCD (no backlight): don't auto-enter screensaver (heavy full_refresh SPI causes freeze)
|
||||||
|
// Only handle wake if we are somehow dimmed (e.g. MCP left it)
|
||||||
|
if (displayDimmed && !isMcpActive) {
|
||||||
|
if (inactive_ms < kWakeActivityThresholdMs) {
|
||||||
stopScreensaver();
|
stopScreensaver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,6 +258,11 @@ bool DisplayIdleService::onStart(ServiceContext& service) {
|
|||||||
|
|
||||||
cachedDisplaySettings = settings::display::loadOrGetDefault();
|
cachedDisplaySettings = settings::display::loadOrGetDefault();
|
||||||
|
|
||||||
|
auto display = getDisplay();
|
||||||
|
if (display != nullptr && !display->supportsBacklightDuty()) {
|
||||||
|
LOG_I(TAG, "Monochrome/RLCD display detected (no backlight control): idle timer will run but auto-backlight off is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::millisToTicks(TICK_INTERVAL_MS), [this]{ this->tick(); });
|
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::millisToTicks(TICK_INTERVAL_MS), [this]{ this->tick(); });
|
||||||
timer->setCallbackPriority(Thread::Priority::Lower);
|
timer->setCallbackPriority(Thread::Priority::Lower);
|
||||||
timer->start();
|
timer->start();
|
||||||
@@ -259,6 +319,62 @@ void DisplayIdleService::reloadSettings() {
|
|||||||
settingsReloadRequested.store(true, std::memory_order_release);
|
settingsReloadRequested.store(true, std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DisplayIdleService::startMcpScreensaver() {
|
||||||
|
if (!lvgl::lock(200)) {
|
||||||
|
LOG_W(TAG, "startMcpScreensaver: failed to acquire LVGL lock");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screensaverOverlay != nullptr) {
|
||||||
|
// Screensaver already active — if drawArea is registered we're done,
|
||||||
|
// otherwise stop the current one so we can replace it with McpScreensaver.
|
||||||
|
const auto& mcpState = mcp::getState();
|
||||||
|
if (mcpState.drawArea != nullptr) {
|
||||||
|
lvgl::unlock();
|
||||||
|
return; // McpScreensaver already running
|
||||||
|
}
|
||||||
|
// Wrong screensaver type active — tear it down first
|
||||||
|
if (screensaver) {
|
||||||
|
screensaver->stop();
|
||||||
|
screensaver.reset();
|
||||||
|
}
|
||||||
|
lv_obj_delete(screensaverOverlay);
|
||||||
|
screensaverOverlay = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
screensaverActiveCounter = 0;
|
||||||
|
backlightOff = false;
|
||||||
|
|
||||||
|
// Ensure backlight is active if the display supports it
|
||||||
|
auto display = getDisplay();
|
||||||
|
if (display != nullptr && display->supportsBacklightDuty()) {
|
||||||
|
uint8_t duty = cachedDisplaySettings.backlightDuty;
|
||||||
|
if (duty == 0) duty = 255; // ensure visible if settings not loaded / default
|
||||||
|
display->setBacklightDuty(duty);
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr);
|
||||||
|
|
||||||
|
lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
|
||||||
|
|
||||||
|
lv_obj_t* top = lv_layer_top();
|
||||||
|
screensaverOverlay = lv_obj_create(top);
|
||||||
|
lv_obj_remove_style_all(screensaverOverlay);
|
||||||
|
lv_obj_set_size(screensaverOverlay, LV_PCT(100), LV_PCT(100));
|
||||||
|
lv_obj_set_pos(screensaverOverlay, 0, 0);
|
||||||
|
lv_obj_set_style_bg_color(screensaverOverlay, lv_color_black(), 0);
|
||||||
|
lv_obj_set_style_bg_opa(screensaverOverlay, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_add_flag(screensaverOverlay, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_event_cb(screensaverOverlay, stopScreensaverCb, LV_EVENT_CLICKED, this);
|
||||||
|
|
||||||
|
screensaver = std::make_unique<McpScreensaver>();
|
||||||
|
screensaver->start(screensaverOverlay, screenW, screenH);
|
||||||
|
|
||||||
|
lvgl::unlock();
|
||||||
|
displayDimmed = true;
|
||||||
|
LOG_I(TAG, "MCP screensaver activated");
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<DisplayIdleService> findService() {
|
std::shared_ptr<DisplayIdleService> findService() {
|
||||||
return std::static_pointer_cast<DisplayIdleService>(
|
return std::static_pointer_cast<DisplayIdleService>(
|
||||||
findServiceById("DisplayIdle")
|
findServiceById("DisplayIdle")
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
|
||||||
|
#include "McpScreensaver.h"
|
||||||
|
#include <Tactility/mcp/McpSystem.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
|
|
||||||
|
constexpr auto* TAG = "McpScreensaver";
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
|
||||||
|
namespace tt::service::displayidle {
|
||||||
|
|
||||||
|
|
||||||
|
void McpScreensaver::start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) {
|
||||||
|
auto& state = mcp::getState();
|
||||||
|
|
||||||
|
// Full-screen canvas on the overlay
|
||||||
|
lv_obj_t* canvas = lv_canvas_create(overlay);
|
||||||
|
lv_obj_set_size(canvas, screenW, screenH);
|
||||||
|
lv_obj_set_pos(canvas, 0, 0);
|
||||||
|
lv_obj_set_style_radius(canvas, 0, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_border_width(canvas, 0, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_pad_all(canvas, 0, LV_PART_MAIN);
|
||||||
|
lv_obj_remove_flag(canvas, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
// Allocate framebuffer (prefer SPIRAM)
|
||||||
|
size_t requiredSize = (size_t)screenW * screenH * sizeof(uint16_t);
|
||||||
|
framebuffer = (uint16_t*)heap_caps_malloc(requiredSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||||
|
if (framebuffer == nullptr) {
|
||||||
|
framebuffer = (uint16_t*)heap_caps_malloc(requiredSize, MALLOC_CAP_8BIT);
|
||||||
|
}
|
||||||
|
framebufferSize = (framebuffer != nullptr) ? requiredSize : 0;
|
||||||
|
|
||||||
|
if (framebuffer == nullptr) {
|
||||||
|
LOG_E(TAG, "Failed to allocate %uB framebuffer", (unsigned)requiredSize);
|
||||||
|
lv_obj_t* err = lv_label_create(canvas);
|
||||||
|
lv_label_set_text(err, "Framebuffer alloc failed");
|
||||||
|
lv_obj_center(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill with a dark slate background (inverted for display path)
|
||||||
|
size_t pixelCount = (size_t)screenW * screenH;
|
||||||
|
for (size_t i = 0; i < pixelCount; ++i) {
|
||||||
|
framebuffer[i] = ~0x18E3; // dark blue-grey
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_canvas_set_buffer(canvas, framebuffer, screenW, screenH, LV_COLOR_FORMAT_RGB565);
|
||||||
|
|
||||||
|
// Waiting label (removed on first MCP draw via lv_obj_clean)
|
||||||
|
lv_obj_t* waitLabel = lv_label_create(canvas);
|
||||||
|
lv_label_set_text(waitLabel, "Waiting for LLM...");
|
||||||
|
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_t* resLabel = lv_label_create(canvas);
|
||||||
|
lv_label_set_text_fmt(resLabel, "Display: %dx%d", (int)screenW, (int)screenH);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Register with McpSystemState
|
||||||
|
std::lock_guard<std::mutex> lock(state.mutex);
|
||||||
|
state.drawArea = canvas;
|
||||||
|
state.framebuffer = framebuffer;
|
||||||
|
state.framebufferSize = framebufferSize;
|
||||||
|
state.displayWidth = (uint16_t)screenW;
|
||||||
|
state.displayHeight = (uint16_t)screenH;
|
||||||
|
state.drawWidth = (uint16_t)screenW;
|
||||||
|
state.drawHeight = (uint16_t)screenH;
|
||||||
|
// Don't reset overrideActive — if the LLM already drew, we keep the content
|
||||||
|
|
||||||
|
LOG_I(TAG, "McpScreensaver started (%dx%d)", (int)screenW, (int)screenH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void McpScreensaver::stop() {
|
||||||
|
auto& state = mcp::getState();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(state.mutex);
|
||||||
|
state.drawArea = nullptr;
|
||||||
|
state.framebuffer = nullptr;
|
||||||
|
state.framebufferSize = 0;
|
||||||
|
state.overrideActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (framebuffer != nullptr) {
|
||||||
|
heap_caps_free(framebuffer);
|
||||||
|
framebuffer = nullptr;
|
||||||
|
framebufferSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_I(TAG, "McpScreensaver stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
void McpScreensaver::update(lv_coord_t /*screenW*/, lv_coord_t /*screenH*/) {
|
||||||
|
// MCP draws on demand via HTTP — no per-frame animation needed
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tt::service::displayidle
|
||||||
|
|
||||||
|
#endif // ESP_PLATFORM
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
|
||||||
|
#include "Screensaver.h"
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace tt::service::displayidle {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Screen screensaver.
|
||||||
|
* Creates a full-screen LVGL canvas on the overlay and registers it in
|
||||||
|
* McpSystemState so that MCP HTTP draw commands can paint to it.
|
||||||
|
* Dismissed by a touch event (handled by the parent DisplayIdle overlay).
|
||||||
|
*/
|
||||||
|
class McpScreensaver final : public Screensaver {
|
||||||
|
uint16_t* framebuffer = nullptr;
|
||||||
|
size_t framebufferSize = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
McpScreensaver() = default;
|
||||||
|
~McpScreensaver() override = default;
|
||||||
|
|
||||||
|
void start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) override;
|
||||||
|
void stop() override;
|
||||||
|
void update(lv_coord_t screenW, lv_coord_t screenH) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace tt::service::displayidle
|
||||||
|
|
||||||
|
#endif // ESP_PLATFORM
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@
|
|||||||
#include <Tactility/service/webserver/AssetVersion.h>
|
#include <Tactility/service/webserver/AssetVersion.h>
|
||||||
#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/mcp/McpSystem.h>
|
||||||
#include <Tactility/MountPoints.h>
|
#include <Tactility/MountPoints.h>
|
||||||
#include <Tactility/file/File.h>
|
#include <Tactility/file/File.h>
|
||||||
#include <Tactility/lvgl/Statusbar.h>
|
#include <Tactility/lvgl/Statusbar.h>
|
||||||
@@ -44,6 +46,8 @@
|
|||||||
#include <esp_system.h>
|
#include <esp_system.h>
|
||||||
#include <esp_vfs_fat.h>
|
#include <esp_vfs_fat.h>
|
||||||
#include <esp_wifi.h>
|
#include <esp_wifi.h>
|
||||||
|
#include <mdns.h>
|
||||||
|
#include <esp_mac.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
@@ -215,7 +219,8 @@ bool WebServerService::onStart(ServiceContext& service) {
|
|||||||
lock.lock();
|
lock.lock();
|
||||||
g_cachedSettings = settings::webserver::loadOrGetDefault();
|
g_cachedSettings = settings::webserver::loadOrGetDefault();
|
||||||
g_settingsCached = true;
|
g_settingsCached = true;
|
||||||
serverEnabled = g_cachedSettings.webServerEnabled;
|
auto mcpSettings = settings::mcp::loadOrGetDefault();
|
||||||
|
serverEnabled = g_cachedSettings.webServerEnabled || mcpSettings.mcpEnabled;
|
||||||
}
|
}
|
||||||
// Subscribe to settings change events to refresh cache
|
// Subscribe to settings change events to refresh cache
|
||||||
settingsEventSubscription = pubsub->subscribe([](WebServerEvent event) {
|
settingsEventSubscription = pubsub->subscribe([](WebServerEvent event) {
|
||||||
@@ -259,13 +264,17 @@ void WebServerService::onStop(ServiceContext& service) {
|
|||||||
void WebServerService::setEnabled(bool enabled) {
|
void WebServerService::setEnabled(bool enabled) {
|
||||||
auto lock = mutex.asScopedLock();
|
auto lock = mutex.asScopedLock();
|
||||||
lock.lock();
|
lock.lock();
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (!httpServer || !httpServer->isStarted()) {
|
if (!httpServer || !httpServer->isStarted()) {
|
||||||
startServer();
|
startServer();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (httpServer && httpServer->isStarted()) {
|
// Stop only if both web server and MCP are disabled
|
||||||
|
auto wsSettings = settings::webserver::loadOrGetDefault();
|
||||||
|
auto mcpSettings = settings::mcp::loadOrGetDefault();
|
||||||
|
bool anyEnabled = wsSettings.webServerEnabled || mcpSettings.mcpEnabled;
|
||||||
|
if (!anyEnabled && httpServer && httpServer->isStarted()) {
|
||||||
stopServer();
|
stopServer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,6 +394,27 @@ bool WebServerService::startApMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOG_I(TAG, "WiFi AP started - SSID: '%s', Channel: %u, IP: 192.168.4.1", settings.apSsid.c_str(), (unsigned)settings.apChannel);
|
LOG_I(TAG, "WiFi AP started - SSID: '%s', Channel: %u, IP: 192.168.4.1", settings.apSsid.c_str(), (unsigned)settings.apChannel);
|
||||||
|
|
||||||
|
// mDNS for AP mode too - kidsOS-XXXX.local
|
||||||
|
{
|
||||||
|
uint8_t mac[6];
|
||||||
|
char hostname[32];
|
||||||
|
if (esp_wifi_get_mac(WIFI_IF_AP, mac) == ESP_OK) {
|
||||||
|
snprintf(hostname, sizeof(hostname), "kidsOS-%02X%02X", mac[4], mac[5]);
|
||||||
|
} else {
|
||||||
|
strncpy(hostname, "kidsOS", sizeof(hostname));
|
||||||
|
}
|
||||||
|
esp_netif_set_hostname(apNetif, hostname);
|
||||||
|
esp_err_t mdns_err = mdns_init();
|
||||||
|
if (mdns_err == ESP_OK) {
|
||||||
|
LOG_I(TAG, "mDNS AP initialized, hostname '%s.local'", hostname);
|
||||||
|
mdns_hostname_set(hostname);
|
||||||
|
mdns_instance_name_set("kidsOS Tactility Device");
|
||||||
|
mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0);
|
||||||
|
mdns_service_add(NULL, "_tactility", "_tcp", 6666, NULL, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,6 +544,11 @@ bool WebServerService::startServer() {
|
|||||||
LOG_I(TAG, "HTTP server started successfully on port %u", (unsigned)settings.webServerPort);
|
LOG_I(TAG, "HTTP server started successfully on port %u", (unsigned)settings.webServerPort);
|
||||||
publish_event(this, WebServerEvent::WebServerStarted);
|
publish_event(this, WebServerEvent::WebServerStarted);
|
||||||
|
|
||||||
|
auto mcpSettings = settings::mcp::loadOrGetDefault();
|
||||||
|
if (mcpSettings.mcpEnabled) {
|
||||||
|
mcp::startVideoStreamServer();
|
||||||
|
}
|
||||||
|
|
||||||
// Show statusbar icon
|
// Show statusbar icon
|
||||||
if (statusbarIconId >= 0) {
|
if (statusbarIconId >= 0) {
|
||||||
lvgl::statusbar_icon_set_image(statusbarIconId, LVGL_ICON_STATUSBAR_CLOUD);
|
lvgl::statusbar_icon_set_image(statusbarIconId, LVGL_ICON_STATUSBAR_CLOUD);
|
||||||
@@ -533,6 +568,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();
|
||||||
@@ -1025,15 +1062,24 @@ esp_err_t WebServerService::handleApiGet(httpd_req_t* request) {
|
|||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API POST dispatcher - all POST endpoints require authentication
|
// API POST dispatcher - all POST endpoints require authentication except MCP
|
||||||
esp_err_t WebServerService::handleApiPost(httpd_req_t* request) {
|
esp_err_t WebServerService::handleApiPost(httpd_req_t* request) {
|
||||||
|
const char* uri = request->uri;
|
||||||
|
|
||||||
|
// MCP endpoints are unauthenticated (local network)
|
||||||
|
if (strncmp(uri, "/api/mcp", 8) == 0) {
|
||||||
|
return handleApiMcp(request);
|
||||||
|
}
|
||||||
|
if (strncmp(uri, "/api/screen/raw", 15) == 0) {
|
||||||
|
return handleApiScreenRaw(request);
|
||||||
|
}
|
||||||
|
|
||||||
bool authPassed = false;
|
bool authPassed = false;
|
||||||
esp_err_t authResult = validateRequestAuth(request, authPassed);
|
esp_err_t authResult = validateRequestAuth(request, authPassed);
|
||||||
if (!authPassed) {
|
if (!authPassed) {
|
||||||
return authResult;
|
return authResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* uri = request->uri;
|
|
||||||
if (strncmp(uri, "/api/apps/run", 13) == 0) {
|
if (strncmp(uri, "/api/apps/run", 13) == 0) {
|
||||||
return handleApiAppsRun(request);
|
return handleApiAppsRun(request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty";
|
|||||||
constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled";
|
constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled";
|
||||||
constexpr auto* SETTINGS_KEY_TIMEOUT_MS = "backlightTimeoutMs";
|
constexpr auto* SETTINGS_KEY_TIMEOUT_MS = "backlightTimeoutMs";
|
||||||
constexpr auto* SETTINGS_KEY_SCREENSAVER_TYPE = "screensaverType";
|
constexpr auto* SETTINGS_KEY_SCREENSAVER_TYPE = "screensaverType";
|
||||||
|
constexpr auto* SETTINGS_KEY_DISABLE_WHEN_CHARGING = "disableScreensaverWhenCharging";
|
||||||
|
|
||||||
static Orientation getDefaultOrientation() {
|
static Orientation getDefaultOrientation() {
|
||||||
auto* display = lv_display_get_default();
|
auto* display = lv_display_get_default();
|
||||||
if (display == nullptr) {
|
if (display == nullptr) {
|
||||||
return Orientation::Landscape;
|
return Orientation::Landscape;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lv_display_get_physical_horizontal_resolution(display) > lv_display_get_physical_vertical_resolution(display)) {
|
if (lv_display_get_physical_horizontal_resolution(display) > lv_display_get_physical_vertical_resolution(display)) {
|
||||||
return Orientation::Landscape;
|
return Orientation::Landscape;
|
||||||
} else {
|
} else {
|
||||||
@@ -39,86 +39,51 @@ static Orientation getDefaultOrientation() {
|
|||||||
static std::string toString(Orientation orientation) {
|
static std::string toString(Orientation orientation) {
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
using enum Orientation;
|
using enum Orientation;
|
||||||
case Portrait:
|
case Portrait: return "Portrait";
|
||||||
return "Portrait";
|
case Landscape: return "Landscape";
|
||||||
case Landscape:
|
case PortraitFlipped: return "PortraitFlipped";
|
||||||
return "Landscape";
|
case LandscapeFlipped: return "LandscapeFlipped";
|
||||||
case PortraitFlipped:
|
default: std::unreachable();
|
||||||
return "PortraitFlipped";
|
|
||||||
case LandscapeFlipped:
|
|
||||||
return "LandscapeFlipped";
|
|
||||||
default:
|
|
||||||
std::unreachable();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool fromString(const std::string& str, Orientation& orientation) {
|
static bool fromString(const std::string& str, Orientation& orientation) {
|
||||||
if (str == "Portrait") {
|
if (str == "Portrait") { orientation = Orientation::Portrait; return true; }
|
||||||
orientation = Orientation::Portrait;
|
else if (str == "Landscape") { orientation = Orientation::Landscape; return true; }
|
||||||
return true;
|
else if (str == "PortraitFlipped") { orientation = Orientation::PortraitFlipped; return true; }
|
||||||
} else if (str == "Landscape") {
|
else if (str == "LandscapeFlipped") { orientation = Orientation::LandscapeFlipped; return true; }
|
||||||
orientation = Orientation::Landscape;
|
else { return false; }
|
||||||
return true;
|
|
||||||
} else if (str == "PortraitFlipped") {
|
|
||||||
orientation = Orientation::PortraitFlipped;
|
|
||||||
return true;
|
|
||||||
} else if (str == "LandscapeFlipped") {
|
|
||||||
orientation = Orientation::LandscapeFlipped;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string toString(ScreensaverType type) {
|
static std::string toString(ScreensaverType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
using enum ScreensaverType;
|
using enum ScreensaverType;
|
||||||
case None:
|
case None: return "None";
|
||||||
return "None";
|
case BouncingBalls: return "BouncingBalls";
|
||||||
case BouncingBalls:
|
case Mystify: return "Mystify";
|
||||||
return "BouncingBalls";
|
case MatrixRain: return "MatrixRain";
|
||||||
case Mystify:
|
case StackChan: return "StackChan";
|
||||||
return "Mystify";
|
case McpScreen: return "McpScreen";
|
||||||
case MatrixRain:
|
case Count: return "None";
|
||||||
return "MatrixRain";
|
default: std::unreachable();
|
||||||
case StackChan:
|
|
||||||
return "StackChan";
|
|
||||||
default:
|
|
||||||
std::unreachable();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool fromString(const std::string& str, ScreensaverType& type) {
|
static bool fromString(const std::string& str, ScreensaverType& type) {
|
||||||
if (str == "None") {
|
if (str == "None") { type = ScreensaverType::None; return true; }
|
||||||
type = ScreensaverType::None;
|
else if (str == "BouncingBalls") { type = ScreensaverType::BouncingBalls; return true; }
|
||||||
return true;
|
else if (str == "Mystify") { type = ScreensaverType::Mystify; return true; }
|
||||||
} else if (str == "BouncingBalls") {
|
else if (str == "MatrixRain") { type = ScreensaverType::MatrixRain; return true; }
|
||||||
type = ScreensaverType::BouncingBalls;
|
else if (str == "StackChan") { type = ScreensaverType::StackChan; return true; }
|
||||||
return true;
|
else if (str == "McpScreen") { type = ScreensaverType::McpScreen; return true; }
|
||||||
} else if (str == "Mystify") {
|
else { return false; }
|
||||||
type = ScreensaverType::Mystify;
|
|
||||||
return true;
|
|
||||||
} else if (str == "MatrixRain") {
|
|
||||||
type = ScreensaverType::MatrixRain;
|
|
||||||
return true;
|
|
||||||
} else if (str == "StackChan") {
|
|
||||||
type = ScreensaverType::StackChan;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool load(DisplaySettings& settings) {
|
bool load(DisplaySettings& settings) {
|
||||||
auto settings_path = getSettingsFilePath();
|
auto settings_path = getSettingsFilePath();
|
||||||
if (!file::isFile(settings_path)) {
|
if (!file::isFile(settings_path)) { return false; }
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::map<std::string, std::string> map;
|
std::map<std::string, std::string> map;
|
||||||
if (!file::loadPropertiesFile(settings_path, map)) {
|
if (!file::loadPropertiesFile(settings_path, map)) { return false; }
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto orientation_entry = map.find(SETTINGS_KEY_ORIENTATION);
|
auto orientation_entry = map.find(SETTINGS_KEY_ORIENTATION);
|
||||||
Orientation orientation;
|
Orientation orientation;
|
||||||
@@ -128,17 +93,13 @@ bool load(DisplaySettings& settings) {
|
|||||||
|
|
||||||
auto gamma_entry = map.find(SETTINGS_KEY_GAMMA_CURVE);
|
auto gamma_entry = map.find(SETTINGS_KEY_GAMMA_CURVE);
|
||||||
int gamma_curve = 0;
|
int gamma_curve = 0;
|
||||||
if (gamma_entry != map.end()) {
|
if (gamma_entry != map.end()) { gamma_curve = atoi(gamma_entry->second.c_str()); }
|
||||||
gamma_curve = atoi(gamma_entry->second.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto backlight_duty_entry = map.find(SETTINGS_KEY_BACKLIGHT_DUTY);
|
auto backlight_duty_entry = map.find(SETTINGS_KEY_BACKLIGHT_DUTY);
|
||||||
int backlight_duty = 200; // default
|
int backlight_duty = 200;
|
||||||
if (backlight_duty_entry != map.end()) {
|
if (backlight_duty_entry != map.end()) {
|
||||||
backlight_duty = atoi(backlight_duty_entry->second.c_str());
|
backlight_duty = atoi(backlight_duty_entry->second.c_str());
|
||||||
if (backlight_duty_entry->second != "0" && backlight_duty == 0) {
|
if (backlight_duty_entry->second != "0" && backlight_duty == 0) backlight_duty = 200;
|
||||||
backlight_duty = 200;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool timeout_enabled = false;
|
bool timeout_enabled = false;
|
||||||
@@ -147,7 +108,7 @@ bool load(DisplaySettings& settings) {
|
|||||||
timeout_enabled = (timeout_enabled_entry->second == "1" || timeout_enabled_entry->second == "true" || timeout_enabled_entry->second == "True");
|
timeout_enabled = (timeout_enabled_entry->second == "1" || timeout_enabled_entry->second == "true" || timeout_enabled_entry->second == "True");
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t timeout_ms = 60000; // default 60s
|
uint32_t timeout_ms = 60000;
|
||||||
auto timeout_ms_entry = map.find(SETTINGS_KEY_TIMEOUT_MS);
|
auto timeout_ms_entry = map.find(SETTINGS_KEY_TIMEOUT_MS);
|
||||||
if (timeout_ms_entry != map.end()) {
|
if (timeout_ms_entry != map.end()) {
|
||||||
timeout_ms = static_cast<uint32_t>(std::strtoul(timeout_ms_entry->second.c_str(), nullptr, 10));
|
timeout_ms = static_cast<uint32_t>(std::strtoul(timeout_ms_entry->second.c_str(), nullptr, 10));
|
||||||
@@ -155,8 +116,12 @@ bool load(DisplaySettings& settings) {
|
|||||||
|
|
||||||
auto screensaver_entry = map.find(SETTINGS_KEY_SCREENSAVER_TYPE);
|
auto screensaver_entry = map.find(SETTINGS_KEY_SCREENSAVER_TYPE);
|
||||||
ScreensaverType screensaver_type = ScreensaverType::BouncingBalls;
|
ScreensaverType screensaver_type = ScreensaverType::BouncingBalls;
|
||||||
if (screensaver_entry != map.end()) {
|
if (screensaver_entry != map.end()) { fromString(screensaver_entry->second, screensaver_type); }
|
||||||
fromString(screensaver_entry->second, screensaver_type);
|
|
||||||
|
bool disable_when_charging = false;
|
||||||
|
auto charging_entry = map.find(SETTINGS_KEY_DISABLE_WHEN_CHARGING);
|
||||||
|
if (charging_entry != map.end()) {
|
||||||
|
disable_when_charging = (charging_entry->second == "1" || charging_entry->second == "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.orientation = orientation;
|
settings.orientation = orientation;
|
||||||
@@ -165,7 +130,7 @@ bool load(DisplaySettings& settings) {
|
|||||||
settings.backlightTimeoutEnabled = timeout_enabled;
|
settings.backlightTimeoutEnabled = timeout_enabled;
|
||||||
settings.backlightTimeoutMs = timeout_ms;
|
settings.backlightTimeoutMs = timeout_ms;
|
||||||
settings.screensaverType = screensaver_type;
|
settings.screensaverType = screensaver_type;
|
||||||
|
settings.disableScreensaverWhenCharging = disable_when_charging;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,15 +141,14 @@ DisplaySettings getDefault() {
|
|||||||
.backlightDuty = 200,
|
.backlightDuty = 200,
|
||||||
.backlightTimeoutEnabled = false,
|
.backlightTimeoutEnabled = false,
|
||||||
.backlightTimeoutMs = 60000,
|
.backlightTimeoutMs = 60000,
|
||||||
.screensaverType = ScreensaverType::BouncingBalls
|
.screensaverType = ScreensaverType::BouncingBalls,
|
||||||
|
.disableScreensaverWhenCharging = false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplaySettings loadOrGetDefault() {
|
DisplaySettings loadOrGetDefault() {
|
||||||
DisplaySettings settings;
|
DisplaySettings settings;
|
||||||
if (!load(settings)) {
|
if (!load(settings)) { settings = getDefault(); }
|
||||||
settings = getDefault();
|
|
||||||
}
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,10 +160,9 @@ bool save(const DisplaySettings& settings) {
|
|||||||
map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0";
|
map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0";
|
||||||
map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs);
|
map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs);
|
||||||
map[SETTINGS_KEY_SCREENSAVER_TYPE] = toString(settings.screensaverType);
|
map[SETTINGS_KEY_SCREENSAVER_TYPE] = toString(settings.screensaverType);
|
||||||
|
map[SETTINGS_KEY_DISABLE_WHEN_CHARGING] = settings.disableScreensaverWhenCharging ? "1" : "0";
|
||||||
auto settings_path = getSettingsFilePath();
|
auto settings_path = getSettingsFilePath();
|
||||||
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
if (!file::findOrCreateParentDirectory(settings_path, 0755)) { return false; }
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return file::savePropertiesFile(settings_path, map);
|
return file::savePropertiesFile(settings_path, map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,40 +170,26 @@ lv_display_rotation_t toLvglDisplayRotation(Orientation orientation) {
|
|||||||
auto* lvgl_display = lv_display_get_default();
|
auto* lvgl_display = lv_display_get_default();
|
||||||
auto rotation = lv_display_get_rotation(lvgl_display);
|
auto rotation = lv_display_get_rotation(lvgl_display);
|
||||||
bool is_originally_landscape;
|
bool is_originally_landscape;
|
||||||
// The lvgl resolution code compensates for rotation. We have to revert the compensation to get the real display resolution
|
|
||||||
// TODO: Use info from display driver
|
|
||||||
if (rotation == LV_DISPLAY_ROTATION_0 || rotation == LV_DISPLAY_ROTATION_180) {
|
if (rotation == LV_DISPLAY_ROTATION_0 || rotation == LV_DISPLAY_ROTATION_180) {
|
||||||
is_originally_landscape = lv_display_get_physical_horizontal_resolution(lvgl_display) > lv_display_get_physical_vertical_resolution(lvgl_display);
|
is_originally_landscape = lv_display_get_physical_horizontal_resolution(lvgl_display) > lv_display_get_physical_vertical_resolution(lvgl_display);
|
||||||
} else {
|
} else {
|
||||||
is_originally_landscape = lv_display_get_physical_horizontal_resolution(lvgl_display) < lv_display_get_physical_vertical_resolution(lvgl_display);
|
is_originally_landscape = lv_display_get_physical_horizontal_resolution(lvgl_display) < lv_display_get_physical_vertical_resolution(lvgl_display);
|
||||||
}
|
}
|
||||||
if (is_originally_landscape) {
|
if (is_originally_landscape) {
|
||||||
// Landscape display
|
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case Orientation::Landscape:
|
case Orientation::Landscape: return LV_DISPLAY_ROTATION_0;
|
||||||
return LV_DISPLAY_ROTATION_0;
|
case Orientation::Portrait: return LV_DISPLAY_ROTATION_90;
|
||||||
case Orientation::Portrait:
|
case Orientation::LandscapeFlipped: return LV_DISPLAY_ROTATION_180;
|
||||||
return LV_DISPLAY_ROTATION_90;
|
case Orientation::PortraitFlipped: return LV_DISPLAY_ROTATION_270;
|
||||||
case Orientation::LandscapeFlipped:
|
default: return LV_DISPLAY_ROTATION_0;
|
||||||
return LV_DISPLAY_ROTATION_180;
|
|
||||||
case Orientation::PortraitFlipped:
|
|
||||||
return LV_DISPLAY_ROTATION_270;
|
|
||||||
default:
|
|
||||||
return LV_DISPLAY_ROTATION_0;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Portrait display
|
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case Orientation::Landscape:
|
case Orientation::Landscape: return LV_DISPLAY_ROTATION_90;
|
||||||
return LV_DISPLAY_ROTATION_90;
|
case Orientation::Portrait: return LV_DISPLAY_ROTATION_0;
|
||||||
case Orientation::Portrait:
|
case Orientation::LandscapeFlipped: return LV_DISPLAY_ROTATION_270;
|
||||||
return LV_DISPLAY_ROTATION_0;
|
case Orientation::PortraitFlipped: return LV_DISPLAY_ROTATION_180;
|
||||||
case Orientation::LandscapeFlipped:
|
default: return LV_DISPLAY_ROTATION_0;
|
||||||
return LV_DISPLAY_ROTATION_270;
|
|
||||||
case Orientation::PortraitFlipped:
|
|
||||||
return LV_DISPLAY_ROTATION_180;
|
|
||||||
default:
|
|
||||||
return LV_DISPLAY_ROTATION_0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#include <Tactility/settings/McpSettings.h>
|
||||||
|
#include <Tactility/file/PropertiesFile.h>
|
||||||
|
#include <Tactility/file/File.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
|
|
||||||
|
constexpr auto* TAG = "McpSettings";
|
||||||
|
#include <Tactility/Paths.h>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace tt::settings::mcp {
|
||||||
|
|
||||||
|
|
||||||
|
static std::string getSettingsFilePath() {
|
||||||
|
return getUserDataPath() + "/settings/mcp.properties";
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto* KEY_MCP_ENABLED = "mcpEnabled";
|
||||||
|
|
||||||
|
bool load(McpSettings& settings) {
|
||||||
|
auto settings_path = getSettingsFilePath();
|
||||||
|
if (!file::isFile(settings_path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::map<std::string, std::string> map;
|
||||||
|
if (!file::loadPropertiesFile(settings_path, map)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mcp_enabled = map.find(KEY_MCP_ENABLED);
|
||||||
|
settings.mcpEnabled = (mcp_enabled != map.end())
|
||||||
|
? (mcp_enabled->second == "1" || mcp_enabled->second == "true")
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
McpSettings getDefault() {
|
||||||
|
return McpSettings{
|
||||||
|
.mcpEnabled = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
McpSettings loadOrGetDefault() {
|
||||||
|
McpSettings settings;
|
||||||
|
if (!load(settings)) {
|
||||||
|
settings = getDefault();
|
||||||
|
if (!save(settings)) {
|
||||||
|
LOG_W(TAG, "Failed to save default MCP settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool save(const McpSettings& settings) {
|
||||||
|
std::map<std::string, std::string> map;
|
||||||
|
map[KEY_MCP_ENABLED] = settings.mcpEnabled ? "true" : "false";
|
||||||
|
|
||||||
|
auto settings_path = getSettingsFilePath();
|
||||||
|
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
||||||
|
LOG_E(TAG, "Failed to create parent dir for %s", settings_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file::savePropertiesFile(settings_path, map)) {
|
||||||
|
LOG_E(TAG, "Failed to save MCP settings to %s", settings_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TactilityC mDNS bindings — usable from external ELF apps.
|
||||||
|
*
|
||||||
|
* Provides synchronous mDNS browsing (PTR queries) and hostname resolution
|
||||||
|
* on top of the ESP-IDF mdns component. The API is intentionally C-only,
|
||||||
|
* string-copy based, to avoid complex lifetime issues across ELF boundaries.
|
||||||
|
*
|
||||||
|
* Implementation lives in Tactility (tt::network::mdns) and is exported via
|
||||||
|
* module symbols. TactilityC provides thin wrappers + symbol export.
|
||||||
|
*
|
||||||
|
* WiFi must be connected for mDNS queries to return results.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define TT_MDNS_MAX_RESULTS 32
|
||||||
|
#define TT_MDNS_MAX_ADDRESSES 4
|
||||||
|
#define TT_MDNS_MAX_TXT_RECORDS 8
|
||||||
|
|
||||||
|
#define TT_MDNS_HOSTNAME_LEN 64
|
||||||
|
#define TT_MDNS_INSTANCE_LEN 64
|
||||||
|
#define TT_MDNS_SERVICE_TYPE_LEN 32
|
||||||
|
#define TT_MDNS_PROTO_LEN 16
|
||||||
|
#define TT_MDNS_IP_LEN 64
|
||||||
|
#define TT_MDNS_TXT_KEY_LEN 32
|
||||||
|
#define TT_MDNS_TXT_VALUE_LEN 64
|
||||||
|
|
||||||
|
/** One TXT key-value pair */
|
||||||
|
typedef struct {
|
||||||
|
char key[TT_MDNS_TXT_KEY_LEN];
|
||||||
|
char value[TT_MDNS_TXT_VALUE_LEN];
|
||||||
|
} TtMdnsTxtRecord;
|
||||||
|
|
||||||
|
/** One discovered service (copy-based, no pointers into mdns_result_t). */
|
||||||
|
typedef struct {
|
||||||
|
char instanceName[TT_MDNS_INSTANCE_LEN]; ///< e.g. "kidsOS-AB12"
|
||||||
|
char serviceType[TT_MDNS_SERVICE_TYPE_LEN];///< e.g. "_http"
|
||||||
|
char proto[TT_MDNS_PROTO_LEN]; ///< e.g. "_tcp"
|
||||||
|
char hostname[TT_MDNS_HOSTNAME_LEN]; ///< without .local
|
||||||
|
uint16_t port; ///< service port
|
||||||
|
char addresses[TT_MDNS_MAX_ADDRESSES][TT_MDNS_IP_LEN]; ///< resolved IPs
|
||||||
|
uint8_t addressCount;
|
||||||
|
char primaryAddress[TT_MDNS_IP_LEN]; ///< first IPv4 or first address
|
||||||
|
uint32_t ttl;
|
||||||
|
TtMdnsTxtRecord txtRecords[TT_MDNS_MAX_TXT_RECORDS];
|
||||||
|
uint8_t txtCount;
|
||||||
|
} TtMdnsService;
|
||||||
|
|
||||||
|
/** Result set returned by browse. */
|
||||||
|
typedef struct {
|
||||||
|
TtMdnsService services[TT_MDNS_MAX_RESULTS];
|
||||||
|
uint8_t count;
|
||||||
|
} TtMdnsBrowseResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if mDNS is initialized and running.
|
||||||
|
*/
|
||||||
|
bool tt_mdns_is_available();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browse for mDNS service instances.
|
||||||
|
*
|
||||||
|
* Blocks for up to timeoutMs.
|
||||||
|
*
|
||||||
|
* @param serviceType e.g. "_http", "_tactility" (with or without leading underscore both accepted, but conventional is with)
|
||||||
|
* @param proto e.g. "_tcp", "_udp"
|
||||||
|
* @param timeoutMs how long to wait (e.g. 3000ms). 0 = use default 3000ms.
|
||||||
|
* @param maxResults clamp to TT_MDNS_MAX_RESULTS. 0 = default 20.
|
||||||
|
* @param outResult filled with 0..maxResults services. Caller provides storage.
|
||||||
|
* @return true on successful query (even 0 results is success). false if mDNS not running.
|
||||||
|
*/
|
||||||
|
bool tt_mdns_browse(const char* serviceType, const char* proto, uint32_t timeoutMs, size_t maxResults, TtMdnsBrowseResult* outResult);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a hostname like "kidsOS-AB12" or "kidsOS-AB12.local" to an IPv4 address string.
|
||||||
|
*
|
||||||
|
* @param hostname hostname (".local" suffix optional)
|
||||||
|
* @param timeoutMs wait time, 0 = 2000ms default
|
||||||
|
* @param outIp buffer of at least TT_MDNS_IP_LEN, filled with IP string e.g. "192.168.1.42"
|
||||||
|
* @return true if resolved.
|
||||||
|
*/
|
||||||
|
bool tt_mdns_resolve_hostname(const char* hostname, uint32_t timeoutMs, char* outIp);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "tt_hal_display.h"
|
#include "tt_hal_display.h"
|
||||||
#include "tt_hal_touch.h"
|
#include "tt_hal_touch.h"
|
||||||
#include "tt_hal_uart.h"
|
#include "tt_hal_uart.h"
|
||||||
|
#include "tt_mdns.h"
|
||||||
#include <tt_lock.h>
|
#include <tt_lock.h>
|
||||||
#include "tt_lvgl.h"
|
#include "tt_lvgl.h"
|
||||||
#include "tt_lvgl_keyboard.h"
|
#include "tt_lvgl_keyboard.h"
|
||||||
@@ -361,6 +362,10 @@ const esp_elfsym main_symbols[] {
|
|||||||
ESP_ELFSYM_EXPORT(tt_timezone_set_format_24_hour),
|
ESP_ELFSYM_EXPORT(tt_timezone_set_format_24_hour),
|
||||||
// tt::lvgl
|
// tt::lvgl
|
||||||
ESP_ELFSYM_EXPORT(tt_lvgl_spinner_create),
|
ESP_ELFSYM_EXPORT(tt_lvgl_spinner_create),
|
||||||
|
// mDNS
|
||||||
|
ESP_ELFSYM_EXPORT(tt_mdns_is_available),
|
||||||
|
ESP_ELFSYM_EXPORT(tt_mdns_browse),
|
||||||
|
ESP_ELFSYM_EXPORT(tt_mdns_resolve_hostname),
|
||||||
|
|
||||||
// stdio.h
|
// stdio.h
|
||||||
ESP_ELFSYM_EXPORT(rename),
|
ESP_ELFSYM_EXPORT(rename),
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#include "tt_mdns.h"
|
||||||
|
|
||||||
|
#include <Tactility/network/Mdns.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
bool tt_mdns_is_available() {
|
||||||
|
return tt::network::mdns::isAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tt_mdns_browse(const char* serviceType, const char* proto, uint32_t timeoutMs, size_t maxResults, TtMdnsBrowseResult* outResult) {
|
||||||
|
if (outResult == nullptr) return false;
|
||||||
|
if (serviceType == nullptr || proto == nullptr) return false;
|
||||||
|
|
||||||
|
memset(outResult, 0, sizeof(TtMdnsBrowseResult));
|
||||||
|
|
||||||
|
uint32_t effectiveTimeout = timeoutMs == 0 ? 3000 : timeoutMs;
|
||||||
|
size_t effectiveMax = maxResults == 0 ? 20 : maxResults;
|
||||||
|
effectiveMax = std::min<size_t>(effectiveMax, TT_MDNS_MAX_RESULTS);
|
||||||
|
|
||||||
|
std::vector<tt::network::mdns::Service> services;
|
||||||
|
if (!tt::network::mdns::browse(serviceType, proto, effectiveTimeout, effectiveMax, services)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t toCopy = std::min(services.size(), static_cast<size_t>(TT_MDNS_MAX_RESULTS));
|
||||||
|
for (size_t i = 0; i < toCopy; i++) {
|
||||||
|
const auto& src = services[i];
|
||||||
|
auto& dst = outResult->services[i];
|
||||||
|
|
||||||
|
strncpy(dst.instanceName, src.instanceName.c_str(), TT_MDNS_INSTANCE_LEN - 1);
|
||||||
|
strncpy(dst.serviceType, src.serviceType.c_str(), TT_MDNS_SERVICE_TYPE_LEN - 1);
|
||||||
|
strncpy(dst.proto, src.proto.c_str(), TT_MDNS_PROTO_LEN - 1);
|
||||||
|
strncpy(dst.hostname, src.hostname.c_str(), TT_MDNS_HOSTNAME_LEN - 1);
|
||||||
|
dst.port = src.port;
|
||||||
|
dst.ttl = src.ttl;
|
||||||
|
strncpy(dst.primaryAddress, src.primaryAddress.c_str(), TT_MDNS_IP_LEN - 1);
|
||||||
|
|
||||||
|
size_t addrCount = std::min(src.addresses.size(), static_cast<size_t>(TT_MDNS_MAX_ADDRESSES));
|
||||||
|
dst.addressCount = static_cast<uint8_t>(addrCount);
|
||||||
|
for (size_t a = 0; a < addrCount; a++) {
|
||||||
|
strncpy(dst.addresses[a], src.addresses[a].c_str(), TT_MDNS_IP_LEN - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t txtCount = std::min(src.txtRecords.size(), static_cast<size_t>(TT_MDNS_MAX_TXT_RECORDS));
|
||||||
|
dst.txtCount = static_cast<uint8_t>(txtCount);
|
||||||
|
size_t idx = 0;
|
||||||
|
for (const auto& kv : src.txtRecords) {
|
||||||
|
if (idx >= txtCount) break;
|
||||||
|
strncpy(dst.txtRecords[idx].key, kv.first.c_str(), TT_MDNS_TXT_KEY_LEN - 1);
|
||||||
|
strncpy(dst.txtRecords[idx].value, kv.second.c_str(), TT_MDNS_TXT_VALUE_LEN - 1);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outResult->count = static_cast<uint8_t>(toCopy);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tt_mdns_resolve_hostname(const char* hostname, uint32_t timeoutMs, char* outIp) {
|
||||||
|
if (hostname == nullptr || outIp == nullptr) return false;
|
||||||
|
memset(outIp, 0, TT_MDNS_IP_LEN);
|
||||||
|
|
||||||
|
uint32_t effectiveTimeout = timeoutMs == 0 ? 2000 : timeoutMs;
|
||||||
|
std::string ip;
|
||||||
|
if (!tt::network::mdns::resolveHostname(hostname, effectiveTimeout, ip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
strncpy(outIp, ip.c_str(), TT_MDNS_IP_LEN - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
@@ -81,3 +81,70 @@ TEST_CASE("parseManifestV2() should fail when the app id is invalid") {
|
|||||||
AppManifest manifest;
|
AppManifest manifest;
|
||||||
CHECK_EQ(parseManifestV2(properties, manifest), false);
|
CHECK_EQ(parseManifestV2(properties, manifest), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parseAppFlagsString() should parse various flag combinations") {
|
||||||
|
CHECK_EQ(parseAppFlagsString(""), 0);
|
||||||
|
CHECK_EQ(parseAppFlagsString("None"), 0);
|
||||||
|
CHECK_EQ(parseAppFlagsString("HideStatusBar"), AppManifest::Flags::HideStatusBar);
|
||||||
|
CHECK_EQ(parseAppFlagsString("hidden"), AppManifest::Flags::Hidden);
|
||||||
|
CHECK_EQ(parseAppFlagsString("HideStatusBar,Hidden"), AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden);
|
||||||
|
CHECK_EQ(parseAppFlagsString(" hidestatusbar , hidden "), AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden);
|
||||||
|
CHECK_EQ(parseAppFlagsString("HideStatusBar, Hidden"), AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parseManifest() should parse V1 flags for fullscreen") {
|
||||||
|
TestFile file("test-manifest-v1-flags.properties");
|
||||||
|
file.writeData(
|
||||||
|
"[manifest]\n"
|
||||||
|
"version=0.1\n"
|
||||||
|
"[target]\n"
|
||||||
|
"sdk=0.0.0\n"
|
||||||
|
"platforms=esp32\n"
|
||||||
|
"[app]\n"
|
||||||
|
"id=one.tactility.sdktest\n"
|
||||||
|
"versionName=0.1.0\n"
|
||||||
|
"versionCode=1\n"
|
||||||
|
"name=SDK Test\n"
|
||||||
|
"flags=HideStatusBar\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
AppManifest manifest;
|
||||||
|
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
|
||||||
|
CHECK_EQ(manifest.appFlags & AppManifest::Flags::HideStatusBar, AppManifest::Flags::HideStatusBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parseManifest() should parse V2 flags for fullscreen") {
|
||||||
|
TestFile file("test-manifest-v2-flags.properties");
|
||||||
|
file.writeData(
|
||||||
|
"manifest.version=0.1\n"
|
||||||
|
"target.sdk=0.0.0\n"
|
||||||
|
"target.platforms=esp32\n"
|
||||||
|
"app.id=one.tactility.sdktest\n"
|
||||||
|
"app.version.name=0.1.0\n"
|
||||||
|
"app.version.code=1\n"
|
||||||
|
"app.name=SDK Test\n"
|
||||||
|
"app.flags=HideStatusBar\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
AppManifest manifest;
|
||||||
|
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
|
||||||
|
CHECK_EQ(manifest.appFlags & AppManifest::Flags::HideStatusBar, AppManifest::Flags::HideStatusBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parseManifest() should parse combined flags") {
|
||||||
|
TestFile file("test-manifest-v2-flags2.properties");
|
||||||
|
file.writeData(
|
||||||
|
"manifest.version=0.1\n"
|
||||||
|
"target.sdk=0.0.0\n"
|
||||||
|
"target.platforms=esp32\n"
|
||||||
|
"app.id=one.tactility.sdktest\n"
|
||||||
|
"app.version.name=0.1.0\n"
|
||||||
|
"app.version.code=1\n"
|
||||||
|
"app.name=SDK Test\n"
|
||||||
|
"app.flags=HideStatusBar,Hidden\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
AppManifest manifest;
|
||||||
|
CHECK_EQ(parseManifest(file.getPath(), manifest), true);
|
||||||
|
CHECK_EQ(manifest.appFlags, (AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user