Compare commits

..

2 Commits

Author SHA1 Message Date
Adolfo Reyna a321bfeb4c fix(es3c35p): align runtime configuration 2026-09-07 23:15:26 -04:00
Adolfo Reyna f95cd7df4c Add ES3C35P board support 2026-09-07 23:04:44 -04:00
95 changed files with 1181 additions and 8008 deletions
+4 -2
View File
@@ -22,17 +22,19 @@ runs:
run: python Buildscripts/release-sdk-posix.py release/TactilitySDK run: python Buildscripts/release-sdk-posix.py release/TactilitySDK
- name: 'Test Integration Prep' - name: 'Test Integration Prep'
shell: bash 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: env:
TACTILITY_ARCH: ${{ steps.arch.outputs.value }} TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
run: | run: |
TACTILITY_SDK_NAME="$(cat version.txt)-posix-$TACTILITY_ARCH" TACTILITY_SDK_NAME="0.0.0-posix-$TACTILITY_ARCH"
mkdir -p test_sdk/$TACTILITY_SDK_NAME mkdir -p test_sdk/$TACTILITY_SDK_NAME
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
- name: 'Test Integration' - name: 'Test Integration'
shell: bash shell: bash
env: env:
TACTILITY_ARCH: ${{ steps.arch.outputs.value }} TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
run: cd Tests/SdkIntegration && TACTILITY_SDK_PATH=../../test_sdk python tactility.py build -a posix-$TACTILITY_ARCH --local-sdk run: cd Tests/SdkIntegration && TACTILITY_SDK_PATH=../../test_sdk python tactility.py build posix-$TACTILITY_ARCH --local-sdk
- name: 'Upload Artifact' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: 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 # 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: # 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: | run: |
TACTILITY_SDK_NAME="$(cat version.txt)-${{ inputs.arch }}" TACTILITY_SDK_NAME="0.0.0-${{ inputs.arch }}"
mkdir -p test_sdk/$TACTILITY_SDK_NAME mkdir -p test_sdk/$TACTILITY_SDK_NAME
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
- name: 'Test Integration' - name: 'Test Integration'
@@ -43,7 +43,7 @@ runs:
with: with:
esp_idf_version: v5.5.2 esp_idf_version: v5.5.2
target: ${{ inputs.arch }} target: ${{ inputs.arch }}
command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build -a ${{ inputs.arch }} --local-sdk command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build ${{ inputs.arch }} --local-sdk
- name: 'Upload Artifact' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -1,87 +0,0 @@
#!/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.

Before

Width:  |  Height:  |  Size: 298 KiB

-5
View File
@@ -20,8 +20,3 @@ lvgl.colorDepth=16
storage.userDataLocation=SD storage.userDataLocation=SD
dependencies.useDeprecatedHal=false 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
@@ -10,13 +10,10 @@ properties:
type: int type: int
default: 20 default: 20
description: Ambient temperature in °C, used for waveform timing compensation description: Ambient temperature in °C, used for waveform timing compensation
quality-draw-mode: draw-mode:
type: int type: int
default: MODE_GC16 default: MODE_DU
description: > description: Default EpdDrawMode waveform used for screen updates (e.g. MODE_DU, MODE_GC16)
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: rotation:
type: int type: int
default: EPD_ROT_PORTRAIT default: EPD_ROT_PORTRAIT
+1 -1
View File
@@ -7,7 +7,7 @@ apps.launcherAppId=tactility.launcher
hardware.target=esp32s3 hardware.target=esp32s3
hardware.flashSize=16MB hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OPI
hardware.spiRamSpeed=80M hardware.spiRamSpeed=80M
hardware.esptoolFlashFreq=80M hardware.esptoolFlashFreq=80M
hardware.tinyUsbMsc=true hardware.tinyUsbMsc=true
@@ -1,177 +0,0 @@
// 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,
};
@@ -1,20 +0,0 @@
// 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,10 +7,8 @@
#include <tactility/error.h> #include <tactility/error.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/module.h> #include <tactility/module.h>
#include <tactility/time.h>
#include "epd_board_m5papers3.h"
#include <epd_board.h>
#include <epdiy.h> #include <epdiy.h>
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
@@ -21,70 +19,6 @@
#define TAG "Papers3Display" #define TAG "Papers3Display"
#define GET_CONFIG(device) (static_cast<const Papers3DisplayConfig*>((device)->config)) #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 "C" {
extern Module m5stack_papers3_module; extern Module m5stack_papers3_module;
@@ -100,15 +34,6 @@ struct Papers3DisplayInternal {
// Scratch buffer for the grayscale8->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap(). // Scratch buffer for the grayscale8->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap().
uint8_t* packed_buffer; uint8_t* packed_buffer;
bool powered; 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) { static void power_on(Papers3DisplayInternal* internal) {
@@ -139,48 +64,9 @@ 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 // 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. // draw_bitmap(), so it never has to undo content LVGL already put on screen.
epd_fullclear(&internal->hl_state, config->temperature_celsius); 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; 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 // 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. // 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. // So draw_bitmap is called once per changed tile, not necessarily the whole panel.
@@ -190,12 +76,11 @@ 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 width = x_end - x_start;
const int32_t height = y_end - y_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 // 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, // (0x00=black..0xFF=white, matching LVGL's L8). EPDiy wants 4bpp packed (2px/byte, 0x0=black,
// 0xF=white); Bayer dithering (full 16-level for the quality pass, binary for MODE_DU) // 0xF=white) - a plain >>4 truncation preserves all 16 real gray levels the panel supports
// spreads the rounding error instead of a flat truncation. // (this panel is not B/W-only; see MODE_GC16/GL16 in papers3-display.yaml's draw-mode doc).
const auto* src = static_cast<const uint8_t*>(color_data); const auto* src = static_cast<const uint8_t*>(color_data);
const size_t src_stride = static_cast<size_t>(width); const size_t src_stride = static_cast<size_t>(width);
const size_t packed_stride = static_cast<size_t>(width + 1) / 2; const size_t packed_stride = static_cast<size_t>(width + 1) / 2;
@@ -205,18 +90,12 @@ 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; uint8_t* dst_row = internal->packed_buffer + static_cast<size_t>(row) * packed_stride;
int32_t col = 0; int32_t col = 0;
for (; col + 2 <= width; col += 2) { for (; col + 2 <= width; col += 2) {
const uint8_t p0 = use_quality const uint8_t p0 = src_row[col] >> 4U;
? dither_to_nibble(src_row[col], x_start + col, y_start + row) const uint8_t p1 = src_row[col + 1] >> 4U;
: 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); dst_row[col / 2] = static_cast<uint8_t>((p1 << 4U) | p0);
} }
if (col < width) { // odd width: last column has no pair, low nibble unused if (col < width) { // odd width: last column has no pair, low nibble unused
dst_row[col / 2] = use_quality dst_row[col / 2] = static_cast<uint8_t>(src_row[col] >> 4U);
? 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);
} }
} }
@@ -229,15 +108,13 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3
power_on(internal); power_on(internal);
epd_draw_rotated_image(update_area, internal->packed_buffer, internal->framebuffer); 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( auto draw_result = epd_hl_update_area(
&internal->hl_state, &internal->hl_state,
static_cast<EpdDrawMode>(draw_mode | MODE_PACKING_2PPB), static_cast<EpdDrawMode>(config->draw_mode | MODE_PACKING_2PPB),
config->temperature_celsius, config->temperature_celsius,
update_area update_area
); );
commit_quality_mode_decision(internal, use_quality, draw_result == EPD_DRAW_SUCCESS);
return draw_result == EPD_DRAW_SUCCESS ? ERROR_NONE : ERROR_RESOURCE; return draw_result == EPD_DRAW_SUCCESS ? ERROR_NONE : ERROR_RESOURCE;
} }
@@ -256,11 +133,13 @@ static DisplayColorFormat papers3_display_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_GRAYSCALE8; return DISPLAY_COLOR_FORMAT_GRAYSCALE8;
} }
// epd_width()/epd_height() are the panel's native, unrotated dimensions; epd_rotated_display_ // epd_width()/epd_height() are the panel's native, unrotated dimensions (display->width/height in
// width()/height() swap them for EPD_ROT_PORTRAIT/INVERTED_PORTRAIT. epd_draw_rotated_image() // epdiy.c) - epd_rotated_display_width()/height() swap them for EPD_ROT_PORTRAIT/INVERTED_PORTRAIT.
// clamps its input rect against the rotated dims and epd_draw_pixel() applies the rotation // epd_draw_rotated_image() clamps its input rect against the *rotated* dims and epd_draw_pixel()
// transform on top of that (see _rotate() in epdiy.c), so both LVGL's canvas size and // applies the rotation transform on top of that (see _rotate() in epdiy.c), so both LVGL's canvas
// draw_bitmap()'s rect must be in rotated-space, not native-space. // 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.
static uint16_t papers3_display_get_resolution_x(Device*) { static uint16_t papers3_display_get_resolution_x(Device*) {
return static_cast<uint16_t>(epd_rotated_display_width()); return static_cast<uint16_t>(epd_rotated_display_width());
} }
@@ -353,11 +232,6 @@ static error_t start(Device* device) {
return ERROR_OUT_OF_MEMORY; 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); 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()); 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 { struct Papers3DisplayConfig {
int temperature_celsius; int temperature_celsius;
enum EpdDrawMode quality_draw_mode; enum EpdDrawMode draw_mode;
enum EpdRotation rotation; enum EpdRotation rotation;
}; };
@@ -16,8 +16,6 @@
#include <esp_elf.h> #include <esp_elf.h>
#include <esp_err.h> #include <esp_err.h>
#include <sys/stat.h>
#include <cstdio> #include <cstdio>
#include <new> #include <new>
#include <string> #include <string>
@@ -66,22 +64,13 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
return ERROR_NONE; return ERROR_NONE;
} }
bool is_regular_file(const std::string& path) { // location.location can be either an app's install directory or the .elf file directly; the
struct stat path_stat {}; // former resolves to the per-target binary at {dir}/elf/{CONFIG_IDF_TARGET}.elf.
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) { std::string resolve_elf_path(const std::string& path) {
if (path.ends_with(".elf")) { if (path.ends_with(".elf")) {
return path; return path;
} }
std::string candidate = path + "/bin/" CONFIG_IDF_TARGET "/app.elf"; return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
return is_regular_file(candidate) ? candidate : "";
} }
constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = { constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
-9
View File
@@ -9,7 +9,6 @@ tactility_add_module(app-module
PRIV_INCLUDE_DIRS private/ PRIV_INCLUDE_DIRS private/
INCLUDE_DIRS include/ INCLUDE_DIRS include/
REQUIRES TactilityKernel service-module minitar REQUIRES TactilityKernel service-module minitar
PRIV_REQUIRES TactilityKernelCpp
) )
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close() # Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close()
@@ -20,11 +19,3 @@ if (NOT APPLE)
tactility_get_module_name(app-module MODULE_NAME) tactility_get_module_name(app-module MODULE_NAME)
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO) target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
endif () 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 ()
+14 -15
View File
@@ -12,8 +12,8 @@ extern "C" {
/** /**
* Computes the install directory for @a app_id (does not check whether anything is actually * Computes the install directory for @a app_id (does not check whether anything is actually
* installed there). * installed there).
* @param[out] path always NULL-terminated on return, even on failure. Empty when @a path_size * @param[out] path always NULL-terminated on return, even on failure (empty string if
* is 0, since nothing is written in that case. * @a path_size == 0 - nothing is written in that case; otherwise at least "" is written)
* @retval ERROR_NONE on success * @retval ERROR_NONE on success
* @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the * @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the
* NULL terminator) * NULL terminator)
@@ -22,15 +22,14 @@ extern "C" {
error_t app_get_install_path(const char* app_id, char* path, size_t path_size); error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
/** /**
* Installs a package from a tarball at @a source_path: extracts it into the package's install * Installs an app from a tarball at @a source_path: extracts it into the app install directory,
* directory, parses the extracted manifest.properties (see app/package_manifest.h) into a * parses the extracted manifest.properties (see app/metadata.h) to determine its id, then
* PackageManifest and one or more AppManifestBindings, then registers each with app_manager_add() * registers it with app_manager_add() as an AppLocation{APP_LOCATION_PATH, <install dir>} app.
* as an AppLocation{APP_LOCATION_PATH, <binary path>} app. * If an app with the same id is already installed (via a previous app_install() call), it is
* If a package 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
* uninstalled first: every one of its apps stopped if running, then its old install directory * one takes its place.
* removed, before the new one takes its place. * @param[in] source_path path to a tar file containing the app (must have manifest.properties
* @param[in] source_path path to a tar file containing the package (must have * at its root)
* manifest.properties at its root)
* @retval ERROR_NONE on success * @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND @a source_path doesn't exist / can't be read * @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 * @retval ERROR_INVALID_ARGUMENT the tarball has no valid manifest.properties at its root
@@ -38,11 +37,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); error_t app_install(const char* source_path);
/** /**
* Uninstalls a previously app_install()-ed package: stops every one of its apps if currently * Uninstalls a previously app_install()-ed app: stops it if currently running, deletes its
* running, deletes its install directory, and unregisters all of them (app_manager_remove()). * install directory, and unregisters it (app_manager_remove()).
* @param[in] app_id the package id it was installed under (PackageManifest::id) * @param[in] app_id the id the app was installed under (AppMetadata::app_id)
* @retval ERROR_NONE on success * @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND no such package was installed via app_install() * @retval ERROR_NOT_FOUND no such app was installed via app_install()
*/ */
error_t app_uninstall(const char* app_id); error_t app_uninstall(const char* app_id);
+2 -2
View File
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#pragma once #pragma once
#include "location.h"
#include <app/manifest.h> #include <app/manifest.h>
#include <tactility/error.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <tactility/error.h> #include "location.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
+2 -45
View File
@@ -3,7 +3,6 @@
#include <app/instance.h> #include <app/instance.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/stream.h> #include <app/stream.h>
#include <tactility/error.h> #include <tactility/error.h>
@@ -45,48 +44,6 @@ error_t app_manager_find_manifest(const char* id, struct AppManifest* out_manife
typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context); typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context);
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, 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 /** 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. */ * through to app_stream_subscribe() as-is; see its own doc for the ownership contracts. */
struct AppStreamBinding { struct AppStreamBinding {
@@ -133,8 +90,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 * 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/package_manifest.h), matching the * @a path is expected to hold a manifest.properties (see app/metadata.h), matching the layout
* layout app_install() creates ({install dir}/{package id}/manifest.properties), though this is not * app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not
* install/uninstall - it only ever adds/removes manifest registrations, never touches files on * 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 - * 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. * call app_manager_install_path_scan() to do that.
+6 -11
View File
@@ -11,10 +11,7 @@ extern "C" {
#endif #endif
// Character count, excluding null terminator // Character count, excluding null terminator
#define APP_MANIFEST_ID_LENGTH 32 #define APP_ID_LENGTH 32
// Character count, excluding null terminator
#define APP_MANIFEST_NAME_LENGTH 32
/** Broad classification of an app, used for grouping/launcher presentation. */ /** Broad classification of an app, used for grouping/launcher presentation. */
enum AppCategory { enum AppCategory {
@@ -48,10 +45,10 @@ struct AppStackConfig {
/** Describes a registrable app. One manifest exists per app id. */ /** Describes a registrable app. One manifest exists per app id. */
struct AppManifest { struct AppManifest {
/** Unique app identifier. Must be NULL-terminated. */ /** Unique app identifier. Should never be NULL. */
char id[APP_MANIFEST_ID_LENGTH + 1]; const char* id;
/** Human-readable name. Must be NULL-terminated. */ /** Human-readable name. Should never be NULL. */
char name[APP_MANIFEST_NAME_LENGTH + 1]; const char* name;
enum AppCategory category; enum AppCategory category;
struct AppLocation location; struct AppLocation location;
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */ /** Bitmask of AppManifestFlags. Most apps should leave this 0. */
@@ -60,9 +57,7 @@ struct AppManifest {
struct AppStackConfig stack; struct AppStackConfig stack;
}; };
bool app_manifest_id_is_valid(const char* id); bool app_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 #ifdef __cplusplus
} }
+74
View File
@@ -0,0 +1,74 @@
// 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
@@ -1,88 +0,0 @@
// 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
@@ -1,22 +0,0 @@
// 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,11 +3,8 @@
#include <app/instance.h> #include <app/instance.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/private/fd_table.h> #include <app/private/fd_table.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h> #include <tactility/concurrent/mutex.h>
#include <tactility/freertos/freertos.h> #include <tactility/freertos/freertos.h>
#include <tactility/freertos/semphr.h> #include <tactility/freertos/semphr.h>
@@ -16,7 +13,6 @@
#include <stdint.h> #include <stdint.h>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector>
/** /**
* A dedicated completion signal(1) for one app instance's task, given as the * A dedicated completion signal(1) for one app instance's task, given as the
@@ -61,16 +57,8 @@ struct AppInstanceRecord {
AppFdTable fd_table {}; 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 { struct AppLedger {
std::unordered_map<std::string, const AppManifest*> manifests; 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; std::unordered_map<uint32_t, AppInstanceRecord> instances;
uint32_t next_instance_id = 1; uint32_t next_instance_id = 1;
Mutex mutex {}; Mutex mutex {};
@@ -0,0 +1,35 @@
// 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));
@@ -1,56 +0,0 @@
// 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);
+75 -93
View File
@@ -2,14 +2,10 @@
#include <app/install.h> #include <app/install.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/package_manifest.h> #include <app/metadata.h>
#include <app/private/binary_path.h>
#include <app/private/fs.h> #include <app/private/fs.h>
#include <app/private/ledger.h> #include <app/private/ledger.h>
#include <app/private/package_manifest_parsing.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h> #include <tactility/concurrent/mutex.h>
#include <tactility/log.h> #include <tactility/log.h>
@@ -33,7 +29,7 @@ constexpr auto* TAG = "app_install";
namespace { 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) // app_metadata_parsing.cpp for the same constraint applied to properties-file loading)
std::string last_path_segment(const std::string& path) { std::string last_path_segment(const std::string& path) {
@@ -168,9 +164,9 @@ bool untar(const std::string& tar_path, const std::string& destination_path) {
// endregion // endregion
// region Staging-path lock: at most one caller may clean up/populate a given staging_path at a // 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_package() // 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 lock, one call's // concurrently, e.g. two uploads sharing a source basename - without this, one call's cleanup
// cleanup could delete or overwrite the staging directory another call is still extracting into. // can delete or overwrite the staging directory another call is still extracting into.
struct StagingLock { struct StagingLock {
Mutex mutex {}; Mutex mutex {};
@@ -227,19 +223,18 @@ void release_staging_lock(const std::string& path) {
// endregion // endregion
// region Installed-package registry: owns the AppManifests (and their backing location-path // region Installed-app registry: owns the AppManifest (and its id/name/path strings) that
// strings) that app_manager's ledger only keeps non-owning pointers to (see app_manager_add()'s // app_manager's ledger only keeps a non-owning pointer to (see app_manager_add()'s contract).
// contract). AppManifest::id/name own their own storage directly (fixed arrays), so this doesn't
// need to separately own those.
struct InstalledPackageRecord { struct InstalledAppRecord {
std::string path; // install directory std::string id;
std::vector<AppManifest> manifests; // one per AppManifest the package declared std::string name;
std::vector<std::string> locations; // backs manifests[i].location.location, same indices std::string path;
AppManifest manifest {};
}; };
struct InstallRegistry { struct InstallRegistry {
std::unordered_map<std::string, std::unique_ptr<InstalledPackageRecord>> apps; std::unordered_map<std::string, std::unique_ptr<InstalledAppRecord>> apps;
Mutex mutex {}; Mutex mutex {};
InstallRegistry() { mutex_construct(&mutex); } InstallRegistry() { mutex_construct(&mutex); }
@@ -250,68 +245,46 @@ InstallRegistry& install_registry() {
return registry; return registry;
} }
// Registers every AppManifest in @a bindings (@a count of them) with app_manager_add(), taking // Registers @a app_dir_path (already confirmed to hold a valid manifest.properties, parsed into
// ownership of their backing location strings, then registers @a package itself. // @a metadata) with app_manager_add(), taking ownership of its id/name/path strings.
// @warning Caller must hold install_registry().mutex. // @warning Caller must hold install_registry().mutex, and must have already ensured
error_t register_installed_package_locked(const PackageManifest& package, const std::string& install_path, const AppManifestBinding* bindings, size_t count) { // @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) {
auto& registry = install_registry(); auto& registry = install_registry();
auto record = std::make_unique<InstalledPackageRecord>(); auto record = std::make_unique<InstalledAppRecord>();
record->path = install_path; record->id = metadata.app_id;
// Sized once, up front: manifests[i].location.location points into locations[i].c_str(), record->name = metadata.app_name;
// which would dangle if either vector reallocated afterward. record->path = app_dir_path;
record->manifests.resize(count); record->manifest = AppManifest {
record->locations.resize(count); .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 },
};
for (size_t i = 0; i < count; i++) { // Belt-and-braces: app_install()'s earlier app_manager_remove() call is meant to have
record->manifests[i] = bindings[i].manifest; // already cleared any stale registration for this id (e.g. left over from
record->locations[i] = app_resolve_binary_path(install_path, bindings[i].binary); // app_manager_install_path_scan()'s separate registry), but that call happens before the
record->manifests[i].location = { APP_LOCATION_PATH, const_cast<char*>(record->locations[i].c_str()) }; // 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++) { registry.apps[record->id] = std::move(record);
// 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; return ERROR_NONE;
} }
// Stops every currently-running instance of @a manifest. Collects matching instance ids while // 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 // 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. // the instance's own thread_main()) is held, or the two threads would deadlock each other.
void stop_all_instances_of(const AppManifest* manifest) { void stop_all_instances_of(const AppManifest* manifest) {
@@ -332,22 +305,20 @@ void stop_all_instances_of(const AppManifest* manifest) {
} }
// Caller must already hold install_registry().mutex // Caller must already hold install_registry().mutex
error_t uninstall_locked(const std::string& package_id) { error_t uninstall_locked(const std::string& app_id) {
auto& registry = install_registry(); auto& registry = install_registry();
auto iterator = registry.apps.find(package_id); auto iterator = registry.apps.find(app_id);
if (iterator == registry.apps.end()) { if (iterator == registry.apps.end()) {
return ERROR_NOT_FOUND; return ERROR_NOT_FOUND;
} }
for (const auto& manifest : iterator->second->manifests) { // Can't uninstall in-memory apps
// Can't uninstall in-memory apps if (iterator->second->manifest.location.type != APP_LOCATION_PATH) {
if (manifest.location.type != APP_LOCATION_PATH) { return ERROR_NOT_SUPPORTED;
continue;
}
stop_all_instances_of(&manifest);
app_manager_remove(manifest.id);
} }
app_manager_remove_package(package_id.c_str());
stop_all_instances_of(&iterator->second->manifest);
app_manager_remove(app_id.c_str());
delete_recursively(iterator->second->path); delete_recursively(iterator->second->path);
registry.apps.erase(iterator); registry.apps.erase(iterator);
@@ -381,7 +352,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) { error_t app_install(const char* source_path) {
LOG_I(TAG, "Installing app package from %s", source_path); LOG_I(TAG, "Installing app from %s", source_path);
std::string app_parent_path; std::string app_parent_path;
if (!get_app_install_directory(app_parent_path)) { if (!get_app_install_directory(app_parent_path)) {
@@ -418,9 +389,8 @@ error_t app_install(const char* source_path) {
return ERROR_INVALID_ARGUMENT; return ERROR_INVALID_ARGUMENT;
} }
PackageManifest package {}; AppMetadata metadata {};
std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>> app_bindings; if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
if (app_package_manifest_parse_into(manifest_path.c_str(), package, app_bindings) != ERROR_NONE) {
LOG_E(TAG, "Install failed: invalid manifest"); LOG_E(TAG, "Install failed: invalid manifest");
delete_recursively(staging_path); delete_recursively(staging_path);
release_staging_lock(staging_path); release_staging_lock(staging_path);
@@ -430,13 +400,24 @@ error_t app_install(const char* source_path) {
auto& registry = install_registry(); auto& registry = install_registry();
mutex_lock(&registry.mutex); mutex_lock(&registry.mutex);
// Replace any previous installation of this package - this also handles the app_manager // Replace any previous install of this app id (mirrors the old install()'s "already
// registrations of its old AppManifests, so there's no separate app_manager_remove() needed // running/present" handling). uninstall_locked() only clears app_install.cpp's own
// here (register_installed_package_locked() below still defends against a stale registration // registry - the same app id may instead be registered by app_manager_install_path_scan()
// per individual id, e.g. one left by app_manager_install_path_scan()'s separate registry). // (manager.cpp's separate registry, scanning this same directory tree), which
uninstall_locked(package.id); // 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);
auto final_path = app_parent_path + "/" + 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;
delete_recursively(final_path); delete_recursively(final_path);
if (rename(staging_path.c_str(), final_path.c_str()) != 0) { if (rename(staging_path.c_str(), final_path.c_str()) != 0) {
@@ -448,8 +429,9 @@ error_t app_install(const char* source_path) {
} }
release_staging_lock(staging_path); release_staging_lock(staging_path);
// app_bindings.size(), not package.app_manifest_count - the safe bound to index by. // Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
error_t add_result = register_installed_package_locked(package, final_path, app_bindings.data(), app_bindings.size()); // already removed any previous registration for this exact id.
error_t add_result = register_installed_app_locked(final_path, metadata);
mutex_unlock(&registry.mutex); mutex_unlock(&registry.mutex);
return add_result; return add_result;
+43 -173
View File
@@ -1,17 +1,13 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#include <app/manager.h> #include <app/manager.h>
#include <app/package_manifest.h> #include <app/metadata.h>
#include <app/private/arguments.h> #include <app/private/arguments.h>
#include <app/private/binary_path.h>
#include <app/private/fd_table.h> #include <app/private/fd_table.h>
#include <app/private/fs.h> #include <app/private/fs.h>
#include <app/private/ledger.h> #include <app/private/ledger.h>
#include <app/private/manager_internal.h> #include <app/private/manager_internal.h>
#include <app/private/package_manifest_parsing.h>
#include <app/private/scheduler.h> #include <app/private/scheduler.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h> #include <tactility/concurrent/mutex.h>
#include <tactility/error.h> #include <tactility/error.h>
#include <tactility/log.h> #include <tactility/log.h>
@@ -76,67 +72,6 @@ void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context)
mutex_unlock(&ledger.mutex); 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) { 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); char** argv = app_arguments_copy(argc, argv_in);
if (argc > 0 && argv == nullptr) { if (argc > 0 && argv == nullptr) {
@@ -265,19 +200,19 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
namespace { namespace {
// Owns the AppManifests (and their backing location-path strings) that app_manager_add() only // Owns the AppManifest (and its id/name/path strings) that app_manager_add() only keeps a
// keeps non-owning pointers to. Separate from app_install.cpp's registry: scanning only // non-owning pointer to. Separate from app_install.cpp's registry: scanning only
// adds/removes registrations, never touches disk or running instances. AppManifest::id/name own // adds/removes registrations, never touches disk or running instances.
// their own storage directly (fixed arrays), so this doesn't need to separately own those. struct ScannedAppManifest {
struct ScannedPackageManifest { std::string id;
std::string path; // scanned directory std::string name;
std::vector<AppManifest> manifests; // one per AppManifest the package declared std::string path;
std::vector<std::string> locations; // backs manifests[i].location.location, same indices AppManifest manifest {};
}; };
struct InstallPathRegistry { struct InstallPathRegistry {
std::vector<std::string> paths; std::vector<std::string> paths;
std::unordered_map<std::string, std::unique_ptr<ScannedPackageManifest>> scanned; std::unordered_map<std::string, std::unique_ptr<ScannedAppManifest>> scanned;
Mutex mutex {}; Mutex mutex {};
InstallPathRegistry() { mutex_construct(&mutex); } InstallPathRegistry() { mutex_construct(&mutex); }
@@ -314,69 +249,50 @@ void app_manager_install_path_scan(void) {
app_fs_list_direct_subdirectories(root, found_app_dirs); app_fs_list_direct_subdirectories(root, found_app_dirs);
} }
// Snapshot once so the rest of the scan doesn't hold registry.mutex. Keeps each known // Snapshot once so the rest of the scan doesn't hold registry.mutex.
// 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); mutex_lock(&registry.mutex);
std::unordered_map<std::string, KnownPackage> known_packages; std::unordered_map<std::string, std::string> known_paths;
for (const auto& [id, record] : registry.scanned) { for (const auto& [id, record] : registry.scanned) {
KnownPackage known { .path = record->path }; known_paths.emplace(id, 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); mutex_unlock(&registry.mutex);
// Parses without registry.mutex held; filesystem IO is slow. // Parses without registry.mutex held; filesystem IO is slow.
std::vector<std::unique_ptr<ScannedPackageManifest>> new_records; std::vector<std::unique_ptr<ScannedAppManifest>> new_records;
std::vector<std::string> new_package_ids;
std::vector<PackageManifest> new_packages;
for (const auto& app_dir : found_app_dirs) { for (const auto& app_dir : found_app_dirs) {
auto manifest_path = app_dir + "/manifest.properties"; auto manifest_path = app_dir + "/manifest.properties";
if (!app_fs_is_file(manifest_path)) { if (!app_fs_is_file(manifest_path)) {
continue; continue;
} }
PackageManifest package {}; AppMetadata metadata {};
// Heap-allocated (not a stack array - too large for a typical app task's stack; this if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
// 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()); LOG_W(TAG, "Invalid manifest at %s", manifest_path.c_str());
continue; continue;
} }
if (known_packages.contains(package.id)) { if (known_paths.contains(metadata.app_id)) {
continue; continue;
} }
auto record = std::make_unique<ScannedPackageManifest>(); auto record = std::make_unique<ScannedAppManifest>();
record->id = metadata.app_id;
record->name = metadata.app_name;
record->path = app_dir; record->path = app_dir;
// Sized once, up front: manifests[i].location.location points into locations[i].c_str(), record->manifest = AppManifest {
// which would dangle if either vector reallocated afterward. .id = record->id.c_str(),
record->manifests.resize(app_bindings.size()); .name = record->name.c_str(),
record->locations.resize(app_bindings.size()); .category = APP_CATEGORY_USER,
for (size_t i = 0; i < app_bindings.size(); i++) { .location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
record->manifests[i] = app_bindings[i].manifest; .flags = 0,
record->locations[i] = app_resolve_binary_path(app_dir, app_bindings[i].binary); .stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
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)); new_records.push_back(std::move(record));
} }
std::vector<std::string> missing_ids; std::vector<std::string> missing_ids;
for (const auto& [id, known] : known_packages) { for (const auto& [id, path] : known_paths) {
if (!app_fs_is_directory(known.path)) { if (!app_fs_is_directory(path)) {
missing_ids.push_back(id); missing_ids.push_back(id);
} }
} }
@@ -385,60 +301,23 @@ void app_manager_install_path_scan(void) {
// registry.mutex would fix a lock order an opposite-order caller could deadlock against. // 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. // registry.mutex is retaken afterward only to publish the in-memory results.
for (const auto& id : missing_ids) { for (const auto& id : missing_ids) {
for (const auto& manifest_id : known_packages.at(id).manifest_ids) { app_manager_remove(id.c_str());
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::unique_ptr<ScannedAppManifest>> added_records;
std::vector<std::string> added_package_ids; for (auto& record : new_records) {
for (size_t r = 0; r < new_records.size(); r++) { if (app_manager_add(&record->manifest) == ERROR_NONE) {
auto& record = new_records[r]; added_records.push_back(std::move(record));
} else {
// known_packages is only a pre-scan snapshot - two new directories can still share an id. LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->id.c_str());
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); mutex_lock(&registry.mutex);
for (const auto& id : missing_ids) { for (const auto& id : missing_ids) {
registry.scanned.erase(id); registry.scanned.erase(id);
} }
for (size_t i = 0; i < added_records.size(); i++) { for (auto& record : added_records) {
registry.scanned[added_package_ids[i]] = std::move(added_records[i]); registry.scanned[record->id] = std::move(record);
} }
mutex_unlock(&registry.mutex); mutex_unlock(&registry.mutex);
} }
@@ -453,10 +332,7 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
return ERROR_NOT_FOUND; return ERROR_NOT_FOUND;
} }
// Pointer, not a copy: the ledger's own AppInstanceRecord::manifest pointers (set by const AppManifest* manifest = &iterator->second->manifest;
// 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; auto path = iterator->second->path;
mutex_unlock(&registry.mutex); mutex_unlock(&registry.mutex);
@@ -466,11 +342,8 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
auto& ledger = app_ledger(); auto& ledger = app_ledger();
mutex_lock(&ledger.mutex); mutex_lock(&ledger.mutex);
for (const auto& [id, record] : ledger.instances) { for (const auto& [id, record] : ledger.instances) {
for (const auto& manifest : *manifests) { if (record.manifest == manifest) {
if (record.manifest == &manifest) { instance_ids.push_back(id);
instance_ids.push_back(id);
break;
}
} }
} }
mutex_unlock(&ledger.mutex); mutex_unlock(&ledger.mutex);
@@ -481,10 +354,7 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
// app_manager_remove() takes ledger.mutex; call outside registry.mutex too, matching // app_manager_remove() takes ledger.mutex; call outside registry.mutex too, matching
// the lock order in app_manager_install_path_scan(). // the lock order in app_manager_install_path_scan().
for (const auto& manifest : *manifests) { app_manager_remove(app_id);
app_manager_remove(manifest.id);
}
app_manager_remove_package(app_id);
// Delete before erasing the scan record, so a failed deletion still leaves the // Delete before erasing the scan record, so a failed deletion still leaves the
// entry discoverable for a retry. // entry discoverable for a retry.
+3 -18
View File
@@ -1,30 +1,15 @@
#include <app/manifest.h> #include <app/manifest.h>
#include <app/private/package_manifest_parsing.h> #include <app/private/metadata_parsing_internal.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
extern "C" { extern "C" {
bool app_manifest_id_is_valid(const char* id) { bool app_id_is_valid(const char* id) {
auto size = strlen(id); auto size = strlen(id);
return size >= 5 && size <= APP_MANIFEST_ID_LENGTH && app_package_manifest_validate_string(id, [](char c) { return size >= 5 && size <= APP_ID_LENGTH && app_metadata_validate_string(id, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || 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;
});
}
} }
@@ -0,0 +1,190 @@
// 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;
}
@@ -0,0 +1,107 @@
// 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;
}
@@ -0,0 +1,149 @@
// 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;
}
+9 -15
View File
@@ -5,7 +5,7 @@
#include <app/io.h> #include <app/io.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/package_manifest.h> #include <app/metadata.h>
#include <app/paths.h> #include <app/paths.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h> #include <app/start.h>
@@ -47,21 +47,20 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_manager_for_each_manifest), DEFINE_MODULE_SYMBOL(app_manager_for_each_manifest),
DEFINE_MODULE_SYMBOL(app_manager_add), DEFINE_MODULE_SYMBOL(app_manager_add),
DEFINE_MODULE_SYMBOL(app_manager_remove), 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_instance_id),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_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_add),
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan), DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall), 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 // app/manifest
DEFINE_MODULE_SYMBOL(app_manifest_id_is_valid), DEFINE_MODULE_SYMBOL(app_id_is_valid),
DEFINE_MODULE_SYMBOL(app_manifest_name_is_valid), // app/metadata
DEFINE_MODULE_SYMBOL(app_manifest_stack_size_is_valid), DEFINE_MODULE_SYMBOL(app_metadata_parse),
// app/package_manifest
DEFINE_MODULE_SYMBOL(app_package_manifest_parse),
// app/paths // app/paths
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory), DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory),
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_path), DEFINE_MODULE_SYMBOL(app_paths_get_user_data_path),
@@ -69,11 +68,6 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_paths_get_assets_path), DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
// app/scheduler // app/scheduler
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id), 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 // app/stream
DEFINE_MODULE_SYMBOL(app_stream_subscribe), DEFINE_MODULE_SYMBOL(app_stream_subscribe),
DEFINE_MODULE_SYMBOL(app_stream_unsubscribe), DEFINE_MODULE_SYMBOL(app_stream_unsubscribe),
@@ -1,201 +0,0 @@
// 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;
}
@@ -1,172 +0,0 @@
// 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;
}
@@ -1,238 +0,0 @@
// 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;
}
+1 -27
View File
@@ -6,27 +6,6 @@
#include <tactility/paths.h> #include <tactility/paths.h>
#include <cstdio> #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" { extern "C" {
@@ -67,12 +46,7 @@ error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_
return ERROR_NOT_FOUND; return ERROR_NOT_FOUND;
} }
std::string install_dir; int written = std::snprintf(out_path, out_path_size, "%s/assets", static_cast<const char*>(manifest.location.location));
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) { if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW; return ERROR_BUFFER_OVERFLOW;
} }
+3 -5
View File
@@ -32,10 +32,8 @@ constexpr size_t APP_INSTANCE_ID_THREAD_SLOT_INDEX = 1;
// Matches TactilityKernel's Thread wrapper's THREAD_PRIORITY_NORMAL. // Matches TactilityKernel's Thread wrapper's THREAD_PRIORITY_NORMAL.
constexpr UBaseType_t APP_TASK_PRIORITY = 4; constexpr UBaseType_t APP_TASK_PRIORITY = 4;
// Used when an app's manifest doesn't request a specific stack depth (0). 12288 bytes' worth. // Used when an app's manifest doesn't request a specific stack depth (0). 8192 bytes' worth.
// Existing window-manager app manifests request at least 2400 words (9600 bytes); use a constexpr size_t APP_DEFAULT_STACK_DEPTH = 8192 / sizeof(StackType_t);
// 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. // 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 }; constexpr MemoryPolicy APP_TASK_TCB_POLICY = { MEMORY_CAPABILITY_INTERNAL, 0, 0 };
@@ -305,7 +303,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
} }
// Same bound package_manifest_parse() enforces on manifest.properties-declared depths - a // Same bound app_metadata_parse() enforces on manifest.properties-declared depths - a
// manifest built directly in C++ (not parsed from a file) must be held to it too. // manifest built directly in C++ (not parsed from a file) must be held to it too.
if (stack.depth > APP_STACK_SIZE_MAX) { 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); LOG_E(TAG, "[instance %lu] stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", app_instance_id, stack.depth, APP_STACK_SIZE_MAX);
@@ -73,11 +73,7 @@ bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_
AppInstanceId start_idle_app(const char* id) { AppInstanceId start_idle_app(const char* id) {
ensure_memory_loader_registered(); ensure_memory_loader_registered();
AppManifest manifest {}; AppManifest manifest { id, id, APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(idle_app_main) } };
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); REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0; AppInstanceId instance_id = 0;
REQUIRE_EQ(app_start(id, 0, nullptr, &instance_id), ERROR_NONE); REQUIRE_EQ(app_start(id, 0, nullptr, &instance_id), ERROR_NONE);
@@ -58,21 +58,19 @@ bool is_executable_file(const std::string& resolved_path) {
#endif #endif
} }
// location.location can be either an app's install directory or the .so file directly, mirroring // location.location can be either an app's install directory or the .so file directly; the
// app_esp32_loader_service.cpp's resolve_elf_path(). A "packaged" app's install directory always // former resolves to the per-architecture binary at {dir}/elf/posix-{TACTILITY_POSIX_ARCH}.so,
// holds its single binary at the fixed path {dir}/bin/posix-{TACTILITY_POSIX_ARCH}/app.so - a // mirroring app_esp32_loader_service.cpp's resolve_elf_path().
// "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) { error_t resolve_app_path(const std::string& path, std::string& resolvedPath) {
if (path.ends_with(".so")) { if (path.ends_with(".so")) {
resolvedPath = path; resolvedPath = path;
return ERROR_NONE; return ERROR_NONE;
} }
std::string candidate = path + "/bin/posix-" TACTILITY_POSIX_ARCH "/app.so"; std::string shared_object_path = path + "/elf/posix-" TACTILITY_POSIX_ARCH ".so";
if (!is_regular_file(candidate)) { if (!is_regular_file(shared_object_path)) {
return ERROR_NOT_FOUND; return ERROR_NOT_FOUND;
} }
resolvedPath = candidate; resolvedPath = shared_object_path;
return ERROR_NONE; return ERROR_NONE;
} }
@@ -96,6 +94,9 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
LOG_I(TAG, "Loading %s", app_path.c_str()); 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); void* handle = dlopen(app_path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (handle == nullptr) { if (handle == nullptr) {
LOG_E(TAG, "dlopen(%s) failed: %s", app_path.c_str(), dlerror()); LOG_E(TAG, "dlopen(%s) failed: %s", app_path.c_str(), dlerror());
@@ -116,8 +117,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[]) { int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
auto* runtime = static_cast<PosixAppRuntime*>(runtime_ptr); auto* runtime = static_cast<PosixAppRuntime*>(runtime_ptr);
// Clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL symbol address apart from a real lookup failure dlerror(); // clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL
dlerror(); // symbol address apart from a real lookup failure
void* symbol = dlsym(runtime->handle, "main"); void* symbol = dlsym(runtime->handle, "main");
const char* lookup_error = dlerror(); const char* lookup_error = dlerror();
if (symbol == nullptr || lookup_error != nullptr) { if (symbol == nullptr || lookup_error != nullptr) {
+1 -14
View File
@@ -15,27 +15,14 @@ set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPEND
set(NON_ELF_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/not-elf.so") set(NON_ELF_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/not-elf.so")
file(WRITE "${NON_ELF_FIXTURE_PATH}" "not an elf file") 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) file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES}) add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture install_dir_fixture) add_dependencies(AppPosixModuleTests app_posix_module_test_fixture)
target_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC}) target_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC})
target_compile_definitions(AppPosixModuleTests PRIVATE target_compile_definitions(AppPosixModuleTests PRIVATE
FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>" FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>"
FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}" FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}"
FIXTURE_INSTALL_DIR_PATH="${INSTALL_DIR_FIXTURE_PATH}"
) )
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests) add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests)
@@ -148,12 +148,6 @@ 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") { TEST_CASE("app_is_executable() rejects an install-directory-shaped path missing its per-arch .so") {
ensure_path_loader_registered(); ensure_path_loader_registered();
// FIXTURE_DIR itself has no bin/posix-<arch>/app.so under it, so resolution fails. // FIXTURE_DIR itself has no elf/posix-<arch>.so under it, so resolution fails.
CHECK_FALSE(is_executable_path(FIXTURE_DIR.c_str())); 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,15 +14,6 @@ enum LvglFontSize {
FONT_SIZE_LARGE, 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); const lv_font_t* lvgl_get_shared_icon_font(void);
uint32_t lvgl_get_shared_icon_font_height(void); uint32_t lvgl_get_shared_icon_font_height(void);
+2 -24
View File
@@ -13,30 +13,8 @@ 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_STATUSBAR_FONT_ICON_SYMBOL;
extern const lv_font_t TT_LVGL_SHARED_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) { uint32_t lvgl_get_text_font_height(enum LvglFontSize font_size) {
switch (resolve_text_font_size(font_size)) { switch (font_size) {
case FONT_SIZE_SMALL: return TT_LVGL_TEXT_FONT_SMALL_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_DEFAULT: return TT_LVGL_TEXT_FONT_DEFAULT_SIZE;
case FONT_SIZE_LARGE: return TT_LVGL_TEXT_FONT_LARGE_SIZE; case FONT_SIZE_LARGE: return TT_LVGL_TEXT_FONT_LARGE_SIZE;
@@ -44,7 +22,7 @@ uint32_t lvgl_get_text_font_height(enum LvglFontSize font_size) {
} }
} }
const lv_font_t* lvgl_get_text_font(enum LvglFontSize font_size) { const lv_font_t* lvgl_get_text_font(enum LvglFontSize font_size) {
switch (resolve_text_font_size(font_size)) { switch (font_size) {
case FONT_SIZE_SMALL: return &TT_LVGL_TEXT_FONT_SMALL_SYMBOL; 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_DEFAULT: return &TT_LVGL_TEXT_FONT_DEFAULT_SYMBOL;
case FONT_SIZE_LARGE: return &TT_LVGL_TEXT_FONT_LARGE_SYMBOL; case FONT_SIZE_LARGE: return &TT_LVGL_TEXT_FONT_LARGE_SYMBOL;
-8
View File
@@ -26,8 +26,6 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lvgl_module), DEFINE_MODULE_SYMBOL(lvgl_module),
DEFINE_MODULE_SYMBOL(lvgl_module_configure), DEFINE_MODULE_SYMBOL(lvgl_module_configure),
// lvgl_fonts // 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),
DEFINE_MODULE_SYMBOL(lvgl_get_shared_icon_font_height), DEFINE_MODULE_SYMBOL(lvgl_get_shared_icon_font_height),
DEFINE_MODULE_SYMBOL(lvgl_get_text_font), DEFINE_MODULE_SYMBOL(lvgl_get_text_font),
@@ -496,12 +494,6 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
// lv_image // lv_image
DEFINE_MODULE_SYMBOL(lv_image_create), DEFINE_MODULE_SYMBOL(lv_image_create),
DEFINE_MODULE_SYMBOL(lv_image_set_src), DEFINE_MODULE_SYMBOL(lv_image_set_src),
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 // lv_anim
DEFINE_MODULE_SYMBOL(lv_anim_init), DEFINE_MODULE_SYMBOL(lv_anim_init),
DEFINE_MODULE_SYMBOL(lv_anim_set_duration), DEFINE_MODULE_SYMBOL(lv_anim_set_duration),
-7
View File
@@ -1,9 +1,5 @@
#pragma once #pragma once
namespace tt::settings::display {
enum class FontSize;
}
namespace tt::lvgl { namespace tt::lvgl {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
@@ -16,9 +12,6 @@ static constexpr auto* PATH_PREFIX = "A:/";
bool isStarted(); bool isStarted();
/** Applies the selected semantic text size to the LVGL theme and future widgets. */
void applyFontSize(settings::display::FontSize fontSize);
void start(); void start();
void stop(); void stop();
@@ -18,20 +18,11 @@ enum class ScreensaverType {
Mystify, Mystify,
MatrixRain, MatrixRain,
StackChan, StackChan,
McpScreen,
Count // Sentinel for bounds checking - must be last Count // Sentinel for bounds checking - must be last
}; };
enum class FontSize {
Small,
Default,
Large,
Count
};
struct DisplaySettings { struct DisplaySettings {
Orientation orientation; Orientation orientation;
FontSize fontSize = FontSize::Default;
uint8_t gammaCurve; uint8_t gammaCurve;
uint8_t backlightDuty; uint8_t backlightDuty;
bool backlightTimeoutEnabled; bool backlightTimeoutEnabled;
@@ -1,14 +0,0 @@
#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
@@ -0,0 +1,9 @@
#pragma once
#include <string>
namespace tt::app::appdetails {
void start(const std::string& appId);
} // namespace
@@ -1,9 +0,0 @@
#pragma once
#include <string>
namespace tt::app::apppackagedetails {
void start(const std::string& packageId);
} // namespace
@@ -1,77 +0,0 @@
#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,7 +63,6 @@ public:
* arbitrary threads while the timer is running. * arbitrary threads while the timer is running.
*/ */
void stopScreensaver(); void stopScreensaver();
void startMcpScreensaver();
/** /**
* Check if the screensaver is currently active. * Check if the screensaver is currently active.
@@ -81,10 +81,6 @@ private:
static error_t handleApiAppsInstall(struct HttpServerRequest* request, void* user_ctx); static error_t handleApiAppsInstall(struct HttpServerRequest* request, void* user_ctx);
static error_t handleApiWifi(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); 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 // Dynamic asset serving
static error_t handleAssets(struct HttpServerRequest* request, void* user_ctx); 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) { std::string getAppInstallPath(const std::string& appId) {
assert(app_manifest_id_is_valid(appId.c_str())); assert(app_id_is_valid(appId.c_str()));
return std::format("{}/{}", getAppInstallPath(), appId); return std::format("{}/{}", getAppInstallPath(), appId);
} }
std::string getAppUserPath(const std::string& appId) { std::string getAppUserPath(const std::string& appId) {
assert(app_manifest_id_is_valid(appId.c_str())); assert(app_id_is_valid(appId.c_str()));
return std::format("{}/app/{}", getUserHomePath(), appId); return std::format("{}/app/{}", getUserHomePath(), appId);
} }
+5 -9
View File
@@ -26,9 +26,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/module.h> #include <app/module.h>
#include <app/start.h>
#include <Tactility/Tactility.h> #include <Tactility/Tactility.h>
@@ -40,7 +40,6 @@
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
#include <Tactility/hal/SdCard.h> #include <Tactility/hal/SdCard.h>
#include <Tactility/lvgl/KeyboardDeviceListener.h> #include <Tactility/lvgl/KeyboardDeviceListener.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/Statusbar.h> #include <Tactility/lvgl/Statusbar.h>
#include <Tactility/lvgl/TrackballInit.h> #include <Tactility/lvgl/TrackballInit.h>
#include <Tactility/lvgl/UsbHidInput.h> #include <Tactility/lvgl/UsbHidInput.h>
@@ -151,9 +150,9 @@ namespace app {
namespace alertdialog { extern const ::AppManifest manifest; } namespace alertdialog { extern const ::AppManifest manifest; }
namespace apphub { extern const ::AppManifest manifest; } namespace apphub { extern const ::AppManifest manifest; }
namespace apphubdetails { extern const ::AppManifest manifest; } namespace apphubdetails { extern const ::AppManifest manifest; }
namespace apppackagedetails { extern const ::AppManifest manifest; } namespace appdetails { extern const ::AppManifest manifest; }
namespace applist { extern const ::AppManifest manifest; } namespace applist { extern const ::AppManifest manifest; }
namespace apppackagelist { extern const ::AppManifest manifest; } namespace appsettings { extern const ::AppManifest manifest; }
namespace audiosettings { extern const ::AppManifest manifest; } namespace audiosettings { extern const ::AppManifest manifest; }
namespace boot { extern const ::AppManifest manifest; } namespace boot { extern const ::AppManifest manifest; }
namespace development { extern const ::AppManifest manifest; } namespace development { extern const ::AppManifest manifest; }
@@ -190,7 +189,6 @@ namespace app {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
namespace apwebserver { extern const ::AppManifest manifest; } namespace apwebserver { extern const ::AppManifest manifest; }
namespace crashdiagnostics { extern const ::AppManifest manifest; } namespace crashdiagnostics { extern const ::AppManifest manifest; }
namespace mcpsettings { extern const ::AppManifest manifest; }
#if CONFIG_TT_TDECK_WORKAROUND == 1 #if CONFIG_TT_TDECK_WORKAROUND == 1
namespace keyboardsettings { extern const ::AppManifest manifest; } // T-Deck only for now namespace keyboardsettings { extern const ::AppManifest manifest; } // T-Deck only for now
#endif #endif
@@ -214,11 +212,11 @@ static void registerInternalApps() {
LOG_I(TAG, "Registering internal apps"); LOG_I(TAG, "Registering internal apps");
app_manager_add(&app::alertdialog::manifest); app_manager_add(&app::alertdialog::manifest);
app_manager_add(&app::apppackagedetails::manifest); app_manager_add(&app::appdetails::manifest);
app_manager_add(&app::apphub::manifest); app_manager_add(&app::apphub::manifest);
app_manager_add(&app::apphubdetails::manifest); app_manager_add(&app::apphubdetails::manifest);
app_manager_add(&app::applist::manifest); app_manager_add(&app::applist::manifest);
app_manager_add(&app::apppackagelist::manifest); app_manager_add(&app::appsettings::manifest);
if (service::audio::isAvailable()) { if (service::audio::isAvailable()) {
app_manager_add(&app::audiosettings::manifest); app_manager_add(&app::audiosettings::manifest);
} }
@@ -254,7 +252,6 @@ static void registerInternalApps() {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
app_manager_add(&app::apwebserver::manifest); app_manager_add(&app::apwebserver::manifest);
app_manager_add(&app::crashdiagnostics::manifest); app_manager_add(&app::crashdiagnostics::manifest);
app_manager_add(&app::mcpsettings::manifest);
#if defined(CONFIG_TT_TDECK_WORKAROUND) #if defined(CONFIG_TT_TDECK_WORKAROUND)
app_manager_add(&app::keyboardsettings::manifest); app_manager_add(&app::keyboardsettings::manifest);
#endif #endif
@@ -436,7 +433,6 @@ static void onLvglStarted() {
if (auto* display = lv_display_get_default(); display != nullptr) { if (auto* display = lv_display_get_default(); display != nullptr) {
auto displaySettings = settings::display::loadOrGetDefault(); auto displaySettings = settings::display::loadOrGetDefault();
lv_display_set_rotation(display, settings::display::toLvglDisplayRotation(displaySettings.orientation)); lv_display_set_rotation(display, settings::display::toLvglDisplayRotation(displaySettings.orientation));
lvgl::applyFontSize(displaySettings.fontSize);
} }
lvgl_unlock(); lvgl_unlock();
@@ -2,9 +2,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -1,10 +1,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/install.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h>
#include <app/start.h> #include <app/start.h>
#include <app/manifest.h>
#include <app/install.h>
#include <app/scheduler.h>
#include <format> #include <format>
@@ -13,15 +12,17 @@
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
#include <Tactility/StringUtils.h>
#include <Tactility/app/alertdialog/AlertDialog.h> #include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/Style.h> #include <Tactility/lvgl/Style.h>
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/log.h> #include <tactility/log.h>
constexpr auto* TAG = "AppPackageDetails"; constexpr auto* TAG = "AppDetails";
namespace tt::app::apppackagedetails { namespace tt::app::appdetails {
extern const ::AppManifest manifest; extern const ::AppManifest manifest;
@@ -29,34 +30,13 @@ namespace {
struct Context { struct Context {
uint32_t appInstanceId; uint32_t appInstanceId;
std::string targetPackageId; std::string targetAppId;
PackageManifest targetPackage = {}; // findAppManifestById() returns the old-model registry's AppManifest type - AppDetails
std::vector<std::string> appIds; // shows details for apps in that registry regardless of which system they run under.
AppManifest targetManifest = { };
uint32_t pendingUninstallDialogId = 0; 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) { void onPressUninstall(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event)); auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
@@ -64,7 +44,7 @@ void onPressUninstall(lv_event_t* event) {
ctx->pendingUninstallDialogId = alertdialog::start( ctx->pendingUninstallDialogId = alertdialog::start(
ctx->appInstanceId, ctx->appInstanceId,
"Confirmation", "Confirmation",
std::format("Uninstall {}?", ctx->targetPackage.id), std::format("Uninstall {}?", ctx->targetManifest.name),
choices choices
); );
} }
@@ -79,7 +59,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto title = std::format("{} details", ctx->targetPackage.id); auto title = std::format("{} details", ctx->targetManifest.name);
auto* toolbar = lvgl_toolbar_create(parent, title.c_str()); auto* toolbar = lvgl_toolbar_create(parent, title.c_str());
// The global toolbar nav callback only knows how to stop old-model apps. // The global toolbar nav callback only knows how to stop old-model apps.
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
@@ -91,39 +71,35 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
lvgl::obj_set_style_bg_invisible(wrapper); lvgl::obj_set_style_bg_invisible(wrapper);
auto identifier = std::format("Identifier: {}", ctx->targetPackage.id); auto identifier = std::format("Identifier: {}", ctx->targetManifest.id);
auto* identifier_label = lv_label_create(wrapper); auto* identifier_label = lv_label_create(wrapper);
lv_label_set_text(identifier_label, identifier.c_str()); lv_label_set_text(identifier_label, identifier.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());
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); auto* location_label = lv_label_create(wrapper);
lv_label_set_text(location_label, location_text.c_str()); std::string location;
bool is_internal = ctx->targetManifest.location.type == APP_LOCATION_MEMORY;
std::string apps; bool is_external = ctx->targetManifest.location.type == APP_LOCATION_PATH;
for (const auto& appId : ctx->appIds) { if (is_internal) {
AppManifest appManifest {}; location = "internal";
const char* label = app_manager_find_manifest(appId.c_str(), &appManifest) == ERROR_NONE ? appManifest.name : appId.c_str(); } else if (is_external) {
apps += apps.empty() ? label : std::format(", {}", label); 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;
} }
auto apps_text = std::format("Apps: {}", apps); std::string location_label_text = std::format("Location: {}", location);
auto* apps_label = lv_label_create(wrapper); lv_label_set_text(location_label, location_label_text.c_str());
lv_label_set_text(apps_label, apps_text.c_str());
auto* uninstall_button = lv_button_create(wrapper); if (is_external) {
lv_obj_set_width(uninstall_button, LV_PCT(100)); auto* uninstall_button = lv_button_create(wrapper);
lv_obj_add_event_cb(uninstall_button, onPressUninstall, LV_EVENT_SHORT_CLICKED, ctx); lv_obj_set_width(uninstall_button, LV_PCT(100));
auto* uninstall_label = lv_label_create(uninstall_button); lv_obj_add_event_cb(uninstall_button, onPressUninstall, LV_EVENT_SHORT_CLICKED, ctx);
lv_obj_align(uninstall_label, LV_ALIGN_CENTER, 0, 0); auto* uninstall_label = lv_label_create(uninstall_button);
lv_label_set_text(uninstall_label, "Uninstall"); lv_obj_align(uninstall_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(uninstall_label, "Uninstall");
}
} }
int32_t appMain(int argc, char* argv[]) { int32_t appMain(int argc, char* argv[]) {
@@ -131,9 +107,9 @@ int32_t appMain(int argc, char* argv[]) {
Context ctx {}; Context ctx {};
ctx.appInstanceId = appInstanceId; ctx.appInstanceId = appInstanceId;
ctx.targetPackageId = (argc > 0) ? argv[0] : std::string(); ctx.targetAppId = (argc > 0) ? argv[0] : std::string();
if (!findPackage(ctx.targetPackageId, ctx.targetPackage, ctx.appIds)) { if (app_manager_find_manifest(ctx.targetAppId.c_str(), &ctx.targetManifest) != ERROR_NONE) {
LOG_W(TAG, "Package %s not found", ctx.targetPackageId.c_str()); LOG_W(TAG, "App %s not found", ctx.targetAppId.c_str());
return 0; return 0;
} }
@@ -158,7 +134,7 @@ int32_t appMain(int argc, char* argv[]) {
case APP_EVENT_RESULT: case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingUninstallDialogId) { if (event.result.launch_id == ctx.pendingUninstallDialogId) {
if (event.result.result == 0) { // 0 = Yes if (event.result.result == 0) { // 0 = Yes
app_uninstall(ctx.targetPackage.id); app_uninstall(ctx.targetManifest.id);
shouldClose = true; shouldClose = true;
} }
app_manager_stop(event.result.launch_id); app_manager_stop(event.result.launch_id);
@@ -180,15 +156,15 @@ int32_t appMain(int argc, char* argv[]) {
} // namespace } // namespace
void start(const std::string& packageId) { void start(const std::string& appId) {
const char* argv[] = { packageId.c_str() }; const char* argv[] = { appId.c_str() };
uint32_t instanceId = 0; uint32_t instanceId = 0;
app_start(manifest.id, 1, argv, &instanceId); app_start(manifest.id, 1, argv, &instanceId);
} }
extern const ::AppManifest manifest = { extern const ::AppManifest manifest = {
.id = "tactility.apppackagedetails", .id = "tactility.appdetails",
.name = "Package Details", .name = "App Details",
.category = APP_CATEGORY_SYSTEM, .category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }, .location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN, .flags = APP_MANIFEST_FLAG_HIDDEN,
+1 -6
View File
@@ -107,9 +107,6 @@ void showNoInternet(Context* ctx) {
} }
void showApps(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 // 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. // lands), which would otherwise reset the user's scroll position each time.
int32_t scrollY; int32_t scrollY;
@@ -269,9 +266,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
void destroyWidgets(void* userData) { void destroyWidgets(void* userData) {
auto* ctx = static_cast<Context*>(userData); auto* ctx = static_cast<Context*>(userData);
if (ctx->contentWrapper != nullptr) { ctx->scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
ctx->scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
}
ctx->contentWrapper = nullptr; ctx->contentWrapper = nullptr;
ctx->refreshButton = nullptr; ctx->refreshButton = nullptr;
} }
@@ -7,11 +7,11 @@
#include <app/event.h> #include <app/event.h>
#include <app/install.h> #include <app/install.h>
#include <app/metadata.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h>
#include <app/start.h> #include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <http/download.h> #include <http/download.h>
@@ -204,9 +204,9 @@ void updateViews(Context* ctx) {
if (is_installed) { if (is_installed) {
std::string metadata_path = std::string(install_path) + "/manifest.properties"; std::string metadata_path = std::string(install_path) + "/manifest.properties";
PackageManifest package; AppMetadata metadata;
if (app_package_manifest_parse(metadata_path.c_str(), &package, nullptr, 0) == ERROR_NONE if (app_metadata_parse(metadata_path.c_str(), &metadata) == ERROR_NONE
&& package.version_code < ctx->entry.appVersionCode) { && metadata.app_version_code < ctx->entry.appVersionCode) {
ctx->updateButton = lvgl_toolbar_add_image_button_action(ctx->toolbar, LV_SYMBOL_DOWNLOAD, onUpdatePressed, ctx); 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); lv_obj_remove_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN);
} }
+2 -2
View File
@@ -1,8 +1,8 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -121,7 +121,7 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM, .category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) }, .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN, .flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 3072, .desired_memory_capability = 0 }, .stack = { .depth = 2400, .desired_memory_capability = 0 },
}; };
} // namespace } // namespace
@@ -1,12 +1,11 @@
#include <lvgl/icons/shared.h> #include <lvgl/icons/shared.h>
#include <lvgl/fonts.h> #include <lvgl/fonts.h>
#include <Tactility/app/apppackagedetails/AppPackageDetails.h> #include <Tactility/app/appdetails/AppDetails.h>
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -19,7 +18,7 @@
#include <cstring> #include <cstring>
#include <vector> #include <vector>
namespace tt::app::apppackagelist { namespace tt::app::appsettings {
extern const ::AppManifest manifest; extern const ::AppManifest manifest;
@@ -27,13 +26,11 @@ namespace {
struct Context { struct Context {
uint32_t appInstanceId; uint32_t appInstanceId;
// Must outlive the widgets - button user-data points into this, not a createWidgets()-local vector.
std::vector<std::string> packageIds;
}; };
void onPackagePressed(lv_event_t* e) { void onAppPressed(lv_event_t* e) {
auto* packageId = static_cast<char*>(lv_event_get_user_data(e)); const auto* target_manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
apppackagedetails::start(packageId); appdetails::start(target_manifest->id);
} }
void onBackPressed(lv_event_t* event) { void onBackPressed(lv_event_t* event) {
@@ -41,16 +38,18 @@ void onBackPressed(lv_event_t* event) {
app_event_emit_close(ctx->appInstanceId); app_event_emit_close(ctx->appInstanceId);
} }
void createPackageWidget(const char* packageId, lv_obj_t* list) { void createAppWidget(const ::AppManifest* target_manifest, lv_obj_t* list) {
lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_TOOLBAR, packageId); // 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);
lv_obj_t* image = lv_obj_get_child(btn, 0); 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_set_style_text_font(image, lvgl_get_shared_icon_font(), LV_PART_MAIN);
lv_obj_add_event_cb(btn, &onPackagePressed, LV_EVENT_SHORT_CLICKED, const_cast<char*>(packageId)); lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, const_cast<::AppManifest*>(target_manifest));
} }
void collectPackageId(const ::AppPackage* pkg, void* context) { void collectManifest(const ::AppManifest* manifest, void* context) {
auto* packageIds = static_cast<std::vector<std::string>*>(context); auto* manifests = static_cast<std::vector<const ::AppManifest*>*>(context);
packageIds->emplace_back(pkg->package.id); manifests->push_back(manifest);
} }
void createWidgets(lv_obj_t* parent, void* userData) { void createWidgets(lv_obj_t* parent, void* userData) {
@@ -69,16 +68,21 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_width(list, LV_PCT(100)); lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1); lv_obj_set_flex_grow(list, 1);
// createWidgets() can rerun for this same Context (window rebuild-on-remove). std::vector<const ::AppManifest*> manifests;
ctx->packageIds.clear(); app_manager_for_each_manifest(collectManifest, &manifests);
app_manager_for_each_package(collectPackageId, &ctx->packageIds); std::ranges::sort(manifests, [](const ::AppManifest* a, const ::AppManifest* b) {
std::ranges::sort(ctx->packageIds); return strcmp(a->name, b->name) < 0;
});
for (const auto& packageId : ctx->packageIds) { size_t app_count = 0;
createPackageWidget(packageId.c_str(), list); for (const auto* target_manifest: manifests) {
if (target_manifest->location.type == APP_LOCATION_PATH) {
app_count++;
createAppWidget(target_manifest, list);
}
} }
if (ctx->packageIds.empty()) { if (app_count == 0) {
// lv_obj_align() is ignored for children of a flex-managed parent, so the empty-state // 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 // 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. // rather than deleted so the wrapper can just take its place in the flex flow.
@@ -135,7 +139,7 @@ int32_t appMain(int argc, char* argv[]) {
} // namespace } // namespace
extern const ::AppManifest manifest = { extern const ::AppManifest manifest = {
.id = "tactility.apppackagelist", .id = "tactility.appsettings",
.name = "Apps", .name = "Apps",
.category = APP_CATEGORY_SETTINGS, .category = APP_CATEGORY_SETTINGS,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) }, .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
+1 -1
View File
@@ -10,9 +10,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -5,9 +5,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -10,9 +10,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -11,8 +11,8 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/manifest.h>
#include <app/start.h> #include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -7,9 +7,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+1 -34
View File
@@ -10,7 +10,6 @@
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include <Tactility/service/displayidle/DisplayIdleService.h> #include <Tactility/service/displayidle/DisplayIdleService.h>
#endif #endif
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/settings/DisplaySettings.h> #include <Tactility/settings/DisplaySettings.h>
#include <app/event.h> #include <app/event.h>
@@ -91,21 +90,6 @@ 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) { void onTimeoutSwitch(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event)); auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event)); auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
@@ -224,23 +208,6 @@ void createWidgets(lv_obj_t* parent, void* userData) {
// Set the dropdown to match current orientation enum // Set the dropdown to match current orientation enum
lv_dropdown_set_selected(orientation_dropdown, static_cast<uint16_t>(ctx->displaySettings.orientation)); 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 // Screen timeout
// Note: DisplayIdleService doesn't act on these settings for kernel-driver displays yet // 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 // (it only looks up the deprecated tt::hal::display::DisplayDevice), so these currently
@@ -307,7 +274,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
ctx->screensaverDropdown = lv_dropdown_create(screensaver_wrapper); ctx->screensaverDropdown = lv_dropdown_create(screensaver_wrapper);
// Note: order correlates with settings::display::ScreensaverType enum order // Note: order correlates with settings::display::ScreensaverType enum order
lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan\nMCP Screen"); lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan");
lv_obj_align(ctx->screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0); 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_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)); lv_dropdown_set_selected(ctx->screensaverDropdown, static_cast<uint16_t>(ctx->displaySettings.screensaverType));
@@ -6,9 +6,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/io.h> #include <app/io.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h> #include <app/stream.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -8,9 +8,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -7,10 +7,10 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/paths.h> #include <app/paths.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -2,9 +2,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h> #include <app/stream.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+171 -255
View File
@@ -1,28 +1,14 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.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 <cstring>
#include <ctime>
#include <string>
#include <lvgl.h> #include <lvgl.h>
#include <lvgl/fonts.h>
#include <lvgl/icons/launcher.h> #include <lvgl/icons/launcher.h>
#include <lvgl/icons/statusbar.h> #include <lvgl/fonts.h>
#include <lvgl/lvgl.h> #include <lvgl/lvgl.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -34,29 +20,64 @@
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/memory.h> #include <tactility/memory.h>
#include <Tactility/app/setup/Setup.h>
#include <Tactility/settings/BootSettings.h>
#include <Tactility/Tactility.h>
namespace tt::app::launcher { namespace tt::app::launcher {
constexpr auto* TAG = "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 { namespace {
struct LauncherWidgets { uint32_t getButtonPadding(UiDensity density, uint32_t buttonSize) {
lv_obj_t* timeLabel = nullptr; if (density == LVGL_UI_DENSITY_COMPACT) {
lv_obj_t* dateLabel = nullptr; return 0;
lv_obj_t* dataLabel = nullptr; } else {
lv_obj_t* statusLabel = nullptr; return buttonSize / 8;
lv_timer_t* updateTimer = nullptr; }
}; }
void onAppPressed(lv_event_t* event) { int32_t computeButtonMargin(int32_t available_span, int32_t total_button_size) {
const auto* app_id = static_cast<const char*>(lv_event_get_user_data(event)); 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));
uint32_t instance_id = 0; uint32_t instance_id = 0;
app_start(app_id, 0, nullptr, &instance_id); 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;
} }
bool shouldShowPowerButton() { bool shouldShowPowerButton() {
@@ -64,288 +85,180 @@ bool shouldShowPowerButton() {
device_for_each_of_type(&POWER_SUPPLY_TYPE, &show_power_button, [](Device* device, void* context) { 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)) { if (device_is_ready(device) && power_supply_supports_power_off(device)) {
*static_cast<bool*>(context) = true; *static_cast<bool*>(context) = true;
return false; return false; // stop iterating
} else {
return true; // continue iterating
} }
return true;
}); });
return show_power_button; return show_power_button;
} }
int getBatteryPercentage() { void onButtonsWrapperResized(lv_event_t* e);
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;
PowerSupplyPropertyValue charge_level; // The screen object outlives this window's own widgets (lvgl-window-manager deletes and
if (power_supply_get_property(power, POWER_SUPPLY_PROP_CAPACITY, &charge_level) != ERROR_NONE) return -1; // recreates only the topmost window's widget on every app switch, not the screen itself), so
return std::clamp(charge_level.int_value, 0, 100); // 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);
} }
const char* getWifiStatusIcon(service::wifi::RadioState state) { // Re-applies the flex direction and per-button margins when the display orientation changes
using enum service::wifi::RadioState; // while the launcher is the visible window (these are decided once at createWidgets() based on
switch (state) { // the resolution at that time, so a later rotation needs this to catch up).
case ConnectionActive: return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_4_BAR; void onButtonsWrapperResized(lv_event_t* e) {
case Off: auto* buttons_wrapper = static_cast<lv_obj_t*>(lv_event_get_user_data(e));
case OffPending: return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_OFF; const auto* display = lv_obj_get_display(buttons_wrapper);
default: return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_0_BAR;
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* getBatteryStatusIcon(int percentage) { void createWidgets(lv_obj_t* parent, void*) {
if (percentage < 0) return ""; auto* buttons_wrapper = lv_obj_create(parent);
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;
}
lv_obj_t* createAppButton(lv_obj_t* parent, const char* icon, const char* app_id, bool emphasized) { auto ui_density = lvgl_get_ui_density();
auto* button = lv_button_create(parent); const auto button_size = lvgl_get_launcher_icon_font_height();
lv_obj_set_size(button, 52, 52); const auto button_padding = getButtonPadding(ui_density, button_size);
lv_obj_set_style_radius(button, 15, LV_PART_MAIN); const auto total_button_size = button_size + (button_padding * 2);
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);
auto* image = lv_image_create(button); lv_obj_align(buttons_wrapper, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_size(image, 36, 36); lv_obj_set_size(buttons_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_center(image); lv_obj_set_style_border_width(buttons_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_text_font(image, lvgl_get_launcher_icon_font(), LV_STATE_DEFAULT); lv_obj_set_flex_grow(buttons_wrapper, 1);
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;
}
void updateInformation(LauncherWidgets& widgets) { // Fix for button selection
const std::time_t now = std::time(nullptr); lv_obj_set_style_pad_all(buttons_wrapper, 6, LV_STATE_DEFAULT);
std::tm local_time {};
localtime_r(&now, &local_time); const auto* display = lv_obj_get_display(parent);
char time_buffer[12]; const auto horizontal_px = lv_display_get_horizontal_resolution(display);
char date_buffer[40]; const auto vertical_px = lv_display_get_vertical_resolution(display);
if (local_time.tm_year >= 125) { const bool is_landscape_display = horizontal_px >= vertical_px;
if (settings::isTimeFormat24Hour()) { if (is_landscape_display) {
std::strftime(time_buffer, sizeof(time_buffer), "%H:%M", &local_time); lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_ROW);
} 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 { } else {
std::strcpy(time_buffer, "--:--"); lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_COLUMN);
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());
}
}
} }
auto* background = lv_image_create(parent); const int32_t margin = is_landscape_display
lv_image_set_src(background, background_path.c_str()); ? computeButtonMargin(lv_display_get_horizontal_resolution(display), total_button_size)
lv_obj_align(background, LV_ALIGN_CENTER, 0, 0); : computeButtonMargin(lv_display_get_vertical_resolution(display), total_button_size);
lv_obj_add_flag(background, LV_OBJ_FLAG_IGNORE_LAYOUT);
widgets.timeLabel = lv_label_create(parent); auto* app_list_button = createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_APPS, "tactility.applist", margin, is_landscape_display);
lv_label_set_text(widgets.timeLabel, "--:--"); createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files", margin, is_landscape_display);
lv_obj_set_style_text_color(widgets.timeLabel, TEXT_COLOR, LV_PART_MAIN); createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings", margin, is_landscape_display);
#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);
widgets.dateLabel = lv_label_create(parent); // The launcher's container is several levels below the screen, and LVGL only sends
lv_obj_set_style_text_font(widgets.dateLabel, lvgl_get_text_font(FONT_SIZE_LARGE), LV_PART_MAIN); // LV_EVENT_SIZE_CHANGED to the screen object itself on a resolution change - so the
lv_obj_set_style_text_color(widgets.dateLabel, TEXT_COLOR, LV_PART_MAIN); // handler is attached there, with buttons_wrapper passed through as user data.
lv_obj_align(widgets.dateLabel, LV_ALIGN_TOP_LEFT, 20, 118); 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.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()) { if (shouldShowPowerButton()) {
auto* power_button = lv_button_create(parent); auto* power_button = lv_button_create(parent);
lv_obj_set_size(power_button, 36, 36); lv_obj_set_style_pad_all(power_button, 8, 0);
lv_obj_align(power_button, LV_ALIGN_BOTTOM_LEFT, 16, -12); lv_obj_align(power_button, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_set_style_radius(power_button, LV_RADIUS_CIRCLE, LV_PART_MAIN); 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_PART_MAIN); lv_obj_set_style_shadow_width(power_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(power_button, lv_color_hex(0x211B20), LV_PART_MAIN); lv_obj_set_style_bg_opa(power_button, 0, 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); auto* power_label = lv_label_create(power_button);
lv_label_set_text(power_label, LV_SYMBOL_POWER); lv_label_set_text(power_label, LV_SYMBOL_POWER);
lv_obj_set_style_text_color(power_label, TEXT_COLOR, LV_PART_MAIN); lv_obj_set_style_text_color(power_label, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
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)) { 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_group_focus_obj(app_list_button);
lv_obj_add_state(app_list_button, LV_STATE_FOCUS_KEY); 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() { void runAutoStart() {
settings::BootSettings boot_properties; settings::BootSettings boot_properties;
AppManifest manifest; AppManifest manifest;
if (strcmp(CONFIG_TT_AUTO_START_APP_ID, "") != 0 && if (
app_manager_find_manifest(CONFIG_TT_AUTO_START_APP_ID, &manifest) == ERROR_NONE) { // 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
) {
LOG_I(TAG, "Starting %s", CONFIG_TT_AUTO_START_APP_ID); LOG_I(TAG, "Starting %s", CONFIG_TT_AUTO_START_APP_ID);
uint32_t app_launch_id; uint32_t app_launch_id;
app_start(CONFIG_TT_AUTO_START_APP_ID, 0, nullptr, &app_launch_id); app_start(CONFIG_TT_AUTO_START_APP_ID, 0, nullptr, &app_launch_id);
} else if (settings::loadBootSettings(boot_properties) && } else if (
!boot_properties.autoStartAppId.empty() && // Auto-start due to user configuration
app_manager_find_manifest(boot_properties.autoStartAppId.c_str(), &manifest) == ERROR_NONE) { 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()); LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
uint32_t app_launch_id; uint32_t app_launch_id;
app_start(boot_properties.autoStartAppId.c_str(), 0, nullptr, &app_launch_id); app_start(boot_properties.autoStartAppId.c_str(), 0, nullptr, &app_launch_id);
} else if (!setup::isCompleted()) { } else {
setup::start(); // No auto-start, consider running system setup
if (!setup::isCompleted()) {
setup::start();
}
} }
} }
int32_t appMain(int argc, char* argv[]) { int32_t appMain(int argc, char* argv[]) {
uint32_t app_instance_id = app_scheduler_current_app_id(); uint32_t appInstanceId = app_scheduler_current_app_id();
runAutoStart(); runAutoStart();
LauncherWidgets widgets;
WindowId window = window_manager_create_ext(app_instance_id, createWidgets, destroyWidgets, &widgets);
TaskEventGroup event_group {}; TaskEventGroup event_group {};
task_event_group_construct(&event_group); task_event_group_construct(&event_group);
AppEventSubscription sub {}; AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); 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) { while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool should_close = false;
bool shouldClose = false;
AppEvent event {}; AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) { while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) { if (event.type == APP_EVENT_CLOSE) {
should_close = true; shouldClose = true;
break; break;
} }
} }
if (should_close) break; if (shouldClose) break;
} }
window_manager_remove(window); window_manager_remove(window);
@@ -362,13 +275,16 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM, .category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) }, .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN, .flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 3072, .desired_memory_capability = MEMORY_CAPABILITY_EXTERNAL } // No file IO, so callstack can be in external RAM
.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 start() {
uint32_t instance_id = 0; uint32_t instance_id = 0;
app_start(manifest.id, 0, nullptr, &instance_id); app_start(manifest.id, 0, nullptr, &instance_id);
return instance_id; return instance_id;
} }
} // namespace tt::app::launcher } // namespace
@@ -1,98 +0,0 @@
#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/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h> #include <app/stream.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -2,9 +2,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+2 -2
View File
@@ -1,8 +1,8 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -118,7 +118,7 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM, .category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) }, .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN, .flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 3072, .desired_memory_capability = 0 }, .stack = { .depth = 2400, .desired_memory_capability = 0 },
}; };
} // namespace } // namespace
+1 -1
View File
@@ -10,9 +10,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -5,9 +5,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -8,9 +8,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -5,9 +5,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
-52
View File
@@ -1,52 +0,0 @@
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/settings/DisplaySettings.h>
#include <lvgl/fonts.h>
#include <lvgl/lvgl.h>
namespace tt::lvgl {
void applyFontSize(settings::display::FontSize fontSize) {
LvglFontScale scale;
switch (fontSize) {
using enum settings::display::FontSize;
case Small:
scale = FONT_SCALE_SMALL;
break;
case Large:
scale = FONT_SCALE_LARGE;
break;
case Default:
default:
scale = FONT_SCALE_DEFAULT;
break;
}
lvgl_set_text_font_scale(scale);
const lv_font_t* font = lvgl_get_text_font(FONT_SIZE_DEFAULT);
for (lv_display_t* display = lv_display_get_next(nullptr);
display != nullptr;
display = lv_display_get_next(display)) {
#if LV_USE_THEME_DEFAULT
if (lv_display_get_theme(display) == lv_theme_default_get()) {
lv_obj_t* screen = lv_display_get_screen_active(display);
lv_theme_default_init(
display,
lv_theme_get_color_primary(screen),
lv_theme_get_color_secondary(screen),
LV_THEME_DEFAULT_DARK,
font
);
}
#endif
// The display layers are independent inheritance roots. Updating all of them makes
// the new size visible immediately in regular screens, overlays, and the status bar.
lv_obj_set_style_text_font(lv_display_get_screen_active(display), font, LV_PART_MAIN);
lv_obj_set_style_text_font(lv_display_get_layer_top(display), font, LV_PART_MAIN);
lv_obj_set_style_text_font(lv_display_get_layer_sys(display), font, LV_PART_MAIN);
lv_obj_set_style_text_font(lv_display_get_layer_bottom(display), font, LV_PART_MAIN);
}
}
} // namespace tt::lvgl
File diff suppressed because it is too large Load Diff
@@ -190,7 +190,7 @@ error_t DevelopmentService::handleAppInstall(HttpServerRequest* request, void*)
if ( if (
name_entry == content_disposition_map.end() || name_entry == content_disposition_map.end() ||
filename_entry == content_disposition_map.end() || filename_entry == content_disposition_map.end() ||
name_entry->second != "app" name_entry->second != "elf"
) { ) {
http_server_request_send_error(request, 400, "Multipart form error: name or filename parameter missing or mismatching"); http_server_request_send_error(request, 400, "Multipart form error: name or filename parameter missing or mismatching");
return ERROR_UNDEFINED; return ERROR_UNDEFINED;
@@ -3,12 +3,10 @@
#include <Tactility/service/displayidle/DisplayIdleService.h> #include <Tactility/service/displayidle/DisplayIdleService.h>
#include <Tactility/service/ServiceManifest.h> #include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h> #include <Tactility/service/ServiceRegistration.h>
#include <Tactility/mcp/McpSystem.h>
#include "BouncingBallsScreensaver.h" #include "BouncingBallsScreensaver.h"
#include "MatrixRainScreensaver.h" #include "MatrixRainScreensaver.h"
#include "MystifyScreensaver.h" #include "MystifyScreensaver.h"
#include "McpScreensaver.h"
#include "Screensaver.h" #include "Screensaver.h"
#include "StackChanScreensaver.h" #include "StackChanScreensaver.h"
@@ -129,9 +127,6 @@ void DisplayIdleService::activateScreensaver() {
case settings::display::ScreensaverType::StackChan: case settings::display::ScreensaverType::StackChan:
screensaver = std::make_unique<StackChanScreensaver>(); screensaver = std::make_unique<StackChanScreensaver>();
break; break;
case settings::display::ScreensaverType::McpScreen:
screensaver = std::make_unique<McpScreensaver>();
break;
case settings::display::ScreensaverType::None: case settings::display::ScreensaverType::None:
default: default:
// Just black screen, no animated screensaver // Just black screen, no animated screensaver
@@ -278,49 +273,6 @@ bool DisplayIdleService::isScreensaverActive() const {
return screensaverOverlay != nullptr; return screensaverOverlay != nullptr;
} }
void DisplayIdleService::startMcpScreensaver() {
if (!lvgl_try_lock(200)) {
LOG_W(TAG, "startMcpScreensaver: failed to acquire LVGL lock");
return;
}
if (screensaverOverlay != nullptr) {
const auto& mcpState = mcp::getState();
if (mcpState.drawArea != nullptr) {
lvgl_unlock();
return;
}
if (screensaver) {
screensaver->stop();
screensaver.reset();
}
lv_obj_delete(screensaverOverlay);
screensaverOverlay = nullptr;
}
screensaverActiveCounter = 0;
backlightOff = false;
setBacklightBrightness(cachedDisplaySettings.backlightDuty == 0
? 255 : cachedDisplaySettings.backlightDuty);
lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr);
lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
screensaverOverlay = lv_obj_create(lv_layer_top());
lv_obj_remove_style_all(screensaverOverlay);
lv_obj_set_size(screensaverOverlay, LV_PCT(100), LV_PCT(100));
lv_obj_set_pos(screensaverOverlay, 0, 0);
lv_obj_set_style_bg_color(screensaverOverlay, lv_color_black(), 0);
lv_obj_set_style_bg_opa(screensaverOverlay, LV_OPA_COVER, 0);
lv_obj_add_flag(screensaverOverlay, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(screensaverOverlay, stopScreensaverCb, LV_EVENT_CLICKED, this);
screensaver = std::make_unique<McpScreensaver>();
screensaver->start(screensaverOverlay, screenW, screenH);
lvgl_unlock();
displayDimmed = true;
LOG_I(TAG, "MCP screensaver activated");
}
void DisplayIdleService::reloadSettings() { void DisplayIdleService::reloadSettings() {
// Set flag for thread-safe reload - actual reload happens in tick() // Set flag for thread-safe reload - actual reload happens in tick()
settingsReloadRequested.store(true, std::memory_order_release); settingsReloadRequested.store(true, std::memory_order_release);
@@ -1,100 +0,0 @@
#ifdef ESP_PLATFORM
#include "McpScreensaver.h"
#include <Tactility/mcp/McpSystem.h>
#include <tactility/log.h>
constexpr auto* TAG = "McpScreensaver";
#include <esp_heap_caps.h>
namespace tt::service::displayidle {
void McpScreensaver::start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) {
auto& state = mcp::getState();
// Full-screen canvas on the overlay
lv_obj_t* canvas = lv_canvas_create(overlay);
lv_obj_set_size(canvas, screenW, screenH);
lv_obj_set_pos(canvas, 0, 0);
lv_obj_set_style_radius(canvas, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(canvas, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(canvas, 0, LV_PART_MAIN);
lv_obj_remove_flag(canvas, LV_OBJ_FLAG_SCROLLABLE);
// Allocate framebuffer (prefer SPIRAM)
size_t requiredSize = (size_t)screenW * screenH * sizeof(uint16_t);
framebuffer = (uint16_t*)heap_caps_malloc(requiredSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (framebuffer == nullptr) {
framebuffer = (uint16_t*)heap_caps_malloc(requiredSize, MALLOC_CAP_8BIT);
}
framebufferSize = (framebuffer != nullptr) ? requiredSize : 0;
if (framebuffer == nullptr) {
LOG_E(TAG, "Failed to allocate %uB framebuffer", (unsigned)requiredSize);
lv_obj_t* err = lv_label_create(canvas);
lv_label_set_text(err, "Framebuffer alloc failed");
lv_obj_center(err);
return;
}
// Fill with a dark slate background (inverted for display path)
size_t pixelCount = (size_t)screenW * screenH;
for (size_t i = 0; i < pixelCount; ++i) {
framebuffer[i] = ~0x18E3; // dark blue-grey
}
lv_canvas_set_buffer(canvas, framebuffer, screenW, screenH, LV_COLOR_FORMAT_RGB565);
// Waiting label (removed on first MCP draw via lv_obj_clean)
lv_obj_t* waitLabel = lv_label_create(canvas);
lv_label_set_text(waitLabel, "Waiting for LLM...");
lv_obj_set_style_text_color(waitLabel, lv_color_black(), LV_PART_MAIN); // white on screen (inverted)
lv_obj_align(waitLabel, LV_ALIGN_CENTER, 0, -20);
lv_obj_t* resLabel = lv_label_create(canvas);
lv_label_set_text_fmt(resLabel, "Display: %dx%d", (int)screenW, (int)screenH);
lv_color_t resColor = lv_palette_lighten(LV_PALETTE_BLUE, 3);
lv_obj_set_style_text_color(resLabel, lv_color_make(~resColor.red, ~resColor.green, ~resColor.blue), LV_PART_MAIN);
lv_obj_align(resLabel, LV_ALIGN_CENTER, 0, 10);
// Register with McpSystemState
std::lock_guard<std::mutex> lock(state.mutex);
state.drawArea = canvas;
state.framebuffer = framebuffer;
state.framebufferSize = framebufferSize;
state.displayWidth = (uint16_t)screenW;
state.displayHeight = (uint16_t)screenH;
state.drawWidth = (uint16_t)screenW;
state.drawHeight = (uint16_t)screenH;
// Don't reset overrideActive — if the LLM already drew, we keep the content
LOG_I(TAG, "McpScreensaver started (%dx%d)", (int)screenW, (int)screenH);
}
void McpScreensaver::stop() {
auto& state = mcp::getState();
{
std::lock_guard<std::mutex> lock(state.mutex);
state.drawArea = nullptr;
state.framebuffer = nullptr;
state.framebufferSize = 0;
state.overrideActive = false;
}
if (framebuffer != nullptr) {
heap_caps_free(framebuffer);
framebuffer = nullptr;
framebufferSize = 0;
}
LOG_I(TAG, "McpScreensaver stopped");
}
void McpScreensaver::update(lv_coord_t /*screenW*/, lv_coord_t /*screenH*/) {
// MCP draws on demand via HTTP — no per-frame animation needed
}
} // namespace tt::service::displayidle
#endif // ESP_PLATFORM
@@ -1,30 +0,0 @@
#pragma once
#ifdef ESP_PLATFORM
#include "Screensaver.h"
#include <cstdint>
namespace tt::service::displayidle {
/**
* MCP Screen screensaver.
* Creates a full-screen LVGL canvas on the overlay and registers it in
* McpSystemState so that MCP HTTP draw commands can paint to it.
* Dismissed by a touch event (handled by the parent DisplayIdle overlay).
*/
class McpScreensaver final : public Screensaver {
uint16_t* framebuffer = nullptr;
size_t framebufferSize = 0;
public:
McpScreensaver() = default;
~McpScreensaver() override = default;
void start(lv_obj_t* overlay, lv_coord_t screenW, lv_coord_t screenH) override;
void stop() override;
void update(lv_coord_t screenW, lv_coord_t screenH) override;
};
} // namespace tt::service::displayidle
#endif // ESP_PLATFORM
File diff suppressed because it is too large Load Diff
@@ -32,8 +32,6 @@
#include "app/manager.h" #include "app/manager.h"
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include <Tactility/mcp/McpSystem.h>
#include <Tactility/settings/McpSettings.h>
#include <esp_chip_info.h> #include <esp_chip_info.h>
#include <esp_flash.h> #include <esp_flash.h>
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
@@ -215,7 +213,7 @@ bool WebServerService::onStart(ServiceContext& service) {
lock.lock(); lock.lock();
g_cachedSettings = settings::webserver::loadOrGetDefault(); g_cachedSettings = settings::webserver::loadOrGetDefault();
g_settingsCached = true; g_settingsCached = true;
serverEnabled = g_cachedSettings.webServerEnabled || settings::mcp::loadOrGetDefault().mcpEnabled; serverEnabled = g_cachedSettings.webServerEnabled;
} }
// Subscribe to settings change events to refresh cache // Subscribe to settings change events to refresh cache
settingsEventSubscription = pubsub->subscribe([](WebServerEvent event) { settingsEventSubscription = pubsub->subscribe([](WebServerEvent event) {
@@ -224,10 +222,6 @@ bool WebServerService::onStart(ServiceContext& service) {
lock.lock(); lock.lock();
g_cachedSettings = settings::webserver::loadOrGetDefault(); g_cachedSettings = settings::webserver::loadOrGetDefault();
g_settingsCached = true; g_settingsCached = true;
const bool enabled = g_cachedSettings.webServerEnabled || settings::mcp::loadOrGetDefault().mcpEnabled;
if (g_webServerInstance.load() != nullptr) {
g_webServerInstance.load()->setEnabled(enabled);
}
} }
}); });
@@ -487,21 +481,6 @@ bool WebServerService::startServer() {
.callback = handleAdminPost, .callback = handleAdminPost,
.user_ctx = ctx .user_ctx = ctx
}, },
#ifdef ESP_PLATFORM
// MCP is LAN-local and is enabled whenever the web server is enabled.
{
.uri = "/api/mcp",
.method = HTTP_METHOD_POST,
.callback = handleApiMcp,
.user_ctx = ctx
},
{
.uri = "/api/screen/raw",
.method = HTTP_METHOD_POST,
.callback = handleApiScreenRaw,
.user_ctx = ctx
},
#endif
// API endpoints for system info, apps, wifi, etc // API endpoints for system info, apps, wifi, etc
{ {
.uri = "/api/*", .uri = "/api/*",
@@ -549,12 +528,6 @@ bool WebServerService::startServer() {
LOG_I(TAG, "HTTP server started successfully on port %u", (unsigned)settings.webServerPort); LOG_I(TAG, "HTTP server started successfully on port %u", (unsigned)settings.webServerPort);
publish_event(this, WebServerEvent::WebServerStarted); publish_event(this, WebServerEvent::WebServerStarted);
#ifdef ESP_PLATFORM
if (settings::mcp::loadOrGetDefault().mcpEnabled) {
mcp::startVideoStreamServer();
}
#endif
// Show statusbar icon // Show statusbar icon
if (statusbarIconId >= 0) { if (statusbarIconId >= 0) {
lvgl::statusbar_icon_set_image(statusbarIconId, LVGL_ICON_STATUSBAR_CLOUD); lvgl::statusbar_icon_set_image(statusbarIconId, LVGL_ICON_STATUSBAR_CLOUD);
@@ -571,9 +544,6 @@ void WebServerService::stopServer() {
return; return;
} }
#ifdef ESP_PLATFORM
mcp::stopVideoStreamServer();
#endif
http_server_free(httpServer); http_server_free(httpServer);
httpServer = nullptr; httpServer = nullptr;
@@ -20,7 +20,6 @@ static std::string getSettingsFilePath() {
} }
constexpr auto* SETTINGS_KEY_ORIENTATION = "orientation"; constexpr auto* SETTINGS_KEY_ORIENTATION = "orientation";
constexpr auto* SETTINGS_KEY_FONT_SIZE = "fontSize";
constexpr auto* SETTINGS_KEY_GAMMA_CURVE = "gammaCurve"; constexpr auto* SETTINGS_KEY_GAMMA_CURVE = "gammaCurve";
constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty"; constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty";
constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled"; constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled";
@@ -74,34 +73,6 @@ static bool fromString(const std::string& str, Orientation& orientation) {
} }
} }
static std::string toString(FontSize font_size) {
switch (font_size) {
using enum FontSize;
case Small:
return "Small";
case Default:
return "Default";
case Large:
return "Large";
default:
std::unreachable();
}
}
static bool fromString(const std::string& str, FontSize& font_size) {
if (str == "Small") {
font_size = FontSize::Small;
return true;
} else if (str == "Default") {
font_size = FontSize::Default;
return true;
} else if (str == "Large") {
font_size = FontSize::Large;
return true;
}
return false;
}
static std::string toString(ScreensaverType type) { static std::string toString(ScreensaverType type) {
switch (type) { switch (type) {
using enum ScreensaverType; using enum ScreensaverType;
@@ -115,8 +86,6 @@ static std::string toString(ScreensaverType type) {
return "MatrixRain"; return "MatrixRain";
case StackChan: case StackChan:
return "StackChan"; return "StackChan";
case McpScreen:
return "McpScreen";
default: default:
std::unreachable(); std::unreachable();
} }
@@ -138,9 +107,6 @@ static bool fromString(const std::string& str, ScreensaverType& type) {
} else if (str == "StackChan") { } else if (str == "StackChan") {
type = ScreensaverType::StackChan; type = ScreensaverType::StackChan;
return true; return true;
} else if (str == "McpScreen") {
type = ScreensaverType::McpScreen;
return true;
} else { } else {
return false; return false;
} }
@@ -163,12 +129,6 @@ bool load(DisplaySettings& settings) {
orientation = getDefaultOrientation(); orientation = getDefaultOrientation();
} }
auto font_size_entry = map.find(SETTINGS_KEY_FONT_SIZE);
FontSize font_size = FontSize::Default;
if (font_size_entry != map.end()) {
fromString(font_size_entry->second, font_size);
}
auto gamma_entry = map.find(SETTINGS_KEY_GAMMA_CURVE); auto gamma_entry = map.find(SETTINGS_KEY_GAMMA_CURVE);
int gamma_curve = 0; int gamma_curve = 0;
if (gamma_entry != map.end()) { if (gamma_entry != map.end()) {
@@ -203,7 +163,6 @@ bool load(DisplaySettings& settings) {
} }
settings.orientation = orientation; settings.orientation = orientation;
settings.fontSize = font_size;
settings.gammaCurve = gamma_curve; settings.gammaCurve = gamma_curve;
settings.backlightDuty = backlight_duty; settings.backlightDuty = backlight_duty;
settings.backlightTimeoutEnabled = timeout_enabled; settings.backlightTimeoutEnabled = timeout_enabled;
@@ -216,7 +175,6 @@ bool load(DisplaySettings& settings) {
DisplaySettings getDefault() { DisplaySettings getDefault() {
return DisplaySettings { return DisplaySettings {
.orientation = getDefaultOrientation(), .orientation = getDefaultOrientation(),
.fontSize = FontSize::Default,
.gammaCurve = 1, .gammaCurve = 1,
.backlightDuty = 200, .backlightDuty = 200,
.backlightTimeoutEnabled = false, .backlightTimeoutEnabled = false,
@@ -238,7 +196,6 @@ bool save(const DisplaySettings& settings) {
map[SETTINGS_KEY_BACKLIGHT_DUTY] = std::to_string(settings.backlightDuty); map[SETTINGS_KEY_BACKLIGHT_DUTY] = std::to_string(settings.backlightDuty);
map[SETTINGS_KEY_GAMMA_CURVE] = std::to_string(settings.gammaCurve); map[SETTINGS_KEY_GAMMA_CURVE] = std::to_string(settings.gammaCurve);
map[SETTINGS_KEY_ORIENTATION] = toString(settings.orientation); map[SETTINGS_KEY_ORIENTATION] = toString(settings.orientation);
map[SETTINGS_KEY_FONT_SIZE] = toString(settings.fontSize);
map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0"; map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0";
map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs); map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs);
map[SETTINGS_KEY_SCREENSAVER_TYPE] = toString(settings.screensaverType); map[SETTINGS_KEY_SCREENSAVER_TYPE] = toString(settings.screensaverType);
-76
View File
@@ -1,76 +0,0 @@
#include <Tactility/settings/McpSettings.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/file/File.h>
#include <tactility/log.h>
#include <app/paths.h>
constexpr auto* TAG = "McpSettings";
#include <map>
#include <string>
namespace tt::settings::mcp {
static std::string getSettingsFilePath() {
char path[256];
if (app_paths_get_user_data_path("tactility.mcpsettings", "mcp.properties", path, sizeof(path)) != ERROR_NONE) {
return "";
}
return path;
}
constexpr auto* KEY_MCP_ENABLED = "mcpEnabled";
bool load(McpSettings& settings) {
auto settings_path = getSettingsFilePath();
if (!file::isFile(settings_path)) {
return false;
}
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(settings_path, map)) {
return false;
}
auto mcp_enabled = map.find(KEY_MCP_ENABLED);
settings.mcpEnabled = (mcp_enabled != map.end())
? (mcp_enabled->second == "1" || mcp_enabled->second == "true")
: false;
return true;
}
McpSettings getDefault() {
return McpSettings{
.mcpEnabled = false
};
}
McpSettings loadOrGetDefault() {
McpSettings settings;
if (!load(settings)) {
settings = getDefault();
if (!save(settings)) {
LOG_W(TAG, "Failed to save default MCP settings");
}
}
return settings;
}
bool save(const McpSettings& settings) {
std::map<std::string, std::string> map;
map[KEY_MCP_ENABLED] = settings.mcpEnabled ? "true" : "false";
auto settings_path = getSettingsFilePath();
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
LOG_E(TAG, "Failed to create parent dir for %s", settings_path.c_str());
return false;
}
if (!file::savePropertiesFile(settings_path, map)) {
LOG_E(TAG, "Failed to save MCP settings to %s", settings_path.c_str());
return false;
}
return true;
}
} // namespace
+2 -2
View File
@@ -81,8 +81,8 @@ dependencies:
espressif/esp_lvgl_port: "2.7.2" espressif/esp_lvgl_port: "2.7.2"
lvgl/lvgl: "9.3.0" lvgl/lvgl: "9.3.0"
epdiy: epdiy:
git: https://github.com/vroland/epdiy.git git: https://github.com/Shadowtrance/epdiy.git
version: 2.1.3 version: 2.0.1
rules: rules:
# More hardware might be supported - enable as needed # More hardware might be supported - enable as needed
- if: "target in [esp32s3]" - if: "target in [esp32s3]"
+6 -8
View File
@@ -1,9 +1,7 @@
manifest.version=0.3 manifest.version=0.2
target.sdk=0.8.0-dev target.sdk=0.0.0
target.platforms=esp32,esp32s3,esp32c6,esp32p4,posix-x86_64 target.platforms=esp32,esp32s3,esp32c6,esp32p4,posix-x86_64
id=tactility.sdktest app.id=tactility.sdktest
version.name=0.1.0 app.version.name=0.1.0
version.code=1 app.version.code=1
app.0.id=sdktest app.name=SDK Test
app.0.name=SDK Test
app.0.binary=main
+125 -350
View File
@@ -12,7 +12,7 @@ import tarfile
from urllib.parse import urlparse from urllib.parse import urlparse
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "6.0.0" ttbuild_version = "5.0.1"
ttbuild_cdn = "https://cdn.tactilityproject.org" ttbuild_cdn = "https://cdn.tactilityproject.org"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@@ -21,7 +21,7 @@ use_local_sdk = False
local_base_path = None local_base_path = None
http_timeout_seconds = 10 http_timeout_seconds = 10
# App install uploads the whole package over HTTP and the device only responds once it's # App install uploads the whole package over HTTP and the device only responds once it's
# fully received, extracted and registered; large packages (e.g. bundled fonts/assets) can # fully received, extracted and registered - large packages (e.g. bundled fonts/assets) can
# easily take well over http_timeout_seconds on a slow SD card, so give it a lot more room. # easily take well over http_timeout_seconds on a slow SD card, so give it a lot more room.
install_timeout_seconds = 120 install_timeout_seconds = 120
@@ -33,55 +33,30 @@ shell_color_cyan = "\033[36m"
shell_color_reset = "\033[m" shell_color_reset = "\033[m"
def print_help(): def print_help():
print("Usage: python tactility.py [action] [options]") print("Usage: python tactility.py [app_path] [action] [options]")
print("") print("")
print("Actions:") print("Actions:")
print("") print(" build [platform] Build the app. Optionally specify a platform.")
print(" build Build the app. Optionally specify a platform.")
print(" Supported platforms are lower case. Example: esp32s3") print(" Supported platforms are lower case. Example: esp32s3")
print(" Supported platforms are read from manifest.properties") print(" Supported platforms are read from manifest.properties")
print(" Parameters:")
print(" (optional) --architecture [input], -a [input]")
print("")
print(" clean Clean the build folders") print(" clean Clean the build folders")
print("")
print(" clearcache Clear the SDK cache") print(" clearcache Clear the SDK cache")
print("")
print(" updateself Update this tool") print(" updateself Update this tool")
print("") print(" run [ip] Run the application")
print(" run Run the application") print(" install [ip] Install the application")
print(" Parameters:") print(" uninstall [ip] Uninstall the application")
print(" (required) --host, -h [input]") print(" bir [ip] [platform] Build, install then run. Optionally specify a platform.")
print("") print(" brrr [ip] [platform] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
print(" install Install the application")
print(" Parameters:")
print(" (required) --host, -h [input]")
print("")
print(" uninstall Uninstall the application")
print(" Parameters:")
print(" (required) --host, -h [input]")
print("")
print(" bir Build, install then run.")
print(" Parameters:")
print(" (required) --host, -h [input]")
print(" (optional) --architecture, -a [input]")
print("")
print(" brrr Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
print(" Parameters:")
print(" (required) --host, -h [input]")
print(" (optional) --architecture, -a [input]")
print("") print("")
print("Options:") print("Options:")
print(" -p, --path [input] Path to the app directory (defaults to current directory)")
print(" --help Show this commandline info") print(" --help Show this commandline info")
print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH with platform subfolders matching target platforms.") print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH with platform subfolders matching target platforms.")
print(" --skip-build Run everything except the idf.py/CMake commands") print(" --skip-build Run everything except the idf.py/CMake commands")
print(" --verbose Show extra console output") print(" --verbose Show extra console output")
print("") print("")
print("Examples:") print("Examples:")
print(" python tactility.py build") print(" python tactility.py Apps/Snake build esp32s3 --verbose")
print(" python tactility.py build --path Apps/Snake --architecture esp32s3 --verbose") print(" python tactility.py Apps/Snake bir 192.168.1.50 esp32s3")
print(" python tactility.py bir --path Apps/Snake --host 192.168.1.50 --architecture esp32s3")
# region Core # region Core
@@ -130,8 +105,8 @@ def exit_with_error(message):
print_error(message) print_error(message)
sys.exit(1) sys.exit(1)
def get_url(host, path): def get_url(ip, path):
return f"http://{host}:{ttport}{path}" return f"http://{ip}:{ttport}{path}"
def read_properties_file(path): def read_properties_file(path):
properties = {} properties = {}
@@ -148,126 +123,6 @@ def read_properties_file(path):
#endregion Core #endregion Core
#region Versioning
class SemanticVersion:
def __init__(self, major, minor, patch, tag=None):
self.major = major
self.minor = minor
self.patch = patch
self.tag = tag
@staticmethod
def parse(version_string):
match = re.match(r"^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$", version_string)
if match is None:
exit_with_error(f"Invalid version format: {version_string}")
major, minor, patch, tag = match.groups()
return SemanticVersion(int(major), int(minor), int(patch), tag)
def _numeric(self):
return (self.major, self.minor, self.patch)
def __eq__(self, other):
return self._numeric() == other._numeric() and self.tag == other.tag
def __lt__(self, other):
if self._numeric() != other._numeric():
return self._numeric() < other._numeric()
# Same major.minor.patch: an untagged version outranks any tagged (pre-release) one.
if self.tag == other.tag:
return False
if self.tag is None:
return False
if other.tag is None:
return True
return self.tag < other.tag
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}" + (f"-{self.tag}" if self.tag else "")
# Ignores tags: a "-dev"/"-rc1"-tagged pre-release build of a version is still that version,
# not an older one, unlike __lt__'s full ordering (which ranks pre-releases below releases).
def is_older_release_of(self, other):
return self._numeric() < other._numeric()
# Append a new entry whenever the tool drops support for older SDKs.
# The last entry is the currently effective minimum.
SDK_COMPATIBILITY_LEDGER = [
SemanticVersion.parse("0.8.0"),
]
def minimum_supported_sdk_version():
return SDK_COMPATIBILITY_LEDGER[-1]
def validate_sdk_compatibility(target_sdk_version):
minimum = minimum_supported_sdk_version()
if SemanticVersion.parse(target_sdk_version).is_older_release_of(minimum):
exit_with_error(
f"This tool requires SDK version {minimum} or newer "
f"(manifest.properties requests {target_sdk_version})"
)
#endregion Versioning
#region Argument parsing
# canonical name -> (short flag or None, long flag, takes_value)
ARG_SPECS = {
"path": ("-p", "--path", True),
"host": ("-h", "--host", True),
"platform": ("-a", "--architecture", True),
"verbose": (None, "--verbose", False),
"skip-build": (None, "--skip-build", False),
"local-sdk": (None, "--local-sdk", False),
"help": (None, "--help", False),
}
class Arguments:
def __init__(self, values):
self.values = values
def get(self, key, default=None):
return self.values.get(key, default)
def has(self, key):
return key in self.values
class ParsedCommand:
def __init__(self, action, args):
self.action = action
self.arguments = args
def parse_command_line(argv):
token_to_spec = {}
for canonical, (short, long, takes_value) in ARG_SPECS.items():
if short is not None:
token_to_spec[short] = (canonical, takes_value)
token_to_spec[long] = (canonical, takes_value)
action = None
args = {}
index = 0
while index < len(argv):
token = argv[index]
if token in token_to_spec:
canonical, takes_value = token_to_spec[token]
if takes_value:
index += 1
if index >= len(argv):
exit_with_error(f"Missing value for {token}")
args[canonical] = argv[index]
else:
args[canonical] = "true"
elif action is None:
action = token
else:
exit_with_error(f"Unexpected argument: {token}")
index += 1
return ParsedCommand(action, Arguments(args))
#endregion Argument parsing
#region SDK helpers #region SDK helpers
def read_sdk_json(): def read_sdk_json():
@@ -377,7 +232,7 @@ def validate_self(sdk_json):
exit_with_error("Server returned invalid SDK data format (toolDownloadUrl not found)") exit_with_error("Server returned invalid SDK data format (toolDownloadUrl not found)")
tool_version = sdk_json["toolVersion"] tool_version = sdk_json["toolVersion"]
tool_compatibility = sdk_json["toolCompatibility"] tool_compatibility = sdk_json["toolCompatibility"]
if SemanticVersion.parse(ttbuild_version) < SemanticVersion.parse(tool_version): if tool_version != ttbuild_version:
print_warning(f"New version available: {tool_version} (currently using {ttbuild_version})") print_warning(f"New version available: {tool_version} (currently using {ttbuild_version})")
print_warning(f"Run 'tactility.py updateself' to update.") print_warning(f"Run 'tactility.py updateself' to update.")
if re.search(tool_compatibility, ttbuild_version) is None: if re.search(tool_compatibility, ttbuild_version) is None:
@@ -392,70 +247,10 @@ def validate_self(sdk_json):
def read_manifest(): def read_manifest():
return read_properties_file("manifest.properties") return read_properties_file("manifest.properties")
def is_v3_manifest(manifest):
return manifest.get("manifest.version") == "0.3"
# The package identifier: a v2 manifest has no package/app distinction, so its single "app.id"
# doubles as both; a v3 manifest's package id is the bare "id" key, distinct from any of its
# (possibly several) "app.N.id" app ids.
def get_package_id(manifest):
return manifest["id"] if is_v3_manifest(manifest) else manifest["app.id"]
# The id of the app "run"/"install" operate on. A v2 manifest has exactly one; a v3 manifest may
# declare several, so this picks the first ("app.0.id") as the one a single-app dev workflow means.
def get_primary_app_id(manifest):
return manifest["app.0.id"] if is_v3_manifest(manifest) else manifest["app.id"]
def validate_manifest(manifest): def validate_manifest(manifest):
for key in ("manifest.version", "target.sdk", "target.platforms"): for key in ("manifest.version", "target.sdk", "target.platforms", "app.id", "app.version.name", "app.version.code", "app.name"):
if key not in manifest: if key not in manifest:
exit_with_error(f"Invalid manifest format: {key} not found") exit_with_error(f"Invalid manifest format: {key} not found")
if manifest["manifest.version"] not in ("0.2", "0.3"):
exit_with_error(f"Unsupported manifest.version: {manifest['manifest.version']}")
if is_v3_manifest(manifest):
for key in ("id", "version.name", "version.code"):
if key not in manifest:
exit_with_error(f"Invalid manifest format: {key} not found")
if "app.0.id" not in manifest:
exit_with_error("Invalid manifest format: app.0.id not found")
index = 0
while f"app.{index}.id" in manifest:
for suffix in ("name", "binary"):
key = f"app.{index}.{suffix}"
if key not in manifest:
exit_with_error(f"Invalid manifest format: {key} not found")
index += 1
else:
for key in ("app.id", "app.version.name", "app.version.code", "app.name"):
if key not in manifest:
exit_with_error(f"Invalid manifest format: {key} not found")
validate_sdk_compatibility(manifest["target.sdk"])
# Maps each binary this app builds to the directory its own CMake project lives in. A v2
# manifest always describes exactly one app, built at the app's own root (see
# package_intermediate_binaries() for its fixed "app.{elf,so}" package filename, matching
# package_manifest_parse_v2()'s single implicit binary). A v3 manifest declaring a single
# "app.0.*" block is the same case: built at the app's own root. Only when a v3 manifest
# declares more than one "app.N.*" block (0-indexed, contiguous - the first missing "app.N.id"
# ends the list, mirroring package_manifest_parse_v3()) does each get its own subdirectory,
# named after its own "app.N.binary" (the filename it installs as - see
# package_manifest_parse_v3()'s doc).
def get_binary_dirs(manifest):
if is_v3_manifest(manifest):
binaries = []
index = 0
while f"app.{index}.id" in manifest:
binary = manifest[f"app.{index}.binary"]
binaries.append((binary, binary))
index += 1
if len(binaries) == 1:
return [(binaries[0][0], ".")]
for binary, directory in binaries:
if not os.path.isdir(directory):
exit_with_error(f"Binary directory not found for '{binary}': {directory}")
return binaries
else:
return [(manifest["app.id"], ".")]
def is_valid_manifest_platform(manifest, platform): def is_valid_manifest_platform(manifest, platform):
manifest_platforms = manifest["target.platforms"].split(",") manifest_platforms = manifest["target.platforms"].split(",")
@@ -550,7 +345,7 @@ tactility_project_post(%(app_id)s)
def cmakelists_version_marker(): def cmakelists_version_marker():
return f"# tactility-cmakelists-version: {CMAKELISTS_VERSION}" return f"# tactility-cmakelists-version: {CMAKELISTS_VERSION}"
def ensure_cmakelists_up_to_date(project_id): def ensure_cmakelists_up_to_date(manifest):
marker = cmakelists_version_marker() marker = cmakelists_version_marker()
if os.path.exists("CMakeLists.txt"): if os.path.exists("CMakeLists.txt"):
with open("CMakeLists.txt", "r") as file: with open("CMakeLists.txt", "r") as file:
@@ -558,7 +353,7 @@ def ensure_cmakelists_up_to_date(project_id):
if first_line == marker: if first_line == marker:
return return
print(f"Updating CMakeLists.txt to {marker}") print(f"Updating CMakeLists.txt to {marker}")
content = CMAKELISTS_TEMPLATE % {"version": CMAKELISTS_VERSION, "app_id": project_id} content = CMAKELISTS_TEMPLATE % {"version": CMAKELISTS_VERSION, "app_id": manifest["app.id"]}
with open("CMakeLists.txt", "w") as file: with open("CMakeLists.txt", "w") as file:
file.write(content) file.write(content)
@@ -737,58 +532,43 @@ def package_intermediate_manifest(target_path):
shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties")) shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties"))
return True return True
def get_artifact_extension(platform): def package_intermediate_binaries(target_path, platforms):
# POSIX apps are dlopen()ed shared objects (app-posix-module), not idf.py/elf_loader elf_dir = os.path.join(target_path, "elf")
# relocatable images, so they land as a plain ".so" instead of ".elf". os.makedirs(elf_dir, exist_ok=True)
return ".so" if platform.startswith("posix") else ".elf" for platform in platforms:
elf_path = find_elf_file(platform)
def package_intermediate_binaries(target_path, platforms, manifest): if elf_path is None:
# Each binary is built in its own directory (get_binary_dirs()), with its own print_error(f"ELF file not found for {platform}")
# build/cmake-build-{platform} tree; chdir into it so find_elf_file()/get_cmake_path() return False
# (both CWD-relative) resolve against the right one, then restore CWD for the next binary. # app-posix-module's loader resolves an installed app to "elf/posix-<arch>.so", matching
original_cwd = os.getcwd() # its own compile-time architecture, not "*.elf".
for binary, directory in get_binary_dirs(manifest): extension = ".so" if platform.startswith("posix") else ".elf"
os.chdir(directory) shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}{extension}"))
try:
for platform in platforms:
elf_path = find_elf_file(platform)
if elf_path is None:
print_error(f"ELF file not found for '{binary}' on {platform}")
return False
# v2's single app always installs at the fixed path bin/{platform}/app.{elf,so}
# (package_manifest_parse_v2()); v3 apps install under their own declared
# app.N.binary name (package_manifest_parse_v3()).
artifact_name = binary if is_v3_manifest(manifest) else "app"
platform_dir = os.path.join(target_path, "bin", platform)
os.makedirs(platform_dir, exist_ok=True)
shutil.copy(elf_path, os.path.join(platform_dir, f"{artifact_name}{get_artifact_extension(platform)}"))
finally:
os.chdir(original_cwd)
return True return True
def package_intermediate_assets(target_path): def package_intermediate_assets(target_path):
if os.path.isdir("assets"): if os.path.isdir("assets"):
shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True) shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True)
def package_intermediate(platforms, manifest): def package_intermediate(platforms):
target_path = os.path.abspath(os.path.join("build", "package-intermediate")) target_path = os.path.join("build", "package-intermediate")
if os.path.isdir(target_path): if os.path.isdir(target_path):
shutil.rmtree(target_path) shutil.rmtree(target_path)
os.makedirs(target_path, exist_ok=True) os.makedirs(target_path, exist_ok=True)
if not package_intermediate_manifest(target_path): if not package_intermediate_manifest(target_path):
return False return False
if not package_intermediate_binaries(target_path, platforms, manifest): if not package_intermediate_binaries(target_path, platforms):
return False return False
package_intermediate_assets(target_path) package_intermediate_assets(target_path)
return True return True
def package_name(manifest): def package_name(manifest):
return os.path.join("build", f"{get_package_id(manifest)}.app") return os.path.join("build", f"{manifest['app.id']}.app")
def package_all(manifest, platforms): def package_all(manifest, platforms):
status = f"Building package with {platforms}" status = f"Building package with {platforms}"
print_status_busy(status) print_status_busy(status)
if not package_intermediate(platforms, manifest): if not package_intermediate(platforms):
print_status_error("Building package failed: missing inputs") print_status_error("Building package failed: missing inputs")
return False return False
# Create build/something.app # Create build/something.app
@@ -808,9 +588,8 @@ def setup_environment():
global ttbuild_path global ttbuild_path
os.makedirs(ttbuild_path, exist_ok=True) os.makedirs(ttbuild_path, exist_ok=True)
def build_action(manifest, arguments): def build_action(manifest, platform_arg, skip_build):
platform_arg = arguments.get("platform") ensure_cmakelists_up_to_date(manifest)
skip_build = arguments.has("skip-build")
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg) platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
# Environment validation # Environment validation
validate_environment(platforms_to_build) validate_environment(platforms_to_build)
@@ -819,10 +598,10 @@ def build_action(manifest, arguments):
global local_base_path global local_base_path
local_base_path = os.environ.get("TACTILITY_SDK_PATH") local_base_path = os.environ.get("TACTILITY_SDK_PATH")
validate_local_sdks(platforms_to_build, manifest["target.sdk"]) validate_local_sdks(platforms_to_build, manifest["target.sdk"])
if should_fetch_sdkconfig_files(platforms_to_build): if should_fetch_sdkconfig_files(platforms_to_build):
fetch_sdkconfig_files(platforms_to_build) fetch_sdkconfig_files(platforms_to_build)
if not use_local_sdk: if not use_local_sdk:
sdk_json = read_sdk_json() sdk_json = read_sdk_json()
validate_self(sdk_json) validate_self(sdk_json)
@@ -831,49 +610,19 @@ def build_action(manifest, arguments):
if not use_local_sdk: if not use_local_sdk:
if not sdk_download_all(sdk_version, platforms_to_build): if not sdk_download_all(sdk_version, platforms_to_build):
exit_with_error("Failed to download one or more SDKs") exit_with_error("Failed to download one or more SDKs")
if not build_all(sdk_version, platforms_to_build, skip_build): # Environment validation
# A multi-binary app builds each of its binaries in its own subdirectory as an independent return False
# CMake project; a single-binary app has exactly one binary, built at the app root
# ("." from get_binary_dirs()), matching the tool's original single-binary behavior exactly.
original_cwd = os.getcwd()
for binary, directory in get_binary_dirs(manifest):
os.chdir(directory)
try:
ensure_cmakelists_up_to_date(binary if directory == "." else f"{get_package_id(manifest)}.{binary}")
if not build_all(sdk_version, platforms_to_build, skip_build):
return False
finally:
os.chdir(original_cwd)
if not skip_build: if not skip_build:
if not package_all(manifest, platforms_to_build): if not package_all(manifest, platforms_to_build):
return False return False
return True return True
def clean_action(manifest): def clean_action():
cleaned_any = False
# The app root always has its own build/ (package-intermediate + the final .app/.elf,
# regardless of binary count). A multi-binary app additionally has one build/ per binary
# subdirectory (get_binary_dirs()), which "." (the root, handled above) doesn't repeat.
if os.path.exists("build"): if os.path.exists("build"):
print_status_busy("Removing build/") print_status_busy("Removing build/")
shutil.rmtree("build") shutil.rmtree("build")
print_status_success("Removed build/") print_status_success("Removed build/")
cleaned_any = True else:
original_cwd = os.getcwd()
for _, directory in get_binary_dirs(manifest):
if directory == ".":
continue
os.chdir(directory)
try:
if os.path.exists("build"):
print_status_busy(f"Removing {directory}/build/")
shutil.rmtree("build")
print_status_success(f"Removed {directory}/build/")
cleaned_any = True
finally:
os.chdir(original_cwd)
if not cleaned_any:
print("Nothing to clean") print("Nothing to clean")
def clear_cache_action(): def clear_cache_action():
@@ -892,9 +641,9 @@ def update_self_action():
else: else:
exit_with_error("Update failed") exit_with_error("Update failed")
def get_device_info(host): def get_device_info(ip):
print_status_busy(f"Requesting device info") print_status_busy(f"Requesting device info")
url = get_url(host, "/info") url = get_url(ip, "/info")
try: try:
response = requests.get(url, timeout=http_timeout_seconds) response = requests.get(url, timeout=http_timeout_seconds)
if response.status_code != 200: if response.status_code != 200:
@@ -905,13 +654,10 @@ def get_device_info(host):
except requests.RequestException as e: except requests.RequestException as e:
print_status_error(f"Device info request failed: {e}") print_status_error(f"Device info request failed: {e}")
def run_action(manifest, arguments): def run_action(manifest, ip):
host = arguments.get("host") app_id = manifest["app.id"]
if host is None:
exit_with_error("Missing required argument: --host")
app_id = get_primary_app_id(manifest)
print_status_busy("Running") print_status_busy("Running")
url = get_url(host, "/app/run") url = get_url(ip, "/app/run")
params = {'id': app_id} params = {'id': app_id}
try: try:
response = requests.post(url, params=params, timeout=http_timeout_seconds) response = requests.post(url, params=params, timeout=http_timeout_seconds)
@@ -922,21 +668,21 @@ def run_action(manifest, arguments):
except requests.RequestException as e: except requests.RequestException as e:
print_status_error(f"Running request failed: {e}") print_status_error(f"Running request failed: {e}")
def install_action(manifest, arguments): def install_action(manifest, ip, platforms):
host = arguments.get("host")
if host is None:
exit_with_error("Missing required argument: --host")
package_path = package_name(manifest)
if not os.path.isfile(package_path):
print_status_error(f"Package not found: {package_path} (run 'build' first)")
return False
print_status_busy("Installing") print_status_busy("Installing")
url = get_url(host, "/app/install") for platform in platforms:
elf_path = find_elf_file(platform)
if elf_path is None:
print_status_error(f"ELF file not built for {platform}")
return False
package_path = package_name(manifest)
# print(f"Installing {package_path} to {ip}")
url = get_url(ip, "/app/install")
try: try:
# Prepare multipart form data # Prepare multipart form data
with open(package_path, 'rb') as file: with open(package_path, 'rb') as file:
files = { files = {
'app': file 'elf': file
} }
response = requests.put(url, files=files, timeout=install_timeout_seconds) response = requests.put(url, files=files, timeout=install_timeout_seconds)
if response.status_code != 200: if response.status_code != 200:
@@ -952,13 +698,10 @@ def install_action(manifest, arguments):
print_status_error(f"Install file error: {e}") print_status_error(f"Install file error: {e}")
return False return False
def uninstall_action(manifest, arguments): def uninstall_action(manifest, ip):
host = arguments.get("host") app_id = manifest["app.id"]
if host is None:
exit_with_error("Missing required argument: --host")
app_id = get_package_id(manifest)
print_status_busy("Uninstalling") print_status_busy("Uninstalling")
url = get_url(host, "/app/uninstall") url = get_url(ip, "/app/uninstall")
params = {'id': app_id} params = {'id': app_id}
try: try:
response = requests.put(url, params=params, timeout=http_timeout_seconds) response = requests.put(url, params=params, timeout=http_timeout_seconds)
@@ -973,34 +716,38 @@ def uninstall_action(manifest, arguments):
if __name__ == "__main__": if __name__ == "__main__":
print(f"Tactility Build System v{ttbuild_version}") print(f"Tactility Build System v{ttbuild_version}")
if "--help" in sys.argv:
# Anchor the cache to the invocation directory, before --path (below) can chdir into the app.
ttbuild_path = os.path.abspath(ttbuild_path)
argv = sys.argv[1:]
if len(argv) == 0:
print_help()
sys.exit(1)
parsed_command = parse_command_line(argv)
if parsed_command.arguments.has("help"):
print_help() print_help()
sys.exit() sys.exit()
if parsed_command.action is None: # Argument validation
if len(sys.argv) == 1:
print_help() print_help()
sys.exit(1) sys.exit(1)
if "--verbose" in sys.argv:
verbose = parsed_command.arguments.has("verbose") verbose = True
use_local_sdk = parsed_command.arguments.has("local-sdk") sys.argv.remove("--verbose")
skip_build = False
app_path = parsed_command.arguments.get("path") if "--skip-build" in sys.argv:
if app_path is not None: skip_build = True
if not os.path.isdir(app_path) or not os.path.isfile(os.path.join(app_path, "manifest.properties")): sys.argv.remove("--skip-build")
exit_with_error(f"App path not found or missing manifest.properties: {app_path}") if "--local-sdk" in sys.argv:
if verbose: use_local_sdk = True
print_status_success(f"Switching to app directory: {app_path}") sys.argv.remove("--local-sdk")
os.chdir(app_path)
# Check if the first argument is a path to an app directory
if len(sys.argv) > 2:
potential_app_dir = sys.argv[1]
if os.path.isdir(potential_app_dir) and os.path.isfile(os.path.join(potential_app_dir, "manifest.properties")):
if verbose:
print_status_success(f"Switching to app directory: {potential_app_dir}")
os.chdir(potential_app_dir)
sys.argv = [sys.argv[0]] + sys.argv[2:]
if len(sys.argv) < 2:
print_help()
sys.exit(1)
action_arg = sys.argv[1]
# Environment setup # Environment setup
setup_environment() setup_environment()
@@ -1008,30 +755,58 @@ if __name__ == "__main__":
exit_with_error("manifest.properties not found") exit_with_error("manifest.properties not found")
manifest = read_manifest() manifest = read_manifest()
validate_manifest(manifest) validate_manifest(manifest)
all_platform_targets = manifest["target.platforms"].split(",")
# Update SDK cache (tool.json) # Update SDK cache (tool.json)
if not use_local_sdk and should_update_tool_json() and not update_tool_json(): if not use_local_sdk and should_update_tool_json() and not update_tool_json():
exit_with_error("Failed to retrieve SDK info") exit_with_error("Failed to retrieve SDK info")
# Actions # Actions
action_arg = parsed_command.action
if action_arg == "build": if action_arg == "build":
if not build_action(manifest, parsed_command.arguments): if len(sys.argv) < 2:
print_help()
exit_with_error("Commandline parameter missing")
platform = None
if len(sys.argv) > 2:
platform = sys.argv[2]
if not build_action(manifest, platform, skip_build):
sys.exit(1) sys.exit(1)
elif action_arg == "clean": elif action_arg == "clean":
clean_action(manifest) clean_action()
elif action_arg == "clearcache": elif action_arg == "clearcache":
clear_cache_action() clear_cache_action()
elif action_arg == "updateself": elif action_arg == "updateself":
update_self_action() update_self_action()
elif action_arg == "run": elif action_arg == "run":
run_action(manifest, parsed_command.arguments) if len(sys.argv) < 3:
print_help()
exit_with_error("Commandline parameter missing")
run_action(manifest, sys.argv[2])
elif action_arg == "install": elif action_arg == "install":
install_action(manifest, parsed_command.arguments) if len(sys.argv) < 3:
print_help()
exit_with_error("Commandline parameter missing")
platform = None
platforms_to_install = all_platform_targets
if len(sys.argv) >= 4:
platform = sys.argv[3]
platforms_to_install = [platform]
install_action(manifest, sys.argv[2], platforms_to_install)
elif action_arg == "uninstall": elif action_arg == "uninstall":
uninstall_action(manifest, parsed_command.arguments) if len(sys.argv) < 3:
print_help()
exit_with_error("Commandline parameter missing")
uninstall_action(manifest, sys.argv[2])
elif action_arg == "bir" or action_arg == "brrr": elif action_arg == "bir" or action_arg == "brrr":
if build_action(manifest, parsed_command.arguments): if len(sys.argv) < 3:
if install_action(manifest, parsed_command.arguments): print_help()
run_action(manifest, parsed_command.arguments) exit_with_error("Commandline parameter missing")
platform = None
platforms_to_install = all_platform_targets
if len(sys.argv) >= 4:
platform = sys.argv[3]
platforms_to_install = [platform]
if build_action(manifest, platform, skip_build):
if install_action(manifest, sys.argv[2], platforms_to_install):
run_action(manifest, sys.argv[2])
else: else:
print_help() print_help()
exit_with_error("Unknown commandline parameter") exit_with_error("Unknown commandline parameter")
+1 -1
View File
@@ -3,4 +3,4 @@
nvs, data, nvs, 0x9000, 0x6000, nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000, phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 4M, factory, app, factory, 0x10000, 4M,
system, data, fat, , 512k, system, data, fat, , 128k,
1 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 4M,
6 system, data, fat, , 512k, system, data, fat, , 128k,