tirtc-device-builder 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +23 -0
  3. package/README.md +44 -14
  4. package/bin/tirtc-device-builder.js +33 -1
  5. package/package.json +1 -1
  6. package/skills/tirtc-esp32-builder/SKILL.md +139 -39
  7. package/skills/tirtc-esp32-builder/USAGE.md +3 -2
  8. package/skills/tirtc-esp32-builder/VERSION +1 -1
  9. package/skills/tirtc-esp32-builder/agents/openai.yaml +1 -1
  10. package/skills/tirtc-esp32-builder/assets/board-audio-contract.example.json +4 -0
  11. package/skills/tirtc-esp32-builder/assets/board-identity.example.json +16 -0
  12. package/skills/tirtc-esp32-builder/assets/developer-intake-prompt.md +3 -3
  13. package/skills/tirtc-esp32-builder/assets/hardware-ir-v2.example.json +9 -1
  14. package/skills/tirtc-esp32-builder/assets/lckfb-szpi-esp32s3-portable-prompt.md +5 -5
  15. package/skills/tirtc-esp32-builder/assets/report-template.md +6 -1
  16. package/skills/tirtc-esp32-builder/assets/tirtc-runtime-contract.example.json +13 -0
  17. package/skills/tirtc-esp32-builder/knowledge/board-registry.json +4 -0
  18. package/skills/tirtc-esp32-builder/references/audio-contract.md +33 -3
  19. package/skills/tirtc-esp32-builder/references/board-knowledge.md +72 -0
  20. package/skills/tirtc-esp32-builder/references/capability-rules.md +11 -1
  21. package/skills/tirtc-esp32-builder/references/environment.md +20 -0
  22. package/skills/tirtc-esp32-builder/references/hardware-ir.md +17 -3
  23. package/skills/tirtc-esp32-builder/references/porting-risks.md +25 -0
  24. package/skills/tirtc-esp32-builder/references/reporting.md +1 -1
  25. package/skills/tirtc-esp32-builder/references/runtime-contract.md +12 -2
  26. package/skills/tirtc-esp32-builder/references/tirtc-platform.md +92 -0
  27. package/skills/tirtc-esp32-builder/references/workflow.md +31 -11
  28. package/skills/tirtc-esp32-builder/scripts/audio_contract.py +105 -3
  29. package/skills/tirtc-esp32-builder/scripts/board_registry.py +677 -0
  30. package/skills/tirtc-esp32-builder/scripts/hardware_ir.py +147 -8
  31. package/skills/tirtc-esp32-builder/scripts/project_portability.py +178 -3
  32. package/skills/tirtc-esp32-builder/scripts/runtime_contract.py +122 -1
@@ -192,18 +192,114 @@ def check_topology(contract: dict[str, Any], errors: list[str]) -> None:
192
192
  )
193
193
  if simultaneous is False and handoff == "none":
194
194
  errors.append("half-duplex shared clocks require an explicit handoff")
195
+ if simultaneous is True and handoff != "none":
196
+ errors.append("simultaneous directions must not use a clock handoff")
197
+ paired_channels = shared.get("paired_channels", False)
198
+ if not isinstance(paired_channels, bool):
199
+ errors.append("shared_clock.paired_channels must be true or false")
200
+ paired_channels = False
201
+
202
+ mixed_paired_ok = False
195
203
  if simultaneous is True and capture.get("mode") != playback.get("mode"):
196
- errors.append("simultaneous directions cannot use different I2S modes")
204
+ if paired_channels is not True:
205
+ errors.append(
206
+ "simultaneous mixed I2S modes require shared_clock.paired_channels=true"
207
+ )
208
+ if capture.get("controller") != playback.get("controller"):
209
+ errors.append(
210
+ "simultaneous mixed I2S modes require paired directions on one controller"
211
+ )
212
+ frame_bits: dict[str, int] = {}
213
+ for endpoint, label in ((capture, "capture"), (playback, "playback")):
214
+ slots = require_positive_int(
215
+ endpoint.get("slot_count"), f"{label}.slot_count", errors
216
+ )
217
+ width = require_positive_int(
218
+ endpoint.get("slot_bit_width"),
219
+ f"{label}.slot_bit_width",
220
+ errors,
221
+ )
222
+ if slots is not None and width is not None:
223
+ frame_bits[label] = slots * width
224
+ if len(frame_bits) == 2 and frame_bits["capture"] != frame_bits["playback"]:
225
+ errors.append(
226
+ "simultaneous mixed I2S modes require equal BCLKs per frame: "
227
+ f"capture={frame_bits['capture']} playback={frame_bits['playback']}"
228
+ )
229
+ mixed_paired_ok = (
230
+ paired_channels is True
231
+ and capture.get("controller") == playback.get("controller")
232
+ and len(frame_bits) == 2
233
+ and frame_bits["capture"] == frame_bits["playback"]
234
+ )
197
235
  if (
198
236
  capture.get("controller") == playback.get("controller")
199
237
  and capture.get("mode") != playback.get("mode")
238
+ and not mixed_paired_ok
200
239
  and handoff != "delete_recreate"
201
240
  ):
202
241
  errors.append(
203
242
  "one I2S controller cannot retain different capture/playback modes; "
204
- "use distinct controllers or delete_recreate handoff"
243
+ "use paired frame-compatible directions, distinct controllers, or "
244
+ "delete_recreate handoff"
205
245
  )
206
246
 
247
+ aec_value = contract.get("echo_cancellation")
248
+ if aec_value is None:
249
+ return
250
+ aec = require_mapping(aec_value, "echo_cancellation", errors)
251
+ enabled = aec.get("enabled")
252
+ if not isinstance(enabled, bool):
253
+ errors.append("echo_cancellation.enabled must be true or false")
254
+ return
255
+ if not enabled:
256
+ return
257
+ if simultaneous is not True:
258
+ errors.append("echo cancellation requires simultaneous capture and playback")
259
+ if capture_mode != "tdm":
260
+ errors.append("echo cancellation reference mapping requires TDM capture")
261
+ sample_rate = require_positive_int(
262
+ aec.get("sample_rate_hz"), "echo_cancellation.sample_rate_hz", errors
263
+ )
264
+ clock = contract.get("clock", {})
265
+ if sample_rate is not None and sample_rate != clock.get("sample_rate_hz"):
266
+ errors.append("echo cancellation sample rate must match the hardware clock")
267
+
268
+ microphone_slot = aec.get("microphone_slot")
269
+ reference_slot = aec.get("reference_slot")
270
+ microphone_signal = require_nonempty_string(
271
+ aec.get("microphone_signal"), "echo_cancellation.microphone_signal", errors
272
+ )
273
+ reference_signal = require_nonempty_string(
274
+ aec.get("reference_signal"), "echo_cancellation.reference_signal", errors
275
+ )
276
+ slot_signals = capture.get("slot_signals")
277
+ slot_count = capture.get("slot_count")
278
+ for slot, signal, label in (
279
+ (microphone_slot, microphone_signal, "microphone"),
280
+ (reference_slot, reference_signal, "reference"),
281
+ ):
282
+ if (
283
+ isinstance(slot, bool)
284
+ or not isinstance(slot, int)
285
+ or slot < 0
286
+ or not isinstance(slot_count, int)
287
+ or slot >= slot_count
288
+ ):
289
+ errors.append(
290
+ f"echo_cancellation.{label}_slot must select an available TDM slot"
291
+ )
292
+ elif isinstance(slot_signals, list) and slot < len(slot_signals):
293
+ if signal != slot_signals[slot]:
294
+ errors.append(
295
+ f"echo_cancellation.{label}_signal does not match "
296
+ f"capture.slot_signals at slot {slot}"
297
+ )
298
+ if microphone_slot == reference_slot:
299
+ errors.append("echo cancellation microphone and reference slots must differ")
300
+ if microphone_signal is not None and microphone_signal == reference_signal:
301
+ errors.append("echo cancellation microphone and reference signals must differ")
302
+
207
303
 
208
304
  def check_assertions(
209
305
  project: Path,
@@ -277,12 +373,18 @@ def verify_contract(contract_path: Path, project_path: Path) -> dict[str, Any]:
277
373
  check_topology(contract, errors)
278
374
  check_assertions(project, contract, errors, inputs)
279
375
  clock = contract.get("clock", {})
376
+ shared = contract.get("shared_clock", {})
377
+ aec = contract.get("echo_cancellation", {})
280
378
  return {
281
379
  "ok": not errors,
282
380
  "summary": (
283
381
  f"sample_rate={clock.get('sample_rate_hz')}Hz "
284
- f"mclk={clock.get('mclk_hz')}Hz ratio={clock.get('mclk_ratio')}"
382
+ f"mclk={clock.get('mclk_hz')}Hz ratio={clock.get('mclk_ratio')} "
383
+ f"simultaneous={shared.get('directions_simultaneous')} "
384
+ f"aec={aec.get('enabled', False)}"
285
385
  ),
386
+ "simultaneous_capture_playback": shared.get("directions_simultaneous") is True,
387
+ "echo_cancellation_enabled": aec.get("enabled") is True,
286
388
  "inputs": inputs,
287
389
  "errors": errors,
288
390
  }