@typecad/framework-zephyr 1.0.0-alpha.12 → 1.0.0-alpha.13

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/dist/strategy.js CHANGED
@@ -44,7 +44,7 @@ import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
44
44
  import { buildTimerPolyfill } from './async/timer-polyfill.js';
45
45
  import { resolveZephyrDisplayOp, newDisplayState } from './display/index.js';
46
46
  import { buildDisplayRuntime } from './display/gfx.js';
47
- import { ZEPHYR_DISPLAY_PROFILES } from './display/profiles.js';
47
+ import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
48
48
  import { zephyrDisplayAdapterGenerator } from './display/ui-adapter.js';
49
49
  import { zephyrTouchAdapter } from './display/touch-adapter.js';
50
50
  export class ZephyrStrategy {
@@ -113,7 +113,23 @@ export class ZephyrStrategy {
113
113
  // true so nothing is stripped — mirrors framework-esp32's forcedIncludes.
114
114
  const a = ctx?.analysis;
115
115
  const uses = (f) => (a ? !!a[f] : true);
116
- const inc = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdio>', '<cstdint>'];
116
+ // <zephyr/drivers/gpio.h> and <cstdint> stay unconditional: gpio.h is
117
+ // cross-cutting (gpio/power/interrupt/spi/pulse lowerings + the DT-spec
118
+ // machinery all reference its API, and no single usesX flag owns it), and
119
+ // the fixed-width types come via <zephyr/kernel.h> regardless — DIRECT_CPP_TYPE_MAP
120
+ // passes int32_t/uint8_t through verbatim.
121
+ const inc = ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdint>'];
122
+ // <cstdio> backs the printf family only: __tc_print/__tc_println (emitted
123
+ // solely when @typecad/expect's preprocessor injected them — tracked via
124
+ // usedPolyfillHelpers), raw printf/snprintf in user code (usesCstdio), and
125
+ // the fs/preferences/uart shims (their lowerings snprintf into buffers).
126
+ // A program touching none of those needs no <cstdio>.
127
+ const helpers = a?.usedPolyfillHelpers;
128
+ const needsCstdio = uses('usesCstdio') || uses('usesFS') || uses('usesPreferences')
129
+ || uses('usesUart')
130
+ || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println');
131
+ if (needsCstdio)
132
+ inc.push('<cstdio>');
117
133
  if (uses('usesI2C'))
118
134
  inc.push('<zephyr/drivers/i2c.h>');
119
135
  if (uses('usesSPI'))
@@ -239,83 +255,171 @@ export class ZephyrStrategy {
239
255
  visit(program);
240
256
  return found;
241
257
  }
258
+ /** Pins referenced by gpio.* hal-ops in the program IR. lowerGpio routes a
259
+ * pin to its devicetree spec by pin NUMBER, so the structured hal-op pins
260
+ * are the authoritative signal for which __tc_dt_* specs are needed —
261
+ * regardless of when the final call text is rendered. */
262
+ collectGpioPinUsage(program) {
263
+ const pins = new Set();
264
+ if (!program)
265
+ return pins;
266
+ const visit = (node) => {
267
+ if (!node || typeof node !== 'object')
268
+ return;
269
+ if (node.operation && typeof node.operation === 'object'
270
+ && typeof node.operation.operation === 'string'
271
+ && node.operation.operation.startsWith('gpio.')
272
+ && typeof node.operation.pin === 'number') {
273
+ pins.add(node.operation.pin);
274
+ }
275
+ for (const v of Object.values(node)) {
276
+ if (Array.isArray(v)) {
277
+ for (const item of v)
278
+ visit(item);
279
+ }
280
+ else if (v && typeof v === 'object')
281
+ visit(v);
282
+ }
283
+ };
284
+ visit(program);
285
+ return pins;
286
+ }
287
+ /** Run `re` (global) against every raw string in the IR — raw expression
288
+ * values plus raw hal-op codes — returning capture group 1 of each match
289
+ * (the full match when the regex has no group). This is how references the
290
+ * text scanners must see but that never appear as IR call nodes (e.g. a
291
+ * rawCpp() escape hatch naming `__tc_dt_sw0` directly) are discovered. */
292
+ collectRawMatches(program, re) {
293
+ const found = new Set();
294
+ if (!program)
295
+ return found;
296
+ const scan = (text) => {
297
+ for (const m of text.matchAll(re))
298
+ found.add(m[1] ?? m[0]);
299
+ };
300
+ const visit = (node) => {
301
+ if (!node || typeof node !== 'object')
302
+ return;
303
+ if (node.kind === 'raw' && typeof node.value === 'string')
304
+ scan(node.value);
305
+ if (node.operation && typeof node.operation === 'object'
306
+ && node.operation.operation === 'raw' && typeof node.operation.code === 'string') {
307
+ scan(node.operation.code);
308
+ }
309
+ for (const v of Object.values(node)) {
310
+ if (Array.isArray(v)) {
311
+ for (const item of v)
312
+ visit(item);
313
+ }
314
+ else if (v && typeof v === 'object')
315
+ visit(v);
316
+ }
317
+ };
318
+ visit(program);
319
+ return found;
320
+ }
321
+ /** Whether the wiring-compat GPIO read surface (__tc_gpio_read definition,
322
+ * __tc_gpio_dev dispatcher, and the wiring_compat polyfill's digitalRead /
323
+ * HIGH / LOW macros) must be emitted. Consumers: user digitalRead() calls
324
+ * (usesDigitalRead), the @typecad/safety voter (calls __tc_gpio_read
325
+ * directly via lowered raw text), and the UI runtime header's
326
+ * unconditional digitalRead() poll (entryHasUI — build-global, so every TU
327
+ * in a UI build carries the macros). With no analysis present (capability
328
+ * query), default to emitting — same convention as the uses() helper. */
329
+ needsGpioReadShim(program, ctx) {
330
+ if (program && programUsesSafety(program))
331
+ return true;
332
+ if (entryHasUI())
333
+ return true;
334
+ const a = ctx?.analysis;
335
+ return a ? !!a.usesDigitalRead : true;
336
+ }
242
337
  shimLines(program, ctx) {
243
338
  const chip = this.resolveChip(ctx, program);
244
339
  const isPrintf = this.resolveDebugMode(ctx) === 'printf';
245
- const lines = [
246
- '// cuttlefish runtime shim. Wrapped in a single include guard so the',
247
- '// block is safe to emit into multiple headers and .cpp files within',
248
- '// one translation unit (a .cpp may #include several headers that each',
249
- '// carry the shim). The guard ensures the definitions are seen exactly',
250
- '// once per TU.',
251
- '#ifndef CUTTLEFISH_SHIM_DEFINED',
252
- '#define CUTTLEFISH_SHIM_DEFINED',
253
- '#ifndef CUTTLEFISH_UNDEFINED',
254
- '#define CUTTLEFISH_UNDEFINED 0',
255
- '#endif',
256
- 'template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }',
257
- 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }',
258
- 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }',
259
- 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }',
260
- 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }',
261
- 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }',
262
- 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }',
263
- 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
264
- // millis() backed by the Zephyr uptime counter. uint32_t return matches
265
- // the Arduino API the shared runtime expects (wraps every ~49.7 days).
266
- 'inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }',
267
- // Arduino-compat defines referenced by the shared runtime polyfills.
268
- '#ifndef HIGH', '#define HIGH 1', '#endif',
269
- '#ifndef LOW', '#define LOW 0', '#endif',
270
- '#ifndef PROGMEM', '#define PROGMEM', '#endif',
271
- 'inline long map(long x, long in_min, long in_max, long out_min, long out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; }',
272
- 'inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }',
273
- // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
274
- // for protocol output. Overloaded for string (const char*) and numeric
275
- // (double) so the same call site works for markers and test values.
276
- 'inline void __tc_print(const char* s) { printf("%s", s); }',
277
- 'inline void __tc_print(double v) { printf("%g", v); }',
278
- 'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
279
- 'inline void __tc_println(double v) { printf("%g\\n", v); }',
280
- ];
281
- // Devicetree specs for every board-defined GPIO pin. Emitted unconditionally
282
- // (guarded by the include guard) so any of them is available whether or not
283
- // a given program uses it. Safe because every spec references a node that
284
- // exists in the active board's devicetree.
285
- for (const spec of chip.gpio.dtSpecs) {
286
- lines.push(`static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`);
340
+ const a = ctx?.analysis;
341
+ const uses = (f) => (a ? !!a[f] : true);
342
+ const helpers = a?.usedPolyfillHelpers;
343
+ // --- Core shim, gated item by item on actual use ------------------------
344
+ // A minimal program (blink) uses none of these, and its output carries no
345
+ // shim block at all. Everything up to the #endif composes into one guard
346
+ // body; the guard itself is only stamped when the body is non-empty.
347
+ const guardBody = [];
348
+ // CUTTLEFISH_UNDEFINED: needed when the file references null/undefined
349
+ // literals (usesNullish), emits nullish helper CALLS (usesNullishHelper),
350
+ // or has async functions (the async state machine uses the macro for
351
+ // default waitFor* timeouts not visible to the nullish scanners).
352
+ if (uses('usesNullish') || uses('usesNullishHelper') || uses('hasAsync')) {
353
+ guardBody.push('#ifndef CUTTLEFISH_UNDEFINED', '#define CUTTLEFISH_UNDEFINED 0', '#endif');
354
+ }
355
+ // Nullish helpers: only when the file actually emits cuttlefish_nullish /
356
+ // cuttlefish_exists CALLS (?? / ?. lowering). A file that only references
357
+ // null/undefined literals needs just the macro above the same
358
+ // distinction the setup emitter's strip filter documents.
359
+ if (uses('usesNullishHelper')) {
360
+ guardBody.push('template<typename T> inline bool cuttlefish_is_nullish(const T& v) { return false; }', 'inline bool cuttlefish_is_nullish(long long v) { return v == CUTTLEFISH_UNDEFINED; }', 'inline bool cuttlefish_is_nullish(int v) { return v == CUTTLEFISH_UNDEFINED; }', 'inline bool cuttlefish_is_nullish(double v) { return v == static_cast<double>(CUTTLEFISH_UNDEFINED); }', 'inline bool cuttlefish_is_nullish(bool v) { return v == false; }', 'template<typename T> inline bool cuttlefish_is_nullish(T* v) { return v == nullptr; }', 'template<typename T> inline bool cuttlefish_exists(const T& v) { return !cuttlefish_is_nullish(v); }', 'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }');
361
+ }
362
+ // millis() backed by the Zephyr uptime counter. uint32_t return matches
363
+ // the Arduino API the shared runtime expects (wraps every ~49.7 days).
364
+ // Kept when the program reads the clock itself — usesWallClock,
365
+ // deliberately WITHOUT the delay() conflation usesMillis carries, because
366
+ // Zephyr's delay lowers straight to k_msleep or has a hidden poller:
367
+ // async functions / the async runtime, the setInterval/setTimeout
368
+ // scheduler, or a mounted UI's per-frame tick.
369
+ if (uses('usesWallClock') || uses('hasAsync') || (!a || a.timerCallCount > 0)
370
+ || this.programUsesAsyncRuntime(program) || entryHasUI()) {
371
+ guardBody.push('inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }');
372
+ }
373
+ // PROGMEM: only the (Arduino-oriented) UI runtime header can reference it.
374
+ if (entryHasUI()) {
375
+ guardBody.push('#ifndef PROGMEM', '#define PROGMEM', '#endif');
376
+ }
377
+ // map()/constrain() Arduino-API helpers dead code unless called. The
378
+ // setup emitter ORs entryHasUI() into usesConstrain before we see it (the
379
+ // UI runtime's progress/range draw calls constrain).
380
+ if (uses('usesMap')) {
381
+ guardBody.push('inline long map(long x, long in_min, long in_max, long out_min, long out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; }');
382
+ }
383
+ if (uses('usesConstrain')) {
384
+ guardBody.push('inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }');
385
+ }
386
+ // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
387
+ // for protocol output. Overloaded for string (const char*) and numeric
388
+ // (double) so the same call site works for markers and test values.
389
+ // Emitted only when the expect preprocessor actually injected the calls
390
+ // (tracked as usedPolyfillHelpers).
391
+ if (!a || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println')) {
392
+ guardBody.push('inline void __tc_print(const char* s) { printf("%s", s); }', 'inline void __tc_print(double v) { printf("%g", v); }', 'inline void __tc_println(const char* s) { printf("%s\\n", s); }', 'inline void __tc_println(double v) { printf("%g\\n", v); }');
287
393
  }
288
394
  // Per-peripheral bus state — gated on the same ctx.analysis.usesX flags as
289
395
  // forcedIncludes, so an unused peripheral emits no state (and its header is
290
396
  // not included). Mirrors framework-esp32's shimLines espInit block.
291
- const a = ctx?.analysis;
292
- const uses = (f) => (a ? !!a[f] : true);
293
397
  if (uses('usesI2C') && chip.i2c) {
294
398
  for (let i = 0; i < chip.i2c.controllers.length; i++)
295
- lines.push(...i2cInitLines(chip, i));
399
+ guardBody.push(...i2cInitLines(chip, i));
296
400
  }
297
401
  if (uses('usesSPI') && chip.spi) {
298
402
  for (let i = 0; i < chip.spi.controllers.length; i++)
299
- lines.push(...spiInitLines(chip, i));
403
+ guardBody.push(...spiInitLines(chip, i));
300
404
  }
301
405
  if (uses('usesUart') && chip.uart) {
302
406
  for (let i = 0; i < chip.uart.controllers.length; i++)
303
- lines.push(...uartInitLines(chip, i));
407
+ guardBody.push(...uartInitLines(chip, i));
304
408
  }
305
409
  if (uses('usesADC') && chip.adc)
306
- lines.push(...adcInitLines(chip));
410
+ guardBody.push(...adcInitLines(chip));
307
411
  if (uses('usesPWM') && chip.pwm)
308
- lines.push(...pwmInitLines(chip));
412
+ guardBody.push(...pwmInitLines(chip));
309
413
  if (uses('usesDAC') && chip.dac)
310
- lines.push(...dacInitLines(chip));
414
+ guardBody.push(...dacInitLines(chip));
311
415
  if (uses('usesHwtimer') && chip.hwtimer)
312
- lines.push(...hwtimerInitLines(chip));
416
+ guardBody.push(...hwtimerInitLines(chip));
313
417
  if (uses('usesInterrupts'))
314
- lines.push(...interruptInitLines(chip));
418
+ guardBody.push(...interruptInitLines(chip));
315
419
  if (uses('usesWDT') && chip.wdt)
316
- lines.push(...wdtInitLines(chip));
420
+ guardBody.push(...wdtInitLines(chip));
317
421
  if (uses('usesBle'))
318
- lines.push(...bleInitLines());
422
+ guardBody.push(...bleInitLines());
319
423
  // Display runtime (rect/text renderer): the DIRECT-call display path (user
320
424
  // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
321
425
  // when the program uses display.* but is NOT a UI program — the UI display
@@ -329,23 +433,42 @@ export class ZephyrStrategy {
329
433
  // previously dead code).
330
434
  if (uses('usesDisplay') && !entryHasUI()) {
331
435
  const rt = buildDisplayRuntime(this._displayState.profile);
332
- lines.push(...rt.stateLines);
333
- lines.push(rt.fontTable);
334
- lines.push(rt.helpers);
436
+ guardBody.push(...rt.stateLines);
437
+ guardBody.push(rt.fontTable);
438
+ guardBody.push(rt.helpers);
335
439
  }
336
440
  if (uses('usesWifi'))
337
- lines.push(...wifiInitLines());
441
+ guardBody.push(...wifiInitLines());
338
442
  if (uses('usesHttp'))
339
- lines.push(...httpInitLines());
443
+ guardBody.push(...httpInitLines());
340
444
  if (uses('usesMqtt'))
341
- lines.push(...mqttInitLines());
445
+ guardBody.push(...mqttInitLines());
342
446
  if (uses('usesPreferences'))
343
- lines.push(...preferencesInitLines());
447
+ guardBody.push(...preferencesInitLines());
344
448
  if (uses('usesFS'))
345
- lines.push(...fsInitLines());
449
+ guardBody.push(...fsInitLines());
346
450
  if (uses('usesRandom'))
347
- lines.push(...randomInitLines());
348
- lines.push('#endif // CUTTLEFISH_SHIM_DEFINED');
451
+ guardBody.push(...randomInitLines());
452
+ const lines = [];
453
+ if (guardBody.length > 0) {
454
+ lines.push('// cuttlefish runtime shim. Wrapped in a single include guard so the', '// block is safe to emit into multiple headers and .cpp files within', '// one translation unit (a .cpp may #include several headers that each', '// carry the shim). The guard ensures the definitions are seen exactly', '// once per TU.', '#ifndef CUTTLEFISH_SHIM_DEFINED', '#define CUTTLEFISH_SHIM_DEFINED', ...guardBody, '#endif // CUTTLEFISH_SHIM_DEFINED');
455
+ }
456
+ // Devicetree specs — one per board-defined GPIO pin, but ONLY for pins the
457
+ // program actually addresses (lowerGpio routes by pin number, and the
458
+ // structured gpio.* hal-op pins are visible here) plus aliases named
459
+ // verbatim in raw code (rawCpp escape hatches). Emitted OUTSIDE the single
460
+ // CUTTLEFISH_SHIM_DEFINED guard with a per-symbol guard: per-file pin sets
461
+ // differ, and in a multi-header TU the first header's TU-wide guard would
462
+ // otherwise hide the second header's specs. Without a program (capability
463
+ // query), emit them all.
464
+ const usedPins = this.collectGpioPinUsage(program);
465
+ const dtTextRefs = this.collectRawMatches(program, /__tc_dt_([A-Za-z0-9_]+)/g);
466
+ for (const spec of chip.gpio.dtSpecs) {
467
+ if (program && !usedPins.has(spec.pin) && !dtTextRefs.has(spec.dtSpec))
468
+ continue;
469
+ const guard = `__TC_DT_${spec.dtSpec.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}_SPEC`;
470
+ lines.push(`#ifndef ${guard}`, `#define ${guard}`, `static const struct gpio_dt_spec __tc_dt_${spec.dtSpec} = GPIO_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}), gpios);`, `#endif // ${guard}`);
471
+ }
349
472
  // --- Debug-mode halt + per-breakpoint disable registry ---
350
473
  //
351
474
  // Printf mode only. In gdb mode the cuttlefish debug preprocessor is
@@ -378,9 +501,12 @@ export class ZephyrStrategy {
378
501
  // separate translation unit when generateHeaderFile() splits them into the
379
502
  // header.
380
503
  lines.push('extern void setup(void);', 'extern void loop(void);', '', 'int main(void) {', ' setup();', ' for (;;) {', ' loop();', ' k_msleep(1);', ' }', ' return 0;', '}');
381
- // GPIO read shim: the wiring_compat polyfill routes the UI runtime
382
- // header's unconditional digitalRead() poll (init-press-input.ts) to
383
- // __tc_gpio_read, so the definition must NOT be gated on @typecad/safety.
504
+ // GPIO read shim: emitted only when something actually reads a pin at
505
+ // runtime user digitalRead() calls, the @typecad/safety voter (calls
506
+ // __tc_gpio_read directly), or the UI runtime header's digitalRead() poll
507
+ // (init-press-input.ts). A program that only writes/toggles GPIO needs
508
+ // neither the dispatcher nor the reader.
509
+ //
384
510
  // The signature is `int` to match wiring_compat's forward declaration —
385
511
  // a uint32_t definition alongside it would leave the declared int
386
512
  // overload undefined (int wins overload resolution for small integer
@@ -393,8 +519,10 @@ export class ZephyrStrategy {
393
519
  // dispatcher that resolves the owning controller's device per pin;
394
520
  // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
395
521
  // still compile-time-resolved per branch, so it is always statically valid.
396
- lines.push(...emitGpioDevDispatcher(chip));
397
- lines.push('inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), static_cast<gpio_pin_t>(pin)); }');
522
+ if (this.needsGpioReadShim(program, ctx)) {
523
+ lines.push(...emitGpioDevDispatcher(chip));
524
+ lines.push('inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), static_cast<gpio_pin_t>(pin)); }');
525
+ }
398
526
  // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
399
527
  // lowering, so they stay gated on it.
400
528
  if (program && programUsesSafety(program)) {
@@ -1017,6 +1145,45 @@ export class ZephyrStrategy {
1017
1145
  ]);
1018
1146
  }
1019
1147
  generateNativePolyfills(program, ctx) {
1148
+ // wiring_compat (digitalRead/HIGH/LOW macros + the __tc_gpio_read forward
1149
+ // declaration) is emitted only when something reads a pin: user
1150
+ // digitalRead() calls, the @typecad/safety voter, or the UI runtime
1151
+ // header's unconditional digitalRead() poll (init-press-input.ts — the
1152
+ // loop body is dead when no pin watchers are configured but must
1153
+ // compile). needsGpioReadShim defaults to true without analysis so
1154
+ // capability queries keep seeing it.
1155
+ const wiringCompat = {
1156
+ // Wiring-compatibility shims for symbols the UI runtime header
1157
+ // references unconditionally (e.g. init-press-input.ts polls pin
1158
+ // watchers via digitalRead/HIGH/LOW even when none are configured —
1159
+ // the loop body is dead but must compile). Zephyr lowers GPIO through
1160
+ // its __tc_gpio_* helpers (defined in shimLines); these macros route
1161
+ // the Wiring tokens to them.
1162
+ kind: 'polyfill',
1163
+ id: 'wiring_compat',
1164
+ domain: 'standard',
1165
+ requiredIncludes: [],
1166
+ forwardDeclarations: [
1167
+ // Forward-declared so the digitalRead macro (below) can reference it
1168
+ // before the shim block defines the body. The shim emits the full
1169
+ // definition via gpio_pin_get_raw.
1170
+ 'int __tc_gpio_read(int pin);',
1171
+ ],
1172
+ helperStructs: [],
1173
+ helperFunctions: [],
1174
+ shimMacros: [
1175
+ '#ifndef HIGH',
1176
+ '#define HIGH 1',
1177
+ '#endif',
1178
+ '#ifndef LOW',
1179
+ '#define LOW 0',
1180
+ '#endif',
1181
+ '#ifndef digitalRead',
1182
+ '#define digitalRead(pin) __tc_gpio_read(pin)',
1183
+ '#endif',
1184
+ ],
1185
+ dependencies: [],
1186
+ };
1020
1187
  const polyfills = [
1021
1188
  {
1022
1189
  kind: 'polyfill',
@@ -1031,38 +1198,7 @@ export class ZephyrStrategy {
1031
1198
  shimMacros: [],
1032
1199
  dependencies: [],
1033
1200
  },
1034
- {
1035
- // Wiring-compatibility shims for symbols the UI runtime header
1036
- // references unconditionally (e.g. init-press-input.ts polls pin
1037
- // watchers via digitalRead/HIGH/LOW even when none are configured —
1038
- // the loop body is dead but must compile). Zephyr lowers GPIO through
1039
- // its __tc_gpio_* helpers (defined in shimLines); these macros route
1040
- // the Wiring tokens to them.
1041
- kind: 'polyfill',
1042
- id: 'wiring_compat',
1043
- domain: 'standard',
1044
- requiredIncludes: [],
1045
- forwardDeclarations: [
1046
- // Forward-declared so the digitalRead macro (below) can reference it
1047
- // before the shim block defines the body. The shim emits the full
1048
- // definition via gpio_pin_get_raw.
1049
- 'int __tc_gpio_read(int pin);',
1050
- ],
1051
- helperStructs: [],
1052
- helperFunctions: [],
1053
- shimMacros: [
1054
- '#ifndef HIGH',
1055
- '#define HIGH 1',
1056
- '#endif',
1057
- '#ifndef LOW',
1058
- '#define LOW 0',
1059
- '#endif',
1060
- '#ifndef digitalRead',
1061
- '#define digitalRead(pin) __tc_gpio_read(pin)',
1062
- '#endif',
1063
- ],
1064
- dependencies: [],
1065
- },
1201
+ ...(this.needsGpioReadShim(program, ctx) ? [wiringCompat] : []),
1066
1202
  {
1067
1203
  // STL-free string-method polyfills. String methods (.toUpperCase(),
1068
1204
  // .includes(), .substring(), …) lower at IR level to __tc_* helpers for
@@ -1269,22 +1405,10 @@ struct __tc_StaticArray {
1269
1405
  // Named display-profile registry: maps config `profile` values (e.g.
1270
1406
  // "st7796-zephyr") to the shared DisplayProfile shape so transpile.ts can
1271
1407
  // resolve them per-framework. The Zephyr profiles are DT-binding descriptors;
1272
- // they're mapped to the shared shape (driver/width/height/colorFormat/
1273
- // rotation) the profile resolver expects.
1408
+ // BUILT_IN_PROFILES (display/profiles.ts) is the single DT-binding
1409
+ // shared-shape mapping, shared with the preview's registry loader.
1274
1410
  getProfileRegistry() {
1275
- const m = new Map();
1276
- for (const [name, p] of Object.entries(ZEPHYR_DISPLAY_PROFILES)) {
1277
- m.set(name, {
1278
- driver: p.driver,
1279
- width: p.width,
1280
- height: p.height,
1281
- nativeWidth: p.nativeWidth,
1282
- nativeHeight: p.nativeHeight,
1283
- colorFormat: p.colorFormat,
1284
- rotation: p.rotation ?? 1,
1285
- });
1286
- }
1287
- return m;
1411
+ return new Map(Object.entries(BUILT_IN_PROFILES));
1288
1412
  }
1289
1413
  colorFormat() {
1290
1414
  return 'rgb565';
@@ -13,8 +13,10 @@ export interface DebugConfigOptions {
13
13
  sourceMapPath?: string;
14
14
  }
15
15
  /**
16
- * Resolve the GDB binary path for the target from the build cache. Zephyr
17
- * records ZEPHYR_SDK_INSTALL_DIR in CMakeCache.txt at configure time, and the
16
+ * Resolve the GDB binary path for the target from the build cache, falling
17
+ * back to a filesystem scan of known Zephyr SDK locations when no build
18
+ * exists yet (the create-time starter artifacts path). Zephyr records
19
+ * ZEPHYR_SDK_INSTALL_DIR in CMakeCache.txt at configure time, and the
18
20
  * xtensa GDB lives at <sdk>/xtensa-espressif_esp32s3_zephyr-elf/bin/... (note
19
21
  * the Zephyr-SDK naming, distinct from the ESP-IDF xtensa-esp32s3-elf-gdb).
20
22
  *
@@ -22,6 +24,24 @@ export interface DebugConfigOptions {
22
24
  * omits gdbPath and relies on Cortex-Debug's default resolution).
23
25
  */
24
26
  export declare function resolveGdbPath(buildDir: string, target: string): string | undefined;
27
+ /** The esp32s3 xtensa GDB location inside a Zephyr SDK root (verified against
28
+ * zephyr-sdk-0.17.4). Returns a forward-slash absolute path or undefined. */
29
+ export declare function gdbPathFromSdkRoot(sdkRoot: string): string | undefined;
30
+ /**
31
+ * Probe the well-known Zephyr SDK install locations, newest version first:
32
+ * 1. $ZEPHYR_SDK_INSTALL_DIR (the var board.cmake reads)
33
+ * 2. <MAMBA_ROOT_PREFIX | ~/micromamba>/zephyr-sdk/zephyr-sdk-<ver> — the
34
+ * @typecad/zephyr-installer layout
35
+ * 3. ~/zephyr-sdk-<ver> — the standalone download layout
36
+ *
37
+ * Only roots that actually contain the esp32s3 GDB are useful to callers;
38
+ * this returns candidate roots (gdbPathFromSdkRoot does the existence check)
39
+ * so tests can inject home/env overrides.
40
+ */
41
+ export declare function discoverZephyrSdkRoots(opts?: {
42
+ home?: string;
43
+ env?: Record<string, string | undefined>;
44
+ }): string[];
25
45
  /**
26
46
  * Resolve the Espressif OpenOCD binary path. The esp32s3 needs the Espressif
27
47
  * OpenOCD fork (openocd-esp32) — not the Zephyr SDK's openocd and not a
@@ -80,3 +100,24 @@ export declare function generateGdbScript(sourceMapPath?: string): string | null
80
100
  * <projectRoot>/.cuttlefish/.cuttlefish-gdb.py (lambda frame filter, conditional)
81
101
  */
82
102
  export declare function writeDebugConfig(o: DebugConfigOptions): void;
103
+ /**
104
+ * Create-time starter debug artifacts. Called by the cuttlefish `create` flow
105
+ * (via the package's `writeProjectDebugArtifacts` export) so a fresh project
106
+ * has a working F5 before any build exists:
107
+ *
108
+ * The launch.json's preLaunchTask runs `cuttlefish build --compile --upload
109
+ * --debug`, which builds + flashes AND rewrites this same launch entry (merged
110
+ * by name) with the CMakeCache-resolved gdbPath — so the starter files upgrade
111
+ * themselves on the first debug build.
112
+ *
113
+ * No-ops (returns []) for targets without native GDB support (debugMode() !==
114
+ * 'gdb'); the gdb frame-filter script is skipped (no source map exists yet).
115
+ *
116
+ * Returns the workspace-relative paths written, for CLI reporting.
117
+ */
118
+ export declare function writeProjectDebugArtifacts(o: {
119
+ /** Absolute path to the cuttlefish project root (contains cuttlefish.config.ts). */
120
+ workspaceRoot: string;
121
+ /** The Zephyr board id from the project config (frameworkData.buildTarget). */
122
+ buildTarget?: string;
123
+ }): string[];