tirtc-device-builder 0.8.0 → 0.9.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.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +22 -0
- package/README.md +68 -18
- package/bin/agent-clients.js +141 -0
- package/bin/setup-esp32.js +43 -17
- package/bin/tirtc-device-builder.js +42 -17
- package/package.json +6 -2
- package/skills/tirtc-esp32-builder/SKILL.md +19 -8
- package/skills/tirtc-esp32-builder/USAGE.md +22 -7
- package/skills/tirtc-esp32-builder/VERSION +1 -1
- package/skills/tirtc-esp32-builder/assets/developer-intake-prompt.md +1 -1
- package/skills/tirtc-esp32-builder/assets/lckfb-szpi-esp32s3-portable-prompt.md +10 -8
- package/skills/tirtc-esp32-builder/assets/report-template.md +4 -1
- package/skills/tirtc-esp32-builder/references/environment.md +9 -3
- package/skills/tirtc-esp32-builder/references/firmware-delivery.md +83 -0
- package/skills/tirtc-esp32-builder/references/porting-risks.md +29 -1
- package/skills/tirtc-esp32-builder/references/product-controls.md +76 -0
- package/skills/tirtc-esp32-builder/references/reporting.md +7 -3
- package/skills/tirtc-esp32-builder/references/workflow.md +11 -0
- package/skills/tirtc-esp32-builder/scripts/firmware_identity.py +180 -0
|
@@ -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())
|