Skip to content

Latest commit

 

History

History
82 lines (61 loc) · 8.43 KB

File metadata and controls

82 lines (61 loc) · 8.43 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this repo is

A desktop simulator for CrossPoint firmware. It is not a standalone app, it ships as a PlatformIO library that downstream firmware adds as a lib_dep (named simulator) and builds with platform = native and -DSIMULATOR. The result is the firmware compiled as a host binary, with the e-ink display rendered into an SDL2 window.

There is no build target inside this repo. Build and run happen in the consuming firmware project. See README.md for end-user setup, and .claude/CONTEXT-sim-notes.md for the deep architecture notes and bug-fix history (read this before non-trivial changes).

Build and run (from the consuming firmware repo)

pio run -e simulator -t run_simulator   # build + launch
pio run -e simulator                    # build only, then .pio/build/simulator/program
rm -rf ./fs_/.crosspoint/               # clear stale on-disk caches after storage/cache changes

For local dev against this repo, the firmware's platformio.ini should reference it as simulator=symlink://../crosspoint-simulator instead of the git URL.

There are no tests, no linter, and no per-file build commands. A change is "tested" by running the simulator and exercising the affected feature.

Architecture

The simulator is a collection of host-side reimplementations of the firmware's hardware abstraction layer (HAL) and its Arduino/ESP-IDF dependencies. Each Hal*.cpp/.h here corresponds to a Hal* class in the firmware's lib/hal/, and must keep the same public surface or the firmware will not link.

The HAL stub rule. When the firmware adds a new method to a HAL class and calls it, the simulator fails to link until a matching stub is added to the corresponding Hal*.cpp here. Most additions are one-line no-ops. This is the single most common reason a simulator build breaks after pulling firmware updates.

Why the simulator's design has the shape it does (the non-obvious parts):

  • SDL on main thread. macOS requires all SDL calls to come from the main thread, but firmware drives rendering from a FreeRTOS render task. The split lives in src/HalDisplay.cpp: refreshDisplay (background thread) converts the 1bpp framebuffer to ARGB and sets an atomic pendingPresent flag. presentIfNeeded (called from simulator_main on the main thread) does the actual SDL upload and present. Do not call SDL render functions from anywhere else.
  • Orientation rotation lives in two places. The firmware's renderer rotates content into the landscape framebuffer (90 CCW for Portrait). The simulator undoes that with SDL_RenderCopyEx. If you change one, change the other. The dst rect is landscape-shaped and centre-offset because SDL_RenderCopyEx rotates around the dst centre.
  • HiDPI / dithering. Set SDL_HINT_RENDER_SCALE_QUALITY=1 before SDL_CreateTexture, plus SDL_WINDOW_ALLOW_HIGHDPI and SDL_RenderSetLogicalSize. Without all three, Bayer-dithered grays render as harsh black/white stripes on Retina.
  • POSIX fds, not std::fstream, in src/HalStorage.cpp. This was a deliberate rewrite. fstream's separate get/put pointers, eofbit-blocks-seek behaviour, and write-only seek restrictions caused several silent-corruption bugs. Do not reintroduce fstream here. All paths are prefixed with ./fs_ so the simulated filesystem stays sandboxed under the binary's working directory; /books/ on the SD card maps to ./fs_/books/. Directory iteration skips only the special . and .. entries; firmware applies its own hidden-file policy.
  • FreeRTOS shim. src/freertos/ maps xTaskCreate to std::thread, task notifies to a condvar + counter, and SemaphoreHandle_t to std::recursive_mutex. A thread_local SimTaskHandle* lets each task thread find its own handle.
  • _exit(0) not return 0. src/simulator_main.cpp ends with _exit(0) after SDL_Quit() to skip C++ global destructors. The render task is [[noreturn]], so running destructors while it is mid-render races and produces a "quit unexpectedly" dialog. Keep this.
  • Time uses steady_clock. millis() / micros() in src/Arduino.h deliberately use steady_clock, not system_clock, so wall-clock changes do not perturb timing.

Host-specific code paths:

  • MD5: src/MD5Builder.h is a thin dispatcher that auto-selects the implementation via #ifdef __APPLE__ / #elif __linux__. src/MD5Builder_mac.h uses CommonCrypto; src/MD5Builder_linux.h uses OpenSSL. No downstream swapping is needed - just include MD5Builder.h.
  • Web server shims: src/WebServer.cpp, src/WebSocketsServer.cpp, and src/NetworkClient.cpp expose firmware port 80 as http://127.0.0.1:8080/ and port 81 WebSockets as ws://127.0.0.1:8081/. CROSSPOINT_SIM_HTTP_PORT moves the pair together when either port is occupied. Current CrossPoint builds compile their firmware-owned CrossPointWebServer.cpp and WebDAVHandler.cpp against these shims; CROSSPOINT_SIMULATOR_PROJECT_WEBSERVER disables only the legacy reduced substitute in this library.
  • Build flags: macOS gets architecture-correct SDL compiler and linker flags from sdl2-config, so the same sample works on Intel and Apple Silicon. Linux/WSL additionally links OpenSSL with -lssl -lcrypto -Wno-deprecated-declarations (OpenSSL 3.x deprecates MD5_*). See sample-platformio-macos.ini and sample-platformio-linux-wsl.ini. Keep both in sync when build flags change. Native Windows is not supported, WSL is.
  • Linker stubs: src/firmware_link_stubs.cpp provides symbols the firmware expects from other translation units (uzlib checksums, HWCDC Serial shim, LUT stubs). When the firmware adds a new global-extern symbol with no simulator counterpart, add its stub here.

Device profiles and input mapping

src/BoardConfig.h selects X4 by default, SIMULATOR_DEVICE_X3 for X3, SIMULATOR_DEVICE_X4_PRO for X4 Pro, and SIMULATOR_DEVICE_STICKY for Seeed Sticky. SIMULATOR_DISPLAY_UC8179 and SIMULATOR_DISPLAY_UC8279 select per-batch controller revisions without changing a device's geometry or capabilities. Keep the reported board and controller aligned with the firmware SDK. X4 Pro uses the same 800x480 display geometry as X4 but adds touch, a capacitive Home key, frontlight state, inversion, and an RTC. Sticky also uses 800x480 and adds touch, RTC, and tilt without a Home key or frontlight.

HalGPIO::update owns the SDL event pump for the whole simulator, do not poll SDL events elsewhere. Scancodes map to button indices BTN_BACK=0 through BTN_POWER=6. SDL_QUIT sets the quitRequested atomic that HalDisplay::shouldQuit() reads.

For repeatable QA, CROSSPOINT_SIM_INPUT_SCRIPT schedules synthetic key and touch-device edges through the same HalGPIO state as real SDL input, and CROSSPOINT_SIM_SCREENSHOTS captures renderer output on the SDL main thread. Keep synthetic held-time timestamps on the SDL_GetTicks() clock used by real keyboard events; the firmware's millis() clock has a different origin. The deep-sleep loop must also process synthetic input. Process relaunch promotes the optional *_AFTER_WAKE schedules and clears the pre-sleep schedules so automation cannot enter an infinite sleep/relaunch cycle.

When making changes

  • Adding a new HAL method? Mirror the firmware signature exactly and stub it (usually no-op) in the matching Hal*.cpp/.h. Do not invent new public methods that don't exist in the firmware HAL.
  • Adding a new Arduino/ESP-IDF symbol? Add the minimum stub to the corresponding header in src/ (e.g. src/WiFi.h, src/Arduino.h). Match the upstream signature, return a sensible default.
  • Touching storage or caching code? After the change, rm -rf ./fs_/.crosspoint/ in the firmware project before re-running, otherwise stale caches built by the old code will mask the fix.
  • Touching display, threading, or shutdown? Re-read the "Why the simulator's design has the shape it does" section above first. Several of those decisions undo subtle bugs that will resurface if reverted.