@polderlabs/bizar 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/cli/audit.mjs +144 -0
  4. package/cli/banner.mjs +41 -0
  5. package/cli/bin.mjs +186 -0
  6. package/cli/copy.mjs +508 -0
  7. package/cli/export.mjs +87 -0
  8. package/cli/init.mjs +147 -0
  9. package/cli/install.mjs +390 -0
  10. package/cli/plan-templates.mjs +523 -0
  11. package/cli/plan.mjs +2087 -0
  12. package/cli/prompts.mjs +163 -0
  13. package/cli/update.mjs +273 -0
  14. package/cli/utils.mjs +153 -0
  15. package/config/AGENTS.md +282 -0
  16. package/config/agents/baldr.md +148 -0
  17. package/config/agents/forseti.md +112 -0
  18. package/config/agents/frigg.md +101 -0
  19. package/config/agents/heimdall.md +157 -0
  20. package/config/agents/hermod.md +144 -0
  21. package/config/agents/mimir.md +115 -0
  22. package/config/agents/odin.md +309 -0
  23. package/config/agents/quick.md +78 -0
  24. package/config/agents/semble-search.md +44 -0
  25. package/config/agents/thor.md +97 -0
  26. package/config/agents/tyr.md +96 -0
  27. package/config/agents/vidarr.md +100 -0
  28. package/config/agents/vor.md +140 -0
  29. package/config/commands/audit.md +1 -0
  30. package/config/commands/explain.md +1 -0
  31. package/config/commands/init.md +1 -0
  32. package/config/commands/learn.md +1 -0
  33. package/config/commands/pr-review.md +1 -0
  34. package/config/commands/tailscale-serve.md +96 -0
  35. package/config/hooks/README.md +29 -0
  36. package/config/hooks/post-tool-use.md +16 -0
  37. package/config/hooks/pre-tool-use.md +16 -0
  38. package/config/opencode.json +52 -0
  39. package/config/opencode.json.template +52 -0
  40. package/config/rules/general.md +8 -0
  41. package/config/rules/git.md +11 -0
  42. package/config/rules/javascript.md +10 -0
  43. package/config/rules/python.md +10 -0
  44. package/config/rules/testing.md +10 -0
  45. package/config/skills/bizar/README.md +9 -0
  46. package/config/skills/bizar/SKILL.md +187 -0
  47. package/config/skills/cpp-coding-standards/README.md +28 -0
  48. package/config/skills/cpp-coding-standards/SKILL.md +634 -0
  49. package/config/skills/cpp-coding-standards/agents/openai.yaml +4 -0
  50. package/config/skills/cpp-coding-standards/references/concurrency.md +320 -0
  51. package/config/skills/cpp-coding-standards/references/error-handling.md +229 -0
  52. package/config/skills/cpp-coding-standards/references/memory-safety.md +216 -0
  53. package/config/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
  54. package/config/skills/cpp-coding-standards/references/review-checklist.md +96 -0
  55. package/config/skills/cpp-testing/README.md +28 -0
  56. package/config/skills/cpp-testing/SKILL.md +304 -0
  57. package/config/skills/cpp-testing/agents/openai.yaml +4 -0
  58. package/config/skills/cpp-testing/references/coverage.md +370 -0
  59. package/config/skills/cpp-testing/references/framework-compare.md +175 -0
  60. package/config/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
  61. package/config/skills/cpp-testing/references/mocking.md +364 -0
  62. package/config/skills/cpp-testing/references/tdd-workflow.md +308 -0
  63. package/config/skills/embedded-esp-idf/README.md +41 -0
  64. package/config/skills/embedded-esp-idf/SKILL.md +439 -0
  65. package/config/skills/embedded-esp-idf/agents/openai.yaml +4 -0
  66. package/config/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
  67. package/config/skills/embedded-esp-idf/references/host-tests.md +164 -0
  68. package/config/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
  69. package/config/skills/embedded-esp-idf/references/kconfig.md +159 -0
  70. package/config/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
  71. package/config/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
  72. package/config/skills/embedded-esp-idf/references/nvs.md +121 -0
  73. package/config/skills/embedded-esp-idf/references/packed-structs.md +192 -0
  74. package/config/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
  75. package/config/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
  76. package/config/skills/self-improvement/SKILL.md +64 -0
  77. package/package.json +47 -0
  78. package/templates/plan/htmx.min.js +1 -0
  79. package/templates/plan/library/bug-investigation.mdx +79 -0
  80. package/templates/plan/library/decision-record.mdx +71 -0
  81. package/templates/plan/library/feature-design.mdx +92 -0
  82. package/templates/plan/meta.json.template +8 -0
  83. package/templates/plan/plan.canvas.template +1711 -0
  84. package/templates/plan/plan.html.template +937 -0
  85. package/templates/plan/plan.mdx.template +46 -0
@@ -0,0 +1,439 @@
1
+ ---
2
+ name: embedded-esp-idf
3
+ description: Write, review, or debug ESP-IDF v5.x C++ firmware. Triggers on idf.py build/flash/monitor/menuconfig/size, FreeRTOS tasks and ISRs, IRAM/DRAM/PSRAM budgeting, packed binary protocols with static_assert on sizeof, NVS, BLE/NimBLE, ESP-NOW, deep-sleep/light-sleep, Kconfig option design, or host-side C++ unit tests that compile firmware headers without idf.py. AMS7 ambulatory-monitoring firmware conventions (SD-first storage, low-rate aggregate logs, CONFIG_AMS7_* options, ESP-NOW framing discipline) are flagged as project-specific extensions.
4
+ ---
5
+
6
+ # Embedded ESP-IDF
7
+
8
+ ESP-IDF v5.x C++ firmware patterns: build/flash/monitor workflow, FreeRTOS, IRAM/DRAM memory model, packed binary protocols, drivers, and host-side testing without idf.py. Universal ESP-IDF rules are the default. Anything tagged **(AMS7)** is a project-specific extension for the `/projects/ams7_esp32` ambulatory-monitoring firmware — do not silently generalize it.
9
+
10
+ ## Quick start
11
+
12
+ ```bash
13
+ # 1. Source the ESP-IDF environment (required once per shell)
14
+ source esp/esp-idf/export.sh
15
+
16
+ # 2. Build / flash / monitor
17
+ idf.py build # compile firmware
18
+ idf.py -p /dev/ttyUSB0 flash # flash (Linux; COMx on Windows)
19
+ idf.py -p /dev/ttyUSB0 monitor # serial monitor (Ctrl-] to exit)
20
+
21
+ # 3. Configure / inspect
22
+ idf.py menuconfig # TUI Kconfig editor
23
+ idf.py size # IRAM/DRAM/flash breakdown (text)
24
+ idf.py size --format json # machine-readable for tooling
25
+ ```
26
+
27
+ `idf.py` is the single entry point: build, flash, monitor, menuconfig, size, clean, full-clean, set-target, partition-table, and OTA-related commands are all subcommands. See `references/idf-py-commands.md` for the full list and exit codes.
28
+
29
+ A helper that validates your environment is at `scripts/idf_env.sh`:
30
+
31
+ ```bash
32
+ bash scripts/idf_env.sh # prints the source line and verifies idf.py is reachable
33
+ ```
34
+
35
+ ## Project layout (typical ESP-IDF + AMS7)
36
+
37
+ ```
38
+ project-root/
39
+ ├── CMakeLists.txt # top-level: project(<name>), C++ standard, EXTRA_COMPONENT_DIRS
40
+ ├── sdkconfig # PRIMARY build configuration (Kconfig output)
41
+ ├── sdkconfig.<profile> # SECONDARY overlays (per-flow variants) — AMS7 uses these
42
+ ├── partitions.csv # flash partition table
43
+ ├── main/ # app component (idf_component_register lives here)
44
+ │ ├── CMakeLists.txt # main component sources and INCLUDE_DIRS
45
+ │ ├── Kconfig # app-level CONFIG_* options
46
+ │ ├── ams7_esp32.cpp # app_main(), task creation, system bring-up
47
+ │ └── ...
48
+ ├── components/ # first-party shared components
49
+ ├── module/ # submodules vendored into the build (set EXTRA_COMPONENT_DIRS)
50
+ ├── managed_components/ # populated by `idf.py add-dependency` (idf-component-manager)
51
+ ├── docs/ # design notes, memory budget, runbooks
52
+ └── tools/ # project-specific helpers (size budget, etc.)
53
+ ```
54
+
55
+ `(AMS7)` In the AMS7 project the top-level `CMakeLists.txt` sets:
56
+
57
+ ```cmake
58
+ set(EXTRA_COMPONENT_DIRS "./module") # pull ./module/* into the component search path
59
+ set(CMAKE_CXX_STANDARD 20) # C++20 across all components
60
+ set(PROJECT_VER "beta.studio.core.v1")
61
+ include($ENV{IDF_PATH}/tools/cmake/project.cmake)
62
+ project(ams7_esp32)
63
+ ```
64
+
65
+ ## Build profiles
66
+
67
+ `sdkconfig` is the primary, always-applied configuration. Project overlays under `sdkconfig.<name>` are layered on top for variant builds:
68
+
69
+ ```bash
70
+ # (AMS7) Build a flow-specific firmware variant in a separate build directory
71
+ idf.py -B build_espnow_flow -C . \
72
+ --sdkconfig sdkconfig.espnow_flow \
73
+ build flash
74
+ ```
75
+
76
+ `(AMS7)` AMS7 ships `sdkconfig.espnow_flow`, `sdkconfig.espnow_flow_minadv`, `sdkconfig.espnow_flow_studylock`. These toggle `CONFIG_AMS7_*` options (e.g., `CONFIG_AMS7_SUMMARY_ENABLE=y`, `CONFIG_AMS7_ESPNOW_WIFI_CHANNEL=6`) without touching the primary `sdkconfig`. Never hand-edit `sdkconfig` for flow-specific tweaks — add a profile file and merge with `--sdkconfig`.
77
+
78
+ ## Kconfig
79
+
80
+ Add options under `main/Kconfig` (or a component's `Kconfig` file). ESP-IDF auto-sources any `Kconfig` file inside a registered component.
81
+
82
+ ```kconfig
83
+ config AMS7_SUMMARY_ENABLE
84
+ bool "Enable ESP-NOW summary workflow"
85
+ default n
86
+ help
87
+ Master switch for the recording workflow that starts with BLE control
88
+ and then switches to ESP-NOW summary mode after acquisition begins.
89
+
90
+ config AMS7_BLE_VITALS_ENABLE
91
+ bool "Enable custom BLE vitals characteristic"
92
+ depends on AMS7_VITALS_ENABLE
93
+ default y
94
+
95
+ config AMS7_HR_HOLD_MS
96
+ int "Hold time for fused HR before fallback"
97
+ depends on AMS7_VITALS_ENABLE
98
+ range 0 60000
99
+ default 10000
100
+ ```
101
+
102
+ Key operators:
103
+
104
+ - `bool` / `int` / `string` / `hex` — value type
105
+ - `default y|n|"value"` — default when not explicitly set
106
+ - `depends on FOO` — option only visible when `FOO` is set
107
+ - `select BAR` — when this option is enabled, force `BAR = y`
108
+ - `imply BAR` — soft suggestion; user can still disable
109
+ - `range MIN MAX` — integer range constraint
110
+ - `choice ... endchoice` — radio group with mutually-exclusive values
111
+
112
+ `(AMS7)` AMS7 namespacing uses the `AMS7_*` prefix on every project-defined symbol. Treat `CONFIG_AMS7_*` as AMS7-specific — do not propose `CONFIG_AMS7_*` symbols for a different ESP-IDF project.
113
+
114
+ See `references/kconfig.md` for menus, sourcing custom Kconfig files, and component-level isolation.
115
+
116
+ ## Component CMakeLists.txt
117
+
118
+ Each component has a `CMakeLists.txt` with one call to `idf_component_register`:
119
+
120
+ ```cmake
121
+ idf_component_register(
122
+ SRCS
123
+ "ams7fileheader.cpp"
124
+ "ams7fileheader.h"
125
+ INCLUDE_DIRS
126
+ "."
127
+ REQUIRES # public link deps (visible in component.h)
128
+ nvs_flash
129
+ esp_wifi
130
+ PRIV_REQUIRES # private link deps (only used in component .cpp)
131
+ mbedtls
132
+ app_update
133
+ )
134
+ ```
135
+
136
+ - `SRCS` — sources for this component.
137
+ - `INCLUDE_DIRS` — headers exported to dependents.
138
+ - `REQUIRES` — other components whose public headers are used here (public dependency).
139
+ - `PRIV_REQUIRES` — used only in `.cpp` (private dependency; smaller rebuild blast radius).
140
+
141
+ `(AMS7)` The main component in AMS7 (`main/CMakeLists.txt`) registers ~50 sources and lists ~25 components under `REQUIRES` (drivers, BLE, NVS, OTA, etc.). Keep this list explicit — do not switch to glob patterns.
142
+
143
+ ## FreeRTOS patterns
144
+
145
+ ESP-IDF uses FreeRTOS. The universal rules:
146
+
147
+ - **Tasks**: `xTaskCreatePinnedToCore` to pin to core 0 or 1. App/main runs on the PRO CPU (core 1) by default; pin acquisition ISRs and time-critical tasks deliberately.
148
+ - **Priorities**: 0 is idle. 1–4 normal tasks. Real-time acquisition can use 5–24. `(AMS7)` AMS7 acquisition tasks run at priority 20+.
149
+ - **Blocking calls**: `vTaskDelay(pdMS_TO_TICKS(N))`, `xQueueReceive(..., portMAX_DELAY)`.
150
+ - **Queues**: prefer a queue over a shared buffer. Producer/consumer with `xQueueSend` / `xQueueReceive`.
151
+ - **Semaphores / mutexes**: binary/counting semaphores for ISR-to-task signaling; mutexes for shared resources. Prefer `xSemaphoreCreateBinary` and give it from `xQueueSendFromISR`/`xTaskNotifyFromISR`.
152
+ - **Timers**: `xTimerCreate` for periodic callbacks. One-shot timers for deferred work.
153
+ - **Event groups**: `xEventGroupWaitBits` for "wait until several subsystems ready" patterns.
154
+ - **Stream/ring buffers**: `xStreamBufferCreate` for variable-length byte streams; `xRingbufferCreate` for the IDF lockless variant.
155
+ - **ISR rules**: never block in an ISR. Use `xQueueSendFromISR`, `xTaskNotifyFromISR`, or `xTimerPendFunctionCallFromISR` and check the returned `BaseType_t xHigherPriorityTaskWoken` to force a context switch with `portYIELD_FROM_ISR`.
156
+
157
+ `portTICK_PERIOD_MS` is `10` by default (10 ms tick). Always convert via `pdMS_TO_TICKS(ms)` rather than multiplying by hand.
158
+
159
+ See `references/freertos-patterns.md` for task lifecycle, pinning to cores, watchdog setup, and ISR-to-task handoff patterns.
160
+
161
+ ## Memory model
162
+
163
+ ESP32 (Xtensa LX6/LX7, or RISC-V on S3/C3) has distinct address spaces:
164
+
165
+ - **IRAM** (instruction RAM, ~128 KB on classic ESP32): code marked with `IRAM_ATTR` lives here. Used for ISR handlers, flash-cache-disabled paths, and time-critical code. **Tight budget.**
166
+ - **DRAM** (data RAM, ~120 KB on classic ESP32): `.data`, `.bss`, heap, stack. Default for all variables and most code.
167
+ - **Flash** (4–16 MB): program text, read-only data (`.rodata`), filesystem. Code lives here by default; XIP cache fetches it.
168
+ - **PSRAM** (when chip has it): slow external RAM via SPI. Use `MALLOC_CAP_SPIRAM` for large buffers you cannot fit in DRAM.
169
+
170
+ Universal guidance:
171
+
172
+ - **Prefer `static` over heap.** DRAM fragmentation on ESP32 is severe; long-running firmware that `malloc`s in a loop will eventually fail.
173
+ - **Mark only what must run from IRAM**: ISR handlers, flash-cache-disabled paths (e.g., SPI drivers during DMA), and a few hot loops. Most code is fine in flash.
174
+ - **`MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT`** for buffers that must not touch PSRAM (DMA-capable memory).
175
+ - **`MALLOC_CAP_SPIRAM`** for big buffers that need not be DMA-capable (logs, JSON caches).
176
+ - **`heap_caps_print_heap_info(MALLOC_CAP_8BIT)`** in a debug build to confirm.
177
+
178
+ `(AMS7)` **IRAM is the tightest budget on AMS7.** Reference baseline (see `docs/firmware/memory_budget.md`):
179
+
180
+ ```
181
+ IRAM: 108,726 used, 22,346 free, 131,072 total
182
+ DRAM: 43,152 used, 81,428 free, 124,580 total
183
+ ```
184
+
185
+ Keep IRAM remaining ≥ 20 KiB; below 24 KiB prints a warning because new `IRAM_ATTR` code can hurt quickly. Do not add `IRAM_ATTR` unless the function is required in ISR/cache-disabled paths. Always re-run the size check after acquisition-path changes.
186
+
187
+ See `references/memory-and-iram.md` for IRAM_ATTR placement rules, PSRAM trade-offs, and the things that blow the IRAM budget (logging, format strings in IRAM, ISR deep paths).
188
+
189
+ ## Packed binary protocols
190
+
191
+ Wire-format frames over ESP-NOW, BLE, or any byte-oriented transport must be deterministic in size. AMS7 uses the same shape across all frames — see `main/connectivity/espnow_imu_frame.hpp` for a clean example:
192
+
193
+ ```cpp
194
+ #define CORE_ESPNOW_IMU_MAGIC 0x494dU // 'IM'
195
+ #define CORE_ESPNOW_IMU_VERSION 1U
196
+ #define CORE_ESPNOW_FRAME_IMU_RAW 1U
197
+
198
+ typedef struct __attribute__((packed)) {
199
+ uint16_t magic; // first 2 bytes identify the frame family
200
+ uint8_t version; // protocol version of this struct
201
+ uint8_t frame_type; // sub-type within family
202
+ uint16_t id; // sender / subject
203
+ uint16_t seq; // monotonic sequence
204
+ uint16_t age_ms; // capture age in ms (sender-stamped)
205
+ int16_t accel_x, accel_y, accel_z;
206
+ int16_t gyro_x, gyro_y, gyro_z;
207
+ } core_espnow_imu_frame_v1_t;
208
+
209
+ // Compile-time size lock — breaks the build if the struct grows or shrinks.
210
+ static_assert(sizeof(core_espnow_imu_frame_v1_t) == 22,
211
+ "core_espnow_imu_frame_v1_t must be 22 bytes");
212
+ ```
213
+
214
+ Universal rules:
215
+
216
+ - Always `__attribute__((packed))` and always back it with `static_assert(sizeof(T) == N)` (or `_Static_assert` in C linkage). Catch drift at compile time, not over the air.
217
+ - Fixed-width types only (`uint16_t`, `int16_t`, not `int`).
218
+ - First fields are `magic` (16-bit ASCII-ish) + `version` + `frame_type`. Magic is how receivers reject foreign frames; version is how they migrate.
219
+ - All multi-byte fields are **little-endian on the wire** — ESP32 is LE natively, but make byte order explicit when porting.
220
+ - Wire framing is **length-prefixed**: receiver gets a 2-byte length, then exactly that many bytes. No delimiter-scanning, no escape bytes.
221
+ - For variable payloads, define `frame_header_t` once and reuse — don't reinvent per frame family.
222
+
223
+ `(AMS7)` The naming convention `core_<transport>_<family>_frame_vN_t` and the `static_assert` on `sizeof` are non-negotiable in AMS7. Every frame header under `main/connectivity/` follows this pattern (imu, resp, resp_icg, beat_event, capability, name, protocol_status, gateway_command, marker).
224
+
225
+ See `references/packed-structs.md` for the full pattern, capability bits, and migration across versions.
226
+
227
+ ## Logging
228
+
229
+ ```cpp
230
+ static const char *TAG = "ams7driver";
231
+ ESP_LOGI(TAG, "Start acquisition stack_hw=%lu", (unsigned long)stack_hw);
232
+ ESP_LOGW(TAG, "Skipping channel-info drain wait for device without ringbuffer");
233
+ ESP_LOGE(TAG, "Failed to start stop finalization task");
234
+ ```
235
+
236
+ Universal guidance:
237
+
238
+ - Use `ESP_LOGE` for failures, `ESP_LOGW` for recoverable issues, `ESP_LOGI` for state transitions, `ESP_LOGD` for verbose debug (off by default), `ESP_LOGV` for trace.
239
+ - Pick a stable per-module tag — file-scoped `static const char *TAG = "modname";`.
240
+ - Format strings are validated against the args at compile time — `%lu` for `unsigned long`, `%" PRIu32 "` for `uint32_t`, etc.
241
+ - Gating with `CONFIG_*_LOG_ENABLE` switches lets you build a release binary with no debug logs at all (`idf.py menuconfig → Component config → Log output → Default log verbosity`).
242
+
243
+ `(AMS7)` **Logging discipline.** Acquisition builds must not log per-sample or per-frame — at 125–500 Hz that is unprintable and useless. Acceptable log rates:
244
+
245
+ - Per-event: BLE connect, ESP-NOW peer add, acquisition start/stop, file open/close.
246
+ - Periodic aggregate: low-rate counters via a `pl:` console line every N ms (e.g., `ESP_LOGI(TAG, "pl: beats=%lu rr_ms=%lu q=%u", ...)`).
247
+ - Trial-debug toggles (e.g., `CONFIG_AMS7_RESP_TRIAL_DEBUG_LOG_ENABLE=y`) gate extra lines.
248
+
249
+ For full AMS7 logging rules see `references/logging-discipline.md`.
250
+
251
+ ## Drivers
252
+
253
+ ESP-IDF v5 unified drivers (used by AMS7 modules):
254
+
255
+ - **I2C**: `i2c_master_bus_config_t` + `i2c_master_bus_handle_t`, then per-device `i2c_device_config_t`. `i2c_master_transmit`, `i2c_master_receive`, `i2c_master_transmit_receive`.
256
+ - **SPI**: `spi_bus_initialize` + `spi_device_interface_config_t` + `spi_device_queue_transfer`. Use `IRAM_ATTR` on `queueTransfer`/`getTransferResult` for DMA-completion paths.
257
+ - **GPIO**: `gpio_config`, `gpio_set_level`, `gpio_install_isr_service` (zero-arg ISR service install once at boot, then `gpio_isr_handler_add`).
258
+ - **ADC**: `esp_adc/adc_oneshot.h` for one-shot reads; `esp_adc/adc_continuous.h` for DMA-driven streams.
259
+ - **RTC**: `esp_sleep_enable_timer_wakeup`, `esp_sleep_enable_ext0_wakeup` (single GPIO), `esp_sleep_enable_ext1_wakeup` (multiple GPIOs), `esp_deep_sleep_start`.
260
+ - **NVS**: see next section.
261
+ - **BLE**: NimBLE (`nimble/`), or the Bluedroid stack. AMS7 uses NimBLE via `components/bt`.
262
+ - **Wi-Fi / ESP-NOW**: `esp_wifi_set_mode(WIFI_MODE_STA)`, `esp_wifi_set_promiscuous`, `esp_wifi_set_channel`, then `esp_now_init`, `esp_now_register_send_cb`, `esp_now_send`.
263
+
264
+ ## NVS (preferences)
265
+
266
+ `nvs_flash_init()` once at boot. Per-namespace open/get/set/commit:
267
+
268
+ ```cpp
269
+ nvs_handle_t h;
270
+ esp_err_t err = nvs_open("storage", NVS_READWRITE, &h);
271
+ if (err != ESP_OK) { /* handle */ }
272
+
273
+ uint8_t mac[6] = {};
274
+ size_t len = sizeof(mac);
275
+ err = nvs_get_blob(h, "peer_mac", mac, &len);
276
+
277
+ uint32_t counter = 0;
278
+ nvs_set_u32(h, "boot_count", counter + 1);
279
+ nvs_commit(h);
280
+ nvs_close(h);
281
+ ```
282
+
283
+ - Namespaces (`"storage"`, `"wifi"`, `"ams7cfg"`) isolate unrelated settings.
284
+ - Blobs for binary data; `nvs_set_str` / `nvs_get_str` for short strings; `nvs_set_u8/u16/u32/u64/i32` for integers.
285
+ - `nvs_commit` is required after writes; reads do not need commit.
286
+ - Flash wear is real — prefer batched writes. Use a memory cache and flush on shutdown or every N minutes.
287
+
288
+ `(AMS7)` AMS7 stores device-name, hardware-rev, last-acquisition metadata in NVS under the `"ams7cfg"` namespace, and persists peer / study-locks state.
289
+
290
+ For NVS review tasks (most common: catching `nvs_flash_init()` anti-patterns, swallowed errors, missing commits, wrong namespaces), see `references/nvs.md`. NVS issues are the highest-yield review finding for non-driver firmware code, so this reference is worth loading early.
291
+
292
+ ## Power management
293
+
294
+ - `esp_sleep_enable_timer_wakeup(seconds)` for timed wakes.
295
+ - `esp_sleep_enable_ext0_wakeup(gpio_num, level)` for one external wake pin.
296
+ - `esp_sleep_enable_ext1_wakeup(bitmask, mode)` for multiple pins (EXT1 only on classic ESP32; on S3 use `gpio_wakeup`).
297
+ - `esp_deep_sleep_start()` — full power-off; only RTC fast memory survives (`RTC_DATA_ATTR` / `RTC_IRAM_ATTR`).
298
+ - `esp_light_sleep_start()` — pause CPU, peripherals resume on wake; lower latency than deep sleep.
299
+ - `esp_sleep_pd_domain_config` to power down unused domains.
300
+ - `RTC_DATA_ATTR static uint32_t boot_count;` survives deep sleep.
301
+ - `esp_wifi_set_ps(WIFI_PS_MIN_MODEM)` for Wi-Fi connected/idle power save.
302
+
303
+ `(AMS7)` AMS7 enters deep sleep after acquisition stop with a wake-on-tap (`esp_sleep_enable_ext0_wakeup` on ICM-42688-P INT1) plus a wake-after-N-seconds safety timer.
304
+
305
+ ## Size budget
306
+
307
+ `idf.py size` shows IRAM / DRAM / flash app image. Read it after every change to acquisition-path code.
308
+
309
+ ```bash
310
+ idf.py size # text summary
311
+ idf.py size --format json # for tooling
312
+ idf.py size --output-file size.json
313
+ ```
314
+
315
+ What to watch:
316
+
317
+ - **IRAM used**: this is the dangerous one. Adding `IRAM_ATTR` to a large function or pulling logging into ISR paths inflates it.
318
+ - **DRAM used**: static `.bss` and `.data`. Large `static` buffers add here.
319
+ - **Flash app binary**: usually comfortable; check `0xfb650 bytes, 0xf49b0 bytes free in the smallest app partition`.
320
+
321
+ `(AMS7)` AMS7 ships `tools/check_idf_size_budget.py` — a hard gate that runs `idf.py size --format json` and fails if IRAM remaining drops below 20 KiB or DRAM remaining below 40 KiB. Always run it after changes:
322
+
323
+ ```bash
324
+ source esp/esp-idf/export.sh
325
+ tools/check_idf_size_budget.py --min-iram-remain 20480 --min-dram-remain 40960
326
+ ```
327
+
328
+ A non-project wrapper that delegates to it when present (and falls back to `idf.py size`) is at `scripts/size_check.sh`:
329
+
330
+ ```bash
331
+ bash scripts/size_check.sh # uses current dir as project
332
+ bash scripts/size_check.sh --project-dir build_espnow_flow
333
+ ```
334
+
335
+ ## Host-side tests
336
+
337
+ Policy modules, payload encoders/decoders, and pure-C++ algorithms can be unit-tested on the host **without `idf.py`**. ESP-IDF firmware that depends on FreeRTOS / drivers stays out of these tests.
338
+
339
+ `(AMS7)` AMS7's host tests are deliberately minimal: a `tests/<area>/<name>_test.cpp` source file plus a `run_<name>_test.sh` shell wrapper that compiles with `g++ -std=c++17`, links the firmware `.cpp` source files directly, and runs. Example:
340
+
341
+ ```bash
342
+ #!/usr/bin/env bash
343
+ set -euo pipefail
344
+ g++ -std=c++17 \
345
+ tests/connectivity/espnow_imu_frame_test.cpp \
346
+ main/connectivity/espnow_imu_frame.cpp \
347
+ -I main/connectivity \
348
+ -o /tmp/espnow_imu_frame_test
349
+ /tmp/espnow_imu_frame_test
350
+ ```
351
+
352
+ Two key distinctions vs the firmware build:
353
+
354
+ - **No `idf.py`**: pure `g++` (or `cmake --build` for larger suites). Fast, runs anywhere.
355
+ - **No FreeRTOS or driver code**: the host testable surface should not include `<freertos/FreeRTOS.h>`, `esp_log.h`, or anything that pulls in `driver/`. If a header you want to test transitively includes those, factor the pure logic out.
356
+
357
+ For larger host-test projects with fakes and a CMake build, see the **`cpp-testing`** skill — it covers GoogleTest, Catch2, host fakes for FreeRTOS, and fixture patterns. See `references/host-tests.md` for the AMS7 host-test layout and pitfalls.
358
+
359
+ ## Do / Don't
360
+
361
+ **Do**
362
+
363
+ ```cpp
364
+ // Mark an ISR handler and let it defer work to a task
365
+ void IRAM_ATTR gpio_isr(void *arg) {
366
+ BaseType_t hpw = pdFALSE;
367
+ xQueueSendFromISR(gpio_evt_queue, &pin, &hpw);
368
+ portYIELD_FROM_ISR(hpw);
369
+ }
370
+ ```
371
+
372
+ ```cpp
373
+ // Lock the wire size at compile time
374
+ typedef struct __attribute__((packed)) { /* ... */ } my_frame_v1_t;
375
+ static_assert(sizeof(my_frame_v1_t) == N, "wire size must match");
376
+ ```
377
+
378
+ ```cpp
379
+ // Aggregate, low-rate log instead of per-frame spam
380
+ ESP_LOGI(TAG, "pl: beats=%lu rr_ms=%lu q=%u", beats, rr_ms, quality);
381
+ ```
382
+
383
+ **Don't**
384
+
385
+ ```cpp
386
+ // Don't malloc in a hot path
387
+ while (running) {
388
+ auto *p = new uint8_t[256]; // DRAM fragmentation, leaks on errors
389
+ // ...
390
+ }
391
+
392
+ // Don't block inside an ISR
393
+ void IRAM_ATTR bad_isr(void *a) {
394
+ vTaskDelay(pdMS_TO_TICKS(10)); // illegal — use xTaskNotifyFromISR + task
395
+ }
396
+
397
+ // Don't add IRAM_ATTR unless required
398
+ void IRAM_ATTR slow_json_formatter(...); // pulls a big function into IRAM
399
+
400
+ // Don't hand-roll endianness
401
+ uint32_t be = (raw[0] << 24) | (raw[1] << 16) | ...; // use explicit <bit> byte-swap helpers
402
+ ```
403
+
404
+ ## Resources
405
+
406
+ ### Task-to-reference index
407
+
408
+ For most C++ review tasks on non-driver firmware code (NVS, logging, Kconfig, BLE/ESP-NOW data paths), the highest-yield references are:
409
+
410
+ | Task | Load first | Also useful |
411
+ |---|---|---|
412
+ | Reviewing NVS / persistence code | `references/nvs.md` | `logging-discipline.md` |
413
+ | Reviewing logging or `ESP_LOG*` calls | `references/logging-discipline.md` | `kconfig.md` |
414
+ | Reviewing Kconfig options | `kconfig.md` | (inline guidance in SKILL.md) |
415
+ | Reviewing packed binary frames / structs | `packed-structs.md` | (inline guidance in SKILL.md) |
416
+ | Reviewing task/queue/ISR code | `freertos-patterns.md` | `memory-and-iram.md` |
417
+ | Reviewing IRAM/disk usage after a build | `memory-and-iram.md` | `scripts/size_check.sh` |
418
+ | Building, flashing, monitoring | `idf-py-commands.md` | `scripts/idf_env.sh` |
419
+ | Writing a host-side C++ test | `host-tests.md` | `$cpp-testing` skill |
420
+
421
+ ### references/
422
+
423
+ Load on demand for deeper detail:
424
+
425
+ - `idf-py-commands.md` — full `idf.py` subcommand list, exit codes, monitor filters, partition-table flow.
426
+ - `freertos-patterns.md` — task lifecycle, pinning, priorities, queue + ISR-to-task handoff, ringbuffer for streams, watchdog, ISR rules.
427
+ - `memory-and-iram.md` — IRAM vs DRAM vs flash vs PSRAM, `iram_attr` placement, what blows the IRAM budget.
428
+ - `kconfig.md` — adding options, menus, `depends on` / `select` / `imply` / `range` / `choice`, component-level Kconfig isolation.
429
+ - `nvs.md` — NVS init/handle/error pattern, namespace hygiene, `nvs_commit`, `nvs_flash_init()` anti-patterns, AMS7 `"ams7cfg"` namespace. **High-yield for review tasks.**
430
+ - `packed-structs.md` — `__attribute__((packed))`, byte order, `static_assert`, versioning, capability bits, magic byte, length-prefixed framing. Mirrors AMS7's `main/connectivity/espnow_*_frame.hpp`.
431
+ - `logging-discipline.md` — `ESP_LOG*`, log levels, tagging, no-per-frame logs, `pl:` aggregate lines, gating with `CONFIG_*_LOG_ENABLE`. **High-yield for review tasks.**
432
+ - `host-tests.md` — pure-CMake host tests that include firmware `.cpp` from `main/`, fakes for FreeRTOS, common pitfalls.
433
+
434
+ ### scripts/
435
+
436
+ Small, runnable helpers:
437
+
438
+ - `scripts/idf_env.sh` — prints the `source esp/esp-idf/export.sh` line and validates that `idf.py` is reachable. Exits 0 if the env is good, 1 otherwise.
439
+ - `scripts/size_check.sh` — runs AMS7's `tools/check_idf_size_budget.py` if available, else falls back to `idf.py size`. Exits 0 on pass, 1 on size violation.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Embedded ESP-IDF"
3
+ short_description: "ESP-IDF v5.3 firmware C++ patterns and idf.py"
4
+ default_prompt: "Use $embedded-esp-idf to write, review, or debug ESP-IDF C++ firmware, including FreeRTOS tasks, packed structs, IRAM/DRAM budget, and the idf.py build/flash/monitor workflow."
@@ -0,0 +1,214 @@
1
+ # FreeRTOS patterns
2
+
3
+ ESP-IDF v5 uses FreeRTOS. Most of these patterns are the same as FreeRTOS elsewhere; the ESP32-specific bits are task pinning, ISR rules (no blocking), and `portTICK_PERIOD_MS`.
4
+
5
+ ## Task lifecycle
6
+
7
+ Create, run, delete:
8
+
9
+ ```cpp
10
+ void acquisition_task(void *arg) {
11
+ while (true) {
12
+ // ... do work, sleep, wait on a queue ...
13
+ }
14
+ vTaskDelete(nullptr); // never reached in a normal acquisition loop
15
+ }
16
+
17
+ void start() {
18
+ xTaskCreatePinnedToCore(
19
+ acquisition_task, // task function
20
+ "acq", // name (shown in runtime stats)
21
+ 4096, // stack depth in WORDS (not bytes) on ESP32
22
+ nullptr, // arg
23
+ 20, // priority (0 = idle, higher = more preemptive)
24
+ nullptr, // optional task handle
25
+ 1 // core ID (0 = APP CPU on classic ESP32, 1 = PRO CPU)
26
+ );
27
+ }
28
+ ```
29
+
30
+ Notes:
31
+
32
+ - Stack depth is in **words**, not bytes. On ESP32 (32-bit) `4096` = 16 KB. Allocate generously for tasks with deep call chains; oversized stack wastes DRAM but undersized stack corrupts memory silently.
33
+ - Pass `nullptr` if you do not need the handle. Use the handle later for `vTaskSuspend` / `vTaskResume` / `vTaskDelete`.
34
+ - Pinning is mandatory for acquisition paths — non-pinned tasks can bounce between cores and lose cache locality.
35
+
36
+ `(AMS7)` AMS7 acquisition tasks run at priority 20+, pinned to core 0 (APP CPU). The PRO CPU (core 1) handles BLE/Wi-Fi by default.
37
+
38
+ ## Priorities
39
+
40
+ | Range | Use for |
41
+ |--------|------------------------------------------------------------|
42
+ | 0 | Idle task only — do not use |
43
+ | 1–4 | Normal app tasks, command handling, BLE host |
44
+ | 5–19 | Latency-sensitive but not real-time (rate control, queues) |
45
+ | 20–24 | Real-time acquisition, time-critical ISRs |
46
+
47
+ Do not stack many tasks at priority 20+ — round-robin between them defeats the point.
48
+
49
+ ## Pinning to cores
50
+
51
+ ESP32 dual-core (Xtensa LX6):
52
+
53
+ - **Core 0 (APP CPU)** — protocol stacks (Wi-Fi, BLE), low-level drivers.
54
+ - **Core 1 (PRO CPU)** — `app_main` runs here by default.
55
+
56
+ Pick a split that puts time-critical work on the under-used core. `xTaskCreatePinnedToCore(handle, name, stack, arg, prio, handle_out, core_id)`.
57
+
58
+ ESP32-S3 / C3 / H2 are single-core: pinning is a no-op but the API still works.
59
+
60
+ ## Queues
61
+
62
+ Producer/consumer with bounded queues. Decouples ISR rate from task rate.
63
+
64
+ ```cpp
65
+ QueueHandle_t evt_q = xQueueCreate(16, sizeof(uint32_t));
66
+
67
+ // Producer (task context)
68
+ uint32_t pin = read_pin();
69
+ xQueueSend(evt_q, &pin, pdMS_TO_TICKS(10));
70
+
71
+ // Consumer
72
+ uint32_t pin;
73
+ if (xQueueReceive(evt_q, &pin, portMAX_DELAY) == pdTRUE) {
74
+ handle(pin);
75
+ }
76
+ ```
77
+
78
+ Use `portMAX_DELAY` only when the consumer must never return. Use `pdMS_TO_TICKS(N)` when the consumer should time out (e.g., to check a "stop" flag).
79
+
80
+ ## ISR → task handoff
81
+
82
+ ISRs must not block. The standard handoff uses `xQueueSendFromISR` and the higher-priority-task-woken flag:
83
+
84
+ ```cpp
85
+ static QueueHandle_t gpio_evt_q;
86
+
87
+ void IRAM_ATTR gpio_isr(void *arg) {
88
+ uint32_t pin = (uint32_t)arg;
89
+ BaseType_t hpw = pdFALSE;
90
+ xQueueSendFromISR(gpio_evt_q, &pin, &hpw);
91
+ if (hpw == pdTRUE) portYIELD_FROM_ISR();
92
+ }
93
+
94
+ void consumer_task(void *arg) {
95
+ uint32_t pin;
96
+ while (true) {
97
+ if (xQueueReceive(gpio_evt_q, &pin, portMAX_DELAY) == pdTRUE) {
98
+ // process pin (NOT in ISR context — can block here)
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ Alternatives:
105
+
106
+ - `xTaskNotifyFromISR` / `xTaskNotifyWait` — single-task notifications, very fast.
107
+ - `xTimerPendFunctionCallFromISR` — defer a callback into the timer service task (useful for one-shot IRAM work).
108
+ - Direct event bits with `xEventGroupSetBitsFromISR` — broadcast to multiple waiters.
109
+
110
+ ## Stream and ring buffers
111
+
112
+ For variable-length byte streams (e.g., SPI DMA into a parser):
113
+
114
+ ```cpp
115
+ StreamBufferHandle_t sb = xStreamBufferCreate(2048, 256); // total, trigger
116
+ // Producer (DMA ISR)
117
+ xStreamBufferSendFromISR(sb, data, len, &hpw);
118
+ // Consumer task
119
+ uint8_t buf[256];
120
+ size_t got = xStreamBufferReceive(sb, buf, sizeof(buf), pdMS_TO_TICKS(100));
121
+ ```
122
+
123
+ `xRingbufferCreate` (IDF) is the lock-free variant — better for high-rate ISR streams where the producer must never wait.
124
+
125
+ ## Semaphores and mutexes
126
+
127
+ ```cpp
128
+ SemaphoreHandle_t ready = xSemaphoreCreateBinary();
129
+ xSemaphoreGive(ready);
130
+ xSemaphoreTake(ready, portMAX_DELAY);
131
+
132
+ SemaphoreHandle_t in_flight = xSemaphoreCreateCounting(4, 0); // max 4, starts empty
133
+ ```
134
+
135
+ Mutexes for shared resources:
136
+
137
+ ```cpp
138
+ SemaphoreHandle_t cfg_lock = xSemaphoreCreateMutex();
139
+ if (xSemaphoreTake(cfg_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
140
+ // ... access shared resource ...
141
+ xSemaphoreGive(cfg_lock);
142
+ }
143
+ ```
144
+
145
+ **Recursive mutexes** (`xSemaphoreCreateRecursiveMutex`) for code that may take the same lock twice in a call chain.
146
+
147
+ ## Timers
148
+
149
+ ```cpp
150
+ TimerHandle_t t = xTimerCreate("rate", pdMS_TO_TICKS(1000), pdTRUE, nullptr, [](TimerHandle_t x) {
151
+ // fired every 1000 ms; called in the timer service task (NOT ISR context)
152
+ });
153
+ xTimerStart(t, 0);
154
+ ```
155
+
156
+ One-shot for delayed work:
157
+
158
+ ```cpp
159
+ TimerHandle_t oneshot = xTimerCreate("once", pdMS_TO_TICKS(50), pdFALSE, nullptr, cb);
160
+ xTimerStart(oneshot, 0);
161
+ ```
162
+
163
+ ## Event groups
164
+
165
+ Useful when a task must wait for several subsystems to be ready before proceeding:
166
+
167
+ ```cpp
168
+ EventGroupHandle_t eg = xEventGroupCreate();
169
+ xEventGroupSetBits(eg, BIT_SD_READY | BIT_BLE_READY | BIT_ACQ_READY);
170
+
171
+ EventBits_t bits = xEventGroupWaitBits(
172
+ eg,
173
+ BIT_SD_READY | BIT_BLE_READY | BIT_ACQ_READY,
174
+ pdFALSE, // don't clear on exit
175
+ pdTRUE, // wait for ALL bits
176
+ portMAX_DELAY
177
+ );
178
+ ```
179
+
180
+ ## Watchdog
181
+
182
+ The IDF task watchdog resets the system if a task does not feed it within a window. Always feed:
183
+
184
+ ```cpp
185
+ void heavy_task(void *arg) {
186
+ while (true) {
187
+ // chunk of work
188
+ vTaskDelay(pdMS_TO_TICKS(10)); // yields — watchdog is fed by idle
189
+ // OR if you spin:
190
+ // vTaskDelay(1); // minimum yield
191
+ }
192
+ }
193
+ ```
194
+
195
+ If a task really must spin for >1 second (rare), subscribe it to the watchdog and call `esp_task_wdt_reset()` periodically. Long ISR-disabled windows also trip the watchdog — keep them < one tick (`portTICK_PERIOD_MS` = 10 ms by default).
196
+
197
+ ## portTICK_PERIOD_MS
198
+
199
+ Always convert via `pdMS_TO_TICKS(ms)`:
200
+
201
+ ```cpp
202
+ vTaskDelay(pdMS_TO_TICKS(100)); // 100 ms regardless of tick rate
203
+ xQueueReceive(q, &x, pdMS_TO_TICKS(50));
204
+ ```
205
+
206
+ Never multiply by hand (`ms * portTICK_PERIOD_MS`) — it lies when the tick rate changes.
207
+
208
+ ## Common pitfalls
209
+
210
+ - **Blocking in an ISR.** Symptom: hard fault or watchdog reset. Fix: defer to a task via `xQueueSendFromISR` / `xTaskNotifyFromISR`.
211
+ - **Stack too small.** Symptom: random crashes after a few minutes, often deep in call chains. Fix: bump stack depth, or enable `CONFIG_COMPILER_STACK_CHECK_MODE_NORM` to catch overflows at runtime.
212
+ - **Priority inversion.** Symptom: low-priority task holds a mutex, blocks a high-priority task. Fix: `xSemaphoreCreateRecursiveMutex` and keep critical sections short.
213
+ - **Forgetting `portYIELD_FROM_ISR`.** Symptom: producer wakes the consumer but the consumer doesn't run until the next tick. Fix: check the `xHigherPriorityTaskWoken` returned from `*FromISR` and yield.
214
+ - **Non-pinned tasks bouncing cores.** Symptom: jittery timing, occasional cache misses. Fix: pin acquisition and protocol tasks explicitly.