Compare commits

...

3 Commits

Author SHA1 Message Date
Adolfo Reyna 310b955652 Migrate ES3C35P to latest main firmware 2026-09-10 13:31:10 -04:00
Ken Van Hoeylandt 8556103eb1 Implemented multi-binary app packaging (#648) 2026-09-09 20:05:04 +02:00
Shadowtrance 2496dea5c2 M5Stack PaperS3 display driver improved (#647) 2026-09-08 21:57:41 +02:00
114 changed files with 8966 additions and 1183 deletions
+2 -4
View File
@@ -22,19 +22,17 @@ runs:
run: python Buildscripts/release-sdk-posix.py release/TactilitySDK
- name: 'Test Integration Prep'
shell: bash
# The manifest.properties of our integration test uses version 0.0.0 to indicate that it is not using a normal SDK
# This way, it only works with our custom build. That means we have to create a copy of the SDK with the correct folder structure:
env:
TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
run: |
TACTILITY_SDK_NAME="0.0.0-posix-$TACTILITY_ARCH"
TACTILITY_SDK_NAME="$(cat version.txt)-posix-$TACTILITY_ARCH"
mkdir -p test_sdk/$TACTILITY_SDK_NAME
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
- name: 'Test Integration'
shell: bash
env:
TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
run: cd Tests/SdkIntegration && TACTILITY_SDK_PATH=../../test_sdk python tactility.py build posix-$TACTILITY_ARCH --local-sdk
run: cd Tests/SdkIntegration && TACTILITY_SDK_PATH=../../test_sdk python tactility.py build -a posix-$TACTILITY_ARCH --local-sdk
- name: 'Upload Artifact'
uses: actions/upload-artifact@v4
with:
+2 -2
View File
@@ -35,7 +35,7 @@ runs:
# The manifest.properties of our integration test uses version 0.0.0 to indicate that it is not using a normal SDK
# This way, it only works with our custom build. That means we have to create a copy of the SDK with the correct folder structure:
run: |
TACTILITY_SDK_NAME="0.0.0-${{ inputs.arch }}"
TACTILITY_SDK_NAME="$(cat version.txt)-${{ inputs.arch }}"
mkdir -p test_sdk/$TACTILITY_SDK_NAME
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
- name: 'Test Integration'
@@ -43,7 +43,7 @@ runs:
with:
esp_idf_version: v5.5.2
target: ${{ inputs.arch }}
command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build ${{ inputs.arch }} --local-sdk
command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build -a ${{ inputs.arch }} --local-sdk
- name: 'Upload Artifact'
uses: actions/upload-artifact@v4
with:
@@ -311,11 +311,39 @@ def test_compile_missing_config():
print("PASSED")
return True
def test_es3c35p_uses_current_runtime_contract():
print("Running test_es3c35p_uses_current_runtime_contract...")
repository_root = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "..", ".."))
device_dir = os.path.join(repository_root, "Devices", "es3c35p")
with open(os.path.join(device_dir, "device.properties")) as f:
properties = f.read()
with open(os.path.join(device_dir, "es3c35p.dts")) as f:
devicetree = f.read()
requirements = [
("apps.launcherAppId=tactility.launcher" in properties, "current launcher app id"),
("hardware.tinyUsb=" not in properties, "no obsolete hardware.tinyUsb property"),
('wifi0 {\n\t\tcompatible = "espressif,esp32-wifi-pinned";\n\t};' in devicetree,
"Wi-Fi enabled for web server and MCP"),
('ble0 {\n\t\tcompatible = "espressif,esp32-ble";\n\t};' in devicetree,
"BLE node matches hardware.bluetooth=true"),
]
missing = [description for condition, description in requirements if not condition]
if missing:
print("FAILED: " + ", ".join(missing))
return False
print("PASSED")
return True
if __name__ == "__main__":
tests = [
test_compile_success,
test_compile_invalid_dts,
test_compile_missing_config,
test_es3c35p_uses_current_runtime_contract,
test_minmax_within_range_succeeds,
test_minmax_below_minimum_fails,
test_minmax_above_maximum_fails,
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Convert an image to an uncompressed LVGL RGB565 launcher background."""
import argparse
import shutil
import struct
import subprocess
from pathlib import Path
LV_IMAGE_HEADER_MAGIC = 0x19
LV_COLOR_FORMAT_RGB565 = 0x12
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Create an uncompressed LVGL .bin image for "
"/sdcard/tactility/launcher/background.bin. Use a square image sized "
"to the display's longest edge to support both orientations without scaling "
"(for example, 320x320 for a 320x240 display)."
)
)
parser.add_argument("input", type=Path, help="Source image")
parser.add_argument("output", type=Path, help="Destination .bin file")
parser.add_argument("--width", type=int, required=True, help="Output width")
parser.add_argument("--height", type=int, required=True, help="Output height")
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.width <= 0 or args.width > 65535 or args.height <= 0 or args.height > 65535:
raise SystemExit("width and height must be between 1 and 65535")
magick = shutil.which("magick")
if magick is None:
raise SystemExit("ImageMagick is required (the 'magick' command was not found)")
command = [
magick,
str(args.input),
"-resize",
f"{args.width}x{args.height}^",
"-gravity",
"center",
"-extent",
f"{args.width}x{args.height}",
"-depth",
"8",
"rgb:-",
]
rgb888 = subprocess.run(command, check=True, stdout=subprocess.PIPE).stdout
expected_size = args.width * args.height * 3
if len(rgb888) != expected_size:
raise SystemExit(f"unexpected ImageMagick output: {len(rgb888)} bytes, expected {expected_size}")
rgb565 = bytearray(args.width * args.height * 2)
for source_offset in range(0, len(rgb888), 3):
r, g, b = rgb888[source_offset : source_offset + 3]
pixel = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
destination_offset = (source_offset // 3) * 2
struct.pack_into("<H", rgb565, destination_offset, pixel)
stride = args.width * 2
header = struct.pack(
"<BBHHHHH",
LV_IMAGE_HEADER_MAGIC,
LV_COLOR_FORMAT_RGB565,
0,
args.width,
args.height,
stride,
0,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_bytes(header + rgb565)
print(
f"Wrote {args.output} ({args.width}x{args.height}, "
f"{len(header) + len(rgb565)} bytes, uncompressed RGB565)"
)
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

+4
View File
@@ -0,0 +1,4 @@
idf_component_register(
SRCS "source/module.cpp"
REQUIRES TactilityKernel
)
+27
View File
@@ -0,0 +1,27 @@
general.vendor=LCDWIKI/Hosyond
general.name=ES3C35P
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
display.size=3.5"
display.shape=rectangle
display.dpi=165
lvgl.colorDepth=16
storage.userDataLocation=SD
dependencies.useDeprecatedHal=false
# Launcher clock and full-screen wallpaper
sdkconfig.CONFIG_LV_FONT_MONTSERRAT_48=y
sdkconfig.CONFIG_LV_CACHE_DEF_SIZE=1048576
sdkconfig.CONFIG_LV_IMAGE_HEADER_CACHE_DEF_CNT=16
+6
View File
@@ -0,0 +1,6 @@
dependencies:
- Platforms/platform-esp32
- Drivers/st77922-module
- Drivers/es8311-module
- Drivers/audio-stream-module
dts: es3c35p.dts
+141
View File
@@ -0,0 +1,141 @@
/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/st77922.h>
#include <bindings/st77922_touch.h>
#include <bindings/es8311.h>
/ {
compatible = "root";
model = "LCDWIKI/Hosyond ES3C35P";
wifi0 {
compatible = "espressif,esp32-wifi-pinned";
};
ble0 {
compatible = "espressif,esp32-ble";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;
};
/* FM8002E speaker amplifier 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_7 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>;
};
/* BAT+ through a 200K/200K divider into GPIO8 / ADC1_CH7. */
battery-sense {
compatible = "battery-sense";
io-channel = <&adc0 0>;
reference-voltage-mv = <3300>;
multiplier = <2000>;
};
i2s0 {
compatible = "espressif,esp32-i2s";
port = <I2S_NUM_0>;
pin-bclk = <&gpio0 18 GPIO_FLAG_NONE>;
pin-ws = <&gpio0 21 GPIO_FLAG_NONE>;
pin-data-out = <&gpio0 15 GPIO_FLAG_NONE>;
pin-data-in = <&gpio0 16 GPIO_FLAG_NONE>;
pin-mclk = <&gpio0 17 GPIO_FLAG_NONE>;
};
i2c0 {
compatible = "espressif,esp32-i2c";
port = <I2C_NUM_0>;
clock-frequency = <400000>;
pin-sda = <&gpio0 38 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 39 GPIO_FLAG_NONE>;
touch@55 {
compatible = "sitronix,st77922-touch";
reg = <0x55>;
x-max = <320>;
y-max = <480>;
pin-reset = <&gpio0 48 GPIO_FLAG_NONE>;
pin-interrupt = <&gpio0 47 GPIO_FLAG_NONE>;
};
es8311: es8311@18 {
compatible = "everest,es8311";
reg = <0x18>;
i2s = <&i2s0>;
};
};
display_backlight_pwm {
compatible = "espressif,esp32-pwm-ledc";
pin = <&gpio0 41 GPIO_FLAG_NONE>;
period-ns = <200000>;
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-sclk = <&gpio0 12 GPIO_FLAG_NONE>;
pin-mosi = <&gpio0 11 GPIO_FLAG_NONE>;
pin-miso = <&gpio0 13 GPIO_FLAG_NONE>;
pin-wp = <&gpio0 14 GPIO_FLAG_NONE>;
pin-hd = <&gpio0 9 GPIO_FLAG_NONE>;
cs-gpios = <&gpio0 10 GPIO_FLAG_NONE>;
/* Accommodate the driver's 1/10-frame DMA staging transfers, matching
* the vendor LVGL port's full-frame refresh path. */
max-transfer-size = <65536>;
display@0 {
compatible = "sitronix,st77922";
horizontal-resolution = <320>;
vertical-resolution = <480>;
/* 80 MHz works in the vendor demo, but produces visible QSPI corruption on
* some modules/cables. The component's 40 MHz default is reliably clean. */
pixel-clock-hz = <40000000>;
backlight = <&display_backlight>;
};
};
sdmmc0 {
compatible = "espressif,esp32-sdmmc";
pin-clk = <&gpio0 5 GPIO_FLAG_NONE>;
pin-cmd = <&gpio0 4 GPIO_FLAG_NONE>;
pin-d0 = <&gpio0 6 GPIO_FLAG_NONE>;
pin-d1 = <&gpio0 7 GPIO_FLAG_NONE>;
pin-d2 = <&gpio0 2 GPIO_FLAG_NONE>;
pin-d3 = <&gpio0 3 GPIO_FLAG_NONE>;
slot = <SDMMC_HOST_SLOT_1>;
bus-width = <4>;
};
};
+9
View File
@@ -0,0 +1,9 @@
#include <tactility/module.h>
extern "C" {
Module es3c35p_module = {
.name = "es3c35p"
};
}
@@ -10,10 +10,13 @@ properties:
type: int
default: 20
description: Ambient temperature in °C, used for waveform timing compensation
draw-mode:
quality-draw-mode:
type: int
default: MODE_DU
description: Default EpdDrawMode waveform used for screen updates (e.g. MODE_DU, MODE_GC16)
default: MODE_GC16
description: >
EpdDrawMode waveform used for full-quality refreshes (e.g. MODE_GC16, MODE_GL16).
Fast partial updates always use MODE_DU internally and are not configurable - see
driver comments.
rotation:
type: int
default: EPD_ROT_PORTRAIT
+1 -1
View File
@@ -7,7 +7,7 @@ apps.launcherAppId=tactility.launcher
hardware.target=esp32s3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OPI
hardware.spiRamMode=OCT
hardware.spiRamSpeed=80M
hardware.esptoolFlashFreq=80M
hardware.tinyUsbMsc=true
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: Apache-2.0
/**
* Board definition for M5Stack PaperS3.
*
* Kept out-of-tree (see epd_board_m5papers3.h) instead of forking epdiy: this file only
* uses epdiy's public API, so it builds as an ordinary consumer of the upstream component.
*
* Pin mapping from M5GFX source code (authoritative reference):
* https://github.com/m5stack/M5GFX/blob/master/src/M5GFX.cpp
*
* Data bus: DB0-DB7 on GPIO 6,14,7,12,9,11,8,10
* Control: STH=13, LEH=15, STV=17, CKV=18, CKH=16
* Power: PWR=46, OE=45
*/
#include "epd_board_m5papers3.h"
#include <stdint.h>
#include "epdiy.h"
#include <output_lcd/lcd_driver.h>
#include "esp_log.h"
#include <driver/gpio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#ifndef CONFIG_IDF_TARGET_ESP32S3
#error "M5Paper S3 board only supports ESP32-S3"
#endif
static const char* TAG = "m5paper_s3";
/* Data Lines - from M5GFX source */
#define D0 GPIO_NUM_6
#define D1 GPIO_NUM_14
#define D2 GPIO_NUM_7
#define D3 GPIO_NUM_12
#define D4 GPIO_NUM_9
#define D5 GPIO_NUM_11
#define D6 GPIO_NUM_8
#define D7 GPIO_NUM_10
/* Control Lines - from M5GFX source */
#define STH GPIO_NUM_13 /* Start pulse horizontal (active low) */
#define LEH GPIO_NUM_15 /* Latch enable horizontal */
#define STV GPIO_NUM_17 /* Start vertical */
#define CKV GPIO_NUM_18 /* Clock vertical */
#define CKH GPIO_NUM_16 /* Clock horizontal - LCD peripheral clock output */
/* Power control - from M5GFX source */
#define PWR_PIN GPIO_NUM_46 /* Main power enable */
#define OE_PIN GPIO_NUM_45 /* Output enable */
static lcd_bus_config_t lcd_config = {
.clock = CKH,
.ckv = CKV,
.leh = LEH,
.start_pulse = STH,
.stv = STV,
.data[0] = D0,
.data[1] = D1,
.data[2] = D2,
.data[3] = D3,
.data[4] = D4,
.data[5] = D5,
.data[6] = D6,
.data[7] = D7,
};
static void epd_board_init(uint32_t epd_row_width, const EpdInitConfig* init_config) {
(void)init_config;
ESP_LOGI(TAG, "Initializing M5Paper S3 board");
/* Configure power pin - start with power off */
gpio_reset_pin(PWR_PIN);
gpio_set_direction(PWR_PIN, GPIO_MODE_OUTPUT);
gpio_set_level(PWR_PIN, 0);
/* Configure output enable pin - active high */
gpio_reset_pin(OE_PIN);
gpio_set_direction(OE_PIN, GPIO_MODE_OUTPUT);
gpio_set_level(OE_PIN, 0);
const EpdDisplay_t* display = epd_get_display();
LcdEpdConfig_t config = {
.pixel_clock = display->bus_speed * 1000 * 1000,
.ckv_high_time = 60,
.line_front_porch = 4,
.le_high_time = 4,
.bus_width = display->bus_width,
.bus = lcd_config,
};
epd_lcd_init(&config, display->width, display->height);
ESP_LOGI(TAG, "Board initialized: %dx%d @ %dMHz",
display->width, display->height, display->bus_speed);
}
static void epd_board_deinit() {
ESP_LOGI(TAG, "Deinitializing M5Paper S3 board");
/* Disable output first */
gpio_set_level(OE_PIN, 0);
/* Power off display */
gpio_set_level(PWR_PIN, 0);
epd_lcd_deinit();
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
/* Handle output enable changes */
if (mask->ep_output_enable) {
gpio_set_level(OE_PIN, state->ep_output_enable ? 1 : 0);
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
ESP_LOGI(TAG, "Powering on display");
/* Enable main power first */
gpio_set_level(PWR_PIN, 1);
/* Wait for power to stabilize */
vTaskDelay(pdMS_TO_TICKS(100));
/* Enable output */
gpio_set_level(OE_PIN, 1);
/* Update state */
state->ep_stv = true;
state->ep_mode = false;
state->ep_output_enable = true;
state->ep_sth = true;
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
ESP_LOGI(TAG, "Powering off display");
/* Disable output first */
gpio_set_level(OE_PIN, 0);
state->ep_stv = false;
state->ep_output_enable = false;
state->ep_mode = false;
state->ep_sth = false;
/* Small delay before cutting power */
vTaskDelay(pdMS_TO_TICKS(10));
/* Cut main power */
gpio_set_level(PWR_PIN, 0);
}
static float epd_board_ambient_temperature() {
/* TODO: Could read from BMI270 temperature sensor */
return 20.0f;
}
const EpdBoardDefinition epd_board_m5papers3 = {
.init = epd_board_init,
.deinit = epd_board_deinit,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.get_temperature = epd_board_ambient_temperature,
// No hardware VCOM control path on this board yet - a non-null callback that doesn't touch
// hardware would let epd_set_vcom() complete without changing anything on the panel.
.set_vcom = NULL,
.gpio_set_direction = NULL,
.gpio_read = NULL,
.gpio_write = NULL,
};
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
/**
* @file "epd_board_m5papers3.h"
* @brief Board definition for M5Stack PaperS3, kept out-of-tree because upstream epdiy
* (https://github.com/vroland/epdiy) does not support this board.
*/
#pragma once
#include <epd_board.h>
#ifdef __cplusplus
extern "C" {
#endif
extern const EpdBoardDefinition epd_board_m5papers3;
#ifdef __cplusplus
}
#endif
@@ -7,8 +7,10 @@
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <tactility/time.h>
#include "epd_board_m5papers3.h"
#include <epd_board.h>
#include <epdiy.h>
#include <esp_heap_caps.h>
@@ -19,6 +21,70 @@
#define TAG "Papers3Display"
#define GET_CONFIG(device) (static_cast<const Papers3DisplayConfig*>((device)->config))
// Fast partial updates are always MODE_DU (strict black/white); config->quality_draw_mode is
// only used for the periodic full-quality pass.
static constexpr EpdDrawMode FAST_DRAW_MODE = MODE_DU;
// A partial update covering at least this fraction of the panel is a full-screen content change
// (e.g. an app switch rebuilding the whole window, see lvgl.md) rather than a small widget
// redraw, and is promoted to a quality refresh immediately.
static constexpr float FULL_AREA_QUALITY_THRESHOLD = 0.6f;
// Bounds worst-case ghost accumulation during sustained fast-mode interaction (e.g. scrolling),
// regardless of idle time. LVGL's PARTIAL-mode draw buffer covers vres/10 rows (see
// lvgl-module/source/devices/devices.cpp's buffer_height), so a single full-screen redraw is
// already ~10 tiles - this must clear a full sweep comfortably, or a normal full-screen redraw
// gets promoted to slow GC16 partway through.
static constexpr uint32_t QUALITY_REFRESH_PARTIAL_COUNT = 20;
// Cleans up ghosting left behind after interaction stops, since nothing else triggers a refresh
// once draw_bitmap() calls stop arriving. Matches the M5Stack official demo's timer.
static constexpr uint32_t QUALITY_REFRESH_IDLE_SECONDS = 10;
// LVGL's PARTIAL render mode flushes one draw_bitmap() call per still-unjoined dirty rect, so
// one visual refresh is usually several back-to-back calls, not one; this holds quality mode
// across a sibling rect's near-zero gap so they don't end up on inconsistent modes. Must stay
// well under a GC16 draw's own duration (400ms+), or it also bridges the much larger gap between
// separate real frames and pins a whole multi-frame interaction to GC16.
static constexpr uint32_t QUALITY_HOLD_MS = 50;
// epd_fullclear() (white fill + GC16 draw + 3-cycle black/white flash, see epdiy's
// highlevel.c/render.c) only runs once, at boot (papers3_display_init()), never periodically:
// it wipes the whole panel, and this driver has no way to force LVGL to redraw everything
// afterward - only whatever rect is drawn next gets restored, leaving the rest blank.
// 4x4 ordered (Bayer) dither thresholds, spread evenly across a 0-15 nibble range.
static constexpr uint8_t BAYER_4X4[4][4] = {
{ 0, 8, 2, 10 },
{ 12, 4, 14, 6 },
{ 3, 11, 1, 9 },
{ 15, 7, 13, 5 },
};
// Dithers an 8-bit luminance sample (0x00=black..0xFF=white) down to a 4-bit nibble
// (0x0=black..0xF=white, matching EPDiy's MODE_PACKING_2PPB), spreading the rounding error
// spatially instead of truncating every pixel the same way - this is what turns flat/banded
// output into something that reads as smooth grayscale.
static inline uint8_t dither_to_nibble(uint8_t luminance, int32_t x, int32_t y) {
// BAYER_4X4 is 0-15; scaled by 17 it spans a full 0-255 quantization step (one increment of
// luminance*15), so the dither bias is actually comparable to the rounding it perturbs
// instead of a few percent of one step.
const uint32_t threshold = BAYER_4X4[y & 3][x & 3] * 17U;
const uint32_t level = (static_cast<uint32_t>(luminance) * 15U + threshold) / 255U;
return static_cast<uint8_t>(level > 15U ? 15U : level);
}
// Binary variant for MODE_DU, which only supports pure black/white (see epdiy.h) - dithering
// still applies so a partial-update area doesn't look coarser than the quality pass that
// preceded it.
static inline uint8_t dither_to_bw_nibble(uint8_t luminance, int32_t x, int32_t y) {
// *16 (not 17) keeps the max threshold at 240, strictly below 255 - otherwise luminance 0xFF
// (pure white) would tie the top Bayer cell's threshold and the strict ">" would misclassify
// it as black.
const uint32_t threshold = BAYER_4X4[y & 3][x & 3] * 16U;
return luminance > threshold ? 0xF : 0x0;
}
extern "C" {
extern Module m5stack_papers3_module;
@@ -34,6 +100,15 @@ struct Papers3DisplayInternal {
// Scratch buffer for the grayscale8->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap().
uint8_t* packed_buffer;
bool powered;
uint32_t panel_pixel_count;
// Fast (MODE_DU) partial updates since the last quality refresh; see
// QUALITY_REFRESH_PARTIAL_COUNT.
uint32_t partial_count_since_quality;
// get_ticks() at the last quality refresh; see QUALITY_REFRESH_IDLE_SECONDS.
TickType_t last_quality_refresh_tick;
// While get_ticks() < this, every draw_bitmap() call uses quality mode regardless of the
// other triggers; see QUALITY_HOLD_MS.
TickType_t quality_hold_until_tick;
};
static void power_on(Papers3DisplayInternal* internal) {
@@ -64,9 +139,48 @@ static error_t papers3_display_init(Device* device) {
// pass, leaving a faint ghost. Run a full clear now, before LVGL's first flush ever reaches
// draw_bitmap(), so it never has to undo content LVGL already put on screen.
epd_fullclear(&internal->hl_state, config->temperature_celsius);
internal->partial_count_since_quality = 0;
internal->last_quality_refresh_tick = get_ticks();
internal->quality_hold_until_tick = 0;
return ERROR_NONE;
}
// Decides whether this update should be a full-quality (config->quality_draw_mode) refresh or
// a fast MODE_DU one. Read-only - see commit_quality_mode_decision() for the state this decision
// leads to.
static bool should_use_quality_mode(Papers3DisplayInternal* internal, int32_t width, int32_t height) {
const TickType_t now = get_ticks();
const uint32_t area = static_cast<uint32_t>(width) * static_cast<uint32_t>(height);
const bool is_full_screen_change = area >= static_cast<uint32_t>(
static_cast<float>(internal->panel_pixel_count) * FULL_AREA_QUALITY_THRESHOLD
);
const bool partial_count_exceeded = internal->partial_count_since_quality >= QUALITY_REFRESH_PARTIAL_COUNT;
// Idle refresh is too problematic to worth the possible gains. So it isn't done on purpose.
// Ghosting is very minimal now anyway (it's still around but way less bad)
const bool idle_exceeded = now - internal->last_quality_refresh_tick >= seconds_to_ticks(QUALITY_REFRESH_IDLE_SECONDS);
const bool within_hold = now < internal->quality_hold_until_tick;
return is_full_screen_change || partial_count_exceeded || idle_exceeded || within_hold;
}
// Applies should_use_quality_mode()'s decision, but only commits the quality-mode reset once the
// draw actually succeeded - a failed quality refresh must not make a still-ghosting panel look
// freshly cleaned to every trigger above. A failed fast update still counts toward the partial
// count, since it was still MODE_DU content, not a clean slate.
static void commit_quality_mode_decision(Papers3DisplayInternal* internal, bool used_quality, bool draw_succeeded) {
if (used_quality) {
if (draw_succeeded) {
const TickType_t now = get_ticks();
internal->partial_count_since_quality = 0;
internal->last_quality_refresh_tick = now;
internal->quality_hold_until_tick = now + millis_to_ticks(QUALITY_HOLD_MS);
}
} else {
internal->partial_count_since_quality++;
}
}
// Reports GRAYSCALE8 (not MONOCHROME) so LVGL uses partial/tile updates instead of forcing
// full-frame - the bridge hardcodes full-frame for MONOCHROME/I1 regardless of capability flags.
// So draw_bitmap is called once per changed tile, not necessarily the whole panel.
@@ -76,11 +190,12 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3
const int32_t width = x_end - x_start;
const int32_t height = y_end - y_start;
const bool use_quality = should_use_quality_mode(internal, width, height);
// color_data is DISPLAY_COLOR_FORMAT_GRAYSCALE8: row-major, 1 byte/pixel luminance
// (0x00=black..0xFF=white, matching LVGL's L8). EPDiy wants 4bpp packed (2px/byte, 0x0=black,
// 0xF=white) - a plain >>4 truncation preserves all 16 real gray levels the panel supports
// (this panel is not B/W-only; see MODE_GC16/GL16 in papers3-display.yaml's draw-mode doc).
// 0xF=white); Bayer dithering (full 16-level for the quality pass, binary for MODE_DU)
// spreads the rounding error instead of a flat truncation.
const auto* src = static_cast<const uint8_t*>(color_data);
const size_t src_stride = static_cast<size_t>(width);
const size_t packed_stride = static_cast<size_t>(width + 1) / 2;
@@ -90,12 +205,18 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3
uint8_t* dst_row = internal->packed_buffer + static_cast<size_t>(row) * packed_stride;
int32_t col = 0;
for (; col + 2 <= width; col += 2) {
const uint8_t p0 = src_row[col] >> 4U;
const uint8_t p1 = src_row[col + 1] >> 4U;
const uint8_t p0 = use_quality
? dither_to_nibble(src_row[col], x_start + col, y_start + row)
: dither_to_bw_nibble(src_row[col], x_start + col, y_start + row);
const uint8_t p1 = use_quality
? dither_to_nibble(src_row[col + 1], x_start + col + 1, y_start + row)
: dither_to_bw_nibble(src_row[col + 1], x_start + col + 1, y_start + row);
dst_row[col / 2] = static_cast<uint8_t>((p1 << 4U) | p0);
}
if (col < width) { // odd width: last column has no pair, low nibble unused
dst_row[col / 2] = static_cast<uint8_t>(src_row[col] >> 4U);
dst_row[col / 2] = use_quality
? dither_to_nibble(src_row[col], x_start + col, y_start + row)
: dither_to_bw_nibble(src_row[col], x_start + col, y_start + row);
}
}
@@ -108,13 +229,15 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3
power_on(internal);
epd_draw_rotated_image(update_area, internal->packed_buffer, internal->framebuffer);
const auto draw_mode = use_quality ? config->quality_draw_mode : FAST_DRAW_MODE;
auto draw_result = epd_hl_update_area(
&internal->hl_state,
static_cast<EpdDrawMode>(config->draw_mode | MODE_PACKING_2PPB),
static_cast<EpdDrawMode>(draw_mode | MODE_PACKING_2PPB),
config->temperature_celsius,
update_area
);
commit_quality_mode_decision(internal, use_quality, draw_result == EPD_DRAW_SUCCESS);
return draw_result == EPD_DRAW_SUCCESS ? ERROR_NONE : ERROR_RESOURCE;
}
@@ -133,13 +256,11 @@ static DisplayColorFormat papers3_display_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_GRAYSCALE8;
}
// epd_width()/epd_height() are the panel's native, unrotated dimensions (display->width/height in
// epdiy.c) - epd_rotated_display_width()/height() swap them for EPD_ROT_PORTRAIT/INVERTED_PORTRAIT.
// epd_draw_rotated_image() clamps its input rect against the *rotated* dims and epd_draw_pixel()
// applies the rotation transform on top of that (see _rotate() in epdiy.c), so both LVGL's canvas
// size and draw_bitmap()'s rect must be in rotated-space, not native-space - using the native
// epd_width()/epd_height() here fed rotated-space code a landscape-sized canvas, which produced
// exactly the "rotated + landscape" symptom this was fixed for.
// epd_width()/epd_height() are the panel's native, unrotated dimensions; epd_rotated_display_
// width()/height() swap them for EPD_ROT_PORTRAIT/INVERTED_PORTRAIT. epd_draw_rotated_image()
// clamps its input rect against the rotated dims and epd_draw_pixel() applies the rotation
// transform on top of that (see _rotate() in epdiy.c), so both LVGL's canvas size and
// draw_bitmap()'s rect must be in rotated-space, not native-space.
static uint16_t papers3_display_get_resolution_x(Device*) {
return static_cast<uint16_t>(epd_rotated_display_width());
}
@@ -232,6 +353,11 @@ static error_t start(Device* device) {
return ERROR_OUT_OF_MEMORY;
}
internal->panel_pixel_count = static_cast<uint32_t>(epd_rotated_display_width()) * static_cast<uint32_t>(epd_rotated_display_height());
internal->partial_count_since_quality = 0;
internal->last_quality_refresh_tick = get_ticks();
internal->quality_hold_until_tick = 0;
device_set_driver_data(device, internal);
LOG_I(TAG, "EPDiy initialized (%dx%d native, %dx%d rotated)", epd_width(), epd_height(), epd_rotated_display_width(), epd_rotated_display_height());
@@ -10,7 +10,7 @@ extern "C" {
struct Papers3DisplayConfig {
int temperature_celsius;
enum EpdDrawMode draw_mode;
enum EpdDrawMode quality_draw_mode;
enum EpdRotation rotation;
};
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(st77922-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel platform-esp32 esp_lcd_st77922 driver
)
@@ -0,0 +1,37 @@
description: Touch interface integrated in the Sitronix ST77922 TDDI
include: ["i2c-device.yaml"]
compatible: "sitronix,st77922-touch"
bus: i2c
properties:
x-max:
type: int
required: true
description: Maximum X coordinate
y-max:
type: int
required: true
description: Maximum Y coordinate
swap-xy:
type: boolean
default: false
description: Swap the X and Y axes
mirror-x:
type: boolean
default: false
description: Mirror the X axis
mirror-y:
type: boolean
default: false
description: Mirror the Y axis
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Reset GPIO pin
pin-interrupt:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Interrupt GPIO pin
@@ -0,0 +1,47 @@
description: Sitronix ST77922 QSPI display panel
compatible: "sitronix,st77922"
bus: spi
properties:
horizontal-resolution:
type: int
required: true
description: Horizontal resolution in pixels
vertical-resolution:
type: int
required: true
description: Vertical resolution in pixels
mirror-x:
type: boolean
default: false
description: Mirror the X axis
mirror-y:
type: boolean
default: false
description: Mirror the Y axis
invert-color:
type: boolean
default: false
description: Invert the panel's color output
bgr-order:
type: boolean
default: false
description: Use BGR element order instead of RGB
bits-per-pixel:
type: int
default: 16
description: Color depth in bits per pixel
pixel-clock-hz:
type: int
default: 80000000
description: QSPI pixel clock frequency in Hz
transaction-queue-depth:
type: int
default: 10
description: Size of the internal SPI transaction queue
backlight:
type: phandle
default: "NULL"
description: Optional reference to this display's backlight device
+3
View File
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/st77922.h>
DEFINE_DEVICETREE(st77922, struct St77922Config)
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/st77922_touch.h>
DEFINE_DEVICETREE(st77922_touch, struct St77922TouchConfig)
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <tactility/device.h>
struct St77922Config {
uint16_t horizontal_resolution;
uint16_t vertical_resolution;
bool mirror_x;
bool mirror_y;
bool invert_color;
bool bgr_order;
uint32_t bits_per_pixel;
uint32_t pixel_clock_hz;
uint8_t transaction_queue_depth;
struct Device* backlight;
};
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <tactility/drivers/gpio.h>
struct St77922TouchConfig {
uint8_t address;
uint16_t x_max;
uint16_t y_max;
bool swap_xy;
bool mirror_x;
bool mirror_y;
struct GpioPinSpec pin_reset;
struct GpioPinSpec pin_interrupt;
};
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
extern Module st77922_module;
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver st77922_driver;
extern Driver st77922_touch_driver;
static Driver* const st77922_drivers[] = {
&st77922_driver,
&st77922_touch_driver,
nullptr
};
Module st77922_module = {
.name = "st77922",
.drivers = st77922_drivers
};
} // extern "C"
+270
View File
@@ -0,0 +1,270 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/st77922.h>
#include <st77922_module.h>
#include "st77922_init.h"
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/esp32_spi.h>
#include <tactility/drivers/spi_controller.h>
#include <tactility/log.h>
#include <esp_err.h>
#include <esp_heap_caps.h>
#include <esp_lcd_io_spi.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_st77922.h>
#include <freertos/semphr.h>
#include <cstdlib>
#define TAG "ST77922"
#define GET_CONFIG(device) (static_cast<const St77922Config*>((device)->config))
struct St77922Internal {
esp_lcd_panel_io_handle_t io_handle;
esp_lcd_panel_handle_t panel_handle;
SemaphoreHandle_t draw_done;
uint8_t* transfer_buffer;
size_t transfer_buffer_size;
};
static bool IRAM_ATTR transfer_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void* context) {
auto* internal = static_cast<St77922Internal*>(context);
BaseType_t task_woken = pdFALSE;
xSemaphoreGiveFromISR(internal->draw_done, &task_woken);
return task_woken == pdTRUE;
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
const auto* spi = static_cast<const Esp32SpiConfig*>(parent->config);
const auto* config = GET_CONFIG(device);
GpioPinSpec cs;
if (esp32_spi_get_cs_pin(device, &cs) != ERROR_NONE) {
return ERROR_RESOURCE;
}
auto* internal = static_cast<St77922Internal*>(calloc(1, sizeof(St77922Internal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->draw_done = xSemaphoreCreateBinary();
if (internal->draw_done == nullptr) {
free(internal);
return ERROR_OUT_OF_MEMORY;
}
// The vendor port renders a complete frame in PSRAM, then copies it through a
// 1/10-frame DMA buffer. Besides making full-frame refresh possible, this keeps
// the panel's GRAM synchronized when animated objects invalidate old and new
// positions in separate LVGL regions.
const size_t bytes_per_pixel = config->bits_per_pixel / 8;
const size_t rows_per_transfer = config->vertical_resolution > 10
? config->vertical_resolution / 10 : config->vertical_resolution;
internal->transfer_buffer_size =
static_cast<size_t>(config->horizontal_resolution) * rows_per_transfer * bytes_per_pixel;
if (spi->max_transfer_size > 0
&& internal->transfer_buffer_size > static_cast<size_t>(spi->max_transfer_size)) {
internal->transfer_buffer_size = static_cast<size_t>(spi->max_transfer_size);
}
internal->transfer_buffer = static_cast<uint8_t*>(heap_caps_malloc(
internal->transfer_buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
if (internal->transfer_buffer == nullptr) {
vSemaphoreDelete(internal->draw_done);
free(internal);
return ERROR_OUT_OF_MEMORY;
}
esp_lcd_panel_io_spi_config_t io_config = {
.cs_gpio_num = static_cast<int>(cs.pin),
.dc_gpio_num = -1,
.spi_mode = 0,
.pclk_hz = config->pixel_clock_hz,
.trans_queue_depth = config->transaction_queue_depth,
.on_color_trans_done = transfer_done,
.user_ctx = internal,
.lcd_cmd_bits = 32,
.lcd_param_bits = 8,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.flags = {
.dc_high_on_cmd = 0,
.dc_low_on_data = 0,
.dc_low_on_param = 0,
.octal_mode = 0,
.quad_mode = 1,
.sio_mode = 0,
.lsb_first = 0,
.cs_high_active = 0,
},
};
esp_err_t result = esp_lcd_new_panel_io_spi(
static_cast<esp_lcd_spi_bus_handle_t>(spi->host), &io_config, &internal->io_handle);
if (result != ESP_OK) {
LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(result));
heap_caps_free(internal->transfer_buffer);
vSemaphoreDelete(internal->draw_done);
free(internal);
return ERROR_RESOURCE;
}
size_t init_count = 0;
st77922_vendor_config_t vendor = {
.init_cmds = st77922_board_init_commands(&init_count),
.init_cmds_size = static_cast<uint16_t>(init_count),
.flags = { .use_qspi_interface = 1 },
};
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = -1,
.rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
.bits_per_pixel = config->bits_per_pixel,
.flags = { .reset_active_high = false },
.vendor_config = &vendor,
};
result = esp_lcd_new_panel_st77922(internal->io_handle, &panel_config, &internal->panel_handle);
bool ok = result == ESP_OK;
ok = ok && esp_lcd_panel_reset(internal->panel_handle) == ESP_OK;
ok = ok && esp_lcd_panel_init(internal->panel_handle) == ESP_OK;
ok = ok && ((!config->mirror_x && !config->mirror_y)
|| esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK);
ok = ok && (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK);
ok = ok && esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK;
if (!ok) {
LOG_E(TAG, "Failed to bring up panel: %s", esp_err_to_name(result));
if (internal->panel_handle != nullptr) esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
heap_caps_free(internal->transfer_buffer);
vSemaphoreDelete(internal->draw_done);
free(internal);
return ERROR_RESOURCE;
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<St77922Internal*>(device_get_driver_data(device));
if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK
|| esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
return ERROR_RESOURCE;
}
heap_caps_free(internal->transfer_buffer);
vSemaphoreDelete(internal->draw_done);
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
static error_t reset(Device* device) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_reset(data->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t init(Device* device) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_init(data->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t draw_bitmap(Device* device, int32_t xs, int32_t ys, int32_t xe, int32_t ye, const void* pixels) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
const size_t row_bytes = static_cast<size_t>(xe - xs) * config->bits_per_pixel / 8;
if (row_bytes == 0 || row_bytes > data->transfer_buffer_size) {
return ERROR_OUT_OF_RANGE;
}
const size_t rows_per_transfer = data->transfer_buffer_size / row_bytes;
const auto* source = static_cast<const uint8_t*>(pixels);
int32_t chunk_y = ys;
while (chunk_y < ye) {
const size_t remaining_rows = static_cast<size_t>(ye - chunk_y);
const size_t chunk_rows = remaining_rows < rows_per_transfer ? remaining_rows : rows_per_transfer;
const size_t chunk_bytes = chunk_rows * row_bytes;
memcpy(data->transfer_buffer, source, chunk_bytes);
xSemaphoreTake(data->draw_done, 0);
if (esp_lcd_panel_draw_bitmap(
data->panel_handle, xs, chunk_y, xe, chunk_y + static_cast<int32_t>(chunk_rows),
data->transfer_buffer) != ESP_OK) {
LOG_E(TAG, "Failed to queue color transfer at y=%ld", static_cast<long>(chunk_y));
return ERROR_RESOURCE;
}
if (xSemaphoreTake(data->draw_done, pdMS_TO_TICKS(1000)) != pdTRUE) {
LOG_E(TAG, "Timed out waiting for color transfer at y=%ld", static_cast<long>(chunk_y));
return ERROR_TIMEOUT;
}
source += chunk_bytes;
chunk_y += static_cast<int32_t>(chunk_rows);
}
return ERROR_NONE;
}
static error_t mirror(Device* device, bool x, bool y) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_mirror(data->panel_handle, x, y) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static bool get_mirror_x(Device* device) { return GET_CONFIG(device)->mirror_x; }
static bool get_mirror_y(Device* device) { return GET_CONFIG(device)->mirror_y; }
static error_t invert(Device* device, bool value) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_invert_color(data->panel_handle, value) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t on_off(Device* device, bool value) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_disp_on_off(data->panel_handle, value) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t sleep(Device* device, bool value) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_disp_sleep(data->panel_handle, value) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static DisplayColorFormat color_format(Device*) { return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED; }
static uint16_t resolution_x(Device* device) { return GET_CONFIG(device)->horizontal_resolution; }
static uint16_t resolution_y(Device* device) { return GET_CONFIG(device)->vertical_resolution; }
static void frame_buffer(Device*, uint8_t, void** output) { *output = nullptr; }
static uint8_t frame_buffer_count(Device*) { return 0; }
static error_t backlight(Device* device, Device** output) {
*output = GET_CONFIG(device)->backlight;
return *output == nullptr ? ERROR_NOT_SUPPORTED : ERROR_NONE;
}
static const DisplayApi display_api = {
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR |
DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_BACKLIGHT |
DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME,
.reset = reset,
.init = init,
.draw_bitmap = draw_bitmap,
.mirror = mirror,
.swap_xy = nullptr,
.get_swap_xy = nullptr,
.get_mirror_x = get_mirror_x,
.get_mirror_y = get_mirror_y,
.set_gap = nullptr,
.get_gap_x = nullptr,
.get_gap_y = nullptr,
.invert_color = invert,
.disp_on_off = on_off,
.disp_sleep = sleep,
.get_color_format = color_format,
.get_resolution_x = resolution_x,
.get_resolution_y = resolution_y,
.get_frame_buffer = frame_buffer,
.get_frame_buffer_count = frame_buffer_count,
.get_backlight = backlight,
.has_capability = nullptr,
};
Driver st77922_driver = {
.name = "st77922",
.compatible = (const char*[]) { "sitronix,st77922", nullptr },
.start_device = start,
.stop_device = stop,
.api = &display_api,
.device_type = &DISPLAY_TYPE,
.owner = &st77922_module,
.internal = nullptr
};
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
#include "st77922_init.h"
// Initialization sequence supplied with the LCDWIKI/Hosyond ES3C35P vendor ESP-IDF demo.
static const st77922_lcd_init_cmd_t init_commands[] = {
{0xF1, (uint8_t []){0x00}, 1, 0},
{0x60, (uint8_t []){0x00, 0x00, 0x00}, 3, 0},
{0x65, (uint8_t []){0x80}, 1, 0},
{0x79, (uint8_t []){0x06}, 1, 0},
{0x7B, (uint8_t []){0x00, 0x08, 0x08}, 3, 0},
{0x80, (uint8_t []){0x55, 0x62, 0x2F, 0x17, 0xF0, 0x52, 0x70, 0xD2, 0x52, 0x62, 0xEA}, 11, 0},
{0x81, (uint8_t []){0x26, 0x52, 0x72, 0x27}, 4, 0},
{0x84, (uint8_t []){0x92, 0x25}, 2, 0},
{0x87, (uint8_t []){0x10, 0x10, 0x58, 0x00, 0x02, 0x3A}, 6, 0},
{0x88, (uint8_t []){0x00, 0x00, 0x2C, 0x10, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x06}, 15, 0},
{0x89, (uint8_t []){0x00, 0x00, 0x00}, 3, 0},
{0x8A, (uint8_t []){0x13, 0x00, 0x2C, 0x00, 0x00, 0x2C, 0x10, 0x10, 0x00, 0x3E, 0x19}, 11, 0},
{0x8B, (uint8_t []){0x15, 0xB1, 0xB1, 0x44, 0x96, 0x2C, 0x10, 0x97, 0x8E}, 9, 0},
{0x8C, (uint8_t []){0x1D, 0xB1, 0xB1, 0x44, 0x96, 0x2C, 0x10, 0x50, 0x0F, 0x01, 0xC5, 0x12, 0x09}, 13, 0},
{0x8D, (uint8_t []){0x0C}, 1, 0},
{0x8E, (uint8_t []){0x33, 0x01, 0x0C, 0x13, 0x01, 0x01}, 6, 0},
{0xB3, (uint8_t []){0x00, 0x30}, 2, 0},
{0xF1, (uint8_t []){0x00}, 1, 0},
{0x71, (uint8_t []){0xD0}, 1, 0},
{0x66, (uint8_t []){0x02, 0x3F}, 2, 0},
{0xBE, (uint8_t []){0x26, 0x00, 0x9D}, 3, 0},
{0x70, (uint8_t []){0x01, 0xA0, 0x11, 0x40, 0xE0, 0x00, 0x11, 0x69, 0x11, 0x00, 0x00, 0x1A}, 12, 0},
{0x90, (uint8_t []){0x04, 0x04, 0x55, 0x74, 0x00, 0x40, 0x43, 0x27, 0x27}, 9, 0},
{0x91, (uint8_t []){0x04, 0x04, 0x55, 0x75, 0x00, 0x40, 0x42, 0x27, 0x27}, 9, 0},
{0x92, (uint8_t []){0x04, 0x44, 0x55, 0xC0, 0x06, 0x00, 0x07, 0x05, 0x90, 0x27}, 10, 0},
{0x93, (uint8_t []){0x04, 0x43, 0x11, 0x00, 0x00, 0x00, 0x00, 0x05, 0x90, 0x27}, 10, 0},
{0x94, (uint8_t []){0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 6, 0},
{0x95, (uint8_t []){0x96, 0x16, 0x00, 0x00, 0xFF}, 5, 0},
{0x96, (uint8_t []){0x44, 0x53, 0x03, 0x12, 0x23, 0x24, 0x06, 0x05, 0x94, 0x27, 0x00, 0x44}, 12, 0},
{0x97, (uint8_t []){0x44, 0x53, 0x47, 0x56, 0x20, 0x20, 0x02, 0x01, 0x94, 0x27, 0x00, 0x44}, 12, 0},
{0xBA, (uint8_t []){0x55, 0x94, 0x2D, 0x94, 0x27}, 5, 0},
{0x9A, (uint8_t []){0x40, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00}, 7, 0},
{0x9B, (uint8_t []){0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00}, 7, 0},
{0x9C, (uint8_t []){0x5C, 0x12, 0x00, 0x00, 0x10, 0x12, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x00}, 13, 0},
{0x9D, (uint8_t []){0x8A, 0x51, 0x00, 0x00, 0x00, 0x80, 0x1E, 0x01}, 8, 0},
{0x9E, (uint8_t []){0x51, 0x00, 0x00, 0x00, 0x80, 0x1E, 0x01}, 7, 0},
{0xB4, (uint8_t []){0x1D, 0x1C, 0x1E, 0x0B, 0x14, 0x02, 0x13, 0x09, 0x1E, 0x00, 0x1E, 0x10}, 12, 0},
{0xB5, (uint8_t []){0x1D, 0x1C, 0x1E, 0x0A, 0x15, 0x03, 0x11, 0x08, 0x1E, 0x01, 0x1E, 0x12}, 12, 0},
{0xB6, (uint8_t []){0x77, 0x77, 0x00, 0x0A, 0xFF, 0x0A, 0xFF}, 7, 0},
{0x86, (uint8_t []){0xCD, 0x04, 0xB1, 0x02, 0x58, 0x12, 0x58, 0x0C, 0x13, 0x01, 0xA5, 0x00, 0xA5, 0xA5}, 14, 0},
{0xB7, (uint8_t []){0x07, 0x0A, 0x0E, 0x06, 0x05, 0x03, 0x2B, 0x03, 0x03, 0x42, 0x07, 0x10, 0x10, 0x2E, 0x3F, 0x0D}, 16, 0},
{0xB8, (uint8_t []){0x07, 0x0A, 0x0D, 0x05, 0x05, 0x02, 0x2B, 0x02, 0x03, 0x42, 0x06, 0x10, 0x0F, 0x2E, 0x3F, 0x0D}, 16, 0},
{0xB9, (uint8_t []){0x23, 0x23}, 2, 0},
{0xBF, (uint8_t []){0x10, 0x14, 0x14, 0x0B, 0x0B, 0x0B}, 6, 0},
{0xF2, (uint8_t []){0x00}, 1, 0},
{0x73, (uint8_t []){0x04, 0xDA, 0x12, 0x54, 0x47}, 5, 0},
{0x77, (uint8_t []){0x6B, 0x5B, 0xFD, 0xC3, 0xC5}, 5, 0},
{0x7A, (uint8_t []){0x15, 0x27}, 2, 0},
{0x7B, (uint8_t []){0x04, 0x57}, 2, 0},
{0x7E, (uint8_t []){0x01, 0x0E}, 2, 0},
{0xBF, (uint8_t []){0x36}, 1, 0},
{0xE3, (uint8_t []){0x40, 0x40}, 2, 0},
{0xF0, (uint8_t []){0x00}, 1, 0},
{0xD0, (uint8_t []){0x00}, 1, 0},
{0x2A, (uint8_t []){0x00, 0x00, 0x01, 0x3F}, 4, 0},
{0x2B, (uint8_t []){0x00, 0x00, 0x01, 0xDF}, 4, 0},
{0x21, NULL, 0, 0},
{0x11, NULL, 0, 120},
{0x29, NULL, 0, 0},
{0x2C, NULL, 0, 0},
{0x3A, (uint8_t []){0x01}, 1, 0},
{0x36, (uint8_t []){0x00}, 1, 0},
{0x35, (uint8_t []){0x01}, 1, 20},
};
const st77922_lcd_init_cmd_t* st77922_board_init_commands(size_t* count) {
*count = sizeof(init_commands) / sizeof(init_commands[0]);
return init_commands;
}
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <esp_lcd_st77922.h>
#ifdef __cplusplus
extern "C" {
#endif
const st77922_lcd_init_cmd_t* st77922_board_init_commands(size_t* count);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,207 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/st77922_touch.h>
#include <st77922_module.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/pointer.h>
#include <tactility/log.h>
#include <freertos/task.h>
#include <algorithm>
#include <cstdlib>
#define TAG "ST77922 touch"
#define GET_CONFIG(device) (static_cast<const St77922TouchConfig*>((device)->config))
static constexpr uint16_t REG_MAX_TOUCHES = 0x0009;
static constexpr uint16_t REG_TOUCH_INFO = 0x0010;
static constexpr uint16_t REG_TOUCH_POINT0 = 0x0014;
static constexpr uint8_t MAX_POINTS = 10;
static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(1000);
struct TouchPoint {
uint16_t x;
uint16_t y;
};
struct St77922TouchInternal {
Device* i2c;
GpioDescriptor* reset;
uint8_t supported_points;
uint8_t point_count;
TouchPoint points[MAX_POINTS];
bool swap_xy;
bool mirror_x;
bool mirror_y;
};
static error_t read_register(Device* i2c, uint16_t reg, uint8_t* data, size_t size, TickType_t timeout) {
const uint8_t address[] = {
static_cast<uint8_t>(reg >> 8),
static_cast<uint8_t>(reg & 0xFF),
};
return i2c_controller_write_read(i2c, 0x55, address, sizeof(address), data, size, timeout);
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<St77922TouchInternal*>(calloc(1, sizeof(St77922TouchInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->i2c = parent;
internal->supported_points = 1;
internal->swap_xy = config->swap_xy;
internal->mirror_x = config->mirror_x;
internal->mirror_y = config->mirror_y;
if (config->pin_reset.gpio_controller != nullptr) {
internal->reset = gpio_descriptor_acquire(config->pin_reset.gpio_controller,
config->pin_reset.pin, GPIO_FLAG_DIRECTION_OUTPUT | GPIO_FLAG_ACTIVE_LOW, GPIO_OWNER_GPIO);
if (internal->reset == nullptr) {
free(internal);
return ERROR_RESOURCE;
}
if (gpio_descriptor_set_level(internal->reset, true) != ERROR_NONE) {
gpio_descriptor_release(internal->reset);
free(internal);
return ERROR_RESOURCE;
}
vTaskDelay(pdMS_TO_TICKS(10));
gpio_descriptor_set_level(internal->reset, false);
vTaskDelay(pdMS_TO_TICKS(100));
}
uint8_t supported = 0;
if (read_register(parent, REG_MAX_TOUCHES, &supported, 1, TIMEOUT) == ERROR_NONE
&& supported > 0 && supported <= MAX_POINTS) {
internal->supported_points = supported;
}
LOG_I(TAG, "Controller reports %u touch points", internal->supported_points);
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<St77922TouchInternal*>(device_get_driver_data(device));
if (internal->reset != nullptr) {
gpio_descriptor_release(internal->reset);
}
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
static error_t enter_sleep(Device*) { return ERROR_NOT_SUPPORTED; }
static error_t exit_sleep(Device*) { return ERROR_NOT_SUPPORTED; }
static error_t read_data(Device* device, TickType_t timeout) {
auto* internal = static_cast<St77922TouchInternal*>(device_get_driver_data(device));
uint8_t touch_info = 0;
if (read_register(internal->i2c, REG_TOUCH_INFO, &touch_info, 1, timeout) != ERROR_NONE) {
internal->point_count = 0;
return ERROR_RESOURCE;
}
if ((touch_info & 0x08) == 0) {
internal->point_count = 0;
return ERROR_NONE;
}
uint8_t data[7 * MAX_POINTS] = {};
const size_t read_size = 7 * internal->supported_points;
if (read_register(internal->i2c, REG_TOUCH_POINT0, data, read_size, timeout) != ERROR_NONE) {
internal->point_count = 0;
return ERROR_RESOURCE;
}
internal->point_count = 0;
for (uint8_t index = 0; index < internal->supported_points; index++) {
const uint8_t offset = index * 7;
if ((data[offset] & 0x80) == 0) {
continue;
}
internal->points[internal->point_count++] = {
.x = static_cast<uint16_t>(((data[offset] & 0x3F) << 8) | data[offset + 1]),
.y = static_cast<uint16_t>(((data[offset + 2] & 0x3F) << 8) | data[offset + 3]),
};
}
return ERROR_NONE;
}
static bool get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength,
uint8_t* count, uint8_t maximum) {
auto* internal = static_cast<St77922TouchInternal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
*count = std::min(internal->point_count, maximum);
for (uint8_t index = 0; index < *count; index++) {
uint16_t point_x = internal->points[index].x;
uint16_t point_y = internal->points[index].y;
if (internal->swap_xy) {
std::swap(point_x, point_y);
}
const uint16_t max_x = internal->swap_xy ? config->y_max : config->x_max;
const uint16_t max_y = internal->swap_xy ? config->x_max : config->y_max;
x[index] = internal->mirror_x ? max_x - point_x : point_x;
y[index] = internal->mirror_y ? max_y - point_y : point_y;
if (strength != nullptr) {
strength[index] = 0;
}
}
return *count > 0;
}
static error_t set_swap_xy(Device* device, bool value) {
static_cast<St77922TouchInternal*>(device_get_driver_data(device))->swap_xy = value;
return ERROR_NONE;
}
static error_t get_swap_xy(Device* device, bool* value) {
*value = static_cast<St77922TouchInternal*>(device_get_driver_data(device))->swap_xy;
return ERROR_NONE;
}
static error_t set_mirror_x(Device* device, bool value) {
static_cast<St77922TouchInternal*>(device_get_driver_data(device))->mirror_x = value;
return ERROR_NONE;
}
static error_t get_mirror_x(Device* device, bool* value) {
*value = static_cast<St77922TouchInternal*>(device_get_driver_data(device))->mirror_x;
return ERROR_NONE;
}
static error_t set_mirror_y(Device* device, bool value) {
static_cast<St77922TouchInternal*>(device_get_driver_data(device))->mirror_y = value;
return ERROR_NONE;
}
static error_t get_mirror_y(Device* device, bool* value) {
*value = static_cast<St77922TouchInternal*>(device_get_driver_data(device))->mirror_y;
return ERROR_NONE;
}
static const PointerApi pointer_api = {
.enter_sleep = enter_sleep,
.exit_sleep = exit_sleep,
.read_data = read_data,
.get_touched_points = get_touched_points,
.set_swap_xy = set_swap_xy,
.get_swap_xy = get_swap_xy,
.set_mirror_x = set_mirror_x,
.get_mirror_x = get_mirror_x,
.set_mirror_y = set_mirror_y,
.get_mirror_y = get_mirror_y,
};
Driver st77922_touch_driver = {
.name = "st77922-touch",
.compatible = (const char*[]) { "sitronix,st77922-touch", nullptr },
.start_device = start,
.stop_device = stop,
.api = &pointer_api,
.device_type = &POINTER_TYPE,
.owner = &st77922_module,
.internal = nullptr
};
@@ -16,6 +16,8 @@
#include <esp_elf.h>
#include <esp_err.h>
#include <sys/stat.h>
#include <cstdio>
#include <new>
#include <string>
@@ -64,13 +66,22 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
return ERROR_NONE;
}
// location.location can be either an app's install directory or the .elf file directly; the
// former resolves to the per-target binary at {dir}/elf/{CONFIG_IDF_TARGET}.elf.
bool is_regular_file(const std::string& path) {
struct stat path_stat {};
return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode);
}
// location.location can be either an app's install directory or the .elf file directly. A
// "packaged" app's install directory always holds its single binary at the fixed path
// {dir}/bin/{CONFIG_IDF_TARGET}/app.elf - a "terminal" app has no such file (its several
// binaries keep their own names), so this correctly leaves it unresolvable - terminal apps
// aren't run through AppLoaderApi (see app/install.h).
std::string resolve_elf_path(const std::string& path) {
if (path.ends_with(".elf")) {
return path;
}
return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
std::string candidate = path + "/bin/" CONFIG_IDF_TARGET "/app.elf";
return is_regular_file(candidate) ? candidate : "";
}
constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
+9
View File
@@ -9,6 +9,7 @@ tactility_add_module(app-module
PRIV_INCLUDE_DIRS private/
INCLUDE_DIRS include/
REQUIRES TactilityKernel service-module minitar
PRIV_REQUIRES TactilityKernelCpp
)
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close()
@@ -19,3 +20,11 @@ if (NOT APPLE)
tactility_get_module_name(app-module MODULE_NAME)
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
endif ()
# install.cpp resolves an installed binary's fixed bin/<platform>/<binary>.so path itself, so it
# needs the same platform-arch string app-posix-module's own CMakeLists.txt defines for its
# loader (the ESP32 equivalent, CONFIG_IDF_TARGET, comes for free from sdkconfig.h there).
if (NOT ESP_PLATFORM)
tactility_get_module_name(app-module MODULE_NAME)
target_compile_definitions(${MODULE_NAME} PRIVATE "TACTILITY_POSIX_ARCH=\"${CMAKE_SYSTEM_PROCESSOR}\"")
endif ()
+15 -14
View File
@@ -12,8 +12,8 @@ extern "C" {
/**
* Computes the install directory for @a app_id (does not check whether anything is actually
* installed there).
* @param[out] path always NULL-terminated on return, even on failure (empty string if
* @a path_size == 0 - nothing is written in that case; otherwise at least "" is written)
* @param[out] path always NULL-terminated on return, even on failure. Empty when @a path_size
* is 0, since nothing is written in that case.
* @retval ERROR_NONE on success
* @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the
* NULL terminator)
@@ -22,14 +22,15 @@ extern "C" {
error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
/**
* Installs an app from a tarball at @a source_path: extracts it into the app install directory,
* parses the extracted manifest.properties (see app/metadata.h) to determine its id, then
* registers it with app_manager_add() as an AppLocation{APP_LOCATION_PATH, <install dir>} app.
* If an app with the same id is already installed (via a previous app_install() call), it is
* uninstalled first - stopped if running, its old install directory removed - before the new
* one takes its place.
* @param[in] source_path path to a tar file containing the app (must have manifest.properties
* at its root)
* Installs a package from a tarball at @a source_path: extracts it into the package's install
* directory, parses the extracted manifest.properties (see app/package_manifest.h) into a
* PackageManifest and one or more AppManifestBindings, then registers each with app_manager_add()
* as an AppLocation{APP_LOCATION_PATH, <binary path>} app.
* If a package with the same id is already installed (via a previous app_install() call), it is
* uninstalled first: every one of its apps stopped if running, then its old install directory
* removed, before the new one takes its place.
* @param[in] source_path path to a tar file containing the package (must have
* manifest.properties at its root)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND @a source_path doesn't exist / can't be read
* @retval ERROR_INVALID_ARGUMENT the tarball has no valid manifest.properties at its root
@@ -37,11 +38,11 @@ error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
error_t app_install(const char* source_path);
/**
* Uninstalls a previously app_install()-ed app: stops it if currently running, deletes its
* install directory, and unregisters it (app_manager_remove()).
* @param[in] app_id the id the app was installed under (AppMetadata::app_id)
* Uninstalls a previously app_install()-ed package: stops every one of its apps if currently
* running, deletes its install directory, and unregisters all of them (app_manager_remove()).
* @param[in] app_id the package id it was installed under (PackageManifest::id)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND no such app was installed via app_install()
* @retval ERROR_NOT_FOUND no such package was installed via app_install()
*/
error_t app_uninstall(const char* app_id);
+2 -2
View File
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "location.h"
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdbool.h>
#include <stdint.h>
#include "location.h"
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
+45 -2
View File
@@ -3,6 +3,7 @@
#include <app/instance.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/stream.h>
#include <tactility/error.h>
@@ -44,6 +45,48 @@ error_t app_manager_find_manifest(const char* id, struct AppManifest* out_manife
typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context);
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context);
/**
* Registers a package for enumeration via app_manager_for_each_package(). Separate from
* registering its apps - the caller still calls app_manager_add() for each AppManifest.
* @param[in] app_ids the ids of the AppManifest(s) this package registered
* @retval ERROR_INVALID_ARGUMENT a package with the same id is already registered
* @retval ERROR_NONE on success
*/
error_t app_manager_add_package(const struct PackageManifest* package, const char* const* app_ids, size_t app_id_count);
/**
* Unregisters a previously-added package. Does not touch its apps' own registrations.
* @retval ERROR_NOT_FOUND no package with this id is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_remove_package(const char* package_id);
/**
* @param[out] out_package set to a copy of the package on success
* @retval ERROR_NOT_FOUND no package with this id is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_find_package(const char* package_id, struct PackageManifest* out_package);
/** One registered package, handed to AppPackageVisitorFn - see app_manager_for_each_package(). */
struct AppPackage {
struct PackageManifest package;
/** How many entries @a app_ids points to. */
size_t app_id_count;
/** Valid only for the duration of the app_manager_for_each_package() call that produced
* this - copy out what's needed before returning from the visitor. */
const char* const* app_ids;
};
typedef void (*AppPackageVisitorFn)(const struct AppPackage* pkg, void* context);
/**
* Calls @a visitor once for every registered package. Iteration order is unspecified.
* @warning Same threading contract as app_manager_for_each_manifest(): runs with an internal
* lock held - do not call any app_manager_*() function from inside @a visitor.
*/
void app_manager_for_each_package(AppPackageVisitorFn visitor, void* context);
/** One fd-to-stream binding for app_start_with_streams() (app/start.h). Every field is passed
* through to app_stream_subscribe() as-is; see its own doc for the ownership contracts. */
struct AppStreamBinding {
@@ -90,8 +133,8 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);
/**
* Registers @a path as a directory to scan for app manifests - each direct subdirectory of
* @a path is expected to hold a manifest.properties (see app/metadata.h), matching the layout
* app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not
* @a path is expected to hold a manifest.properties (see app/package_manifest.h), matching the
* layout app_install() creates ({install dir}/{package id}/manifest.properties), though this is not
* install/uninstall - it only ever adds/removes manifest registrations, never touches files on
* disk or running instances. No-op if @a path is already registered. Does not scan immediately -
* call app_manager_install_path_scan() to do that.
+11 -6
View File
@@ -11,7 +11,10 @@ extern "C" {
#endif
// Character count, excluding null terminator
#define APP_ID_LENGTH 32
#define APP_MANIFEST_ID_LENGTH 32
// Character count, excluding null terminator
#define APP_MANIFEST_NAME_LENGTH 32
/** Broad classification of an app, used for grouping/launcher presentation. */
enum AppCategory {
@@ -45,10 +48,10 @@ struct AppStackConfig {
/** Describes a registrable app. One manifest exists per app id. */
struct AppManifest {
/** Unique app identifier. Should never be NULL. */
const char* id;
/** Human-readable name. Should never be NULL. */
const char* name;
/** Unique app identifier. Must be NULL-terminated. */
char id[APP_MANIFEST_ID_LENGTH + 1];
/** Human-readable name. Must be NULL-terminated. */
char name[APP_MANIFEST_NAME_LENGTH + 1];
enum AppCategory category;
struct AppLocation location;
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */
@@ -57,7 +60,9 @@ struct AppManifest {
struct AppStackConfig stack;
};
bool app_id_is_valid(const char* id);
bool app_manifest_id_is_valid(const char* id);
bool app_manifest_name_is_valid(const char* name);
bool app_manifest_stack_size_is_valid(const char* value);
#ifdef __cplusplus
}
-74
View File
@@ -1,74 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define APP_METADATA_TARGET_SDK_LENGTH 16
#define APP_METADATA_APP_ID_LENGTH 32
#define APP_METADATA_APP_NAME_LENGTH 32
#define APP_METADATA_APP_VERSION_NAME_LENGTH 16
#define APP_METADATA_REQUIRES_DEVICE_ID_LENGTH 64
struct AppMetadata {
/**
* The SDK version that was used to compile this app. (e.g. "0.6.0")
* Must be NULL-terminated.
*/
char target_sdk[APP_METADATA_TARGET_SDK_LENGTH + 1];
/**
* The identifier by which the app is launched by the system and other apps.
* Must be NULL-terminated.
*/
char app_id[APP_METADATA_APP_ID_LENGTH + 1];
/**
* The user-readable name of the app. Used in UI.
* Must be NULL-terminated.
*/
char app_name[APP_METADATA_APP_NAME_LENGTH + 1];
/**
* The version as it is displayed to the user (e.g. "1.2.0")
* Must be NULL-terminated.
*/
char app_version_name[APP_METADATA_APP_VERSION_NAME_LENGTH + 1];
/** The technical version (must be incremented with new releases of the app) */
uint64_t app_version_code;
/**
* Comma-separated list of device ids the app is restricted to (e.g. "m5stack-tab5"), matching
* the folder names under Devices/. Empty means unrestricted.
* Must be NULL-terminated.
*/
char requires_device_id[APP_METADATA_REQUIRES_DEVICE_ID_LENGTH + 1];
/**
* Stack depth (in words) for the app's task. Optional; 0 means scheduler default.
* @warning Avoid default values: the default is conservative, which wastes memory.
*/
uint32_t stack_depth;
};
/**
* Parses a manifest.properties file at @a path into @a out_metadata, auto-detecting the V1
* (sectioned, e.g. "[app]id=...") or V2 (flat dot-notation, e.g. "app.id=...") format from its
* first line.
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* @a out_metadata's fixed-size buffers
*/
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PACKAGE_MANIFEST_TARGET_SDK_LENGTH 16
#define PACKAGE_MANIFEST_ID_LENGTH 32
#define PACKAGE_MANIFEST_VERSION_NAME_LENGTH 16
#define PACKAGE_MANIFEST_REQUIRES_DEVICE_ID_LENGTH 64
/** Character count, excluding null terminator, for AppManifestBinding::binary. */
#define APP_MANIFEST_BINARY_LENGTH 31
/** Largest number of AppManifest entries package_manifest_parse() can produce from a single
* manifest.properties file. */
#define PACKAGE_MANIFEST_MAX_APP_MANIFESTS 32
/** A package-level manifest.properties: SDK/version metadata that applies to the whole package,
* not to any one of its (possibly several) apps. Not kept in memory at runtime - only used to
* create and cache the AppManifest(s) it describes. */
struct PackageManifest {
/**
* The package identifier (e.g. the install directory name). Distinct from any of its own
* AppManifest ids.
* Must be NULL-terminated.
*/
char id[PACKAGE_MANIFEST_ID_LENGTH + 1];
/**
* The package version as it is displayed to the user (e.g. "1.2.0")
* Must be NULL-terminated.
*/
char version_name[PACKAGE_MANIFEST_VERSION_NAME_LENGTH + 1];
/** The package's technical version (must be incremented with new releases). */
uint64_t version_code;
/**
* The SDK version that was used to compile this package. (e.g. "0.6.0")
* Must be NULL-terminated.
*/
char target_sdk[PACKAGE_MANIFEST_TARGET_SDK_LENGTH + 1];
/**
* Comma-separated list of device ids the package is restricted to (e.g. "m5stack-tab5"),
* matching the folder names under Devices/. Empty means unrestricted.
* Must be NULL-terminated.
*/
char requires_device_id[PACKAGE_MANIFEST_REQUIRES_DEVICE_ID_LENGTH + 1];
/** How many AppManifest entries this package's manifest.properties declared. */
uint32_t app_manifest_count;
};
/** Pairs a parsed AppManifest with the filename (without extension) it installs as under
* bin/<platform>/ - e.g. "main" resolves to bin/posix-x86_64/main.so. Not kept anywhere at
* runtime - same lifetime as PackageManifest, only used to hand parse results to the installer/
* scanner, which resolve `binary` into AppManifest::location::location. */
struct AppManifestBinding {
struct AppManifest manifest;
char binary[APP_MANIFEST_BINARY_LENGTH + 1];
};
/**
* Parses a manifest.properties file at @a path (flat dot-notation, e.g. "app.id=...") into
* @a out_package and @a out_bindings.
* @param[out] out_bindings written with up to @a bindings_capacity entries (see
* PackageManifest::app_manifest_count for how many)
* @param[in] bindings_capacity the capacity of @a out_bindings
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* its fixed-size buffer
* @retval ERROR_BUFFER_OVERFLOW the manifest declares more apps than @a bindings_capacity
*/
error_t app_package_manifest_parse(const char* path, struct PackageManifest* out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#include <string>
// Resolves an AppManifestBinding::binary filename (without extension) to its installed path
// under this fixed, predictable location, mirroring what each platform's own loader
// (app_posix_loader_service.cpp / app_esp32_loader_service.cpp) already resolves a plain
// ".so"/".elf" AppLocation::location straight through unchanged - so setting location.location
// to this exact path means neither loader needs to guess which of a package's several binaries
// an AppManifest refers to.
inline std::string app_resolve_binary_path(const std::string& install_dir, const std::string& binary) {
#ifdef ESP_PLATFORM
return install_dir + "/bin/" CONFIG_IDF_TARGET "/" + binary + ".elf";
#else
return install_dir + "/bin/posix-" TACTILITY_POSIX_ARCH "/" + binary + ".so";
#endif
}
@@ -3,8 +3,11 @@
#include <app/instance.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/private/fd_table.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/semphr.h>
@@ -13,6 +16,7 @@
#include <stdint.h>
#include <string>
#include <unordered_map>
#include <vector>
/**
* A dedicated completion signal(1) for one app instance's task, given as the
@@ -57,8 +61,16 @@ struct AppInstanceRecord {
AppFdTable fd_table {};
};
/** A registered installed package - see app_manager_add_package() (app/manager.h). */
struct AppPackageRecord {
struct PackageManifest package;
std::vector<std::string> app_ids;
};
struct AppLedger {
std::unordered_map<std::string, const AppManifest*> manifests;
// OptExternalAllocator: bigger entries than `manifests`, and unlike `instances` isn't on the app start/stop hot path.
std::unordered_map<std::string, AppPackageRecord, std::hash<std::string>, std::equal_to<std::string>, tt::OptExternalAllocator<std::pair<const std::string, AppPackageRecord>>> packages;
std::unordered_map<uint32_t, AppInstanceRecord> instances;
uint32_t next_instance_id = 1;
Mutex mutex {};
@@ -1,35 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/metadata.h>
#include <map>
#include <string>
/** Shared helpers + per-format parsers for app_metadata_parse() (source/app_metadata_parsing.cpp)
* - split out like the old tt::app manifest parser (AppManifestParsing/V1/V2.cpp) that this is
* modelled on, one file per format plus a shared dispatcher. */
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
bool app_metadata_is_valid_format_version(const std::string& version);
bool app_metadata_is_valid_name(const std::string& name);
bool app_metadata_is_valid_version_name(const std::string& version);
bool app_metadata_is_valid_version_code(const std::string& version);
bool app_metadata_is_valid_stack_size(const std::string& value);
/** Validates a comma-separated list of device ids (alphanumeric + '-' items, matching Devices/<id> folder names). */
bool app_metadata_is_valid_device_id_list(const std::string& value);
/** Copies @a value into @a dest (a fixed-size buffer of @a dest_size bytes, including the NULL
* terminator) if it fits.
* @retval false @a value doesn't fit in @a dest_size bytes - @a dest is left untouched */
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value);
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map into @a out_metadata. */
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_metadata. */
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char));
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/package_manifest.h>
#include <TactilityCpp/Allocator.h>
#include <map>
#include <string>
#include <vector>
/** Shared helpers + parser for app_package_manifest_parse() (source/package_manifest_parsing.cpp). */
bool app_package_manifest_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
bool app_package_manifest_is_valid_format_version(const std::string& version);
bool app_package_manifest_is_valid_version_name(const std::string& version);
bool app_package_manifest_is_valid_version_code(const std::string& version);
bool app_package_manifest_is_valid_bool(const std::string& value);
/** Validates a comma-separated list of device ids (alphanumeric + '-' items, matching Devices/<id> folder names). */
bool app_package_manifest_is_valid_device_id_list(const std::string& value);
/** Validates a binary filename stem (AppManifestBinding::binary): alphanumeric plus '.', '_', '-'. */
bool app_package_manifest_is_valid_binary_name(const std::string& value);
/** Copies @a value into @a dest (a fixed-size buffer of @a dest_size bytes, including the NULL
* terminator) if it fits.
* @retval false @a value doesn't fit in @a dest_size bytes - @a dest is left untouched */
bool app_package_manifest_copy_bounded(char* dest, size_t dest_size, const std::string& value);
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_package
* and (unless @a bindings_capacity is 0) its single AppManifestBinding, out_bindings[0].
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT a required key is missing or a value doesn't fit/validate
* @retval ERROR_BUFFER_OVERFLOW @a bindings_capacity is nonzero but smaller than needed */
error_t package_manifest_parse_v2(const std::map<std::string, std::string>& properties, struct PackageManifest& out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
/** Parses a V3 (flat dot-notation with 0-indexed "app.N.*" blocks) manifest map into
* @a out_package and up to @a bindings_capacity AppManifestBinding entries.
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT a required key is missing or a value doesn't fit/validate
* @retval ERROR_BUFFER_OVERFLOW @a bindings_capacity is nonzero but smaller than the
* manifest's app_manifest_count */
error_t package_manifest_parse_v3(const std::map<std::string, std::string>& properties, struct PackageManifest& out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
bool app_package_manifest_validate_string(const std::string& value, bool (*is_valid_char)(char));
/** Convenience wrapper around app_package_manifest_parse(): parses @a path once to learn
* PackageManifest::app_manifest_count, then resizes @a out_bindings to fit exactly and parses
* again to fill it - so the caller never needs to pre-allocate a fixed maximum.
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* its fixed-size buffer */
error_t app_package_manifest_parse_into(const char* path, struct PackageManifest& out_package, std::vector<struct AppManifestBinding, tt::OptExternalAllocator<struct AppManifestBinding>>& out_bindings);
+93 -75
View File
@@ -2,10 +2,14 @@
#include <app/install.h>
#include <app/manager.h>
#include <app/metadata.h>
#include <app/package_manifest.h>
#include <app/private/binary_path.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
#include <app/private/package_manifest_parsing.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>
@@ -29,7 +33,7 @@ constexpr auto* TAG = "app_install";
namespace {
// region Filesystem helpers (app-module may not depend upward on Tactility::file - see
// region Filesystem helpers (app-module may not depend upward on Tactility::file; see
// app_metadata_parsing.cpp for the same constraint applied to properties-file loading)
std::string last_path_segment(const std::string& path) {
@@ -164,9 +168,9 @@ bool untar(const std::string& tar_path, const std::string& destination_path) {
// endregion
// region Staging-path lock: at most one caller may clean up/populate a given staging_path at a
// time, keyed by source basename. The HTTP server and app tasks can call app_install()
// concurrently, e.g. two uploads sharing a source basename - without this, one call's cleanup
// can delete or overwrite the staging directory another call is still extracting into.
// time, keyed by source basename. The HTTP server and app tasks can call app_install_package()
// concurrently, e.g. two uploads sharing a source basename; without this lock, one call's
// cleanup could delete or overwrite the staging directory another call is still extracting into.
struct StagingLock {
Mutex mutex {};
@@ -223,18 +227,19 @@ void release_staging_lock(const std::string& path) {
// endregion
// region Installed-app registry: owns the AppManifest (and its id/name/path strings) that
// app_manager's ledger only keeps a non-owning pointer to (see app_manager_add()'s contract).
// region Installed-package registry: owns the AppManifests (and their backing location-path
// strings) that app_manager's ledger only keeps non-owning pointers to (see app_manager_add()'s
// contract). AppManifest::id/name own their own storage directly (fixed arrays), so this doesn't
// need to separately own those.
struct InstalledAppRecord {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
struct InstalledPackageRecord {
std::string path; // install directory
std::vector<AppManifest> manifests; // one per AppManifest the package declared
std::vector<std::string> locations; // backs manifests[i].location.location, same indices
};
struct InstallRegistry {
std::unordered_map<std::string, std::unique_ptr<InstalledAppRecord>> apps;
std::unordered_map<std::string, std::unique_ptr<InstalledPackageRecord>> apps;
Mutex mutex {};
InstallRegistry() { mutex_construct(&mutex); }
@@ -245,46 +250,68 @@ InstallRegistry& install_registry() {
return registry;
}
// Registers @a app_dir_path (already confirmed to hold a valid manifest.properties, parsed into
// @a metadata) with app_manager_add(), taking ownership of its id/name/path strings.
// @warning Caller must hold install_registry().mutex, and must have already ensured
// @a metadata.app_id isn't already registered (app_manager_add() rejects duplicates, but the
// InstalledAppRecord for the earlier registration would leak since this always inserts fresh).
error_t register_installed_app_locked(const std::string& app_dir_path, const AppMetadata& metadata) {
// Registers every AppManifest in @a bindings (@a count of them) with app_manager_add(), taking
// ownership of their backing location strings, then registers @a package itself.
// @warning Caller must hold install_registry().mutex.
error_t register_installed_package_locked(const PackageManifest& package, const std::string& install_path, const AppManifestBinding* bindings, size_t count) {
auto& registry = install_registry();
auto record = std::make_unique<InstalledAppRecord>();
record->id = metadata.app_id;
record->name = metadata.app_name;
record->path = app_dir_path;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
};
auto record = std::make_unique<InstalledPackageRecord>();
record->path = install_path;
// Sized once, up front: manifests[i].location.location points into locations[i].c_str(),
// which would dangle if either vector reallocated afterward.
record->manifests.resize(count);
record->locations.resize(count);
// Belt-and-braces: app_install()'s earlier app_manager_remove() call is meant to have
// already cleared any stale registration for this id (e.g. left over from
// app_manager_install_path_scan()'s separate registry), but that call happens before the
// tarball is even extracted - remove once more, right before add, so a duplicate id can
// never turn a filesystem-level install success into a reported failure.
app_manager_remove(record->id.c_str());
error_t add_result = app_manager_add(&record->manifest);
if (add_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register app '%s': %s", record->id.c_str(), error_to_string(add_result));
return add_result;
for (size_t i = 0; i < count; i++) {
record->manifests[i] = bindings[i].manifest;
record->locations[i] = app_resolve_binary_path(install_path, bindings[i].binary);
record->manifests[i].location = { APP_LOCATION_PATH, const_cast<char*>(record->locations[i].c_str()) };
}
registry.apps[record->id] = std::move(record);
for (size_t i = 0; i < count; i++) {
// The caller's earlier uninstall_locked() call is meant to have already cleared any stale registration for this id
// (e.g. left over from app_manager_install_path_scan()'s separate registry),
// but that call happens before the package is even extracted / the binaries moved into place; remove once more
// right before add, so a duplicate id can never turn a filesystem-level install success into a reported failure.
// Only if installed (APP_LOCATION_PATH) - never steal a built-in's id.
AppManifest existing {};
if (app_manager_find_manifest(record->manifests[i].id, &existing) == ERROR_NONE && existing.location.type == APP_LOCATION_PATH) {
app_manager_remove(record->manifests[i].id);
}
error_t add_result = app_manager_add(&record->manifests[i]);
if (add_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register app '%s': %s", record->manifests[i].id, error_to_string(add_result));
// All-or-nothing: unregister whatever this package already added before failing.
for (size_t j = 0; j < i; j++) {
app_manager_remove(record->manifests[j].id);
}
return add_result;
}
}
std::vector<const char*> app_id_ptrs;
app_id_ptrs.reserve(count);
for (size_t i = 0; i < count; i++) {
app_id_ptrs.push_back(record->manifests[i].id);
}
app_manager_remove_package(package.id);
error_t add_package_result = app_manager_add_package(&package, app_id_ptrs.data(), app_id_ptrs.size());
if (add_package_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register package '%s': %s", package.id, error_to_string(add_package_result));
for (size_t i = 0; i < count; i++) {
app_manager_remove(record->manifests[i].id);
}
return add_package_result;
}
registry.apps[package.id] = std::move(record);
return ERROR_NONE;
}
// Stops every currently-running instance of @a manifest. Collects matching instance ids while
// holding the ledger lock, then calls app_manager_stop() on each after releasing it - that call
// holding the ledger lock, then calls app_manager_stop() on each after releasing it: that call
// bound-joins the instance's thread, which must not happen while the ledger mutex (also taken by
// the instance's own thread_main()) is held, or the two threads would deadlock each other.
void stop_all_instances_of(const AppManifest* manifest) {
@@ -305,20 +332,22 @@ void stop_all_instances_of(const AppManifest* manifest) {
}
// Caller must already hold install_registry().mutex
error_t uninstall_locked(const std::string& app_id) {
error_t uninstall_locked(const std::string& package_id) {
auto& registry = install_registry();
auto iterator = registry.apps.find(app_id);
auto iterator = registry.apps.find(package_id);
if (iterator == registry.apps.end()) {
return ERROR_NOT_FOUND;
}
// Can't uninstall in-memory apps
if (iterator->second->manifest.location.type != APP_LOCATION_PATH) {
return ERROR_NOT_SUPPORTED;
for (const auto& manifest : iterator->second->manifests) {
// Can't uninstall in-memory apps
if (manifest.location.type != APP_LOCATION_PATH) {
continue;
}
stop_all_instances_of(&manifest);
app_manager_remove(manifest.id);
}
stop_all_instances_of(&iterator->second->manifest);
app_manager_remove(app_id.c_str());
app_manager_remove_package(package_id.c_str());
delete_recursively(iterator->second->path);
registry.apps.erase(iterator);
@@ -352,7 +381,7 @@ error_t app_get_install_path(const char* app_id, char* path, size_t path_size) {
}
error_t app_install(const char* source_path) {
LOG_I(TAG, "Installing app from %s", source_path);
LOG_I(TAG, "Installing app package from %s", source_path);
std::string app_parent_path;
if (!get_app_install_directory(app_parent_path)) {
@@ -389,8 +418,9 @@ error_t app_install(const char* source_path) {
return ERROR_INVALID_ARGUMENT;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
PackageManifest package {};
std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>> app_bindings;
if (app_package_manifest_parse_into(manifest_path.c_str(), package, app_bindings) != ERROR_NONE) {
LOG_E(TAG, "Install failed: invalid manifest");
delete_recursively(staging_path);
release_staging_lock(staging_path);
@@ -400,24 +430,13 @@ error_t app_install(const char* source_path) {
auto& registry = install_registry();
mutex_lock(&registry.mutex);
// Replace any previous install of this app id (mirrors the old install()'s "already
// running/present" handling). uninstall_locked() only clears app_install.cpp's own
// registry - the same app id may instead be registered by app_manager_install_path_scan()
// (manager.cpp's separate registry, scanning this same directory tree), which
// uninstall_locked() doesn't know about. Clear the app-manager registration unconditionally
// too, or app_manager_add() below rejects the re-add as a duplicate.
uninstall_locked(metadata.app_id);
// Replace any previous installation of this package - this also handles the app_manager
// registrations of its old AppManifests, so there's no separate app_manager_remove() needed
// here (register_installed_package_locked() below still defends against a stale registration
// per individual id, e.g. one left by app_manager_install_path_scan()'s separate registry).
uninstall_locked(package.id);
error_t remove_result = app_manager_remove(metadata.app_id);
if (remove_result != ERROR_NONE && remove_result != ERROR_NOT_FOUND) {
LOG_E(TAG, "Install failed: failed to remove existing installation");
mutex_unlock(&registry.mutex);
delete_recursively(staging_path);
release_staging_lock(staging_path);
return ERROR_RESOURCE;
}
auto final_path = app_parent_path + "/" + metadata.app_id;
auto final_path = app_parent_path + "/" + package.id;
delete_recursively(final_path);
if (rename(staging_path.c_str(), final_path.c_str()) != 0) {
@@ -429,9 +448,8 @@ error_t app_install(const char* source_path) {
}
release_staging_lock(staging_path);
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
// already removed any previous registration for this exact id.
error_t add_result = register_installed_app_locked(final_path, metadata);
// app_bindings.size(), not package.app_manifest_count - the safe bound to index by.
error_t add_result = register_installed_package_locked(package, final_path, app_bindings.data(), app_bindings.size());
mutex_unlock(&registry.mutex);
return add_result;
+174 -44
View File
@@ -1,13 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manager.h>
#include <app/metadata.h>
#include <app/package_manifest.h>
#include <app/private/arguments.h>
#include <app/private/binary_path.h>
#include <app/private/fd_table.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
#include <app/private/manager_internal.h>
#include <app/private/package_manifest_parsing.h>
#include <app/private/scheduler.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
#include <tactility/log.h>
@@ -72,6 +76,67 @@ void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context)
mutex_unlock(&ledger.mutex);
}
error_t app_manager_add_package(const PackageManifest* package, const char* const* app_ids, size_t app_id_count) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
if (ledger.packages.contains(package->id)) {
mutex_unlock(&ledger.mutex);
LOG_E(TAG, "Package with id '%s' is already registered", package->id);
return ERROR_INVALID_ARGUMENT;
}
AppPackageRecord record { .package = *package };
record.app_ids.reserve(app_id_count);
for (size_t i = 0; i < app_id_count; i++) {
record.app_ids.emplace_back(app_ids[i]);
}
ledger.packages[package->id] = std::move(record);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
error_t app_manager_remove_package(const char* package_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.packages.find(package_id);
if (iterator == ledger.packages.end()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
ledger.packages.erase(iterator);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
error_t app_manager_find_package(const char* package_id, PackageManifest* out_package) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.packages.find(package_id);
if (iterator == ledger.packages.end()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
*out_package = iterator->second.package;
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
void app_manager_for_each_package(AppPackageVisitorFn visitor, void* context) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (auto& [id, record] : ledger.packages) {
std::vector<const char*> app_id_ptrs;
app_id_ptrs.reserve(record.app_ids.size());
for (const auto& app_id : record.app_ids) {
app_id_ptrs.push_back(app_id.c_str());
}
AppPackage pkg { .package = record.package, .app_id_count = app_id_ptrs.size(), .app_ids = app_id_ptrs.data() };
visitor(&pkg, context);
}
mutex_unlock(&ledger.mutex);
}
error_t app_manager_start_internal(const AppManifest* manifest, AppLocation location, AppStackConfig stack, AppInstanceId parent_instance_id, int argc, const char* const argv_in[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) {
char** argv = app_arguments_copy(argc, argv_in);
if (argc > 0 && argv == nullptr) {
@@ -200,19 +265,19 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
namespace {
// Owns the AppManifest (and its id/name/path strings) that app_manager_add() only keeps a
// non-owning pointer to. Separate from app_install.cpp's registry: scanning only
// adds/removes registrations, never touches disk or running instances.
struct ScannedAppManifest {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
// Owns the AppManifests (and their backing location-path strings) that app_manager_add() only
// keeps non-owning pointers to. Separate from app_install.cpp's registry: scanning only
// adds/removes registrations, never touches disk or running instances. AppManifest::id/name own
// their own storage directly (fixed arrays), so this doesn't need to separately own those.
struct ScannedPackageManifest {
std::string path; // scanned directory
std::vector<AppManifest> manifests; // one per AppManifest the package declared
std::vector<std::string> locations; // backs manifests[i].location.location, same indices
};
struct InstallPathRegistry {
std::vector<std::string> paths;
std::unordered_map<std::string, std::unique_ptr<ScannedAppManifest>> scanned;
std::unordered_map<std::string, std::unique_ptr<ScannedPackageManifest>> scanned;
Mutex mutex {};
InstallPathRegistry() { mutex_construct(&mutex); }
@@ -249,50 +314,69 @@ void app_manager_install_path_scan(void) {
app_fs_list_direct_subdirectories(root, found_app_dirs);
}
// Snapshot once so the rest of the scan doesn't hold registry.mutex.
// Snapshot once so the rest of the scan doesn't hold registry.mutex. Keeps each known
// package's own manifest ids too, so a package whose directory has disappeared can have all
// of its (possibly several) app_manager registrations removed below, not just one.
struct KnownPackage {
std::string path;
std::vector<std::string> manifest_ids;
};
mutex_lock(&registry.mutex);
std::unordered_map<std::string, std::string> known_paths;
std::unordered_map<std::string, KnownPackage> known_packages;
for (const auto& [id, record] : registry.scanned) {
known_paths.emplace(id, record->path);
KnownPackage known { .path = record->path };
for (const auto& manifest : record->manifests) {
known.manifest_ids.emplace_back(manifest.id);
}
known_packages.emplace(id, std::move(known));
}
mutex_unlock(&registry.mutex);
// Parses without registry.mutex held; filesystem IO is slow.
std::vector<std::unique_ptr<ScannedAppManifest>> new_records;
std::vector<std::unique_ptr<ScannedPackageManifest>> new_records;
std::vector<std::string> new_package_ids;
std::vector<PackageManifest> new_packages;
for (const auto& app_dir : found_app_dirs) {
auto manifest_path = app_dir + "/manifest.properties";
if (!app_fs_is_file(manifest_path)) {
continue;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
PackageManifest package {};
// Heap-allocated (not a stack array - too large for a typical app task's stack; this
// function runs on whichever task calls app_manager_install_path_scan(), e.g. Boot's, via
// registerInstalledAppsFromFileSystems()) and sized to fit exactly, not pre-allocated to
// some fixed maximum (see app_package_manifest_parse_into()). OptExternalAllocator prefers
// PSRAM for this transient buffer, freeing up scarce internal RAM.
std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>> app_bindings;
if (app_package_manifest_parse_into(manifest_path.c_str(), package, app_bindings) != ERROR_NONE) {
LOG_W(TAG, "Invalid manifest at %s", manifest_path.c_str());
continue;
}
if (known_paths.contains(metadata.app_id)) {
if (known_packages.contains(package.id)) {
continue;
}
auto record = std::make_unique<ScannedAppManifest>();
record->id = metadata.app_id;
record->name = metadata.app_name;
auto record = std::make_unique<ScannedPackageManifest>();
record->path = app_dir;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
};
// Sized once, up front: manifests[i].location.location points into locations[i].c_str(),
// which would dangle if either vector reallocated afterward.
record->manifests.resize(app_bindings.size());
record->locations.resize(app_bindings.size());
for (size_t i = 0; i < app_bindings.size(); i++) {
record->manifests[i] = app_bindings[i].manifest;
record->locations[i] = app_resolve_binary_path(app_dir, app_bindings[i].binary);
record->manifests[i].location = { APP_LOCATION_PATH, const_cast<char*>(record->locations[i].c_str()) };
}
new_package_ids.emplace_back(package.id);
new_packages.push_back(package);
new_records.push_back(std::move(record));
}
std::vector<std::string> missing_ids;
for (const auto& [id, path] : known_paths) {
if (!app_fs_is_directory(path)) {
for (const auto& [id, known] : known_packages) {
if (!app_fs_is_directory(known.path)) {
missing_ids.push_back(id);
}
}
@@ -301,23 +385,60 @@ void app_manager_install_path_scan(void) {
// registry.mutex would fix a lock order an opposite-order caller could deadlock against.
// registry.mutex is retaken afterward only to publish the in-memory results.
for (const auto& id : missing_ids) {
app_manager_remove(id.c_str());
}
std::vector<std::unique_ptr<ScannedAppManifest>> added_records;
for (auto& record : new_records) {
if (app_manager_add(&record->manifest) == ERROR_NONE) {
added_records.push_back(std::move(record));
} else {
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->id.c_str());
for (const auto& manifest_id : known_packages.at(id).manifest_ids) {
app_manager_remove(manifest_id.c_str());
}
app_manager_remove_package(id.c_str());
}
std::vector<std::unique_ptr<ScannedPackageManifest>> added_records;
std::vector<std::string> added_package_ids;
for (size_t r = 0; r < new_records.size(); r++) {
auto& record = new_records[r];
// known_packages is only a pre-scan snapshot - two new directories can still share an id.
if (std::ranges::find(added_package_ids, new_package_ids[r]) != added_package_ids.end()) {
LOG_W(TAG, "Skipping duplicate package id %s found in this scan", new_package_ids[r].c_str());
continue;
}
size_t added_count = 0;
for (; added_count < record->manifests.size(); added_count++) {
if (app_manager_add(&record->manifests[added_count]) != ERROR_NONE) {
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->manifests[added_count].id);
break;
}
}
if (added_count != record->manifests.size()) {
// All-or-nothing: unregister whatever this package already added before failing.
for (size_t j = 0; j < added_count; j++) {
app_manager_remove(record->manifests[j].id);
}
continue;
}
std::vector<const char*> app_id_ptrs;
app_id_ptrs.reserve(record->manifests.size());
for (const auto& app_manifest : record->manifests) {
app_id_ptrs.push_back(app_manifest.id);
}
if (app_manager_add_package(&new_packages[r], app_id_ptrs.data(), app_id_ptrs.size()) != ERROR_NONE) {
LOG_E(TAG, "Failed to register package %s (duplicate id?)", new_packages[r].id);
for (const auto& app_manifest : record->manifests) {
app_manager_remove(app_manifest.id);
}
continue;
}
added_package_ids.push_back(new_package_ids[r]);
added_records.push_back(std::move(record));
}
mutex_lock(&registry.mutex);
for (const auto& id : missing_ids) {
registry.scanned.erase(id);
}
for (auto& record : added_records) {
registry.scanned[record->id] = std::move(record);
for (size_t i = 0; i < added_records.size(); i++) {
registry.scanned[added_package_ids[i]] = std::move(added_records[i]);
}
mutex_unlock(&registry.mutex);
}
@@ -332,7 +453,10 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
return ERROR_NOT_FOUND;
}
const AppManifest* manifest = &iterator->second->manifest;
// Pointer, not a copy: the ledger's own AppInstanceRecord::manifest pointers (set by
// app_manager_add() from this exact vector) are compared against it by address below, same
// as the pre-existing single-manifest version of this function did.
const std::vector<AppManifest>* manifests = &iterator->second->manifests;
auto path = iterator->second->path;
mutex_unlock(&registry.mutex);
@@ -342,8 +466,11 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (const auto& [id, record] : ledger.instances) {
if (record.manifest == manifest) {
instance_ids.push_back(id);
for (const auto& manifest : *manifests) {
if (record.manifest == &manifest) {
instance_ids.push_back(id);
break;
}
}
}
mutex_unlock(&ledger.mutex);
@@ -354,7 +481,10 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
// app_manager_remove() takes ledger.mutex; call outside registry.mutex too, matching
// the lock order in app_manager_install_path_scan().
app_manager_remove(app_id);
for (const auto& manifest : *manifests) {
app_manager_remove(manifest.id);
}
app_manager_remove_package(app_id);
// Delete before erasing the scan record, so a failed deletion still leaves the
// entry discoverable for a retry.
+18 -3
View File
@@ -1,15 +1,30 @@
#include <app/manifest.h>
#include <app/private/metadata_parsing_internal.h>
#include <app/private/package_manifest_parsing.h>
#include <stdlib.h>
#include <string.h>
extern "C" {
bool app_id_is_valid(const char* id) {
bool app_manifest_id_is_valid(const char* id) {
auto size = strlen(id);
return size >= 5 && size <= APP_ID_LENGTH && app_metadata_validate_string(id, [](char c) {
return size >= 5 && size <= APP_MANIFEST_ID_LENGTH && app_package_manifest_validate_string(id, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_manifest_name_is_valid(const char* name) {
auto size = strlen(name);
return size >= 2 && size <= APP_MANIFEST_NAME_LENGTH && app_package_manifest_validate_string(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
});
}
bool app_manifest_stack_size_is_valid(const char* value) {
// 10 digits is the maximum decimal width of uint32_t.
auto size = strlen(value);
return size > 0 && size <= 10 && app_package_manifest_validate_string(value, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
}
@@ -1,190 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/metadata.h>
#include <app/private/metadata_parsing_internal.h>
#include <tactility/log.h>
#include <cctype>
#include <cstring>
#include <fstream>
#include <map>
#include <string>
constexpr auto* TAG = "app_metadata";
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
namespace {
#define validate_string app_metadata_validate_string
std::string trim(const std::string& value) {
constexpr auto* whitespace = " \t\r\n";
auto start = value.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
auto end = value.find_last_not_of(whitespace);
return value.substr(start, end - start + 1);
}
/** Validates a comma-separated list: non-empty, no leading/trailing/double commas (which would
* produce an empty item), and every item passing @a is_valid_item. */
bool validate_csv_list(const std::string& value, bool (*is_valid_item)(const std::string&)) {
if (value.empty()) {
return false;
}
size_t start = 0;
while (true) {
auto comma = value.find(',', start);
auto end = comma == std::string::npos ? value.size() : comma;
if (end == start || !is_valid_item(value.substr(start, end - start))) {
return false;
}
if (comma == std::string::npos) {
return true;
}
start = comma + 1;
}
}
/** manifest.properties format: "key=value" lines, "[section]" lines prefix every following key
* until the next section, "#" lines are comments, blank lines are skipped. Deliberately a local,
* minimal re-implementation rather than depending on Tactility's file::loadPropertiesFile() -
* app-module (like every other kernel module) may not depend upward on the Tactility layer. */
bool load_properties(const std::string& path, std::map<std::string, std::string>& out_properties, std::string& out_first_line) {
std::ifstream file(path);
if (!file.is_open()) {
return false;
}
std::string line;
std::string section_prefix;
bool got_first_line = false;
while (std::getline(file, line)) {
auto trimmed_line = trim(line);
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
continue;
}
if (!got_first_line) {
out_first_line = trimmed_line;
got_first_line = true;
}
if (trimmed_line.starts_with("[")) {
section_prefix = trimmed_line;
continue;
}
auto separator_index = trimmed_line.find('=');
if (separator_index == std::string::npos) {
LOG_E(TAG, "Failed to parse manifest line (skipped): %s", trimmed_line.c_str());
continue;
}
auto key = section_prefix + trim(trimmed_line.substr(0, separator_index));
auto value = trim(trimmed_line.substr(separator_index + 1));
out_properties[key] = value;
}
return true;
}
} // namespace
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value) {
const auto iterator = properties.find(key);
if (iterator == properties.end()) {
LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
return false;
}
out_value = iterator->second;
return true;
}
bool app_metadata_is_valid_format_version(const std::string& version) {
return !version.empty() && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_metadata_is_valid_name(const std::string& name) {
return name.size() >= 2 && name.size() <= APP_METADATA_APP_NAME_LENGTH && validate_string(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
});
}
bool app_metadata_is_valid_version_name(const std::string& version) {
return !version.empty() && version.size() <= APP_METADATA_APP_VERSION_NAME_LENGTH && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '-' || c == '_';
});
}
bool app_metadata_is_valid_version_code(const std::string& version) {
// 20 digits is the maximum decimal width of uint64_t.
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_is_valid_stack_size(const std::string& value) {
// 10 digits is the maximum decimal width of uint32_t.
return !value.empty() && value.size() <= 10 && validate_string(value, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_is_valid_device_id_list(const std::string& value) {
return validate_csv_list(value, [](const std::string& item) {
bool has_alnum = false;
for (char c: item) {
if (std::isalnum(static_cast<unsigned char>(c)) != 0) {
has_alnum = true;
} else if (c != '-') {
return false;
}
}
return has_alnum;
});
}
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
}
memcpy(dest, value.c_str(), value.size() + 1);
return true;
}
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata) {
LOG_I(TAG, "Parsing manifest %s", path);
// requires_device_id is optional in V2 and unwritten by V1; zeroing here (rather than relying
// on the caller) guarantees it reads back as empty ("unrestricted") either way.
*out_metadata = {};
std::map<std::string, std::string> properties;
std::string first_line;
if (!load_properties(path, properties, first_line)) {
LOG_E(TAG, "Failed to load manifest at %s", path);
return ERROR_NOT_FOUND;
}
// The V1 format's first line is always the literal "[manifest]" section header; V2 files are
// flat from the first line onward.
bool is_v1_format = first_line == "[manifest]";
bool success = is_v1_format
? app_metadata_parse_v1(properties, *out_metadata)
: app_metadata_parse_v2(properties, *out_metadata);
return success ? ERROR_NONE : ERROR_INVALID_ARGUMENT;
}
@@ -1,107 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/metadata.h>
#include <app/private/metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v1";
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// [manifest]
LOG_W(TAG, "This manifest version is deprecated. Replace it with the newer version.");
std::string format_version;
if (!app_metadata_get_value(properties, "[manifest]version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// [app]
std::string id;
if (!app_metadata_get_value(properties, "[app]id", id)) {
return false;
}
if (!app_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "[app]name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "[app]versionName", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "[app]versionCode", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
std::string target_sdk;
if (!app_metadata_get_value(properties, "[target]sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
out_metadata.stack_depth = 0;
return true;
}
@@ -1,149 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/metadata.h>
#include <app/private/metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v2";
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// manifest
std::string format_version;
if (!app_metadata_get_value(properties, "manifest.version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// app
std::string id;
if (!app_metadata_get_value(properties, "app.id", id)) {
return false;
}
if (!app_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "app.name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "app.version.name", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "app.version.code", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
// target
std::string target_sdk;
if (!app_metadata_get_value(properties, "target.sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
// requires.device.id (optional; if present, must be a non-empty comma-separated list)
auto device_id_iterator = properties.find("requires.device.id");
if (device_id_iterator != properties.end()) {
const std::string& device_id = device_id_iterator->second;
if (!app_metadata_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.requires_device_id, sizeof(out_metadata.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return false;
}
}
// app.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find("app.stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_metadata_is_valid_stack_size(stack_size_string)) {
LOG_E(TAG, "Invalid app.stack.depth");
return false;
}
uint32_t stack_size = 0;
const auto* stack_size_first = stack_size_string.data();
const auto* stack_size_last = stack_size_first + stack_size_string.size();
if (std::from_chars(stack_size_first, stack_size_last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "App stack depth out of range");
return false;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "App stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", stack_size, APP_STACK_SIZE_MAX);
return false;
}
out_metadata.stack_depth = stack_size;
}
return true;
}
+15 -9
View File
@@ -5,7 +5,7 @@
#include <app/io.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/metadata.h>
#include <app/package_manifest.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <app/start.h>
@@ -47,20 +47,21 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_manager_for_each_manifest),
DEFINE_MODULE_SYMBOL(app_manager_add),
DEFINE_MODULE_SYMBOL(app_manager_remove),
DEFINE_MODULE_SYMBOL(app_manager_add_package),
DEFINE_MODULE_SYMBOL(app_manager_remove_package),
DEFINE_MODULE_SYMBOL(app_manager_find_package),
DEFINE_MODULE_SYMBOL(app_manager_for_each_package),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_instance_id),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_id),
DEFINE_MODULE_SYMBOL(app_manager_install_path_add),
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall),
// app/start
DEFINE_MODULE_SYMBOL(app_start),
DEFINE_MODULE_SYMBOL(app_start_for_result),
DEFINE_MODULE_SYMBOL(app_start_with_streams),
DEFINE_MODULE_SYMBOL(app_start_for_result_with_streams),
// app/manifest
DEFINE_MODULE_SYMBOL(app_id_is_valid),
// app/metadata
DEFINE_MODULE_SYMBOL(app_metadata_parse),
DEFINE_MODULE_SYMBOL(app_manifest_id_is_valid),
DEFINE_MODULE_SYMBOL(app_manifest_name_is_valid),
DEFINE_MODULE_SYMBOL(app_manifest_stack_size_is_valid),
// app/package_manifest
DEFINE_MODULE_SYMBOL(app_package_manifest_parse),
// app/paths
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory),
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_path),
@@ -68,6 +69,11 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
// app/scheduler
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
// app/start
DEFINE_MODULE_SYMBOL(app_start),
DEFINE_MODULE_SYMBOL(app_start_for_result),
DEFINE_MODULE_SYMBOL(app_start_with_streams),
DEFINE_MODULE_SYMBOL(app_start_for_result_with_streams),
// app/stream
DEFINE_MODULE_SYMBOL(app_stream_subscribe),
DEFINE_MODULE_SYMBOL(app_stream_unsubscribe),
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/private/package_manifest_parsing.h>
#include <tactility/log.h>
#include <cctype>
#include <cstring>
#include <fstream>
#include <map>
#include <string>
constexpr auto* TAG = "app_metadata";
bool app_package_manifest_validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
namespace {
#define validate_string app_package_manifest_validate_string
std::string trim(const std::string& value) {
constexpr auto* whitespace = " \t\r\n";
auto start = value.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
auto end = value.find_last_not_of(whitespace);
return value.substr(start, end - start + 1);
}
/** Validates a comma-separated list: non-empty, no leading/trailing/double commas (which would
* produce an empty item), and every item passing @a is_valid_item. */
bool validate_csv_list(const std::string& value, bool (*is_valid_item)(const std::string&)) {
if (value.empty()) {
return false;
}
size_t start = 0;
while (true) {
auto comma = value.find(',', start);
auto end = comma == std::string::npos ? value.size() : comma;
if (end == start || !is_valid_item(value.substr(start, end - start))) {
return false;
}
if (comma == std::string::npos) {
return true;
}
start = comma + 1;
}
}
/** manifest.properties format: flat "key=value" lines, "#" lines are comments, blank lines are
* skipped. Deliberately a local, minimal re-implementation rather than depending on Tactility's
* file::loadPropertiesFile() - app-module (like every other kernel module) may not depend upward
* on the Tactility layer. */
bool load_properties(const std::string& path, std::map<std::string, std::string>& out_properties) {
std::ifstream file(path);
if (!file.is_open()) {
return false;
}
std::string line;
while (std::getline(file, line)) {
auto trimmed_line = trim(line);
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
continue;
}
auto separator_index = trimmed_line.find('=');
if (separator_index == std::string::npos) {
LOG_E(TAG, "Failed to parse manifest line (skipped): %s", trimmed_line.c_str());
continue;
}
auto key = trim(trimmed_line.substr(0, separator_index));
auto value = trim(trimmed_line.substr(separator_index + 1));
out_properties[key] = value;
}
return true;
}
} // namespace
bool app_package_manifest_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value) {
const auto iterator = properties.find(key);
if (iterator == properties.end()) {
LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
return false;
}
out_value = iterator->second;
return true;
}
bool app_package_manifest_is_valid_format_version(const std::string& version) {
return !version.empty() && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_package_manifest_is_valid_version_name(const std::string& version) {
return !version.empty() && version.size() <= PACKAGE_MANIFEST_VERSION_NAME_LENGTH && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '-' || c == '_';
});
}
bool app_package_manifest_is_valid_version_code(const std::string& version) {
// 20 digits is the maximum decimal width of uint64_t.
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_package_manifest_is_valid_bool(const std::string& value) {
return value == "true" || value == "false";
}
bool app_package_manifest_is_valid_device_id_list(const std::string& value) {
return validate_csv_list(value, [](const std::string& item) {
bool has_alnum = false;
for (char c: item) {
if (std::isalnum(static_cast<unsigned char>(c)) != 0) {
has_alnum = true;
} else if (c != '-') {
return false;
}
}
return has_alnum;
});
}
bool app_package_manifest_is_valid_binary_name(const std::string& value) {
return !value.empty() && value.size() <= APP_MANIFEST_BINARY_LENGTH && validate_string(value, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '_' || c == '-';
});
}
bool app_package_manifest_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
}
memcpy(dest, value.c_str(), value.size() + 1);
return true;
}
error_t app_package_manifest_parse(const char* path, struct PackageManifest* out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity) {
LOG_I(TAG, "Parsing manifest %s", path);
// requires_device_id is optional; zeroing here (rather than relying on the caller) guarantees
// it reads back as empty ("unrestricted") when the manifest omits it.
*out_package = {};
for (size_t i = 0; i < bindings_capacity; i++) {
out_bindings[i] = {};
}
std::map<std::string, std::string> properties;
if (!load_properties(path, properties)) {
LOG_E(TAG, "Failed to load manifest at %s", path);
return ERROR_NOT_FOUND;
}
std::string format_version;
if (!app_package_manifest_get_value(properties, "manifest.version", format_version)) {
return ERROR_INVALID_ARGUMENT;
}
if (format_version == "0.2") {
return package_manifest_parse_v2(properties, *out_package, out_bindings, bindings_capacity);
} else if (format_version == "0.3") {
return package_manifest_parse_v3(properties, *out_package, out_bindings, bindings_capacity);
} else {
LOG_E(TAG, "Unsupported manifest.version: %s", format_version.c_str());
return ERROR_INVALID_ARGUMENT;
}
}
error_t app_package_manifest_parse_into(const char* path, PackageManifest& out_package, std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>>& out_bindings) {
error_t result = app_package_manifest_parse(path, &out_package, nullptr, 0);
if (result != ERROR_NONE) {
return result;
}
out_bindings.resize(out_package.app_manifest_count);
result = app_package_manifest_parse(path, &out_package, out_bindings.data(), out_bindings.size());
if (result != ERROR_NONE) {
return result;
}
// Manifest could have changed between the two parses above.
if (out_package.app_manifest_count != out_bindings.size()) {
LOG_E(TAG, "Manifest at %s changed while being parsed", path);
return ERROR_INVALID_ARGUMENT;
}
return ERROR_NONE;
}
@@ -0,0 +1,172 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/package_manifest.h>
#include <app/private/package_manifest_parsing.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v2";
error_t package_manifest_parse_v2(const std::map<std::string, std::string>& properties, PackageManifest& out_package, AppManifestBinding* out_bindings, size_t bindings_capacity) {
// manifest
std::string format_version;
if (!app_package_manifest_get_value(properties, "manifest.version", format_version)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return ERROR_INVALID_ARGUMENT;
}
// app
std::string id;
if (!app_package_manifest_get_value(properties, "app.id", id)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.id, sizeof(out_package.id), id)) {
LOG_E(TAG, "App id too long");
return ERROR_INVALID_ARGUMENT;
}
std::string name;
if (!app_package_manifest_get_value(properties, "app.name", name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_name_is_valid(name.c_str())) {
LOG_E(TAG, "Invalid app name");
return ERROR_INVALID_ARGUMENT;
}
std::string version_name;
if (!app_package_manifest_get_value(properties, "app.version.name", version_name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.version_name, sizeof(out_package.version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return ERROR_INVALID_ARGUMENT;
}
std::string version_code_string;
if (!app_package_manifest_get_value(properties, "app.version.code", version_code_string)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return ERROR_INVALID_ARGUMENT;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return ERROR_INVALID_ARGUMENT;
}
out_package.version_code = version_code;
// target
std::string target_sdk;
if (!app_package_manifest_get_value(properties, "target.sdk", target_sdk)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.target_sdk, sizeof(out_package.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return ERROR_INVALID_ARGUMENT;
}
// requires.device.id (optional; if present, must be a non-empty comma-separated list)
auto device_id_iterator = properties.find("requires.device.id");
if (device_id_iterator != properties.end()) {
const std::string& device_id = device_id_iterator->second;
if (!app_package_manifest_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.requires_device_id, sizeof(out_package.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return ERROR_INVALID_ARGUMENT;
}
}
// A v2 manifest always describes exactly one app, sharing the package's own id.
out_package.app_manifest_count = 1;
if (bindings_capacity == 0) {
return ERROR_NONE;
}
AppManifestBinding& binding = out_bindings[0];
AppManifest& manifest = binding.manifest;
// v2 predates the "binary" key - the single app always installs as bin/<platform>/app.{elf,so}.
if (!app_package_manifest_copy_bounded(binding.binary, sizeof(binding.binary), "app")) {
LOG_E(TAG, "Binary name too long");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(manifest.id, sizeof(manifest.id), id)) {
LOG_E(TAG, "App id too long");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(manifest.name, sizeof(manifest.name), name)) {
LOG_E(TAG, "App name too long");
return ERROR_INVALID_ARGUMENT;
}
// app.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find("app.stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_manifest_stack_size_is_valid(stack_size_string.c_str())) {
LOG_E(TAG, "Invalid app.stack.depth");
return ERROR_INVALID_ARGUMENT;
}
uint32_t stack_size = 0;
const auto* stack_size_first = stack_size_string.data();
const auto* stack_size_last = stack_size_first + stack_size_string.size();
if (std::from_chars(stack_size_first, stack_size_last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "App stack depth out of range");
return ERROR_INVALID_ARGUMENT;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "App stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", stack_size, APP_STACK_SIZE_MAX);
return ERROR_INVALID_ARGUMENT;
}
manifest.stack.depth = static_cast<uint16_t>(stack_size);
}
// v2 predates per-app visibility - always visible.
manifest.flags = 0;
manifest.category = APP_CATEGORY_USER;
return ERROR_NONE;
}
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/package_manifest.h>
#include <app/private/package_manifest_parsing.h>
#include <charconv>
#include <format>
#include <optional>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v3";
namespace {
// Numeric index out of an "app.<index>.<rest>" key, or nullopt if it doesn't match that shape.
std::optional<size_t> parse_app_key_index(const std::string& key) {
constexpr auto* prefix = "app.";
constexpr size_t prefix_len = 4;
if (!key.starts_with(prefix)) {
return std::nullopt;
}
auto dot = key.find('.', prefix_len);
if (dot == std::string::npos || dot == prefix_len) {
return std::nullopt;
}
size_t index = 0;
const auto* first = key.data() + prefix_len;
const auto* last = key.data() + dot;
auto result = std::from_chars(first, last, index);
if (result.ec != std::errc {} || result.ptr != last) {
return std::nullopt;
}
return index;
}
// Parses one "app.<index>.*" block into @a out_binding.
error_t parse_app_manifest(const std::map<std::string, std::string>& properties, size_t index, AppManifestBinding& out_binding) {
auto prefix = std::format("app.{}.", index);
AppManifest& out_manifest = out_binding.manifest;
std::string id;
if (!app_package_manifest_get_value(properties, prefix + "id", id)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid %sid", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_manifest.id, sizeof(out_manifest.id), id)) {
LOG_E(TAG, "%sid too long", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
std::string binary;
if (!app_package_manifest_get_value(properties, prefix + "binary", binary)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_binary_name(binary)) {
LOG_E(TAG, "Invalid %sbinary", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
// Filename (without extension) this app installs as under bin/<platform>/ - see
// app_package_manifest_parse()'s own doc.
if (!app_package_manifest_copy_bounded(out_binding.binary, sizeof(out_binding.binary), binary)) {
LOG_E(TAG, "%sbinary too long", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
std::string name;
if (!app_package_manifest_get_value(properties, prefix + "name", name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_name_is_valid(name.c_str())) {
LOG_E(TAG, "Invalid %sname", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_manifest.name, sizeof(out_manifest.name), name)) {
LOG_E(TAG, "%sname too long", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
// <index>.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find(prefix + "stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_manifest_stack_size_is_valid(stack_size_string.c_str())) {
LOG_E(TAG, "Invalid %sstack.depth", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
uint32_t stack_size = 0;
const auto* first = stack_size_string.data();
const auto* last = first + stack_size_string.size();
if (std::from_chars(first, last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "%sstack.depth out of range", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "%sstack.depth %u exceeds APP_STACK_SIZE_MAX(%u)", prefix.c_str(), stack_size, APP_STACK_SIZE_MAX);
return ERROR_INVALID_ARGUMENT;
}
out_manifest.stack.depth = static_cast<uint16_t>(stack_size);
}
// <index>.hidden (optional; defaults to false)
auto hidden_iterator = properties.find(prefix + "hidden");
if (hidden_iterator != properties.end()) {
if (!app_package_manifest_is_valid_bool(hidden_iterator->second)) {
LOG_E(TAG, "Invalid %shidden", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
out_manifest.flags = hidden_iterator->second == "true" ? APP_MANIFEST_FLAG_HIDDEN : 0;
}
out_manifest.category = APP_CATEGORY_USER;
return ERROR_NONE;
}
} // namespace
error_t package_manifest_parse_v3(const std::map<std::string, std::string>& properties, PackageManifest& out_package, AppManifestBinding* out_bindings, size_t bindings_capacity) {
// package-level fields: bare keys, no "app." prefix (that's reserved for the app.<index>.*
// blocks below).
std::string id;
if (!app_package_manifest_get_value(properties, "id", id)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.id, sizeof(out_package.id), id)) {
LOG_E(TAG, "id too long");
return ERROR_INVALID_ARGUMENT;
}
std::string version_name;
if (!app_package_manifest_get_value(properties, "version.name", version_name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid version.name");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.version_name, sizeof(out_package.version_name), version_name)) {
LOG_E(TAG, "version.name too long");
return ERROR_INVALID_ARGUMENT;
}
std::string version_code_string;
if (!app_package_manifest_get_value(properties, "version.code", version_code_string)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid version.code");
return ERROR_INVALID_ARGUMENT;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "version.code out of range");
return ERROR_INVALID_ARGUMENT;
}
out_package.version_code = version_code;
std::string target_sdk;
if (!app_package_manifest_get_value(properties, "target.sdk", target_sdk)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.target_sdk, sizeof(out_package.target_sdk), target_sdk)) {
LOG_E(TAG, "target.sdk too long");
return ERROR_INVALID_ARGUMENT;
}
auto device_id_iterator = properties.find("requires.device.id");
if (device_id_iterator != properties.end()) {
const std::string& device_id = device_id_iterator->second;
if (!app_package_manifest_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.requires_device_id, sizeof(out_package.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return ERROR_INVALID_ARGUMENT;
}
}
// app.<index>.* blocks: 0-indexed, contiguous - the first missing "app.<index>.id" ends the list.
size_t count = 0;
while (properties.contains(std::format("app.{}.id", count))) {
count++;
// Bounded here, not just against the caller's bindings_capacity below: a caller that
// sizes its own buffer off PackageManifest::app_manifest_count (see
// app_package_manifest_parse_into()) would otherwise let a malicious/malformed manifest
// demand an unbounded allocation.
if (count > PACKAGE_MANIFEST_MAX_APP_MANIFESTS) {
LOG_E(TAG, "Manifest declares more than %d apps", PACKAGE_MANIFEST_MAX_APP_MANIFESTS);
return ERROR_BUFFER_OVERFLOW;
}
}
out_package.app_manifest_count = static_cast<uint32_t>(count);
// Catch a gap the count loop above would otherwise silently drop, e.g. app.0.* + app.3.*.
for (const auto& [key, value] : properties) {
auto index = parse_app_key_index(key);
if (index.has_value() && *index >= count) {
LOG_E(TAG, "Manifest declares %s but only %zu contiguous app(s) starting at app.0 were found", key.c_str(), count);
return ERROR_INVALID_ARGUMENT;
}
}
if (count == 0 || bindings_capacity == 0) {
return ERROR_NONE;
}
if (count > bindings_capacity) {
LOG_E(TAG, "Manifest declares %zu apps, capacity is %zu", count, bindings_capacity);
return ERROR_BUFFER_OVERFLOW;
}
for (size_t i = 0; i < count; i++) {
error_t result = parse_app_manifest(properties, i, out_bindings[i]);
if (result != ERROR_NONE) {
return result;
}
}
return ERROR_NONE;
}
+27 -1
View File
@@ -6,6 +6,27 @@
#include <tactility/paths.h>
#include <cstdio>
#include <string>
namespace {
// manifest.location.location is the fully-resolved binary file path
// ({install_dir}/bin/<platform>/<binary>.{elf,so} - see app_resolve_binary_path()), not the
// install directory itself - strip that fixed 3-segment suffix to recover it.
bool app_get_install_dir_from_binary_path(const char* binary_path, std::string& out_install_dir) {
std::string path = binary_path;
for (int i = 0; i < 3; i++) {
auto separator = path.find_last_of('/');
if (separator == std::string::npos) {
return false;
}
path = path.substr(0, separator);
}
out_install_dir = path;
return true;
}
} // namespace
extern "C" {
@@ -46,7 +67,12 @@ error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_
return ERROR_NOT_FOUND;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", static_cast<const char*>(manifest.location.location));
std::string install_dir;
if (!app_get_install_dir_from_binary_path(static_cast<const char*>(manifest.location.location), install_dir)) {
return ERROR_NOT_FOUND;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", install_dir.c_str());
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
+5 -3
View File
@@ -32,8 +32,10 @@ constexpr size_t APP_INSTANCE_ID_THREAD_SLOT_INDEX = 1;
// Matches TactilityKernel's Thread wrapper's THREAD_PRIORITY_NORMAL.
constexpr UBaseType_t APP_TASK_PRIORITY = 4;
// Used when an app's manifest doesn't request a specific stack depth (0). 8192 bytes' worth.
constexpr size_t APP_DEFAULT_STACK_DEPTH = 8192 / sizeof(StackType_t);
// Used when an app's manifest doesn't request a specific stack depth (0). 12288 bytes' worth.
// Existing window-manager app manifests request at least 2400 words (9600 bytes); use a
// conservative default above that baseline so a zero-depth app task cannot exhaust its stack.
constexpr size_t APP_DEFAULT_STACK_DEPTH = 12288 / sizeof(StackType_t);
// Task control blocks must stay in internal RAM; only the stack itself may live in external memory.
constexpr MemoryPolicy APP_TASK_TCB_POLICY = { MEMORY_CAPABILITY_INTERNAL, 0, 0 };
@@ -303,7 +305,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
return ERROR_OUT_OF_MEMORY;
}
// Same bound app_metadata_parse() enforces on manifest.properties-declared depths - a
// Same bound package_manifest_parse() enforces on manifest.properties-declared depths - a
// manifest built directly in C++ (not parsed from a file) must be held to it too.
if (stack.depth > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "[instance %lu] stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", app_instance_id, stack.depth, APP_STACK_SIZE_MAX);
@@ -73,7 +73,11 @@ bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_
AppInstanceId start_idle_app(const char* id) {
ensure_memory_loader_registered();
AppManifest manifest { id, id, APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(idle_app_main) } };
AppManifest manifest {};
std::strncpy(manifest.id, id, sizeof(manifest.id) - 1);
std::strncpy(manifest.name, id, sizeof(manifest.name) - 1);
manifest.category = APP_CATEGORY_USER;
manifest.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(idle_app_main) };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0;
REQUIRE_EQ(app_start(id, 0, nullptr, &instance_id), ERROR_NONE);
@@ -58,19 +58,21 @@ bool is_executable_file(const std::string& resolved_path) {
#endif
}
// location.location can be either an app's install directory or the .so file directly; the
// former resolves to the per-architecture binary at {dir}/elf/posix-{TACTILITY_POSIX_ARCH}.so,
// mirroring app_esp32_loader_service.cpp's resolve_elf_path().
// location.location can be either an app's install directory or the .so file directly, mirroring
// app_esp32_loader_service.cpp's resolve_elf_path(). A "packaged" app's install directory always
// holds its single binary at the fixed path {dir}/bin/posix-{TACTILITY_POSIX_ARCH}/app.so - a
// "terminal" app has no such file (its several binaries keep their own names), so this correctly
// leaves it unresolvable - terminal apps aren't run through AppLoaderApi (see app/install.h).
error_t resolve_app_path(const std::string& path, std::string& resolvedPath) {
if (path.ends_with(".so")) {
resolvedPath = path;
return ERROR_NONE;
}
std::string shared_object_path = path + "/elf/posix-" TACTILITY_POSIX_ARCH ".so";
if (!is_regular_file(shared_object_path)) {
std::string candidate = path + "/bin/posix-" TACTILITY_POSIX_ARCH "/app.so";
if (!is_regular_file(candidate)) {
return ERROR_NOT_FOUND;
}
resolvedPath = shared_object_path;
resolvedPath = candidate;
return ERROR_NONE;
}
@@ -94,9 +96,6 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
LOG_I(TAG, "Loading %s", app_path.c_str());
// RTLD_NOW: a missing symbol fails here, not mid-run(). RTLD_LOCAL: this app's own exported
// symbols (if any beyond its entry point) don't leak into the process's global scope and
// clash with a different app's.
void* handle = dlopen(app_path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (handle == nullptr) {
LOG_E(TAG, "dlopen(%s) failed: %s", app_path.c_str(), dlerror());
@@ -117,8 +116,8 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
auto* runtime = static_cast<PosixAppRuntime*>(runtime_ptr);
dlerror(); // clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL
// symbol address apart from a real lookup failure
// Clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL symbol address apart from a real lookup failure
dlerror();
void* symbol = dlsym(runtime->handle, "main");
const char* lookup_error = dlerror();
if (symbol == nullptr || lookup_error != nullptr) {
+14 -1
View File
@@ -15,14 +15,27 @@ set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPEND
set(NON_ELF_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/not-elf.so")
file(WRITE "${NON_ELF_FIXTURE_PATH}" "not an elf file")
# An install-directory-shaped fixture: {dir}/bin/posix-{arch}/app.so, matching where
# app_posix_loader_service.cpp's resolve_app_path() looks for a "packaged" app's single binary.
set(INSTALL_DIR_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/install-dir-fixture")
set(INSTALL_DIR_FIXTURE_BIN_DIR "${INSTALL_DIR_FIXTURE_PATH}/bin/posix-${CMAKE_SYSTEM_PROCESSOR}")
add_custom_command(
OUTPUT "${INSTALL_DIR_FIXTURE_BIN_DIR}/app.so"
COMMAND ${CMAKE_COMMAND} -E make_directory "${INSTALL_DIR_FIXTURE_BIN_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:app_posix_module_test_fixture>" "${INSTALL_DIR_FIXTURE_BIN_DIR}/app.so"
DEPENDS app_posix_module_test_fixture
)
add_custom_target(install_dir_fixture DEPENDS "${INSTALL_DIR_FIXTURE_BIN_DIR}/app.so")
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture)
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture install_dir_fixture)
target_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC})
target_compile_definitions(AppPosixModuleTests PRIVATE
FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>"
FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}"
FIXTURE_INSTALL_DIR_PATH="${INSTALL_DIR_FIXTURE_PATH}"
)
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests)
@@ -148,6 +148,12 @@ TEST_CASE("app_is_executable() rejects a nonexistent path") {
TEST_CASE("app_is_executable() rejects an install-directory-shaped path missing its per-arch .so") {
ensure_path_loader_registered();
// FIXTURE_DIR itself has no elf/posix-<arch>.so under it, so resolution fails.
// FIXTURE_DIR itself has no bin/posix-<arch>/app.so under it, so resolution fails.
CHECK_FALSE(is_executable_path(FIXTURE_DIR.c_str()));
}
TEST_CASE("app_is_executable() accepts an install-directory-shaped path with bin/<arch>/app.so") {
ensure_path_loader_registered();
CHECK(is_executable_path(FIXTURE_INSTALL_DIR_PATH));
}
+9
View File
@@ -14,6 +14,15 @@ enum LvglFontSize {
FONT_SIZE_LARGE,
};
enum LvglFontScale {
FONT_SCALE_SMALL,
FONT_SCALE_DEFAULT,
FONT_SCALE_LARGE,
};
void lvgl_set_text_font_scale(enum LvglFontScale font_scale);
enum LvglFontScale lvgl_get_text_font_scale(void);
const lv_font_t* lvgl_get_shared_icon_font(void);
uint32_t lvgl_get_shared_icon_font_height(void);
+24 -2
View File
@@ -13,8 +13,30 @@ extern const lv_font_t TT_LVGL_LAUNCHER_FONT_ICON_SYMBOL;
extern const lv_font_t TT_LVGL_STATUSBAR_FONT_ICON_SYMBOL;
extern const lv_font_t TT_LVGL_SHARED_FONT_ICON_SYMBOL;
static enum LvglFontScale text_font_scale = FONT_SCALE_DEFAULT;
void lvgl_set_text_font_scale(enum LvglFontScale font_scale) {
check(font_scale >= FONT_SCALE_SMALL && font_scale <= FONT_SCALE_LARGE);
text_font_scale = font_scale;
}
enum LvglFontScale lvgl_get_text_font_scale(void) {
return text_font_scale;
}
static enum LvglFontSize resolve_text_font_size(enum LvglFontSize font_size) {
int resolved = (int)font_size + (int)text_font_scale - (int)FONT_SCALE_DEFAULT;
if (resolved < FONT_SIZE_SMALL) {
return FONT_SIZE_SMALL;
}
if (resolved > FONT_SIZE_LARGE) {
return FONT_SIZE_LARGE;
}
return (enum LvglFontSize)resolved;
}
uint32_t lvgl_get_text_font_height(enum LvglFontSize font_size) {
switch (font_size) {
switch (resolve_text_font_size(font_size)) {
case FONT_SIZE_SMALL: return TT_LVGL_TEXT_FONT_SMALL_SIZE;
case FONT_SIZE_DEFAULT: return TT_LVGL_TEXT_FONT_DEFAULT_SIZE;
case FONT_SIZE_LARGE: return TT_LVGL_TEXT_FONT_LARGE_SIZE;
@@ -22,7 +44,7 @@ uint32_t lvgl_get_text_font_height(enum LvglFontSize font_size) {
}
}
const lv_font_t* lvgl_get_text_font(enum LvglFontSize font_size) {
switch (font_size) {
switch (resolve_text_font_size(font_size)) {
case FONT_SIZE_SMALL: return &TT_LVGL_TEXT_FONT_SMALL_SYMBOL;
case FONT_SIZE_DEFAULT: return &TT_LVGL_TEXT_FONT_DEFAULT_SYMBOL;
case FONT_SIZE_LARGE: return &TT_LVGL_TEXT_FONT_LARGE_SYMBOL;
+8
View File
@@ -26,6 +26,8 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lvgl_module),
DEFINE_MODULE_SYMBOL(lvgl_module_configure),
// lvgl_fonts
DEFINE_MODULE_SYMBOL(lvgl_set_text_font_scale),
DEFINE_MODULE_SYMBOL(lvgl_get_text_font_scale),
DEFINE_MODULE_SYMBOL(lvgl_get_shared_icon_font),
DEFINE_MODULE_SYMBOL(lvgl_get_shared_icon_font_height),
DEFINE_MODULE_SYMBOL(lvgl_get_text_font),
@@ -494,6 +496,12 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
// lv_image
DEFINE_MODULE_SYMBOL(lv_image_create),
DEFINE_MODULE_SYMBOL(lv_image_set_src),
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_get_scale),
DEFINE_MODULE_SYMBOL(lv_image_get_scale_x),
DEFINE_MODULE_SYMBOL(lv_image_get_scale_y),
// lv_anim
DEFINE_MODULE_SYMBOL(lv_anim_init),
DEFINE_MODULE_SYMBOL(lv_anim_set_duration),
+7
View File
@@ -1,5 +1,9 @@
#pragma once
namespace tt::settings::display {
enum class FontSize;
}
namespace tt::lvgl {
#ifdef ESP_PLATFORM
@@ -12,6 +16,9 @@ static constexpr auto* PATH_PREFIX = "A:/";
bool isStarted();
/** Applies the selected semantic text size to the LVGL theme and future widgets. */
void applyFontSize(settings::display::FontSize fontSize);
void start();
void stop();
@@ -18,11 +18,20 @@ enum class ScreensaverType {
Mystify,
MatrixRain,
StackChan,
McpScreen,
Count // Sentinel for bounds checking - must be last
};
enum class FontSize {
Small,
Default,
Large,
Count
};
struct DisplaySettings {
Orientation orientation;
FontSize fontSize = FontSize::Default;
uint8_t gammaCurve;
uint8_t backlightDuty;
bool backlightTimeoutEnabled;
@@ -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
@@ -1,9 +0,0 @@
#pragma once
#include <string>
namespace tt::app::appdetails {
void start(const std::string& appId);
} // namespace
@@ -0,0 +1,9 @@
#pragma once
#include <string>
namespace tt::app::apppackagedetails {
void start(const std::string& packageId);
} // 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
@@ -63,6 +63,7 @@ public:
* arbitrary threads while the timer is running.
*/
void stopScreensaver();
void startMcpScreensaver();
/**
* Check if the screensaver is currently active.
@@ -81,6 +81,10 @@ private:
static error_t handleApiAppsInstall(struct HttpServerRequest* request, void* user_ctx);
static error_t handleApiWifi(struct HttpServerRequest* request, void* user_ctx);
static error_t handleApiScreenshot(struct HttpServerRequest* request, void* user_ctx);
#ifdef ESP_PLATFORM
static error_t handleApiMcp(struct HttpServerRequest* request, void* user_ctx);
static error_t handleApiScreenRaw(struct HttpServerRequest* request, void* user_ctx);
#endif
// Dynamic asset serving
static error_t handleAssets(struct HttpServerRequest* request, void* user_ctx);
+2 -2
View File
@@ -68,12 +68,12 @@ std::string getUserHomePath() {
}
std::string getAppInstallPath(const std::string& appId) {
assert(app_id_is_valid(appId.c_str()));
assert(app_manifest_id_is_valid(appId.c_str()));
return std::format("{}/{}", getAppInstallPath(), appId);
}
std::string getAppUserPath(const std::string& appId) {
assert(app_id_is_valid(appId.c_str()));
assert(app_manifest_id_is_valid(appId.c_str()));
return std::format("{}/app/{}", getUserHomePath(), appId);
}
+9 -5
View File
@@ -26,9 +26,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/module.h>
#include <app/start.h>
#include <Tactility/Tactility.h>
@@ -40,6 +40,7 @@
#include <Tactility/file/File.h>
#include <Tactility/hal/SdCard.h>
#include <Tactility/lvgl/KeyboardDeviceListener.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/lvgl/TrackballInit.h>
#include <Tactility/lvgl/UsbHidInput.h>
@@ -150,9 +151,9 @@ namespace app {
namespace alertdialog { extern const ::AppManifest manifest; }
namespace apphub { extern const ::AppManifest manifest; }
namespace apphubdetails { extern const ::AppManifest manifest; }
namespace appdetails { extern const ::AppManifest manifest; }
namespace apppackagedetails { extern const ::AppManifest manifest; }
namespace applist { extern const ::AppManifest manifest; }
namespace appsettings { extern const ::AppManifest manifest; }
namespace apppackagelist { extern const ::AppManifest manifest; }
namespace audiosettings { extern const ::AppManifest manifest; }
namespace boot { extern const ::AppManifest manifest; }
namespace development { extern const ::AppManifest manifest; }
@@ -189,6 +190,7 @@ namespace app {
#ifdef ESP_PLATFORM
namespace apwebserver { extern const ::AppManifest manifest; }
namespace crashdiagnostics { extern const ::AppManifest manifest; }
namespace mcpsettings { extern const ::AppManifest manifest; }
#if CONFIG_TT_TDECK_WORKAROUND == 1
namespace keyboardsettings { extern const ::AppManifest manifest; } // T-Deck only for now
#endif
@@ -212,11 +214,11 @@ static void registerInternalApps() {
LOG_I(TAG, "Registering internal apps");
app_manager_add(&app::alertdialog::manifest);
app_manager_add(&app::appdetails::manifest);
app_manager_add(&app::apppackagedetails::manifest);
app_manager_add(&app::apphub::manifest);
app_manager_add(&app::apphubdetails::manifest);
app_manager_add(&app::applist::manifest);
app_manager_add(&app::appsettings::manifest);
app_manager_add(&app::apppackagelist::manifest);
if (service::audio::isAvailable()) {
app_manager_add(&app::audiosettings::manifest);
}
@@ -252,6 +254,7 @@ static void registerInternalApps() {
#ifdef ESP_PLATFORM
app_manager_add(&app::apwebserver::manifest);
app_manager_add(&app::crashdiagnostics::manifest);
app_manager_add(&app::mcpsettings::manifest);
#if defined(CONFIG_TT_TDECK_WORKAROUND)
app_manager_add(&app::keyboardsettings::manifest);
#endif
@@ -433,6 +436,7 @@ static void onLvglStarted() {
if (auto* display = lv_display_get_default(); display != nullptr) {
auto displaySettings = settings::display::loadOrGetDefault();
lv_display_set_rotation(display, settings::display::toLvglDisplayRotation(displaySettings.orientation));
lvgl::applyFontSize(displaySettings.fontSize);
}
lvgl_unlock();
@@ -2,9 +2,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
+6 -1
View File
@@ -107,6 +107,9 @@ void showNoInternet(Context* ctx) {
}
void showApps(Context* ctx) {
if (ctx->contentWrapper == nullptr) {
return;
}
// Refresh rebuilds the list from scratch (cached copy, then again once the network fetch
// lands), which would otherwise reset the user's scroll position each time.
int32_t scrollY;
@@ -266,7 +269,9 @@ void createWidgets(lv_obj_t* parent, void* userData) {
void destroyWidgets(void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
if (ctx->contentWrapper != nullptr) {
ctx->scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
}
ctx->contentWrapper = nullptr;
ctx->refreshButton = nullptr;
}
@@ -7,11 +7,11 @@
#include <app/event.h>
#include <app/install.h>
#include <app/metadata.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <http/download.h>
@@ -204,9 +204,9 @@ void updateViews(Context* ctx) {
if (is_installed) {
std::string metadata_path = std::string(install_path) + "/manifest.properties";
AppMetadata metadata;
if (app_metadata_parse(metadata_path.c_str(), &metadata) == ERROR_NONE
&& metadata.app_version_code < ctx->entry.appVersionCode) {
PackageManifest package;
if (app_package_manifest_parse(metadata_path.c_str(), &package, nullptr, 0) == ERROR_NONE
&& package.version_code < ctx->entry.appVersionCode) {
ctx->updateButton = lvgl_toolbar_add_image_button_action(ctx->toolbar, LV_SYMBOL_DOWNLOAD, onUpdatePressed, ctx);
lv_obj_remove_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN);
}
+2 -2
View File
@@ -1,8 +1,8 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -121,7 +121,7 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 2400, .desired_memory_capability = 0 },
.stack = { .depth = 3072, .desired_memory_capability = 0 },
};
} // namespace
@@ -1,9 +1,10 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/install.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <format>
@@ -12,17 +13,15 @@
#include <lvgl_window_manager/window_manager.h>
#include <Tactility/StringUtils.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/Style.h>
#include <tactility/check.h>
#include <tactility/log.h>
constexpr auto* TAG = "AppDetails";
constexpr auto* TAG = "AppPackageDetails";
namespace tt::app::appdetails {
namespace tt::app::apppackagedetails {
extern const ::AppManifest manifest;
@@ -30,13 +29,34 @@ namespace {
struct Context {
uint32_t appInstanceId;
std::string targetAppId;
// findAppManifestById() returns the old-model registry's AppManifest type - AppDetails
// shows details for apps in that registry regardless of which system they run under.
AppManifest targetManifest = { };
std::string targetPackageId;
PackageManifest targetPackage = {};
std::vector<std::string> appIds;
uint32_t pendingUninstallDialogId = 0;
};
struct FindPackageContext {
const std::string* packageId;
PackageManifest* outPackage;
std::vector<std::string>* outAppIds;
bool found = false;
};
void onVisitPackage(const ::AppPackage* pkg, void* context) {
auto* findContext = static_cast<FindPackageContext*>(context);
if (findContext->found || *findContext->packageId != pkg->package.id) {
return;
}
*findContext->outPackage = pkg->package;
findContext->outAppIds->assign(pkg->app_ids, pkg->app_ids + pkg->app_id_count);
findContext->found = true;
}
bool findPackage(const std::string& packageId, PackageManifest& outPackage, std::vector<std::string>& outAppIds) {
FindPackageContext findContext { &packageId, &outPackage, &outAppIds };
app_manager_for_each_package(onVisitPackage, &findContext);
return findContext.found;
}
void onPressUninstall(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
@@ -44,7 +64,7 @@ void onPressUninstall(lv_event_t* event) {
ctx->pendingUninstallDialogId = alertdialog::start(
ctx->appInstanceId,
"Confirmation",
std::format("Uninstall {}?", ctx->targetManifest.name),
std::format("Uninstall {}?", ctx->targetPackage.id),
choices
);
}
@@ -59,7 +79,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto title = std::format("{} details", ctx->targetManifest.name);
auto title = std::format("{} details", ctx->targetPackage.id);
auto* toolbar = lvgl_toolbar_create(parent, title.c_str());
// The global toolbar nav callback only knows how to stop old-model apps.
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
@@ -71,35 +91,39 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
lvgl::obj_set_style_bg_invisible(wrapper);
auto identifier = std::format("Identifier: {}", ctx->targetManifest.id);
auto identifier = std::format("Identifier: {}", ctx->targetPackage.id);
auto* identifier_label = lv_label_create(wrapper);
lv_label_set_text(identifier_label, identifier.c_str());
auto* location_label = lv_label_create(wrapper);
std::string location;
bool is_internal = ctx->targetManifest.location.type == APP_LOCATION_MEMORY;
bool is_external = ctx->targetManifest.location.type == APP_LOCATION_PATH;
if (is_internal) {
location = "internal";
} else if (is_external) {
if (!string::getPathParent(static_cast<const char*>(ctx->targetManifest.location.location), location)) {
location = "external";
}
} else {
LOG_E(TAG, "Unknown app location type %d", ctx->targetManifest.location.type);
return;
}
std::string location_label_text = std::format("Location: {}", location);
lv_label_set_text(location_label, location_label_text.c_str());
auto version = std::format("Version: {} ({})", ctx->targetPackage.version_name, ctx->targetPackage.version_code);
auto* version_label = lv_label_create(wrapper);
lv_label_set_text(version_label, version.c_str());
if (is_external) {
auto* uninstall_button = lv_button_create(wrapper);
lv_obj_set_width(uninstall_button, LV_PCT(100));
lv_obj_add_event_cb(uninstall_button, onPressUninstall, LV_EVENT_SHORT_CLICKED, ctx);
auto* uninstall_label = lv_label_create(uninstall_button);
lv_obj_align(uninstall_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(uninstall_label, "Uninstall");
char install_path[192];
std::string location = "unknown";
if (app_get_install_path(ctx->targetPackage.id, install_path, sizeof(install_path)) == ERROR_NONE) {
location = install_path;
}
auto location_text = std::format("Location: {}", location);
auto* location_label = lv_label_create(wrapper);
lv_label_set_text(location_label, location_text.c_str());
std::string apps;
for (const auto& appId : ctx->appIds) {
AppManifest appManifest {};
const char* label = app_manager_find_manifest(appId.c_str(), &appManifest) == ERROR_NONE ? appManifest.name : appId.c_str();
apps += apps.empty() ? label : std::format(", {}", label);
}
auto apps_text = std::format("Apps: {}", apps);
auto* apps_label = lv_label_create(wrapper);
lv_label_set_text(apps_label, apps_text.c_str());
auto* uninstall_button = lv_button_create(wrapper);
lv_obj_set_width(uninstall_button, LV_PCT(100));
lv_obj_add_event_cb(uninstall_button, onPressUninstall, LV_EVENT_SHORT_CLICKED, ctx);
auto* uninstall_label = lv_label_create(uninstall_button);
lv_obj_align(uninstall_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(uninstall_label, "Uninstall");
}
int32_t appMain(int argc, char* argv[]) {
@@ -107,9 +131,9 @@ int32_t appMain(int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
ctx.targetAppId = (argc > 0) ? argv[0] : std::string();
if (app_manager_find_manifest(ctx.targetAppId.c_str(), &ctx.targetManifest) != ERROR_NONE) {
LOG_W(TAG, "App %s not found", ctx.targetAppId.c_str());
ctx.targetPackageId = (argc > 0) ? argv[0] : std::string();
if (!findPackage(ctx.targetPackageId, ctx.targetPackage, ctx.appIds)) {
LOG_W(TAG, "Package %s not found", ctx.targetPackageId.c_str());
return 0;
}
@@ -134,7 +158,7 @@ int32_t appMain(int argc, char* argv[]) {
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingUninstallDialogId) {
if (event.result.result == 0) { // 0 = Yes
app_uninstall(ctx.targetManifest.id);
app_uninstall(ctx.targetPackage.id);
shouldClose = true;
}
app_manager_stop(event.result.launch_id);
@@ -156,15 +180,15 @@ int32_t appMain(int argc, char* argv[]) {
} // namespace
void start(const std::string& appId) {
const char* argv[] = { appId.c_str() };
void start(const std::string& packageId) {
const char* argv[] = { packageId.c_str() };
uint32_t instanceId = 0;
app_start(manifest.id, 1, argv, &instanceId);
}
extern const ::AppManifest manifest = {
.id = "tactility.appdetails",
.name = "App Details",
.id = "tactility.apppackagedetails",
.name = "Package Details",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN,
@@ -1,11 +1,12 @@
#include <lvgl/icons/shared.h>
#include <lvgl/fonts.h>
#include <Tactility/app/appdetails/AppDetails.h>
#include <Tactility/app/apppackagedetails/AppPackageDetails.h>
#include <app/event.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h>
@@ -18,7 +19,7 @@
#include <cstring>
#include <vector>
namespace tt::app::appsettings {
namespace tt::app::apppackagelist {
extern const ::AppManifest manifest;
@@ -26,11 +27,13 @@ namespace {
struct Context {
uint32_t appInstanceId;
// Must outlive the widgets - button user-data points into this, not a createWidgets()-local vector.
std::vector<std::string> packageIds;
};
void onAppPressed(lv_event_t* e) {
const auto* target_manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
appdetails::start(target_manifest->id);
void onPackagePressed(lv_event_t* e) {
auto* packageId = static_cast<char*>(lv_event_get_user_data(e));
apppackagedetails::start(packageId);
}
void onBackPressed(lv_event_t* event) {
@@ -38,18 +41,16 @@ void onBackPressed(lv_event_t* event) {
app_event_emit_close(ctx->appInstanceId);
}
void createAppWidget(const ::AppManifest* target_manifest, lv_obj_t* list) {
// The new AppManifest has no per-app icon - use a shared generic one for every entry, same
// fallback AppList.cpp uses.
lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_TOOLBAR, target_manifest->name);
void createPackageWidget(const char* packageId, lv_obj_t* list) {
lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_TOOLBAR, packageId);
lv_obj_t* image = lv_obj_get_child(btn, 0);
lv_obj_set_style_text_font(image, lvgl_get_shared_icon_font(), LV_PART_MAIN);
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, const_cast<::AppManifest*>(target_manifest));
lv_obj_add_event_cb(btn, &onPackagePressed, LV_EVENT_SHORT_CLICKED, const_cast<char*>(packageId));
}
void collectManifest(const ::AppManifest* manifest, void* context) {
auto* manifests = static_cast<std::vector<const ::AppManifest*>*>(context);
manifests->push_back(manifest);
void collectPackageId(const ::AppPackage* pkg, void* context) {
auto* packageIds = static_cast<std::vector<std::string>*>(context);
packageIds->emplace_back(pkg->package.id);
}
void createWidgets(lv_obj_t* parent, void* userData) {
@@ -68,21 +69,16 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
std::vector<const ::AppManifest*> manifests;
app_manager_for_each_manifest(collectManifest, &manifests);
std::ranges::sort(manifests, [](const ::AppManifest* a, const ::AppManifest* b) {
return strcmp(a->name, b->name) < 0;
});
// createWidgets() can rerun for this same Context (window rebuild-on-remove).
ctx->packageIds.clear();
app_manager_for_each_package(collectPackageId, &ctx->packageIds);
std::ranges::sort(ctx->packageIds);
size_t app_count = 0;
for (const auto* target_manifest: manifests) {
if (target_manifest->location.type == APP_LOCATION_PATH) {
app_count++;
createAppWidget(target_manifest, list);
}
for (const auto& packageId : ctx->packageIds) {
createPackageWidget(packageId.c_str(), list);
}
if (app_count == 0) {
if (ctx->packageIds.empty()) {
// lv_obj_align() is ignored for children of a flex-managed parent, so the empty-state
// label needs its own flex-growing wrapper to center within; the (empty) list is hidden
// rather than deleted so the wrapper can just take its place in the flex flow.
@@ -139,7 +135,7 @@ int32_t appMain(int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "tactility.appsettings",
.id = "tactility.apppackagelist",
.name = "Apps",
.category = APP_CATEGORY_SETTINGS,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
+1 -1
View File
@@ -10,9 +10,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -5,9 +5,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -10,9 +10,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -11,8 +11,8 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/start.h>
#include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h>
@@ -7,9 +7,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
+34 -1
View File
@@ -10,6 +10,7 @@
#ifdef ESP_PLATFORM
#include <Tactility/service/displayidle/DisplayIdleService.h>
#endif
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/settings/DisplaySettings.h>
#include <app/event.h>
@@ -90,6 +91,21 @@ void onOrientationSet(lv_event_t* event) {
}
}
void onFontSizeChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
uint32_t selected_index = lv_dropdown_get_selected(dropdown);
if (selected_index >= static_cast<uint32_t>(settings::display::FontSize::Count)) {
return;
}
auto selected_size = static_cast<settings::display::FontSize>(selected_index);
if (selected_size != ctx->displaySettings.fontSize) {
ctx->displaySettings.fontSize = selected_size;
ctx->displaySettingsUpdated = true;
lvgl::applyFontSize(selected_size);
}
}
void onTimeoutSwitch(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
@@ -208,6 +224,23 @@ void createWidgets(lv_obj_t* parent, void* userData) {
// Set the dropdown to match current orientation enum
lv_dropdown_set_selected(orientation_dropdown, static_cast<uint16_t>(ctx->displaySettings.orientation));
// Font size
auto* font_size_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(font_size_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(font_size_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(font_size_wrapper, 0, LV_STATE_DEFAULT);
auto* font_size_label = lv_label_create(font_size_wrapper);
lv_label_set_text(font_size_label, "Font size");
lv_obj_align(font_size_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* font_size_dropdown = lv_dropdown_create(font_size_wrapper);
lv_dropdown_set_options(font_size_dropdown, "Small\nDefault\nLarge");
lv_obj_align(font_size_dropdown, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(font_size_dropdown, onFontSizeChanged, LV_EVENT_VALUE_CHANGED, ctx);
lv_dropdown_set_selected(font_size_dropdown, static_cast<uint16_t>(ctx->displaySettings.fontSize));
// Screen timeout
// Note: DisplayIdleService doesn't act on these settings for kernel-driver displays yet
// (it only looks up the deprecated tt::hal::display::DisplayDevice), so these currently
@@ -274,7 +307,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
ctx->screensaverDropdown = lv_dropdown_create(screensaver_wrapper);
// Note: order correlates with settings::display::ScreensaverType enum order
lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan");
lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan\nMCP Screen");
lv_obj_align(ctx->screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(ctx->screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, ctx);
lv_dropdown_set_selected(ctx->screensaverDropdown, static_cast<uint16_t>(ctx->displaySettings.screensaverType));
@@ -6,9 +6,9 @@
#include <app/event.h>
#include <app/io.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h>
#include <lvgl_window_manager/window_manager.h>
@@ -8,9 +8,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -7,10 +7,10 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -2,9 +2,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h>
#include <lvgl_window_manager/window_manager.h>
+255 -171
View File
@@ -1,14 +1,28 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <Tactility/DeprecatedPaths.h>
#include <Tactility/MountPoints.h>
#include <Tactility/Tactility.h>
#include <Tactility/app/setup/Setup.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/settings/BootSettings.h>
#include <Tactility/settings/Time.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <string>
#include <lvgl.h>
#include <lvgl/icons/launcher.h>
#include <lvgl/fonts.h>
#include <lvgl/icons/launcher.h>
#include <lvgl/icons/statusbar.h>
#include <lvgl/lvgl.h>
#include <lvgl_window_manager/window_manager.h>
@@ -20,64 +34,29 @@
#include <tactility/log.h>
#include <tactility/memory.h>
#include <Tactility/app/setup/Setup.h>
#include <Tactility/settings/BootSettings.h>
#include <Tactility/Tactility.h>
namespace tt::app::launcher {
constexpr auto* TAG = "Launcher";
constexpr auto* BACKGROUND_ASSET = "color-field.png";
constexpr auto* CUSTOM_BACKGROUND_PATH = "tactility/launcher/background.bin";
constexpr lv_color_t TEXT_COLOR = LV_COLOR_MAKE(0xF8, 0xF5, 0xF2);
constexpr lv_color_t MUTED_TEXT_COLOR = LV_COLOR_MAKE(0xDF, 0xD6, 0xD3);
constexpr lv_color_t ACCENT_COLOR = LV_COLOR_MAKE(0xEC, 0x75, 0x69);
namespace {
uint32_t getButtonPadding(UiDensity density, uint32_t buttonSize) {
if (density == LVGL_UI_DENSITY_COMPACT) {
return 0;
} else {
return buttonSize / 8;
}
}
struct LauncherWidgets {
lv_obj_t* timeLabel = nullptr;
lv_obj_t* dateLabel = nullptr;
lv_obj_t* dataLabel = nullptr;
lv_obj_t* statusLabel = nullptr;
lv_timer_t* updateTimer = nullptr;
};
int32_t computeButtonMargin(int32_t available_span, int32_t total_button_size) {
const int32_t usable = std::max<int32_t>(0, available_span - (3 * total_button_size));
return std::min<int32_t>(usable / 16, total_button_size / 2);
}
void onAppPressed(lv_event_t* e) {
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
void onAppPressed(lv_event_t* event) {
const auto* app_id = static_cast<const char*>(lv_event_get_user_data(event));
uint32_t instance_id = 0;
app_start(appId, 0, nullptr, &instance_id);
}
lv_obj_t* createAppButton(lv_obj_t* parent, UiDensity uiDensity, const char* imageFile, const char* appId, int32_t itemMargin, bool isLandscape) {
const auto button_size = lvgl_get_launcher_icon_font_height();
const auto button_padding = getButtonPadding(uiDensity, button_size);
auto* apps_button = lv_button_create(parent);
lv_obj_set_style_pad_all(apps_button, static_cast<int32_t>(button_padding), LV_STATE_DEFAULT);
if (isLandscape) {
lv_obj_set_style_margin_hor(apps_button, itemMargin, LV_STATE_DEFAULT);
} else {
lv_obj_set_style_margin_ver(apps_button, itemMargin, LV_STATE_DEFAULT);
}
lv_obj_set_style_shadow_width(apps_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(apps_button, 0, LV_STATE_DEFAULT);
// create the image first
auto* button_image = lv_image_create(apps_button);
lv_obj_set_style_text_font(button_image, lvgl_get_launcher_icon_font(), LV_STATE_DEFAULT);
lv_image_set_src(button_image, imageFile);
lv_obj_set_style_text_color(button_image, lv_theme_get_color_primary(button_image), LV_STATE_DEFAULT);
lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, LV_STATE_DEFAULT);
// Ensure it's square (Material Symbols are slightly wider than tall)
lv_obj_set_size(button_image, button_size, button_size);
lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId);
return apps_button;
app_start(app_id, 0, nullptr, &instance_id);
}
bool shouldShowPowerButton() {
@@ -85,180 +64,288 @@ bool shouldShowPowerButton() {
device_for_each_of_type(&POWER_SUPPLY_TYPE, &show_power_button, [](Device* device, void* context) {
if (device_is_ready(device) && power_supply_supports_power_off(device)) {
*static_cast<bool*>(context) = true;
return false; // stop iterating
} else {
return true; // continue iterating
return false;
}
return true;
});
return show_power_button;
}
void onButtonsWrapperResized(lv_event_t* e);
int getBatteryPercentage() {
Device* power = nullptr;
device_for_each_of_type(&POWER_SUPPLY_TYPE, &power, [](Device* device, void* context) {
if (device_is_ready(device) && power_supply_supports_property(device, POWER_SUPPLY_PROP_CAPACITY)) {
*static_cast<Device**>(context) = device;
return false;
}
return true;
});
if (power == nullptr) return -1;
// The screen object outlives this window's own widgets (lvgl-window-manager deletes and
// recreates only the topmost window's widget on every app switch, not the screen itself), so
// the LV_EVENT_SIZE_CHANGED callback registered on it must be removed once buttons_wrapper is
// destroyed, to avoid a dangling user-data pointer the next time the display rotates while a
// different window is topmost.
void onButtonsWrapperDeleted(lv_event_t* e) {
auto* buttons_wrapper = lv_event_get_target_obj(e);
auto* screen = lv_obj_get_screen(buttons_wrapper);
lv_obj_remove_event_cb_with_user_data(screen, onButtonsWrapperResized, buttons_wrapper);
PowerSupplyPropertyValue charge_level;
if (power_supply_get_property(power, POWER_SUPPLY_PROP_CAPACITY, &charge_level) != ERROR_NONE) return -1;
return std::clamp(charge_level.int_value, 0, 100);
}
// Re-applies the flex direction and per-button margins when the display orientation changes
// while the launcher is the visible window (these are decided once at createWidgets() based on
// the resolution at that time, so a later rotation needs this to catch up).
void onButtonsWrapperResized(lv_event_t* e) {
auto* buttons_wrapper = static_cast<lv_obj_t*>(lv_event_get_user_data(e));
const auto* display = lv_obj_get_display(buttons_wrapper);
const auto button_size = lvgl_get_launcher_icon_font_height();
const auto button_padding = getButtonPadding(lvgl_get_ui_density(), button_size);
const auto total_button_size = button_size + (button_padding * 2);
const auto horizontal_px = lv_display_get_horizontal_resolution(display);
const auto vertical_px = lv_display_get_vertical_resolution(display);
const bool is_landscape_display = horizontal_px >= vertical_px;
const auto current_flow = lv_obj_get_style_flex_flow(buttons_wrapper, LV_PART_MAIN);
const bool was_landscape = current_flow == LV_FLEX_FLOW_ROW;
if (is_landscape_display == was_landscape) {
return;
}
lv_obj_set_flex_flow(buttons_wrapper, is_landscape_display ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN);
const int32_t margin = is_landscape_display
? computeButtonMargin(horizontal_px, total_button_size)
: computeButtonMargin(vertical_px, total_button_size);
const uint32_t child_count = lv_obj_get_child_count(buttons_wrapper);
for (uint32_t i = 0; i < child_count; i++) {
auto* button = lv_obj_get_child(buttons_wrapper, i);
lv_obj_set_style_margin_hor(button, is_landscape_display ? margin : 0, LV_STATE_DEFAULT);
lv_obj_set_style_margin_ver(button, is_landscape_display ? 0 : margin, LV_STATE_DEFAULT);
const char* getWifiStatusIcon(service::wifi::RadioState state) {
using enum service::wifi::RadioState;
switch (state) {
case ConnectionActive: return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_4_BAR;
case Off:
case OffPending: return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_OFF;
default: return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_0_BAR;
}
}
void createWidgets(lv_obj_t* parent, void*) {
auto* buttons_wrapper = lv_obj_create(parent);
const char* getBatteryStatusIcon(int percentage) {
if (percentage < 0) return "";
if (percentage >= 95) return LVGL_ICON_STATUSBAR_BATTERY_ANDROID_FRAME_FULL;
if (percentage >= 64) return LVGL_ICON_STATUSBAR_BATTERY_ANDROID_FRAME_5;
if (percentage >= 32) return LVGL_ICON_STATUSBAR_BATTERY_ANDROID_FRAME_3;
return LVGL_ICON_STATUSBAR_BATTERY_ANDROID_FRAME_1;
}
auto ui_density = lvgl_get_ui_density();
const auto button_size = lvgl_get_launcher_icon_font_height();
const auto button_padding = getButtonPadding(ui_density, button_size);
const auto total_button_size = button_size + (button_padding * 2);
lv_obj_t* createAppButton(lv_obj_t* parent, const char* icon, const char* app_id, bool emphasized) {
auto* button = lv_button_create(parent);
lv_obj_set_size(button, 52, 52);
lv_obj_set_style_radius(button, 15, LV_PART_MAIN);
lv_obj_set_style_shadow_width(button, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(button, emphasized ? 2 : 1, LV_PART_MAIN);
lv_obj_set_style_border_color(button, emphasized ? ACCENT_COLOR : lv_color_hex(0x746568), LV_PART_MAIN);
lv_obj_set_style_border_opa(button, emphasized ? LV_OPA_COVER : LV_OPA_60, LV_PART_MAIN);
lv_obj_set_style_bg_color(button, lv_color_hex(0x211B20), LV_PART_MAIN);
lv_obj_set_style_bg_opa(button, emphasized ? LV_OPA_80 : LV_OPA_70, LV_PART_MAIN);
lv_obj_set_style_bg_color(button, lv_color_hex(0x392C32), LV_STATE_PRESSED);
lv_obj_set_style_transform_scale(button, 238, LV_STATE_PRESSED);
lv_obj_set_style_outline_color(button, ACCENT_COLOR, LV_STATE_FOCUSED);
lv_obj_set_style_outline_width(button, 2, LV_STATE_FOCUSED);
lv_obj_set_style_outline_pad(button, 2, LV_STATE_FOCUSED);
lv_obj_align(buttons_wrapper, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_size(buttons_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_border_width(buttons_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_grow(buttons_wrapper, 1);
auto* image = lv_image_create(button);
lv_obj_set_size(image, 36, 36);
lv_obj_center(image);
lv_obj_set_style_text_font(image, lvgl_get_launcher_icon_font(), LV_STATE_DEFAULT);
lv_image_set_src(image, icon);
lv_obj_set_style_image_recolor(image, TEXT_COLOR, LV_STATE_DEFAULT);
lv_obj_set_style_image_recolor_opa(image, LV_OPA_COVER, LV_STATE_DEFAULT);
lv_obj_add_event_cb(button, onAppPressed, LV_EVENT_SHORT_CLICKED, const_cast<char*>(app_id));
return button;
}
// Fix for button selection
lv_obj_set_style_pad_all(buttons_wrapper, 6, LV_STATE_DEFAULT);
const auto* display = lv_obj_get_display(parent);
const auto horizontal_px = lv_display_get_horizontal_resolution(display);
const auto vertical_px = lv_display_get_vertical_resolution(display);
const bool is_landscape_display = horizontal_px >= vertical_px;
if (is_landscape_display) {
lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_ROW);
void updateInformation(LauncherWidgets& widgets) {
const std::time_t now = std::time(nullptr);
std::tm local_time {};
localtime_r(&now, &local_time);
char time_buffer[12];
char date_buffer[40];
if (local_time.tm_year >= 125) {
if (settings::isTimeFormat24Hour()) {
std::strftime(time_buffer, sizeof(time_buffer), "%H:%M", &local_time);
} else {
std::strftime(time_buffer, sizeof(time_buffer), "%I:%M", &local_time);
if (time_buffer[0] == '0') std::memmove(time_buffer, time_buffer + 1, std::strlen(time_buffer));
}
std::strftime(date_buffer, sizeof(date_buffer), "%A, %B %e", &local_time);
} else {
lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_COLUMN);
std::strcpy(time_buffer, "--:--");
std::strcpy(date_buffer, "Set date and time");
}
lv_label_set_text(widgets.timeLabel, time_buffer);
lv_label_set_text(widgets.dateLabel, date_buffer);
const auto wifi_state = service::wifi::getRadioState();
std::string sd_card_path;
const bool sd_ready = findFirstMountedSdCardPath(sd_card_path);
const int battery_percentage = getBatteryPercentage();
char data_buffer[80];
if (battery_percentage >= 0) {
std::snprintf(data_buffer, sizeof(data_buffer), "%s • %s • %d%%",
wifi_state == service::wifi::RadioState::ConnectionActive ? "Wi-Fi connected" : "Wi-Fi offline",
sd_ready ? "SD ready" : "No SD", battery_percentage);
} else {
std::snprintf(data_buffer, sizeof(data_buffer), "%s • %s",
wifi_state == service::wifi::RadioState::ConnectionActive ? "Wi-Fi connected" : "Wi-Fi offline",
sd_ready ? "SD ready" : "No SD");
}
lv_label_set_text(widgets.dataLabel, data_buffer);
char status_buffer[24];
std::snprintf(status_buffer, sizeof(status_buffer), "%s%s", getWifiStatusIcon(wifi_state), getBatteryStatusIcon(battery_percentage));
lv_label_set_text(widgets.statusLabel, status_buffer);
}
void onUpdateTimer(lv_timer_t* timer) {
updateInformation(*static_cast<LauncherWidgets*>(lv_timer_get_user_data(timer)));
}
std::string getDefaultBackgroundPath() {
return std::string(file::MOUNT_POINT_SYSTEM) + "/app/Launcher/assets/" + BACKGROUND_ASSET;
}
void createWidgets(lv_obj_t* parent, void* user_data) {
auto& widgets = *static_cast<LauncherWidgets*>(user_data);
lv_obj_set_style_bg_color(parent, lv_color_hex(0x211A20), LV_PART_MAIN);
lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_pad_all(parent, 0, LV_PART_MAIN);
lv_obj_clear_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
auto* display = lv_obj_get_display(parent);
const auto display_width = lv_display_get_horizontal_resolution(display);
const auto display_height = lv_display_get_vertical_resolution(display);
const bool is_portrait = display_height > display_width;
auto background_path = lvgl::PATH_PREFIX + getDefaultBackgroundPath();
std::string sd_card_path;
if (findFirstMountedSdCardPath(sd_card_path)) {
const auto custom_background_path = file::getChildPath(sd_card_path, CUSTOM_BACKGROUND_PATH);
if (file::isFile(custom_background_path)) {
const auto lvgl_custom_background_path = lvgl::PATH_PREFIX + custom_background_path;
lv_image_header_t header {};
if (lv_image_decoder_get_info(lvgl_custom_background_path.c_str(), &header) == LV_RESULT_OK) {
if (header.w >= display_width && header.h >= display_height) {
background_path = lvgl_custom_background_path;
LOG_I(TAG, "Using SD background %s (%ux%u, format 0x%02x)",
custom_background_path.c_str(), header.w, header.h, header.cf);
} else {
LOG_W(TAG, "Ignoring undersized SD background %s (%ux%u for %dx%d display)",
custom_background_path.c_str(), header.w, header.h, display_width, display_height);
}
} else {
LOG_W(TAG, "Ignoring invalid SD background %s", custom_background_path.c_str());
}
}
}
const int32_t margin = is_landscape_display
? computeButtonMargin(lv_display_get_horizontal_resolution(display), total_button_size)
: computeButtonMargin(lv_display_get_vertical_resolution(display), total_button_size);
auto* background = lv_image_create(parent);
lv_image_set_src(background, background_path.c_str());
lv_obj_align(background, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_flag(background, LV_OBJ_FLAG_IGNORE_LAYOUT);
auto* app_list_button = createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_APPS, "tactility.applist", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings", margin, is_landscape_display);
widgets.timeLabel = lv_label_create(parent);
lv_label_set_text(widgets.timeLabel, "--:--");
lv_obj_set_style_text_color(widgets.timeLabel, TEXT_COLOR, LV_PART_MAIN);
#if LV_FONT_MONTSERRAT_48
lv_obj_set_style_text_font(widgets.timeLabel, &lv_font_montserrat_48, LV_PART_MAIN);
#else
lv_obj_set_style_text_font(widgets.timeLabel, lvgl_get_text_font(FONT_SIZE_LARGE), LV_PART_MAIN);
#endif
lv_obj_align(widgets.timeLabel, LV_ALIGN_TOP_LEFT, 18, 58);
// The launcher's container is several levels below the screen, and LVGL only sends
// LV_EVENT_SIZE_CHANGED to the screen object itself on a resolution change - so the
// handler is attached there, with buttons_wrapper passed through as user data.
lv_obj_add_event_cb(lv_obj_get_screen(parent), onButtonsWrapperResized, LV_EVENT_SIZE_CHANGED, buttons_wrapper);
lv_obj_add_event_cb(buttons_wrapper, onButtonsWrapperDeleted, LV_EVENT_DELETE, nullptr);
widgets.dateLabel = lv_label_create(parent);
lv_obj_set_style_text_font(widgets.dateLabel, lvgl_get_text_font(FONT_SIZE_LARGE), LV_PART_MAIN);
lv_obj_set_style_text_color(widgets.dateLabel, TEXT_COLOR, LV_PART_MAIN);
lv_obj_align(widgets.dateLabel, LV_ALIGN_TOP_LEFT, 20, 118);
widgets.dataLabel = lv_label_create(parent);
lv_obj_set_width(widgets.dataLabel, is_portrait ? display_width - 40 : 225);
lv_label_set_long_mode(widgets.dataLabel, LV_LABEL_LONG_MODE_WRAP);
lv_obj_set_style_text_font(widgets.dataLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_PART_MAIN);
lv_obj_set_style_text_color(widgets.dataLabel, MUTED_TEXT_COLOR, LV_PART_MAIN);
lv_obj_align(widgets.dataLabel, LV_ALIGN_TOP_LEFT, 20, 148);
widgets.statusLabel = lv_label_create(parent);
lv_obj_set_style_text_font(widgets.statusLabel, lvgl_get_statusbar_icon_font(), LV_PART_MAIN);
lv_obj_set_style_text_color(widgets.statusLabel, TEXT_COLOR, LV_PART_MAIN);
lv_obj_align(widgets.statusLabel, LV_ALIGN_TOP_RIGHT, -15, 8);
auto* button_rail = lv_obj_create(parent);
if (is_portrait) {
lv_obj_set_size(button_rail, 184, 60);
lv_obj_align(button_rail, LV_ALIGN_BOTTOM_MID, 0, -7);
lv_obj_set_flex_flow(button_rail, LV_FLEX_FLOW_ROW);
} else {
lv_obj_set_size(button_rail, 60, 184);
lv_obj_align(button_rail, LV_ALIGN_RIGHT_MID, -7, 8);
lv_obj_set_flex_flow(button_rail, LV_FLEX_FLOW_COLUMN);
}
lv_obj_set_flex_align(button_rail, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(button_rail, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(button_rail, 0, LV_PART_MAIN);
lv_obj_set_style_bg_opa(button_rail, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_clear_flag(button_rail, LV_OBJ_FLAG_SCROLLABLE);
auto* app_list_button = createAppButton(button_rail, LVGL_ICON_LAUNCHER_APPS, "tactility.applist", true);
createAppButton(button_rail, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files", false);
createAppButton(button_rail, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings", false);
// Some devices (e.g. T-Lora Pager) have no other way to power off, so the
// button stays in the launcher; the confirmation flow lives in the PowerOff app.
if (shouldShowPowerButton()) {
auto* power_button = lv_button_create(parent);
lv_obj_set_style_pad_all(power_button, 8, 0);
lv_obj_align(power_button, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_add_event_cb(power_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)"tactility.poweroff");
lv_obj_set_style_shadow_width(power_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(power_button, 0, LV_PART_MAIN);
lv_obj_set_size(power_button, 36, 36);
lv_obj_align(power_button, LV_ALIGN_BOTTOM_LEFT, 16, -12);
lv_obj_set_style_radius(power_button, LV_RADIUS_CIRCLE, LV_PART_MAIN);
lv_obj_set_style_shadow_width(power_button, 0, LV_PART_MAIN);
lv_obj_set_style_bg_color(power_button, lv_color_hex(0x211B20), LV_PART_MAIN);
lv_obj_set_style_bg_opa(power_button, LV_OPA_70, LV_PART_MAIN);
lv_obj_add_event_cb(power_button, onAppPressed, LV_EVENT_SHORT_CLICKED, const_cast<char*>("tactility.poweroff"));
auto* power_label = lv_label_create(power_button);
lv_label_set_text(power_label, LV_SYMBOL_POWER);
lv_obj_set_style_text_color(power_label, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
lv_obj_set_style_text_color(power_label, TEXT_COLOR, LV_PART_MAIN);
lv_obj_center(power_label);
}
// If we don't have a touch device, we assume there's some other kind of input like a keyboard, an encoder or button control
// In that scenario we want to automatically have the app list button selected so the user doesn't have to press the widget selection
// an extra time.
if (!device_has_active_by_type(&POINTER_TYPE)) {
// lv_obj_update_layout(parent); // Resolve flex layout first, so focus/state invalidate against final coords
lv_group_focus_obj(app_list_button);
lv_obj_add_state(app_list_button, LV_STATE_FOCUS_KEY);
}
updateInformation(widgets);
widgets.updateTimer = lv_timer_create(onUpdateTimer, 1000, &widgets);
}
void destroyWidgets(void* user_data) {
auto& widgets = *static_cast<LauncherWidgets*>(user_data);
if (widgets.updateTimer != nullptr) {
lv_timer_delete(widgets.updateTimer);
widgets.updateTimer = nullptr;
}
widgets.timeLabel = nullptr;
widgets.dateLabel = nullptr;
widgets.dataLabel = nullptr;
widgets.statusLabel = nullptr;
}
void runAutoStart() {
settings::BootSettings boot_properties;
AppManifest manifest;
if (
// Auto-start due to built-in requirement
strcmp(CONFIG_TT_AUTO_START_APP_ID, "") != 0 &&
app_manager_find_manifest(CONFIG_TT_AUTO_START_APP_ID, &manifest) == ERROR_NONE
) {
if (strcmp(CONFIG_TT_AUTO_START_APP_ID, "") != 0 &&
app_manager_find_manifest(CONFIG_TT_AUTO_START_APP_ID, &manifest) == ERROR_NONE) {
LOG_I(TAG, "Starting %s", CONFIG_TT_AUTO_START_APP_ID);
uint32_t app_launch_id;
app_start(CONFIG_TT_AUTO_START_APP_ID, 0, nullptr, &app_launch_id);
} else if (
// Auto-start due to user configuration
settings::loadBootSettings(boot_properties) &&
!boot_properties.autoStartAppId.empty() &&
app_manager_find_manifest(boot_properties.autoStartAppId.c_str(), &manifest) == ERROR_NONE
) {
} else if (settings::loadBootSettings(boot_properties) &&
!boot_properties.autoStartAppId.empty() &&
app_manager_find_manifest(boot_properties.autoStartAppId.c_str(), &manifest) == ERROR_NONE) {
LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
uint32_t app_launch_id;
app_start(boot_properties.autoStartAppId.c_str(), 0, nullptr, &app_launch_id);
} else {
// No auto-start, consider running system setup
if (!setup::isCompleted()) {
setup::start();
}
} else if (!setup::isCompleted()) {
setup::start();
}
}
int32_t appMain(int argc, char* argv[]) {
uint32_t appInstanceId = app_scheduler_current_app_id();
uint32_t app_instance_id = app_scheduler_current_app_id();
runAutoStart();
LauncherWidgets widgets;
WindowId window = window_manager_create_ext(app_instance_id, createWidgets, destroyWidgets, &widgets);
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
// The launcher is meant to stay resident (it's the home screen) - it only gives up its
// thread when app-module's scheduler asks it to (e.g. another new-model app is started).
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
bool should_close = false;
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
should_close = true;
break;
}
}
if (shouldClose) break;
if (should_close) break;
}
window_manager_remove(window);
@@ -275,16 +362,13 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN,
// No file IO, so callstack can be in external RAM
.stack = { .depth = 3072 , .desired_memory_capability = MEMORY_CAPABILITY_EXTERNAL }
.stack = { .depth = 3072, .desired_memory_capability = MEMORY_CAPABILITY_EXTERNAL }
};
// Kept for Tactility/Private/Tactility/app/launcher/Launcher.h's existing declaration (still
// used by the old, unconverted CrashDiagnostics app to return to the launcher after a crash).
uint32_t start() {
uint32_t instance_id = 0;
app_start(manifest.id, 0, nullptr, &instance_id);
return instance_id;
}
} // namespace
} // namespace tt::app::launcher
@@ -0,0 +1,98 @@
#ifdef ESP_PLATFORM
#include <Tactility/Tactility.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/settings/McpSettings.h>
#include <Tactility/settings/WebServerSettings.h>
#include <Tactility/service/webserver/WebServerService.h>
#include <app/event.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h>
#include <lvgl/widgets/toolbar.h>
#include <lvgl/lvgl.h>
#include <tactility/check.h>
#include <tactility/log.h>
#include <esp_netif.h>
#include <lvgl.h>
#include <string>
namespace tt::app::mcpsettings {
constexpr auto* TAG = "McpSettingsApp";
extern const ::AppManifest manifest;
namespace {
struct Context {
uint32_t appInstanceId;
settings::mcp::McpSettings mcpSettings;
settings::webserver::WebServerSettings wsSettings;
bool updated = false;
lv_obj_t* switchMcpEnabled = nullptr;
lv_obj_t* labelUrlValue = nullptr;
};
void updateUrlDisplay(Context* ctx) {
if (ctx->labelUrlValue == nullptr) return;
if (!ctx->mcpSettings.mcpEnabled) { lv_label_set_text(ctx->labelUrlValue, "Disabled"); return; }
std::string url = "http://";
bool ipAdded = false;
for (const char* key : {"WIFI_STA_DEF", "WIFI_AP_DEF"}) {
auto* netif = esp_netif_get_handle_from_ifkey(key);
if (netif != nullptr) {
esp_netif_ip_info_t info;
if (esp_netif_get_ip_info(netif, &info) == ESP_OK && info.ip.addr != 0) {
char ip[16]; snprintf(ip, sizeof(ip), IPSTR, IP2STR(&info.ip));
url += ip; ipAdded = true; break;
}
}
}
if (!ipAdded) url += ctx->wsSettings.wifiMode == settings::webserver::WiFiMode::AccessPoint ? "192.168.4.1" : "Connecting...";
if (url.starts_with("http://") && ctx->wsSettings.webServerPort != 80) url += ":" + std::to_string(ctx->wsSettings.webServerPort);
url += "/api/mcp";
lv_label_set_text(ctx->labelUrlValue, url.c_str());
}
void onBackPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
app_event_emit_close(ctx->appInstanceId);
}
void onMcpEnabledSwitch(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
const bool enabled = lv_obj_has_state(ctx->switchMcpEnabled, LV_STATE_CHECKED);
getMainDispatcher().dispatch([ctx, enabled] {
ctx->mcpSettings.mcpEnabled = enabled; ctx->updated = true;
lvgl_lock(); updateUrlDisplay(ctx); lvgl_unlock();
if (!settings::mcp::save(ctx->mcpSettings)) LOG_W(TAG, "Failed to persist MCP settings");
service::webserver::getPubsub()->publish(service::webserver::WebServerEvent::WebServerSettingsChanged);
service::webserver::setWebServerEnabled(enabled);
});
}
void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->wsSettings = settings::webserver::loadOrGetDefault();
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = lvgl_toolbar_create(parent, "MCP Settings");
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
ctx->switchMcpEnabled = lvgl_toolbar_add_switch_action(toolbar);
if (ctx->mcpSettings.mcpEnabled) lv_obj_add_state(ctx->switchMcpEnabled, LV_STATE_CHECKED);
lv_obj_add_event_cb(ctx->switchMcpEnabled, onMcpEnabledSwitch, LV_EVENT_VALUE_CHANGED, ctx);
auto* main = lv_obj_create(parent); lv_obj_set_flex_flow(main, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main, LV_PCT(100)); lv_obj_set_flex_grow(main, 1);
auto* wrapper = lv_obj_create(main); lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(wrapper, 10, LV_STATE_DEFAULT); lv_obj_set_style_border_width(wrapper, 1, LV_STATE_DEFAULT); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_flex_cross_place(wrapper, LV_FLEX_ALIGN_START, 0);
auto* title = lv_label_create(wrapper); lv_label_set_text(title, "MCP Endpoint URL:");
ctx->labelUrlValue = lv_label_create(wrapper); updateUrlDisplay(ctx);
auto* info = lv_label_create(main); lv_label_set_long_mode(info, LV_LABEL_LONG_WRAP); lv_obj_set_width(info, LV_PCT(95));
lv_label_set_text(info, "MCP (Model Context Protocol) Screen service allows LLMs to interact with the device screen, audio, and tools directly.\n\nEndpoints:\n- POST /api/mcp (JSON-RPC tools)\n- POST /api/screen/raw (big-endian RGB565 writes)\n\nTo show the LLM canvas, select 'MCP Screen' in Settings -> Display -> Screensaver. The canvas also pops up automatically when an LLM sends a draw command.");
}
int32_t appMain(int, char**) {
Context ctx{}; ctx.appInstanceId = app_scheduler_current_app_id(); ctx.mcpSettings = settings::mcp::loadOrGetDefault();
TaskEventGroup group{}; task_event_group_construct(&group); AppEventSubscription sub{}; check(app_event_subscribe(&sub, &group) == ERROR_NONE);
auto window = window_manager_create(ctx.appInstanceId, createWidgets, &ctx); bool close = false;
while (!close) { task_event_group_wait_any(&group, nullptr, portMAX_DELAY); AppEvent event{}; while (app_event_poll(&sub, &event) == ERROR_NONE) if (event.type == APP_EVENT_CLOSE) { close = true; break; } }
window_manager_remove(window); check(app_event_unsubscribe(&sub) == ERROR_NONE); task_event_group_destruct(&group); return 0;
}
}
extern const ::AppManifest manifest = { .id = "McpSettings", .name = "MCP Screen", .category = APP_CATEGORY_SETTINGS, .location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }, .flags = 0, .stack = { .depth = 8192, .desired_memory_capability = 0 } };
}
#endif
+1 -1
View File
@@ -7,9 +7,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h>
#include <lvgl_window_manager/window_manager.h>
@@ -2,9 +2,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
+2 -2
View File
@@ -1,8 +1,8 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -118,7 +118,7 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 2400, .desired_memory_capability = 0 },
.stack = { .depth = 3072, .desired_memory_capability = 0 },
};
} // namespace
+1 -1
View File
@@ -10,9 +10,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -5,9 +5,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -8,9 +8,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -5,9 +5,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>

Some files were not shown because too many files have changed in this diff Show More