@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.
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/audit.mjs +144 -0
- package/cli/banner.mjs +41 -0
- package/cli/bin.mjs +186 -0
- package/cli/copy.mjs +508 -0
- package/cli/export.mjs +87 -0
- package/cli/init.mjs +147 -0
- package/cli/install.mjs +390 -0
- package/cli/plan-templates.mjs +523 -0
- package/cli/plan.mjs +2087 -0
- package/cli/prompts.mjs +163 -0
- package/cli/update.mjs +273 -0
- package/cli/utils.mjs +153 -0
- package/config/AGENTS.md +282 -0
- package/config/agents/baldr.md +148 -0
- package/config/agents/forseti.md +112 -0
- package/config/agents/frigg.md +101 -0
- package/config/agents/heimdall.md +157 -0
- package/config/agents/hermod.md +144 -0
- package/config/agents/mimir.md +115 -0
- package/config/agents/odin.md +309 -0
- package/config/agents/quick.md +78 -0
- package/config/agents/semble-search.md +44 -0
- package/config/agents/thor.md +97 -0
- package/config/agents/tyr.md +96 -0
- package/config/agents/vidarr.md +100 -0
- package/config/agents/vor.md +140 -0
- package/config/commands/audit.md +1 -0
- package/config/commands/explain.md +1 -0
- package/config/commands/init.md +1 -0
- package/config/commands/learn.md +1 -0
- package/config/commands/pr-review.md +1 -0
- package/config/commands/tailscale-serve.md +96 -0
- package/config/hooks/README.md +29 -0
- package/config/hooks/post-tool-use.md +16 -0
- package/config/hooks/pre-tool-use.md +16 -0
- package/config/opencode.json +52 -0
- package/config/opencode.json.template +52 -0
- package/config/rules/general.md +8 -0
- package/config/rules/git.md +11 -0
- package/config/rules/javascript.md +10 -0
- package/config/rules/python.md +10 -0
- package/config/rules/testing.md +10 -0
- package/config/skills/bizar/README.md +9 -0
- package/config/skills/bizar/SKILL.md +187 -0
- package/config/skills/cpp-coding-standards/README.md +28 -0
- package/config/skills/cpp-coding-standards/SKILL.md +634 -0
- package/config/skills/cpp-coding-standards/agents/openai.yaml +4 -0
- package/config/skills/cpp-coding-standards/references/concurrency.md +320 -0
- package/config/skills/cpp-coding-standards/references/error-handling.md +229 -0
- package/config/skills/cpp-coding-standards/references/memory-safety.md +216 -0
- package/config/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
- package/config/skills/cpp-coding-standards/references/review-checklist.md +96 -0
- package/config/skills/cpp-testing/README.md +28 -0
- package/config/skills/cpp-testing/SKILL.md +304 -0
- package/config/skills/cpp-testing/agents/openai.yaml +4 -0
- package/config/skills/cpp-testing/references/coverage.md +370 -0
- package/config/skills/cpp-testing/references/framework-compare.md +175 -0
- package/config/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
- package/config/skills/cpp-testing/references/mocking.md +364 -0
- package/config/skills/cpp-testing/references/tdd-workflow.md +308 -0
- package/config/skills/embedded-esp-idf/README.md +41 -0
- package/config/skills/embedded-esp-idf/SKILL.md +439 -0
- package/config/skills/embedded-esp-idf/agents/openai.yaml +4 -0
- package/config/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
- package/config/skills/embedded-esp-idf/references/host-tests.md +164 -0
- package/config/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
- package/config/skills/embedded-esp-idf/references/kconfig.md +159 -0
- package/config/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
- package/config/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
- package/config/skills/embedded-esp-idf/references/nvs.md +121 -0
- package/config/skills/embedded-esp-idf/references/packed-structs.md +192 -0
- package/config/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
- package/config/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
- package/config/skills/self-improvement/SKILL.md +64 -0
- package/package.json +47 -0
- package/templates/plan/htmx.min.js +1 -0
- package/templates/plan/library/bug-investigation.mdx +79 -0
- package/templates/plan/library/decision-record.mdx +71 -0
- package/templates/plan/library/feature-design.mdx +92 -0
- package/templates/plan/meta.json.template +8 -0
- package/templates/plan/plan.canvas.template +1711 -0
- package/templates/plan/plan.html.template +937 -0
- package/templates/plan/plan.mdx.template +46 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Memory model and IRAM
|
|
2
|
+
|
|
3
|
+
ESP32 (Xtensa LX6/LX7, or RISC-V on S3/C3) has distinct address spaces. Code and data are placed into them by the linker based on attributes; getting this wrong is the difference between a working firmware and one that crashes on the first SPI-DMA transfer.
|
|
4
|
+
|
|
5
|
+
## Address spaces at a glance
|
|
6
|
+
|
|
7
|
+
| Space | Size (classic ESP32) | What lives here |
|
|
8
|
+
|-----------|-----------------------------|--------------------------------------------------------------|
|
|
9
|
+
| IRAM | ~128 KB (some chips 256 KB) | ISR handlers, `IRAM_ATTR` functions, flash-cache-disabled |
|
|
10
|
+
| DRAM | ~120 KB | `.data`, `.bss`, heap, stacks, default-code-section RAM |
|
|
11
|
+
| Flash | 4–16 MB | Program text (XIP), read-only data, filesystems |
|
|
12
|
+
| PSRAM | 0–8 MB optional | Large buffers via `MALLOC_CAP_SPIRAM` |
|
|
13
|
+
|
|
14
|
+
## IRAM (instruction RAM)
|
|
15
|
+
|
|
16
|
+
IRAM is the tightest budget on most ESP32 firmware because it has to be physically present on-chip and is not expandable. Code in IRAM can run without flash cache, which is required during:
|
|
17
|
+
|
|
18
|
+
- **ISR handlers** — the cache may be disabled.
|
|
19
|
+
- **SPI / DMA completion paths** — flash-cache-disabled periods when the SPI peripheral is busy.
|
|
20
|
+
- **Time-critical inner loops** — a few hot loops that would be slowed by XIP cache misses.
|
|
21
|
+
|
|
22
|
+
Mark a function IRAM-resident with the attribute:
|
|
23
|
+
|
|
24
|
+
```cpp
|
|
25
|
+
#include "esp_attr.h"
|
|
26
|
+
|
|
27
|
+
void IRAM_ATTR spi_complete_isr(spi_transaction_t *trans) {
|
|
28
|
+
// runs from IRAM, must be small and avoid non-IRAM APIs
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`(AMS7)` Keep IRAM remaining ≥ 20 KiB on AMS7. Below 24 KiB is a warning. Do not add `IRAM_ATTR` unless the function is genuinely on a cache-disabled path. See `docs/firmware/memory_budget.md`.
|
|
33
|
+
|
|
34
|
+
### What blows the IRAM budget
|
|
35
|
+
|
|
36
|
+
- **Every `IRAM_ATTR` function** — even a 200-byte routine pulls in 200 bytes of IRAM that flash would otherwise have held.
|
|
37
|
+
- **Logging inside ISRs** — `ESP_LOG*` is not `IRAM_ATTR` by default; if you call it from an ISR, the linker pulls the entire logging chain into IRAM. Use direct `uart_tx` or a deferred task instead.
|
|
38
|
+
- **Format strings in IRAM** — `printf`-style code in IRAM is expensive. Move formatted output to a task.
|
|
39
|
+
- **Per-frame work in IRAM** — even non-IRAM helpers reached only from IRAM functions can end up in IRAM if the linker decides so (`-ffunction-sections` + linker `--gc-sections` reduces but does not eliminate this).
|
|
40
|
+
- **Inlining** — `static inline` in a header used by an IRAM file ends up in IRAM for every consumer.
|
|
41
|
+
|
|
42
|
+
### Diagnosing IRAM growth
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
idf.py size --format json --output-file size.json
|
|
46
|
+
python3 -c "import json; s=json.load(open('size.json')); print(s['iram_size'], s['dram_size'])"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For per-symbol breakdown:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
$IDF_PATH/tools/uf2/elf2image.py build/<project>.elf # if needed
|
|
53
|
+
xtensa-esp32-elf-objdump -d build/<project>.elf | grep '<.*>:' | head
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The AMS7-specific gate `tools/check_idf_size_budget.py` (wrapped by `scripts/size_check.sh`) reads the JSON and fails on hard thresholds.
|
|
57
|
+
|
|
58
|
+
## DRAM (data RAM)
|
|
59
|
+
|
|
60
|
+
DRAM holds:
|
|
61
|
+
|
|
62
|
+
- `.data` — initialized globals (`int x = 5;`).
|
|
63
|
+
- `.bss` — zero-initialized globals (`int x;`).
|
|
64
|
+
- **Heap** — `malloc` / `new` returns here by default.
|
|
65
|
+
- **Stacks** — one stack per FreeRTOS task; `app_main`'s stack is fixed.
|
|
66
|
+
|
|
67
|
+
### Prefer static
|
|
68
|
+
|
|
69
|
+
Heap fragmentation is severe on ESP32 — there is no `mmap` or `sbrk`, just a fixed free list. Long-running firmware that `malloc`s in a loop will eventually fail with `ESP_ERR_NO_MEM`.
|
|
70
|
+
|
|
71
|
+
Rules:
|
|
72
|
+
|
|
73
|
+
- `static const` arrays for lookup tables.
|
|
74
|
+
- `static` (file-scope) for buffers reused across calls.
|
|
75
|
+
- `std::array<T, N>` or `std::span` over `std::vector`.
|
|
76
|
+
- Pool allocators if you really need dynamic.
|
|
77
|
+
|
|
78
|
+
When you must malloc, prefer `heap_caps_malloc(size, MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL)` and free in matching scope. Always check the returned pointer.
|
|
79
|
+
|
|
80
|
+
### Stacks
|
|
81
|
+
|
|
82
|
+
FreeRTOS stacks grow downward; overflow corrupts whatever is below them. Tuning:
|
|
83
|
+
|
|
84
|
+
- Stack depth is in **words** (4 bytes on Xtensa, 4 bytes on RISC-V too). `4096` = 16 KiB.
|
|
85
|
+
- Over-budget: enable `CONFIG_COMPILER_STACK_CHECK_MODE_NORM` (compiler-instrumented) or `CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK` (hardware watchpoint, only one task at a time).
|
|
86
|
+
- Use `uxTaskGetStackHighWaterMark(handle)` to measure headroom at runtime.
|
|
87
|
+
|
|
88
|
+
`(AMS7)` AMS7 acquisition stacks are 8–16 KB; command-handler stack on the PRO CPU is 16 KB (`CONFIG_MAIN_TASK_STACK_SIZE=16384`).
|
|
89
|
+
|
|
90
|
+
### Internal vs PSRAM (DMA-capable)
|
|
91
|
+
|
|
92
|
+
`MALLOC_CAP_INTERNAL` requests memory in DRAM. `MALLOC_CAP_SPIRAM` requests external PSRAM (slow, no DMA). Combine as needed:
|
|
93
|
+
|
|
94
|
+
```cpp
|
|
95
|
+
// 8-bit accessible, internal (DMA-capable) — for SPI/I2S buffers
|
|
96
|
+
uint8_t *dma_buf = (uint8_t *)heap_caps_malloc(4096, MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
|
|
97
|
+
|
|
98
|
+
// large, no DMA requirement — JSON caches, log buffers
|
|
99
|
+
char *big_buf = (char *)heap_caps_malloc(64 * 1024, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Always check the return value — `MALLOC_CAP_SPIRAM` may fail if PSRAM is not configured, or return NULL if the chip has none.
|
|
103
|
+
|
|
104
|
+
## Flash
|
|
105
|
+
|
|
106
|
+
Most code lives in flash and is executed in place (XIP). The flash cache is on-chip and small (~32 KB on classic ESP32). Functions that are not in cache take a flash-read penalty.
|
|
107
|
+
|
|
108
|
+
To check cache hit rate at runtime:
|
|
109
|
+
|
|
110
|
+
```cpp
|
|
111
|
+
#include "esp_heap_caps.h"
|
|
112
|
+
ESP_LOGI(TAG, "free 8-bit internal: %u", heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL));
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For static asserts on flash size, see `idf.py size` output. The relevant numbers are:
|
|
116
|
+
|
|
117
|
+
- **Flash app binary** — total image size.
|
|
118
|
+
- **`bytes free in the smallest app partition`** — partition headroom; matters for OTA.
|
|
119
|
+
|
|
120
|
+
## `.iram0.lit`, `.iram0.text`, and `.dram0.*`
|
|
121
|
+
|
|
122
|
+
The default linker script places:
|
|
123
|
+
|
|
124
|
+
- `.iram0.lit` — literal pools (constants referenced from IRAM).
|
|
125
|
+
- `.iram0.text` — `IRAM_ATTR` functions.
|
|
126
|
+
- `.dram0.data` / `.dram0.bss` — initialized / zero-initialized data.
|
|
127
|
+
- `.flash.text` — all other code.
|
|
128
|
+
|
|
129
|
+
`idf.py size` aggregates these. Per-section breakdown requires `xtensa-esp32-elf-size -A build/<project>.elf`.
|
|
130
|
+
|
|
131
|
+
## Common pitfalls
|
|
132
|
+
|
|
133
|
+
- **Calling a non-IRAM function from an ISR.** Symptom: intermittent crash in the ISR. Fix: mark the called function `IRAM_ATTR` or move the work to a task.
|
|
134
|
+
- **Using `printf` / `ESP_LOG*` from an ISR.** Symptom: large IRAM growth. Fix: use `xQueueSendFromISR` to defer the log to a task.
|
|
135
|
+
- **Malloc with `MALLOC_CAP_SPIRAM` on a chip without PSRAM.** Symptom: NULL return, hard fault on deref. Fix: fall back to internal with a smaller size, or fail loudly.
|
|
136
|
+
- **Stack overflow from deep recursion.** Symptom: hard fault or corruption deep in the call stack. Fix: convert to iteration, or bump the task's stack.
|
|
137
|
+
- **Static buffers in headers.** A `static constexpr` buffer in a header included by many `.cpp` files duplicates per translation unit, then collides at link. Use `inline constexpr` (C++17) or move to a single `.cpp`.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# NVS (Non-Volatile Storage) on ESP-IDF
|
|
2
|
+
|
|
3
|
+
NVS stores small key-value pairs in flash and survives reboot. ESP-IDF provides `nvs_flash.h` (low-level) and `nvs.h` (typed handle API). Most firmware code uses the typed handle API.
|
|
4
|
+
|
|
5
|
+
## Initialization — once at boot
|
|
6
|
+
|
|
7
|
+
`nvs_flash_init()` must be called **exactly once at application startup**, not on every operation. Calling it in a getter/setter is an anti-pattern.
|
|
8
|
+
|
|
9
|
+
```cpp
|
|
10
|
+
// GOOD — in app_main()
|
|
11
|
+
extern "C" void app_main() {
|
|
12
|
+
esp_err_t ret = nvs_flash_init();
|
|
13
|
+
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
|
14
|
+
ESP_ERROR_CHECK(nvs_flash_erase());
|
|
15
|
+
ret = nvs_flash_init();
|
|
16
|
+
}
|
|
17
|
+
ESP_ERROR_CHECK(ret);
|
|
18
|
+
// ... start tasks, services, etc.
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// BAD — calling nvs_flash_init() in every Get/Set is wasteful
|
|
22
|
+
esp_err_t Configuration::GetInt(const char* key, int32_t* out) {
|
|
23
|
+
nvs_flash_init(); // <-- wrong
|
|
24
|
+
nvs_handle_t h;
|
|
25
|
+
nvs_open("ns", NVS_READONLY, &h);
|
|
26
|
+
// ...
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For partition-encrypted or large NVS partitions, also handle `ESP_ERR_NVS_NO_FREE_PAGES` and `ESP_ERR_NVS_NEW_VERSION_FOUND` by erase + retry. See `examples/storage/nvs_value_iterator` in ESP-IDF.
|
|
31
|
+
|
|
32
|
+
## Namespaces and handles
|
|
33
|
+
|
|
34
|
+
Open a handle per operation or per long-lived component:
|
|
35
|
+
|
|
36
|
+
```cpp
|
|
37
|
+
// Per-operation (simple, slightly more overhead)
|
|
38
|
+
nvs_handle_t h;
|
|
39
|
+
ESP_ERROR_CHECK(nvs_open("ams7cfg", NVS_READWRITE, &h));
|
|
40
|
+
int32_t val = 42;
|
|
41
|
+
ESP_ERROR_CHECK(nvs_set_i32(h, "ble_adv", val));
|
|
42
|
+
ESP_ERROR_CHECK(nvs_commit(h));
|
|
43
|
+
nvs_close(h);
|
|
44
|
+
|
|
45
|
+
// Per-component (preferred for hot paths)
|
|
46
|
+
class ConfigStore {
|
|
47
|
+
public:
|
|
48
|
+
esp_err_t Init() {
|
|
49
|
+
esp_err_t err = nvs_open("ams7cfg", NVS_READWRITE, &handle_);
|
|
50
|
+
if (err != ESP_OK) return err;
|
|
51
|
+
// optional: pre-load known keys
|
|
52
|
+
return ESP_OK;
|
|
53
|
+
}
|
|
54
|
+
~ConfigStore() { nvs_close(handle_); }
|
|
55
|
+
private:
|
|
56
|
+
nvs_handle_t handle_{};
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`nvs_commit()` is **required** for write durability — `nvs_set_*` only updates the in-memory cache.
|
|
61
|
+
|
|
62
|
+
## Typed vs blob storage
|
|
63
|
+
|
|
64
|
+
| Type | Use for |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `nvs_set_i8/u8/i16/u16/i32/u64` | Counters, flags, IDs, durations |
|
|
67
|
+
| `nvs_set_str` | Variable-length strings (max 4000 bytes per key, ~1984 bytes safe) |
|
|
68
|
+
| `nvs_set_blob` | Packed structs, calibration data, JSON payloads |
|
|
69
|
+
| `nvs_set_str` with fixed keys | Persistent default values (e.g., `feature_flag_default` = "on") |
|
|
70
|
+
|
|
71
|
+
Use **typed keys** for atomic per-field updates; use **blob** only when the data is always written and read as a unit (otherwise partial writes corrupt the format).
|
|
72
|
+
|
|
73
|
+
## Error handling
|
|
74
|
+
|
|
75
|
+
`ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_set_*(...))` silently swallows NVS failures. The caller has no way to know the persist operation succeeded. Two patterns:
|
|
76
|
+
|
|
77
|
+
```cpp
|
|
78
|
+
// 1. Propagate the error to the caller
|
|
79
|
+
esp_err_t ConfigStore::SetInt(const char* key, int32_t val) {
|
|
80
|
+
esp_err_t err = nvs_set_i32(handle_, key, val);
|
|
81
|
+
if (err != ESP_OK) return err;
|
|
82
|
+
return nvs_commit(handle_);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Log and degrade gracefully
|
|
86
|
+
esp_err_t err = nvs_set_i32(handle_, key, val);
|
|
87
|
+
if (err != ESP_OK) {
|
|
88
|
+
ESP_LOGE(TAG, "nvs_set_i32(%s) failed: %s", key, esp_err_to_name(err));
|
|
89
|
+
return; // caller decides what to do
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Avoid bare `ESP_ERROR_CHECK_WITHOUT_ABORT` followed by an assumed-success return value.
|
|
94
|
+
|
|
95
|
+
## Common pitfalls
|
|
96
|
+
|
|
97
|
+
- **Forgetting `nvs_commit()`** — `nvs_set_*` is buffered; without commit, values are lost on power cycle
|
|
98
|
+
- **Using the same handle from two tasks** — `nvs_handle_t` is not thread-safe; serialize access or use one handle per task
|
|
99
|
+
- **Hitting the 4-6 KB per-namespace limit** — large blobs need a dedicated namespace; check `nvs_get_stats()` first
|
|
100
|
+
- **Storing `std::string` directly** — NVS stores C strings, convert with `.c_str()` for write and `nvs_get_str` + sized buffer for read
|
|
101
|
+
- **Initializing on every getter** — see "Initialization" above
|
|
102
|
+
- **Hardcoded partition not present** — confirm `partitions.csv` includes an `nvs` entry; default is usually fine but custom layouts can break
|
|
103
|
+
|
|
104
|
+
## Kconfig gating for NVS debug
|
|
105
|
+
|
|
106
|
+
Expose NVS internals under a debug option so production builds stay quiet:
|
|
107
|
+
|
|
108
|
+
```kconfig
|
|
109
|
+
config AMS7_NVS_DEBUG_ENABLE
|
|
110
|
+
bool "Enable verbose NVS logging"
|
|
111
|
+
default n
|
|
112
|
+
help
|
|
113
|
+
When enabled, every NVS get/set logs a line. Disable in production
|
|
114
|
+
to avoid per-write console output.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## AMS7-specific patterns
|
|
118
|
+
|
|
119
|
+
`(AMS7)` The AMS7 firmware uses the namespace `"ams7cfg"` for runtime feature flags and study metadata. The NVS handle is owned by `Configuration` in `main/ams7conf.cpp`. The boot path in `main/ams7_esp32.cpp` calls `nvs_flash_init()` once before any `Configuration` access.
|
|
120
|
+
|
|
121
|
+
`(AMS7)` Persisted feature-flag defaults are stored as NVS strings (e.g., `ble_adv_snapshot_default = "on"`) — the runtime override is in RAM only. This lets `_SAVE` commands persist while plain `!FEATURE=true` commands stay ephemeral.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Packed binary protocols
|
|
2
|
+
|
|
3
|
+
Wire-format frames over ESP-NOW, BLE characteristic writes, or any byte-oriented transport must have deterministic layout. ESP32 is little-endian natively, but `__attribute__((packed))` structs are still required to lock field offsets across compilers and configurations.
|
|
4
|
+
|
|
5
|
+
## The canonical AMS7 frame shape
|
|
6
|
+
|
|
7
|
+
Every AMS7 wire frame lives in `main/connectivity/espnow_*_frame.hpp` and follows the same pattern. The IMU frame is the cleanest example:
|
|
8
|
+
|
|
9
|
+
```cpp
|
|
10
|
+
// main/connectivity/espnow_imu_frame.hpp
|
|
11
|
+
#pragma once
|
|
12
|
+
|
|
13
|
+
#include <stdint.h>
|
|
14
|
+
|
|
15
|
+
#ifdef __cplusplus
|
|
16
|
+
extern "C" {
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
#define CORE_ESPNOW_IMU_MAGIC 0x494dU // ASCII 'I','M'
|
|
20
|
+
#define CORE_ESPNOW_IMU_VERSION 1U
|
|
21
|
+
#define CORE_ESPNOW_FRAME_IMU_RAW 1U
|
|
22
|
+
|
|
23
|
+
typedef struct __attribute__((packed)) {
|
|
24
|
+
uint16_t magic; // identifies the frame family on the wire
|
|
25
|
+
uint8_t version; // protocol version of this struct
|
|
26
|
+
uint8_t frame_type; // sub-type within the family
|
|
27
|
+
uint16_t id; // sender / subject id
|
|
28
|
+
uint16_t seq; // monotonic sequence
|
|
29
|
+
uint16_t age_ms; // capture age in ms (sender-stamped)
|
|
30
|
+
int16_t accel_x, accel_y, accel_z;
|
|
31
|
+
int16_t gyro_x, gyro_y, gyro_z;
|
|
32
|
+
} core_espnow_imu_frame_v1_t;
|
|
33
|
+
|
|
34
|
+
void espnow_imu_frame_init(core_espnow_imu_frame_v1_t *frame,
|
|
35
|
+
uint16_t id,
|
|
36
|
+
uint16_t seq,
|
|
37
|
+
uint16_t age_ms,
|
|
38
|
+
int16_t accel_x,
|
|
39
|
+
int16_t accel_y,
|
|
40
|
+
int16_t accel_z,
|
|
41
|
+
int16_t gyro_x,
|
|
42
|
+
int16_t gyro_y,
|
|
43
|
+
int16_t gyro_z);
|
|
44
|
+
|
|
45
|
+
#ifdef __cplusplus
|
|
46
|
+
}
|
|
47
|
+
static_assert(sizeof(core_espnow_imu_frame_v1_t) == 22,
|
|
48
|
+
"core_espnow_imu_frame_v1_t must be 22 bytes");
|
|
49
|
+
#else
|
|
50
|
+
_Static_assert(sizeof(core_espnow_imu_frame_v1_t) == 22,
|
|
51
|
+
"core_espnow_imu_frame_v1_t must be 22 bytes");
|
|
52
|
+
#endif
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This pattern is **load-bearing** for the project:
|
|
56
|
+
|
|
57
|
+
- `magic` is the first 16 bits so the receiver can reject foreign frames in O(1) before parsing.
|
|
58
|
+
- `version` distinguishes `_v1_t` from `_v2_t` so the receiver can dispatch to the right decoder.
|
|
59
|
+
- `frame_type` distinguishes sub-types within the same magic family (e.g., `IMU_RAW` vs a future `IMU_QUAT`).
|
|
60
|
+
- A trailing `static_assert` locks the on-wire size — adding a field is a build error, not a silent wire-format drift.
|
|
61
|
+
|
|
62
|
+
`(AMS7)` Naming convention is `core_<transport>_<family>_frame_v<N>_t`. Static asserts are mandatory on every wire struct.
|
|
63
|
+
|
|
64
|
+
## The universal five rules
|
|
65
|
+
|
|
66
|
+
1. **`__attribute__((packed))`** on every wire struct. Never rely on natural alignment.
|
|
67
|
+
2. **`static_assert(sizeof(T) == N)`** for every wire struct. The assertion fires at compile time, not over the air.
|
|
68
|
+
3. **Fixed-width types** only: `uint16_t`, `int16_t`, `uint32_t`, `int32_t`. Never `int`, `long`, `short`.
|
|
69
|
+
4. **Magic byte first**, then version, then frame_type. Let the receiver reject foreign frames cheaply.
|
|
70
|
+
5. **Length-prefixed framing** on the wire: 2 bytes little-endian length, then that many bytes of payload. No delimiter scanning, no escape bytes.
|
|
71
|
+
|
|
72
|
+
## Byte order
|
|
73
|
+
|
|
74
|
+
ESP32 is little-endian natively; a `uint16_t` written via `memcpy` lands as low-byte-then-high-byte. When porting to a big-endian host (or a future big-endian SoC), use explicit byte-swap helpers instead of leaving `<<` chains:
|
|
75
|
+
|
|
76
|
+
```cpp
|
|
77
|
+
// Bad: silent byte-order dependence
|
|
78
|
+
uint32_t be = (raw[0] << 24) | (raw[1] << 16) | (raw[2] << 8) | raw[3];
|
|
79
|
+
|
|
80
|
+
// Good: explicit and obvious
|
|
81
|
+
#include <endian.h>
|
|
82
|
+
uint32_t value = le32toh(*reinterpret_cast<const uint32_t *>(raw));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Most ESP-IDF code uses the native LE order and stays put — the explicit byte swap is only needed when a host tool expects BE or when you genuinely don't know the SoC.
|
|
86
|
+
|
|
87
|
+
## Versioning and capability bits
|
|
88
|
+
|
|
89
|
+
When you need to evolve a frame without breaking old receivers, **bump the version** and add a `_v2_t`. Old receivers see the new magic and ignore it; new receivers see the version byte and dispatch:
|
|
90
|
+
|
|
91
|
+
```cpp
|
|
92
|
+
typedef struct __attribute__((packed)) {
|
|
93
|
+
uint16_t magic;
|
|
94
|
+
uint8_t version; // 2
|
|
95
|
+
uint8_t frame_type;
|
|
96
|
+
uint16_t id;
|
|
97
|
+
uint16_t seq;
|
|
98
|
+
uint32_t timestamp_us; // new in v2
|
|
99
|
+
/* ... existing fields ... */
|
|
100
|
+
} core_espnow_imu_frame_v2_t;
|
|
101
|
+
|
|
102
|
+
static_assert(sizeof(core_espnow_imu_frame_v2_t) == 30,
|
|
103
|
+
"core_espnow_imu_frame_v2_t must be 30 bytes");
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For optional fields, use a `capability_bits` bitmask after the header:
|
|
107
|
+
|
|
108
|
+
```cpp
|
|
109
|
+
typedef struct __attribute__((packed)) {
|
|
110
|
+
uint16_t magic;
|
|
111
|
+
uint8_t version;
|
|
112
|
+
uint8_t frame_type;
|
|
113
|
+
uint16_t id;
|
|
114
|
+
uint16_t seq;
|
|
115
|
+
uint32_t cap_bits; // bit 0 = has_quaternion, bit 1 = has_temperature, ...
|
|
116
|
+
/* ... variable ... */
|
|
117
|
+
} core_espnow_capability_frame_v1_t;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Receivers mask-and-test `cap_bits` to know which optional fields are present.
|
|
121
|
+
|
|
122
|
+
## Wire framing (length-prefixed)
|
|
123
|
+
|
|
124
|
+
ESP-NOW itself has a 250-byte MTU per packet and 6-byte receiver MAC; AMS7 adds a 2-byte length prefix and a magic-byte envelope:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
+--------+--------+--------------+----------+
|
|
128
|
+
| length | magic | payload | (rounded |
|
|
129
|
+
| (u16) | (u16) | (frame_t) | to MTU) |
|
|
130
|
+
+--------+--------+--------------+----------+
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The receiver reads `length`, then reads exactly `length` bytes. If `length` exceeds the packet, the packet is dropped. If `magic` does not match a known family, drop.
|
|
134
|
+
|
|
135
|
+
For ESP-NOW specifically, AMS7 frames use a per-frame structure with sender-stamped `age_ms` so receivers can detect stale samples without keeping state.
|
|
136
|
+
|
|
137
|
+
## Init functions
|
|
138
|
+
|
|
139
|
+
Always provide an `_init` function that fills the struct from named arguments. This is the only thing that touches the wire struct's bytes, so the layout can change without callers changing:
|
|
140
|
+
|
|
141
|
+
```cpp
|
|
142
|
+
void espnow_imu_frame_init(core_espnow_imu_frame_v1_t *frame,
|
|
143
|
+
uint16_t id, uint16_t seq, uint16_t age_ms,
|
|
144
|
+
int16_t ax, int16_t ay, int16_t az,
|
|
145
|
+
int16_t gx, int16_t gy, int16_t gz) {
|
|
146
|
+
frame->magic = CORE_ESPNOW_IMU_MAGIC;
|
|
147
|
+
frame->version = CORE_ESPNOW_IMU_VERSION;
|
|
148
|
+
frame->frame_type = CORE_ESPNOW_FRAME_IMU_RAW;
|
|
149
|
+
frame->id = id;
|
|
150
|
+
frame->seq = seq;
|
|
151
|
+
frame->age_ms = age_ms;
|
|
152
|
+
frame->accel_x = ax; frame->accel_y = ay; frame->accel_z = az;
|
|
153
|
+
frame->gyro_x = gx; frame->gyro_y = gy; frame->gyro_z = gz;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The init function is the seam — add a field, the init function gets a new argument, every caller updates. The wire size assertion catches a missed caller at compile time.
|
|
158
|
+
|
|
159
|
+
## Sending and receiving
|
|
160
|
+
|
|
161
|
+
```cpp
|
|
162
|
+
// Send: copy struct bytes into a flat buffer, prefix with length, hand to ESP-NOW.
|
|
163
|
+
core_espnow_imu_frame_v1_t f{};
|
|
164
|
+
espnow_imu_frame_init(&f, id, seq, age_ms, ax, ay, az, gx, gy, gz);
|
|
165
|
+
uint8_t wire[2 + sizeof(f)];
|
|
166
|
+
wire[0] = sizeof(f) & 0xff;
|
|
167
|
+
wire[1] = (sizeof(f) >> 8) & 0xff;
|
|
168
|
+
memcpy(wire + 2, &f, sizeof(f));
|
|
169
|
+
esp_now_send(peer, wire, sizeof(wire));
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```cpp
|
|
173
|
+
// Receive: validate length, magic, version; then consume fields.
|
|
174
|
+
void on_recv(const uint8_t *data, size_t len) {
|
|
175
|
+
if (len < 2 + sizeof(core_espnow_imu_frame_v1_t)) return;
|
|
176
|
+
uint16_t want = (uint16_t)data[0] | ((uint16_t)data[1] << 8);
|
|
177
|
+
if (want != sizeof(core_espnow_imu_frame_v1_t)) return;
|
|
178
|
+
core_espnow_imu_frame_v1_t f;
|
|
179
|
+
memcpy(&f, data + 2, sizeof(f));
|
|
180
|
+
if (f.magic != CORE_ESPNOW_IMU_MAGIC || f.version != CORE_ESPNOW_IMU_VERSION) return;
|
|
181
|
+
handle_imu(&f);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Common pitfalls
|
|
186
|
+
|
|
187
|
+
- **No `static_assert`.** A field added in one header silently breaks every receiver. Always lock `sizeof`.
|
|
188
|
+
- **`int` or `long` fields.** Their size varies across compilers and platforms; never use them on the wire.
|
|
189
|
+
- **Natural alignment assumed.** Without `__attribute__((packed))`, the compiler may insert padding that you cannot see at the source level.
|
|
190
|
+
- **Magic-byte collision.** Two frame families accidentally using the same 2-byte magic. Pick from a sparse range; document in `docs/transport_payload_reference.md`.
|
|
191
|
+
- **Sending the struct directly via ESP-NOW.** ESP-NOW needs a flat byte buffer and a length prefix; sending `&frame, sizeof(frame)` skips the length prefix and confuses the receiver's parser.
|
|
192
|
+
- **Endianness assumptions on the host.** If a host-side tool (Rust gateway, Python decoder) reads the bytes, it must apply the same byte order. Document in `docs/transport_payload_reference.md`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# idf_env.sh — print the source line for the ESP-IDF environment and verify idf.py is reachable.
|
|
3
|
+
# Run as: bash scripts/idf_env.sh
|
|
4
|
+
# Exits 0 if `idf.py` is on PATH after a (dry-run) env probe; 1 otherwise.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Find the repo root from the skill directory. We resolve the script's real
|
|
9
|
+
# path so it works whether invoked from the skill dir or anywhere else.
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
|
|
12
|
+
# Heuristic: the project root is two levels up from scripts/ inside the skill.
|
|
13
|
+
# Skill layout: <repo>/scripts/<this>.sh → PROJECT_ROOT is SCRIPT_DIR's parent.
|
|
14
|
+
PROJECT_ROOT_DEFAULT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
15
|
+
|
|
16
|
+
PROJECT_ROOT="${PROJECT_ROOT:-${PROJECT_ROOT_DEFAULT}}"
|
|
17
|
+
|
|
18
|
+
# The vendored ESP-IDF is conventionally at <repo>/esp/esp-idf.
|
|
19
|
+
IDF_DIR_CANDIDATE="${PROJECT_ROOT}/esp/esp-idf"
|
|
20
|
+
EXPORT_LINE="source ${IDF_DIR_CANDIDATE}/export.sh"
|
|
21
|
+
|
|
22
|
+
if [[ ! -d "${IDF_DIR_CANDIDATE}" ]]; then
|
|
23
|
+
echo "Run: ${EXPORT_LINE} # (note: esp-idf not found at ${IDF_DIR_CANDIDATE})" >&2
|
|
24
|
+
echo "info: set PROJECT_ROOT to override the discovery path" >&2
|
|
25
|
+
echo "info: set IDF_PATH to a different ESP-IDF checkout" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ ! -x "${IDF_DIR_CANDIDATE}/tools/idf.py" ]] && [[ ! -f "${IDF_DIR_CANDIDATE}/tools/idf.py" ]]; then
|
|
30
|
+
echo "Run: ${EXPORT_LINE} # (note: idf.py not present at expected path)" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Probe: source the env in a subshell, then check whether idf.py is on PATH.
|
|
35
|
+
# We avoid printing the env's own banner by sending output to /dev/null.
|
|
36
|
+
if (
|
|
37
|
+
set +e
|
|
38
|
+
# shellcheck disable=SC1091
|
|
39
|
+
source "${IDF_DIR_CANDIDATE}/export.sh" >/dev/null 2>&1
|
|
40
|
+
command -v idf.py >/dev/null 2>&1
|
|
41
|
+
) ; then
|
|
42
|
+
echo "Run: ${EXPORT_LINE}"
|
|
43
|
+
exit 0
|
|
44
|
+
else
|
|
45
|
+
echo "Run: ${EXPORT_LINE} # (env probe failed; check IDF_PATH and Python)" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# size_check.sh — run a project size-budget check.
|
|
3
|
+
# Prefers the AMS7-specific `tools/check_idf_size_budget.py` if present, else falls back to `idf.py size`.
|
|
4
|
+
# Exits 0 on pass, 1 on size violation (AMS7 budget tool) or build failure (fallback).
|
|
5
|
+
# Usage: bash scripts/size_check.sh [--project-dir DIR] [--min-iram-remain BYTES] [--min-dram-remain BYTES]
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
PROJECT_DIR="."
|
|
10
|
+
MIN_IRAM_REMAIN=""
|
|
11
|
+
MIN_DRAM_REMAIN=""
|
|
12
|
+
|
|
13
|
+
while [[ $# -gt 0 ]]; do
|
|
14
|
+
case "$1" in
|
|
15
|
+
--project-dir)
|
|
16
|
+
PROJECT_DIR="${2:?--project-dir requires a value}"
|
|
17
|
+
shift 2
|
|
18
|
+
;;
|
|
19
|
+
--min-iram-remain)
|
|
20
|
+
MIN_IRAM_REMAIN="${2:?--min-iram-remain requires a value}"
|
|
21
|
+
shift 2
|
|
22
|
+
;;
|
|
23
|
+
--min-dram-remain)
|
|
24
|
+
MIN_DRAM_REMAIN="${2:?--min-dram-remain requires a value}"
|
|
25
|
+
shift 2
|
|
26
|
+
;;
|
|
27
|
+
-h|--help)
|
|
28
|
+
sed -n '2,6p' "$0" | sed 's/^# \?//'
|
|
29
|
+
exit 0
|
|
30
|
+
;;
|
|
31
|
+
*)
|
|
32
|
+
echo "size_check.sh: unknown argument: $1" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
# Resolve absolute path so the AMS7 budget tool's argparse sees a clean --project-dir.
|
|
39
|
+
if [[ -d "${PROJECT_DIR}" ]]; then
|
|
40
|
+
PROJECT_DIR="$(cd "${PROJECT_DIR}" && pwd)"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
AMS7_BUDGET="${PROJECT_DIR}/tools/check_idf_size_budget.py"
|
|
44
|
+
|
|
45
|
+
if [[ -f "${AMS7_BUDGET}" ]]; then
|
|
46
|
+
# AMS7-specific path: uses hard thresholds from tools/check_idf_size_budget.py.
|
|
47
|
+
cmd=(python3 "${AMS7_BUDGET}" --project-dir "${PROJECT_DIR}")
|
|
48
|
+
if [[ -n "${MIN_IRAM_REMAIN}" ]]; then
|
|
49
|
+
cmd+=(--min-iram-remain "${MIN_IRAM_REMAIN}")
|
|
50
|
+
fi
|
|
51
|
+
if [[ -n "${MIN_DRAM_REMAIN}" ]]; then
|
|
52
|
+
cmd+=(--min-dram-remain "${MIN_DRAM_REMAIN}")
|
|
53
|
+
fi
|
|
54
|
+
echo "size_check: running AMS7 budget tool: ${cmd[*]}"
|
|
55
|
+
if "${cmd[@]}"; then
|
|
56
|
+
exit 0
|
|
57
|
+
else
|
|
58
|
+
echo "size_check: AMS7 size budget failed" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Fallback: plain idf.py size. This prints IRAM/DRAM/flash but does NOT enforce
|
|
64
|
+
# a hard threshold — exit code is whatever idf.py returns.
|
|
65
|
+
if ! command -v idf.py >/dev/null 2>&1; then
|
|
66
|
+
echo "size_check: idf.py not on PATH and no AMS7 budget tool found." >&2
|
|
67
|
+
echo "size_check: source esp/esp-idf/export.sh first." >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
echo "size_check: no AMS7 budget tool; running 'idf.py size' as a fallback."
|
|
72
|
+
if idf.py -C "${PROJECT_DIR}" size; then
|
|
73
|
+
exit 0
|
|
74
|
+
else
|
|
75
|
+
echo "size_check: idf.py size failed" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: self-improvement
|
|
3
|
+
description: Use when setting up, configuring, or debugging the project-level self-improvement system. Every task records lessons learned to .bizar/AGENTS_SELF_IMPROVEMENT.md for agent behavior improvement across sessions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Self Improvement
|
|
7
|
+
|
|
8
|
+
Project-level learning system. Every task records what worked, what didn't, and what patterns to follow next time — stored in `.bizar/AGENTS_SELF_IMPROVEMENT.md` at the project root.
|
|
9
|
+
|
|
10
|
+
## How It Works
|
|
11
|
+
|
|
12
|
+
1. **Session start**: Odin reads `.bizar/AGENTS_SELF_IMPROVEMENT.md` from project root and factors active rules into routing
|
|
13
|
+
2. **During work**: Agents follow documented patterns and avoid previously-caught mistakes
|
|
14
|
+
3. **Task completion**: Odin dispatches @heimdall to append a structured entry to the file
|
|
15
|
+
4. **Next session**: The cycle repeats — agents get smarter over time
|
|
16
|
+
|
|
17
|
+
## File Format
|
|
18
|
+
|
|
19
|
+
The file lives at `<project-root>/.bizar/AGENTS_SELF_IMPROVEMENT.md`. Structure:
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
# Self Improvement
|
|
23
|
+
|
|
24
|
+
## Active Rules
|
|
25
|
+
<!-- Keep top 5-10 actionable patterns here. Extract from recent entries. -->
|
|
26
|
+
|
|
27
|
+
## Log
|
|
28
|
+
|
|
29
|
+
### 2026-06-16: Brief descriptive title
|
|
30
|
+
- **Context**: What was the task
|
|
31
|
+
- **Lesson**: What we learned
|
|
32
|
+
- **Pattern**: What to do next time
|
|
33
|
+
- **Files**: src/foo.ts, src/bar.ts
|
|
34
|
+
- **Agent**: thor, tyr
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Entry Rules for Agents
|
|
38
|
+
|
|
39
|
+
When writing an entry:
|
|
40
|
+
|
|
41
|
+
- **File per project** — `.bizar/AGENTS_SELF_IMPROVEMENT.md` at the root of whatever project you're working in
|
|
42
|
+
- **If file doesn't exist**, create it with the header template
|
|
43
|
+
- **Entry format**: H3 date header, bullet list with Context, Lesson, Pattern, Files, Agent
|
|
44
|
+
- **Active Rules**: At the top, keep 5-10 distilled patterns from recent entries. If adding a new entry makes it necessary, add a rule too or promote a pattern from an entry.
|
|
45
|
+
- **Deduplicate**: Don't repeat the same lesson. If the same lesson comes up again, update the existing entry's date instead.
|
|
46
|
+
- **Be specific**: "Always use strictNullChecks" not "TypeScript is good"
|
|
47
|
+
- **Agent tag**: Use the subagent name (thor, tyr, heimdall, mimir, etc.)
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
For a new project where no such file exists yet, create the initial file:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
# Self Improvement
|
|
55
|
+
|
|
56
|
+
Template for project-specific agent learning. Entries are auto-appended by Odin
|
|
57
|
+
at task completion and read at session start.
|
|
58
|
+
|
|
59
|
+
## Active Rules
|
|
60
|
+
|
|
61
|
+
<!-- Top 5-10 actionable patterns extracted from recent entries -->
|
|
62
|
+
|
|
63
|
+
## Log
|
|
64
|
+
```
|