lockwire 0.2.0__tar.gz
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.
- lockwire-0.2.0/PKG-INFO +192 -0
- lockwire-0.2.0/README.md +160 -0
- lockwire-0.2.0/pyproject.toml +103 -0
- lockwire-0.2.0/src/lockwire/__init__.py +9 -0
- lockwire-0.2.0/src/lockwire/__main__.py +9 -0
- lockwire-0.2.0/src/lockwire/advice.py +119 -0
- lockwire-0.2.0/src/lockwire/api.py +1216 -0
- lockwire-0.2.0/src/lockwire/api_payload.py +985 -0
- lockwire-0.2.0/src/lockwire/bench.py +182 -0
- lockwire-0.2.0/src/lockwire/blame.py +100 -0
- lockwire-0.2.0/src/lockwire/catalog.py +100 -0
- lockwire-0.2.0/src/lockwire/catalog_seed.yaml +545 -0
- lockwire-0.2.0/src/lockwire/checks.py +3483 -0
- lockwire-0.2.0/src/lockwire/cli.py +1799 -0
- lockwire-0.2.0/src/lockwire/cli_line.py +73 -0
- lockwire-0.2.0/src/lockwire/constraints.py +313 -0
- lockwire-0.2.0/src/lockwire/copy.py +302 -0
- lockwire-0.2.0/src/lockwire/describe.py +682 -0
- lockwire-0.2.0/src/lockwire/edit.py +2098 -0
- lockwire-0.2.0/src/lockwire/emit_ts.py +150 -0
- lockwire-0.2.0/src/lockwire/export_mhs.py +115 -0
- lockwire-0.2.0/src/lockwire/factdiff.py +254 -0
- lockwire-0.2.0/src/lockwire/forces.py +334 -0
- lockwire-0.2.0/src/lockwire/gitref.py +188 -0
- lockwire-0.2.0/src/lockwire/hw.py +388 -0
- lockwire-0.2.0/src/lockwire/mcp_server.py +276 -0
- lockwire-0.2.0/src/lockwire/mechanism.py +379 -0
- lockwire-0.2.0/src/lockwire/mesh_dae.py +555 -0
- lockwire-0.2.0/src/lockwire/mesh_simple.py +198 -0
- lockwire-0.2.0/src/lockwire/page.py +1485 -0
- lockwire-0.2.0/src/lockwire/plan.py +849 -0
- lockwire-0.2.0/src/lockwire/plan_payload.py +151 -0
- lockwire-0.2.0/src/lockwire/plugins.py +70 -0
- lockwire-0.2.0/src/lockwire/report.py +848 -0
- lockwire-0.2.0/src/lockwire/report_html.py +707 -0
- lockwire-0.2.0/src/lockwire/report_json.py +168 -0
- lockwire-0.2.0/src/lockwire/review_draft.py +315 -0
- lockwire-0.2.0/src/lockwire/scan.py +732 -0
- lockwire-0.2.0/src/lockwire/scene.py +535 -0
- lockwire-0.2.0/src/lockwire/scene_import.py +309 -0
- lockwire-0.2.0/src/lockwire/scene_urdf.py +750 -0
- lockwire-0.2.0/src/lockwire/scene_view.py +376 -0
- lockwire-0.2.0/src/lockwire/schema.py +3616 -0
- lockwire-0.2.0/src/lockwire/sdf.py +240 -0
- lockwire-0.2.0/src/lockwire/serve.py +1256 -0
- lockwire-0.2.0/src/lockwire/start_operation.py +92 -0
- lockwire-0.2.0/src/lockwire/statics.py +77 -0
- lockwire-0.2.0/src/lockwire/units.py +115 -0
- lockwire-0.2.0/src/lockwire/urdf.py +348 -0
- lockwire-0.2.0/src/lockwire/vcs.py +402 -0
- lockwire-0.2.0/src/lockwire/whatif.py +161 -0
lockwire-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lockwire
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: The physical-assumption contract for robots: physics lockfile + preflight gate
|
|
5
|
+
Keywords: robotics,urdf,ros,preflight,lockfile,ci,hardware,drone
|
|
6
|
+
Author: Shivansh
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Intended Audience :: Manufacturing
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Requires-Dist: numpy>=2.5.1
|
|
18
|
+
Requires-Dist: pydantic>=2.13.4
|
|
19
|
+
Requires-Dist: rich>=15.0.0
|
|
20
|
+
Requires-Dist: ruamel-yaml>=0.19.1
|
|
21
|
+
Requires-Dist: typer>=0.26.8
|
|
22
|
+
Requires-Dist: mcp>=2.1.1 ; extra == 'mcp'
|
|
23
|
+
Requires-Dist: fastapi>=0.141.1 ; extra == 'serve'
|
|
24
|
+
Requires-Dist: uvicorn>=0.52.4 ; extra == 'serve'
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Project-URL: Homepage, https://github.com/Shivansh2703/lockwire
|
|
27
|
+
Project-URL: Repository, https://github.com/Shivansh2703/lockwire
|
|
28
|
+
Project-URL: Issues, https://github.com/Shivansh2703/lockwire/issues
|
|
29
|
+
Provides-Extra: mcp
|
|
30
|
+
Provides-Extra: serve
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# lockwire
|
|
34
|
+
|
|
35
|
+
**Find out your robot won't work before you buy the parts.**
|
|
36
|
+
|
|
37
|
+
Every robot is a stack of numbers from a dozen datasheets: what each part weighs, what each power rail can supply, how much the bus can carry, how much torque each joint gets. Lockwire keeps those numbers in one file and tells you the moment they stop adding up. Use it while you're still planning a build, then keep it in CI so a part swap can't quietly break it later.
|
|
38
|
+
|
|
39
|
+
Prefer clicking to typing? The free desktop app does the same visually: Open, Design, Declare, Plan, Check.
|
|
40
|
+
|
|
41
|
+
## Plan a robot from scratch
|
|
42
|
+
|
|
43
|
+
Start from a URDF (one line is enough), add the parts you're thinking of buying, and check:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install "git+https://github.com/Shivansh2703/lockwire@v0.2.0" # not on PyPI
|
|
47
|
+
echo '<robot name="rover"><link name="base_link"/></robot>' > robot.urdf
|
|
48
|
+
lockwire start robot.urdf
|
|
49
|
+
lockwire add rail lockwire.yaml --id rail.5v --voltage "5 V" --capacity "15 W" --source "UBEC datasheet: 5 V 3 A" --witness Sam
|
|
50
|
+
lockwire add device lockwire.yaml --id dev.pi5 --family fam.pi --model mod.pi5 --rail rail.5v --draw "12 W" --mass "46 g" --mount-frame base_link --owner you --source "Raspberry Pi 5 product brief" --witness Sam
|
|
51
|
+
lockwire add device lockwire.yaml --id dev.servo --family fam.servo --model mod.mg996r --rail rail.5v --draw "6 W" --mass "55 g" --mount-frame base_link --owner you --source "MG996R datasheet" --witness Sam
|
|
52
|
+
lockwire add budget lockwire.yaml --id bud.5v --kind rail_power --rail rail.5v --limit "15 W" --owner you --source "UBEC datasheet" --witness Sam
|
|
53
|
+
lockwire check lockwire.yaml
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`--witness` is the person who read the number off the datasheet; use a real name, not "you".
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
VIOLATION budgets/bud.5v — observed 18.00 W, expected <= 15.00 W (Δ +3.00 W (+20%))
|
|
60
|
+
dependent: dev.pi5, dev.servo
|
|
61
|
+
owner: you
|
|
62
|
+
source: UBEC datasheet: 5 V 3 A
|
|
63
|
+
action: shed load on rail 'rail.5v' or raise its capacity with provenance
|
|
64
|
+
warning model/links/base_link/mass — observed zero or missing mass, expected a positive mass with units
|
|
65
|
+
…
|
|
66
|
+
scored 2: 0 add up, 1 do not · 0 not scored
|
|
67
|
+
|
|
68
|
+
NO-GO — 1 violation(s), 1 warning(s)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The check exits 1. A Pi 5 and one servo on a common 3 A regulator: caught before you ordered either. Swap in a 5 A regulator and it passes.
|
|
72
|
+
|
|
73
|
+
## A real-world example: the X500 drone
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
git clone https://github.com/Shivansh2703/lockwire && cd lockwire # the X500 example lives in the repo, not the wheel
|
|
77
|
+
lockwire check tests/fixtures/x500/contract.yaml
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Output (trimmed):
|
|
81
|
+
|
|
82
|
+
```console
|
|
83
|
+
$ lockwire check tests/fixtures/x500/contract.yaml
|
|
84
|
+
VIOLATION budgets/bud.power.pm02-5v — observed 35.20 W (draw mode: nominal), expected <= 15.60 W (Δ +19.60 W (+126%))
|
|
85
|
+
dependent: dev.camera.oak-d-lite, dev.companion.jetson-orin-nano, dev.radio.rfd900x
|
|
86
|
+
owner: power-integration
|
|
87
|
+
action: shed load on rail 'rail.pm02-5v' or raise its capacity with provenance
|
|
88
|
+
VIOLATION budgets/bud.power.pm02-5v.7w-mode — observed 17.20 W (draw mode: 7W), expected <= 15.60 W (Δ +1.60 W (+10%))
|
|
89
|
+
… (same rail, Jetson in its 7 W mode: the radio's transmit peak still tips it over)
|
|
90
|
+
not scored devices/dev.camera.oak-d-lite/physical/mass — observed mass not declared, expected a positive mass with units
|
|
91
|
+
… (more not-scored lines: things the lockfile does not declare yet, listed, never hidden)
|
|
92
|
+
scored 10: 8 add up, 2 do not · 9 not scored
|
|
93
|
+
|
|
94
|
+
NO-GO — 2 violation(s), 0 warning(s)
|
|
95
|
+
$ echo $?
|
|
96
|
+
1
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## What just happened
|
|
100
|
+
|
|
101
|
+
A Holybro PM02 power module publishes a regulated 5 V rail at 3 A: 15.6 W. An NVIDIA Jetson
|
|
102
|
+
Orin Nano publishes a 25 W power mode at 5 V: 5 A. Put a camera on the same rail and the
|
|
103
|
+
published parts **cannot power the published companion computer**. This is a custom
|
|
104
|
+
integration of published parts, and the two vendors' own numbers still do not add up.
|
|
105
|
+
|
|
106
|
+
Nobody printed "this build is broken". Nobody printed "5 A" either: that is 25 W / 5 V, a
|
|
107
|
+
quotient of figures published on different pages. The records disagree with each other, and
|
|
108
|
+
you find out before you apply power. Give the Jetson its own regulator
|
|
109
|
+
(`tests/fixtures/x500/contract_corrected.yaml`) and the same check passes.
|
|
110
|
+
|
|
111
|
+
## Your own robot
|
|
112
|
+
|
|
113
|
+
`lockwire start` reads links, joints, masses and the structure hash from your URDF or SDF. A fresh
|
|
114
|
+
lockfile knows the frame and nothing else; fill in devices, rails and buses as you go, and
|
|
115
|
+
`check` lists what it still could not check.
|
|
116
|
+
|
|
117
|
+
A lockfile whose `schema_version` this lockwire does not recognise is refused before any check runs.
|
|
118
|
+
`check` prints `REFUSED — contract declares schema_version '0.99'; this lockwire supports 0.2, 0.3, 0.4` and exits 2.
|
|
119
|
+
A lockfile at an older supported version is accepted as-is, with no automatic upgrade.
|
|
120
|
+
|
|
121
|
+
Re-running `lockwire start --force` on the same URDF is destructive:
|
|
122
|
+
re-declare every fact in the lockfile by hand, because no command carries those facts forward.
|
|
123
|
+
Any field you hand-edited (including `robot.name`) is silently overwritten back to the URDF-derived value.
|
|
124
|
+
|
|
125
|
+
## Exit codes
|
|
126
|
+
|
|
127
|
+
| command | 0 | 1 | 2 | 3 |
|
|
128
|
+
|---|---|---|---|---|
|
|
129
|
+
| `check` | `GO`, `GO with notes` | `NO-GO` | a contract that could not be found, read, or parsed | `NOT SCORED` (nothing compared) |
|
|
130
|
+
| `start` | the contract was written (or an existing one kept); no page is written | the URDF/SDF could not be parsed, or the contract could not be read or written | — | — |
|
|
131
|
+
|
|
132
|
+
`NOT SCORED` (exit 3) means no comparison had both operands: it is not a pass and not a failure.
|
|
133
|
+
The `scored N: …` line above the verdict is where a script should look.
|
|
134
|
+
|
|
135
|
+
## In CI
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
- uses: actions/checkout@v4
|
|
139
|
+
- uses: Shivansh2703/lockwire@v0 # composite action: installs lockwire, runs the check, uploads an HTML report
|
|
140
|
+
with:
|
|
141
|
+
contract: lockwire.yaml
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Inputs: `contract` (default `lockwire.yaml`), `version` (default latest), `python-version` (default 3.12).
|
|
145
|
+
Or just `pip install "git+https://github.com/Shivansh2703/lockwire@v0.2.0" && lockwire check lockwire.yaml`.
|
|
146
|
+
|
|
147
|
+
## Pre-commit
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
repos:
|
|
151
|
+
- repo: https://github.com/Shivansh2703/lockwire
|
|
152
|
+
rev: v0.2.0
|
|
153
|
+
hooks:
|
|
154
|
+
- id: lockwire-check
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Runs `lockwire check` (on `lockwire.yaml` in the repo root) whenever a lockfile changes.
|
|
158
|
+
|
|
159
|
+
## What it checks
|
|
160
|
+
|
|
161
|
+
Each check compares **two separately declared figures**, each with its own provenance line.
|
|
162
|
+
It shows who said what; it cannot verify that a datasheet is true.
|
|
163
|
+
|
|
164
|
+
| check | source A | source B |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| rail power budget | device draw (datasheet) | rail capacity (power module datasheet) |
|
|
167
|
+
| rail tree rollup | child rail draw / efficiency | parent rail capacity |
|
|
168
|
+
| torque budget | joint effort limit (URDF) | actuator continuous torque x gear ratio |
|
|
169
|
+
| bus bandwidth | message rate x payload | bus bitrate x utilisation ceiling |
|
|
170
|
+
| mass budget | per-device mass | declared total |
|
|
171
|
+
| endurance | pack energy | summed draw |
|
|
172
|
+
| voltage compatibility | rail voltage | device input voltage range |
|
|
173
|
+
| reference integrity | every `bus_ref`, `mount_frame`, dependency edge | the thing it points at |
|
|
174
|
+
|
|
175
|
+
No physics simulation, no machine learning, no network. The answer is the same twice.
|
|
176
|
+
|
|
177
|
+
## Free and Pro
|
|
178
|
+
|
|
179
|
+
- **Free forever (this repo):** the lockfile, all deterministic checks including
|
|
180
|
+
power/thermal/bus/link/torque budgets, edit / describe / plan / what-if, the MCP server, the
|
|
181
|
+
HTML report, and the desktop planning app (Open, Design, Declare, Plan, Check).
|
|
182
|
+
- **Lockwire Pro** adds the powered-on preflight gate (Test, Run), recording analysis
|
|
183
|
+
(rosbag2 / ULog), and health.
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Python 3.12 or newer; runtime dependencies are
|
|
188
|
+
pydantic, typer, rich, ruamel.yaml and numpy.
|
|
189
|
+
|
|
190
|
+
## Licence
|
|
191
|
+
|
|
192
|
+
Apache-2.0. See [LICENSE](LICENSE).
|
lockwire-0.2.0/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# lockwire
|
|
2
|
+
|
|
3
|
+
**Find out your robot won't work before you buy the parts.**
|
|
4
|
+
|
|
5
|
+
Every robot is a stack of numbers from a dozen datasheets: what each part weighs, what each power rail can supply, how much the bus can carry, how much torque each joint gets. Lockwire keeps those numbers in one file and tells you the moment they stop adding up. Use it while you're still planning a build, then keep it in CI so a part swap can't quietly break it later.
|
|
6
|
+
|
|
7
|
+
Prefer clicking to typing? The free desktop app does the same visually: Open, Design, Declare, Plan, Check.
|
|
8
|
+
|
|
9
|
+
## Plan a robot from scratch
|
|
10
|
+
|
|
11
|
+
Start from a URDF (one line is enough), add the parts you're thinking of buying, and check:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install "git+https://github.com/Shivansh2703/lockwire@v0.2.0" # not on PyPI
|
|
15
|
+
echo '<robot name="rover"><link name="base_link"/></robot>' > robot.urdf
|
|
16
|
+
lockwire start robot.urdf
|
|
17
|
+
lockwire add rail lockwire.yaml --id rail.5v --voltage "5 V" --capacity "15 W" --source "UBEC datasheet: 5 V 3 A" --witness Sam
|
|
18
|
+
lockwire add device lockwire.yaml --id dev.pi5 --family fam.pi --model mod.pi5 --rail rail.5v --draw "12 W" --mass "46 g" --mount-frame base_link --owner you --source "Raspberry Pi 5 product brief" --witness Sam
|
|
19
|
+
lockwire add device lockwire.yaml --id dev.servo --family fam.servo --model mod.mg996r --rail rail.5v --draw "6 W" --mass "55 g" --mount-frame base_link --owner you --source "MG996R datasheet" --witness Sam
|
|
20
|
+
lockwire add budget lockwire.yaml --id bud.5v --kind rail_power --rail rail.5v --limit "15 W" --owner you --source "UBEC datasheet" --witness Sam
|
|
21
|
+
lockwire check lockwire.yaml
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`--witness` is the person who read the number off the datasheet; use a real name, not "you".
|
|
25
|
+
|
|
26
|
+
```console
|
|
27
|
+
VIOLATION budgets/bud.5v — observed 18.00 W, expected <= 15.00 W (Δ +3.00 W (+20%))
|
|
28
|
+
dependent: dev.pi5, dev.servo
|
|
29
|
+
owner: you
|
|
30
|
+
source: UBEC datasheet: 5 V 3 A
|
|
31
|
+
action: shed load on rail 'rail.5v' or raise its capacity with provenance
|
|
32
|
+
warning model/links/base_link/mass — observed zero or missing mass, expected a positive mass with units
|
|
33
|
+
…
|
|
34
|
+
scored 2: 0 add up, 1 do not · 0 not scored
|
|
35
|
+
|
|
36
|
+
NO-GO — 1 violation(s), 1 warning(s)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The check exits 1. A Pi 5 and one servo on a common 3 A regulator: caught before you ordered either. Swap in a 5 A regulator and it passes.
|
|
40
|
+
|
|
41
|
+
## A real-world example: the X500 drone
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/Shivansh2703/lockwire && cd lockwire # the X500 example lives in the repo, not the wheel
|
|
45
|
+
lockwire check tests/fixtures/x500/contract.yaml
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Output (trimmed):
|
|
49
|
+
|
|
50
|
+
```console
|
|
51
|
+
$ lockwire check tests/fixtures/x500/contract.yaml
|
|
52
|
+
VIOLATION budgets/bud.power.pm02-5v — observed 35.20 W (draw mode: nominal), expected <= 15.60 W (Δ +19.60 W (+126%))
|
|
53
|
+
dependent: dev.camera.oak-d-lite, dev.companion.jetson-orin-nano, dev.radio.rfd900x
|
|
54
|
+
owner: power-integration
|
|
55
|
+
action: shed load on rail 'rail.pm02-5v' or raise its capacity with provenance
|
|
56
|
+
VIOLATION budgets/bud.power.pm02-5v.7w-mode — observed 17.20 W (draw mode: 7W), expected <= 15.60 W (Δ +1.60 W (+10%))
|
|
57
|
+
… (same rail, Jetson in its 7 W mode: the radio's transmit peak still tips it over)
|
|
58
|
+
not scored devices/dev.camera.oak-d-lite/physical/mass — observed mass not declared, expected a positive mass with units
|
|
59
|
+
… (more not-scored lines: things the lockfile does not declare yet, listed, never hidden)
|
|
60
|
+
scored 10: 8 add up, 2 do not · 9 not scored
|
|
61
|
+
|
|
62
|
+
NO-GO — 2 violation(s), 0 warning(s)
|
|
63
|
+
$ echo $?
|
|
64
|
+
1
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What just happened
|
|
68
|
+
|
|
69
|
+
A Holybro PM02 power module publishes a regulated 5 V rail at 3 A: 15.6 W. An NVIDIA Jetson
|
|
70
|
+
Orin Nano publishes a 25 W power mode at 5 V: 5 A. Put a camera on the same rail and the
|
|
71
|
+
published parts **cannot power the published companion computer**. This is a custom
|
|
72
|
+
integration of published parts, and the two vendors' own numbers still do not add up.
|
|
73
|
+
|
|
74
|
+
Nobody printed "this build is broken". Nobody printed "5 A" either: that is 25 W / 5 V, a
|
|
75
|
+
quotient of figures published on different pages. The records disagree with each other, and
|
|
76
|
+
you find out before you apply power. Give the Jetson its own regulator
|
|
77
|
+
(`tests/fixtures/x500/contract_corrected.yaml`) and the same check passes.
|
|
78
|
+
|
|
79
|
+
## Your own robot
|
|
80
|
+
|
|
81
|
+
`lockwire start` reads links, joints, masses and the structure hash from your URDF or SDF. A fresh
|
|
82
|
+
lockfile knows the frame and nothing else; fill in devices, rails and buses as you go, and
|
|
83
|
+
`check` lists what it still could not check.
|
|
84
|
+
|
|
85
|
+
A lockfile whose `schema_version` this lockwire does not recognise is refused before any check runs.
|
|
86
|
+
`check` prints `REFUSED — contract declares schema_version '0.99'; this lockwire supports 0.2, 0.3, 0.4` and exits 2.
|
|
87
|
+
A lockfile at an older supported version is accepted as-is, with no automatic upgrade.
|
|
88
|
+
|
|
89
|
+
Re-running `lockwire start --force` on the same URDF is destructive:
|
|
90
|
+
re-declare every fact in the lockfile by hand, because no command carries those facts forward.
|
|
91
|
+
Any field you hand-edited (including `robot.name`) is silently overwritten back to the URDF-derived value.
|
|
92
|
+
|
|
93
|
+
## Exit codes
|
|
94
|
+
|
|
95
|
+
| command | 0 | 1 | 2 | 3 |
|
|
96
|
+
|---|---|---|---|---|
|
|
97
|
+
| `check` | `GO`, `GO with notes` | `NO-GO` | a contract that could not be found, read, or parsed | `NOT SCORED` (nothing compared) |
|
|
98
|
+
| `start` | the contract was written (or an existing one kept); no page is written | the URDF/SDF could not be parsed, or the contract could not be read or written | — | — |
|
|
99
|
+
|
|
100
|
+
`NOT SCORED` (exit 3) means no comparison had both operands: it is not a pass and not a failure.
|
|
101
|
+
The `scored N: …` line above the verdict is where a script should look.
|
|
102
|
+
|
|
103
|
+
## In CI
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
- uses: actions/checkout@v4
|
|
107
|
+
- uses: Shivansh2703/lockwire@v0 # composite action: installs lockwire, runs the check, uploads an HTML report
|
|
108
|
+
with:
|
|
109
|
+
contract: lockwire.yaml
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Inputs: `contract` (default `lockwire.yaml`), `version` (default latest), `python-version` (default 3.12).
|
|
113
|
+
Or just `pip install "git+https://github.com/Shivansh2703/lockwire@v0.2.0" && lockwire check lockwire.yaml`.
|
|
114
|
+
|
|
115
|
+
## Pre-commit
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
repos:
|
|
119
|
+
- repo: https://github.com/Shivansh2703/lockwire
|
|
120
|
+
rev: v0.2.0
|
|
121
|
+
hooks:
|
|
122
|
+
- id: lockwire-check
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Runs `lockwire check` (on `lockwire.yaml` in the repo root) whenever a lockfile changes.
|
|
126
|
+
|
|
127
|
+
## What it checks
|
|
128
|
+
|
|
129
|
+
Each check compares **two separately declared figures**, each with its own provenance line.
|
|
130
|
+
It shows who said what; it cannot verify that a datasheet is true.
|
|
131
|
+
|
|
132
|
+
| check | source A | source B |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| rail power budget | device draw (datasheet) | rail capacity (power module datasheet) |
|
|
135
|
+
| rail tree rollup | child rail draw / efficiency | parent rail capacity |
|
|
136
|
+
| torque budget | joint effort limit (URDF) | actuator continuous torque x gear ratio |
|
|
137
|
+
| bus bandwidth | message rate x payload | bus bitrate x utilisation ceiling |
|
|
138
|
+
| mass budget | per-device mass | declared total |
|
|
139
|
+
| endurance | pack energy | summed draw |
|
|
140
|
+
| voltage compatibility | rail voltage | device input voltage range |
|
|
141
|
+
| reference integrity | every `bus_ref`, `mount_frame`, dependency edge | the thing it points at |
|
|
142
|
+
|
|
143
|
+
No physics simulation, no machine learning, no network. The answer is the same twice.
|
|
144
|
+
|
|
145
|
+
## Free and Pro
|
|
146
|
+
|
|
147
|
+
- **Free forever (this repo):** the lockfile, all deterministic checks including
|
|
148
|
+
power/thermal/bus/link/torque budgets, edit / describe / plan / what-if, the MCP server, the
|
|
149
|
+
HTML report, and the desktop planning app (Open, Design, Declare, Plan, Check).
|
|
150
|
+
- **Lockwire Pro** adds the powered-on preflight gate (Test, Run), recording analysis
|
|
151
|
+
(rosbag2 / ULog), and health.
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Python 3.12 or newer; runtime dependencies are
|
|
156
|
+
pydantic, typer, rich, ruamel.yaml and numpy.
|
|
157
|
+
|
|
158
|
+
## Licence
|
|
159
|
+
|
|
160
|
+
Apache-2.0. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "lockwire"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "The physical-assumption contract for robots: physics lockfile + preflight gate"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Shivansh" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
keywords = ["robotics", "urdf", "ros", "preflight", "lockfile", "ci", "hardware", "drone"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"License :: OSI Approved :: Apache Software License",
|
|
15
|
+
"Intended Audience :: Manufacturing",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"numpy>=2.5.1",
|
|
25
|
+
"pydantic>=2.13.4",
|
|
26
|
+
"rich>=15.0.0",
|
|
27
|
+
"ruamel-yaml>=0.19.1",
|
|
28
|
+
"typer>=0.26.8",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
# Serving lockwire over MCP to an agent. Optional for the same reason as `ros2bag`:
|
|
33
|
+
# a plain install must stay lean, and the SDK is only needed by `lockwire mcp`.
|
|
34
|
+
mcp = [
|
|
35
|
+
"mcp>=2.1.1",
|
|
36
|
+
]
|
|
37
|
+
# The Electron app's sidecar: `lockwire serve` over local HTTP. Optional for the
|
|
38
|
+
# same reason as `desktop`/`ros2bag`/`mcp` — a plain CLI install stays lean; only the
|
|
39
|
+
# packaged app build and anyone running the sidecar by hand need these. Versions are
|
|
40
|
+
# today's PyPI current stable (measured, E1-a).
|
|
41
|
+
serve = [
|
|
42
|
+
"fastapi>=0.141.1",
|
|
43
|
+
"uvicorn>=0.52.4",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/Shivansh2703/lockwire"
|
|
48
|
+
Repository = "https://github.com/Shivansh2703/lockwire"
|
|
49
|
+
Issues = "https://github.com/Shivansh2703/lockwire/issues"
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
lockwire = "lockwire:main"
|
|
53
|
+
|
|
54
|
+
[build-system]
|
|
55
|
+
requires = ["uv_build>=0.11.26,<0.12.0"]
|
|
56
|
+
build-backend = "uv_build"
|
|
57
|
+
|
|
58
|
+
[tool.uv.build-backend]
|
|
59
|
+
|
|
60
|
+
[dependency-groups]
|
|
61
|
+
dev = [
|
|
62
|
+
"mcp>=2.1.1",
|
|
63
|
+
# tests/test_serve.py needs the sidecar (`lockwire serve`) importable and
|
|
64
|
+
# runnable as a subprocess. Same reasoning as `mcp`/`mcap` above: CI's
|
|
65
|
+
# `test` job runs a plain `uv sync --dev`, so without these here that
|
|
66
|
+
# whole test file would silently never run. Dev-only: the runtime `serve`
|
|
67
|
+
# extra above stays optional so a plain install stays lean.
|
|
68
|
+
"fastapi>=0.141.1",
|
|
69
|
+
"uvicorn>=0.52.4",
|
|
70
|
+
"httpx>=0.28.1",
|
|
71
|
+
# Freezing the sidecar binary: `uv run pyinstaller packaging/lockwire.spec`. Dev-only —
|
|
72
|
+
# it is a build tool, never imported at runtime — and `tests/test_cli.py` imports it to
|
|
73
|
+
# anchor the uvicorn hidden-import test against uvicorn's real module list (box e1-d2).
|
|
74
|
+
"pyinstaller>=6.21.0",
|
|
75
|
+
"pytest>=9.1.1",
|
|
76
|
+
"pytest-xdist>=3.6.1",
|
|
77
|
+
"ruff>=0.15.20,<0.16", # 0.17 changes default rules
|
|
78
|
+
"pytest-cov>=7.1.0",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[tool.ruff]
|
|
82
|
+
# This codebase is written to ~100 columns: schema.py and checks.py carry aligned
|
|
83
|
+
# trailing comments naming each field's consumer, and wrapping at ruff's default 88
|
|
84
|
+
# splits those annotations off the line they annotate. E501 is not in ruff's default
|
|
85
|
+
# rule set, so before this block the two documented commands disagreed — `ruff check`
|
|
86
|
+
# passed while `ruff format` wanted to rewrite 12 files. At 100 the sweep is a net
|
|
87
|
+
# reduction (83 insertions / 105 deletions); at 88 it was +219/-80.
|
|
88
|
+
line-length = 100
|
|
89
|
+
|
|
90
|
+
# measure_stacked_gate.py bootstraps sys.path before imports (B76) — E402 is intentional there.
|
|
91
|
+
[tool.ruff.lint.per-file-ignores]
|
|
92
|
+
"scripts/measure_stacked_gate.py" = ["E402"]
|
|
93
|
+
|
|
94
|
+
[tool.pytest.ini_options]
|
|
95
|
+
pythonpath = ["."]
|
|
96
|
+
testpaths = ["tests"]
|
|
97
|
+
# chrome: drives real headless Chrome (subprocess spawn). scripts/pytest_slot.sh
|
|
98
|
+
# excludes this marker from every non-exclusive (targeted) run so workers' test
|
|
99
|
+
# runs never spawn Chrome processes; the cadence Check (--exclusive) still runs
|
|
100
|
+
# them. Registered here so --strict-markers, if ever turned on, does not bite.
|
|
101
|
+
markers = [
|
|
102
|
+
"chrome: drives real headless Chrome; excluded from targeted runs by scripts/pytest_slot.sh",
|
|
103
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""The alternatives half of the advice sentence.
|
|
2
|
+
|
|
3
|
+
``check_budgets`` (``checks.py``) already knows a rail_power budget is over its
|
|
4
|
+
limit; this module answers the second half of the sentence — "what else could
|
|
5
|
+
carry it" — headroom on every OTHER rail_power budget, plus a projected
|
|
6
|
+
regulator suggestion. It computes nothing about rail membership itself:
|
|
7
|
+
Reuse means ``checks.power_rollup``'s own return values (rail totals, the
|
|
8
|
+
per-rail unknown-consumer sets) are passed in as plain data, never re-walked.
|
|
9
|
+
This module never imports ``checks`` — the call only ever goes the other way,
|
|
10
|
+
so there is no cycle.
|
|
11
|
+
|
|
12
|
+
Every number carries its ``kind`` (a catalog or projected
|
|
13
|
+
figure is shown only with its own visual mark and word, never in the same
|
|
14
|
+
style as a measured one): headroom is ``derived-by-<check>`` — the same
|
|
15
|
+
rollup ``check_budgets`` itself scores against; a suggested regulator rating
|
|
16
|
+
is ``projected``, a number this product is choosing, not measuring.
|
|
17
|
+
|
|
18
|
+
**Raising the over-budget rail's own declared capacity is never offered**
|
|
19
|
+
: that clears the finding by moving the limit rather than
|
|
20
|
+
the draw, which leaves a standing mark. Offering it as
|
|
21
|
+
advice would make the product recommend the exact thing that mark exists to flag.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
27
|
+
|
|
28
|
+
from lockwire.schema import Contract
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Alternative(BaseModel):
|
|
32
|
+
"""One remedy suggestion, its number, and the kind that number carries."""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(extra="forbid")
|
|
35
|
+
|
|
36
|
+
text: str
|
|
37
|
+
value: float
|
|
38
|
+
unit: str
|
|
39
|
+
kind: str # "derived-by-<check>" | "projected" | "none"
|
|
40
|
+
#: The word for a ``projected`` figure. Required when ``kind ==
|
|
41
|
+
#: "projected"`` — a projected figure reaching a door carries its mark and
|
|
42
|
+
#: its word, not a corrected sentence saying it is unenforced.
|
|
43
|
+
mark: str | None = None
|
|
44
|
+
|
|
45
|
+
@model_validator(mode="after")
|
|
46
|
+
def _carries_its_word(self) -> Alternative:
|
|
47
|
+
if self.kind == "projected" and not self.mark:
|
|
48
|
+
raise ValueError("a 'projected' number must carry its own word, e.g. mark='projected'")
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_rail_alternatives(
|
|
53
|
+
contract: Contract,
|
|
54
|
+
*,
|
|
55
|
+
over_rail_id: str,
|
|
56
|
+
deficit_w: float,
|
|
57
|
+
rail_totals: dict[str, float],
|
|
58
|
+
rail_unknown: dict[str, set[str]],
|
|
59
|
+
check_name: str = "check_budgets",
|
|
60
|
+
) -> list[Alternative]:
|
|
61
|
+
"""Alternatives for a rail_power budget over its limit on ``over_rail_id``.
|
|
62
|
+
|
|
63
|
+
``rail_totals``/``rail_unknown`` must come from ``checks.power_rollup`` run
|
|
64
|
+
against the same contract and draw mode as the violation being explained —
|
|
65
|
+
the shared builder that every call site (and every mutation bite) goes
|
|
66
|
+
through, so a broken kind anywhere breaks every caller alike.
|
|
67
|
+
"""
|
|
68
|
+
alternatives: list[Alternative] = []
|
|
69
|
+
for budget in contract.budgets:
|
|
70
|
+
if budget.kind != "rail_power" or budget.rail in (None, over_rail_id):
|
|
71
|
+
continue
|
|
72
|
+
if rail_unknown.get(budget.rail):
|
|
73
|
+
# a rail with any consumer of unknown draw cannot be summed,
|
|
74
|
+
# so it cannot be claimed to have headroom either.
|
|
75
|
+
continue
|
|
76
|
+
used = rail_totals.get(budget.rail, 0.0)
|
|
77
|
+
headroom_w = budget.limit.base() - used
|
|
78
|
+
if headroom_w > 0.0:
|
|
79
|
+
alternatives.append(
|
|
80
|
+
Alternative(
|
|
81
|
+
text=(
|
|
82
|
+
f"move the device to rail {budget.rail!r}, which has "
|
|
83
|
+
f"{headroom_w:.2f} W free"
|
|
84
|
+
),
|
|
85
|
+
value=headroom_w,
|
|
86
|
+
unit="W",
|
|
87
|
+
kind=f"derived-by-{check_name}",
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
over_rail = contract.rail(over_rail_id)
|
|
92
|
+
source_rail_id = "battery"
|
|
93
|
+
if over_rail is not None and over_rail.upstream is not None:
|
|
94
|
+
source_rail_id = over_rail.upstream
|
|
95
|
+
rating_w = max(deficit_w, 0.0)
|
|
96
|
+
if rating_w > 0.0:
|
|
97
|
+
alternatives.append(
|
|
98
|
+
Alternative(
|
|
99
|
+
text=f"add a {rating_w:.2f} W regulator fed from rail {source_rail_id!r}",
|
|
100
|
+
value=rating_w,
|
|
101
|
+
unit="W",
|
|
102
|
+
kind="projected",
|
|
103
|
+
mark="projected",
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if not alternatives:
|
|
108
|
+
alternatives.append(
|
|
109
|
+
Alternative(
|
|
110
|
+
text=(
|
|
111
|
+
f"no other rail has headroom for {over_rail_id!r} and no regulator "
|
|
112
|
+
"rating could be projected"
|
|
113
|
+
),
|
|
114
|
+
value=0.0,
|
|
115
|
+
unit="W",
|
|
116
|
+
kind="none",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return alternatives
|