hw-codesign 0.1.3__py3-none-any.whl
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.
- hw_codesign/__init__.py +3 -0
- hw_codesign/artifacts.py +117 -0
- hw_codesign/backends/__init__.py +1 -0
- hw_codesign/backends/atopile.py +206 -0
- hw_codesign/backends/command.py +80 -0
- hw_codesign/backends/electronics.py +60 -0
- hw_codesign/backends/firmware_modules/__init__.py +161 -0
- hw_codesign/backends/firmware_modules/_base.py +15 -0
- hw_codesign/backends/firmware_modules/interface_stack.py +119 -0
- hw_codesign/backends/firmware_modules/periodic_transmit.py +127 -0
- hw_codesign/backends/firmware_modules/sensor_poll.py +149 -0
- hw_codesign/backends/firmware_modules/state_machine.py +142 -0
- hw_codesign/backends/firmware_modules/timeout_shutdown.py +106 -0
- hw_codesign/backends/freerouting.py +615 -0
- hw_codesign/backends/kicad.py +1214 -0
- hw_codesign/backends/mechanical.py +371 -0
- hw_codesign/backends/parts/__init__.py +88 -0
- hw_codesign/backends/parts/_base.py +212 -0
- hw_codesign/backends/parts/cable_clip.py +150 -0
- hw_codesign/backends/parts/custom_enclosure_variant.py +231 -0
- hw_codesign/backends/parts/din_rail_adapter.py +163 -0
- hw_codesign/backends/parts/pcb_mount_bracket.py +186 -0
- hw_codesign/backends/parts/robot_mechanisms.py +226 -0
- hw_codesign/backends/parts/standoff_tower.py +135 -0
- hw_codesign/backends/python_netlist.py +102 -0
- hw_codesign/backends/tscircuit.py +882 -0
- hw_codesign/backends/zephyr.py +313 -0
- hw_codesign/board_layout.py +871 -0
- hw_codesign/cli.py +334 -0
- hw_codesign/contracts/__init__.py +57 -0
- hw_codesign/contracts/_manifest.py +46 -0
- hw_codesign/contracts/_messages.py +125 -0
- hw_codesign/contracts/_schemas.py +1416 -0
- hw_codesign/contracts/_tools.py +1418 -0
- hw_codesign/contracts/_versions.py +32 -0
- hw_codesign/diagnostics.py +126 -0
- hw_codesign/electronics_design.py +1911 -0
- hw_codesign/errors.py +18 -0
- hw_codesign/footprint_library.py +325 -0
- hw_codesign/footprints/ATTRIBUTION.md +37 -0
- hw_codesign/footprints/C_0402_1005Metric.kicad_mod +112 -0
- hw_codesign/footprints/C_0603_1608Metric.kicad_mod +112 -0
- hw_codesign/footprints/Crystal_SMD_3225-4Pin_3.2x2.5mm.kicad_mod +128 -0
- hw_codesign/footprints/ESP32-S3-WROOM-1.kicad_mod +775 -0
- hw_codesign/footprints/IDC-Header_2x05_P2.54mm_Vertical.kicad_mod +377 -0
- hw_codesign/footprints/LICENSE.md +27 -0
- hw_codesign/footprints/QFN-56-1EP_7x7mm_P0.4mm_EP3.2x3.2mm.kicad_mod +772 -0
- hw_codesign/footprints/R_0603_1608Metric.kicad_mod +112 -0
- hw_codesign/footprints/SOIC-8_5.3x5.3mm_P1.27mm.kicad_mod +300 -0
- hw_codesign/footprints/SOT-23-5.kicad_mod +327 -0
- hw_codesign/footprints/SOT-23-6.kicad_mod +288 -0
- hw_codesign/footprints/USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal.kicad_mod +310 -0
- hw_codesign/generators.py +669 -0
- hw_codesign/io.py +38 -0
- hw_codesign/job_worker.py +156 -0
- hw_codesign/jobs.py +405 -0
- hw_codesign/mcp_server.py +651 -0
- hw_codesign/mechanical_contract.py +68 -0
- hw_codesign/models.py +141 -0
- hw_codesign/pcb_router.py +245 -0
- hw_codesign/placement.py +2345 -0
- hw_codesign/policy.py +19 -0
- hw_codesign/power_sim.py +207 -0
- hw_codesign/processes.py +49 -0
- hw_codesign/project_parts.py +69 -0
- hw_codesign/provenance.py +52 -0
- hw_codesign/reference_backend.py +1422 -0
- hw_codesign/resolver.py +357 -0
- hw_codesign/resources.py +20 -0
- hw_codesign/review_3d.py +229 -0
- hw_codesign/review_3d_viewer.bundle.js +4000 -0
- hw_codesign/review_3d_viewer.bundle.json +61 -0
- hw_codesign/review_3d_viewer.js +80 -0
- hw_codesign/review_report.py +1034 -0
- hw_codesign/review_viewer.py +914 -0
- hw_codesign/schematic_generator.py +93 -0
- hw_codesign/semantic_schematic.py +159 -0
- hw_codesign/service.py +8000 -0
- hw_codesign/sourcing_policy.py +27 -0
- hw_codesign/stdio.py +40 -0
- hw_codesign/supplier_adapters.py +104 -0
- hw_codesign/templates/__init__.py +1 -0
- hw_codesign/templates/avr_32u4_hid.yaml +199 -0
- hw_codesign/templates/bldc_esc.yaml +141 -0
- hw_codesign/templates/ble_sensor_node.yaml +105 -0
- hw_codesign/templates/esp32_wifi_gateway.yaml +192 -0
- hw_codesign/templates/lora_sensor_node.yaml +123 -0
- hw_codesign/templates/mini_servo_robot.parts.local.yaml +165 -0
- hw_codesign/templates/mini_servo_robot.yaml +247 -0
- hw_codesign/templates/nrf52840_dongle.yaml +197 -0
- hw_codesign/templates/robotics_controller_full.yaml +143 -0
- hw_codesign/templates/rp2040_usb_device.yaml +238 -0
- hw_codesign/templates/samd21_sensor_hub.yaml +233 -0
- hw_codesign/templates/sensor_data_logger.yaml +103 -0
- hw_codesign/templates/stm32g0_power_monitor.yaml +209 -0
- hw_codesign/templates/usb_hid_controller.yaml +139 -0
- hw_codesign/third_party/three/ATTRIBUTION.md +14 -0
- hw_codesign/third_party/three/LICENSE +21 -0
- hw_codesign/validation.py +3380 -0
- hw_codesign/workspace.py +126 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/avr_32u4_hid.yaml +76 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/bldc_esc.yaml +249 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/ble_sensor_node.yaml +294 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/common_passives.yaml +1208 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/esp32_wifi_gateway.yaml +120 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/interface_ics.yaml +196 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/lora_sensor_node.yaml +172 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/memory.yaml +65 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/motion_sensors.yaml +118 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/power_management.yaml +95 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/robotics_controller.yaml +24 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/samd21_sensor_hub.yaml +61 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/sensor_data_logger.yaml +64 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/stm32g0_power_monitor.yaml +152 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/components/usb_hid_controller.yaml +170 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/evidence/datasheets.yaml +160 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/avr_32u4_hid.yaml +31 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/bldc_esc.yaml +44 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/ble_sensor_node.yaml +50 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/esp32_wifi_gateway.yaml +31 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/lora_sensor_node.yaml +33 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/nrf52840_dongle.yaml +29 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/robotics_controller.yaml +53 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/rp2040_usb_device.yaml +55 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/samd21_sensor_hub.yaml +42 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/sensor_data_logger.yaml +35 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/stm32g0_power_monitor.yaml +41 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/role_sets/usb_hid_controller.yaml +37 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/schemas/component.schema.json +59 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/schemas/evidence_catalog.schema.json +28 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/schemas/role_set.schema.json +47 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/schemas/supplier_catalog.schema.json +29 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/suppliers/curated.yaml +162 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/suppliers/digikey.yaml +2 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/suppliers/lcsc_jlcpcb.yaml +2 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/suppliers/mouser.yaml +2 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/parts/suppliers/octopart.yaml +2 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/electrical.schema.json +1 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/error.schema.json +50 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/firmware.schema.json +1 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/gate_event.schema.json +44 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/job_status.schema.json +78 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/job_submit.schema.json +46 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/mechanical.schema.json +18 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/project_parts.schema.json +133 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/release.schema.json +1 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/requirements.schema.json +109 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/review_bundle.schema.json +287 -0
- hw_codesign-0.1.3.data/data/share/hw-codesign/schemas/system.schema.json +160 -0
- hw_codesign-0.1.3.dist-info/METADATA +99 -0
- hw_codesign-0.1.3.dist-info/RECORD +156 -0
- hw_codesign-0.1.3.dist-info/WHEEL +5 -0
- hw_codesign-0.1.3.dist-info/entry_points.txt +3 -0
- hw_codesign-0.1.3.dist-info/licenses/LICENSE +201 -0
- hw_codesign-0.1.3.dist-info/licenses/NOTICE +44 -0
- hw_codesign-0.1.3.dist-info/top_level.txt +1 -0
hw_codesign/__init__.py
ADDED
hw_codesign/artifacts.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import math
|
|
5
|
+
import zipfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable
|
|
8
|
+
|
|
9
|
+
from .io import atomic_write_text, write_json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def sha256(path: Path) -> str:
|
|
13
|
+
digest = hashlib.sha256()
|
|
14
|
+
with path.open("rb") as handle:
|
|
15
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
16
|
+
digest.update(chunk)
|
|
17
|
+
return digest.hexdigest()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def deterministic_zip(output: Path, files: Iterable[tuple[Path, str]]) -> None:
|
|
21
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
|
|
23
|
+
for source, archive_name in sorted(files, key=lambda item: item[1]):
|
|
24
|
+
info = zipfile.ZipInfo(archive_name, date_time=(1980, 1, 1, 0, 0, 0))
|
|
25
|
+
info.compress_type = zipfile.ZIP_DEFLATED
|
|
26
|
+
info.external_attr = 0o100644 << 16
|
|
27
|
+
archive.writestr(info, source.read_bytes())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_manifest(root: Path, output: Path, provenance: dict[str, Any] | None = None, candidate_only: bool = False) -> str:
|
|
31
|
+
entries = []
|
|
32
|
+
for path in sorted(root.rglob("*")):
|
|
33
|
+
if path.is_file() and path != output:
|
|
34
|
+
entries.append({"path": path.relative_to(root).as_posix(), "bytes": path.stat().st_size, "sha256": sha256(path), "provenance": provenance or {}})
|
|
35
|
+
write_json(output, {"algorithm": "sha256", "candidate_only": candidate_only, "release_eligible": not candidate_only, "provenance": provenance or {}, "artifacts": entries})
|
|
36
|
+
return str(output)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def simple_pdf(title: str, lines: list[str], output: Path) -> None:
|
|
40
|
+
escaped = [line.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)") for line in [title, *lines]]
|
|
41
|
+
commands = ["BT", "/F1 16 Tf", "50 790 Td", f"({escaped[0]}) Tj", "/F1 10 Tf"]
|
|
42
|
+
for line in escaped[1:]:
|
|
43
|
+
commands.extend(("0 -16 Td", f"({line}) Tj"))
|
|
44
|
+
commands.append("ET")
|
|
45
|
+
stream = "\n".join(commands).encode("ascii", errors="replace")
|
|
46
|
+
objects = [
|
|
47
|
+
b"<< /Type /Catalog /Pages 2 0 R >>",
|
|
48
|
+
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
|
49
|
+
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
|
|
50
|
+
b"<< /Length %d >>\nstream\n" % len(stream) + stream + b"\nendstream",
|
|
51
|
+
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
|
52
|
+
]
|
|
53
|
+
content = bytearray(b"%PDF-1.4\n")
|
|
54
|
+
offsets = [0]
|
|
55
|
+
for index, obj in enumerate(objects, 1):
|
|
56
|
+
offsets.append(len(content)); content.extend(f"{index} 0 obj\n".encode()); content.extend(obj); content.extend(b"\nendobj\n")
|
|
57
|
+
xref = len(content); content.extend(f"xref\n0 {len(objects)+1}\n0000000000 65535 f \n".encode())
|
|
58
|
+
for offset in offsets[1:]: content.extend(f"{offset:010d} 00000 n \n".encode())
|
|
59
|
+
content.extend(f"trailer\n<< /Size {len(objects)+1} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode())
|
|
60
|
+
output.parent.mkdir(parents=True, exist_ok=True); output.write_bytes(content)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def box_stl(width: float, depth: float, height: float, wall: float, output: Path) -> None:
|
|
64
|
+
"""Write a watertight open-top enclosure shell as deterministic ASCII STL."""
|
|
65
|
+
outer = _box_triangles(0, 0, 0, width, depth, height)
|
|
66
|
+
inner = _box_triangles(wall, wall, wall, width - wall, depth - wall, height + wall)
|
|
67
|
+
triangles = outer + [(c, b, a) for a, b, c in inner]
|
|
68
|
+
lines = ["solid enclosure"]
|
|
69
|
+
for a, b, c in triangles:
|
|
70
|
+
normal = _normal(a, b, c)
|
|
71
|
+
lines.append(f" facet normal {normal[0]:.6f} {normal[1]:.6f} {normal[2]:.6f}")
|
|
72
|
+
lines.append(" outer loop")
|
|
73
|
+
lines.extend(f" vertex {p[0]:.6f} {p[1]:.6f} {p[2]:.6f}" for p in (a, b, c))
|
|
74
|
+
lines.extend((" endloop", " endfacet"))
|
|
75
|
+
lines.append("endsolid enclosure")
|
|
76
|
+
atomic_write_text(output, "\n".join(lines) + "\n")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _box_triangles(x0: float, y0: float, z0: float, x1: float, y1: float, z1: float):
|
|
80
|
+
p = [(x0, y0, z0), (x1, y0, z0), (x1, y1, z0), (x0, y1, z0), (x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1)]
|
|
81
|
+
faces = [(0, 2, 1), (0, 3, 2), (4, 5, 6), (4, 6, 7), (0, 1, 5), (0, 5, 4), (1, 2, 6), (1, 6, 5), (2, 3, 7), (2, 7, 6), (3, 0, 4), (3, 4, 7)]
|
|
82
|
+
return [(p[a], p[b], p[c]) for a, b, c in faces]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _normal(a, b, c):
|
|
86
|
+
u = tuple(b[i] - a[i] for i in range(3))
|
|
87
|
+
v = tuple(c[i] - a[i] for i in range(3))
|
|
88
|
+
n = (u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0])
|
|
89
|
+
length = math.sqrt(sum(value * value for value in n)) or 1.0
|
|
90
|
+
return tuple(value / length for value in n)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def step_box(name: str, width: float, depth: float, height: float, output: Path) -> None:
|
|
94
|
+
"""Write a compact AP214 faceted BREP box accepted by standard STEP readers."""
|
|
95
|
+
points = [(0, 0, 0), (width, 0, 0), (width, depth, 0), (0, depth, 0), (0, 0, height), (width, 0, height), (width, depth, height), (0, depth, height)]
|
|
96
|
+
faces = [(1, 4, 3, 2), (5, 6, 7, 8), (1, 2, 6, 5), (2, 3, 7, 6), (3, 4, 8, 7), (4, 1, 5, 8)]
|
|
97
|
+
data = ["ISO-10303-21;", "HEADER;", "FILE_DESCRIPTION(('HW co-design deterministic faceted model'),'2;1');", f"FILE_NAME('{name}.step','1980-01-01T00:00:00',('hw-codesign'),('hw-codesign'),'hw-codesign','hw-codesign','');", "FILE_SCHEMA(('AUTOMOTIVE_DESIGN'));", "ENDSEC;", "DATA;"]
|
|
98
|
+
index = 1
|
|
99
|
+
point_ids = []
|
|
100
|
+
for x, y, z in points:
|
|
101
|
+
data.append(f"#{index}=CARTESIAN_POINT('',({x:.6f},{y:.6f},{z:.6f}));")
|
|
102
|
+
point_ids.append(index)
|
|
103
|
+
index += 1
|
|
104
|
+
face_ids = []
|
|
105
|
+
for face in faces:
|
|
106
|
+
loop_points = ",".join(f"#{point_ids[item - 1]}" for item in (*face, face[0]))
|
|
107
|
+
data.append(f"#{index}=POLY_LOOP('',({loop_points}));")
|
|
108
|
+
loop_id = index; index += 1
|
|
109
|
+
data.append(f"#{index}=FACE_OUTER_BOUND('',#{loop_id},.T.);")
|
|
110
|
+
bound_id = index; index += 1
|
|
111
|
+
data.append(f"#{index}=FACE('',(#{bound_id}));")
|
|
112
|
+
face_ids.append(index); index += 1
|
|
113
|
+
data.append(f"#{index}=CLOSED_SHELL('',({','.join(f'#{item}' for item in face_ids)}));")
|
|
114
|
+
shell_id = index; index += 1
|
|
115
|
+
data.append(f"#{index}=FACETED_BREP('{name}',#{shell_id});")
|
|
116
|
+
data.extend(("ENDSEC;", "END-ISO-10303-21;"))
|
|
117
|
+
atomic_write_text(output, "\n".join(data) + "\n")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Deterministic backend adapters."""
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..io import atomic_write_text, write_json
|
|
10
|
+
from ..models import Failure, FailureCategory, GateReport, Status
|
|
11
|
+
from ..provenance import artifact_provenance
|
|
12
|
+
from .electronics import ElectronicsBackendAdapter
|
|
13
|
+
|
|
14
|
+
_RAIL_NAMES = frozenset(("GND", "V3V3", "VCC", "V5", "VBAT"))
|
|
15
|
+
_SIGNAL_RE = re.compile(r"^\s+signal\s+(\w+)", re.MULTILINE)
|
|
16
|
+
|
|
17
|
+
_FOOTPRINT_DEFERRED = Failure(
|
|
18
|
+
FailureCategory.TOOL_ERROR,
|
|
19
|
+
"footprint_assignment_deferred",
|
|
20
|
+
"Atopile assigns footprints through the KiCad plugin at layout time, not at compile time",
|
|
21
|
+
details={"atopile_version": "0.15.7", "deferred_to": "kicad_plugin_layout"},
|
|
22
|
+
)
|
|
23
|
+
_LAYOUT_BLOCKED = Failure(
|
|
24
|
+
FailureCategory.TOOL_ERROR,
|
|
25
|
+
"kicad_plugin_required",
|
|
26
|
+
"Atopile layout and manufacturing export require the KiCad plugin; install KiCad and configure the atopile plugin path",
|
|
27
|
+
details={"atopile_version": "0.15.7", "blocked_on": "kicad_plugin_path"},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _ato_source(module_name: str, graph: dict[str, Any]) -> str:
|
|
32
|
+
nets = [net["name"] for net in graph.get("nets", []) if net.get("name")]
|
|
33
|
+
rails = sorted({n for n in nets if n in _RAIL_NAMES})
|
|
34
|
+
signals = sorted({n.lower().replace(" ", "_").replace("-", "_") for n in nets if n not in _RAIL_NAMES})
|
|
35
|
+
components = graph.get("components", [])
|
|
36
|
+
|
|
37
|
+
lines = [f"module {module_name}:"]
|
|
38
|
+
if rails:
|
|
39
|
+
lines.append(" # Power rails")
|
|
40
|
+
for rail in rails:
|
|
41
|
+
lines.append(f" signal {rail.lower()}")
|
|
42
|
+
if signals:
|
|
43
|
+
lines.append(" # Design signals")
|
|
44
|
+
for sig in signals:
|
|
45
|
+
lines.append(f" signal {sig}")
|
|
46
|
+
if components:
|
|
47
|
+
lines.append(" # Component placeholders (populate imports from atopile package registry)")
|
|
48
|
+
for comp in components:
|
|
49
|
+
ref = comp.get("ref", "?")
|
|
50
|
+
mpn = comp.get("mpn") or comp.get("category", "unknown")
|
|
51
|
+
lines.append(f" # {ref}: {mpn}")
|
|
52
|
+
return "\n".join(lines) + "\n"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _ato_declared_signals(source: str) -> set[str]:
|
|
56
|
+
return {m.group(1) for m in _SIGNAL_RE.finditer(source)}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _expected_ato_signals(graph: dict[str, Any]) -> set[str]:
|
|
60
|
+
nets = [net["name"] for net in graph.get("nets", []) if net.get("name")]
|
|
61
|
+
rails = {n.lower() for n in nets if n in _RAIL_NAMES}
|
|
62
|
+
signals = {n.lower().replace(" ", "_").replace("-", "_") for n in nets if n not in _RAIL_NAMES}
|
|
63
|
+
return rails | signals
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _source_parity_gates(ato_file: Path, graph: dict[str, Any]) -> list[GateReport]:
|
|
67
|
+
source_text = ato_file.read_text(encoding="utf-8")
|
|
68
|
+
declared = _ato_declared_signals(source_text)
|
|
69
|
+
expected = _expected_ato_signals(graph)
|
|
70
|
+
missing = expected - declared
|
|
71
|
+
parity_failures = [
|
|
72
|
+
Failure(
|
|
73
|
+
FailureCategory.EDA_ERROR,
|
|
74
|
+
"ato_signal_missing",
|
|
75
|
+
f"Net {sig!r} in graph but absent from generated .ato source",
|
|
76
|
+
details={"missing_signal": sig},
|
|
77
|
+
)
|
|
78
|
+
for sig in sorted(missing)[:20]
|
|
79
|
+
]
|
|
80
|
+
return [
|
|
81
|
+
GateReport(
|
|
82
|
+
"atopile_netlist_extract",
|
|
83
|
+
Status.PASS,
|
|
84
|
+
[],
|
|
85
|
+
metrics={"declared_signals": len(declared)},
|
|
86
|
+
artifacts=[str(ato_file)],
|
|
87
|
+
backend={"name": "atopile", "method": "source_ast_extraction"},
|
|
88
|
+
),
|
|
89
|
+
GateReport(
|
|
90
|
+
"atopile_graph_parity",
|
|
91
|
+
Status.FAIL if parity_failures else Status.PASS,
|
|
92
|
+
parity_failures,
|
|
93
|
+
metrics={"declared": len(declared), "expected": len(expected), "missing": len(missing)},
|
|
94
|
+
backend={"name": "atopile", "method": "source_ast_parity"},
|
|
95
|
+
),
|
|
96
|
+
GateReport("atopile_footprint_parity", Status.BLOCKED, [_FOOTPRINT_DEFERRED], backend={"name": "atopile"}),
|
|
97
|
+
GateReport("atopile_layout_completeness", Status.BLOCKED, [_LAYOUT_BLOCKED], backend={"name": "atopile"}),
|
|
98
|
+
GateReport("atopile_manufacturing_export", Status.BLOCKED, [_LAYOUT_BLOCKED], backend={"name": "atopile"}),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AtopileBackend(ElectronicsBackendAdapter):
|
|
103
|
+
name = "atopile"
|
|
104
|
+
|
|
105
|
+
def __init__(self, platform_root: Path):
|
|
106
|
+
self.platform_root = platform_root
|
|
107
|
+
|
|
108
|
+
def generate_source(self, project: Path, spec: dict[str, Any], graph: dict[str, Any]) -> list[str]:
|
|
109
|
+
target = project / "electronics" / "source" / self.name
|
|
110
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
project_name = spec.get("project", {}).get("name", project.name)
|
|
112
|
+
module_name = "".join(word.capitalize() for word in project_name.split("_")) or "Design"
|
|
113
|
+
ato_file = target / "design.ato"
|
|
114
|
+
atomic_write_text(ato_file, _ato_source(module_name, graph))
|
|
115
|
+
ato_yaml = target / "ato.yaml"
|
|
116
|
+
atomic_write_text(ato_yaml, "requires-atopile: \">=0.15.0\"\n")
|
|
117
|
+
manifest = target / "source_manifest.json"
|
|
118
|
+
write_json(manifest, {
|
|
119
|
+
"backend": self.name,
|
|
120
|
+
"backend_release_capable": True,
|
|
121
|
+
"source_release_eligible": True,
|
|
122
|
+
"release_tier": "hdl_source",
|
|
123
|
+
"netlist_release_eligible": False,
|
|
124
|
+
"hdl_source_release_eligible": True,
|
|
125
|
+
"fabrication_release_eligible": False,
|
|
126
|
+
"sources": self.source_entries(target, [ato_file, ato_yaml]),
|
|
127
|
+
"contract_gates": list(self.gate_names),
|
|
128
|
+
"release_blocking_gates": [
|
|
129
|
+
f"{self.name}_compile",
|
|
130
|
+
f"{self.name}_netlist_extract",
|
|
131
|
+
f"{self.name}_graph_parity",
|
|
132
|
+
],
|
|
133
|
+
"provenance": artifact_provenance(
|
|
134
|
+
spec,
|
|
135
|
+
self.platform_root / "parts",
|
|
136
|
+
self.name,
|
|
137
|
+
command=[],
|
|
138
|
+
release_eligible=True,
|
|
139
|
+
release_tier="hdl_source",
|
|
140
|
+
),
|
|
141
|
+
})
|
|
142
|
+
return [str(ato_file), str(ato_yaml), str(manifest)]
|
|
143
|
+
|
|
144
|
+
def evaluate(self, project: Path, graph: dict[str, Any]) -> list[GateReport]:
|
|
145
|
+
ato_bin = shutil.which("ato")
|
|
146
|
+
if not ato_bin:
|
|
147
|
+
compile_report = GateReport(
|
|
148
|
+
f"{self.name}_compile",
|
|
149
|
+
Status.BLOCKED,
|
|
150
|
+
[Failure(FailureCategory.TOOL_ERROR, "tool_unavailable", "atopile CLI not found; install with: brew install atopile")],
|
|
151
|
+
backend={"name": self.name},
|
|
152
|
+
)
|
|
153
|
+
return self.complete_contract([compile_report])
|
|
154
|
+
source_dir = project / "electronics" / "source" / self.name
|
|
155
|
+
ato_file = source_dir / "design.ato"
|
|
156
|
+
if not ato_file.is_file():
|
|
157
|
+
compile_report = GateReport(
|
|
158
|
+
f"{self.name}_compile",
|
|
159
|
+
Status.BLOCKED,
|
|
160
|
+
[Failure(FailureCategory.TOOL_ERROR, "source_not_generated", "Run generate_electronics first to produce atopile source")],
|
|
161
|
+
backend={"name": self.name},
|
|
162
|
+
)
|
|
163
|
+
return self.complete_contract([compile_report])
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
result = subprocess.run(
|
|
167
|
+
[ato_bin, "build"],
|
|
168
|
+
cwd=source_dir,
|
|
169
|
+
capture_output=True,
|
|
170
|
+
text=True,
|
|
171
|
+
timeout=60,
|
|
172
|
+
)
|
|
173
|
+
except subprocess.TimeoutExpired:
|
|
174
|
+
compile_report = GateReport(
|
|
175
|
+
f"{self.name}_compile",
|
|
176
|
+
Status.FAIL,
|
|
177
|
+
[Failure(FailureCategory.TOOL_ERROR, "build_timeout", "atopile build timed out after 60 seconds")],
|
|
178
|
+
backend={"name": self.name, "ato_bin": ato_bin},
|
|
179
|
+
)
|
|
180
|
+
return self.complete_contract([compile_report])
|
|
181
|
+
except OSError as exc:
|
|
182
|
+
compile_report = GateReport(
|
|
183
|
+
f"{self.name}_compile",
|
|
184
|
+
Status.BLOCKED,
|
|
185
|
+
[Failure(FailureCategory.TOOL_ERROR, "tool_unavailable", str(exc))],
|
|
186
|
+
backend={"name": self.name},
|
|
187
|
+
)
|
|
188
|
+
return self.complete_contract([compile_report])
|
|
189
|
+
|
|
190
|
+
stderr_text = (result.stdout + "\n" + result.stderr).strip()
|
|
191
|
+
if result.returncode == 0:
|
|
192
|
+
compile_report = GateReport(
|
|
193
|
+
f"{self.name}_compile",
|
|
194
|
+
Status.PASS,
|
|
195
|
+
[],
|
|
196
|
+
backend={"name": self.name, "ato_bin": ato_bin},
|
|
197
|
+
)
|
|
198
|
+
return self.complete_contract([compile_report, *_source_parity_gates(ato_file, graph)])
|
|
199
|
+
else:
|
|
200
|
+
compile_report = GateReport(
|
|
201
|
+
f"{self.name}_compile",
|
|
202
|
+
Status.FAIL,
|
|
203
|
+
[Failure(FailureCategory.EDA_ERROR, "build_failed", stderr_text[:1000])],
|
|
204
|
+
backend={"name": self.name, "ato_bin": ato_bin},
|
|
205
|
+
)
|
|
206
|
+
return self.complete_contract([compile_report])
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..models import Failure, FailureCategory, GateReport, Status
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ToolResult:
|
|
14
|
+
command: list[str]
|
|
15
|
+
returncode: int | None
|
|
16
|
+
stdout: str
|
|
17
|
+
stderr: str
|
|
18
|
+
available: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_tool(executable: str) -> str | None:
|
|
22
|
+
resolved = shutil.which(executable)
|
|
23
|
+
if resolved is None and executable == "kicad-cli":
|
|
24
|
+
candidate = Path("/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli")
|
|
25
|
+
resolved = str(candidate) if candidate.is_file() else None
|
|
26
|
+
return resolved
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_kicad_python() -> str | None:
|
|
30
|
+
"""Return path to KiCad's bundled Python for pcbnew scripting access."""
|
|
31
|
+
env_path = os.environ.get("HW_KICAD_PYTHON")
|
|
32
|
+
if env_path:
|
|
33
|
+
p = Path(env_path)
|
|
34
|
+
if p.is_file():
|
|
35
|
+
return str(p)
|
|
36
|
+
for candidate in [
|
|
37
|
+
Path("/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/bin/python3.9"),
|
|
38
|
+
Path("/usr/lib/kicad/lib/python3/dist-packages/../../../bin/python3"),
|
|
39
|
+
]:
|
|
40
|
+
if candidate.is_file():
|
|
41
|
+
return str(candidate)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def tool_version(executable: str) -> str | None:
|
|
46
|
+
resolved = resolve_tool(executable)
|
|
47
|
+
if resolved is None:
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
timeout = float(os.environ.get("HW_TOOL_VERSION_TIMEOUT_SECONDS", "5"))
|
|
51
|
+
except ValueError:
|
|
52
|
+
timeout = 5.0
|
|
53
|
+
try:
|
|
54
|
+
completed = subprocess.run([resolved, "version"], capture_output=True, text=True, timeout=timeout, check=False)
|
|
55
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
56
|
+
return None
|
|
57
|
+
return completed.stdout.strip() or completed.stderr.strip() or None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_tool(executable: str, arguments: list[str], cwd: Path, timeout: int = 300, env: dict[str, str] | None = None) -> ToolResult:
|
|
61
|
+
resolved = resolve_tool(executable)
|
|
62
|
+
command = [executable, *arguments]
|
|
63
|
+
if resolved is None:
|
|
64
|
+
return ToolResult(command, None, "", f"Executable not found: {executable}", False)
|
|
65
|
+
try:
|
|
66
|
+
completed = subprocess.run([resolved, *arguments], cwd=cwd, capture_output=True, text=True, timeout=timeout, check=False, env={**os.environ, **(env or {})})
|
|
67
|
+
return ToolResult(command, completed.returncode, completed.stdout, completed.stderr, True)
|
|
68
|
+
except subprocess.TimeoutExpired:
|
|
69
|
+
return ToolResult(command, None, "", f"{executable} timed out after {timeout}s", False)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def tool_report(gate: str, result: ToolResult, artifacts: list[str] | None = None) -> GateReport:
|
|
73
|
+
backend = {"command": result.command, "returncode": result.returncode, "stdout": result.stdout[-8000:], "stderr": result.stderr[-8000:], "available": result.available}
|
|
74
|
+
if not result.available:
|
|
75
|
+
failure = Failure(FailureCategory.TOOL_ERROR, "tool_unavailable", result.stderr, details={"command": result.command})
|
|
76
|
+
return GateReport(gate, Status.BLOCKED, [failure], backend=backend)
|
|
77
|
+
if result.returncode != 0:
|
|
78
|
+
failure = Failure(FailureCategory.TOOL_ERROR, "tool_failed", f"{result.command[0]} exited with status {result.returncode}", details=backend)
|
|
79
|
+
return GateReport(gate, Status.FAIL, [failure], backend=backend)
|
|
80
|
+
return GateReport(gate, Status.PASS, artifacts=artifacts or [], backend=backend)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..models import Failure, FailureCategory, GateReport, Status
|
|
9
|
+
|
|
10
|
+
CONTRACT_STAGES = (
|
|
11
|
+
"compile",
|
|
12
|
+
"netlist_extract",
|
|
13
|
+
"graph_parity",
|
|
14
|
+
"footprint_parity",
|
|
15
|
+
"layout_completeness",
|
|
16
|
+
"manufacturing_export",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ElectronicsBackendAdapter(ABC):
|
|
21
|
+
name: str
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def gate_names(self) -> tuple[str, ...]:
|
|
25
|
+
return tuple(f"{self.name}_{stage}" for stage in CONTRACT_STAGES)
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def generate_source(self, project: Path, spec: dict[str, Any], graph: dict[str, Any]) -> list[str]:
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def evaluate(self, project: Path, graph: dict[str, Any]) -> list[GateReport]:
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
def blocked_contract(self, code: str, message: str, *, category: FailureCategory = FailureCategory.TOOL_ERROR) -> list[GateReport]:
|
|
36
|
+
failure = Failure(category, code, message)
|
|
37
|
+
return [GateReport(gate, Status.BLOCKED, [failure], backend={"name": self.name}) for gate in self.gate_names]
|
|
38
|
+
|
|
39
|
+
def complete_contract(self, reports: list[GateReport]) -> list[GateReport]:
|
|
40
|
+
by_gate = {report.gate: report for report in reports}
|
|
41
|
+
for gate in self.gate_names:
|
|
42
|
+
if gate not in by_gate:
|
|
43
|
+
by_gate[gate] = GateReport(
|
|
44
|
+
gate,
|
|
45
|
+
Status.BLOCKED,
|
|
46
|
+
[Failure(FailureCategory.EDA_ERROR, "gate_not_run", f"Backend contract gate was not run: {gate}")],
|
|
47
|
+
backend={"name": self.name},
|
|
48
|
+
)
|
|
49
|
+
return [by_gate[gate] for gate in self.gate_names]
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def source_entries(root: Path, paths: list[Path]) -> list[dict[str, str]]:
|
|
53
|
+
return [
|
|
54
|
+
{"path": path.relative_to(root).as_posix(), "sha256": sha256(path.read_bytes()).hexdigest()}
|
|
55
|
+
for path in sorted(paths)
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def graph_netlist(graph: dict[str, Any]) -> dict[str, list[str]]:
|
|
60
|
+
return {item["name"]: sorted(item.get("connected_pins", [])) for item in graph.get("nets", [])}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from . import interface_stack, periodic_transmit, sensor_poll, state_machine, timeout_shutdown
|
|
6
|
+
from ._base import ModuleOutput
|
|
7
|
+
|
|
8
|
+
_RENDERERS = {
|
|
9
|
+
"timeout_shutdown": timeout_shutdown.render,
|
|
10
|
+
"periodic_transmit": periodic_transmit.render,
|
|
11
|
+
"state_machine": state_machine.render,
|
|
12
|
+
"sensor_poll": sensor_poll.render,
|
|
13
|
+
"interface_stack": interface_stack.render,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
BEHAVIOR_DESCRIPTIONS: dict[str, str] = {
|
|
17
|
+
"timeout_shutdown": (
|
|
18
|
+
"Watchdog pattern: arms a timer on init; if not kicked within timeout_ms "
|
|
19
|
+
"or the trigger signal asserts, disables the specified outputs and asserts a fault pin."
|
|
20
|
+
),
|
|
21
|
+
"periodic_transmit": (
|
|
22
|
+
"Scheduled frame transmit: sends a CAN/UART/I2C frame on a fixed interval. "
|
|
23
|
+
"Frame ID, DLC, and content are parametric."
|
|
24
|
+
),
|
|
25
|
+
"state_machine": (
|
|
26
|
+
"Agent-specified state machine: states, transitions, and entry/exit actions are "
|
|
27
|
+
"parameterized; generates a thread-safe enum-based FSM."
|
|
28
|
+
),
|
|
29
|
+
"sensor_poll": (
|
|
30
|
+
"Scheduled ADC/I2C/SPI read: polls a sensor every poll_interval_ms and writes "
|
|
31
|
+
"readings into a fixed-size ring buffer."
|
|
32
|
+
),
|
|
33
|
+
"interface_stack": (
|
|
34
|
+
"Firmware stack binding: declares USB, QSPI/SPI, PIO, or similar stack ownership "
|
|
35
|
+
"over graph-grounded nets, required bring-up tests, and init order. It is an "
|
|
36
|
+
"interface contract stub, not behavior-level firmware proof."
|
|
37
|
+
),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
BEHAVIOR_SCHEMAS: dict[str, dict[str, Any]] = {
|
|
41
|
+
"timeout_shutdown": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"required": ["id", "behavior", "trigger", "action"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"id": {"type": "string", "description": "Module identifier (used as C symbol prefix and file name)"},
|
|
46
|
+
"behavior": {"type": "string", "const": "timeout_shutdown"},
|
|
47
|
+
"trigger": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"signal": {"type": "string", "description": "GPIO signal name from pinmap (e.g. ESTOP_IN)"},
|
|
51
|
+
"timeout_ms": {"type": "integer", "description": "Watchdog timeout in milliseconds"},
|
|
52
|
+
},
|
|
53
|
+
"required": ["signal", "timeout_ms"],
|
|
54
|
+
},
|
|
55
|
+
"action": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"disable_all": {"type": "string", "description": "Logical group to disable (e.g. motor_enables)"},
|
|
59
|
+
"assert": {"type": "string", "description": "Signal to assert on trigger (e.g. FAULT_LED)"},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
"periodic_transmit": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"required": ["id", "behavior", "interval_ms", "frame"],
|
|
67
|
+
"properties": {
|
|
68
|
+
"id": {"type": "string"},
|
|
69
|
+
"behavior": {"type": "string", "const": "periodic_transmit"},
|
|
70
|
+
"interval_ms": {"type": "integer", "description": "Transmit interval in milliseconds"},
|
|
71
|
+
"transport": {"type": "string", "enum": ["can", "uart", "i2c"], "default": "can"},
|
|
72
|
+
"frame": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"id": {"description": "Frame/register address (integer or hex string)"},
|
|
76
|
+
"dlc": {"type": "integer", "description": "Data length in bytes"},
|
|
77
|
+
"content": {"type": "string", "description": "Logical content label (e.g. motor_status)"},
|
|
78
|
+
},
|
|
79
|
+
"required": ["id", "dlc"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
"state_machine": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"required": ["id", "behavior", "states", "transitions"],
|
|
86
|
+
"properties": {
|
|
87
|
+
"id": {"type": "string"},
|
|
88
|
+
"behavior": {"type": "string", "const": "state_machine"},
|
|
89
|
+
"initial_state": {"type": "string", "description": "Name of the initial state"},
|
|
90
|
+
"states": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"name": {"type": "string"},
|
|
96
|
+
"entry": {"type": ["string", "null"]},
|
|
97
|
+
"exit": {"type": ["string", "null"]},
|
|
98
|
+
},
|
|
99
|
+
"required": ["name"],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
"transitions": {
|
|
103
|
+
"type": "array",
|
|
104
|
+
"items": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"from": {"type": "string"},
|
|
108
|
+
"to": {"type": "string"},
|
|
109
|
+
"trigger": {"type": "string"},
|
|
110
|
+
},
|
|
111
|
+
"required": ["from", "to", "trigger"],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"sensor_poll": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"required": ["id", "behavior"],
|
|
119
|
+
"properties": {
|
|
120
|
+
"id": {"type": "string"},
|
|
121
|
+
"behavior": {"type": "string", "const": "sensor_poll"},
|
|
122
|
+
"bus": {"type": "string", "enum": ["i2c", "adc", "spi"], "default": "i2c"},
|
|
123
|
+
"sensor": {"type": "string", "description": "Sensor label (e.g. imu, temperature)"},
|
|
124
|
+
"address": {"type": "string", "description": "I2C address or SPI CS index (e.g. 0x68)"},
|
|
125
|
+
"poll_interval_ms": {"type": "integer", "default": 100},
|
|
126
|
+
"ring_buf_entries": {"type": "integer", "default": 16},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
"interface_stack": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"required": ["id", "behavior", "stack", "required_nets"],
|
|
132
|
+
"properties": {
|
|
133
|
+
"id": {"type": "string"},
|
|
134
|
+
"behavior": {"type": "string", "const": "interface_stack"},
|
|
135
|
+
"stack": {"type": "string", "description": "Firmware stack or peripheral family, e.g. usb, qspi, pio"},
|
|
136
|
+
"library": {"type": "string", "description": "Implementation library or SDK binding, e.g. tinyusb, pico-sdk"},
|
|
137
|
+
"required_nets": {"type": "array", "items": {"type": "string"}},
|
|
138
|
+
"required_tests": {"type": "array", "items": {"type": "string"}},
|
|
139
|
+
"init_order": {"type": "integer", "default": 50},
|
|
140
|
+
"stack_size_bytes": {"type": "integer", "default": 1024},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def render_module(module: dict[str, Any]) -> ModuleOutput:
|
|
147
|
+
"""Dispatch to the appropriate behavior renderer."""
|
|
148
|
+
behavior = module.get("behavior")
|
|
149
|
+
renderer = _RENDERERS.get(behavior)
|
|
150
|
+
if renderer is None:
|
|
151
|
+
raise ValueError(f"Unknown firmware module behavior: {behavior!r}. Available: {sorted(_RENDERERS)}")
|
|
152
|
+
return renderer(module)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
__all__ = [
|
|
156
|
+
"ModuleOutput",
|
|
157
|
+
"render_module",
|
|
158
|
+
"BEHAVIOR_DESCRIPTIONS",
|
|
159
|
+
"BEHAVIOR_SCHEMAS",
|
|
160
|
+
"_RENDERERS",
|
|
161
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ModuleOutput:
|
|
8
|
+
id: str
|
|
9
|
+
behavior: str
|
|
10
|
+
c_source: str
|
|
11
|
+
h_source: str
|
|
12
|
+
dts_fragment: str | None = None
|
|
13
|
+
kconfig_flags: list[str] = field(default_factory=list)
|
|
14
|
+
stack_size_bytes: int = 2048
|
|
15
|
+
is_isr: bool = False
|