Various improvements (#614)
- Auto-select widgets in Launcher and apps with toolbars on devices without touch. - Improved USB HID input reliability, cleanup - Updated PSRAM settings to improve boot stability on supported devices. - Prevented duplicate Wi-Fi event subscriptions during screen rebuilds. - Updated docs - Fixes in WifiManage and WifiConnect - Reduced main task stack size - Moved USB HID stack size to PSRAM when available - app_manager_find_manifest() now returns a copy instead of a pointer
This commit is contained in:
committed by
GitHub
parent
cc8be3faef
commit
d6b1d15e56
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
Before implementing:
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them - don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
## 2. Simplicity First
|
||||
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
## 3. Surgical Changes
|
||||
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it - don't delete it.
|
||||
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
## 4. Goal-Driven Execution
|
||||
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" → "Ensure tests pass before and after"
|
||||
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] → verify: [check]
|
||||
2. [Step] → verify: [check]
|
||||
3. [Step] → verify: [check]
|
||||
```
|
||||
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
|
||||
---
|
||||
|
||||
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Architecture: App Framework
|
||||
|
||||
Apps are event-driven, C API (`app-module`, `<app/*.h>`), not a C++ class. Each app has an `AppManifest` (`id`, `name`, `category`, `location`, `flags`) and a `main(app_instance_id, argc, argv)` entry point (`AppMainFn`), modelled on a C program's `main()`. Every app instance gets its own dedicated task for its whole lifetime, and blocks in that task until it returns.
|
||||
|
||||
Lifecycle and inter-app communication go through `app_manager_*()` (`app/manager.h`) and `app_event_*()` (`app/event.h`):
|
||||
- `app_manager_start()`/`app_manager_start_with_parameters()` launch a plain instance; `app_manager_start_for_result()` launches a modal child that reports back to a parent instance.
|
||||
- An app subscribes with `app_event_subscribe()`/`app_event_await()` and reacts to `APP_EVENT_CLOSE` (terminate now) and `APP_EVENT_RESULT` (a child it started reported back).
|
||||
- An app closes itself by calling `app_manager_finish()` right before returning from `main()`; another instance is closed via `app_manager_stop()`.
|
||||
|
||||
Apps are registered at startup via `app_manager_add()`. External apps can be loaded from SD card via `manifest.properties` files, or side-loaded as ELF binaries on ESP32 (see `app/loader.h`'s `AppLoaderApi`).
|
||||
|
||||
Apps can be loaded from:
|
||||
|
||||
- memory (`APP_LOCATION_MEMORY`)
|
||||
- a path pointing to an install folder where an `.app` file was installed (`APP_LOCATION_PATH`)
|
||||
- a path pointing to an `.elf` file (`APP_LOCATION_PATH`)
|
||||
|
||||
An app can build an optional UI via the LVGL window-manager module (see `lvgl.md`).
|
||||
@@ -0,0 +1,10 @@
|
||||
# Architecture: Device/Driver/Module System (kernel layer, C API)
|
||||
|
||||
The kernel uses a Linux-inspired device model:
|
||||
|
||||
- **Module** (`struct Module`): loadable unit that registers drivers, hardware and symbols. Lifecycle: `module_construct` → `module_add` → `module_start`. Each device board and platform is a module.
|
||||
- **Driver** (`struct Driver`): binds to devices via `compatible` strings (like devicetree). Has `start_device`/`stop_device` callbacks and an `api` pointer for type-specific operations.
|
||||
- **Device** (`struct Device`): represents hardware. Lifecycle: `device_construct` → `device_add` → `device_start`. Has a parent-child tree, driver binding, and locking.
|
||||
- **DeviceType** (`struct DeviceType`): enables discovering devices by category (e.g. `DISPLAY_TYPE`, `TOUCH_TYPE`, `UART_CONTROLLER_TYPE`).
|
||||
|
||||
Devices are defined via **devicetree** `.dts` files in each `Devices/<id>/` folder. A custom devicetree compiler (`Buildscripts/DevicetreeCompiler/compile.py`) generates C code from these files. Each device folder also has a `devicetree.yaml` specifying dependencies and the `.dts` file.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Architecture: Layer Stack (bottom to top)
|
||||
|
||||
- **TactilityKernel** — C API kernel: device/driver/module lifecycle, concurrency primitives (thread, mutex, timer, dispatcher), filesystem, logging. Header convention: `<tactility/*.h>` (lowercase snake_case).
|
||||
- **TactilityFreeRtos** — Thin C++ wrappers around FreeRTOS primitives.
|
||||
- **Tactility** — Main OS layer: app framework, service framework, LVGL integration, networking and services (Wi-Fi, BLE, NTP, ESP-NOW), settings, i18n.
|
||||
- **TactilityC** — C bindings (`tt_*.h`) for Tactility, used by side-loaded ELF apps on ESP32. Deprecated, replaced by TactilityKernel.
|
||||
- **Firmware** — Entry point (`app_main`).
|
||||
@@ -0,0 +1,5 @@
|
||||
# Architecture: Build System
|
||||
|
||||
The `tactility_add_module()` CMake macro (in `Buildscripts/module.cmake`) wraps ESP-IDF's `idf_component_register` on ESP32 and standard `add_library` on POSIX, allowing the same source to build for both targets.
|
||||
|
||||
`device.py` reads `Devices/<id>/device.properties` and generates the `sdkconfig` file with all necessary ESP-IDF config (target chip, flash size, SPIRAM, LVGL fonts, Bluetooth, USB, etc.).
|
||||
@@ -0,0 +1,62 @@
|
||||
# Building
|
||||
|
||||
## Git
|
||||
|
||||
The repository uses git submodules. Make sure to use `--recurse-submodules` on relevant git commands.
|
||||
|
||||
## Simulator (Linux/macOS, no ESP-IDF needed)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The simulator does **NOT** build or run on native Windows (Win32/PowerShell/cmd). This is
|
||||
> a hard platform limitation, not a missing tool or PATH issue — do not attempt `cmake -B
|
||||
> buildsim` on Windows, it will not work. WSL is a separate, Linux environment and is fine.
|
||||
|
||||
```bash
|
||||
cmake -B buildsim -G Ninja
|
||||
ninja -C buildsim # build firmware + tests
|
||||
./buildsim/Firmware/Tactility # run simulator
|
||||
```
|
||||
|
||||
## ESP32 firmware
|
||||
|
||||
```bash
|
||||
python device.py <device-id> # generate sdkconfig for device (e.g. lilygo-tdeck)
|
||||
python device.py <device-id> --dev # dev mode: force 4MB partition table
|
||||
idf.py build # build firmware
|
||||
idf.py flash monitor # flash and monitor
|
||||
```
|
||||
|
||||
Device IDs are the folder names under `Devices/` (e.g. `lilygo-tdeck`, `m5stack-cores3`, `cyd-2432s028r`).
|
||||
|
||||
### Windows: activating the ESP-IDF environment
|
||||
|
||||
On native Windows, `idf.py` is not on PATH by default — it must be activated per-shell first.
|
||||
The install script places a PowerShell profile activator per IDF version at
|
||||
`%IDF_TOOLS_PATH%\Microsoft.v<version>.PowerShell_profile.ps1` (path controlled by the
|
||||
`IDF_TOOLS_PATH` environment variable, set to wherever ESP-IDF's tools were installed, e.g.
|
||||
`C:\Espressif\tools`). Source it before running any `idf.py` command:
|
||||
|
||||
```powershell
|
||||
. "$env:IDF_TOOLS_PATH\Microsoft.v5.5.2.PowerShell_profile.ps1" # match the installed IDF version
|
||||
Set-Location "<repo-root>"
|
||||
idf.py build 2>&1 | Select-Object -Last 250
|
||||
```
|
||||
|
||||
This is Windows-specific setup (the main dev works on Linux, where `idf.py` is normally
|
||||
already on PATH via `export.sh`/`. ./export.sh` or a shell profile).
|
||||
|
||||
## Devicetree
|
||||
|
||||
A device implementation has a `.dts` file.
|
||||
The parser at `Buildscripts/DevicetreeCompiler/` converts DTS into C code.
|
||||
It's called from the `Firmware/` build process.
|
||||
|
||||
## Tests
|
||||
|
||||
Tests use Doctest and run on simulator (POSIX) target only:
|
||||
|
||||
```bash
|
||||
cmake -B buildsim -G Ninja
|
||||
ninja -C buildsim build-tests
|
||||
cd buildsim && ctest --test-dir Tests
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
# Coding Style
|
||||
|
||||
Two conventions coexist; which one to use depends on the project layer:
|
||||
|
||||
- **C code** (TactilityKernel, drivers): `lower_snake_case` for files, functions, variables. `UpperCamelCase` for types. Files in `source/`, `include/`, `private/` directories.
|
||||
- **C++ code** (Tactility, apps, services): `UpperCamelCase` for files and types. `lowerCamelCase` for functions. Files in `Source/`, `Include/`, `Private/` directories.
|
||||
|
||||
For projects that emit C headers and have a C++ implementation file: the internal C++ function naming should be snake_case.
|
||||
|
||||
Formatting is enforced by `.clang-format` (LLVM-based, 4-space indent, no column limit).
|
||||
Never throw exceptions — use return types for error handling. Use `enum class` over plain `enum` when writing C++ code.
|
||||
Do not add redundant null checks for parameters with an explicit non-null precondition.
|
||||
|
||||
Code Comments:
|
||||
|
||||
- Should be as short as possible, leaving only important context.
|
||||
- Should avoid explaining what the code does, unless the code complexity is high enough to warrant an explanation.
|
||||
- Must avoid explaining how the code was before, or how it was changed.
|
||||
- Should explain why code is implemented.
|
||||
- Should be as brief as possible without losing critical information.
|
||||
- Should avoid explaining what was not implemented.
|
||||
- Should avoid referring to designs of other subsystems.
|
||||
- Must avoid interjections: avoid hyphens or braces to interject. If interjections provide crucial info, use Doxygen entity/anchor references like:
|
||||
/**
|
||||
* A dedicated completion \signal for one app instance's task.
|
||||
* Whichever \side finishes with it last is the one that deletes `semaphore` and frees this struct.
|
||||
*
|
||||
* \signal Not the task's shared default FreeRTOS notification, which app_event.cpp's AppEventSubscription also uses.
|
||||
* An unrelated event delivered to the same task could otherwise unblock a waiter early.
|
||||
* \side The exiting task or a concurrent app_scheduler_stop() that found the entry in time and is waiting on `semaphore`.
|
||||
*/
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Architecture: Hardware Abstraction Layer
|
||||
|
||||
## Driver
|
||||
|
||||
A driver generally consists of:
|
||||
- Registration of driver in parent module (optional, but desirable)
|
||||
- YAML bindings in the `bindings/` folder
|
||||
- An `#include` that is used in the `.dts` file. The include is in `[projectname]/bindings/[drivername].h`
|
||||
- The driver implementation: a `.cpp` and `.h` file. The implementation is C++, but the header exposes pure C functions. C implementations are allowed, but C++ is preferred.
|
||||
|
||||
Drivers are part of a kernel module.
|
||||
|
||||
Modules with drivers can be stored in:
|
||||
- TactilityKernel
|
||||
- A subproject in `Platforms` folder
|
||||
- A subproject in `Devices` folder
|
||||
- A subproject in `Drivers` folder
|
||||
|
||||
## Kernel Modules
|
||||
|
||||
Kernel module names are lower case and postfixed with `-module`.
|
||||
|
||||
Projects that are kernel modules:
|
||||
|
||||
1. Declare a `struct Module`
|
||||
2. Contain a `devicetree.yaml` file that declares a list of dependencies (for parsing the devicetree) and specifies the bindings folder that contains the drivers' YAML definitions. For example:
|
||||
```yaml
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# Key Conventions
|
||||
|
||||
- Shared cross-platform code uses `#ifdef ESP_PLATFORM` for ESP32-specific paths.
|
||||
Code in `Platforms/PlatformEsp32/` is already ESP-only and does not need guards around ESP-IDF includes.
|
||||
- The `Drivers/` directory contains hardware drivers (display controllers, touch controllers, PMICs, etc.) — each is its own CMake component.
|
||||
- `Modules/` contains cross-cutting modules. e.g.`lvgl-module` (LVGL task management).
|
||||
- `Data/system/` and `Data/data/` are flashed as FAT filesystem images on ESP32.
|
||||
- Translations are in `Translations/` as CSV files, generated via `generate.py`.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Architecture: LVGL
|
||||
|
||||
User interfaces should scale well for everything between very large (e.g. 1280x720) and small (e.g. 135x240) displays. Vertical and horizontal layouts are supported.
|
||||
|
||||
Two kernel modules cover LVGL:
|
||||
|
||||
- **`lvgl-module`** (`Modules/lvgl-module/`, `<lvgl/*.h>`) owns LVGL's lifecycle: init/deinit, the LVGL task loop, and `lvgl_lock()`/`lvgl_try_lock()`/`lvgl_unlock()` mutex-based locking that any task must hold before touching LVGL objects. It bridges Tactility's device model to LVGL indevs (`lvgl/devices/*.h`: `display`, `pointer`, `keyboard`, `trackball`), and provides shared fonts (`lvgl/fonts.h`: Montserrat text sizes, Material Symbols icon sets for statusbar/launcher/shared use) and a few shared widgets (`lvgl/widgets/*.h`: `toolbar`, `spinner`, `sliderbox`).
|
||||
- **`lvgl-window-manager-module`** (`Modules/lvgl-window-manager-module/`, `<lvgl_window_manager/*.h>`) manages a single stacked window per app instance on top of `lvgl-module`. `window_manager_start()`/`window_manager_stop()` create/tear down the root widget (plus optional chrome from a configured `WindowManagerScreenInitFn`); `window_manager_create()`/`window_manager_remove()` push/pop an app's window and (re)populate it via a `WindowCreateWidgetsFn`. Only the topmost window ever has live widgets - burying and resurfacing a window deletes and rebuilds its widget tree rather than hiding/showing it. That rebuild-on-remove path can run `create_widgets` on a *different* app's thread (whichever app's `window_manager_remove()` call caused this window to resurface), so `create_widgets` must only rebuild already-committed state, never decide what happens next - state transitions belong in the app's own `main()` event loop, driven by real `APP_EVENT_RESULT`s.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Architecture: Platform Abstraction
|
||||
|
||||
- `Platforms/platform-esp32/` — ESP-IDF specific implementations
|
||||
- `Platforms/platform-posix/` — POSIX simulator implementations (SDL for display)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Project Overview
|
||||
|
||||
Tactility is an operating system for the ESP32 microcontroller family. It runs on 40+ supported devices (CYD boards, LilyGO, M5Stack, Elecrow, etc.) and includes a desktop simulator. Built with C++23, ESP-IDF, LVGL, and FreeRTOS.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture: Service Framework
|
||||
|
||||
Services are a C API (`service-module`, `<service/*.h>`), not a C++ class. Each service has a `ServiceManifest` (`id`, `create_service`/`destroy_service` for its custom data, `on_start`/`on_stop` callbacks) registered via `service_manager_add()`, and is started/stopped via `service_manager_start()`/`service_manager_stop()`. Services are long-running background processes (GUI, Wi-Fi, loader, statusbar, GPS, etc.).
|
||||
Reference in New Issue
Block a user