dsh-embedded-workbench 0.7.1
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.en-US.md +262 -0
- package/README.md +260 -0
- package/cordis.patch.yml +11 -0
- package/lib/index.js +197 -0
- package/lib/types/index.d.ts +43 -0
- package/package.json +81 -0
- package/skills/c-cpp-dev/SKILL.md +121 -0
- package/skills/debug-methodology/SKILL.md +83 -0
- package/skills/debug-methodology/references/iterative-debug-case-study.md +103 -0
- package/skills/embedded-firmware-dev/SKILL.md +133 -0
- package/skills/embedded-firmware-dev/references/architecture-principles.md +204 -0
- package/skills/embedded-firmware-dev/references/embedded-patterns.md +95 -0
- package/skills/embedded-firmware-dev/references/lvgl-pitfalls.md +68 -0
- package/skills/embedded-workbench/SKILL.md +240 -0
- package/skills/embedded-workbench/references/INDEX.md +88 -0
- package/skills/embedded-workbench/references/audit-ledger.md +38 -0
- package/skills/embedded-workbench/references/contract-matrix.md +31 -0
- package/skills/embedded-workbench/references/decision-log.md +31 -0
- package/skills/embedded-workbench/references/detailed-change-plan.md +76 -0
- package/skills/embedded-workbench/references/durable-requirement-notes.md +27 -0
- package/skills/embedded-workbench/references/final-qc.md +40 -0
- package/skills/embedded-workbench/references/iteration-notes.md +53 -0
- package/skills/embedded-workbench/references/platform-tool-mapping.md +88 -0
- package/skills/embedded-workbench/references/result-note.md +54 -0
- package/skills/embedded-workbench/references/steward-memo.md +54 -0
- package/skills/embedded-workbench/references/task-charter.md +53 -0
- package/skills/hardfault-triage/SKILL.md +237 -0
- package/skills/keil-mdk-build/SKILL.md +237 -0
- package/skills/state-machine-design/SKILL.md +190 -0
- package/src/index.ts +209 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: keil-mdk-build
|
|
3
|
+
description: "Use when building, flashing, or packaging firmware with Keil MDK (UV4 CLI, ARMCLANG), analyzing .map files for ROM/RAM optimization and memory budget, or diagnosing Keil-specific build failures. NOT for non-Keil build systems (Makefile, CMake, IAR, GCC-only). For crash triage see hardfault-triage."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<HARD-GATE>
|
|
7
|
+
This is a domain implementation skill. If you are planning, designing, or entering plan mode — load `Skill("embedded-workbench")` first to activate the workflow gates (Plan Verification Gate, Approval Gate, Closure Gate). Domain skills carry implementation guidance, not workflow enforcement.
|
|
8
|
+
</HARD-GATE>
|
|
9
|
+
|
|
10
|
+
# Keil MDK Build
|
|
11
|
+
|
|
12
|
+
Patterns for building embedded firmware with Keil MDK. Covers both ARM Compiler 5 (armcc) and ARM Compiler 6 (armclang).
|
|
13
|
+
|
|
14
|
+
## UV4 CLI Build (Authoritative)
|
|
15
|
+
|
|
16
|
+
UV4.exe batch mode is the canonical build path. The Python CLI reimplementation is useful for CI but may miss include paths.
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
<Keil>\UV4\UV4.exe -b project.uvprojx -t TargetName -j0 -o <log_path>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Replace `<Keil>` with the Keil install root. Auto-discover by checking common locations or ask the user when unknown.
|
|
23
|
+
|
|
24
|
+
**Flags**:
|
|
25
|
+
|
|
26
|
+
| Flag | Meaning |
|
|
27
|
+
| ---- | ------- |
|
|
28
|
+
| `-b` | Batch mode (no GUI) |
|
|
29
|
+
| `-t <target>` | Target name within the multi-target project |
|
|
30
|
+
| `-j0` | Auto-parallelism (use all cores) |
|
|
31
|
+
| `-o <path>` | Log output file |
|
|
32
|
+
|
|
33
|
+
**Exit codes**:
|
|
34
|
+
|
|
35
|
+
| Code | Meaning |
|
|
36
|
+
| :--: | ------- |
|
|
37
|
+
| 0 | Success (no errors, no warnings) |
|
|
38
|
+
| 1 | Warnings but no errors |
|
|
39
|
+
| 2 | Errors |
|
|
40
|
+
| 3 | Errors (when `<StopOnExitCode>3</StopOnExitCode>` is set in uvprojx) |
|
|
41
|
+
|
|
42
|
+
**Critical: Log path resolution** — The `-o` path is resolved **relative to the `.uvprojx` file's directory**, not the current working directory. Always use an absolute path or a path under a known-existing subdirectory (e.g., `objects\`) of the project directory.
|
|
43
|
+
|
|
44
|
+
**Recommended invocation** — UV4 produces no stdout; use `Start-Process` with `-Wait -PassThru` and check `ExitCode`:
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
$keil = "<Keil_install_root>" # Ask user or auto-discover
|
|
48
|
+
$log = Join-Path (Get-Location) "build.log"
|
|
49
|
+
$p = Start-Process -FilePath "$keil\UV4\UV4.exe" `
|
|
50
|
+
-ArgumentList "-b project.uvprojx -t Target -j0 -o $log" `
|
|
51
|
+
-Wait -PassThru -NoNewWindow
|
|
52
|
+
if ($p.ExitCode -ne 0) { throw "Build failed (exit $($p.ExitCode))" }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Compiler Selection
|
|
56
|
+
|
|
57
|
+
Keil MDK supports two compiler generations. Identify which one the project uses before generating commands.
|
|
58
|
+
|
|
59
|
+
| Compiler | Keil Name | Binary | Install Path | Check Version |
|
|
60
|
+
|----------|-----------|--------|-------------|---------------|
|
|
61
|
+
| ARM Compiler 5 | AC5 | `armcc` | `<Keil>\ARM\ARMCC` | `armcc --vsn` |
|
|
62
|
+
| ARM Compiler 6 | AC6 | `armclang` | `<Keil>\ARM\ARMCLANG*` | `armclang --version` |
|
|
63
|
+
|
|
64
|
+
**How to identify**: Check the `.uvprojx` XML for `<ARMCC>` (AC5) or `<ARMCLANG>` (AC6) sections. A project can mix both — check per-file or per-group settings.
|
|
65
|
+
|
|
66
|
+
## ARM Compiler 5 (armcc)
|
|
67
|
+
|
|
68
|
+
Legacy compiler, still common in long-lived projects. Uses its own flag syntax, incompatible with AC6.
|
|
69
|
+
|
|
70
|
+
**Detection**: Installed at `<Keil>\ARM\ARMCC`. Run `armcc --vsn` to verify.
|
|
71
|
+
|
|
72
|
+
**Optimization levels** — AC5 has two orthogonal axes: optimization level (`-On`) and optimization goal (`-Ospace` vs `-Otime`). Keil's UI combines them into a single dropdown:
|
|
73
|
+
|
|
74
|
+
| Keil Level | AC5 Flags (actual) | Effect |
|
|
75
|
+
| :----------: | -------------------- | -------- |
|
|
76
|
+
| 0 | `-O0` | Minimum optimization, best debug view |
|
|
77
|
+
| 1 | `-O1` | Restricted optimization, good debug view |
|
|
78
|
+
| 2 | `-O2` | High optimization (AC5 **default**) |
|
|
79
|
+
| 3 | `-O3` | Maximum optimization |
|
|
80
|
+
| 4 | `-O3 -Otime` | Max optimization + favor speed over size |
|
|
81
|
+
|
|
82
|
+
`-Ospace` is the default goal at levels 0-3 (favor smaller code). `-Otime` swaps to favor speed. These are separate from the `-On` level.
|
|
83
|
+
|
|
84
|
+
Sources: ARM Compiler v5.06 User Guide ([DUI0472M](https://developer.arm.com/documentation/dui0472m)), §3.154-3.159.
|
|
85
|
+
|
|
86
|
+
**Key flags**:
|
|
87
|
+
|
|
88
|
+
- `--cpu Cortex-M4` (adjust to target MCU; use `--cpu=list` to see supported targets)
|
|
89
|
+
- `--c99` or `--c11` (language standard; C90 is the AC5 default)
|
|
90
|
+
- `--gnu` (enable GNU extensions if project relies on them)
|
|
91
|
+
- `--apcs=/interwork` (ARM/Thumb interworking)
|
|
92
|
+
- `-c` (compile only, no link)
|
|
93
|
+
- `--split_sections` (equivalent to `-ffunction-sections -fdata-sections`)
|
|
94
|
+
|
|
95
|
+
**Warning control**: `--diag_suppress=<id>` to suppress specific warnings; `--diag_error=<id>` to promote to error.
|
|
96
|
+
|
|
97
|
+
## ARM Compiler 6 (armclang)
|
|
98
|
+
|
|
99
|
+
LLVM-based, current generation. Installed at `<Keil>\ARM\ARMCLANG*`.
|
|
100
|
+
|
|
101
|
+
**Detection**: Search common install roots for directories matching `ARMCLANG*`. Validate with `armclang --version`. If detection fails, ask the user.
|
|
102
|
+
|
|
103
|
+
**Optimization levels**:
|
|
104
|
+
|
|
105
|
+
| Keil Level | AC6 Flag |
|
|
106
|
+
| :----------: | ------ |
|
|
107
|
+
| 0 | `-O0` |
|
|
108
|
+
| 1 | `-O1` |
|
|
109
|
+
| 2 | `-O2` |
|
|
110
|
+
| 3 | `-O3` |
|
|
111
|
+
| 4 | `-Os` |
|
|
112
|
+
| 5 | `-Ofast` |
|
|
113
|
+
|
|
114
|
+
**Key flags for Cortex-M**:
|
|
115
|
+
|
|
116
|
+
- `--target=arm-arm-none-eabi -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard` (adjust MCU/FPU to target)
|
|
117
|
+
- `-c` (compile only, no link)
|
|
118
|
+
- `-ffunction-sections -fdata-sections` (enable linker garbage collection)
|
|
119
|
+
- `-fshort-enums -fshort-wchar` (common embedded defaults)
|
|
120
|
+
|
|
121
|
+
## AC5 to AC6 Migration Traps
|
|
122
|
+
|
|
123
|
+
Projects migrating from AC5 to AC6 commonly hit these issues. Sources: [ARM Compiler Migration Guide (DUI0742)](https://developer.arm.com/documentation/dui0742), [Arm Compiler for Embedded FuSa Migration Guide](https://developer.arm.com/documentation/109444).
|
|
124
|
+
|
|
125
|
+
| AC5 | AC6 | Trap |
|
|
126
|
+
| ----- | ----- | ------ |
|
|
127
|
+
| `__packed struct { ... }` | `struct __attribute__((packed, aligned(1))) { ... }` | AC5 keyword silently ignored by AC6; struct layout changes. `aligned(1)` ensures no implicit alignment. |
|
|
128
|
+
| `__irq void Handler()` | `void Handler(void) __attribute__((interrupt))` | AC5 attribute not recognized; ISR stack frame broken. Alternatively use CMSIS `IRQn_Type`. |
|
|
129
|
+
| `__asm { ... }` | `__asm volatile ("..." : : : )` | Inline assembly switches from armasm syntax to GAS (GNU assembler) syntax with GCC-style operand constraints. |
|
|
130
|
+
| `--c99` | `-std=c99` | Flag syntax differs; AC6 defaults to gnu11 |
|
|
131
|
+
| `--gnu` | `-fgnu89-inline` | GNU inline semantics differ between compilers |
|
|
132
|
+
| `--diag_suppress=<n>` | `-Wno-<name>` | Warning names differ; numbers don't map 1:1. Use AC6 `-Weverything` to list available warnings. |
|
|
133
|
+
| `#pragma diag_suppress` | `#pragma clang diagnostic ignored "-Wname"` | Pragma syntax differs |
|
|
134
|
+
| `char` unsigned by default | `-funsigned-char` | AC5 defaulted to `unsigned char`; AC6 defaults to signed. Add flag to preserve behavior. |
|
|
135
|
+
|
|
136
|
+
**Migration verification**: After switching compiler, compare `.map` file sizes and symbol lists. Unexpected size changes often indicate a packing or inlining difference.
|
|
137
|
+
|
|
138
|
+
## Build Lifecycle
|
|
139
|
+
|
|
140
|
+
A full Keil build has 5 stages:
|
|
141
|
+
|
|
142
|
+
1. **Pre-build** — Version header generation, manifest updates
|
|
143
|
+
2. **Compile** — `.c`/`.cpp`/`.s` → `.o` via armcc (AC5) or armclang (AC6)
|
|
144
|
+
3. **Link** — `armlink --via=<response>.lnp` → `.axf`
|
|
145
|
+
4. **Post-build (fromelf)** — `fromelf --bin objects/app.axf → application.bin`
|
|
146
|
+
5. **Post-build (merge)** — Merge application BIN + filesystem + bootloader into flash image
|
|
147
|
+
|
|
148
|
+
If bypassing UV4 (CI build), all 5 stages must be replicated. The Python CLI build tool handles this internally.
|
|
149
|
+
|
|
150
|
+
## Merge / Packaging
|
|
151
|
+
|
|
152
|
+
Embedded firmware packages typically merge multiple components into a single flash image.
|
|
153
|
+
|
|
154
|
+
### Bootloader Selection (Parity Rule)
|
|
155
|
+
|
|
156
|
+
A common pattern: use `version.build` parity to select development vs. production bootloader.
|
|
157
|
+
|
|
158
|
+
- Parse `version.build` as hexadecimal (not decimal)
|
|
159
|
+
- **Odd** → production bootloader
|
|
160
|
+
- **Even** → development bootloader
|
|
161
|
+
|
|
162
|
+
This is the most common source of confusion — `0x10` (hex) is even, even though "16" as a decimal number looks like it could be interpreted differently.
|
|
163
|
+
|
|
164
|
+
### Manifest Structure
|
|
165
|
+
|
|
166
|
+
Merged firmware images include a manifest at a fixed address with:
|
|
167
|
+
|
|
168
|
+
- Version fields (major, minor, patch, build)
|
|
169
|
+
- Component sizes (firmware, filesystem)
|
|
170
|
+
- CRC32 checksums for each component
|
|
171
|
+
- Magic number for validation
|
|
172
|
+
|
|
173
|
+
Components are typically padded to alignment boundaries before CRC calculation.
|
|
174
|
+
|
|
175
|
+
### Non-Standard CRC32
|
|
176
|
+
|
|
177
|
+
Embedded firmware CRC32 often differs from the standard `zlib`/`crc32` implementation:
|
|
178
|
+
|
|
179
|
+
- **Byte-swapped within each word** (MCU word order)
|
|
180
|
+
- No reflection (forward bit order)
|
|
181
|
+
- No final XOR
|
|
182
|
+
- Polynomial: `0x104C11DB7`
|
|
183
|
+
|
|
184
|
+
Verify the CRC implementation against a known-good reference before trusting any reimplementation.
|
|
185
|
+
|
|
186
|
+
## Common Build Failures
|
|
187
|
+
|
|
188
|
+
| Failure | Cause | Fix |
|
|
189
|
+
| --------- | ------- | ----- |
|
|
190
|
+
| UV4 log written to wrong location | `-o` path is relative to uvprojx directory | Use absolute path |
|
|
191
|
+
| CLI build: missing CMSIS headers | Pack directory detection incomplete | Use UV4 CLI (`UV4.exe -b`) for authoritative builds |
|
|
192
|
+
| CLI build: no compile entries | `.dep` file stale or from different target | Run Keil IDE build first to regenerate |
|
|
193
|
+
| merge: input file not found | fromelf step didn't produce `application.bin` | Check after-build hooks; ensure fromelf completed |
|
|
194
|
+
| Wrong bootloader selected | `version.build` parsed as decimal instead of hex | Always parse build number as hexadecimal |
|
|
195
|
+
| "file not found" for OTA component | Filesystem image not generated | Build filesystem assets before merge step |
|
|
196
|
+
| AC6: struct layout differs from AC5 | `__packed` ignored by AC6 | Replace with `__attribute__((packed))` |
|
|
197
|
+
| AC6: ISR crashes after migration | `__irq` attribute not recognized | Use `__attribute__((interrupt))` or CMSIS `IRQn_Type` |
|
|
198
|
+
| AC6: inline asm syntax errors | AC5 `__asm { }` in sources | Rewrite as `__asm volatile ("...")` |
|
|
199
|
+
| Linker: "No section matches selector" | Scatter file syntax differs between AC5/AC6 | Check scatter file against compiler docs; AC5 uses different section naming |
|
|
200
|
+
|
|
201
|
+
## MAP File Analysis
|
|
202
|
+
|
|
203
|
+
The `.map` file (at `<listings>/<target>.map`) is the linker's memory blueprint. Make it a habit to review after each build.
|
|
204
|
+
|
|
205
|
+
### Key Sections
|
|
206
|
+
|
|
207
|
+
| Section | What It Tells You |
|
|
208
|
+
| --------- | ------------------- |
|
|
209
|
+
| **Image component sizes** | Per-file Code / RO Data / RW Data / ZI Data breakdown. Find the bloat. |
|
|
210
|
+
| **Memory Map of the image** | Flash and RAM layout: load regions, execution regions, stack, heap |
|
|
211
|
+
| **Global Symbols** | Every function/variable address and size — essential for HardFault analysis |
|
|
212
|
+
| **Removing unused sections** | What the linker eliminated. Check for unexpected removals. |
|
|
213
|
+
| **Cross References** | Which `.o` calls which. Trace startup and verify call graphs. |
|
|
214
|
+
|
|
215
|
+
### Size Optimization Workflow
|
|
216
|
+
|
|
217
|
+
1. Sort **Image component sizes** by Code + RO Data. Focus on the top 5 files.
|
|
218
|
+
2. In **Global Symbols**, find functions with large `Size` values. Consider splitting or rewriting.
|
|
219
|
+
3. Check `.constdata` / `.rodata` for debug strings — guard with `#if` or move to runtime generation.
|
|
220
|
+
4. In **Memory Map**, look for `PAD` entries — these are alignment waste. Reorder struct members to minimize.
|
|
221
|
+
5. Verify MicroLIB is enabled; check that no accidental `printf`/`sprintf`/`malloc` drags in heavy library code.
|
|
222
|
+
6. Keep an optimization log: ROM/RAM before and after each change.
|
|
223
|
+
|
|
224
|
+
### Memory Budget Verification
|
|
225
|
+
|
|
226
|
+
From the **Memory Map** section:
|
|
227
|
+
|
|
228
|
+
- **Load Region LR_IROM1** size = total Flash used (Code + RO + RW initial values)
|
|
229
|
+
- **Execution Region RW_IRAM1** = total RAM used (RW data + ZI data + Stack + Heap)
|
|
230
|
+
- **RW Data** consumes BOTH Flash and RAM — initial values stored in Flash, copied to RAM at startup
|
|
231
|
+
- **ZI Data** consumes RAM only — zero-initialized at startup
|
|
232
|
+
|
|
233
|
+
Check that Stack + Heap sizes match the worst-case call chains (from `.htm` call graph) plus margin.
|
|
234
|
+
|
|
235
|
+
## HardFault / Exception Triage
|
|
236
|
+
|
|
237
|
+
For crash analysis — fault registers, stack-frame capture, PC-to-source resolution, root-cause classification — load `Skill("hardfault-triage")`. The `.map` file sections described above (Global Symbols, Memory Map) are the bridge between the two skills: build the `.map` here, debug the crash there.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: state-machine-design
|
|
3
|
+
description: "Use when reviewing or fixing async protocols, retries, ACK/NACK handling, pending flags, timeout logic, or state-machine lockups in embedded firmware. NOT for generic network protocol design (TCP/HTTP/MQTT) unless targeting embedded firmware stack."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<HARD-GATE>
|
|
7
|
+
This is a domain implementation skill. If you are planning, designing, or entering plan mode — load `Skill("embedded-workbench")` first to activate the workflow gates (Plan Verification Gate, Approval Gate, Closure Gate). Domain skills carry implementation guidance, not workflow enforcement.
|
|
8
|
+
</HARD-GATE>
|
|
9
|
+
|
|
10
|
+
# State Machine Design
|
|
11
|
+
|
|
12
|
+
## Core Rules
|
|
13
|
+
|
|
14
|
+
- Fix the state model, not the symptom. Every in-progress or pending flag must have explicit success, failure, timeout, and reset exits.
|
|
15
|
+
- Timeout logic must be gated on real pending work. Idle states must not trigger retry, recovery, or error transitions.
|
|
16
|
+
- Do not trust a low-level send return value as proof of delivery when an application-layer ACK exists. Use the protocol's completion signal.
|
|
17
|
+
- When adding retries, also define attempt timestamps, backoff rules, and cleanup paths so the state machine cannot lock up silently.
|
|
18
|
+
- If pause, stop, or reconnect can interrupt the normal flow, add an explicit recovery or re-drive branch instead of assuming the old path will naturally resume.
|
|
19
|
+
|
|
20
|
+
## Transition Gates
|
|
21
|
+
|
|
22
|
+
- When a state transition depends on multiple preconditions, verify every one explicitly at the transition gate. Do not rely on implicit assumptions (e.g., "the timer expired, therefore everything must be healthy"). A single unchecked precondition is the most common source of silent state corruption.
|
|
23
|
+
- If a target state's preconditions can become false while already in that state, define a reverse transition back to the source state. One-way state latches without fallback paths will eventually leak incorrect state to downstream consumers.
|
|
24
|
+
|
|
25
|
+
## Transient Tolerance
|
|
26
|
+
|
|
27
|
+
- Distinguish between genuine state-changing events and transient perturbations during mode switches, direction reversals, or re-initialization windows. The latter need a tolerance or grace window; only the former should advance the state machine or increment error counters.
|
|
28
|
+
|
|
29
|
+
## Implementation Patterns
|
|
30
|
+
|
|
31
|
+
### Pattern A: Per-State Handlers + Unified Error Gate
|
|
32
|
+
|
|
33
|
+
Each state gets its own handler function. The dispatcher is a pure `switch(state)`. A unified fault-threshold check runs **after all** state handlers — no handler triggers the error transition itself. This keeps handlers simple and fault logic centralized.
|
|
34
|
+
|
|
35
|
+
```c
|
|
36
|
+
// === State enum: exactly one valid state at all times ===
|
|
37
|
+
typedef enum {
|
|
38
|
+
COMM_STATE_INIT,
|
|
39
|
+
COMM_STATE_IDLE,
|
|
40
|
+
COMM_STATE_SAMPLE_STARTING,
|
|
41
|
+
COMM_STATE_SAMPLING,
|
|
42
|
+
COMM_STATE_ERROR,
|
|
43
|
+
COMM_STATE_RECOVERING,
|
|
44
|
+
} comm_state_t;
|
|
45
|
+
|
|
46
|
+
// === Runtime context: all flags explicit in one struct ===
|
|
47
|
+
typedef struct {
|
|
48
|
+
comm_state_t state;
|
|
49
|
+
uint32_t command_fail_count;
|
|
50
|
+
bool data_ready;
|
|
51
|
+
bool communication_lost;
|
|
52
|
+
} comm_runtime_t;
|
|
53
|
+
|
|
54
|
+
// === Per-state handlers: each reads only what it needs ===
|
|
55
|
+
static void comm_handle_idle(comm_runtime_t *rt) {
|
|
56
|
+
rt->warmup_start_time = sys_tick();
|
|
57
|
+
comm_start_sample();
|
|
58
|
+
rt->state = COMM_STATE_SAMPLE_STARTING;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static void comm_handle_error(comm_runtime_t *rt) {
|
|
62
|
+
static uint32_t retry_tick = 0;
|
|
63
|
+
if (retry_tick == 0) {
|
|
64
|
+
retry_tick = sys_tick();
|
|
65
|
+
rt->data_ready = false;
|
|
66
|
+
}
|
|
67
|
+
comm_power_off();
|
|
68
|
+
if (sys_tick() - retry_tick < 500) return; // 500ms cooldown
|
|
69
|
+
retry_tick = 0;
|
|
70
|
+
rt->state = COMM_STATE_RECOVERING;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static void comm_handle_recovering(comm_runtime_t *rt) {
|
|
74
|
+
comm_handle_initializing(rt); // Recovery RE-USES init — no duplicated paths
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// === Dispatcher: pure switch, single exit ===
|
|
78
|
+
static void comm_state_process(comm_runtime_t *rt) {
|
|
79
|
+
switch (rt->state) {
|
|
80
|
+
case COMM_STATE_INIT: comm_handle_initializing(rt); break;
|
|
81
|
+
case COMM_STATE_IDLE: comm_handle_idle(rt); break;
|
|
82
|
+
case COMM_STATE_SAMPLE_STARTING: comm_handle_sample_starting(rt);break;
|
|
83
|
+
case COMM_STATE_SAMPLING: comm_handle_sampling(rt); break;
|
|
84
|
+
case COMM_STATE_ERROR: comm_handle_error(rt); break;
|
|
85
|
+
case COMM_STATE_RECOVERING: comm_handle_recovering(rt); break;
|
|
86
|
+
default:
|
|
87
|
+
rt->state = COMM_STATE_INIT; // Unknown state → safe fallback
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Unified error gate: checked AFTER every state, not buried inside handlers.
|
|
92
|
+
// A new state cannot accidentally bypass this check.
|
|
93
|
+
if (rt->command_fail_count >= COMM_MAX_FAILS) {
|
|
94
|
+
rt->state = COMM_STATE_ERROR;
|
|
95
|
+
rt->command_fail_count = 0;
|
|
96
|
+
rt->communication_lost = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Key properties:
|
|
102
|
+
|
|
103
|
+
- **Fault logic is centralized** — the error gate runs exactly once, after every state. New states cannot bypass it.
|
|
104
|
+
- **Recovery reuses init** — `comm_handle_recovering()` calls `comm_handle_initializing()`. No duplicated paths to drift apart.
|
|
105
|
+
- **All exits are explicit** — `Error` has a cooldown period (500ms), then transitions to `Recovering`. No fall-through, no implicit assumption.
|
|
106
|
+
- **Unknown state → safe fallback** — the `default` case resets to `Init`.
|
|
107
|
+
|
|
108
|
+
### Pattern B: Function-Pointer Table Dispatch
|
|
109
|
+
|
|
110
|
+
Heavier than switch-case, but useful when states are added/removed frequently or handlers need different signatures.
|
|
111
|
+
|
|
112
|
+
```c
|
|
113
|
+
static const struct {
|
|
114
|
+
comm_state_t state;
|
|
115
|
+
void (*process)(void);
|
|
116
|
+
} comm_state_table[] = {
|
|
117
|
+
{COMM_STATE_INIT, comm_init_process},
|
|
118
|
+
{COMM_STATE_IDLE, comm_idle_process},
|
|
119
|
+
{COMM_STATE_CONNECTED, comm_connected_process},
|
|
120
|
+
{COMM_STATE_ERROR, comm_error_process},
|
|
121
|
+
{COMM_STATE_RECOVERING, comm_recovering_process},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
void comm_state_dispatch(void) {
|
|
125
|
+
for (size_t i = 0; i < ARRAY_LEN(comm_state_table); i++) {
|
|
126
|
+
if (g_comm_runtime.state == comm_state_table[i].state
|
|
127
|
+
&& comm_state_table[i].process != NULL) {
|
|
128
|
+
comm_state_table[i].process();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Unknown state: reset to safe default
|
|
133
|
+
g_comm_runtime.state = COMM_STATE_INIT;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Pattern C: ACK Timeout With Explicit Retry Limit
|
|
138
|
+
|
|
139
|
+
All core rules in one function: timeout gated only when work is pending, explicit retry count, predefined max retries, all exits defined.
|
|
140
|
+
|
|
141
|
+
```c
|
|
142
|
+
static void comm_ack_check(uint32_t now_sec) {
|
|
143
|
+
// GUARD: timeout logic only runs when there is real pending work
|
|
144
|
+
if (!g_comm.report_in_progress) return;
|
|
145
|
+
|
|
146
|
+
// GUARD: timeout hasn't expired yet
|
|
147
|
+
if (elapsed_sec(g_comm.send_time, now_sec) < COMM_ACK_TIMEOUT_S) return;
|
|
148
|
+
|
|
149
|
+
// Timeout fired. Explicit retry branch:
|
|
150
|
+
if (g_comm.retry_count == 0) {
|
|
151
|
+
g_comm.retry_count++;
|
|
152
|
+
g_comm.send_time = now_sec;
|
|
153
|
+
comm_send_report(); // One automatic retry
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// All retries exhausted → terminal exit
|
|
157
|
+
g_comm.report_in_progress = false;
|
|
158
|
+
g_comm.retry_count = 0; // Reset for next cycle
|
|
159
|
+
comm_report_result(false); // Notify caller: failed
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Anti-Patterns
|
|
164
|
+
|
|
165
|
+
```c
|
|
166
|
+
// BAD: implicit state via flags — new flag creates untested state combinations
|
|
167
|
+
if (g_flags.busy && !g_flags.paused && g_flags.online) { ... }
|
|
168
|
+
// Fix: use explicit enum — exactly one valid state at all times
|
|
169
|
+
|
|
170
|
+
// BAD: idle state triggers timeout — retry fires with nothing pending
|
|
171
|
+
if (elapsed_ms(t0, now) > TIMEOUT) { retry(); }
|
|
172
|
+
// t0 is always running, even when no work is in flight
|
|
173
|
+
|
|
174
|
+
// BAD: retry loop with no exit condition
|
|
175
|
+
void retry_forever(void) {
|
|
176
|
+
while (!send_packet()) { delay(100); } // Will lock up if HW is dead
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// BAD: recovery path duplicates init logic instead of reusing it.
|
|
180
|
+
// The copy drifts over time — one path gets a fix, the other doesn't.
|
|
181
|
+
|
|
182
|
+
// BAD: Error handler directly calls power_off() without cooldown period.
|
|
183
|
+
// Power-cycling faster than the hardware spec causes unpredictable state.
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## When To Escalate
|
|
187
|
+
|
|
188
|
+
- When diagnostics point to an architecture-level or state-machine design defect, proactively offer high-level remediation focused on boundary clarity, lifecycle contracts, and reversible transitions — don't just propose ad-hoc runtime patches.
|
|
189
|
+
|
|
190
|
+
**REQUIRED SUB-SKILL:** If you find a state machine bug, also load `Skill("debug-methodology")` to apply structured root-cause analysis. If the bug involves async lifecycle flags or hardware events, load `Skill("embedded-firmware-dev")`. If the state machine lockup triggers a watchdog reset or HardFault, load `Skill("hardfault-triage")`.
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embedded-workbench — DeepSeek Harness native plugin for the Embedded
|
|
3
|
+
* Workbench toolbox. Injects the session-start gate text (1% Rule, Red
|
|
4
|
+
* Flags, Plan Verification Gate, skills roster) into the first model step
|
|
5
|
+
* of every agent session, mirroring the SessionStart hook the Claude Code
|
|
6
|
+
* plugin installs. The 7 skills ship in this package's `skills/` directory
|
|
7
|
+
* and are registered at apply time into dsh's `ctx.skills` registry through
|
|
8
|
+
* the standard filesystem provider, so they appear in every session catalog
|
|
9
|
+
* without a manual copy step.
|
|
10
|
+
*
|
|
11
|
+
* Injection listens on agent/pre-step and appends the gate to the FIRST
|
|
12
|
+
* model step that runs, once per session (guarded by the session's durable
|
|
13
|
+
* history). Session-start inbox injection was dropped: a blank-session preset
|
|
14
|
+
* switch (agentPreset.select -> recompose) can clear the inbox before the
|
|
15
|
+
* first step, losing the gate for the whole session. The pre-step decision is
|
|
16
|
+
* the durable path - anchored/bootstrap presets that strip first-step injected
|
|
17
|
+
* reminders (skill catalog, AGENTS.md, gate plugins) simply defer this message
|
|
18
|
+
* to the first step after their promotion, and the history guard re-injects it
|
|
19
|
+
* there. The default gate text is the dsh-native adaptation of
|
|
20
|
+
* `hooks/session-start-content.md`: behavior rules
|
|
21
|
+
* (1% Rule / Red Flags / Plan Verification Gate) stay in sync, while
|
|
22
|
+
* presentation is adapted to dsh's native skill catalog — no roster table
|
|
23
|
+
* (the model sees skills in its catalog) and no install instructions (those
|
|
24
|
+
* live in `.dsh/INSTALL.md`). Deployments override via Config.
|
|
25
|
+
*
|
|
26
|
+
* @module embedded-workbench-dsh
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { fileURLToPath } from 'node:url'
|
|
30
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
31
|
+
import z from '@deepseek-ai/schemastery'
|
|
32
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
33
|
+
import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
|
|
34
|
+
import type { HostCordisInspectProviderRegistration } from '@deepseek-ai/dsh-cordis-host-runner'
|
|
35
|
+
import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem'
|
|
36
|
+
|
|
37
|
+
export const name = 'embedded-workbench'
|
|
38
|
+
|
|
39
|
+
// Skills are contributed through the registry service, which dsh-base always
|
|
40
|
+
// mounts before bundle rows such as this one apply.
|
|
41
|
+
export const inject = ['skills']
|
|
42
|
+
|
|
43
|
+
// Absolute path of the package's shipped skills directory. `lib/index.js`
|
|
44
|
+
// lives one level below the package root, so `../skills` from the module URL
|
|
45
|
+
// lands on `<package>/skills` regardless of where the package was installed.
|
|
46
|
+
const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url))
|
|
47
|
+
|
|
48
|
+
const GATE_PLUGIN_ID = 'embedded-workbench'
|
|
49
|
+
|
|
50
|
+
const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
|
|
51
|
+
Plugin embedded-workbench is active. You have embedded C/C++ firmware development skills — names and "Use when" triggers are in your skill catalog; load them with the skill tool. No custom agents in dsh: use the native subagent tooling for parallel work.
|
|
52
|
+
|
|
53
|
+
**1% Rule**: If there is even a 1% chance a skill applies to your task, invoke it before responding. If the skill turns out to be wrong for the situation, discard it and move on. The cost of loading a skill is trivial compared to the cost of a preventable mistake.
|
|
54
|
+
|
|
55
|
+
**Red Flags** — if you think any of these, STOP. You are rationalizing:
|
|
56
|
+
|
|
57
|
+
| You think | Reality |
|
|
58
|
+
|-----------|---------|
|
|
59
|
+
| "This is just a quick fix" | Quick fixes break things. A 3-line design check costs 30 seconds. |
|
|
60
|
+
| "I already understand this code" | You are looking at one file. The blast radius may span 5 modules. |
|
|
61
|
+
| "The skill is overkill for this" | Simple things become complex. Check for skills. |
|
|
62
|
+
| "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. |
|
|
63
|
+
| "I can just read the file directly" | Skills have patterns and pitfalls you will not discover by reading. |
|
|
64
|
+
| "I remember this skill content" | Skills evolve. Always load the current version. |
|
|
65
|
+
| "I've explored enough, time to exit plan mode" | The exit_plan_mode tool is the verification gate. Have you loaded the logicprobe skill? Every plan must pass this gate before exit. |
|
|
66
|
+
| "This plan is too simple for logicprobe" | The skill auto-classifies depth. You don't decide. |
|
|
67
|
+
| "I already read the code, I know the file paths are correct" | Load the logicprobe skill, run Phase 0, append the "## Plan Verification" block. |
|
|
68
|
+
|
|
69
|
+
**Plan Verification Gate**: Before calling exit_plan_mode (or presenting a plan for approval), either load the logicprobe skill (a separate plugin — if it is missing from your skill catalog, tell the user to install it) OR inform the user "此计划未经 logicprobe 验证,是否需要核查?" Silent skip is not an option.
|
|
70
|
+
|
|
71
|
+
To load workflows and engineering policies: load the embedded-workbench skill.
|
|
72
|
+
|
|
73
|
+
**Proactive features**: When you see state machines, protocol refactoring, behavioral claims ("always"/"never"), or multi-module tasks — suggest verification, adversarial probing, or parallel subagents BEFORE the user asks. Most users do not know these exist.
|
|
74
|
+
</EXTREMELY_IMPORTANT>`
|
|
75
|
+
|
|
76
|
+
export interface Config {
|
|
77
|
+
enabled: boolean
|
|
78
|
+
gateContent: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const Config = z.object({
|
|
82
|
+
enabled: z.boolean().default(true),
|
|
83
|
+
gateContent: z.string().default(DEFAULT_GATE_CONTENT),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
function gateMessage(text: string): UserMessage {
|
|
87
|
+
return createUserMessage({
|
|
88
|
+
content: [{ type: 'text', text }],
|
|
89
|
+
// `form` omitted — an undeclared context is the documented default.
|
|
90
|
+
source: { kind: 'plugin', plugin: GATE_PLUGIN_ID },
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Model-visible catalog entry (cordis_inspect_list / cordis_inspect_query):
|
|
96
|
+
* lets the model read this plugin's runtime status without guessing. Mirrors
|
|
97
|
+
* the registration pattern of the official dsh-tool-cordis host providers.
|
|
98
|
+
*/
|
|
99
|
+
function inspectProvider(config: Config): HostCordisInspectProviderRegistration {
|
|
100
|
+
return {
|
|
101
|
+
manifest: {
|
|
102
|
+
id: 'embedded-workbench',
|
|
103
|
+
description: 'Session-start gate injection for the Embedded Workbench toolbox — folds the 1% Rule / Red Flags / Plan Verification Gate text into the first model step of every agent session.',
|
|
104
|
+
methods: [
|
|
105
|
+
{
|
|
106
|
+
name: 'status',
|
|
107
|
+
description: 'Read whether the gate injection is active and how large the injected gate text is.',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {},
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
},
|
|
113
|
+
outputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
description: 'Gate-injection plugin status.',
|
|
116
|
+
properties: {
|
|
117
|
+
enabled: { type: 'boolean', description: 'Whether the gate folds into the first model step.' },
|
|
118
|
+
gateContentLength: { type: 'integer', description: 'Length in characters of the injected gate text.' },
|
|
119
|
+
},
|
|
120
|
+
required: ['enabled', 'gateContentLength'],
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
query: async (method) => {
|
|
127
|
+
if (method === 'status') {
|
|
128
|
+
return {
|
|
129
|
+
enabled: config.enabled,
|
|
130
|
+
gateContentLength: config.gateContent.length,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function apply(ctx: Context, config: Config): void {
|
|
139
|
+
// Catalog visibility is optional: register only when the inspect registry
|
|
140
|
+
// service is mounted, so headless assemblies without it keep the gate
|
|
141
|
+
// injection working. The registry may be provided AFTER this row applies
|
|
142
|
+
// (base-bundle rows can mount later), so registration is retried on the
|
|
143
|
+
// first agent/session-start — by then the app is fully booted.
|
|
144
|
+
let providerRegistered = false
|
|
145
|
+
const registerProvider = (): void => {
|
|
146
|
+
if (providerRegistered) return
|
|
147
|
+
const inspect = ctx.get('cordisInspect')
|
|
148
|
+
if (inspect === undefined) return
|
|
149
|
+
try {
|
|
150
|
+
ctx.effect(() => inspect.register(inspectProvider(config)), 'embedded-workbench: inspect provider')
|
|
151
|
+
providerRegistered = true
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.warn('[embedded-workbench] inspect provider registration failed', err)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
registerProvider()
|
|
157
|
+
// Ship the bundled skills through the registry: reuse the standard
|
|
158
|
+
// filesystem provider over this package's own `skills/` directory, so
|
|
159
|
+
// catalog discovery, frontmatter parsing, and SKILL.md loading behave
|
|
160
|
+
// exactly like project/user skills while the plugin stays self-contained.
|
|
161
|
+
// Registration lands in the global registry layer (this row mounts at the
|
|
162
|
+
// profile root), so every agent preset sees the skills. `registerProvider`
|
|
163
|
+
// returns the effect disposer; its teardown unregisters and invalidates.
|
|
164
|
+
ctx.skills.registerProvider((control) => {
|
|
165
|
+
return new FileSystemSkillProvider(ctx, control, {
|
|
166
|
+
providerName: 'embedded-workbench',
|
|
167
|
+
includeDefaultRoots: false,
|
|
168
|
+
customSkillDirs: [SKILLS_DIR],
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
if (!config.enabled) return
|
|
172
|
+
// Inject the gate once per session on the FIRST model step that runs,
|
|
173
|
+
// instead of at session-start: session-start injection lands in the agent's
|
|
174
|
+
// inbox, which a blank-session preset switch (agentPreset.select ->
|
|
175
|
+
// recompose) can clear before the first step - the gate would then be lost
|
|
176
|
+
// for the whole session. The pre-step decision is the durable path a
|
|
177
|
+
// first-step injection takes: the gate is appended to the first step's
|
|
178
|
+
// decision and enters session history there, so every later step (and a
|
|
179
|
+
// resume) skips it. Anchored/bootstrap presets that strip first-step
|
|
180
|
+
// injected reminders (skill catalog, AGENTS.md, gate plugins) simply defer
|
|
181
|
+
// this message to the first step after their promotion - the history guard
|
|
182
|
+
// re-injects it there, so the gate still lands exactly once per session.
|
|
183
|
+
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
184
|
+
const decision = await next()
|
|
185
|
+
if (decision.kind === 'reject') return decision
|
|
186
|
+
registerProvider()
|
|
187
|
+
if (gateInHistory(agent.session)) return decision
|
|
188
|
+
return {
|
|
189
|
+
kind: 'enter',
|
|
190
|
+
messages: [...decision.messages, gateMessage(config.gateContent)],
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Whether the gate already entered this session's durable history. The
|
|
197
|
+
* pre-step listener re-appends the gate until it does; once a step committed
|
|
198
|
+
* it, every later step (and a resume of a session that kept it) skips the
|
|
199
|
+
* injection. A session whose gate was dropped before any step ran (e.g. an
|
|
200
|
+
* inbox cleared by a blank-session preset switch) simply re-injects on the
|
|
201
|
+
* first step that runs.
|
|
202
|
+
*/
|
|
203
|
+
function gateInHistory(session: Session): boolean {
|
|
204
|
+
return session.events.some((event) => {
|
|
205
|
+
if (event.type !== 'user/message') return false
|
|
206
|
+
const source = event.data.source
|
|
207
|
+
return source.kind === 'plugin' && source.plugin === GATE_PLUGIN_ID
|
|
208
|
+
})
|
|
209
|
+
}
|