lockwire 0.2.0__tar.gz → 0.2.4__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.
Files changed (84) hide show
  1. {lockwire-0.2.0 → lockwire-0.2.4}/PKG-INFO +148 -34
  2. lockwire-0.2.4/README.md +272 -0
  3. {lockwire-0.2.0 → lockwire-0.2.4}/pyproject.toml +6 -4
  4. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/__init__.py +1 -1
  5. lockwire-0.2.4/src/lockwire/_filelock.py +38 -0
  6. lockwire-0.2.4/src/lockwire/advice.py +215 -0
  7. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/api.py +20 -0
  8. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/api_payload.py +59 -0
  9. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/bench.py +3 -1
  10. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/catalog_seed.yaml +233 -6
  11. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/checks.py +435 -35
  12. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/cli.py +234 -35
  13. lockwire-0.2.4/src/lockwire/confine.py +31 -0
  14. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/copy.py +69 -13
  15. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/describe.py +158 -46
  16. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/edit.py +25 -14
  17. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/emit_ts.py +5 -1
  18. lockwire-0.2.4/src/lockwire/examples/arm/LICENSE +201 -0
  19. lockwire-0.2.4/src/lockwire/examples/arm/NOTICE +5 -0
  20. lockwire-0.2.4/src/lockwire/examples/arm/contract.yaml +306 -0
  21. lockwire-0.2.4/src/lockwire/examples/arm/plan.yaml +9 -0
  22. lockwire-0.2.4/src/lockwire/examples/arm/scene.glb +0 -0
  23. lockwire-0.2.4/src/lockwire/examples/arm/starter.json +7 -0
  24. lockwire-0.2.4/src/lockwire/examples/drone/contract.yaml +617 -0
  25. lockwire-0.2.4/src/lockwire/examples/drone/plan.yaml +10 -0
  26. lockwire-0.2.4/src/lockwire/examples/drone/scene.glb +0 -0
  27. lockwire-0.2.4/src/lockwire/examples/drone/starter.json +7 -0
  28. lockwire-0.2.4/src/lockwire/examples/humanoid/NOTICE +26 -0
  29. lockwire-0.2.4/src/lockwire/examples/humanoid/contract.yaml +1685 -0
  30. lockwire-0.2.4/src/lockwire/examples/humanoid/plan.yaml +10 -0
  31. lockwire-0.2.4/src/lockwire/examples/humanoid/scene.glb +0 -0
  32. lockwire-0.2.4/src/lockwire/examples/humanoid/starter.json +1 -0
  33. lockwire-0.2.4/src/lockwire/examples/quadruped/NOTICE +32 -0
  34. lockwire-0.2.4/src/lockwire/examples/quadruped/contract.yaml +489 -0
  35. lockwire-0.2.4/src/lockwire/examples/quadruped/plan.yaml +5 -0
  36. lockwire-0.2.4/src/lockwire/examples/quadruped/scene.glb +0 -0
  37. lockwire-0.2.4/src/lockwire/examples/quadruped/starter.json +7 -0
  38. lockwire-0.2.4/src/lockwire/examples/rover/NOTICE +12 -0
  39. lockwire-0.2.4/src/lockwire/examples/rover/contract.yaml +320 -0
  40. lockwire-0.2.4/src/lockwire/examples/rover/plan.yaml +9 -0
  41. lockwire-0.2.4/src/lockwire/examples/rover/scene.glb +0 -0
  42. lockwire-0.2.4/src/lockwire/examples/rover/starter.json +1 -0
  43. lockwire-0.2.4/src/lockwire/examples/servo-bench.yaml +113 -0
  44. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/gitref.py +1 -3
  45. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/mcp_server.py +28 -6
  46. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/mechanism.py +25 -8
  47. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/mesh_dae.py +2 -1
  48. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/page.py +174 -46
  49. lockwire-0.2.4/src/lockwire/parts_list.py +164 -0
  50. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/plan.py +33 -8
  51. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/plan_payload.py +3 -0
  52. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/report.py +72 -6
  53. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/report_html.py +17 -0
  54. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/review_draft.py +1 -0
  55. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/scan.py +57 -22
  56. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/scene_import.py +2 -6
  57. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/scene_urdf.py +33 -15
  58. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/scene_view.py +21 -6
  59. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/schema.py +142 -50
  60. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/serve.py +98 -40
  61. lockwire-0.2.4/src/lockwire/start_operation.py +180 -0
  62. lockwire-0.2.4/src/lockwire/statics.py +284 -0
  63. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/urdf.py +94 -7
  64. lockwire-0.2.0/README.md +0 -160
  65. lockwire-0.2.0/src/lockwire/advice.py +0 -119
  66. lockwire-0.2.0/src/lockwire/start_operation.py +0 -92
  67. lockwire-0.2.0/src/lockwire/statics.py +0 -77
  68. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/__main__.py +0 -0
  69. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/blame.py +0 -0
  70. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/catalog.py +0 -0
  71. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/cli_line.py +0 -0
  72. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/constraints.py +0 -0
  73. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/export_mhs.py +0 -0
  74. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/factdiff.py +0 -0
  75. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/forces.py +0 -0
  76. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/hw.py +0 -0
  77. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/mesh_simple.py +0 -0
  78. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/plugins.py +0 -0
  79. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/report_json.py +0 -0
  80. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/scene.py +0 -0
  81. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/sdf.py +0 -0
  82. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/units.py +0 -0
  83. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/vcs.py +0 -0
  84. {lockwire-0.2.0 → lockwire-0.2.4}/src/lockwire/whatif.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lockwire
3
- Version: 0.2.0
4
- Summary: The physical-assumption contract for robots: physics lockfile + preflight gate
3
+ Version: 0.2.4
4
+ Summary: The physical-assumption contract for robots: checks that a robot's declared parts (power, weight, bus, torque) add up
5
5
  Keywords: robotics,urdf,ros,preflight,lockfile,ci,hardware,drone
6
6
  Author: Shivansh
7
7
  License-Expression: Apache-2.0
@@ -19,7 +19,9 @@ Requires-Dist: pydantic>=2.13.4
19
19
  Requires-Dist: rich>=15.0.0
20
20
  Requires-Dist: ruamel-yaml>=0.19.1
21
21
  Requires-Dist: typer>=0.26.8
22
- Requires-Dist: mcp>=2.1.1 ; extra == 'mcp'
22
+ Requires-Dist: xacro>=2.1.1
23
+ Requires-Dist: mcp>=2.2.0 ; extra == 'mcp'
24
+ Requires-Dist: pyjwt>=2.15 ; extra == 'mcp'
23
25
  Requires-Dist: fastapi>=0.141.1 ; extra == 'serve'
24
26
  Requires-Dist: uvicorn>=0.52.4 ; extra == 'serve'
25
27
  Requires-Python: >=3.12
@@ -30,45 +32,156 @@ Provides-Extra: mcp
30
32
  Provides-Extra: serve
31
33
  Description-Content-Type: text/markdown
32
34
 
33
- # lockwire
35
+ <p align="center">
36
+ <picture>
37
+ <source media="(prefers-color-scheme: dark)" srcset="docs/img/logo-dark.svg">
38
+ <img src="docs/img/logo-light.svg" alt="lockwire" height="64">
39
+ </picture>
40
+ </p>
34
41
 
35
42
  **Find out your robot won't work before you buy the parts.**
36
43
 
44
+ ![The drone in 3D with its power parts in red, and a NO-GO finding that the 5 V rail is overloaded](docs/img/hero.png)
45
+
37
46
  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
47
 
39
- Prefer clicking to typing? The free desktop app does the same visually: Open, Design, Declare, Plan, Check.
48
+ ## Try it in 30 seconds
49
+
50
+ Needs Python 3.12 or newer (the Python that ships with macOS is older).
51
+
52
+ ```bash
53
+ pipx install lockwire
54
+ lockwire start --example servo-bench
55
+ lockwire check
56
+ ```
57
+
58
+ ```console
59
+ VIOLATION budgets/bud.power.servo-5v — observed 32.10 W, expected <= 15.00 W (Δ +17.10 W (+114%))
60
+ dependent: dev.pi5, dev.servo1, dev.servo2, dev.servo3, dev.servo4
61
+ ...
62
+ NO-GO — 1 violation(s), 0 warning(s)
63
+ ```
64
+
65
+ A Raspberry Pi 5 and four servos on one 3 A regulator: caught before you ordered either.
40
66
 
41
- ## Plan a robot from scratch
67
+ ## Try it on your robot
42
68
 
43
- Start from a URDF (one line is enough), add the parts you're thinking of buying, and check:
69
+ Point it at your URDF and see what it flags:
44
70
 
45
71
  ```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
72
+ lockwire start path/to/robot.urdf
73
+ lockwire check
74
+ ```
75
+
76
+ No URDF? Start from the example and swap in your parts:
77
+
78
+ ```bash
79
+ lockwire start --example servo-bench
80
+ lockwire add device lockwire.yaml --id dev.servo5 --family fam.servo --model mod.mg996r --rail rail.servo-5v --draw "6 W" --mass "55 g" --owner you --source "MG996R datasheet" --witness Sam
81
+ lockwire check
82
+ ```
83
+
84
+ See `lockwire add --help` for rails, budgets and the rest.
85
+
86
+ ## Download the app
87
+
88
+ The free desktop app does the same visually. Get it from the [latest release](https://github.com/Shivansh2703/lockwire/releases/latest).
89
+
90
+ ![Open the app, click the Drone starter, and Lockwire shows the drone in 3D with a NO-GO on its 5 V power rail](docs/img/try-example.gif)
91
+
92
+ - **macOS (Apple silicon):** unzip, open Lockwire. The first open shows a warning because the app is not notarized: open System Settings, Privacy & Security, scroll down and click "Open Anyway".
93
+ - **Linux:** `chmod +x Lockwire-*.AppImage`, then run it. Ubuntu 22.04 needs `sudo apt install libfuse2`; Ubuntu 24.04 needs `sudo apt install libfuse2t64`.
94
+ - **Windows app:** not yet.
95
+
96
+ ## Other ways to use it
97
+
98
+ **Command line.** Make a lockfile and check it from a terminal.
99
+
100
+ ```
101
+ pipx install lockwire
102
+ ```
103
+
104
+ ```
105
+ lockwire start --example drone
106
+ ```
107
+
108
+ Starters: `lockwire start --example <name>` with drone, rover, arm, quadruped or humanoid. Add your own: see [CONTRIBUTING](CONTRIBUTING.md).
109
+
110
+ ```
53
111
  lockwire check lockwire.yaml
54
112
  ```
55
113
 
56
- `--witness` is the person who read the number off the datasheet; use a real name, not "you".
114
+ **Desktop app.** Download it above, click a New robot starter, or open your own lockfile.
57
115
 
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
116
+ ```
117
+ https://github.com/Shivansh2703/lockwire/releases/latest
118
+ ```
67
119
 
68
- NO-GO — 1 violation(s), 1 warning(s)
120
+ **GitHub Action.** Run the check on every push and keep an HTML report.
121
+
122
+ ```
123
+ - uses: actions/checkout@v4
124
+ - uses: Shivansh2703/lockwire@v0
125
+ with:
126
+ contract: lockwire.yaml
127
+ ```
128
+
129
+ **AI agents (MCP).** Let Claude Desktop, Claude Code or Cursor check your robot before it acts.
130
+
131
+ ```
132
+ pipx install "lockwire[mcp]"
133
+ ```
134
+
135
+ ```
136
+ claude mcp add lockwire -- lockwire mcp
137
+ ```
138
+
139
+ ```
140
+ {
141
+ "mcpServers": {
142
+ "lockwire": { "command": "lockwire", "args": ["mcp"] }
143
+ }
144
+ }
145
+ ```
146
+
147
+ Claude Code: the claude mcp add line. Claude Desktop: put the JSON in claude_desktop_config.json. Cursor: put the JSON in .cursor/mcp.json (or ~/.cursor/mcp.json). If the app cannot find lockwire, use the full path from which lockwire. Then ask: Check my robot's lockwire.yaml and explain the NO-GO.
148
+
149
+ **Agent plan and what-if.** Score an intended plan, or a proposed change, before anything executes.
150
+
151
+ ```
152
+ lockwire plan lockwire.yaml plan.yaml
153
+ ```
154
+
155
+ ```
156
+ lockwire what-if lockwire.yaml --change "devices/dev.onboard-computer/physical/mass=300 g" --source "my scale" --witness me
157
+ ```
158
+
159
+ plan.yaml holds steps, for example: steps: [{kind: rail_load, rail: rail.supply-5v, draw: {value: 10, unit: W}}]
160
+
161
+ **Python library.** Call the same check from your own script.
162
+
163
+ ```
164
+ import lockwire.api as lw
165
+ print(lw.check("lockwire.yaml")["verdict"])
69
166
  ```
70
167
 
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.
168
+ **ROS, preflight and the local API.** The go/no-go gate for a powered-on robot, ROS and the local server.
169
+
170
+ Part of Lockwire Pro.
171
+
172
+ ### Features
173
+
174
+ - **Check:** See which numbers do not add up. `lockwire check lockwire.yaml`
175
+ - **Declare a fact:** Set one value with a source and a witness. `lockwire declare lockwire.yaml devices/dev.power-module.pm02-v3/physical/mass "25 g" --source "my scale" --witness me`
176
+ - **Add a part:** Add a device, rail, bus or battery block. `lockwire add --help`
177
+ - **Design (place, move, rotate, reparent):** Put a part in the 3D model and move it. Switch on the Power, Data and Mechanical connection views to see them drawn on the robot, and use W, A, S, D to move the camera. `lockwire move lockwire.yaml dev.power-module.pm02-v3 --offset 0.01,0,0.02 --unit m --source "tape measure" --witness me`
178
+ - **Plan and what-if:** Score a plan or a change without editing the lockfile. `lockwire plan lockwire.yaml plan.yaml`
179
+ - **Report:** Write one HTML page to share. `lockwire report lockwire.yaml -o report.html --no-open`
180
+ - **Describe:** Write a reference sheet per part. `lockwire describe lockwire.yaml --out ref`
181
+ - **History (diff, log, blame):** See what changed, by whom, and why (needs git). `lockwire diff`
182
+ - **Scene:** Read part positions from a 3D model. `lockwire scene import --help`
183
+ - **Gearbox:** Add a gearbox with its ratio and joints. `lockwire gearbox --help`
184
+ - **Upgrade:** Write a new lockfile from an older one. `lockwire upgrade --help`
72
185
 
73
186
  ## A real-world example: the X500 drone
74
187
 
@@ -87,9 +200,9 @@ VIOLATION budgets/bud.power.pm02-5v — observed 35.20 W (draw mode: nominal), e
87
200
  action: shed load on rail 'rail.pm02-5v' or raise its capacity with provenance
88
201
  VIOLATION budgets/bud.power.pm02-5v.7w-mode — observed 17.20 W (draw mode: 7W), expected <= 15.60 W (Δ +1.60 W (+10%))
89
202
  … (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
203
+ not scored budgets/bud.mass.payload — observed mass unknown for dev.companion.jetson-orin-nano, dev.camera.oak-d-lite, dev.magnetometer.ist8310; total not scored, expected every device and link in this budget to declare a mass
91
204
  … (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
205
+ scored 9: 7 add up, 2 do not · 10 not scored
93
206
 
94
207
  NO-GO — 2 violation(s), 0 warning(s)
95
208
  $ echo $?
@@ -106,7 +219,7 @@ integration of published parts, and the two vendors' own numbers still do not ad
106
219
  Nobody printed "this build is broken". Nobody printed "5 A" either: that is 25 W / 5 V, a
107
220
  quotient of figures published on different pages. The records disagree with each other, and
108
221
  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.
222
+ (`tests/fixtures/x500/contract_corrected.yaml`) and the same check passes: `GO — records agree (10 scored); 10 check(s) not scored`.
110
223
 
111
224
  ## Your own robot
112
225
 
@@ -130,7 +243,9 @@ Any field you hand-edited (including `robot.name`) is silently overwritten back
130
243
  | `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
244
 
132
245
  `NOT SCORED` (exit 3) means no comparison had both operands: it is not a pass and not a failure.
246
+
133
247
  The `scored N: …` line above the verdict is where a script should look.
248
+ `lockwire check --strict` makes a `GO with notes` run exit 1 (the `NO-GO` code); the table does not change.
134
249
 
135
250
  ## In CI
136
251
 
@@ -141,15 +256,15 @@ The `scored N: …` line above the verdict is where a script should look.
141
256
  contract: lockwire.yaml
142
257
  ```
143
258
 
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`.
259
+ Inputs: `contract` (default `lockwire.yaml`), `version` (pin a PyPI release; default installs the action's own code), `strict` (default `false`; `true` passes `--strict`), `python-version` (default 3.12).
260
+ Or just `pipx install lockwire && lockwire check lockwire.yaml`.
146
261
 
147
262
  ## Pre-commit
148
263
 
149
264
  ```yaml
150
265
  repos:
151
266
  - repo: https://github.com/Shivansh2703/lockwire
152
- rev: v0.2.0
267
+ rev: v0.2.3
153
268
  hooks:
154
269
  - id: lockwire-check
155
270
  ```
@@ -179,8 +294,7 @@ No physics simulation, no machine learning, no network. The answer is the same t
179
294
  - **Free forever (this repo):** the lockfile, all deterministic checks including
180
295
  power/thermal/bus/link/torque budgets, edit / describe / plan / what-if, the MCP server, the
181
296
  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.
297
+ - **Lockwire Pro** is the paid tier (powered-on preflight, recording analysis).
184
298
 
185
299
  ## Development
186
300
 
@@ -0,0 +1,272 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="docs/img/logo-dark.svg">
4
+ <img src="docs/img/logo-light.svg" alt="lockwire" height="64">
5
+ </picture>
6
+ </p>
7
+
8
+ **Find out your robot won't work before you buy the parts.**
9
+
10
+ ![The drone in 3D with its power parts in red, and a NO-GO finding that the 5 V rail is overloaded](docs/img/hero.png)
11
+
12
+ 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.
13
+
14
+ ## Try it in 30 seconds
15
+
16
+ Needs Python 3.12 or newer (the Python that ships with macOS is older).
17
+
18
+ ```bash
19
+ pipx install lockwire
20
+ lockwire start --example servo-bench
21
+ lockwire check
22
+ ```
23
+
24
+ ```console
25
+ VIOLATION budgets/bud.power.servo-5v — observed 32.10 W, expected <= 15.00 W (Δ +17.10 W (+114%))
26
+ dependent: dev.pi5, dev.servo1, dev.servo2, dev.servo3, dev.servo4
27
+ ...
28
+ NO-GO — 1 violation(s), 0 warning(s)
29
+ ```
30
+
31
+ A Raspberry Pi 5 and four servos on one 3 A regulator: caught before you ordered either.
32
+
33
+ ## Try it on your robot
34
+
35
+ Point it at your URDF and see what it flags:
36
+
37
+ ```bash
38
+ lockwire start path/to/robot.urdf
39
+ lockwire check
40
+ ```
41
+
42
+ No URDF? Start from the example and swap in your parts:
43
+
44
+ ```bash
45
+ lockwire start --example servo-bench
46
+ lockwire add device lockwire.yaml --id dev.servo5 --family fam.servo --model mod.mg996r --rail rail.servo-5v --draw "6 W" --mass "55 g" --owner you --source "MG996R datasheet" --witness Sam
47
+ lockwire check
48
+ ```
49
+
50
+ See `lockwire add --help` for rails, budgets and the rest.
51
+
52
+ ## Download the app
53
+
54
+ The free desktop app does the same visually. Get it from the [latest release](https://github.com/Shivansh2703/lockwire/releases/latest).
55
+
56
+ ![Open the app, click the Drone starter, and Lockwire shows the drone in 3D with a NO-GO on its 5 V power rail](docs/img/try-example.gif)
57
+
58
+ - **macOS (Apple silicon):** unzip, open Lockwire. The first open shows a warning because the app is not notarized: open System Settings, Privacy & Security, scroll down and click "Open Anyway".
59
+ - **Linux:** `chmod +x Lockwire-*.AppImage`, then run it. Ubuntu 22.04 needs `sudo apt install libfuse2`; Ubuntu 24.04 needs `sudo apt install libfuse2t64`.
60
+ - **Windows app:** not yet.
61
+
62
+ ## Other ways to use it
63
+
64
+ **Command line.** Make a lockfile and check it from a terminal.
65
+
66
+ ```
67
+ pipx install lockwire
68
+ ```
69
+
70
+ ```
71
+ lockwire start --example drone
72
+ ```
73
+
74
+ Starters: `lockwire start --example <name>` with drone, rover, arm, quadruped or humanoid. Add your own: see [CONTRIBUTING](CONTRIBUTING.md).
75
+
76
+ ```
77
+ lockwire check lockwire.yaml
78
+ ```
79
+
80
+ **Desktop app.** Download it above, click a New robot starter, or open your own lockfile.
81
+
82
+ ```
83
+ https://github.com/Shivansh2703/lockwire/releases/latest
84
+ ```
85
+
86
+ **GitHub Action.** Run the check on every push and keep an HTML report.
87
+
88
+ ```
89
+ - uses: actions/checkout@v4
90
+ - uses: Shivansh2703/lockwire@v0
91
+ with:
92
+ contract: lockwire.yaml
93
+ ```
94
+
95
+ **AI agents (MCP).** Let Claude Desktop, Claude Code or Cursor check your robot before it acts.
96
+
97
+ ```
98
+ pipx install "lockwire[mcp]"
99
+ ```
100
+
101
+ ```
102
+ claude mcp add lockwire -- lockwire mcp
103
+ ```
104
+
105
+ ```
106
+ {
107
+ "mcpServers": {
108
+ "lockwire": { "command": "lockwire", "args": ["mcp"] }
109
+ }
110
+ }
111
+ ```
112
+
113
+ Claude Code: the claude mcp add line. Claude Desktop: put the JSON in claude_desktop_config.json. Cursor: put the JSON in .cursor/mcp.json (or ~/.cursor/mcp.json). If the app cannot find lockwire, use the full path from which lockwire. Then ask: Check my robot's lockwire.yaml and explain the NO-GO.
114
+
115
+ **Agent plan and what-if.** Score an intended plan, or a proposed change, before anything executes.
116
+
117
+ ```
118
+ lockwire plan lockwire.yaml plan.yaml
119
+ ```
120
+
121
+ ```
122
+ lockwire what-if lockwire.yaml --change "devices/dev.onboard-computer/physical/mass=300 g" --source "my scale" --witness me
123
+ ```
124
+
125
+ plan.yaml holds steps, for example: steps: [{kind: rail_load, rail: rail.supply-5v, draw: {value: 10, unit: W}}]
126
+
127
+ **Python library.** Call the same check from your own script.
128
+
129
+ ```
130
+ import lockwire.api as lw
131
+ print(lw.check("lockwire.yaml")["verdict"])
132
+ ```
133
+
134
+ **ROS, preflight and the local API.** The go/no-go gate for a powered-on robot, ROS and the local server.
135
+
136
+ Part of Lockwire Pro.
137
+
138
+ ### Features
139
+
140
+ - **Check:** See which numbers do not add up. `lockwire check lockwire.yaml`
141
+ - **Declare a fact:** Set one value with a source and a witness. `lockwire declare lockwire.yaml devices/dev.power-module.pm02-v3/physical/mass "25 g" --source "my scale" --witness me`
142
+ - **Add a part:** Add a device, rail, bus or battery block. `lockwire add --help`
143
+ - **Design (place, move, rotate, reparent):** Put a part in the 3D model and move it. Switch on the Power, Data and Mechanical connection views to see them drawn on the robot, and use W, A, S, D to move the camera. `lockwire move lockwire.yaml dev.power-module.pm02-v3 --offset 0.01,0,0.02 --unit m --source "tape measure" --witness me`
144
+ - **Plan and what-if:** Score a plan or a change without editing the lockfile. `lockwire plan lockwire.yaml plan.yaml`
145
+ - **Report:** Write one HTML page to share. `lockwire report lockwire.yaml -o report.html --no-open`
146
+ - **Describe:** Write a reference sheet per part. `lockwire describe lockwire.yaml --out ref`
147
+ - **History (diff, log, blame):** See what changed, by whom, and why (needs git). `lockwire diff`
148
+ - **Scene:** Read part positions from a 3D model. `lockwire scene import --help`
149
+ - **Gearbox:** Add a gearbox with its ratio and joints. `lockwire gearbox --help`
150
+ - **Upgrade:** Write a new lockfile from an older one. `lockwire upgrade --help`
151
+
152
+ ## A real-world example: the X500 drone
153
+
154
+ ```bash
155
+ git clone https://github.com/Shivansh2703/lockwire && cd lockwire # the X500 example lives in the repo, not the wheel
156
+ lockwire check tests/fixtures/x500/contract.yaml
157
+ ```
158
+
159
+ Output (trimmed):
160
+
161
+ ```console
162
+ $ lockwire check tests/fixtures/x500/contract.yaml
163
+ VIOLATION budgets/bud.power.pm02-5v — observed 35.20 W (draw mode: nominal), expected <= 15.60 W (Δ +19.60 W (+126%))
164
+ dependent: dev.camera.oak-d-lite, dev.companion.jetson-orin-nano, dev.radio.rfd900x
165
+ owner: power-integration
166
+ action: shed load on rail 'rail.pm02-5v' or raise its capacity with provenance
167
+ VIOLATION budgets/bud.power.pm02-5v.7w-mode — observed 17.20 W (draw mode: 7W), expected <= 15.60 W (Δ +1.60 W (+10%))
168
+ … (same rail, Jetson in its 7 W mode: the radio's transmit peak still tips it over)
169
+ not scored budgets/bud.mass.payload — observed mass unknown for dev.companion.jetson-orin-nano, dev.camera.oak-d-lite, dev.magnetometer.ist8310; total not scored, expected every device and link in this budget to declare a mass
170
+ … (more not-scored lines: things the lockfile does not declare yet, listed, never hidden)
171
+ scored 9: 7 add up, 2 do not · 10 not scored
172
+
173
+ NO-GO — 2 violation(s), 0 warning(s)
174
+ $ echo $?
175
+ 1
176
+ ```
177
+
178
+ ## What just happened
179
+
180
+ A Holybro PM02 power module publishes a regulated 5 V rail at 3 A: 15.6 W. An NVIDIA Jetson
181
+ Orin Nano publishes a 25 W power mode at 5 V: 5 A. Put a camera on the same rail and the
182
+ published parts **cannot power the published companion computer**. This is a custom
183
+ integration of published parts, and the two vendors' own numbers still do not add up.
184
+
185
+ Nobody printed "this build is broken". Nobody printed "5 A" either: that is 25 W / 5 V, a
186
+ quotient of figures published on different pages. The records disagree with each other, and
187
+ you find out before you apply power. Give the Jetson its own regulator
188
+ (`tests/fixtures/x500/contract_corrected.yaml`) and the same check passes: `GO — records agree (10 scored); 10 check(s) not scored`.
189
+
190
+ ## Your own robot
191
+
192
+ `lockwire start` reads links, joints, masses and the structure hash from your URDF or SDF. A fresh
193
+ lockfile knows the frame and nothing else; fill in devices, rails and buses as you go, and
194
+ `check` lists what it still could not check.
195
+
196
+ A lockfile whose `schema_version` this lockwire does not recognise is refused before any check runs.
197
+ `check` prints `REFUSED — contract declares schema_version '0.99'; this lockwire supports 0.2, 0.3, 0.4` and exits 2.
198
+ A lockfile at an older supported version is accepted as-is, with no automatic upgrade.
199
+
200
+ Re-running `lockwire start --force` on the same URDF is destructive:
201
+ re-declare every fact in the lockfile by hand, because no command carries those facts forward.
202
+ Any field you hand-edited (including `robot.name`) is silently overwritten back to the URDF-derived value.
203
+
204
+ ## Exit codes
205
+
206
+ | command | 0 | 1 | 2 | 3 |
207
+ |---|---|---|---|---|
208
+ | `check` | `GO`, `GO with notes` | `NO-GO` | a contract that could not be found, read, or parsed | `NOT SCORED` (nothing compared) |
209
+ | `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 | — | — |
210
+
211
+ `NOT SCORED` (exit 3) means no comparison had both operands: it is not a pass and not a failure.
212
+
213
+ The `scored N: …` line above the verdict is where a script should look.
214
+ `lockwire check --strict` makes a `GO with notes` run exit 1 (the `NO-GO` code); the table does not change.
215
+
216
+ ## In CI
217
+
218
+ ```yaml
219
+ - uses: actions/checkout@v4
220
+ - uses: Shivansh2703/lockwire@v0 # composite action: installs lockwire, runs the check, uploads an HTML report
221
+ with:
222
+ contract: lockwire.yaml
223
+ ```
224
+
225
+ Inputs: `contract` (default `lockwire.yaml`), `version` (pin a PyPI release; default installs the action's own code), `strict` (default `false`; `true` passes `--strict`), `python-version` (default 3.12).
226
+ Or just `pipx install lockwire && lockwire check lockwire.yaml`.
227
+
228
+ ## Pre-commit
229
+
230
+ ```yaml
231
+ repos:
232
+ - repo: https://github.com/Shivansh2703/lockwire
233
+ rev: v0.2.3
234
+ hooks:
235
+ - id: lockwire-check
236
+ ```
237
+
238
+ Runs `lockwire check` (on `lockwire.yaml` in the repo root) whenever a lockfile changes.
239
+
240
+ ## What it checks
241
+
242
+ Each check compares **two separately declared figures**, each with its own provenance line.
243
+ It shows who said what; it cannot verify that a datasheet is true.
244
+
245
+ | check | source A | source B |
246
+ |---|---|---|
247
+ | rail power budget | device draw (datasheet) | rail capacity (power module datasheet) |
248
+ | rail tree rollup | child rail draw / efficiency | parent rail capacity |
249
+ | torque budget | joint effort limit (URDF) | actuator continuous torque x gear ratio |
250
+ | bus bandwidth | message rate x payload | bus bitrate x utilisation ceiling |
251
+ | mass budget | per-device mass | declared total |
252
+ | endurance | pack energy | summed draw |
253
+ | voltage compatibility | rail voltage | device input voltage range |
254
+ | reference integrity | every `bus_ref`, `mount_frame`, dependency edge | the thing it points at |
255
+
256
+ No physics simulation, no machine learning, no network. The answer is the same twice.
257
+
258
+ ## Free and Pro
259
+
260
+ - **Free forever (this repo):** the lockfile, all deterministic checks including
261
+ power/thermal/bus/link/torque budgets, edit / describe / plan / what-if, the MCP server, the
262
+ HTML report, and the desktop planning app (Open, Design, Declare, Plan, Check).
263
+ - **Lockwire Pro** is the paid tier (powered-on preflight, recording analysis).
264
+
265
+ ## Development
266
+
267
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Python 3.12 or newer; runtime dependencies are
268
+ pydantic, typer, rich, ruamel.yaml and numpy.
269
+
270
+ ## Licence
271
+
272
+ Apache-2.0. See [LICENSE](LICENSE).
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "lockwire"
3
- version = "0.2.0"
4
- description = "The physical-assumption contract for robots: physics lockfile + preflight gate"
3
+ version = "0.2.4"
4
+ description = "The physical-assumption contract for robots: checks that a robot's declared parts (power, weight, bus, torque) add up"
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Shivansh" }
@@ -26,13 +26,15 @@ dependencies = [
26
26
  "rich>=15.0.0",
27
27
  "ruamel-yaml>=0.19.1",
28
28
  "typer>=0.26.8",
29
+ "xacro>=2.1.1",
29
30
  ]
30
31
 
31
32
  [project.optional-dependencies]
32
33
  # Serving lockwire over MCP to an agent. Optional for the same reason as `ros2bag`:
33
34
  # a plain install must stay lean, and the SDK is only needed by `lockwire mcp`.
34
35
  mcp = [
35
- "mcp>=2.1.1",
36
+ "mcp>=2.2.0",
37
+ "pyjwt>=2.15", # direct floor: a fresh lock must not resolve below the fixed release
36
38
  ]
37
39
  # The Electron app's sidecar: `lockwire serve` over local HTTP. Optional for the
38
40
  # same reason as `desktop`/`ros2bag`/`mcp` — a plain CLI install stays lean; only the
@@ -59,7 +61,7 @@ build-backend = "uv_build"
59
61
 
60
62
  [dependency-groups]
61
63
  dev = [
62
- "mcp>=2.1.1",
64
+ "mcp>=2.2.0",
63
65
  # tests/test_serve.py needs the sidecar (`lockwire serve`) importable and
64
66
  # runnable as a subprocess. Same reasoning as `mcp`/`mcap` above: CI's
65
67
  # `test` job runs a plain `uv sync --dev`, so without these here that
@@ -1,6 +1,6 @@
1
1
  """Lockwire — the physical-assumption contract for robots, enforced in CI and at preflight."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.4"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -0,0 +1,38 @@
1
+ """Blocking exclusive lock on an open fd, POSIX and Windows, stdlib only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+
8
+
9
+ def lock(fd: int) -> None:
10
+ if os.name == "nt":
11
+ import msvcrt
12
+
13
+ os.lseek(fd, 0, os.SEEK_SET)
14
+ deadline = time.monotonic() + 30
15
+ while True:
16
+ try:
17
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
18
+ return
19
+ except OSError:
20
+ if time.monotonic() >= deadline:
21
+ raise TimeoutError("lock held by another lockwire process") from None
22
+ time.sleep(0.1)
23
+ else:
24
+ import fcntl
25
+
26
+ fcntl.flock(fd, fcntl.LOCK_EX)
27
+
28
+
29
+ def unlock(fd: int) -> None:
30
+ if os.name == "nt":
31
+ import msvcrt
32
+
33
+ os.lseek(fd, 0, os.SEEK_SET)
34
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
35
+ else:
36
+ import fcntl
37
+
38
+ fcntl.flock(fd, fcntl.LOCK_UN)