@typecad/framework-zephyr 1.0.0-alpha.18 → 1.0.0-alpha.19

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 (114) hide show
  1. package/dist/audit.d.ts +111 -0
  2. package/dist/audit.js +416 -0
  3. package/dist/boardgen.js +63 -5
  4. package/dist/chips/resolve.js +20 -0
  5. package/dist/chips/types.d.ts +21 -0
  6. package/dist/display/bindings.d.ts +55 -0
  7. package/dist/display/bindings.js +316 -0
  8. package/dist/display/gfx.d.ts +2 -3
  9. package/dist/display/gfx.js +166 -154
  10. package/dist/display/index.js +20 -1
  11. package/dist/display/mipi-dbi-host.d.ts +9 -0
  12. package/dist/display/mipi-dbi-host.js +174 -0
  13. package/dist/display/profiles.d.ts +109 -4
  14. package/dist/display/profiles.js +270 -7
  15. package/dist/display/touch-adapter.js +118 -48
  16. package/dist/display/ui-adapter-eink.d.ts +2 -0
  17. package/dist/display/ui-adapter-eink.js +4 -0
  18. package/dist/display/ui-adapter-gray.d.ts +8 -0
  19. package/dist/display/ui-adapter-gray.js +170 -0
  20. package/dist/display/ui-adapter-mono.d.ts +13 -0
  21. package/dist/display/ui-adapter-mono.js +230 -0
  22. package/dist/display/ui-adapter-native.d.ts +10 -0
  23. package/dist/display/ui-adapter-native.js +295 -0
  24. package/dist/display/ui-adapter-shared.d.ts +11 -0
  25. package/dist/display/ui-adapter-shared.js +122 -0
  26. package/dist/display/ui-adapter.js +53 -101
  27. package/dist/dt-config/kconfig.d.ts +62 -1
  28. package/dist/dt-config/kconfig.js +123 -26
  29. package/dist/dt-config/overlay.d.ts +13 -0
  30. package/dist/dt-config/overlay.js +431 -16
  31. package/dist/framework.manifest.d.ts +9 -3
  32. package/dist/framework.manifest.js +123 -10
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +9 -1
  35. package/dist/licenses.js +5 -84
  36. package/dist/lowering/can.d.ts +25 -0
  37. package/dist/lowering/can.js +97 -0
  38. package/dist/lowering/clock.d.ts +17 -0
  39. package/dist/lowering/clock.js +58 -0
  40. package/dist/lowering/hid.d.ts +27 -0
  41. package/dist/lowering/hid.js +244 -0
  42. package/dist/lowering/i2c.d.ts +8 -0
  43. package/dist/lowering/i2c.js +140 -0
  44. package/dist/lowering/i2s.d.ts +27 -0
  45. package/dist/lowering/i2s.js +98 -0
  46. package/dist/lowering/index.d.ts +9 -1
  47. package/dist/lowering/index.js +25 -1
  48. package/dist/lowering/matrix.d.ts +15 -0
  49. package/dist/lowering/matrix.js +63 -0
  50. package/dist/lowering/power.d.ts +10 -0
  51. package/dist/lowering/power.js +35 -0
  52. package/dist/lowering/pwm.js +25 -0
  53. package/dist/lowering/sensor.d.ts +2 -2
  54. package/dist/lowering/sensor.js +8 -4
  55. package/dist/lowering/strip.d.ts +16 -0
  56. package/dist/lowering/strip.js +70 -0
  57. package/dist/lowering/thread.js +5 -1
  58. package/dist/lowering/trace.d.ts +44 -0
  59. package/dist/lowering/trace.js +239 -0
  60. package/dist/lowering/uart.js +6 -1
  61. package/dist/lowering/usb.d.ts +3 -1
  62. package/dist/lowering/usb.js +6 -3
  63. package/dist/sbom.d.ts +181 -0
  64. package/dist/sbom.js +901 -0
  65. package/dist/strategy.d.ts +60 -2
  66. package/dist/strategy.js +485 -25
  67. package/dist/toolchain/index.d.ts +12 -1
  68. package/dist/toolchain/index.js +115 -27
  69. package/dist/toolchain/scaffold.d.ts +4 -1
  70. package/dist/toolchain/scaffold.js +67 -3
  71. package/dist/west-inventory.d.ts +25 -0
  72. package/dist/west-inventory.js +97 -0
  73. package/package.json +3 -3
  74. package/src/audit.ts +529 -0
  75. package/src/boardgen.ts +54 -5
  76. package/src/chips/resolve.ts +21 -0
  77. package/src/chips/types.ts +576 -567
  78. package/src/display/bindings.ts +347 -0
  79. package/src/display/gfx.ts +318 -306
  80. package/src/display/index.ts +87 -70
  81. package/src/display/mipi-dbi-host.ts +183 -0
  82. package/src/display/profiles.ts +458 -139
  83. package/src/display/touch-adapter.ts +274 -204
  84. package/src/display/ui-adapter-eink.ts +13 -0
  85. package/src/display/ui-adapter-gray.ts +178 -0
  86. package/src/display/ui-adapter-mono.ts +238 -0
  87. package/src/display/ui-adapter-native.ts +304 -0
  88. package/src/display/ui-adapter-shared.ts +125 -0
  89. package/src/display/ui-adapter.ts +51 -100
  90. package/src/dt-config/kconfig.ts +647 -511
  91. package/src/dt-config/overlay.ts +433 -16
  92. package/src/framework.manifest.ts +131 -10
  93. package/src/index.ts +11 -1
  94. package/src/licenses.ts +5 -84
  95. package/src/lowering/can.ts +140 -0
  96. package/src/lowering/clock.ts +91 -0
  97. package/src/lowering/hid.ts +261 -0
  98. package/src/lowering/i2c.ts +146 -0
  99. package/src/lowering/i2s.ts +143 -0
  100. package/src/lowering/index.ts +18 -1
  101. package/src/lowering/matrix.ts +70 -0
  102. package/src/lowering/power.ts +41 -0
  103. package/src/lowering/pwm.ts +192 -167
  104. package/src/lowering/sensor.ts +159 -155
  105. package/src/lowering/strip.ts +81 -0
  106. package/src/lowering/thread.ts +5 -1
  107. package/src/lowering/trace.ts +270 -0
  108. package/src/lowering/uart.ts +6 -1
  109. package/src/lowering/usb.ts +224 -221
  110. package/src/sbom.ts +1117 -0
  111. package/src/strategy.ts +440 -25
  112. package/src/toolchain/index.ts +117 -28
  113. package/src/toolchain/scaffold.ts +62 -3
  114. package/src/west-inventory.ts +102 -0
@@ -26,6 +26,7 @@ import type { ToolchainOptions, CompileResult, UploadResult } from '@typecad/cut
26
26
  import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
27
27
  import { scaffoldZephyrProject, writeIfChanged, appendLibraryOverlayFragments } from './scaffold.js';
28
28
  import { parseZephyrDts, asBuiltJson } from '../as-built.js';
29
+ import { stampBuildSbom } from '../sbom.js';
29
30
  import { westSpawn, buildEnv } from './west-spawn.js';
30
31
  import { discoverWest } from './west-discover.js';
31
32
  import { writeDebugConfig, resolveDebugLocations, debugArtifactsNeedRewrite, DEBUG_SERVER_PORT, DEBUG_SERVER_TCL_PORT } from './debug-config.js';
@@ -39,7 +40,7 @@ import { resolveChipFromBoard } from '../chips/resolve.js';
39
40
  import type { ZephyrChipDescriptor } from '../chips/types.js';
40
41
  import { pwmDtAliasToken } from '../lowering/pwm.js';
41
42
  import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget } from './compat.js';
42
- import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
43
+ import { DEFAULT_ZEPHYR_DISPLAY_PROFILE, profileFromEmittedSource } from '../display/profiles.js';
43
44
 
44
45
  /**
45
46
  * Resolve the chip for a build the same way the strategy does at emit time —
@@ -194,6 +195,35 @@ function scanPwmUsedPins(src: string, chip: ZephyrChipDescriptor): number[] { c
194
195
  return pins;
195
196
  }
196
197
 
198
+ export function scanMatrix(src: string): { rows: number[]; cols: number[] } | undefined {
199
+ // The on_key lowering's comment marker carries the construction lists.
200
+ const m = src.match(/cuttlefish-matrix: rows=([\d,]*) cols=([\d,]*)/);
201
+ if (!m) return undefined;
202
+ const rows = m[1] ? m[1].split(',').filter(Boolean).map(Number) : [];
203
+ const cols = m[2] ? m[2].split(',').filter(Boolean).map(Number) : [];
204
+ if (rows.length === 0 || cols.length === 0) return undefined;
205
+ return { rows, cols };
206
+ }
207
+
208
+ export function scanHid(src: string): 'keyboard' | 'mouse' | undefined {
209
+ // The shim's report buffer names are the presence signal.
210
+ if (src.includes('__tc_mouse_report')) return 'mouse';
211
+ if (src.includes('__tc_kb_report')) return 'keyboard';
212
+ return undefined;
213
+ }
214
+
215
+ export function scanStrips(src: string): Array<{ index: number; count: number }> {
216
+ // The shim's buffer declaration is the presence signal:
217
+ // `static struct led_rgb __tc_strip<N>_buf[<COUNT>];`
218
+ const out = new Map<number, number>();
219
+ for (const m of src.matchAll(/__tc_strip(\d+)_buf\[(\d+)\]/g)) {
220
+ const idx = Number(m[1]);
221
+ const count = Number(m[2]);
222
+ out.set(idx, Math.max(out.get(idx) ?? 0, count));
223
+ }
224
+ return [...out.entries()].map(([index, count]) => ({ index, count }));
225
+ }
226
+
197
227
  /**
198
228
  * Bus controller indexes the emitted sources actually reference — the shim
199
229
  * declares one `__tc_<bus><N>_dev` state block per used instance (gated by
@@ -226,8 +256,10 @@ function scanUsedBusInstances(
226
256
  * compatible containing '_i2c<N>_' still resolves to the longest part match.
227
257
  */
228
258
  export interface ScannedSensorPart {
229
- part: string; busIndex: number; port: number; busKind: 'i2c' | 'spi';
259
+ part: string; busIndex: number; port: number; busKind: 'i2c' | 'spi' | 'w1';
230
260
  spiHz: number; spiMode: number; alertPin: number;
261
+ /** 1-Wire parts: the converter resolution in bits. */
262
+ resolution: number;
231
263
  }
232
264
 
233
265
  export interface ScannedSpiTarget {
@@ -248,13 +280,22 @@ export function scanSpiTargets(src: string): ScannedSpiTarget[] {
248
280
 
249
281
  export function scanSensorParts(src: string): ScannedSensorPart[] {
250
282
  const out = new Map<string, ScannedSensorPart>();
283
+ // 1-Wire sensors: the stem carries the data pin (w1 master's GPIO).
284
+ for (const m of src.matchAll(/__tc_sensor_([a-z0-9_]+)_w1_p(\d+)_dev\b/g)) {
285
+ out.set(`${m[1]}|w1|${m[2]}`, { part: m[1], busIndex: 0, port: parseInt(m[2], 10), busKind: 'w1', spiHz: 0, spiMode: 0, alertPin: -1, resolution: 12 });
286
+ }
251
287
  for (const m of src.matchAll(/__tc_sensor_([a-z0-9_]+)_(i2c|spi)(\d+)_(0x[0-9a-f]+|cs[0-9]+)_dev\b/g)) {
252
288
  const port = m[4].startsWith('0x') ? parseInt(m[4], 16) : parseInt(m[4].slice(2), 10);
253
- const ref: ScannedSensorPart = { part: m[1], busIndex: parseInt(m[3], 10), port, busKind: m[2] as 'i2c' | 'spi', spiHz: 0, spiMode: 0, alertPin: -1 };
289
+ const ref: ScannedSensorPart = { part: m[1], busIndex: parseInt(m[3], 10), port, busKind: m[2] as 'i2c' | 'spi', spiHz: 0, spiMode: 0, alertPin: -1, resolution: 12 };
254
290
  out.set(`${ref.part}|${ref.busKind}${ref.busIndex}|${ref.port}`, ref);
255
291
  }
256
292
  // Construction facts ride the state block's config comment — merge by
257
293
  // nodelabel so the scanner stays the single source for the overlay.
294
+ for (const m of src.matchAll(/tc-sensor-cfg: tc_([a-z0-9_]+)_w1_p(\d+) res=(\d+)/g)) {
295
+ const key = `${m[1]}|w1|${m[2]}`;
296
+ const existing = out.get(key);
297
+ if (existing) existing.resolution = parseInt(m[3], 10);
298
+ }
258
299
  for (const m of src.matchAll(/tc-sensor-cfg: tc_([a-z0-9_]+)_(i2c|spi)(\d+)_(0x[0-9a-f]+|cs[0-9]+) hz=(\d+) mode=(\d+) alert=(-?\d+)/g)) {
259
300
  const port = m[4].startsWith('0x') ? parseInt(m[4], 16) : parseInt(m[4].slice(2), 10);
260
301
  const key = `${m[1]}|${m[2]}${m[3]}|${port}`;
@@ -765,11 +806,13 @@ export const Toolchain = {
765
806
  const usesDisplay = uses('display_write') || uses('display_init')
766
807
  || uses('display_fill_rect') || uses('__tc_display_dev')
767
808
  || uses('CuttlefishDisplayTarget');
768
- // Both registered Zephyr display profiles use dtLabel 'display0', so the
769
- // default profile's overlay block (&display0 { status="okay" }) is correct
770
- // for either driver. Thread a non-default profile here only if a future
771
- // board carries a display node under a different nodelabel.
772
- const displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
809
+ // Recover the exact display profile from the marker the adapter stamped
810
+ // into the emitted source (the registry is the single source of truth).
811
+ // Fall back to the default profile only when a display is used but no
812
+ // marker matched (e.g. hand-written source).
813
+ const displayProfile = usesDisplay
814
+ ? (profileFromEmittedSource(src) ?? DEFAULT_ZEPHYR_DISPLAY_PROFILE)
815
+ : undefined;
773
816
  // Touch controller kind comes from which DT nodelabel the emitted adapter
774
817
  // references (FT6336U on I2C, XPT2046 on the display's SPI bus).
775
818
  const usesTouch = uses('ft6336u') || uses('touch_');
@@ -788,12 +831,29 @@ export const Toolchain = {
788
831
  spiTargets: spiTargetParts,
789
832
  usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
790
833
  usesUart: uses('__tc_uart'),
791
- usesUsb: uses('__tc_usb'),
834
+ usesUsb: uses('__tc_usb0'),
792
835
  usesPwm: uses('pwm_'),
793
836
  usesAdc: uses('adc_'),
794
837
  adcReadPins: scanAdcReadPins(src, chip),
795
838
  dacWritePins: scanDacWritePins(src),
796
839
  pwmUsedPins: scanPwmUsedPins(src, chip),
840
+ usesStrip: uses('led_strip'),
841
+ strips: scanStrips(src),
842
+ usesClock: uses('__tc_rtc'),
843
+ usesCan: uses('can_'),
844
+ canLoopback: uses('CAN_MODE_LOOPBACK'),
845
+ usesI2s: uses('i2s_'),
846
+ clockShimCounter: chip.hwtimer && chip.hwtimer.controllers.length > 0
847
+ ? {
848
+ label: chip.hwtimer.controllers[0].nodeLabel,
849
+ ...(chip.hwtimer.controllers[0].counterParent ? { parent: chip.hwtimer.controllers[0].counterParent } : {}),
850
+ }
851
+ : undefined,
852
+ usesHid: uses('__tc_hid_'),
853
+ hidProtocol: scanHid(src),
854
+ usesMatrix: uses('__tc_matrix'),
855
+ matrix: scanMatrix(src),
856
+ usesPower: uses('sys_poweroff'),
797
857
  i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
798
858
  spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
799
859
  uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
@@ -864,7 +924,8 @@ export const Toolchain = {
864
924
  const isGdbDebug = o.debug === true && debugMode === 'gdb';
865
925
  const zc = o.zephyrConfig as Record<string, unknown> | undefined;
866
926
  const userKconfig = zc?.kconfig as Record<string, string> | undefined;
867
- const configChanged = scaffoldZephyrProject(projectRoot, isGdbDebug, userKconfig, o.psram);
927
+ const traceCfg = zc?.trace as { enabled?: boolean; intervalMs?: number } | undefined;
928
+ const configChanged = scaffoldZephyrProject(projectRoot, isGdbDebug, userKconfig, o.psram, traceCfg);
868
929
 
869
930
  // Regenerate the DT overlay for the ACTUAL target board. prepare() writes
870
931
  // it for the default board (the real target is unknown until compile), so
@@ -900,29 +961,24 @@ export const Toolchain = {
900
961
  const usesDisplay = uses('display_write') || uses('display_init')
901
962
  || uses('display_fill_rect') || uses('__tc_display_dev')
902
963
  || uses('CuttlefishDisplayTarget');
903
- // Derive the display dimensions from the emitted adapter code
904
- // (display_width/height return the profile's w/h). This ensures the DT
905
- // overlay's width/height match the panel the adapter targets, not the
906
- // default profile — critical for drivers like ST7796S that initialize
907
- // the panel geometry from the DT node.
908
- let displayProfile = usesDisplay ? DEFAULT_ZEPHYR_DISPLAY_PROFILE : undefined;
909
- if (usesDisplay) {
910
- const wMatch = src.match(/display_width\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
911
- const hMatch = src.match(/display_height\(\)\s*\{\s*return\s+(\d+)\s*;\s*\}/);
912
- if (wMatch && hMatch) {
913
- displayProfile = {
914
- ...DEFAULT_ZEPHYR_DISPLAY_PROFILE,
915
- width: parseInt(wMatch[1], 10),
916
- height: parseInt(hMatch[1], 10),
917
- };
918
- }
919
- }
964
+ // Recover the exact display profile from the marker the adapter emitted
965
+ // (`typecad-display-profile: <driver>`), so the DT overlay's compatible
966
+ // string, geometry, and rotation match the panel the adapter targets.
967
+ // The width/height regex this replaces could produce franken-profiles —
968
+ // e.g. an ST7796S build (480x320) on the default ILI9341 profile's
969
+ // controller/compatible, which emitted an ilitek,ili9341 DT node for a
970
+ // panel the ST7796S adapter drives.
971
+ const displayProfile = usesDisplay
972
+ ? (profileFromEmittedSource(src) ?? DEFAULT_ZEPHYR_DISPLAY_PROFILE)
973
+ : undefined;
920
974
  // Extract display pin wiring (cs/dc/rst/spiFrequency/spiPins) from the
921
975
  // config display section so the DT overlay wires the MIPI DBI bridge to
922
976
  // the correct GPIOs + SPI bus pins.
923
977
  const dispCfg = o.display as Record<string, unknown> | undefined;
924
978
  const spiPins = (dispCfg?.spiPins ?? undefined) as
925
979
  { sck?: unknown; mosi?: unknown; miso?: unknown } | undefined;
980
+ const i2cPins = (dispCfg?.i2cPins ?? undefined) as
981
+ { sda?: unknown; scl?: unknown } | undefined;
926
982
  const wiring: DisplayWiring | undefined = dispCfg
927
983
  ? {
928
984
  cs: typeof dispCfg.cs === 'number' ? dispCfg.cs : undefined,
@@ -932,8 +988,12 @@ export const Toolchain = {
932
988
  sck: typeof spiPins?.sck === 'number' ? spiPins.sck : undefined,
933
989
  mosi: typeof spiPins?.mosi === 'number' ? spiPins.mosi : undefined,
934
990
  miso: typeof spiPins?.miso === 'number' ? spiPins.miso : undefined,
991
+ sda: typeof i2cPins?.sda === 'number' ? i2cPins.sda : undefined,
992
+ scl: typeof i2cPins?.scl === 'number' ? i2cPins.scl : undefined,
935
993
  backlightPin: typeof dispCfg.backlightPin === 'number' ? dispCfg.backlightPin : undefined,
936
994
  tearingEffectPin: typeof dispCfg.tearingEffectPin === 'number' ? dispCfg.tearingEffectPin : undefined,
995
+ busyPin: typeof (dispCfg as { busyPin?: number }).busyPin === 'number' ? (dispCfg as { busyPin?: number }).busyPin : undefined,
996
+ address: typeof dispCfg.address === 'number' ? dispCfg.address : undefined,
937
997
  }
938
998
  : undefined;
939
999
  // Extract touch pin wiring from the config display.touch section so the
@@ -986,13 +1046,30 @@ export const Toolchain = {
986
1046
  spiTargets: spiTargetParts,
987
1047
  usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
988
1048
  usesUart: uses('__tc_uart'),
989
- usesUsb: uses('__tc_usb'),
1049
+ usesUsb: uses('__tc_usb0'),
990
1050
  usesPwm: uses('pwm_'),
991
1051
  usesAdc: uses('adc_'),
992
1052
  adcReadPins: scanAdcReadPins(src, chip),
993
1053
  usesDac: uses('dac_') || uses('__tc_dac'),
994
1054
  dacWritePins: scanDacWritePins(src),
995
1055
  pwmUsedPins: scanPwmUsedPins(src, chip),
1056
+ usesStrip: uses('led_strip'),
1057
+ strips: scanStrips(src),
1058
+ usesHid: uses('__tc_hid_'),
1059
+ hidProtocol: scanHid(src),
1060
+ usesMatrix: uses('__tc_matrix'),
1061
+ matrix: scanMatrix(src),
1062
+ usesPower: uses('sys_poweroff'),
1063
+ usesClock: uses('__tc_rtc'),
1064
+ usesCan: uses('can_'),
1065
+ canLoopback: uses('CAN_MODE_LOOPBACK'),
1066
+ usesI2s: uses('i2s_'),
1067
+ clockShimCounter: chip.hwtimer && chip.hwtimer.controllers.length > 0
1068
+ ? {
1069
+ label: chip.hwtimer.controllers[0].nodeLabel,
1070
+ ...(chip.hwtimer.controllers[0].counterParent ? { parent: chip.hwtimer.controllers[0].counterParent } : {}),
1071
+ }
1072
+ : undefined,
996
1073
  i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
997
1074
  spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
998
1075
  uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
@@ -1175,6 +1252,18 @@ export const Toolchain = {
1175
1252
  } catch { /* best-effort snapshot — nothing to harvest or unreadable */ }
1176
1253
  }
1177
1254
 
1255
+ // SBOM stamp: after a successful build, record what the binary is made of
1256
+ // — kernel + linked west modules with commit SHAs + the hashed firmware
1257
+ // artifact (CycloneDX 1.6, next to zephyr.bin) — so `typecad-hal sbom
1258
+ // --check` can gate releases on the record still matching the build
1259
+ // (EU CRA essential-requirements support). Best-effort: a west-list or
1260
+ // hashing failure never blocks the build.
1261
+ if (result.status === 0) {
1262
+ try {
1263
+ stampBuildSbom({ buildDir, board, projectRoot });
1264
+ } catch { /* best-effort SBOM stamp */ }
1265
+ }
1266
+
1178
1267
  return {
1179
1268
  success: result.status === 0,
1180
1269
  output: header + output,
@@ -10,7 +10,9 @@
10
10
  import { writeFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { resolveKconfigFragments, type KconfigUsage } from '../dt-config/kconfig.js';
13
- import { scanSensorParts } from './index.js';
13
+ import { scanSensorParts, scanStrips, scanHid, scanMatrix } from './index.js';
14
+ import { profileFromEmittedSource, transportFor, panelControllerFor } from '../display/profiles.js';
15
+ import { readDisplayBinding } from '../display/bindings.js';
14
16
  import { readCuttlefishLibrarySidecar } from '@typecad/cuttlefish/library-packages';
15
17
  import { SENSOR_PART_INFO } from '@typecad/hal';
16
18
 
@@ -96,7 +98,7 @@ export function appendLibraryOverlayFragments(overlay: string, projectRoot: stri
96
98
  *
97
99
  * Idempotent. Mirrors scaffoldEspIdfProject's writeIfChanged discipline.
98
100
  */
99
- export function scaffoldZephyrProject(projectRoot: string, debug = false, userKconfig?: Record<string, string>, psram?: 'opi' | 'quad'): boolean {
101
+ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKconfig?: Record<string, string>, psram?: 'opi' | 'quad', trace?: { enabled?: boolean; intervalMs?: number }): boolean {
100
102
  const srcDir = join(projectRoot, 'src');
101
103
  if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
102
104
 
@@ -122,6 +124,22 @@ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKc
122
124
  const usage: KconfigUsage = {
123
125
  usesAdc: uses('adc_'),
124
126
  usesPwm: uses('pwm_'),
127
+ usesStrip: uses('led_strip'),
128
+ strips: scanStrips(src),
129
+ // Sensor parts drive CONFIG_W1 (1-Wire masters) and the exception
130
+ // lines; the same scan the exception block below re-runs.
131
+ sensorParts: scanSensorParts(src),
132
+ usesHid: uses('__tc_hid_'),
133
+ hidProtocol: scanHid(src),
134
+ usesMatrix: uses('__tc_matrix'),
135
+ matrix: scanMatrix(src),
136
+ usesPower: uses('sys_poweroff'),
137
+ usesClock: uses('__tc_rtc'),
138
+ usesCan: uses('can_'),
139
+ canLoopback: uses('CAN_MODE_LOOPBACK'),
140
+ usesI2s: uses('i2s_'),
141
+ // No chip facts at scaffold time — the prepare overlay omits the shim;
142
+ // the chip-aware build path synthesizes it.
125
143
  usesDac: uses('dac_') || uses('__tc_dac'),
126
144
  usesFS: uses('__tc_fs'),
127
145
  usesHwtimer: uses('counter_') || uses('__tc_hw'),
@@ -130,6 +148,9 @@ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKc
130
148
  // call yet), but the shim still declared DEVICE_DT_GET(DT_NODELABEL(...))
131
149
  // so the overlay must enable the node or the device symbol is missing.
132
150
  usesI2c: uses('i2c_') || uses('__tc_i2c'),
151
+ // I2C target mode: every i2c.resp_* lowering calls i2c_target_register
152
+ // (and the shim's state carries the i2c_target_* vocabulary).
153
+ usesI2cTarget: uses('i2c_target_'),
133
154
  // DT-bound sensor parts: the __tc_sensor_* state references and the
134
155
  // sensor_sample_fetch/sensor_channel_get calls both carry the token.
135
156
  usesSensor: uses('sensor_') || uses('__tc_sensor'),
@@ -138,12 +159,28 @@ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKc
138
159
  usesUart: uses('uart_') || uses('__tc_uart'),
139
160
  // USB CDC serial: every usb.* lowering calls into the __tc_usb<N>_* shim
140
161
  // (device + init helper emitted under usesUsb).
141
- usesUsb: uses('__tc_usb'),
162
+ usesUsb: uses('__tc_usb0'),
142
163
  // STM32F4 DBGMCU keep-SWD-alive init present (emitted for stm32f4 socs).
143
164
  usesStm32DebugSleep: uses('__tc_stm32_dbgmcu'),
144
165
  usesWdt: uses('wdt_'),
145
166
  usesBle: uses('bt_') || uses('bt_gatt') || uses('bt_le_'),
146
167
  usesDisplay: uses('display_write') || uses('display_init') || uses('display_fill_rect') || uses('__tc_display_dev') || uses('CuttlefishDisplayTarget'),
168
+ // Display transport + controller: recovered from the profile marker the
169
+ // display adapters stamp into the emitted source (the profile registry is
170
+ // the single source of truth — no geometry re-derivation here).
171
+ ...((): Pick<KconfigUsage, 'displayTransport' | 'displayController' | 'displayBus' | 'displayKconfigExtra'> => {
172
+ const profile = profileFromEmittedSource(src);
173
+ if (!profile) return {};
174
+ // Registry profiles name a DT compatible via dtCompatible (the driver
175
+ // id itself isn't one); drop-in profiles use the driver string directly.
176
+ const bus = readDisplayBinding(profile.dtCompatible ?? profile.driver)?.busFamily;
177
+ if (bus === 'i2c') return { displayBus: 'i2c' };
178
+ // Board-provided panels (native_sim's sdl_dc) carry their own fragments
179
+ // and no bus at all.
180
+ if (profile.boardProvidesDisplay) return { displayKconfigExtra: profile.kconfig };
181
+ if (transportFor(profile) !== 'zephyr-display') return {};
182
+ return { displayTransport: 'zephyr-display', displayController: panelControllerFor(profile) };
183
+ })(),
147
184
  usesTouch: uses('ft6336u') || uses('touch_'),
148
185
  // (The power HAL is removed; its former pm_/k_sleep token scan is gone —
149
186
  // nothing here must match tx_power_dbm's `power` substring.)
@@ -267,6 +304,28 @@ export function scaffoldZephyrProject(projectRoot: string, debug = false, userKc
267
304
  }
268
305
  }
269
306
  }
307
+ // ── Trace heartbeat Kconfig ────────────────────────────────────────────────
308
+ // zephyr.trace.enabled in typecad-hal.config.ts turns on the [TR: sampler
309
+ // (emitted by the strategy at transpile time from the same config record).
310
+ // The symbol set is exactly what the sampler calls: runtime stats
311
+ // (k_thread_runtime_stats_*), the monitor list (k_thread_foreach —
312
+ // kernel/thread_monitor.c only links under THREAD_MONITOR), thread names
313
+ // (k_thread_name_get/set), and stack inspection (0xAA fill +
314
+ // stack_info.size). User zephyr.kconfig overrides still win.
315
+ if (trace?.enabled === true) {
316
+ const traceSymbols: Array<[string, string]> = [
317
+ ['CONFIG_THREAD_RUNTIME_STATS', 'y'],
318
+ ['CONFIG_THREAD_MONITOR', 'y'],
319
+ ['CONFIG_THREAD_NAME', 'y'],
320
+ ['CONFIG_INIT_STACKS', 'y'],
321
+ ['CONFIG_THREAD_STACK_INFO', 'y'],
322
+ ];
323
+ prjConf.push('', '# Trace heartbeat (typecad-hal.config.ts → zephyr.trace).');
324
+ for (const [sym, val] of traceSymbols) {
325
+ if (userKconfig && userKconfig.hasOwnProperty(sym)) continue;
326
+ prjConf.push(`${sym}=${val}`);
327
+ }
328
+ }
270
329
  // Per-part Kconfig exceptions: catalog parts whose driver is NOT default-y
271
330
  // on its DT node carry extra lines here (empty today — every in-tree sensor
272
331
  // driver lights up from the node; the path exists so the first exception
@@ -0,0 +1,102 @@
1
+ // ---------------------------------------------------------------------------
2
+ // @typecad/framework-zephyr — shared west workspace ground truth
3
+ //
4
+ // The as-built inventory both `typecad-hal licenses` and `typecad-hal sbom`
5
+ // report from: which west manifest modules the last build ACTUALLY compiled
6
+ // (linked-module filtering via the build's compile_commands.json), plus the
7
+ // build-directory discovery that locates it. A west manifest carries every
8
+ // vendor HAL/library; almost none are linked by a single project — both the
9
+ // license table and the SBOM scope to the linked set by default, so they stay
10
+ // honest about what is actually inside the firmware.
11
+ //
12
+ // Moved out of licenses.ts when the sbom command grew a second consumer; the
13
+ // behavior (case-insensitive, separator-agnostic path matching; degrade to
14
+ // "no filtering" on unparseable build data) is unchanged.
15
+ // ---------------------------------------------------------------------------
16
+
17
+ import * as path from 'node:path';
18
+ import type { ReadDir, ReadFile } from '@typecad/cuttlefish/api/shared';
19
+
20
+ /** Normalize a path for case-insensitive, separator-agnostic comparison. */
21
+ export function norm(p: string): string {
22
+ return p.split('\\').join('/').toLowerCase();
23
+ }
24
+
25
+ /**
26
+ * Filter the west manifest to the modules whose sources were compiled in the
27
+ * last build. A module is "linked" iff some compiled translation unit lives
28
+ * under its abspath. If the build data is unparseable, no filtering is applied
29
+ * (degrade to listing all modules rather than reporting nothing).
30
+ */
31
+ export function filterLinkedModules(
32
+ ccText: string,
33
+ modules: { name: string; abspath: string }[],
34
+ ): { name: string; abspath: string }[] {
35
+ let arr: unknown;
36
+ try {
37
+ arr = JSON.parse(ccText);
38
+ } catch {
39
+ return modules;
40
+ }
41
+ if (!Array.isArray(arr)) return modules;
42
+ // One normalized blob of every compiled source path; membership is then a
43
+ // substring check per module (O(modules) after an O(TUs) join).
44
+ const blob = arr
45
+ .map((e) => norm(typeof (e as { file?: unknown })?.file === 'string' ? (e as { file: string }).file : ''))
46
+ .join('\n');
47
+ return modules.filter((m) => blob.includes(norm(m.abspath) + '/'));
48
+ }
49
+
50
+ /**
51
+ * Find the build's compile_commands.json path under `cwd`. Cuttlefish's Zephyr
52
+ * project root is the transpile output dir (e.g. `<cwd>/src/out`), so the
53
+ * build dir is `<root>/build` — not necessarily `<cwd>/build`. Check the
54
+ * common layouts, then fall back to a bounded search (skipping
55
+ * node_modules/.git/dist and dot-directories). Returns the PATH (the caller
56
+ * decides whether to read it); undefined when no build exists.
57
+ */
58
+ export function findCompileCommandsPath(
59
+ readFile: ReadFile,
60
+ readdir: ReadDir,
61
+ cwd = process.cwd(),
62
+ ): string | undefined {
63
+ const direct = [
64
+ path.join(cwd, 'build', 'compile_commands.json'),
65
+ path.join(cwd, 'out', 'build', 'compile_commands.json'),
66
+ path.join(cwd, 'src', 'out', 'build', 'compile_commands.json'),
67
+ ];
68
+ for (const c of direct) {
69
+ if (readFile(c)) return c;
70
+ }
71
+ // Bounded recursive search for a build/compile_commands.json.
72
+ const isCc = (p: string) => {
73
+ const n = norm(p);
74
+ return n.endsWith('/build/compile_commands.json') || n.endsWith('\\build\\compile_commands.json');
75
+ };
76
+ let found: string | undefined;
77
+ const seen = new Set<string>();
78
+ const walk = (dir: string, depth: number): void => {
79
+ if (found || depth > 4 || seen.has(dir)) return;
80
+ seen.add(dir);
81
+ let entries: string[];
82
+ try {
83
+ entries = readdir(dir);
84
+ } catch {
85
+ return;
86
+ }
87
+ for (const e of entries) {
88
+ const full = path.join(dir, e);
89
+ if (e === 'compile_commands.json' && isCc(full)) {
90
+ found = full;
91
+ return;
92
+ }
93
+ }
94
+ for (const e of entries) {
95
+ if (found) return;
96
+ if (e === 'node_modules' || e === '.git' || e === 'dist' || e.startsWith('.')) continue;
97
+ walk(path.join(dir, e), depth + 1);
98
+ }
99
+ };
100
+ walk(cwd, 0);
101
+ return found;
102
+ }