tirtc-device-builder 0.2.0 → 0.4.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 +19 -0
- package/README.md +657 -362
- package/bin/esp32-kit-metadata.js +8 -0
- package/bin/install-esp32-kit.js +208 -0
- package/bin/setup-esp32.js +796 -0
- package/bin/tirtc-device-builder.js +19 -2
- package/package.json +2 -1
- package/skills/tirtc-esp32-builder/SKILL.md +11 -8
- package/skills/tirtc-esp32-builder/USAGE.md +30 -22
- package/skills/tirtc-esp32-builder/assets/developer-intake-prompt.md +50 -0
- package/skills/tirtc-esp32-builder/assets/hardware-ir-v2.example.json +122 -0
- package/skills/tirtc-esp32-builder/assets/report-template.md +15 -0
- package/skills/tirtc-esp32-builder/references/capability-rules.md +41 -16
- package/skills/tirtc-esp32-builder/references/environment.md +25 -4
- package/skills/tirtc-esp32-builder/references/hardware-ir.md +44 -24
- package/skills/tirtc-esp32-builder/references/porting-risks.md +49 -0
- package/skills/tirtc-esp32-builder/references/reporting.md +5 -1
- package/skills/tirtc-esp32-builder/references/workflow.md +10 -9
- package/skills/tirtc-esp32-builder/scripts/doctor.py +4 -4
- package/skills/tirtc-esp32-builder/scripts/hardware_ir.py +670 -40
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
|
+
import re
|
|
8
9
|
import shutil
|
|
9
10
|
import sys
|
|
10
11
|
from pathlib import Path
|
|
@@ -25,6 +26,34 @@ VERIFICATION_LEVELS = {
|
|
|
25
26
|
"hil_verified": 5,
|
|
26
27
|
}
|
|
27
28
|
READY_STATUSES = {"READY_TO_PORT", "HIL_VERIFIED"}
|
|
29
|
+
VIDEO_CONTRACTS = {
|
|
30
|
+
"mjpeg": "jpeg_complete_frames",
|
|
31
|
+
"h264": "h264_annex_b_access_units",
|
|
32
|
+
"h265": "h265_annex_b_access_units",
|
|
33
|
+
}
|
|
34
|
+
WIFI_METHOD_TYPES = {
|
|
35
|
+
"softap",
|
|
36
|
+
"ble",
|
|
37
|
+
"smartconfig",
|
|
38
|
+
"factory_nvs",
|
|
39
|
+
"development_config",
|
|
40
|
+
"custom",
|
|
41
|
+
}
|
|
42
|
+
BINDING_METHOD_TYPES = {
|
|
43
|
+
"verification_code",
|
|
44
|
+
"factory_bound",
|
|
45
|
+
"development_credentials",
|
|
46
|
+
"custom",
|
|
47
|
+
}
|
|
48
|
+
ACCEPTANCE_LEVELS = {"L-1", "L0", "L1", "L2", "L3", "L4", "L5", "L6", "L7"}
|
|
49
|
+
FEATURE_HIL_LEVEL = {
|
|
50
|
+
"h5_live_audio": "L5",
|
|
51
|
+
"h5_live_video": "L5",
|
|
52
|
+
"h5_talkback": "L5",
|
|
53
|
+
"ai_talk": "L6",
|
|
54
|
+
}
|
|
55
|
+
SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
|
|
56
|
+
Requirement = tuple[str, str, int]
|
|
28
57
|
|
|
29
58
|
|
|
30
59
|
def load_ir(path: Path) -> dict[str, Any]:
|
|
@@ -51,17 +80,32 @@ def nonempty_string(value: Any, path: str, errors: list[str]) -> None:
|
|
|
51
80
|
errors.append(f"{path} must be a non-empty string")
|
|
52
81
|
|
|
53
82
|
|
|
83
|
+
def nullable_string(value: Any, path: str, errors: list[str]) -> None:
|
|
84
|
+
if value is not None and (not isinstance(value, str) or not value.strip()):
|
|
85
|
+
errors.append(f"{path} must be a non-empty string or null")
|
|
86
|
+
|
|
87
|
+
|
|
54
88
|
def positive_int(value: Any, path: str, errors: list[str], allow_zero: bool = False) -> None:
|
|
55
89
|
minimum = 0 if allow_zero else 1
|
|
56
90
|
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
|
57
91
|
errors.append(f"{path} must be an integer >= {minimum}")
|
|
58
92
|
|
|
59
93
|
|
|
94
|
+
def nullable_positive_int(value: Any, path: str, errors: list[str]) -> None:
|
|
95
|
+
if value is not None:
|
|
96
|
+
positive_int(value, path, errors)
|
|
97
|
+
|
|
98
|
+
|
|
60
99
|
def nullable_bool(value: Any, path: str, errors: list[str]) -> None:
|
|
61
100
|
if value is not None and not isinstance(value, bool):
|
|
62
101
|
errors.append(f"{path} must be true, false, or null")
|
|
63
102
|
|
|
64
103
|
|
|
104
|
+
def validate_verification(value: Any, path: str, errors: list[str]) -> None:
|
|
105
|
+
if value not in VERIFICATION_LEVELS:
|
|
106
|
+
errors.append(f"{path} must be one of " + ", ".join(VERIFICATION_LEVELS))
|
|
107
|
+
|
|
108
|
+
|
|
65
109
|
def validate_source_refs(
|
|
66
110
|
section: dict[str, Any], path: str, source_ids: set[str], errors: list[str]
|
|
67
111
|
) -> None:
|
|
@@ -90,18 +134,268 @@ def validate_codec_list(value: Any, path: str, errors: list[str]) -> None:
|
|
|
90
134
|
else:
|
|
91
135
|
for rate_index, rate in enumerate(rates):
|
|
92
136
|
positive_int(rate, f"{prefix}.sample_rates_hz[{rate_index}]", errors)
|
|
93
|
-
|
|
94
|
-
|
|
137
|
+
validate_verification(codec.get("verification"), f"{prefix}.verification", errors)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def validate_video_profiles(
|
|
141
|
+
camera: dict[str, Any], source_ids: set[str], errors: list[str]
|
|
142
|
+
) -> None:
|
|
143
|
+
nullable_string(
|
|
144
|
+
camera.get("selected_video_profile"),
|
|
145
|
+
"camera.selected_video_profile",
|
|
146
|
+
errors,
|
|
147
|
+
)
|
|
148
|
+
profiles = camera.get("video_profiles")
|
|
149
|
+
if not isinstance(profiles, list):
|
|
150
|
+
errors.append("camera.video_profiles must be an array")
|
|
151
|
+
return
|
|
152
|
+
ids: set[str] = set()
|
|
153
|
+
for index, item in enumerate(profiles):
|
|
154
|
+
prefix = f"camera.video_profiles[{index}]"
|
|
155
|
+
profile = mapping(item, prefix, errors)
|
|
156
|
+
nonempty_string(profile.get("id"), f"{prefix}.id", errors)
|
|
157
|
+
profile_id = profile.get("id")
|
|
158
|
+
if isinstance(profile_id, str) and profile_id:
|
|
159
|
+
if profile_id in ids:
|
|
160
|
+
errors.append(f"duplicate video profile id {profile_id!r}")
|
|
161
|
+
ids.add(profile_id)
|
|
162
|
+
codec = profile.get("codec")
|
|
163
|
+
if codec not in VIDEO_CONTRACTS:
|
|
95
164
|
errors.append(
|
|
96
|
-
f"{prefix}.
|
|
97
|
-
+ ", ".join(VERIFICATION_LEVELS)
|
|
165
|
+
f"{prefix}.codec must be one of " + ", ".join(VIDEO_CONTRACTS)
|
|
98
166
|
)
|
|
167
|
+
nullable_bool(profile.get("available"), f"{prefix}.available", errors)
|
|
168
|
+
nullable_string(profile.get("output_format"), f"{prefix}.output_format", errors)
|
|
169
|
+
nullable_bool(
|
|
170
|
+
profile.get("refresh_frame_control"),
|
|
171
|
+
f"{prefix}.refresh_frame_control",
|
|
172
|
+
errors,
|
|
173
|
+
)
|
|
174
|
+
nullable_positive_int(profile.get("stream_id"), f"{prefix}.stream_id", errors)
|
|
175
|
+
validate_verification(
|
|
176
|
+
profile.get("verification"), f"{prefix}.verification", errors
|
|
177
|
+
)
|
|
178
|
+
validate_source_refs(profile, prefix, source_ids, errors)
|
|
179
|
+
selected = camera.get("selected_video_profile")
|
|
180
|
+
if isinstance(selected, str) and selected and selected not in ids:
|
|
181
|
+
errors.append(
|
|
182
|
+
f"camera.selected_video_profile references unknown profile {selected!r}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def validate_hardware_resources(
|
|
187
|
+
data: dict[str, Any], source_ids: set[str], errors: list[str]
|
|
188
|
+
) -> None:
|
|
189
|
+
resources = mapping(data.get("hardware_resources"), "hardware_resources", errors)
|
|
190
|
+
validate_source_refs(resources, "hardware_resources", source_ids, errors)
|
|
191
|
+
|
|
192
|
+
i2c = mapping(resources.get("i2c"), "hardware_resources.i2c", errors)
|
|
193
|
+
nullable_bool(i2c.get("used"), "hardware_resources.i2c.used", errors)
|
|
194
|
+
driver_family = i2c.get("driver_family")
|
|
195
|
+
if driver_family not in {None, "legacy", "ng", "none"}:
|
|
196
|
+
errors.append(
|
|
197
|
+
"hardware_resources.i2c.driver_family must be legacy, ng, none, or null"
|
|
198
|
+
)
|
|
199
|
+
nullable_bool(
|
|
200
|
+
i2c.get("single_driver_family"),
|
|
201
|
+
"hardware_resources.i2c.single_driver_family",
|
|
202
|
+
errors,
|
|
203
|
+
)
|
|
204
|
+
validate_verification(
|
|
205
|
+
i2c.get("verification"), "hardware_resources.i2c.verification", errors
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
i2s = mapping(resources.get("i2s"), "hardware_resources.i2s", errors)
|
|
209
|
+
nullable_bool(i2s.get("used"), "hardware_resources.i2s.used", errors)
|
|
210
|
+
nullable_bool(
|
|
211
|
+
i2s.get("controller_and_gpio_ownership_resolved"),
|
|
212
|
+
"hardware_resources.i2s.controller_and_gpio_ownership_resolved",
|
|
213
|
+
errors,
|
|
214
|
+
)
|
|
215
|
+
validate_verification(
|
|
216
|
+
i2s.get("verification"), "hardware_resources.i2s.verification", errors
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
mapping_section = mapping(
|
|
220
|
+
resources.get("audio_channel_mapping"),
|
|
221
|
+
"hardware_resources.audio_channel_mapping",
|
|
222
|
+
errors,
|
|
223
|
+
)
|
|
224
|
+
nullable_bool(
|
|
225
|
+
mapping_section.get("required"),
|
|
226
|
+
"hardware_resources.audio_channel_mapping.required",
|
|
227
|
+
errors,
|
|
228
|
+
)
|
|
229
|
+
nullable_bool(
|
|
230
|
+
mapping_section.get("resolved"),
|
|
231
|
+
"hardware_resources.audio_channel_mapping.resolved",
|
|
232
|
+
errors,
|
|
233
|
+
)
|
|
234
|
+
validate_verification(
|
|
235
|
+
mapping_section.get("verification"),
|
|
236
|
+
"hardware_resources.audio_channel_mapping.verification",
|
|
237
|
+
errors,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
realtime = mapping(
|
|
241
|
+
resources.get("camera_realtime"),
|
|
242
|
+
"hardware_resources.camera_realtime",
|
|
243
|
+
errors,
|
|
244
|
+
)
|
|
245
|
+
nullable_bool(
|
|
246
|
+
realtime.get("pipeline_safe"),
|
|
247
|
+
"hardware_resources.camera_realtime.pipeline_safe",
|
|
248
|
+
errors,
|
|
249
|
+
)
|
|
250
|
+
validate_verification(
|
|
251
|
+
realtime.get("verification"),
|
|
252
|
+
"hardware_resources.camera_realtime.verification",
|
|
253
|
+
errors,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
memory = mapping(resources.get("memory"), "hardware_resources.memory", errors)
|
|
257
|
+
nullable_bool(
|
|
258
|
+
memory.get("startup_and_media_budgeted"),
|
|
259
|
+
"hardware_resources.memory.startup_and_media_budgeted",
|
|
260
|
+
errors,
|
|
261
|
+
)
|
|
262
|
+
validate_verification(
|
|
263
|
+
memory.get("verification"), "hardware_resources.memory.verification", errors
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def validate_onboarding(
|
|
268
|
+
data: dict[str, Any], source_ids: set[str], errors: list[str]
|
|
269
|
+
) -> None:
|
|
270
|
+
onboarding = mapping(data.get("onboarding"), "onboarding", errors)
|
|
271
|
+
validate_source_refs(onboarding, "onboarding", source_ids, errors)
|
|
272
|
+
wifi = mapping(onboarding.get("wifi_credentials"), "onboarding.wifi_credentials", errors)
|
|
273
|
+
nullable_string(
|
|
274
|
+
wifi.get("selected_method"), "onboarding.wifi_credentials.selected_method", errors
|
|
275
|
+
)
|
|
276
|
+
nullable_bool(
|
|
277
|
+
wifi.get("credentials_committed_to_source"),
|
|
278
|
+
"onboarding.wifi_credentials.credentials_committed_to_source",
|
|
279
|
+
errors,
|
|
280
|
+
)
|
|
281
|
+
nullable_bool(
|
|
282
|
+
wifi.get("reprovisioning_defined"),
|
|
283
|
+
"onboarding.wifi_credentials.reprovisioning_defined",
|
|
284
|
+
errors,
|
|
285
|
+
)
|
|
286
|
+
methods = wifi.get("methods")
|
|
287
|
+
method_ids: set[str] = set()
|
|
288
|
+
if not isinstance(methods, list):
|
|
289
|
+
errors.append("onboarding.wifi_credentials.methods must be an array")
|
|
290
|
+
else:
|
|
291
|
+
for index, item in enumerate(methods):
|
|
292
|
+
prefix = f"onboarding.wifi_credentials.methods[{index}]"
|
|
293
|
+
method = mapping(item, prefix, errors)
|
|
294
|
+
nonempty_string(method.get("id"), f"{prefix}.id", errors)
|
|
295
|
+
method_id = method.get("id")
|
|
296
|
+
if isinstance(method_id, str) and method_id:
|
|
297
|
+
if method_id in method_ids:
|
|
298
|
+
errors.append(f"duplicate Wi-Fi method id {method_id!r}")
|
|
299
|
+
method_ids.add(method_id)
|
|
300
|
+
method_type = method.get("type")
|
|
301
|
+
if method_type not in WIFI_METHOD_TYPES:
|
|
302
|
+
errors.append(
|
|
303
|
+
f"{prefix}.type must be one of " + ", ".join(sorted(WIFI_METHOD_TYPES))
|
|
304
|
+
)
|
|
305
|
+
nullable_bool(method.get("available"), f"{prefix}.available", errors)
|
|
306
|
+
validate_verification(
|
|
307
|
+
method.get("verification"), f"{prefix}.verification", errors
|
|
308
|
+
)
|
|
309
|
+
validate_source_refs(method, prefix, source_ids, errors)
|
|
310
|
+
selected = wifi.get("selected_method")
|
|
311
|
+
if isinstance(selected, str) and selected and selected not in method_ids:
|
|
312
|
+
errors.append(
|
|
313
|
+
f"onboarding.wifi_credentials.selected_method references unknown method {selected!r}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
binding = mapping(onboarding.get("device_binding"), "onboarding.device_binding", errors)
|
|
317
|
+
nullable_string(
|
|
318
|
+
binding.get("selected_method"),
|
|
319
|
+
"onboarding.device_binding.selected_method",
|
|
320
|
+
errors,
|
|
321
|
+
)
|
|
322
|
+
for field in (
|
|
323
|
+
"credentials_committed_to_source",
|
|
324
|
+
"stored_credential_state_handled",
|
|
325
|
+
"clear_binding_control",
|
|
326
|
+
):
|
|
327
|
+
nullable_bool(binding.get(field), f"onboarding.device_binding.{field}", errors)
|
|
328
|
+
methods = binding.get("methods")
|
|
329
|
+
method_ids = set()
|
|
330
|
+
if not isinstance(methods, list):
|
|
331
|
+
errors.append("onboarding.device_binding.methods must be an array")
|
|
332
|
+
else:
|
|
333
|
+
for index, item in enumerate(methods):
|
|
334
|
+
prefix = f"onboarding.device_binding.methods[{index}]"
|
|
335
|
+
method = mapping(item, prefix, errors)
|
|
336
|
+
nonempty_string(method.get("id"), f"{prefix}.id", errors)
|
|
337
|
+
method_id = method.get("id")
|
|
338
|
+
if isinstance(method_id, str) and method_id:
|
|
339
|
+
if method_id in method_ids:
|
|
340
|
+
errors.append(f"duplicate binding method id {method_id!r}")
|
|
341
|
+
method_ids.add(method_id)
|
|
342
|
+
method_type = method.get("type")
|
|
343
|
+
if method_type not in BINDING_METHOD_TYPES:
|
|
344
|
+
errors.append(
|
|
345
|
+
f"{prefix}.type must be one of "
|
|
346
|
+
+ ", ".join(sorted(BINDING_METHOD_TYPES))
|
|
347
|
+
)
|
|
348
|
+
nullable_bool(method.get("available"), f"{prefix}.available", errors)
|
|
349
|
+
validate_verification(
|
|
350
|
+
method.get("verification"), f"{prefix}.verification", errors
|
|
351
|
+
)
|
|
352
|
+
validate_source_refs(method, prefix, source_ids, errors)
|
|
353
|
+
selected = binding.get("selected_method")
|
|
354
|
+
if isinstance(selected, str) and selected and selected not in method_ids:
|
|
355
|
+
errors.append(
|
|
356
|
+
f"onboarding.device_binding.selected_method references unknown method {selected!r}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def validate_runtime_evidence(
|
|
361
|
+
data: dict[str, Any], source_ids: set[str], errors: list[str]
|
|
362
|
+
) -> None:
|
|
363
|
+
evidence = data.get("runtime_evidence")
|
|
364
|
+
if not isinstance(evidence, list):
|
|
365
|
+
errors.append("runtime_evidence must be an array")
|
|
366
|
+
return
|
|
367
|
+
for index, item in enumerate(evidence):
|
|
368
|
+
prefix = f"runtime_evidence[{index}]"
|
|
369
|
+
record = mapping(item, prefix, errors)
|
|
370
|
+
sha = record.get("artifact_sha256")
|
|
371
|
+
if not isinstance(sha, str) or not SHA256_RE.fullmatch(sha):
|
|
372
|
+
errors.append(f"{prefix}.artifact_sha256 must be a 64-character SHA-256")
|
|
373
|
+
levels = record.get("acceptance_levels")
|
|
374
|
+
if not isinstance(levels, list) or not levels:
|
|
375
|
+
errors.append(f"{prefix}.acceptance_levels must be a non-empty array")
|
|
376
|
+
else:
|
|
377
|
+
for level_index, level in enumerate(levels):
|
|
378
|
+
if level not in ACCEPTANCE_LEVELS:
|
|
379
|
+
errors.append(
|
|
380
|
+
f"{prefix}.acceptance_levels[{level_index}] must be a known acceptance level"
|
|
381
|
+
)
|
|
382
|
+
features = record.get("features")
|
|
383
|
+
if not isinstance(features, list) or not features:
|
|
384
|
+
errors.append(f"{prefix}.features must be a non-empty array")
|
|
385
|
+
else:
|
|
386
|
+
for feature_index, feature in enumerate(features):
|
|
387
|
+
if feature not in FEATURES:
|
|
388
|
+
errors.append(
|
|
389
|
+
f"{prefix}.features[{feature_index}] must be a known feature"
|
|
390
|
+
)
|
|
391
|
+
validate_source_refs(record, prefix, source_ids, errors)
|
|
99
392
|
|
|
100
393
|
|
|
101
394
|
def validate_ir(data: dict[str, Any]) -> list[str]:
|
|
102
395
|
errors: list[str] = []
|
|
103
|
-
|
|
104
|
-
|
|
396
|
+
schema_version = data.get("schema_version")
|
|
397
|
+
if schema_version not in {1, 2}:
|
|
398
|
+
errors.append("schema_version must be 1 or 2")
|
|
105
399
|
|
|
106
400
|
board = mapping(data.get("board"), "board", errors)
|
|
107
401
|
for key in ("id", "vendor", "model", "hardware_revision"):
|
|
@@ -135,12 +429,9 @@ def validate_ir(data: dict[str, Any]) -> list[str]:
|
|
|
135
429
|
nonempty_string(
|
|
136
430
|
toolchain.get("framework_version"), "toolchain.framework_version", errors
|
|
137
431
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"toolchain.verification must be one of "
|
|
142
|
-
+ ", ".join(VERIFICATION_LEVELS)
|
|
143
|
-
)
|
|
432
|
+
validate_verification(
|
|
433
|
+
toolchain.get("verification"), "toolchain.verification", errors
|
|
434
|
+
)
|
|
144
435
|
tirtc = mapping(toolchain.get("tirtc"), "toolchain.tirtc", errors)
|
|
145
436
|
for key in ("platform", "version", "sdk_path", "build_contract"):
|
|
146
437
|
nonempty_string(tirtc.get(key), f"toolchain.tirtc.{key}", errors)
|
|
@@ -148,17 +439,17 @@ def validate_ir(data: dict[str, Any]) -> list[str]:
|
|
|
148
439
|
|
|
149
440
|
camera = mapping(data.get("camera"), "camera", errors)
|
|
150
441
|
nullable_bool(camera.get("present"), "camera.present", errors)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
verification = h264.get("verification")
|
|
157
|
-
if verification not in VERIFICATION_LEVELS:
|
|
158
|
-
errors.append(
|
|
159
|
-
"camera.h264.verification must be one of "
|
|
160
|
-
+ ", ".join(VERIFICATION_LEVELS)
|
|
442
|
+
if schema_version == 1:
|
|
443
|
+
h264 = mapping(camera.get("h264"), "camera.h264", errors)
|
|
444
|
+
nullable_bool(h264.get("available"), "camera.h264.available", errors)
|
|
445
|
+
nullable_bool(
|
|
446
|
+
h264.get("key_frame_control"), "camera.h264.key_frame_control", errors
|
|
161
447
|
)
|
|
448
|
+
validate_verification(
|
|
449
|
+
h264.get("verification"), "camera.h264.verification", errors
|
|
450
|
+
)
|
|
451
|
+
elif schema_version == 2:
|
|
452
|
+
validate_video_profiles(camera, source_ids, errors)
|
|
162
453
|
validate_source_refs(camera, "camera", source_ids, errors)
|
|
163
454
|
|
|
164
455
|
for name in ("audio_input", "audio_output"):
|
|
@@ -183,10 +474,15 @@ def validate_ir(data: dict[str, Any]) -> list[str]:
|
|
|
183
474
|
errors.append(f"features.requested contains duplicate {feature!r}")
|
|
184
475
|
else:
|
|
185
476
|
seen.add(feature)
|
|
477
|
+
|
|
478
|
+
if schema_version == 2:
|
|
479
|
+
validate_hardware_resources(data, source_ids, errors)
|
|
480
|
+
validate_onboarding(data, source_ids, errors)
|
|
481
|
+
validate_runtime_evidence(data, source_ids, errors)
|
|
186
482
|
return errors
|
|
187
483
|
|
|
188
484
|
|
|
189
|
-
def codec_requirement(media: dict[str, Any], section: str) ->
|
|
485
|
+
def codec_requirement(media: dict[str, Any], section: str) -> Requirement:
|
|
190
486
|
present = media.get("present")
|
|
191
487
|
if present is None:
|
|
192
488
|
return "NEEDS_CONFIRMATION", f"{section} presence is unknown", 0
|
|
@@ -210,7 +506,7 @@ def codec_requirement(media: dict[str, Any], section: str) -> tuple[str, str, in
|
|
|
210
506
|
return "BLOCKED", f"{section} has no A-law 8 kHz path", 0
|
|
211
507
|
|
|
212
508
|
|
|
213
|
-
def
|
|
509
|
+
def legacy_video_requirement(camera: dict[str, Any]) -> Requirement:
|
|
214
510
|
present = camera.get("present")
|
|
215
511
|
if present is None:
|
|
216
512
|
return "NEEDS_CONFIRMATION", "camera presence is unknown", 0
|
|
@@ -243,7 +539,219 @@ def video_requirement(camera: dict[str, Any]) -> tuple[str, str, int]:
|
|
|
243
539
|
return "SATISFIED", "camera provides H.264 Annex-B and IDR control", level
|
|
244
540
|
|
|
245
541
|
|
|
246
|
-
def
|
|
542
|
+
def selected_item(items: Any, selected_id: Any) -> dict[str, Any] | None:
|
|
543
|
+
if not isinstance(items, list) or not isinstance(selected_id, str):
|
|
544
|
+
return None
|
|
545
|
+
for item in items:
|
|
546
|
+
if isinstance(item, dict) and item.get("id") == selected_id:
|
|
547
|
+
return item
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def video_requirement_v2(camera: dict[str, Any]) -> Requirement:
|
|
552
|
+
present = camera.get("present")
|
|
553
|
+
if present is None:
|
|
554
|
+
return "NEEDS_CONFIRMATION", "camera presence is unknown", 0
|
|
555
|
+
if present is False:
|
|
556
|
+
return "BLOCKED", "camera is not present", 0
|
|
557
|
+
selected_id = camera.get("selected_video_profile")
|
|
558
|
+
if not isinstance(selected_id, str) or not selected_id:
|
|
559
|
+
return "NEEDS_CONFIRMATION", "selected video profile is unknown", 0
|
|
560
|
+
profile = selected_item(camera.get("video_profiles"), selected_id)
|
|
561
|
+
if profile is None:
|
|
562
|
+
return "BLOCKED", f"selected video profile {selected_id!r} does not exist", 0
|
|
563
|
+
codec = profile.get("codec")
|
|
564
|
+
if codec not in VIDEO_CONTRACTS:
|
|
565
|
+
return "BLOCKED", f"selected video codec {codec!r} is unsupported", 0
|
|
566
|
+
available = profile.get("available")
|
|
567
|
+
if available is None:
|
|
568
|
+
return "NEEDS_CONFIRMATION", f"{codec} path availability is unknown", 0
|
|
569
|
+
if available is False:
|
|
570
|
+
return "BLOCKED", f"selected {codec} path is unavailable", 0
|
|
571
|
+
output_format = profile.get("output_format")
|
|
572
|
+
if output_format is None:
|
|
573
|
+
return "NEEDS_CONFIRMATION", f"{codec} output format is unknown", 0
|
|
574
|
+
expected = VIDEO_CONTRACTS[codec]
|
|
575
|
+
if str(output_format).lower() != expected:
|
|
576
|
+
return (
|
|
577
|
+
"BLOCKED",
|
|
578
|
+
f"selected {codec} profile requires output_format={expected}",
|
|
579
|
+
0,
|
|
580
|
+
)
|
|
581
|
+
refresh = profile.get("refresh_frame_control")
|
|
582
|
+
if refresh is None:
|
|
583
|
+
return "NEEDS_CONFIRMATION", "refresh/key-frame control is unknown", 0
|
|
584
|
+
if refresh is False:
|
|
585
|
+
return "BLOCKED", "H5 refresh requests cannot reach the media pipeline", 0
|
|
586
|
+
verification = profile.get("verification")
|
|
587
|
+
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
588
|
+
if level < VERIFICATION_LEVELS["corroborated"]:
|
|
589
|
+
return (
|
|
590
|
+
"NEEDS_CONFIRMATION",
|
|
591
|
+
f"selected {codec} path is only {verification}",
|
|
592
|
+
level,
|
|
593
|
+
)
|
|
594
|
+
return (
|
|
595
|
+
"SATISFIED",
|
|
596
|
+
f"selected {codec} profile provides {expected} on stream {profile.get('stream_id')}",
|
|
597
|
+
level,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def verified_bool_requirement(
|
|
602
|
+
section: dict[str, Any], field: str, label: str
|
|
603
|
+
) -> Requirement:
|
|
604
|
+
value = section.get(field)
|
|
605
|
+
if value is None:
|
|
606
|
+
return "NEEDS_CONFIRMATION", f"{label} is unknown", 0
|
|
607
|
+
if value is False:
|
|
608
|
+
return "BLOCKED", f"{label} is unresolved", 0
|
|
609
|
+
verification = section.get("verification")
|
|
610
|
+
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
611
|
+
if level < VERIFICATION_LEVELS["corroborated"]:
|
|
612
|
+
return "NEEDS_CONFIRMATION", f"{label} is only {verification}", level
|
|
613
|
+
return "SATISFIED", f"{label} is resolved", level
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def i2c_requirement(resources: dict[str, Any]) -> Requirement:
|
|
617
|
+
i2c = resources.get("i2c", {})
|
|
618
|
+
used = i2c.get("used")
|
|
619
|
+
if used is None:
|
|
620
|
+
return "NEEDS_CONFIRMATION", "I2C usage is unknown", 0
|
|
621
|
+
if used is False:
|
|
622
|
+
return "SATISFIED", "board media path does not use I2C", 2
|
|
623
|
+
family = i2c.get("driver_family")
|
|
624
|
+
if family is None:
|
|
625
|
+
return "NEEDS_CONFIRMATION", "I2C driver family is unknown", 0
|
|
626
|
+
if family == "none":
|
|
627
|
+
return "BLOCKED", "I2C is used but no driver family is selected", 0
|
|
628
|
+
return verified_bool_requirement(
|
|
629
|
+
i2c, "single_driver_family", f"single {family} I2C driver family"
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def i2s_requirement(resources: dict[str, Any]) -> Requirement:
|
|
634
|
+
i2s = resources.get("i2s", {})
|
|
635
|
+
used = i2s.get("used")
|
|
636
|
+
if used is None:
|
|
637
|
+
return "NEEDS_CONFIRMATION", "I2S usage is unknown", 0
|
|
638
|
+
if used is False:
|
|
639
|
+
return "SATISFIED", "audio path does not use I2S", 2
|
|
640
|
+
return verified_bool_requirement(
|
|
641
|
+
i2s,
|
|
642
|
+
"controller_and_gpio_ownership_resolved",
|
|
643
|
+
"I2S controller and GPIO ownership",
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def audio_mapping_requirement(resources: dict[str, Any]) -> Requirement:
|
|
648
|
+
channel = resources.get("audio_channel_mapping", {})
|
|
649
|
+
required = channel.get("required")
|
|
650
|
+
if required is None:
|
|
651
|
+
return "NEEDS_CONFIRMATION", "audio channel/TDM mapping requirement is unknown", 0
|
|
652
|
+
if required is False:
|
|
653
|
+
return "SATISFIED", "audio channel/TDM mapping is not required", 2
|
|
654
|
+
return verified_bool_requirement(
|
|
655
|
+
channel, "resolved", "audio channel/TDM mapping"
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def camera_realtime_requirement(resources: dict[str, Any]) -> Requirement:
|
|
660
|
+
return verified_bool_requirement(
|
|
661
|
+
resources.get("camera_realtime", {}),
|
|
662
|
+
"pipeline_safe",
|
|
663
|
+
"camera DMA/task realtime policy",
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def memory_requirement(resources: dict[str, Any]) -> Requirement:
|
|
668
|
+
return verified_bool_requirement(
|
|
669
|
+
resources.get("memory", {}),
|
|
670
|
+
"startup_and_media_budgeted",
|
|
671
|
+
"startup and media memory budget",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def wifi_requirement(onboarding: dict[str, Any]) -> Requirement:
|
|
676
|
+
wifi = onboarding.get("wifi_credentials", {})
|
|
677
|
+
committed = wifi.get("credentials_committed_to_source")
|
|
678
|
+
if committed is None:
|
|
679
|
+
return "NEEDS_CONFIRMATION", "credential source-control policy is unknown", 0
|
|
680
|
+
if committed is True:
|
|
681
|
+
return "BLOCKED", "Wi-Fi credentials are committed to source", 0
|
|
682
|
+
reprovisioning = wifi.get("reprovisioning_defined")
|
|
683
|
+
if reprovisioning is None:
|
|
684
|
+
return "NEEDS_CONFIRMATION", "Wi-Fi reprovisioning path is unknown", 0
|
|
685
|
+
if reprovisioning is False:
|
|
686
|
+
return "BLOCKED", "Wi-Fi reprovisioning path is undefined", 0
|
|
687
|
+
selected_id = wifi.get("selected_method")
|
|
688
|
+
if not isinstance(selected_id, str) or not selected_id:
|
|
689
|
+
return "NEEDS_CONFIRMATION", "Wi-Fi credential method is not selected", 0
|
|
690
|
+
method = selected_item(wifi.get("methods"), selected_id)
|
|
691
|
+
if method is None:
|
|
692
|
+
return "BLOCKED", f"selected Wi-Fi method {selected_id!r} does not exist", 0
|
|
693
|
+
available = method.get("available")
|
|
694
|
+
if available is None:
|
|
695
|
+
return "NEEDS_CONFIRMATION", "selected Wi-Fi method availability is unknown", 0
|
|
696
|
+
if available is False:
|
|
697
|
+
return "BLOCKED", "selected Wi-Fi method is unavailable", 0
|
|
698
|
+
verification = method.get("verification")
|
|
699
|
+
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
700
|
+
if level < VERIFICATION_LEVELS["corroborated"]:
|
|
701
|
+
return (
|
|
702
|
+
"NEEDS_CONFIRMATION",
|
|
703
|
+
f"selected Wi-Fi method is only {verification}",
|
|
704
|
+
level,
|
|
705
|
+
)
|
|
706
|
+
return (
|
|
707
|
+
"SATISFIED",
|
|
708
|
+
f"Wi-Fi credentials use {method.get('type')} outside source control",
|
|
709
|
+
level,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def binding_requirement(onboarding: dict[str, Any]) -> Requirement:
|
|
714
|
+
binding = onboarding.get("device_binding", {})
|
|
715
|
+
committed = binding.get("credentials_committed_to_source")
|
|
716
|
+
if committed is None:
|
|
717
|
+
return "NEEDS_CONFIRMATION", "device credential source-control policy is unknown", 0
|
|
718
|
+
if committed is True:
|
|
719
|
+
return "BLOCKED", "device credentials are committed to source", 0
|
|
720
|
+
for field in ("stored_credential_state_handled", "clear_binding_control"):
|
|
721
|
+
value = binding.get(field)
|
|
722
|
+
if value is None:
|
|
723
|
+
return "NEEDS_CONFIRMATION", f"device binding {field} is unknown", 0
|
|
724
|
+
if value is False:
|
|
725
|
+
return "BLOCKED", f"device binding {field} is unsupported", 0
|
|
726
|
+
selected_id = binding.get("selected_method")
|
|
727
|
+
if not isinstance(selected_id, str) or not selected_id:
|
|
728
|
+
return "NEEDS_CONFIRMATION", "device binding method is not selected", 0
|
|
729
|
+
method = selected_item(binding.get("methods"), selected_id)
|
|
730
|
+
if method is None:
|
|
731
|
+
return "BLOCKED", f"selected binding method {selected_id!r} does not exist", 0
|
|
732
|
+
available = method.get("available")
|
|
733
|
+
if available is None:
|
|
734
|
+
return "NEEDS_CONFIRMATION", "selected binding method availability is unknown", 0
|
|
735
|
+
if available is False:
|
|
736
|
+
return "BLOCKED", "selected binding method is unavailable", 0
|
|
737
|
+
verification = method.get("verification")
|
|
738
|
+
level = VERIFICATION_LEVELS.get(verification, 0)
|
|
739
|
+
if level < VERIFICATION_LEVELS["corroborated"]:
|
|
740
|
+
return (
|
|
741
|
+
"NEEDS_CONFIRMATION",
|
|
742
|
+
f"selected binding method is only {verification}",
|
|
743
|
+
level,
|
|
744
|
+
)
|
|
745
|
+
return (
|
|
746
|
+
"SATISFIED",
|
|
747
|
+
f"device binding uses {method.get('type')} outside source control",
|
|
748
|
+
level,
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def combine_requirements(
|
|
753
|
+
requirements: list[Requirement], legacy_hil_from_levels: bool = False
|
|
754
|
+
) -> dict[str, Any]:
|
|
247
755
|
reasons = [reason for _, reason, _ in requirements]
|
|
248
756
|
states = {state for state, _, _ in requirements}
|
|
249
757
|
levels = [level for state, _, level in requirements if state == "SATISFIED"]
|
|
@@ -251,15 +759,19 @@ def combine_requirements(requirements: list[tuple[str, str, int]]) -> dict[str,
|
|
|
251
759
|
status = "BLOCKED"
|
|
252
760
|
elif "NEEDS_CONFIRMATION" in states:
|
|
253
761
|
status = "NEEDS_CONFIRMATION"
|
|
254
|
-
elif
|
|
762
|
+
elif (
|
|
763
|
+
legacy_hil_from_levels
|
|
764
|
+
and levels
|
|
765
|
+
and min(levels) >= VERIFICATION_LEVELS["hil_verified"]
|
|
766
|
+
):
|
|
255
767
|
status = "HIL_VERIFIED"
|
|
256
768
|
else:
|
|
257
769
|
status = "READY_TO_PORT"
|
|
258
770
|
return {"status": status, "reasons": reasons}
|
|
259
771
|
|
|
260
772
|
|
|
261
|
-
def project_requirements(data: dict[str, Any]) -> list[
|
|
262
|
-
requirements: list[
|
|
773
|
+
def project_requirements(data: dict[str, Any]) -> list[Requirement]:
|
|
774
|
+
requirements: list[Requirement] = []
|
|
263
775
|
revision = data["board"]["hardware_revision"].strip().lower()
|
|
264
776
|
if revision in {"unknown", "unspecified", "n/a"}:
|
|
265
777
|
requirements.append(
|
|
@@ -312,31 +824,143 @@ def project_requirements(data: dict[str, Any]) -> list[tuple[str, str, int]]:
|
|
|
312
824
|
return requirements
|
|
313
825
|
|
|
314
826
|
|
|
315
|
-
def
|
|
827
|
+
def matching_runtime_evidence(
|
|
828
|
+
data: dict[str, Any], artifact_sha256: str | None
|
|
829
|
+
) -> dict[str, Any] | None:
|
|
830
|
+
if artifact_sha256 is None:
|
|
831
|
+
return None
|
|
832
|
+
normalized = artifact_sha256.lower()
|
|
833
|
+
for record in data.get("runtime_evidence", []):
|
|
834
|
+
if (
|
|
835
|
+
isinstance(record, dict)
|
|
836
|
+
and str(record.get("artifact_sha256", "")).lower() == normalized
|
|
837
|
+
):
|
|
838
|
+
return record
|
|
839
|
+
return None
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def assess_ir(
|
|
843
|
+
data: dict[str, Any], artifact_sha256: str | None = None
|
|
844
|
+
) -> dict[str, Any]:
|
|
845
|
+
schema_version = data.get("schema_version")
|
|
316
846
|
requested = data["features"]["requested"]
|
|
317
847
|
audio_input = data["audio_input"]
|
|
318
848
|
audio_output = data["audio_output"]
|
|
319
849
|
camera = data["camera"]
|
|
320
850
|
result: dict[str, Any] = {}
|
|
851
|
+
project = project_requirements(data)
|
|
852
|
+
selected_video: dict[str, Any] | None = None
|
|
853
|
+
selected_wifi: dict[str, Any] | None = None
|
|
854
|
+
selected_binding: dict[str, Any] | None = None
|
|
855
|
+
|
|
856
|
+
if schema_version == 2:
|
|
857
|
+
resources = data["hardware_resources"]
|
|
858
|
+
onboarding = data["onboarding"]
|
|
859
|
+
project.extend(
|
|
860
|
+
[
|
|
861
|
+
i2c_requirement(resources),
|
|
862
|
+
wifi_requirement(onboarding),
|
|
863
|
+
binding_requirement(onboarding),
|
|
864
|
+
]
|
|
865
|
+
)
|
|
866
|
+
selected_video = selected_item(
|
|
867
|
+
camera.get("video_profiles"), camera.get("selected_video_profile")
|
|
868
|
+
)
|
|
869
|
+
wifi = onboarding.get("wifi_credentials", {})
|
|
870
|
+
selected_wifi = selected_item(wifi.get("methods"), wifi.get("selected_method"))
|
|
871
|
+
binding = onboarding.get("device_binding", {})
|
|
872
|
+
selected_binding = selected_item(
|
|
873
|
+
binding.get("methods"), binding.get("selected_method")
|
|
874
|
+
)
|
|
875
|
+
else:
|
|
876
|
+
resources = {}
|
|
877
|
+
|
|
321
878
|
for feature in requested:
|
|
879
|
+
if schema_version == 1:
|
|
880
|
+
if feature == "h5_live_audio":
|
|
881
|
+
requirements = [codec_requirement(audio_input, "audio_input")]
|
|
882
|
+
elif feature == "h5_live_video":
|
|
883
|
+
requirements = [legacy_video_requirement(camera)]
|
|
884
|
+
elif feature == "h5_talkback":
|
|
885
|
+
requirements = [codec_requirement(audio_output, "audio_output")]
|
|
886
|
+
else:
|
|
887
|
+
requirements = [
|
|
888
|
+
codec_requirement(audio_input, "audio_input"),
|
|
889
|
+
codec_requirement(audio_output, "audio_output"),
|
|
890
|
+
]
|
|
891
|
+
result[feature] = combine_requirements(
|
|
892
|
+
requirements, legacy_hil_from_levels=True
|
|
893
|
+
)
|
|
894
|
+
continue
|
|
895
|
+
|
|
322
896
|
if feature == "h5_live_audio":
|
|
323
|
-
requirements = [
|
|
897
|
+
requirements = [
|
|
898
|
+
codec_requirement(audio_input, "audio_input"),
|
|
899
|
+
i2s_requirement(resources),
|
|
900
|
+
audio_mapping_requirement(resources),
|
|
901
|
+
memory_requirement(resources),
|
|
902
|
+
]
|
|
324
903
|
elif feature == "h5_live_video":
|
|
325
|
-
requirements = [
|
|
904
|
+
requirements = [
|
|
905
|
+
video_requirement_v2(camera),
|
|
906
|
+
camera_realtime_requirement(resources),
|
|
907
|
+
memory_requirement(resources),
|
|
908
|
+
]
|
|
326
909
|
elif feature == "h5_talkback":
|
|
327
|
-
requirements = [
|
|
910
|
+
requirements = [
|
|
911
|
+
codec_requirement(audio_output, "audio_output"),
|
|
912
|
+
i2s_requirement(resources),
|
|
913
|
+
memory_requirement(resources),
|
|
914
|
+
]
|
|
328
915
|
else:
|
|
329
916
|
requirements = [
|
|
330
917
|
codec_requirement(audio_input, "audio_input"),
|
|
331
918
|
codec_requirement(audio_output, "audio_output"),
|
|
919
|
+
i2s_requirement(resources),
|
|
920
|
+
audio_mapping_requirement(resources),
|
|
921
|
+
memory_requirement(resources),
|
|
332
922
|
]
|
|
333
923
|
result[feature] = combine_requirements(requirements)
|
|
334
|
-
|
|
924
|
+
|
|
925
|
+
project_gate = combine_requirements(
|
|
926
|
+
project, legacy_hil_from_levels=(schema_version == 1)
|
|
927
|
+
)
|
|
928
|
+
evidence = matching_runtime_evidence(data, artifact_sha256)
|
|
929
|
+
if schema_version == 2 and evidence is not None:
|
|
930
|
+
evidence_features = set(evidence.get("features", []))
|
|
931
|
+
evidence_levels = set(evidence.get("acceptance_levels", []))
|
|
932
|
+
for feature, assessment in result.items():
|
|
933
|
+
if (
|
|
934
|
+
assessment["status"] == "READY_TO_PORT"
|
|
935
|
+
and feature in evidence_features
|
|
936
|
+
and FEATURE_HIL_LEVEL[feature] in evidence_levels
|
|
937
|
+
):
|
|
938
|
+
assessment["status"] = "HIL_VERIFIED"
|
|
939
|
+
assessment["reasons"].append(
|
|
940
|
+
f"artifact {artifact_sha256} passed {FEATURE_HIL_LEVEL[feature]}"
|
|
941
|
+
)
|
|
942
|
+
if result and all(
|
|
943
|
+
item["status"] == "HIL_VERIFIED" for item in result.values()
|
|
944
|
+
):
|
|
945
|
+
project_gate["status"] = "HIL_VERIFIED"
|
|
946
|
+
project_gate["reasons"].append(
|
|
947
|
+
f"all requested features have matching artifact evidence for {artifact_sha256}"
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
assessment: dict[str, Any] = {
|
|
951
|
+
"schema_version": schema_version,
|
|
335
952
|
"board_id": data["board"]["id"],
|
|
336
953
|
"hardware_revision": data["board"]["hardware_revision"],
|
|
337
|
-
"project_gate":
|
|
954
|
+
"project_gate": project_gate,
|
|
338
955
|
"features": result,
|
|
339
956
|
}
|
|
957
|
+
if schema_version == 2:
|
|
958
|
+
assessment["selected_video_profile"] = selected_video
|
|
959
|
+
assessment["selected_wifi_method"] = selected_wifi
|
|
960
|
+
assessment["selected_binding_method"] = selected_binding
|
|
961
|
+
assessment["artifact_sha256"] = artifact_sha256
|
|
962
|
+
assessment["artifact_evidence_matched"] = evidence is not None
|
|
963
|
+
return assessment
|
|
340
964
|
|
|
341
965
|
|
|
342
966
|
def command_init(args: argparse.Namespace) -> int:
|
|
@@ -344,7 +968,11 @@ def command_init(args: argparse.Namespace) -> int:
|
|
|
344
968
|
if output.exists():
|
|
345
969
|
print(f"refusing to overwrite existing file: {output}", file=sys.stderr)
|
|
346
970
|
return 2
|
|
347
|
-
example =
|
|
971
|
+
example = (
|
|
972
|
+
Path(__file__).resolve().parent.parent
|
|
973
|
+
/ "assets"
|
|
974
|
+
/ "hardware-ir-v2.example.json"
|
|
975
|
+
)
|
|
348
976
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
349
977
|
shutil.copyfile(example, output)
|
|
350
978
|
print(f"created Hardware IR: {output}")
|
|
@@ -377,12 +1005,10 @@ def command_assess(args: argparse.Namespace) -> int:
|
|
|
377
1005
|
for error in errors:
|
|
378
1006
|
print(f"error: {error}", file=sys.stderr)
|
|
379
1007
|
return 2
|
|
380
|
-
assessment = assess_ir(data)
|
|
1008
|
+
assessment = assess_ir(data, artifact_sha256=args.artifact_sha256)
|
|
381
1009
|
print(json.dumps(assessment, ensure_ascii=False, indent=2))
|
|
382
1010
|
if args.strict:
|
|
383
|
-
statuses = {
|
|
384
|
-
item["status"] for item in assessment["features"].values()
|
|
385
|
-
}
|
|
1011
|
+
statuses = {item["status"] for item in assessment["features"].values()}
|
|
386
1012
|
statuses.add(assessment["project_gate"]["status"])
|
|
387
1013
|
if not statuses.issubset(READY_STATUSES):
|
|
388
1014
|
return 3
|
|
@@ -395,7 +1021,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
395
1021
|
)
|
|
396
1022
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
397
1023
|
|
|
398
|
-
init_parser = subparsers.add_parser("init", help="create a new Hardware IR")
|
|
1024
|
+
init_parser = subparsers.add_parser("init", help="create a new Hardware IR v2")
|
|
399
1025
|
init_parser.add_argument("output", type=Path)
|
|
400
1026
|
init_parser.set_defaults(handler=command_init)
|
|
401
1027
|
|
|
@@ -412,6 +1038,10 @@ def parse_args() -> argparse.Namespace:
|
|
|
412
1038
|
action="store_true",
|
|
413
1039
|
help="return non-zero unless every requested feature is ready or HIL verified",
|
|
414
1040
|
)
|
|
1041
|
+
assess_parser.add_argument(
|
|
1042
|
+
"--artifact-sha256",
|
|
1043
|
+
help="bind HIL status to runtime evidence for this exact firmware artifact",
|
|
1044
|
+
)
|
|
415
1045
|
assess_parser.set_defaults(handler=command_assess)
|
|
416
1046
|
return parser.parse_args()
|
|
417
1047
|
|