This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
OpenShock ESP32 firmware — controls shock collars via reverse-engineered 433 MHz sub-1 GHz protocols. Dual-component project: C++20 embedded firmware + SvelteKit web frontend (served from LittleFS).
# Install Python build dependencies
pip install -r requirements.txt
# Build firmware for a specific board
pio run -e Wemos-D1-Mini-ESP32
# Build all board variants
pio run
# Upload firmware to connected board
pio run -e Wemos-D1-Mini-ESP32 -t upload
# Upload LittleFS filesystem (frontend) to connected board
pio run -e Wemos-D1-Mini-ESP32 -t uploadfs
# Serial monitor (115200 baud)
pio device monitor
# Static analysis
pio check -e ci-build
# Regenerate FlatBuffers headers from schemas/ submodule
python scripts/generate_schemas.py
# Frontend (from frontend/ directory)
pnpm install
pnpm run build # Production build (single-file HTML)
pnpm run lint # Prettier + ESLint
pnpm run check # Svelte type checking (svelte-check)Board environments are defined in platformio.ini. Common ones: Wemos-D1-Mini-ESP32, Wemos-Lolin-S3, OpenShock-Core-V2, Seeed-Xiao-ESP32S3, ci-build (for analysis).
C++: clang-format (.clang-format) — 2-space indent, 256 column limit, C++20, LF line endings. Functions use Allman braces; control statements use K&R (multi-line only). Pointers/references left-aligned (int* p).
Frontend: Prettier (.prettierrc) — 2-space indent, semicolons, trailing commas.
Python: Black with --line-length 120 --skip-string-normalization.
setup() → Config::Init() → Events::Init() → OtaUpdateManager::Init()
→ trySetup():
VisualStateManager → EStopManager → SerialInputHandler
→ CommandHandler → WiFiManager → GatewayConnectionManager
→ CaptivePortal
loop() → spawns FreeRTOS task "main_app" → deletes Arduino loop task
main_app() → continuous GatewayConnectionManager::Update()
On OTA boot: same init but rolls back on failure. On normal boot: restarts after 5s on failure.
Each major subsystem is a static namespace under OpenShock:: with Init() / Update() functions. No class instances — singletons via file-static variables. Example: OpenShock::WiFiManager::Init(), OpenShock::Config::GetRFConfig().
Source files define const char* const TAG = "ModuleName"; at the top for the logging system.
- Config (
src/config/,include/config/) — Thread-safe persistent storage on LittleFS. UsesReadWriteMutexwith RAII locks. Dual format: JSON (REST API) and FlatBuffers (binary storage). Sub-configs: WiFi, Backend, RF, OTA, Serial, CaptivePortal, EStop. - GatewayConnectionManager / GatewayClient — WebSocket connection to cloud backend. JSON for text messages, FlatBuffers for binary. Rate limiting on auth failures (5 min cooldown). Broadcasts
AccountLinkStatusEventwhen auth token is validated. - CommandHandler — Routes shocker commands to RF transmission. Protocols: Petrainer, CaiXianlin, etc.
- Radio (
src/radio/rmt/) — Uses ESP32 RMT peripheral for precise 433 MHz signal timing. - CaptivePortal — RFC-8908 compliant. Serves the SvelteKit frontend for device configuration. REST API endpoints for WiFi, account, GPIO, and OTA configuration. WebSocket for real-time events (scan results, network changes, connection status).
- OtaUpdateManager — Partition-based A/B updates with validation and rollback.
- WiFiManager / WiFiScanManager — Connection management with SSID-based connecting (no BSSID required). Supports hidden networks. Auth mode pinning prevents evil twin attacks. Optional BSSID pinning for advanced users.
- Serial (
src/serial/command_handlers/) — 17 UART commands for debugging/configuration. - TaskUtils (
src/util/TaskUtils.cpp) — FreeRTOS task creation helpers with core affinity.StopTask()for cooperative task shutdown with bounded timeout and force-kill fallback.
The captive portal has two interfaces:
- REST API (
/api/*) — HTTP endpoints for configuration actions (WiFi save/forget/connect, account link/unlink, GPIO pin changes, OTA settings). Responses use HTTP status codes for success/failure with JSON error bodies{"error":"..."}. Success with no data returns 200 with empty body. - WebSocket (port 81) — FlatBuffers binary protocol for real-time events (network discovery, scan status, connection/disconnection, account link status) and shocker commands (
ShockerCommandList).
Portal lifecycle:
- Opens when device is not fully configured (no WiFi credentials or no auth token)
- Stays open during setup regardless of WiFi/gateway connection state
- Closes via
/api/portal/close(soft signal — waits for gateway connection) orForceClose()(immediate) - Auto-closes AP after 5 minutes with no WebSocket clients when gateway is connected
- 30-second startup grace period for already-configured devices
Gateway WebSocket → message_handlers/websocket/gateway/ → command dispatch
Local WebSocket → message_handlers/websocket/local/ → config/control
Serial UART → serial/command_handlers/ → debug/config
REST HTTP → captiveportal/CaptivePortalInstance → config/control
Shocker command processing is shared between gateway and local WebSocket handlers via message_handlers/ShockerCommandList.cpp.
Serialization adapters in src/serialization/: JsonAPI, JsonSerial, WSGateway, WSLocal.
Svelte 5 SPA built as a single-file HTML (vite-plugin-singlefile) and served from LittleFS.
src/
App.svelte — Root: routing between Landing, Guided, Advanced, Success views
lib/
views/ — Page-level components (Landing, Guided, Advanced, Success)
components/ — Reusable UI components
steps/ — Wizard step components (HardwareStep, WiFiStep, TestStep, AccountStep)
sections/ — Advanced mode section components
ui/ — shadcn-svelte primitives (button, input, dialog, stepper, etc.)
Layout/ — Header, Footer
stores/ — Svelte 5 reactive state (HubStateStore, ViewModeStore, ColorScheme)
MessageHandlers/ — WebSocket binary message handlers
api.ts — REST API client functions
_fbs/ — Generated FlatBuffers TypeScript bindings
mappers/ — Config data mapping from FlatBuffers to TS types
State management uses Svelte 5 $state/$derived runes. Note: Map mutations require reassignment for reactivity (create a new Map).
FreeRTOS tasks with explicit core affinity. TaskUtils::TaskCreateExpensive() avoids the WiFi core. Thread safety via SimpleMutex (binary semaphore) and ReadWriteMutex with ScopedReadLock/ScopedWriteLock RAII guards.
include/Chipset.h defines unsafe pins (strapping, UART, SPI flash) per ESP32 variant. Runtime validation prevents configuring dangerous pins. Board-specific GPIO assignments are compile-time defines (OPENSHOCK_LED_GPIO, OPENSHOCK_RF_TX_GPIO, OPENSHOCK_ESTOP_PIN).
Schemas live in the schemas/ submodule (tracks github.com/OpenShock/flatbuffers-schemas). The sibling repo at ../flatbuffers-schemas/ is for making schema changes — edit there, copy to the submodule, then regenerate:
cp ../flatbuffers-schemas/HubConfig.fbs schemas/
python scripts/generate_schemas.pyGenerated C++ headers go to include/serialization/_fbs/, TypeScript bindings to frontend/src/lib/_fbs/.
Key schema files:
HubConfig.fbs— Persistent configuration (WiFi credentials with auth mode/BSSID pinning, RF, EStop, OTA)HubToLocalMessage.fbs— Events sent from firmware to frontend (ReadyMessage, WiFi events, AccountLinkStatusEvent)LocalToHubMessage.fbs— Commands from frontend to firmware (ShockerCommandList)GatewayToHubMessage.fbs— Commands from cloud backend to firmwareCommon/ShockerCommand.fbs— Shared shocker command types used by both local and gateway
Arduino → ESP-IDF: We are actively migrating away from the Arduino framework toward native ESP-IDF. When touching code that uses Arduino APIs, prefer replacing them with ESP-IDF equivalents where practical. Avoid introducing new Arduino dependencies.
Per-board → Per-chip builds: The current per-board build matrix (each board is a separate PlatformIO environment) is being phased out in favor of per-chip compiles (ESP32, ESP32-S2, ESP32-S3, ESP32-C3) with runtime or config-time pin assignment. This reduces CI/CD load and simplifies maintenance. When making changes, prefer chip-level abstractions over board-specific #ifdefs and avoid adding new per-board environments.
WS → REST: Local WebSocket commands are being migrated to HTTP REST endpoints on the captive portal. The WebSocket channel is reserved for real-time events and shocker commands (binary FlatBuffers). New configuration endpoints should be REST, not WS.
- C++20 with
-fno-exceptions— use return values for error handling, not try/catch - Logging:
OS_LOGI(TAG, "format %s", arg)— levels:OS_LOGV/D/I/W/E, panics:OS_PANIC/OS_PANIC_OTA/OS_PANIC_INSTANT - Macros from
Common.h:DISABLE_COPY(T),DISABLE_MOVE(T),DISABLE_DEFAULT(T) - HTTP content types: use
HTTP::ContentType::JSON,TextPlain,TextHTMLfrominclude/http/ContentTypes.h - REST error responses:
{"error":"ErrorCode"}with appropriate HTTP status. Success with no data: 200 empty body. Usestatic const char*constants for repeated JSON error strings. - Dynamic JSON in REST handlers: use
cJSON(ESP-IDF built-in), not ArduinoStringconcatenation - Environment variables embedded at build time via
scripts/embed_env_vars.py(see.env,.env.development,.env.production) - FlatBuffers schemas live in
schemas/submodule; generated headers go toinclude/serialization/_fbs/ - Custom board JSON definitions in
boards/, partition tables inchips/ - Libraries are pinned to specific git commits in
platformio.inilib_deps - Frontend: Svelte 5 runes (
$state,$derived,$effect). Avoid<script module>unless exporting types. UseComponent<any>fromsveltefor dynamic component types (not lucide'sIcontype).