Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .claude/rules.zip
Binary file not shown.
65 changes: 65 additions & 0 deletions .claude/rules/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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:
```
Comment thread
KenVanHoeylandt marked this conversation as resolved.
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.
18 changes: 18 additions & 0 deletions .claude/rules/app-framework.md
Original file line number Diff line number Diff line change
@@ -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`).
10 changes: 10 additions & 0 deletions .claude/rules/architecture-device-model.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions .claude/rules/architecture-layers.md
Original file line number Diff line number Diff line change
@@ -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`).
5 changes: 5 additions & 0 deletions .claude/rules/build-system.md
Original file line number Diff line number Diff line change
@@ -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.).
62 changes: 62 additions & 0 deletions .claude/rules/building.md
Original file line number Diff line number Diff line change
@@ -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
```
34 changes: 34 additions & 0 deletions .claude/rules/coding-style.md
Original file line number Diff line number Diff line change
@@ -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`.
*/
```


31 changes: 31 additions & 0 deletions .claude/rules/hardware-abstraction-layer.md
Original file line number Diff line number Diff line change
@@ -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
```
8 changes: 8 additions & 0 deletions .claude/rules/key-conventions.md
Original file line number Diff line number Diff line change
@@ -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`.
8 changes: 8 additions & 0 deletions .claude/rules/lvgl.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions .claude/rules/platform-abstraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Architecture: Platform Abstraction

- `Platforms/platform-esp32/` — ESP-IDF specific implementations
- `Platforms/platform-posix/` — POSIX simulator implementations (SDL for display)
3 changes: 3 additions & 0 deletions .claude/rules/project-overview.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions .claude/rules/service-framework.md
Original file line number Diff line number Diff line change
@@ -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.).
2 changes: 1 addition & 1 deletion Buildscripts/sdkconfig/default.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Increase stack size for Wi-Fi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
# Ensure large enough stack for network operations
CONFIG_ESP_MAIN_TASK_STACK_SIZE=6144
CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096
# Fixes static assertion: FLASH and PSRAM Mode configuration are not supported
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
# Free up IRAM
Expand Down
1 change: 0 additions & 1 deletion CLAUDE.md

This file was deleted.

6 changes: 1 addition & 5 deletions Devices/cl32/device.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ hardware.flashSize=8MB
hardware.spiRam=true
hardware.spiRamMode=QUAD
hardware.spiRamSpeed=80M
hardware.spiRamXipDisabled=true
hardware.esptoolFlashFreq=80M
hardware.bluetooth=true

Expand All @@ -19,8 +20,3 @@ display.dpi=139

lvgl.colorDepth=8
lvgl.theme=Mono

# Fix error "PSRAM space not enough for the Flash instructions" on boot:
sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n
sdkconfig.CONFIG_SPIRAM_RODATA=n
sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n
6 changes: 2 additions & 4 deletions Devices/lilygo-tdeck-plus/device.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.spiRamXipDisabled=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
Expand All @@ -24,7 +25,4 @@ lvgl.colorDepth=16

sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y

# Fix error "PSRAM space not enough for the Flash instructions" on boot:
sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n
sdkconfig.CONFIG_SPIRAM_RODATA=n
sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n

1 change: 1 addition & 0 deletions Devices/lilygo-tdeck/device.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.spiRamXipDisabled=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
Expand Down
6 changes: 1 addition & 5 deletions Devices/waveshare-esp32-s3-geek/device.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M
hardware.spiRamXipDisabled=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
Expand All @@ -22,8 +23,3 @@ display.dpi=143

lvgl.colorDepth=16
lvgl.uiDensity=compact

# Fix error "PSRAM space not enough for the Flash instructions" on boot:
sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n
sdkconfig.CONFIG_SPIRAM_RODATA=n
sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n
6 changes: 1 addition & 5 deletions Devices/waveshare-s3-touch-lcd-128/device.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M
hardware.spiRamXipDisabled=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
Expand All @@ -22,8 +23,3 @@ display.dpi=265

lvgl.colorDepth=16
lvgl.uiDensity=compact

# Fix error "PSRAM space not enough for the Flash instructions" on boot:
sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n
sdkconfig.CONFIG_SPIRAM_RODATA=n
sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n
Loading
Loading