tirtc-device-builder 0.8.0 → 0.8.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tirtc-device-builder",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Codex workflows for building and validating TiRTC device firmware across supported chip platforms.",
5
5
  "author": {
6
6
  "name": "TangeAI",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  This project follows Semantic Versioning.
4
4
 
5
+ ## 0.8.1
6
+
7
+ - Separate rapid `idf.py flash` iteration from portable evidence-bundle export,
8
+ so active development keeps the normal `build/` tree and does not require a
9
+ new multi-BIN archive for every device test.
10
+ - Add deterministic ESP-IDF application-descriptor inspection for explicit
11
+ firmware versions, application-BIN SHA-256, and full ELF SHA-256 matching.
12
+ - Add product-control rules that distinguish MCU buttons, boot straps, reset
13
+ lines and PMIC/power-latch keys across baseboard and battery-carrier variants,
14
+ and route button gestures through the runtime's intent queue.
15
+ - Add trace-based diagnosis for repeated AI/H5 downlink audio across SDK
16
+ callbacks, bounded queues, playback, resampling, and codec/I2S writes.
17
+
5
18
  ## 0.8.0
6
19
 
7
20
  - Add an evidence-backed board identity and curated registry workflow with exact,
package/README.md CHANGED
@@ -549,7 +549,19 @@ python3 ~/.codex/skills/tirtc-esp32-builder/scripts/install_video_gate.py .
549
549
  idf.py build
550
550
  ```
551
551
 
552
- 只安装实际请求能力的门禁。编译后把 BIN/ELF 的路径、大小和 SHA-256 写入 Hardware IR 的 `build_evidence.artifacts[]`,再运行 build 阶段评估。编译成功但语义门禁缺失或失败时只能记录 `COMPILE_PASS / CAPABILITY_BLOCKED`。
552
+ 只安装实际请求能力的门禁。编译后先核对固件内嵌版本、应用 BIN
553
+ SHA-256 和完整 ELF SHA-256:
554
+
555
+ ```bash
556
+ python3 ~/.codex/skills/tirtc-esp32-builder/scripts/firmware_identity.py \
557
+ build/<app>.bin --elf build/<app>.elf --expect-version <expected-version>
558
+ ```
559
+
560
+ 随后按
561
+ [`firmware-delivery.md`](skills/tirtc-esp32-builder/references/firmware-delivery.md)
562
+ 选择快速真机迭代或可移植证据包;该文档也规定从 `build/` 记录迁移到
563
+ `artifacts/` 时如何保持 Hardware IR 和报告一致。编译成功但语义门禁缺失
564
+ 或失败时只能记录 `COMPILE_PASS / CAPABILITY_BLOCKED`。
553
565
 
554
566
  生成器会把 TiRTC SDK 复制到工程的 `third_party/tirtc/`。此后工程不再依赖 `tirtc-server-example`,但换机编译仍需准备兼容的 ESP-IDF 5.5.x 工具链。
555
567
 
@@ -599,7 +611,10 @@ cd /absolute/path/my-esp32-device
599
611
  idf.py -p /dev/ttyACM0 flash monitor
600
612
  ```
601
613
 
602
- 使用 `Ctrl+]` 退出 ESP-IDF Monitor。多个串口同时存在时,需要通过 USB 拔插、设备标识或芯片探测确认目标,不能默认烧录第一个端口。
614
+ 普通开发烧录优先使用这一条命令,让 ESP-IDF 从当前构建目录解析
615
+ bootloader、分区表和应用镜像;无需手工维护多条 `esptool.py` 地址参数。
616
+ 使用 `Ctrl+]` 退出 ESP-IDF Monitor。多个串口同时存在时,需要通过 USB
617
+ 拔插、设备标识或芯片探测确认目标,不能默认烧录第一个端口。
603
618
 
604
619
  ### Wi-Fi 凭证和设备绑定
605
620
 
@@ -911,8 +926,8 @@ metadata 中的版本、标签、上游 commit 和期望 SHA-256 必须与本地
911
926
 
912
927
  ```bash
913
928
  npm test
914
- git tag -a v0.8.0 -m "v0.8.0"
915
- git push origin v0.8.0
929
+ git tag -a v0.8.1 -m "v0.8.1"
930
+ git push origin v0.8.1
916
931
  ```
917
932
 
918
933
  不要重复发布已经存在的 npm 版本。版本变化同步更新 `package.json`、`.codex-plugin/plugin.json` 和发布说明。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tirtc-device-builder",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Install and run TiRTC device-development skills for Codex.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -98,18 +98,29 @@ H5/AI/call runtime behavior, read and install the applicable semantic contracts.
98
98
  Keep concrete board values in Hardware IR, contracts and adapter files rather
99
99
  than Skill defaults.
100
100
 
101
+ When the product request includes a physical button, touch input, reset, power
102
+ key, wake source or enclosure label, read
103
+ [product-controls.md](references/product-controls.md). Treat the exact physical
104
+ product as baseboard plus any carrier, PMIC and enclosure controls. Board input
105
+ code emits bounded intents to the state-owning runtime; it does not own session
106
+ lifecycle.
107
+
101
108
  ## 6. Build and assess
102
109
 
103
- Run focused tests, resolve managed components, validate the exact SDK
110
+ Read [firmware-delivery.md](references/firmware-delivery.md) and select
111
+ `development flash` or `evidence bundle` from the user's current goal. Run
112
+ focused tests, resolve managed components, validate the exact SDK
104
113
  `manifest/build-contract.env`, install every applicable semantic gate, then run
105
114
  the ordinary ESP-IDF build. Requested call/VoIP features must be represented by
106
115
  the runtime/business contract and the unified arbiter implementation; an H5/AI-
107
116
  only starter cannot reach `BUILD_VERIFIED` for those features.
108
117
 
109
- Copy final BIN/ELF deliverables into project-relative `artifacts/`, record actual
110
- size and SHA-256 in Hardware IR, then run strict build assessment with `--project`
111
- and that exact hash. Compiler success with a missing feature gate is
112
- `COMPILE_PASS`, not product completion.
118
+ Set an explicit firmware version, verify the application descriptor and full
119
+ ELF identity with `firmware_identity.py`, and record the application-BIN hash
120
+ with its exact label. Follow the selected delivery mode's artifact paths and
121
+ retention rules, record actual size and SHA-256 in Hardware IR, then run strict
122
+ build assessment with `--project` and that exact hash. Compiler success with a
123
+ missing feature gate is `COMPILE_PASS`, not product completion.
113
124
 
114
125
  ## 7. Flash, verify, and learn
115
126
 
@@ -129,9 +140,9 @@ board; attach artifact evidence; add regression tests for generic invariants;
129
140
  then publish a new Skill/registry version. Never let an installed Skill mutate
130
141
  itself from conversation history.
131
142
 
132
- Remove machine-bound `build/` after the final assessment, retain verified
133
- project-relative artifacts, run `project_portability.py --export`, and return the
134
- project, Hardware IR, identity match, capability result, artifacts and report.
143
+ Complete the selected mode's retention or export checks from
144
+ `firmware-delivery.md`. Return the project, Hardware IR, identity match,
145
+ capability result, exact artifacts, flash command when applicable, and report.
135
146
 
136
147
  ## Security boundary
137
148
 
@@ -102,6 +102,20 @@ python3 <skill-dir>/scripts/hardware_ir.py assess --phase hil \
102
102
 
103
103
  `init` 默认创建 schema v2;v1 仅用于兼容已有 H.264 IR。`BLOCKED` 表示资料已确认硬件/合同/资源或凭证策略不满足;`NEEDS_CONFIRMATION` 表示当前阶段仍有未知项或证据不足;`READY_TO_PORT` 表示可以生成并实现板级适配;`BUILD_VERIFIED` 表示精确 artifact 已通过源码、编译和 post-link 门禁;`HIL_VERIFIED` 还要求同一 SHA-256 的 L5/L6 运行证据。
104
104
 
105
+ ## 固件身份与烧录方式
106
+
107
+ 顶层 `CMakeLists.txt` 应在 `project()` 前设置明确的 `PROJECT_VER`。编译后
108
+ 核对应用 BIN 中的版本和完整 ELF SHA-256:
109
+
110
+ ```bash
111
+ python3 <skill-dir>/scripts/firmware_identity.py build/<app>.bin \
112
+ --elf build/<app>.elf --expect-version <expected-version>
113
+ ```
114
+
115
+ 按 [firmware-delivery.md](references/firmware-delivery.md) 选择快速真机迭代或
116
+ 可移植证据包;模式转换时必须同步迁移 Hardware IR 与报告中的 artifact
117
+ 路径。普通开发烧录命令为 `idf.py -p <exact-port> flash monitor`。
118
+
105
119
  ## 当前边界
106
120
 
107
121
  ThingConnect 仓库提供 ESP32-S3 H5/AI 模板和生成器,但默认媒体适配器不包含特定开发板的摄像头、麦克风、选定视频路径、Wi-Fi 凭证方法和扬声器驱动。模板生成和编译成功只证明工程与协议骨架可用,不代表 Web 已经出图或 AI 音频已经通过实机验收。
@@ -1 +1 @@
1
- 0.8.0
1
+ 0.8.1
@@ -7,7 +7,7 @@ $tirtc-esp32-builder
7
7
  前置条件(必须由开发者在启动本次 Codex 会话前完成,不属于本提示词内的操作):
8
8
 
9
9
  ```bash
10
- npx --yes tirtc-device-builder@0.8.0 setup esp32 --install --force-skill
10
+ npx --yes tirtc-device-builder@0.8.1 setup esp32 --install --force-skill
11
11
  ```
12
12
 
13
13
  该命令只允许在当前用户目录安装固定版本的 Skill、managed ESP32 Device Kit、ESP-IDF 和工具链;禁止 sudo、系统级包变更和修改 shell profile。安装完成后,开发者必须关闭原 Codex 会话,再从本 clean-room 工作区启动一个新会话,然后粘贴本提示词。
@@ -15,11 +15,11 @@ npx --yes tirtc-device-builder@0.8.0 setup esp32 --install --force-skill
15
15
  本轮第一步先只读运行:
16
16
 
17
17
  ```bash
18
- npx --yes tirtc-device-builder@0.8.0 --version
19
- npx --yes tirtc-device-builder@0.8.0 setup esp32
18
+ npx --yes tirtc-device-builder@0.8.1 --version
19
+ npx --yes tirtc-device-builder@0.8.1 setup esp32
20
20
  ```
21
21
 
22
- 必须根据命令的实际输出和本机文件确认:npm 包为 0.8.0、已安装 Skill 的 `VERSION` 为 0.8.0、所选 Device Kit 的 `manifest.json` 中 `kit_version` 为 1.1.1,并且 Doctor 对 `--expected-kit 1.1.1` 输出 `OVERALL: PASS`。Plugin manifest 不属于这种 npm 安装方式的运行时前置条件,不得把不可访问的 Plugin 版本当作阻塞项。如果版本不一致、Skill 是在当前会话启动后才安装,或环境检查未通过,停止并报告前置条件不成立;不要在当前会话中替换 Skill 后继续生成工程。
22
+ 必须根据命令的实际输出和本机文件确认:npm 包为 0.8.1、已安装 Skill 的 `VERSION` 为 0.8.1、所选 Device Kit 的 `manifest.json` 中 `kit_version` 为 1.1.1,并且 Doctor 对 `--expected-kit 1.1.1` 输出 `OVERALL: PASS`。Plugin manifest 不属于这种 npm 安装方式的运行时前置条件,不得把不可访问的 Plugin 版本当作阻塞项。如果版本不一致、Skill 是在当前会话启动后才安装,或环境检查未通过,停止并报告前置条件不成立;不要在当前会话中替换 Skill 后继续生成工程。
23
23
 
24
24
  工作区与 clean-room 边界:
25
25
  - 将启动 Codex 时的当前目录定义为 `WORKSPACE_ROOT`。
@@ -64,9 +64,12 @@
64
64
 
65
65
  ## Firmware and flash record
66
66
 
67
+ - Delivery mode (`development flash` or `evidence bundle`): `{{DELIVERY_MODE}}`
68
+ - Application project/version: `{{FIRMWARE_PROJECT_VERSION}}`
67
69
  - Serial port/chip: `{{SERIAL_TARGET}}`
68
70
  - Project-relative firmware artifacts: `{{FIRMWARE_ARTIFACTS}}`
69
- - Firmware SHA-256: `{{FIRMWARE_SHA256}}`
71
+ - Application-BIN SHA-256: `{{FIRMWARE_BIN_SHA256}}`
72
+ - Descriptor/full ELF SHA-256: `{{FIRMWARE_ELF_SHA256}}`
70
73
  - Flash command/result: `{{FLASH_RESULT}}`
71
74
 
72
75
  ## Artifact-bound runtime evidence
@@ -63,13 +63,19 @@ the packaged S3 archive as a compatible fallback.
63
63
 
64
64
  SDK resolution is independent after generation: an explicit `--sdk-dir` wins, followed by `<project>/third_party/tirtc`, then the SDK packaged in the resolved Device Kit or legacy workspace. The generated project remains diagnosable after it is moved away from the Kit.
65
65
 
66
- Before copying a generated project to another machine, run:
66
+ For the evidence-bundle mode defined in
67
+ [firmware-delivery.md](firmware-delivery.md), run:
67
68
 
68
69
  ```bash
69
70
  python3 <skill-dir>/scripts/project_portability.py <generated-project> --export
70
71
  ```
71
72
 
72
- Copy source inputs only. Never export `build/`: CMake caches absolute source, toolchain, and Python paths from the originating machine. `managed_components/` may be regenerated from the committed `dependencies.lock`; the bundled `third_party/tirtc` SDK and its build contract must remain in the source package. CMake must invoke shell gates through `bash <script>` so the build does not depend on archive- or filesystem-specific executable bits.
73
+ Run it on a delivery copy and export source inputs only. CMake caches absolute
74
+ source, toolchain, and Python paths under `build/`; `managed_components/` may be
75
+ regenerated from the committed `dependencies.lock`. The bundled
76
+ `third_party/tirtc` SDK and its build contract must remain in the source package.
77
+ CMake must invoke shell gates through `bash <script>` so the build does not
78
+ depend on archive- or filesystem-specific executable bits.
73
79
 
74
80
  The export check also requires Hardware IR, every requested-feature semantic
75
81
  contract, `sdkconfig.defaults`, a referenced custom partition table, and each
@@ -0,0 +1,83 @@
1
+ # Firmware identity and delivery
2
+
3
+ Read this reference whenever the task builds, flashes, hands off, compares, or
4
+ retains firmware. Select the delivery mode from the user's immediate goal; do
5
+ not force a release bundle onto a quick device-test loop.
6
+
7
+ ## Choose one mode explicitly
8
+
9
+ ### Development flash
10
+
11
+ Use this mode for rapid build/flash/monitor iteration on one development
12
+ machine. Keep the normal ESP-IDF `build/` tree and use the ordinary command:
13
+
14
+ ```bash
15
+ idf.py -p <exact-port> flash monitor
16
+ ```
17
+
18
+ Do not create a new archive or make the user manually flash bootloader,
19
+ partition-table and application BIN files when `idf.py flash` can resolve the
20
+ build metadata. Before flashing, record the application version and hash of the
21
+ exact application BIN. A build assessment may bind to a project-relative file
22
+ under `build/`; that evidence remains valid only while that exact file exists.
23
+
24
+ If an experiment produces runtime evidence worth retaining, copy the unchanged
25
+ BIN and matching ELF into `artifacts/` before the next rebuild, record their
26
+ sizes and hashes, and update the report. Copying does not create a new firmware
27
+ identity; a rebuild does.
28
+
29
+ ### Evidence bundle
30
+
31
+ Use this mode for a portable handoff, release candidate, rollback image,
32
+ retained HIL result, or source export. Keep project-relative copies of the exact
33
+ application BIN and ELF, plus bootloader and partition-table files when the
34
+ recipient needs manual flashing. Record byte sizes and SHA-256 values in
35
+ Hardware IR, run strict build assessment, then run
36
+ `project_portability.py --export` on the source-only delivery tree.
37
+
38
+ When promoting a development build into an evidence bundle, copy the unchanged
39
+ files first, then replace every retained `build/...` artifact path in Hardware
40
+ IR and the report with its new `artifacts/...` path. Remove transient build
41
+ records that are not being retained; do not merely add bundle records beside
42
+ stale paths. Rerun strict build assessment against the copied application-BIN
43
+ hash so the assessor reopens the new path. The source-only export is ready only
44
+ when every `build_evidence.artifacts[]` path exists outside `build/` and the
45
+ matching report identifies the same hashes.
46
+
47
+ Only this export workflow requires removal of the machine-bound `build/` tree.
48
+ Do not delete it during active iteration, and never delete a user-owned build
49
+ tree merely to make a report look portable.
50
+
51
+ ## Make firmware self-identifying
52
+
53
+ Set an explicit, human-readable `PROJECT_VER` before `project()` in the top-level
54
+ ESP-IDF `CMakeLists.txt`. Keep it at 31 UTF-8 bytes or fewer so it fits the
55
+ `esp_app_desc_t.version` field with its terminator. Use a label that states what
56
+ the image is: for example `audio-repro.2`, `button-test.1`, `fix.3`, or a release
57
+ version. A build that deliberately preserves a known bug is a reproduction or
58
+ diagnostic image, not a fix.
59
+
60
+ At boot, log `project_name`, `version`, build date/time, IDF version, and the
61
+ full ELF SHA-256 from `esp_app_get_description()` or
62
+ `esp_app_get_elf_sha256()`. If the product has a console, expose the same fields
63
+ through `version` or `status`. This lets a tester distinguish a successful
64
+ flash of the wrong image from a runtime regression.
65
+
66
+ Verify the built application BIN before flash or handoff:
67
+
68
+ ```bash
69
+ python3 <skill-dir>/scripts/firmware_identity.py build/<app>.bin \
70
+ --elf build/<app>.elf --expect-version <expected-version>
71
+ ```
72
+
73
+ The script reads the embedded ESP-IDF application descriptor, verifies the
74
+ explicit version, and confirms that the supplied ELF's full SHA-256 matches the
75
+ descriptor. It accepts an application BIN, not a merged whole-flash image.
76
+
77
+ ## Name hashes precisely
78
+
79
+ The outer application-BIN SHA-256 and the descriptor's full ELF SHA-256 identify
80
+ different files. Record both with unambiguous labels; do not call either one
81
+ simply “firmware hash.” Bind Hardware IR artifacts and HIL observations to the
82
+ exact retained artifact SHA required by the assessor, and include the boot-log
83
+ version/full ELF hash so the physical device can be reconciled with that record.
@@ -62,9 +62,37 @@ Disable Wi-Fi power saving for the realtime baseline unless the product contract
62
62
 
63
63
  Confirm SDK send return semantics and callback payload lifetimes from the selected SDK version. Keep SDK callbacks bounded and copy payloads before returning.
64
64
 
65
+ ## Repeated or duplicated downlink audio
66
+
67
+ Do not label a build as a fix merely because it adds logs or makes the symptom
68
+ less frequent. First produce a self-identifying reproduction build, then count
69
+ the same frame across four boundaries: TiRTC receive callback, accepted queue
70
+ item, playback dequeue, and codec/I2S write. Include mode and connection
71
+ generation, stream/media type, payload length, bounded queue depth/drop counters,
72
+ and an available transport sequence or timestamp. Sample payload hashes only as
73
+ diagnostic evidence; do not blindly discard equal hashes because silence or
74
+ legitimately repeated encoded frames may be identical.
75
+
76
+ Carry one local trace ID from the accepted callback through queue and playback,
77
+ and record write offset, requested bytes, returned bytes, and cumulative bytes.
78
+ Multiple codec/I2S writes are valid when a frame is deliberately chunked or a
79
+ partial write advances the offset; a local duplication exists only when the
80
+ same byte/sample range is committed more than once or cumulative output exceeds
81
+ the frame's decoded contract. Repeated callbacks with the same available
82
+ transport identity point upstream or into SDK delivery. Unique downlink frames
83
+ heard repeatedly require checking resampling, DMA/I2S replay and acoustic
84
+ feedback separately. On stop/reconnect, reject stale generations and drain or
85
+ invalidate queued audio. Verify the correction with repeated AI sessions, rapid
86
+ stop/start, delayed callbacks and H5 recovery against the exact reproduction and
87
+ candidate-fix firmware identities.
88
+
65
89
  ## Artifact discipline
66
90
 
67
- Record BIN/ELF SHA-256 for every build used in HIL. Runtime evidence applies only to the exact artifact. Diagnose one failing invariant, make the smallest correction, rerun that layer, and preserve the comparison in the report.
91
+ Follow [firmware-delivery.md](firmware-delivery.md). Record the explicit firmware
92
+ version, application-BIN SHA-256 and descriptor/full ELF SHA-256 for every build
93
+ used in HIL. Runtime evidence applies only to the exact artifact. Diagnose one
94
+ failing invariant, make the smallest correction, rerun that layer, and preserve
95
+ the comparison in the report.
68
96
 
69
97
  Local intermediate snapshots may be ignored, but the Hardware IR, semantic
70
98
  contracts, `dependencies.lock`, custom partition input, and the artifact named
@@ -0,0 +1,76 @@
1
+ # Product controls, reset, and power
2
+
3
+ Read this reference when the product has buttons, touch inputs, reset controls,
4
+ power keys, wake sources, or enclosure labels. Concrete pins and electrical
5
+ behavior belong in the exact board/carrier evidence and adapter, never in this
6
+ Skill.
7
+
8
+ ## Identify the physical product, not just the baseboard
9
+
10
+ A retail or battery-powered device may combine a compute board with a carrier,
11
+ PMIC, latch circuit, flex PCB, or enclosure controls that are absent from the
12
+ baseboard schematic. When a user's physical observation conflicts with the
13
+ available schematic, treat it as evidence of a missing or different variant.
14
+ Do not declare a working control nonexistent. Request or inspect the exact
15
+ carrier schematic, PCB markings, photographs, BSP definitions, or safe probe
16
+ results and keep the unresolved mapping explicit.
17
+
18
+ Classify each physical label before assigning software behavior:
19
+
20
+ - MCU-readable GPIO button or touch input;
21
+ - boot-strapping input;
22
+ - reset/enable line;
23
+ - PMIC or hardware power-latch control;
24
+ - I/O-expander input;
25
+ - indicator with no input function;
26
+ - unknown control on a missing board layer.
27
+
28
+ For an MCU-readable input, record the SoC GPIO, active level, external pull,
29
+ debounce requirement, shared owner, wake capability, and any strapping role.
30
+ Holding a strapping pin during reset can select a boot mode. A reset/enable line
31
+ normally resets the MCU and cannot also be treated as an application button.
32
+ A PMIC/latch power key and an MCU “enter deep sleep” action are different
33
+ product contracts; do not substitute one for the other without circuit evidence.
34
+
35
+ ## Keep controls behind a narrow adapter
36
+
37
+ Use a board/product-controls component to own GPIO setup, polling or ISR work,
38
+ debounce, gestures, and boot-held suppression. It emits bounded product intents
39
+ such as `AI_TOGGLE_REQUESTED`; it does not call TiRTC, HTTP, MQTT, media, or
40
+ session lifecycle APIs from an ISR or polling callback.
41
+
42
+ Prefer one `AI_TOGGLE_REQUESTED` event whose start/stop decision is serialized by
43
+ the state-owning runtime. If the existing runtime exposes only separate start
44
+ and stop intents, the adapter may map a public state snapshot to one of them,
45
+ but the runtime must validate the intent again against its authoritative state.
46
+ Define whether rapid gestures are queued, coalesced, or rejected so two reads of
47
+ the same stale snapshot cannot silently violate the product behavior.
48
+
49
+ The runtime remains responsible for session generation, callback ordering,
50
+ media ownership, timeouts, cleanup, and late-event rejection. A toggle must
51
+ define behavior for waiting, connecting, active, stopping, H5-owned, and
52
+ error/recovery states instead of maintaining a second Boolean inside the button
53
+ driver.
54
+
55
+ ## Deterministic button behavior
56
+
57
+ For a mechanical active-low or active-high input, require all of the following:
58
+
59
+ - a stable active interval before one press event;
60
+ - no repeat event until a stable release;
61
+ - a button already held during boot must be released before it can trigger;
62
+ - short/long-press thresholds have an explicit product meaning;
63
+ - queue overflow or rejected intents are logged and recover safely;
64
+ - polling tasks and ISRs stay bounded and never block on network/session work.
65
+
66
+ Test boot-held, bounce, rapid repeated presses, press during connection, press
67
+ during active audio, press during stop, delayed SDK callbacks, and recovery to
68
+ H5 or waiting state. For a battery product, separately test cold power-on,
69
+ software shutdown/deep sleep, wake, charging/USB behavior, and reset; a passing
70
+ AI toggle does not prove the power path.
71
+
72
+ Keep debounce and gesture state transitions separable from GPIO/RTOS plumbing
73
+ so host tests can prove boot-held suppression, one event per stable press,
74
+ release re-arming, and the selected rapid-gesture policy. Add a focused runtime
75
+ test for stale or duplicate control intents whenever the runtime performs the
76
+ authoritative state validation.
@@ -16,13 +16,17 @@ Use [the report template](../assets/report-template.md) and preserve separate `P
16
16
  | L6 Sessions | AI, device-call and WeChat VoIP requested flows work; simultaneous capture/playback, AEC/double-talk, stop, timeout and H5 recovery are observed separately |
17
17
  | L7 Stability | Requested weak-network, repeated-session, resource, and soak criteria pass |
18
18
 
19
- Run and record the intake assessment before L0, the build assessment with `--project` and the exact artifact SHA-256 at L1, and the HIL assessment only when matching runtime evidence exists. Before assessment, copy final deliverable BIN/ELF files to project-relative `artifacts/` paths and record their actual byte size and SHA-256 in `build_evidence.artifacts[]`; the assessor reopens the file and rejects stale metadata. Missing serial or browser access is a `SKIP` for the affected L2-L7 levels, not an L0/L1 failure.
19
+ Run and record the intake assessment before L0, the build assessment with `--project` and the exact artifact SHA-256 at L1, and the HIL assessment only when matching runtime evidence exists. Follow [firmware-delivery.md](firmware-delivery.md) for artifact location, retention, and mode-transition rules. Record actual byte size and SHA-256 in `build_evidence.artifacts[]`; the assessor reopens the file and rejects stale metadata. Missing serial or browser access is a `SKIP` for the affected L2-L7 levels, not an L0/L1 failure.
20
20
 
21
- Keep `COMPILE_PASS` separate from `BUILD_VERIFIED`. When the compiler succeeds but a required runtime, audio, or video semantic gate fails or is missing, record the compiler result and report the feature and project as blocked. After build assessment, remove `build/` without rebuilding and run `project_portability.py --export`; the source deliverable may retain verified `artifacts/` copies but not a machine-bound build tree. H5 image display is L5 evidence, never an inference from L1.
21
+ Keep `COMPILE_PASS` separate from `BUILD_VERIFIED`. When the compiler succeeds but a required runtime, audio, or video semantic gate fails or is missing, record the compiler result and report the feature and project as blocked. H5 image display is L5 evidence, never an inference from L1.
22
22
 
23
23
  ## Evidence
24
24
 
25
- Record exact board revision, selected media, Wi-Fi, and binding profiles, toolchain and SDK versions, source/adapter revisions, commands, return codes, firmware size and SHA-256, serial port/chip, sanitized log paths, browser or platform observations, and every unavailable dependency.
25
+ Record exact board revision, selected media, Wi-Fi, and binding profiles,
26
+ toolchain and SDK versions, source/adapter revisions, commands, return codes,
27
+ firmware version, application-BIN SHA-256, descriptor/full ELF SHA-256, serial
28
+ port/chip, sanitized log paths, browser or platform observations, and every
29
+ unavailable dependency. Never conflate the BIN and ELF hashes.
26
30
 
27
31
  Runtime evidence belongs to one artifact. Label superseded artifacts and keep their observations as history; do not promote them to the current BIN/ELF. For HIL assessment, add the full SHA-256 to `runtime_evidence` and run `hardware_ir.py assess --phase hil --artifact-sha256 <sha> --strict`.
28
32
 
@@ -30,6 +30,9 @@ Use this branch when the user supplies a board model, vendor URL, schematic, BOM
30
30
  2. Freeze the user-supplied product contract: selected video profile, audio/stream formats, duplex/AEC policy, supported Wi-Fi credential methods, selected onboarding method, transport staging, output path, and mutation boundary. Use `unknown` where the prompt lacks an answer.
31
31
  3. Prefer official schematic/BOM and BSP facts. For a PDF schematic, inspect page labels and net names; prefer an exported netlist, pin CSV, or vendor board definition when available.
32
32
  4. Cross-check critical pins, clocks, power enables, reset lines, sensor/codec variants, ESP-IDF version, resource ownership, and onboarding behavior across at least two independent artifacts when possible.
33
+ For buttons, power keys and enclosure labels, identify the full physical
34
+ product including carrier/PMIC layers; a working control missing from the
35
+ baseboard schematic is a variant or evidence gap, not proof that it is absent.
33
36
  5. Create the Hardware IR v2. Use `null` for unknown facts and retain contradictory values as an explicit issue instead of selecting one silently. Store concrete board values in the IR/adapter rather than Skill files.
34
37
  6. Validate and run the intake assessment. Classify unresolved facts by their next evidence source: source, implementation, build, HIL, or user input. Ask only for `user_blocked` facts that prevent a safe design. SoftAP is optional when another evidenced Wi-Fi credential method is available, keeps credentials outside source, and defines reprovisioning.
35
38
  7. When hardware identity, wiring, product contracts, and an evidenced resource plan reach `READY_TO_PORT`, generate the starter and implement the board adapter. Generate a compile-safe adapter by default when remaining uncertainty is implementation-, build-, or HIL-resolvable. Stop at the IR/report only when missing user evidence or an incompatible dependency makes a safe implementation impossible.
@@ -79,6 +82,14 @@ connection handles, pending calls, monotonic deadlines, session generation, and
79
82
  H5/AI/CALL/VOIP sequencing. SDK lifecycle changes such as disconnect run in a
80
83
  worker/state-machine context, never directly inside an SDK callback.
81
84
 
85
+ A separate product-controls adapter owns physical input setup, debounce,
86
+ boot-held suppression and gestures. It queues bounded intents into the stable
87
+ runtime. It never manipulates TiRTC/session state directly or keeps a competing
88
+ session Boolean. Prefer an atomic runtime-owned toggle intent; when only explicit
89
+ start/stop intents exist, follow
90
+ [product-controls.md](product-controls.md) for snapshot mapping and authoritative
91
+ runtime revalidation.
92
+
82
93
  ## Verification loop
83
94
 
84
95
  Use a bounded loop per layer: diagnose one failing invariant, make the smallest correction, and rerun that layer before moving forward. Change one high-risk variable per HIL comparison. Turn reusable invariants into tests or post-link gates. Stop and report when the remaining failure requires unavailable hardware, credentials, a new SDK binary, a public protocol change, or a user choice.
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """Inspect the ESP-IDF application descriptor in an application BIN."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import struct
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ ESP_IMAGE_MAGIC = 0xE9
16
+ ESP_APP_DESC_MAGIC = 0xABCD5432
17
+ IMAGE_HEADER_SIZE = 24
18
+ SEGMENT_HEADER_SIZE = 8
19
+ APP_DESC_OFFSET = IMAGE_HEADER_SIZE + SEGMENT_HEADER_SIZE
20
+ APP_DESC_REQUIRED_SIZE = 176
21
+
22
+
23
+ def sha256_file(path: Path) -> str:
24
+ digest = hashlib.sha256()
25
+ with path.open("rb") as stream:
26
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
27
+ digest.update(chunk)
28
+ return digest.hexdigest()
29
+
30
+
31
+ def descriptor_string(data: bytes, offset: int, size: int, label: str) -> str:
32
+ raw = data[offset : offset + size]
33
+ value = raw.split(b"\0", 1)[0]
34
+ try:
35
+ decoded = value.decode("utf-8")
36
+ except UnicodeDecodeError as exc:
37
+ raise ValueError(f"application descriptor {label} is not UTF-8") from exc
38
+ if any(ord(character) < 0x20 or ord(character) == 0x7F for character in decoded):
39
+ raise ValueError(f"application descriptor {label} contains control bytes")
40
+ return decoded
41
+
42
+
43
+ def inspect_firmware(
44
+ firmware_path: Path,
45
+ *,
46
+ elf_path: Path | None = None,
47
+ expected_version: str | None = None,
48
+ ) -> dict[str, Any]:
49
+ firmware = firmware_path.expanduser().resolve()
50
+ try:
51
+ data = firmware.read_bytes()
52
+ except OSError as exc:
53
+ raise ValueError(f"cannot read application BIN {firmware}: {exc}") from exc
54
+
55
+ minimum_size = APP_DESC_OFFSET + APP_DESC_REQUIRED_SIZE
56
+ if len(data) < minimum_size:
57
+ raise ValueError(
58
+ f"application BIN is truncated: need at least {minimum_size} bytes"
59
+ )
60
+ if data[0] != ESP_IMAGE_MAGIC:
61
+ raise ValueError(
62
+ f"not an ESP application image: expected magic 0x{ESP_IMAGE_MAGIC:02x}"
63
+ )
64
+
65
+ segment_length = struct.unpack_from("<I", data, IMAGE_HEADER_SIZE + 4)[0]
66
+ if segment_length < APP_DESC_REQUIRED_SIZE:
67
+ raise ValueError("first ESP image segment is too small for esp_app_desc_t")
68
+
69
+ descriptor = data[APP_DESC_OFFSET : APP_DESC_OFFSET + APP_DESC_REQUIRED_SIZE]
70
+ descriptor_magic = struct.unpack_from("<I", descriptor, 0)[0]
71
+ if descriptor_magic != ESP_APP_DESC_MAGIC:
72
+ raise ValueError(
73
+ "first ESP image segment does not start with a valid esp_app_desc_t"
74
+ )
75
+
76
+ version = descriptor_string(descriptor, 16, 32, "version")
77
+ if not version:
78
+ raise ValueError("application descriptor version is empty")
79
+ if expected_version is not None and version != expected_version:
80
+ raise ValueError(
81
+ f"firmware version mismatch: expected {expected_version!r}, got {version!r}"
82
+ )
83
+
84
+ elf_digest = descriptor[144:176].hex()
85
+ result: dict[str, Any] = {
86
+ "firmware_path": str(firmware),
87
+ "size_bytes": len(data),
88
+ "bin_sha256": hashlib.sha256(data).hexdigest(),
89
+ "segment_count": data[1],
90
+ "project_name": descriptor_string(descriptor, 48, 32, "project_name"),
91
+ "version": version,
92
+ "build_time": descriptor_string(descriptor, 80, 16, "time"),
93
+ "build_date": descriptor_string(descriptor, 96, 16, "date"),
94
+ "idf_version": descriptor_string(descriptor, 112, 32, "idf_ver"),
95
+ "app_elf_sha256": elf_digest,
96
+ }
97
+
98
+ if elf_path is not None:
99
+ elf = elf_path.expanduser().resolve()
100
+ try:
101
+ elf_size = elf.stat().st_size
102
+ actual_elf_digest = sha256_file(elf)
103
+ except OSError as exc:
104
+ raise ValueError(f"cannot read ELF {elf}: {exc}") from exc
105
+ if actual_elf_digest != elf_digest:
106
+ raise ValueError(
107
+ "ELF SHA-256 mismatch: application descriptor records "
108
+ f"{elf_digest}, file is {actual_elf_digest}"
109
+ )
110
+ result.update(
111
+ {
112
+ "elf_path": str(elf),
113
+ "elf_size_bytes": elf_size,
114
+ "elf_sha256": actual_elf_digest,
115
+ }
116
+ )
117
+
118
+ return result
119
+
120
+
121
+ def print_human(result: dict[str, Any]) -> None:
122
+ fields = (
123
+ ("firmware", "firmware_path"),
124
+ ("project", "project_name"),
125
+ ("version", "version"),
126
+ ("build", "build_date"),
127
+ ("time", "build_time"),
128
+ ("idf", "idf_version"),
129
+ ("size_bytes", "size_bytes"),
130
+ ("bin_sha256", "bin_sha256"),
131
+ ("app_elf_sha256", "app_elf_sha256"),
132
+ ("elf", "elf_path"),
133
+ ("elf_size_bytes", "elf_size_bytes"),
134
+ ("elf_sha256", "elf_sha256"),
135
+ )
136
+ for label, key in fields:
137
+ if key in result:
138
+ print(f"{label}: {result[key]}")
139
+
140
+
141
+ def build_parser() -> argparse.ArgumentParser:
142
+ parser = argparse.ArgumentParser(
143
+ description=(
144
+ "inspect version and SHA-256 identity from an ESP-IDF application BIN"
145
+ )
146
+ )
147
+ parser.add_argument("firmware", type=Path, help="ESP-IDF application BIN")
148
+ parser.add_argument(
149
+ "--elf",
150
+ type=Path,
151
+ help="matching ELF; its SHA-256 must equal app_elf_sha256 in the BIN",
152
+ )
153
+ parser.add_argument(
154
+ "--expect-version",
155
+ help="fail unless the application descriptor contains this exact version",
156
+ )
157
+ parser.add_argument("--json", action="store_true", help="emit JSON")
158
+ return parser
159
+
160
+
161
+ def main(argv: list[str] | None = None) -> int:
162
+ args = build_parser().parse_args(argv)
163
+ try:
164
+ result = inspect_firmware(
165
+ args.firmware,
166
+ elf_path=args.elf,
167
+ expected_version=args.expect_version,
168
+ )
169
+ except ValueError as exc:
170
+ print(f"error: {exc}", file=sys.stderr)
171
+ return 2
172
+ if args.json:
173
+ print(json.dumps(result, indent=2, sort_keys=True))
174
+ else:
175
+ print_human(result)
176
+ return 0
177
+
178
+
179
+ if __name__ == "__main__":
180
+ raise SystemExit(main())