driverge-mcp 0.0.0 → 0.0.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.
Files changed (81) hide show
  1. package/README.md +275 -29
  2. package/dist/codegen/esp32.d.ts +11 -3
  3. package/dist/codegen/esp32.js +221 -10
  4. package/dist/codegen/esp32.js.map +1 -1
  5. package/dist/codegen/ident.d.ts +24 -2
  6. package/dist/codegen/ident.js +37 -4
  7. package/dist/codegen/ident.js.map +1 -1
  8. package/dist/codegen/index.d.ts +12 -7
  9. package/dist/codegen/index.js +17 -14
  10. package/dist/codegen/index.js.map +1 -1
  11. package/dist/codegen/lint.js +28 -8
  12. package/dist/codegen/lint.js.map +1 -1
  13. package/dist/codegen/portable-cpp.d.ts +8 -0
  14. package/dist/codegen/portable-cpp.js +281 -0
  15. package/dist/codegen/portable-cpp.js.map +1 -0
  16. package/dist/codegen/portable.d.ts +99 -3
  17. package/dist/codegen/portable.js +419 -99
  18. package/dist/codegen/portable.js.map +1 -1
  19. package/dist/codegen/stm32.d.ts +11 -3
  20. package/dist/codegen/stm32.js +156 -11
  21. package/dist/codegen/stm32.js.map +1 -1
  22. package/dist/codegen/types.d.ts +32 -0
  23. package/dist/codegen/types.js +27 -0
  24. package/dist/codegen/types.js.map +1 -1
  25. package/dist/mcp/cache.d.ts +15 -2
  26. package/dist/mcp/cache.js +32 -4
  27. package/dist/mcp/cache.js.map +1 -1
  28. package/dist/mcp/register.d.ts +17 -0
  29. package/dist/mcp/register.js +149 -21
  30. package/dist/mcp/register.js.map +1 -1
  31. package/dist/mcp/summary.js +8 -0
  32. package/dist/mcp/summary.js.map +1 -1
  33. package/dist/pdf/address.d.ts +4 -0
  34. package/dist/pdf/address.js +28 -0
  35. package/dist/pdf/address.js.map +1 -0
  36. package/dist/pdf/command.js +87 -14
  37. package/dist/pdf/command.js.map +1 -1
  38. package/dist/pdf/extract.js +23 -2
  39. package/dist/pdf/extract.js.map +1 -1
  40. package/dist/pdf/generic-register-table.d.ts +5 -0
  41. package/dist/pdf/generic-register-table.js +125 -0
  42. package/dist/pdf/generic-register-table.js.map +1 -0
  43. package/dist/pdf/index.d.ts +2 -0
  44. package/dist/pdf/index.js +2 -0
  45. package/dist/pdf/index.js.map +1 -1
  46. package/dist/pdf/interface-kind.d.ts +11 -0
  47. package/dist/pdf/interface-kind.js +18 -0
  48. package/dist/pdf/interface-kind.js.map +1 -1
  49. package/dist/pdf/manufacturer.js +39 -0
  50. package/dist/pdf/manufacturer.js.map +1 -1
  51. package/dist/pdf/maxim-register-map.d.ts +11 -0
  52. package/dist/pdf/maxim-register-map.js +544 -0
  53. package/dist/pdf/maxim-register-map.js.map +1 -0
  54. package/dist/pdf/part.js +8 -0
  55. package/dist/pdf/part.js.map +1 -1
  56. package/dist/pdf/prose-commands.d.ts +4 -0
  57. package/dist/pdf/prose-commands.js +90 -0
  58. package/dist/pdf/prose-commands.js.map +1 -0
  59. package/dist/pdf/st-bit-layout.d.ts +3 -0
  60. package/dist/pdf/st-bit-layout.js +238 -0
  61. package/dist/pdf/st-bit-layout.js.map +1 -0
  62. package/dist/pdf/ti-field-descriptions.d.ts +10 -0
  63. package/dist/pdf/ti-field-descriptions.js +87 -0
  64. package/dist/pdf/ti-field-descriptions.js.map +1 -0
  65. package/dist/pdf/ti-register-map.d.ts +5 -0
  66. package/dist/pdf/ti-register-map.js +84 -0
  67. package/dist/pdf/ti-register-map.js.map +1 -0
  68. package/dist/pdf/types.d.ts +15 -3
  69. package/dist/pdf/types.js +4 -1
  70. package/dist/pdf/types.js.map +1 -1
  71. package/dist/schema/assemble.d.ts +35 -3
  72. package/dist/schema/assemble.js +86 -13
  73. package/dist/schema/assemble.js.map +1 -1
  74. package/dist/schema/types.d.ts +21 -1
  75. package/dist/schema/validate.js +37 -10
  76. package/dist/schema/validate.js.map +1 -1
  77. package/dist/server.d.ts +18 -0
  78. package/dist/server.js +49 -3
  79. package/dist/server.js.map +1 -1
  80. package/package.json +1 -1
  81. package/schemas/datasheet.schema.json +17 -3
package/README.md CHANGED
@@ -4,14 +4,16 @@
4
4
  <p align="center"><em>Datasheet PDF → embedded C/C++ driver, from any MCP client.</em></p>
5
5
 
6
6
  <p align="center">
7
- <img alt="license" src="https://img.shields.io/badge/license-MIT-blue">
7
+ <a href="https://www.npmjs.com/package/driverge-mcp"><img alt="npm" src="https://img.shields.io/npm/v/driverge-mcp"></a>
8
+ <a href="https://github.com/MehmetTopuz/driverge-mcp/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/MehmetTopuz/driverge-mcp/actions/workflows/ci.yml/badge.svg"></a>
9
+ <a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-blue"></a>
8
10
  <img alt="status" src="https://img.shields.io/badge/status-pre--release-orange">
9
11
  <img alt="mcp" src="https://img.shields.io/badge/MCP-server-black">
10
12
  </p>
11
13
 
12
- > 🚧 **Pre-release** — under active development; not yet published to npm. The
13
- > `npx driverge-mcp` command below is the intended install; until it's published,
14
- > [run from source](#run-from-source-pre-release).
14
+ > 🚧 **Early pre-release** — [`driverge-mcp`](https://www.npmjs.com/package/driverge-mcp)
15
+ > is on npm, so the `npx driverge-mcp` install below works today. APIs and the
16
+ > JSON schema may still change before v0.1.0; expect rough edges.
15
17
 
16
18
  ---
17
19
 
@@ -27,15 +29,68 @@ TypeScript pipeline extracts a *validated, structured JSON* model of the chip, a
27
29
  the host AI you're already talking to fills in the reasoning-heavy parts (init
28
30
  sequence, vendor quirks, docs). Your datasheet never leaves your machine.
29
31
 
32
+ ## Quick start
33
+
34
+ Add one entry to your MCP client's config (Claude Desktop, Claude Code, Cursor —
35
+ paths per client in [Installation](#installation)):
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "driverge": { "command": "npx", "args": ["-y", "driverge-mcp"] }
41
+ }
42
+ }
43
+ ```
44
+
45
+ Restart the client, then ask:
46
+
47
+ > "Analyze the datasheet at `C:/ds/bme280.pdf` with driverge, then generate a
48
+ > portable driver."
49
+
50
+ That's it — no clone, no build, no API key. Details in
51
+ [Installation](#installation) and [Usage](#usage).
52
+
53
+ ## Why Driverge?
54
+
55
+ Bringing up a new sensor or IC means hand-transcribing dozens of register
56
+ addresses, bit-field masks, and command codes out of a 40-page PDF — slow work,
57
+ and a classic source of silent bugs (one wrong mask or transposed address and the
58
+ driver "works" but reads garbage). Driverge does that mechanical part
59
+ deterministically and leaves the reasoning to the AI you already use.
60
+
61
+ - **No hallucinated register maps.** Addresses, bit-field masks, and command codes
62
+ are *extracted from the datasheet and validated* — not guessed. That's the
63
+ failure mode of "just ask an LLM to write the whole driver"; here, invalid or
64
+ incomplete data is rejected before it ever reaches code generation.
65
+ - **Bring your own client — no API keys, no lock-in.** Driverge is a plain MCP
66
+ server with no embedded LLM. It runs inside whatever MCP client you already use
67
+ (Claude Desktop, Claude Code, Cursor, …) and reasons with the model you're
68
+ already paying for — no separate subscription or service.
69
+ - **Private & offline.** The datasheet is parsed locally and never uploaded — safe
70
+ for NDA'd or unreleased parts.
71
+ - **Deterministic & reproducible.** The same PDF always yields the same JSON and
72
+ the same driver skeleton — reviewable, diff-able, and testable, not a one-shot
73
+ black box.
74
+ - **Portable by construction.** One driver core targets any platform through a
75
+ tiny per-bus thin-HAL seam (2–3 functions); the native targets (STM32, ESP32)
76
+ pre-fill that seam for you — switch platforms without touching driver logic.
77
+ - **The AI does only what it's good at.** Register geometry is deterministic;
78
+ init-sequence ordering, timing quirks, and compensation math need judgment.
79
+ Driverge marks exactly those spots with `TODO(driverge)` and a `fill_in_brief`,
80
+ the host AI completes them, then `validate_driver` checks the result.
81
+
82
+ **Good for:** quickly evaluating a new sensor, prototyping, porting an existing
83
+ driver to a different MCU, or just learning an unfamiliar chip's register map.
84
+
30
85
  ## What it does
31
86
 
32
87
  1. **Analyze** a datasheet PDF → detect format, manufacturer, and interface kind
33
88
  (register-map vs. command-set), then extract registers / bit-fields (or
34
89
  commands + CRC) and the bus protocol into a **frozen JSON contract**, gated by
35
90
  a validator.
36
- 2. **Generate** a driver for a target platform: a deterministic **thin-HAL
37
- skeleton** — register/bit-field constants, the 5-function HAL seam, function
38
- stubs — with every reasoning gap marked `TODO(driverge)` plus a `fill_in_brief`
91
+ 2. **Generate** a driver for a target platform, in C or C++: a deterministic
92
+ **thin-HAL skeleton** — register/bit-field constants, the per-bus thin-HAL
93
+ seam, function stubs — with every reasoning gap marked `TODO(driverge)` plus a `fill_in_brief`
39
94
  telling the host AI exactly what to complete.
40
95
  3. **Validate** the completed driver: thin-HAL purity, no leftover TODOs, register
41
96
  references exist, bit-field masks match the JSON.
@@ -46,26 +101,132 @@ Every target specializes the same portable **[thin-HAL](https://en.wikipedia.org
46
101
  seam — the driver core is identical across platforms; only the seam implementation
47
102
  changes.
48
103
 
49
- | Target | Bus binding | Language | Status |
50
- |---|---|---|---|
51
- | **Portable (thin-HAL)** | user-implemented `hal_i2c_*` / `hal_spi_*` / `hal_delay_ms` | C | ✅ |
52
- | **ESP32** | ESP-IDF `i2c_master_*` | C | ✅ |
53
- | **STM32** | CubeHAL `HAL_I2C_Mem_Read/Write` | C | ✅ |
54
- | **Arduino** | `Wire` / `SPI` | C++ | planned |
104
+ | Target | Bus binding | Buses | Language | Status |
105
+ |---|---|---|---|---|
106
+ | **Portable (thin-HAL)** | user-implemented `hal_*` seam | I²C, SPI, UART, CAN | C / C++ | ✅ |
107
+ | **ESP32** | ESP-IDF (`i2c_master_*`, `spi_master`, `uart`, TWAI) | C, SPI, UART, CAN | C / C++ | ✅ |
108
+ | **STM32** | CubeHAL (`HAL_I2C_*`, `HAL_SPI_*` + GPIO CS, `HAL_UART_*`) | C, SPI, UART | C / C++ | ✅ |
109
+ | **Arduino** | `Wire` / `SPI` | — | C++ | planned |
55
110
 
56
- - **Offline & private** the datasheet is parsed locally; nothing is uploaded.
57
- - **Deterministic** the same PDF yields the same JSON and the same skeleton.
58
- - **Cross-platform** one parsed model, many target bindings.
111
+ STM32 CAN is planned (the bxCAN/FDCAN family split needs its own pass); asking
112
+ a target for a bus it doesn't support fails fast with a clear
113
+ `UnsupportedBusError` rather than emitting a wrong seam. Pass
114
+ `language: "cpp"` to `generate_driver` for a class-based C++ driver
115
+ (`.hpp`/`.cpp`) instead of the default C output — same registers, same seam,
116
+ same validation.
59
117
 
60
118
  > ⚠️ **Generated code is a strong draft, not a certified driver.** Init sequences,
61
119
  > compensation formulas, and timing quirks are completed by the host AI and
62
120
  > **must be reviewed** before use on hardware. Not safety-certified.
63
121
 
122
+ ### Verified parts
123
+
124
+ The extraction pipeline is regression-tested against real datasheets from
125
+ **12 manufacturers**. Parts with fully automatic extraction (registers *and*
126
+ bit-fields, or a clean command set):
127
+
128
+ | Part | Manufacturer | Kind | Extracted |
129
+ |---|---|---|---|
130
+ | BME280 | Bosch Sensortec | register map | 16 regs, 19 bit-fields |
131
+ | MCP23017 | Microchip | register map | 12 regs, 96 bit-fields |
132
+ | SHT3x | Sensirion | command set | 6 commands + CRC |
133
+ | TMAG5170 | Texas Instruments | register map (SPI) | 21 regs, 79 bit-fields |
134
+ | DHT20 | Aosong | command set | 2 commands |
135
+ | LSM6DSRX | STMicroelectronics | register map | 31 regs, 91 bit-fields |
136
+ | MAX30102 | Maxim Integrated | register map | 20 regs, 33 bit-fields |
137
+
138
+ Other tested parts (ADXL345, MLX90614, AEAT-8811, PCA9685, VL53L3CX, TLE5014)
139
+ extract **partially** or **defer** to the host AI — the pipeline says so
140
+ explicitly instead of guessing, and the generated skeleton tells the host AI
141
+ what to complete. The full, always-current matrix lives in the
142
+ [coverage scorecard](tests/scorecard/scorecard.snap.md).
143
+
144
+ ## Concepts behind Driverge
145
+
146
+ Driverge splits driver-writing into two kinds of work: the **mechanical part**
147
+ (register addresses, masks, command tables — extracted and checked by
148
+ deterministic code) and the **judgment part** (init ordering, timing quirks,
149
+ compensation math — completed by the host AI). Everything below exists to keep
150
+ that boundary sharp.
151
+
152
+ ```mermaid
153
+ flowchart LR
154
+ PDF["Datasheet PDF"]
155
+ subgraph D["Driverge — deterministic, no LLM"]
156
+ A["analyze_datasheet<br>L1–L5 parse + validate"]
157
+ J[("frozen JSON<br>cached under a ref")]
158
+ G["generate_driver<br>thin-HAL skeleton +<br>TODO(driverge) markers"]
159
+ V["validate_driver<br>static lint"]
160
+ end
161
+ subgraph H["Host AI — reasoning"]
162
+ F["fill the TODOs: init sequence,<br>quirks, compensation docs"]
163
+ end
164
+ OUT["driver.c / driver.h"]
165
+ PDF --> A --> J --> G --> F --> V
166
+ V -->|pass| OUT
167
+ V -->|fail| F
168
+ ```
169
+
170
+ ### Deterministic core, reasoning at the edge
171
+
172
+ Register geometry is mechanical: an address is right or wrong, a mask either
173
+ matches the datasheet or it doesn't. Driverge handles that part with plain
174
+ TypeScript — no internal LLM, no API keys, no sampling — so the output is the
175
+ same on every run. What genuinely needs judgment (in what order to poke the
176
+ registers, which timing quirk applies, how to document a compensation formula)
177
+ is left to the host AI you're already talking to.
178
+
179
+ ### The frozen JSON contract
180
+
181
+ `analyze_datasheet` runs a five-stage pipeline (L1–L5): detect the PDF type,
182
+ map keyword pages, identify the manufacturer and interface kind, extract the
183
+ register table (or command set + CRC), and validate the result against a frozen
184
+ draft-07 [JSON-Schema contract](schemas/datasheet.schema.json) — also exposed
185
+ as the `driverge://schema` resource. Anything that fails validation is rejected
186
+ *before* code generation, so a bad extraction can never silently become a bad
187
+ driver.
188
+
189
+ ### The `ref` handle
190
+
191
+ Parsing a datasheet yields a large JSON document; shuttling it through the chat
192
+ context on every call would be slow and lossy. Instead, the parsed model is
193
+ cached server-side under a content-stable `ref`, and the tools pass that handle
194
+ around. The same `ref` with a different `target` re-renders instantly with no
195
+ re-parse, and the full JSON stays readable at `driverge://datasheet/<ref>`.
196
+
197
+ ### The thin-HAL seam
198
+
199
+ Generated drivers touch hardware through a tiny per-bus seam — and nothing
200
+ else:
201
+
202
+ | Bus | Seam functions (plus `hal_delay_ms`) |
203
+ |---|---|
204
+ | I²C | `hal_i2c_read`, `hal_i2c_write` |
205
+ | SPI | `hal_spi_transfer` (one call = one CS-framed transaction) |
206
+ | UART | `hal_uart_write`, `hal_uart_read` |
207
+ | CAN | `hal_can_transfer` (one call = one frame exchange) |
208
+
209
+ The driver core is therefore identical across platforms; a native target
210
+ (ESP32, STM32) just pre-fills the seam with the vendor calls.
211
+ `validate_driver` enforces this purity: a driver that calls a vendor peripheral
212
+ API outside the seam fails the lint. Buses with no universal register-access
213
+ primitive (UART, CAN) keep the same discipline — the device-specific framing is
214
+ a marked `TODO(driverge)` reasoning gap (`framing_todo`), completed by the host
215
+ AI and then linted.
216
+
217
+ ### The fill-in loop
218
+
219
+ The skeleton marks every reasoning gap with a `TODO(driverge)` comment and
220
+ ships a `fill_in_brief` describing what belongs there. The host AI completes
221
+ the markers using the datasheet resource, then `validate_driver` statically
222
+ checks the result — no leftover TODOs, every register reference real, masks
223
+ matching the JSON — and the loop repeats until it passes.
224
+
64
225
  ## Installation
65
226
 
66
- **Prerequisites:** Node.js LTS (≥ 18).
227
+ **Prerequisites:** Node.js LTS (≥ 18; CI-tested on Node 20 & 22).
67
228
 
68
- Once published, add Driverge to your MCP client (no build step —
229
+ Add Driverge to your MCP client (no build step —
69
230
  [npx](https://docs.npmjs.com/cli/commands/npx) fetches and runs it):
70
231
 
71
232
  **Claude Desktop** — `claude_desktop_config.json`:
@@ -98,9 +259,39 @@ Once published, add Driverge to your MCP client (no build step —
98
259
  Other clients (Codex, Gemini CLI, …) take the same `command` + `args` pair in
99
260
  their own MCP config.
100
261
 
101
- ### Run from source (pre-release)
262
+ **No clone, no global install.** The config above tells your MCP client to
263
+ launch `npx -y driverge-mcp`; npx downloads Driverge from the npm registry on
264
+ first run, caches it, and starts it automatically each time the client does
265
+ (`-y` skips npx's install prompt). You never run it by hand — to confirm it's
266
+ wired up, ask your client to run the `ping` tool, which replies `pong`. Cloning
267
+ the repo (below) is only for development.
102
268
 
103
- Until the npm package is published:
269
+ ### Configuration
270
+
271
+ | Env var | Default | Purpose |
272
+ |---|---|---|
273
+ | `DRIVERGE_OUT_ROOT` | server's working directory | Root that `generate_driver`'s `out_dir` writes are confined to. Any `out_dir` that resolves outside this root is rejected (`out_dir "…" escapes the allowed root`). Set it to the directory you want drivers written into. |
274
+
275
+ Set it in the MCP config's `env` block, e.g.:
276
+
277
+ ```json
278
+ {
279
+ "mcpServers": {
280
+ "driverge": {
281
+ "command": "npx",
282
+ "args": ["-y", "driverge-mcp"],
283
+ "env": { "DRIVERGE_OUT_ROOT": "C:/work/drivers" }
284
+ }
285
+ }
286
+ }
287
+ ```
288
+
289
+ Without `out_dir` the generated files are returned in the tool result only —
290
+ no disk writes, no configuration needed.
291
+
292
+ ### Run from source (development)
293
+
294
+ To contribute, or to run the latest unreleased changes:
104
295
 
105
296
  ```bash
106
297
  git clone https://github.com/MehmetTopuz/driverge-mcp.git
@@ -124,8 +315,13 @@ Give your MCP client a datasheet and ask it to build a driver. The typical flow:
124
315
 
125
316
  1. **`analyze_datasheet`** — `{ "pdf_path": "/abs/path/bme280.pdf" }` → returns a
126
317
  compact summary and a `ref` handle; the full JSON is available as a resource.
318
+ If auto-detection picks the wrong vendor or interface style, the optional
319
+ `manufacturer_hint` (free text) and `interface_kind_hint`
320
+ (`"register_map"` | `"command_set"`) parameters steer it.
127
321
  2. **`generate_driver`** — `{ "ref": "…", "target": "portable" }` → returns the
128
- driver files + a `fill_in_brief`. (`out_dir` also writes them to disk.)
322
+ driver files + a `fill_in_brief`. (`out_dir` also writes them to disk;
323
+ `language: "cpp"` renders a class-based `.hpp`/`.cpp` driver instead of the
324
+ default C.)
129
325
  3. The host AI completes the `TODO(driverge)` markers using the brief and the
130
326
  `driverge://datasheet/<ref>` resource.
131
327
  4. **`validate_driver`** — `{ "ref": "…", "files": [...] }` → static checks; loop
@@ -138,11 +334,35 @@ Reusing the same `ref` with a different `target` re-renders with **no re-parse**
138
334
  > "Analyze the datasheet at `C:/ds/bme280.pdf` with driverge, then generate a
139
335
  > portable driver."
140
336
 
141
- Driverge parses the BME280 memory map (14 registers, bit-fields, I²C address),
142
- validates it, and emits `bme280.h` / `bme280.c` — register `#define`s, bit-field
143
- `MASK`/`SHIFT` macros, the thin-HAL seam, and `bme280_init/read_register/
144
- write_register` stubs. The host AI then fills the init sequence and compensation
145
- docs from the datasheet prose.
337
+ Driverge parses the BME280 memory map (16 registers, 19 bit-fields, I²C
338
+ address), validates it, and emits `bme280.h` / `bme280.c` — register
339
+ `#define`s, bit-field `MASK`/`SHIFT` macros, the thin-HAL seam, and the
340
+ `bme280_init/read_register/write_register` stubs:
341
+
342
+ ```c
343
+ /* bme280.h — generated by Driverge (excerpt) */
344
+ #define BME280_I2C_ADDR 0x76
345
+
346
+ #define BME280_REG_CTRL_MEAS 0xF4
347
+ #define BME280_REG_STATUS 0xF3
348
+ /* … */
349
+ #define BME280_TEMP_XLSB_TEMP_XLSB_MASK 0xF0
350
+ #define BME280_TEMP_XLSB_TEMP_XLSB_SHIFT 4
351
+ ```
352
+
353
+ ```c
354
+ /* bme280.c — every reasoning gap is a marked TODO (excerpt) */
355
+ int bme280_init(bme280_t *dev) {
356
+ /* … */
357
+ /* TODO(driverge): implement the power-on / reset init sequence — the
358
+ * correct register write order and values, plus any required startup
359
+ * delay. See fill_in_brief.init_sequence_todo. */
360
+ return 0;
361
+ }
362
+ ```
363
+
364
+ The host AI then fills the init sequence and compensation docs from the
365
+ datasheet prose, and `validate_driver` checks the result.
146
366
 
147
367
  ### MCP surface
148
368
 
@@ -152,16 +372,42 @@ docs from the datasheet prose.
152
372
  | Tool | `generate_driver` | `ref` + `target` → driver skeleton + `fill_in_brief` |
153
373
  | Tool | `validate_driver` | static-lint a completed driver against its `ref` |
154
374
  | Tool | `validate_datasheet` | re-run the L5 validator over a `ref` or JSON |
375
+ | Tool | `ping` | health check — confirms the server is running |
155
376
  | Resource | `driverge://datasheet/<ref>` | full parsed JSON for an analyzed datasheet |
156
377
  | Resource | `driverge://schema` | the frozen datasheet JSON-Schema contract |
157
378
  | Prompt | `generate-driver` | guided analyze → generate → fill → validate flow |
158
379
 
380
+ ## Troubleshooting
381
+
382
+ - **Is the server even running?** Ask your client to run the `ping` tool — it
383
+ replies `pong`. If npx fails to start, check `node --version` (≥ 18 required).
384
+ - **Scanned / image-only PDFs.** Driverge parses text-based PDFs; scanned or
385
+ mixed PDFs are detected and reported with an explicit warning, and parsing
386
+ will be incomplete (OCR is deferred to a future release).
387
+ - **`extraction: partial` or `deferred` is not a failure.** `partial` means the
388
+ registers were found but detail (e.g. bit-fields) is incomplete; `deferred`
389
+ means the register/command section was detected but not auto-extracted. In
390
+ both cases the generated skeleton tells the host AI exactly what to complete
391
+ from the datasheet resource. Only `none` is a genuine parse failure.
392
+ - **`out_dir "…" escapes the allowed root`.** Disk writes are confined under
393
+ `DRIVERGE_OUT_ROOT` (default: the server's working directory) — see
394
+ [Configuration](#configuration).
395
+ - **`UnsupportedBusError` on a native target.** The target doesn't support that
396
+ part's bus yet (today that means CAN on STM32, or a bus the parser couldn't
397
+ identify). Generate the **portable** target instead and implement its seam.
398
+
159
399
  ## Roadmap
160
400
 
161
401
  - **v0.x** — one reference sensor (BME280), portable thin-HAL core, MCP surface,
162
- multiple clients. *(current)*
163
- - **v0.y** — native targets: ESP32 ✅, STM32 ✅, Arduino (next).
164
- - **v1.0** — multi-manufacturer coverage and a stable, versioned JSON schema.
402
+ multiple clients.
403
+ - **v0.y** — native targets: ESP32 ✅, STM32 ✅, Arduino (next);
404
+ multi-manufacturer extraction (12 vendors tested see
405
+ [Verified parts](#verified-parts)); multi-bus seam families (I²C, SPI, UART,
406
+ CAN) ✅; C or C++ output ✅. *(current)*
407
+ - **v1.0** — broader vendor/part coverage, STM32 CAN (bxCAN/FDCAN), and a
408
+ stable, versioned JSON schema.
409
+
410
+ Day-to-day progress is tracked in the [CHANGELOG](CHANGELOG.md).
165
411
 
166
412
  ## Contributing
167
413
 
@@ -1,4 +1,12 @@
1
1
  import type { DatasheetJson } from "../schema/types.js";
2
- import type { DriverArtifact } from "./types.js";
3
- /** Portable core + an ESP-IDF seam implementation for the same part. */
4
- export declare function generateEsp32Driver(json: DatasheetJson): DriverArtifact;
2
+ import type { CodegenLanguage, DriverArtifact } from "./types.js";
3
+ /**
4
+ * Portable core + an ESP-IDF seam implementation for the same part. With
5
+ * language "c" (default) the seam is `<slug>_hal_esp32.c`, unchanged from
6
+ * before. With language "cpp" the seam becomes `<slug>_hal_esp32.cpp` and its
7
+ * core include is `#include "<slug>.hpp"` (see coreInclude/seamPath above) —
8
+ * everything else in the seam content is identical either way.
9
+ */
10
+ export declare function generateEsp32Driver(json: DatasheetJson, opts?: {
11
+ language?: CodegenLanguage;
12
+ }): DriverArtifact;
@@ -1,12 +1,39 @@
1
1
  // ESP32 (ESP-IDF) target. Per platform-code-generation-targets, a native target
2
2
  // does NOT replace the thin HAL — it *pre-fills* the seam. So the ESP32 driver is
3
3
  // the exact portable core (unchanged, still platform-agnostic) PLUS a
4
- // <part>_hal_esp32.c that implements hal_i2c_*/hal_delay_ms with the ESP-IDF v5
5
- // i2c_master driver. The user drops all three files into an ESP-IDF component;
4
+ // <part>_hal_esp32.c that implements the bus's thin-HAL seam + hal_delay_ms with
5
+ // ESP-IDF v5 drivers. The user drops all three files into an ESP-IDF component;
6
6
  // only the board's bus/pin bring-up stays a TODO(driverge).
7
+ //
8
+ // I2C/SPI/UART/CAN only: the I2C seam references `${PREFIX}_I2C_ADDR`, a macro
9
+ // the portable core only defines for I2C parts; the SPI seam needs the portable
10
+ // core's hal_spi_transfer declaration; the UART seam (Session B) wraps ESP-IDF's
11
+ // uart driver around hal_uart_write/hal_uart_read; the CAN seam (Session C)
12
+ // wraps ESP-IDF's TWAI (Two-Wire Automotive Interface, ESP-IDF's CAN 2.0
13
+ // controller) driver around hal_can_transfer — any other bus (unknown) has no
14
+ // ESP-IDF seam here, so this target refuses (UnsupportedBusError) rather than
15
+ // emit an uncompilable seam.
7
16
  import { prefixOf, slug } from "./ident.js";
17
+ import { generatePortableCppDriver } from "./portable-cpp.js";
8
18
  import { generatePortableDriver } from "./portable.js";
9
- function halImpl(name, prefix) {
19
+ import { UnsupportedBusError } from "./types.js";
20
+ // Contract amendment (post-GREEN 4b3da14/d576ba6): a language:"cpp" seam
21
+ // cannot stay byte-identical to the "c" seam, because that file
22
+ // #include's "<slug>.h" — which doesn't exist in a cpp bundle (only
23
+ // <slug>.hpp/.cpp). So with language "cpp" the seam file itself becomes
24
+ // `<slug>_hal_<target>.cpp` and its core include becomes `#include
25
+ // "<slug>.hpp"`; everything else in the seam is unchanged. This costs
26
+ // nothing: the .hpp declares the hal_* seam inside `extern "C"`, so compiling
27
+ // this seam as C++ still emits plain C-linkage definitions the class methods
28
+ // can call, and both ESP-IDF and a C++-enabled CubeMX project compile .cpp
29
+ // sources natively.
30
+ function coreInclude(name, language) {
31
+ return `#include "${name}.${language === "cpp" ? "hpp" : "h"}"`;
32
+ }
33
+ function seamPath(name, language) {
34
+ return `${name}_hal_esp32.${language === "cpp" ? "cpp" : "c"}`;
35
+ }
36
+ function halImplI2c(name, prefix, language) {
10
37
  const content = [
11
38
  "/*",
12
39
  ` * ${prefix} — ESP-IDF (i2c_master) HAL seam implementation (generated by Driverge).`,
@@ -16,7 +43,7 @@ function halImpl(name, prefix) {
16
43
  ` * i2c_new_master_bus() for your SDA/SCL pins, then call ${name}_esp32_bind()`,
17
44
  " * once before using the driver. The driver core stays byte-for-byte portable.",
18
45
  " */",
19
- `#include "${name}.h"`,
46
+ coreInclude(name, language),
20
47
  "#include <string.h>",
21
48
  '#include "driver/i2c_master.h"',
22
49
  '#include "freertos/FreeRTOS.h"',
@@ -60,18 +87,202 @@ function halImpl(name, prefix) {
60
87
  "}",
61
88
  "",
62
89
  ].join("\n");
63
- return { path: `${name}_hal_esp32.c`, content };
90
+ return { path: seamPath(name, language), content };
91
+ }
92
+ function halImplSpi(name, prefix, language) {
93
+ const content = [
94
+ "/*",
95
+ ` * ${prefix} — ESP-IDF (spi_master) HAL seam implementation (generated by Driverge).`,
96
+ " *",
97
+ " * Drop this beside the portable core; it fills the thin-HAL seam with the",
98
+ " * ESP-IDF v5 spi_master driver: one half-duplex, polling transaction per",
99
+ ` * hal_spi_transfer() call. In your app: bring up the SPI bus with`,
100
+ " * spi_bus_initialize() for your MOSI/MISO/SCLK pins, then call",
101
+ ` * ${name}_esp32_bind() once before using the driver. The driver core stays`,
102
+ " * byte-for-byte portable.",
103
+ " */",
104
+ coreInclude(name, language),
105
+ "#include <string.h>",
106
+ '#include "driver/spi_master.h"',
107
+ '#include "freertos/FreeRTOS.h"',
108
+ '#include "freertos/task.h"',
109
+ "",
110
+ `static spi_device_handle_t s_${name}_dev;`,
111
+ "",
112
+ "/* Add the device to an already-initialized SPI bus (spi_bus_initialize'd",
113
+ " * `host`) and keep the handle for the hal_* calls. Call once at startup;",
114
+ " * see fill_in_brief.hal_setup_todo for the SPI mode this part expects. */",
115
+ `esp_err_t ${name}_esp32_bind(spi_host_device_t host, int cs_gpio, int clock_hz) {`,
116
+ " spi_device_interface_config_t cfg = {",
117
+ " .flags = SPI_DEVICE_HALFDUPLEX,",
118
+ " .spics_io_num = cs_gpio,",
119
+ " .clock_speed_hz = clock_hz,",
120
+ " .mode = 0, /* see fill_in_brief.hal_setup_todo for this part's SPI mode */",
121
+ " .queue_size = 1,",
122
+ " };",
123
+ ` return spi_bus_add_device(host, &cfg, &s_${name}_dev);`,
124
+ "}",
125
+ "",
126
+ "void hal_spi_transfer(const uint8_t *tx, uint16_t tx_len, uint8_t *rx, uint16_t rx_len) {",
127
+ " spi_transaction_t t;",
128
+ " memset(&t, 0, sizeof t);",
129
+ " t.length = 8 * tx_len;",
130
+ " t.rxlength = 8 * rx_len;",
131
+ " t.tx_buffer = tx;",
132
+ " t.rx_buffer = rx;",
133
+ ` spi_device_polling_transmit(s_${name}_dev, &t);`,
134
+ "}",
135
+ "",
136
+ "void hal_delay_ms(uint32_t ms) {",
137
+ " vTaskDelay(pdMS_TO_TICKS(ms));",
138
+ "}",
139
+ "",
140
+ ].join("\n");
141
+ return { path: seamPath(name, language), content };
142
+ }
143
+ function halImplUart(name, prefix, language) {
144
+ const content = [
145
+ "/*",
146
+ ` * ${prefix} — ESP-IDF (uart) HAL seam implementation (generated by Driverge).`,
147
+ " *",
148
+ " * Drop this beside the portable core; it fills the thin-HAL seam with the",
149
+ " * ESP-IDF v5 UART driver. In your app: configure and install the UART driver",
150
+ " * (uart_param_config + uart_driver_install) for your TX/RX pins, port, and",
151
+ ` * this part's baud rate, then call ${name}_esp32_bind() once before using the`,
152
+ " * driver. The driver core stays byte-for-byte portable. See",
153
+ " * fill_in_brief.hal_setup_todo for the baud rate/config to use.",
154
+ " */",
155
+ coreInclude(name, language),
156
+ '#include "driver/uart.h"',
157
+ '#include "freertos/FreeRTOS.h"',
158
+ '#include "freertos/task.h"',
159
+ "",
160
+ `static uart_port_t s_${name}_port;`,
161
+ "",
162
+ "/* Point the seam at an already-installed UART port (uart_driver_install'd).",
163
+ " * Call once at startup — see fill_in_brief.hal_setup_todo. */",
164
+ `void ${name}_esp32_bind(uart_port_t port) {`,
165
+ ` s_${name}_port = port;`,
166
+ "}",
167
+ "",
168
+ "void hal_uart_write(const uint8_t *data, uint16_t len) {",
169
+ ` uart_write_bytes(s_${name}_port, (const char *)data, len);`,
170
+ "}",
171
+ "",
172
+ "uint16_t hal_uart_read(uint8_t *data, uint16_t len, uint32_t timeout_ms) {",
173
+ ` int n = uart_read_bytes(s_${name}_port, data, len, pdMS_TO_TICKS(timeout_ms));`,
174
+ " if (n < 0) {",
175
+ " return 0;",
176
+ " }",
177
+ " return (uint16_t)n;",
178
+ "}",
179
+ "",
180
+ "void hal_delay_ms(uint32_t ms) {",
181
+ " vTaskDelay(pdMS_TO_TICKS(ms));",
182
+ "}",
183
+ "",
184
+ ].join("\n");
185
+ return { path: seamPath(name, language), content };
186
+ }
187
+ function halImplCan(name, prefix, language) {
188
+ const content = [
189
+ "/*",
190
+ ` * ${prefix} — ESP-IDF (TWAI) HAL seam implementation (generated by Driverge).`,
191
+ " *",
192
+ " * Drop this beside the portable core; it fills the thin-HAL seam with the",
193
+ " * ESP-IDF v5 TWAI (Two-Wire Automotive Interface — ESP-IDF's CAN 2.0",
194
+ " * controller) driver. In your app: install and start the TWAI driver",
195
+ " * (twai_driver_install + twai_start) for your TX/RX GPIOs and this part's",
196
+ ` * bitrate, then call ${name}_esp32_bind() once before using the driver.`,
197
+ " * The driver core stays byte-for-byte portable. See",
198
+ " * fill_in_brief.hal_setup_todo for the bitrate/acceptance-filter config.",
199
+ " *",
200
+ ` * ${name}_esp32_bind() deliberately takes NO arguments: unlike I2C/SPI/UART`,
201
+ " * (which bind a specific bus/port handle), ESP-IDF exposes exactly one",
202
+ " * TWAI peripheral per chip, installed once for the whole application — so",
203
+ " * there is nothing instance-specific to store yet. The hook is kept for",
204
+ " * signature parity with the other targets' bind().",
205
+ " */",
206
+ coreInclude(name, language),
207
+ "#include <string.h>",
208
+ '#include "driver/twai.h"',
209
+ '#include "freertos/FreeRTOS.h"',
210
+ '#include "freertos/task.h"',
211
+ "",
212
+ "/* Reserved bind hook (see the file comment above). Call once after",
213
+ " * twai_driver_install()/twai_start(). */",
214
+ `void ${name}_esp32_bind(void) {`,
215
+ "}",
216
+ "",
217
+ "int hal_can_transfer(uint32_t id, const uint8_t *tx, uint8_t tx_len, uint8_t *rx, uint8_t *rx_len, uint32_t timeout_ms) {",
218
+ " twai_message_t tx_msg;",
219
+ " memset(&tx_msg, 0, sizeof tx_msg);",
220
+ " tx_msg.identifier = id;",
221
+ " tx_msg.data_length_code = tx_len;",
222
+ " if (tx != NULL && tx_len > 0) {",
223
+ " memcpy(tx_msg.data, tx, tx_len);",
224
+ " }",
225
+ " if (twai_transmit(&tx_msg, pdMS_TO_TICKS(timeout_ms)) != ESP_OK) {",
226
+ " return -1;",
227
+ " }",
228
+ " if (rx == NULL || rx_len == NULL || *rx_len == 0) {",
229
+ " return 0;",
230
+ " }",
231
+ " twai_message_t rx_msg;",
232
+ " if (twai_receive(&rx_msg, pdMS_TO_TICKS(timeout_ms)) != ESP_OK) {",
233
+ " return -1;",
234
+ " }",
235
+ " uint8_t n = rx_msg.data_length_code;",
236
+ " if (n > *rx_len) {",
237
+ " n = *rx_len;",
238
+ " }",
239
+ " memcpy(rx, rx_msg.data, n);",
240
+ " *rx_len = n;",
241
+ " return 0;",
242
+ "}",
243
+ "",
244
+ "void hal_delay_ms(uint32_t ms) {",
245
+ " vTaskDelay(pdMS_TO_TICKS(ms));",
246
+ "}",
247
+ "",
248
+ ].join("\n");
249
+ return { path: seamPath(name, language), content };
64
250
  }
65
- /** Portable core + an ESP-IDF seam implementation for the same part. */
66
- export function generateEsp32Driver(json) {
251
+ /**
252
+ * Portable core + an ESP-IDF seam implementation for the same part. With
253
+ * language "c" (default) the seam is `<slug>_hal_esp32.c`, unchanged from
254
+ * before. With language "cpp" the seam becomes `<slug>_hal_esp32.cpp` and its
255
+ * core include is `#include "<slug>.hpp"` (see coreInclude/seamPath above) —
256
+ * everything else in the seam content is identical either way.
257
+ */
258
+ export function generateEsp32Driver(json, opts = {}) {
259
+ const bus = json.protocol.bus;
260
+ if (bus !== "I2C" && bus !== "SPI" && bus !== "UART" && bus !== "CAN") {
261
+ throw new UnsupportedBusError("esp32", bus);
262
+ }
67
263
  const name = slug(json.metadata.part);
68
264
  const prefix = prefixOf(json.metadata.part);
69
- const base = generatePortableDriver(json);
265
+ const language = opts.language ?? "c";
266
+ const base = language === "cpp" ? generatePortableCppDriver(json) : generatePortableDriver(json);
267
+ const hal = bus === "SPI"
268
+ ? halImplSpi(name, prefix, language)
269
+ : bus === "UART"
270
+ ? halImplUart(name, prefix, language)
271
+ : bus === "CAN"
272
+ ? halImplCan(name, prefix, language)
273
+ : halImplI2c(name, prefix, language);
274
+ const hal_setup_todo = bus === "SPI"
275
+ ? `Bring up the ESP-IDF SPI bus (spi_bus_initialize with your MOSI/MISO/SCLK pins and host), then call ${name}_esp32_bind(host, cs_gpio, clock_hz) before the first driver call. Confirm this part's SPI mode (0-3) and maximum clock speed from the datasheet's SPI timing section.`
276
+ : bus === "UART"
277
+ ? `Install the ESP-IDF UART driver (uart_param_config + uart_driver_install) on your chosen port with this part's baud rate/parity/stop-bits (see the datasheet's serial interface section), then call ${name}_esp32_bind(port) before the first driver call.`
278
+ : bus === "CAN"
279
+ ? `Install and start the ESP-IDF TWAI driver (twai_driver_install + twai_start) on your chosen TX/RX GPIOs with this part's bitrate and an acceptance filter covering its message IDs (see the datasheet's CAN bus timing/message-ID section), then call ${name}_esp32_bind() before the first driver call.`
280
+ : `Bring up the ESP-IDF I2C master bus (i2c_new_master_bus with your SDA/SCL GPIOs and port), then call ${name}_esp32_bind(bus, 400000) before the first driver call. Tune ${prefix}_I2C_TIMEOUT_MS if needed.`;
70
281
  return {
71
- files: [...base.files, halImpl(name, prefix)],
282
+ files: [...base.files, hal],
72
283
  fill_in_brief: {
73
284
  ...base.fill_in_brief,
74
- hal_setup_todo: `Bring up the ESP-IDF I2C master bus (i2c_new_master_bus with your SDA/SCL GPIOs and port), then call ${name}_esp32_bind(bus, 400000) before the first driver call. Tune ${prefix}_I2C_TIMEOUT_MS if needed.`,
285
+ hal_setup_todo,
75
286
  },
76
287
  };
77
288
  }