lockwire 0.2.4__tar.gz → 0.2.5__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 (80) hide show
  1. {lockwire-0.2.4 → lockwire-0.2.5}/PKG-INFO +1 -1
  2. {lockwire-0.2.4 → lockwire-0.2.5}/pyproject.toml +1 -1
  3. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/__init__.py +1 -1
  4. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/checks.py +22 -0
  5. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/cli.py +10 -0
  6. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/edit.py +4 -0
  7. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/arm/contract.yaml +6 -1
  8. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/drone/contract.yaml +28 -1
  9. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/humanoid/contract.yaml +15 -5
  10. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/quadruped/contract.yaml +3 -1
  11. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/rover/contract.yaml +3 -1
  12. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/schema.py +25 -2
  13. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/urdf.py +2 -1
  14. {lockwire-0.2.4 → lockwire-0.2.5}/README.md +0 -0
  15. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/__main__.py +0 -0
  16. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/_filelock.py +0 -0
  17. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/advice.py +0 -0
  18. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/api.py +0 -0
  19. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/api_payload.py +0 -0
  20. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/bench.py +0 -0
  21. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/blame.py +0 -0
  22. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/catalog.py +0 -0
  23. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/catalog_seed.yaml +0 -0
  24. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/cli_line.py +0 -0
  25. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/confine.py +0 -0
  26. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/constraints.py +0 -0
  27. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/copy.py +0 -0
  28. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/describe.py +0 -0
  29. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/emit_ts.py +0 -0
  30. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/arm/LICENSE +0 -0
  31. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/arm/NOTICE +0 -0
  32. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/arm/plan.yaml +0 -0
  33. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/arm/scene.glb +0 -0
  34. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/arm/starter.json +0 -0
  35. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/drone/plan.yaml +0 -0
  36. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/drone/scene.glb +0 -0
  37. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/drone/starter.json +0 -0
  38. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/humanoid/NOTICE +0 -0
  39. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/humanoid/plan.yaml +0 -0
  40. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/humanoid/scene.glb +0 -0
  41. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/humanoid/starter.json +0 -0
  42. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/quadruped/NOTICE +0 -0
  43. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/quadruped/plan.yaml +0 -0
  44. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/quadruped/scene.glb +0 -0
  45. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/quadruped/starter.json +0 -0
  46. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/rover/NOTICE +0 -0
  47. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/rover/plan.yaml +0 -0
  48. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/rover/scene.glb +0 -0
  49. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/rover/starter.json +0 -0
  50. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/examples/servo-bench.yaml +0 -0
  51. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/export_mhs.py +0 -0
  52. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/factdiff.py +0 -0
  53. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/forces.py +0 -0
  54. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/gitref.py +0 -0
  55. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/hw.py +0 -0
  56. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/mcp_server.py +0 -0
  57. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/mechanism.py +0 -0
  58. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/mesh_dae.py +0 -0
  59. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/mesh_simple.py +0 -0
  60. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/page.py +0 -0
  61. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/parts_list.py +0 -0
  62. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/plan.py +0 -0
  63. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/plan_payload.py +0 -0
  64. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/plugins.py +0 -0
  65. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/report.py +0 -0
  66. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/report_html.py +0 -0
  67. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/report_json.py +0 -0
  68. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/review_draft.py +0 -0
  69. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/scan.py +0 -0
  70. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/scene.py +0 -0
  71. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/scene_import.py +0 -0
  72. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/scene_urdf.py +0 -0
  73. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/scene_view.py +0 -0
  74. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/sdf.py +0 -0
  75. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/serve.py +0 -0
  76. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/start_operation.py +0 -0
  77. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/statics.py +0 -0
  78. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/units.py +0 -0
  79. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/vcs.py +0 -0
  80. {lockwire-0.2.4 → lockwire-0.2.5}/src/lockwire/whatif.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lockwire
3
- Version: 0.2.4
3
+ Version: 0.2.5
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lockwire"
3
- version = "0.2.4"
3
+ version = "0.2.5"
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 = [
@@ -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.4"
3
+ __version__ = "0.2.5"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -2104,6 +2104,28 @@ def check_budgets(c: Contract, *, scores: _ScoreBook | None = None) -> list[Find
2104
2104
  f"({headroom_kg / limit_kg * 100:.0f}%)"
2105
2105
  ),
2106
2106
  )
2107
+ if b.nominal is not None and total_kg > 0.0:
2108
+ nominal_kg = b.nominal.base()
2109
+ gap_pct = (total_kg - nominal_kg) / nominal_kg * 100
2110
+ # a partial sum (unknown masses) can only prove "too heavy", never "too light"
2111
+ if abs(gap_pct) > (b.tolerance_pct or 0.0) and (not unknown_mass or gap_pct > 0):
2112
+ out.append(
2113
+ Finding(
2114
+ severity="violation",
2115
+ quantity=f"budgets/{b.id}/nominal",
2116
+ observed=format_magnitude(total_kg, "kg"),
2117
+ expected=f"{format_magnitude(nominal_kg, 'kg')} +/- {b.tolerance_pct or 0:g}%",
2118
+ observed_quantity=Quantity(value=total_kg, unit="kg"),
2119
+ expected_quantity=Quantity(value=nominal_kg, unit="kg"),
2120
+ delta=f"{gap_pct:+.0f}% ({total_kg - nominal_kg:+.2f} kg)",
2121
+ dependent=", ".join(contributors),
2122
+ owner=b.owner,
2123
+ source=b.provenance.source if b.provenance is not None else None,
2124
+ witness=b.provenance.witness if b.provenance is not None else None,
2125
+ locked_at=b.provenance.locked_at if b.provenance is not None else None,
2126
+ action="the summed link masses disagree with the declared nominal mass; fix the model or the declared value",
2127
+ )
2128
+ )
2107
2129
 
2108
2130
  elif b.kind == "endurance":
2109
2131
  _, _, battery_w, battery_witnesses, _, battery_unknown = _power_rollup(c, b.draw_mode)
@@ -1584,6 +1584,14 @@ def add_cmd(
1584
1584
  sources: str | None = typer.Option(
1585
1585
  None, "--sources", help="budget (mass_total only): devices|links|links+devices."
1586
1586
  ),
1587
+ nominal: str | None = typer.Option(
1588
+ None,
1589
+ "--nominal",
1590
+ help="budget (mass_total): datasheet mass the sum must match, e.g. '1.25 kg'.",
1591
+ ),
1592
+ tolerance_pct: float | None = typer.Option(
1593
+ None, "--tolerance-pct", help="budget: percent the sum may differ from --nominal."
1594
+ ),
1587
1595
  nominal_voltage: str | None = typer.Option(
1588
1596
  None, "--nominal-voltage", help="battery: voltage, e.g. '25.9 V'."
1589
1597
  ),
@@ -1649,6 +1657,8 @@ def add_cmd(
1649
1657
  "limit": limit,
1650
1658
  "draw_mode": draw_mode,
1651
1659
  "sources": sources,
1660
+ "nominal": nominal,
1661
+ "tolerance_pct": tolerance_pct,
1652
1662
  "rail": rail,
1653
1663
  "nominal_voltage": nominal_voltage,
1654
1664
  "chemistry": chemistry,
@@ -1655,6 +1655,10 @@ def _build_budget(fields: dict[str, Any], doc: Any) -> CommentedMap:
1655
1655
  entry["draw_mode"] = fields["draw_mode"]
1656
1656
  if fields.get("sources"):
1657
1657
  entry["sources"] = fields["sources"]
1658
+ if fields.get("nominal"):
1659
+ entry["nominal"] = parse_value(fields["nominal"], field_name="nominal")
1660
+ if fields.get("tolerance_pct"):
1661
+ entry["tolerance_pct"] = float(fields["tolerance_pct"])
1658
1662
  entry["owner"] = fields["owner"]
1659
1663
  return entry
1660
1664
 
@@ -137,10 +137,15 @@ device_models:
137
137
  witness: w0
138
138
  source: "https://www.waveshare.com/st3215-servo.htm; github.com/TheRobotStudio/SO-ARM100 README.md (Bill of Materials, follower-arm note)"
139
139
  locked_at: 2026-10-15
140
- note: "STS3215 12 V version: operating voltage 6-12.6 V, 30 kg.cm at 12 V, no-load current 200 mA, locked-rotor current 2.7 A (Waveshare retailer listing, not manufacturer). The README says the 12 V version has 30 kg.cm stall torque; the 7.4 V version is 16.5 kg.cm at 6 V."
140
+ note: "STS3215 12 V version: operating voltage 6-12.6 V, 30 kg.cm at 12 V, no-load current 200 mA, locked-rotor current 2.7 A (Waveshare retailer listing, not manufacturer). The README says the 12 V version has 30 kg.cm stall torque; the 7.4 V version is 16.5 kg.cm at 6 V. temperature_c hard limit 70 C: Feetech STS memory table (https://aifitlab-wiki.super.site/feetech-servo-motor-docs/feetech-sts-servo-memory-table-analysis, read 2026-10-22, a third-party transcription of the Feetech table): row Maximum Temperature Limit, address 13 (0x0D), initial value 70, range 0 ~ 100, unit C; row Present Temperature, address 63 (0x3F), read-only, unit C, Internal temperature of the servo. The 70 C is a user-writable default, not an absolute rating; a translated vendor datasheet is said to state torque output turns off above 70 C (second-hand, could not read the PDF). No lower temperature bound is published, so the floor is absolute zero."
141
141
  input_voltage_range:
142
142
  min: {value: 6, unit: V}
143
143
  max: {value: 12.6, unit: V}
144
+ hard_limits:
145
+ # Feetech STS memory table: Maximum Temperature Limit, address 13 (0x0D), initial value 70, range 0-100, unit C;
146
+ # Present Temperature, address 63 (0x3F), read-only, unit C. The lower bound is NOT published: absolute zero is
147
+ # used only as a non-binding floor so the bound is one-sided.
148
+ temperature_c: {lo: -273.15, hi: 70, unit: degC, why: "Feetech STS servos switch torque off above their Maximum Temperature Limit, default 70 C"}
144
149
  - id: mdl.controller.waveshare-bus-servo-adapter-a
145
150
  family: fam.controller.servo-adapter
146
151
  provenance:
@@ -64,7 +64,14 @@ device_models:
64
64
  witness: w0
65
65
  source: docs.holybro.com/autopilot/pixhawk-6c/technical-specification
66
66
  locked_at: 2026-07-25
67
- note: "Board current draw is unknown; aluminium/plastic case masses are variants, so no installed mass is selected."
67
+ note: "Board current draw is unknown; aluminium/plastic case masses are variants, so no installed mass is selected. Sensor limits (hard_limits): the board's primary IMU is the ICM-42688-P (see mdl.imu.icm-42688-p), so the limits below are its selected full-scale ranges from the TDK InvenSense datasheet DS-000347 section 3.1/3.2 (read 2026-10-22): accelerometer +/-16 g (ACCEL_FS_SEL=0) = +/-156.9064 m/s^2, gyroscope +/-2000 deg/s (GYRO_FS_SEL=0) = +/-34.9066 rad/s. These are the ranges the flight stack configures only if it selects the widest scale; a narrower selection would clip earlier."
68
+ hard_limits:
69
+ accel_x: {lo: -156.9064, hi: 156.9064, unit: "m/s^2"}
70
+ accel_y: {lo: -156.9064, hi: 156.9064, unit: "m/s^2"}
71
+ accel_z: {lo: -156.9064, hi: 156.9064, unit: "m/s^2"}
72
+ gyro_x: {lo: -34.9066, hi: 34.9066, unit: "rad/s"}
73
+ gyro_y: {lo: -34.9066, hi: 34.9066, unit: "rad/s"}
74
+ gyro_z: {lo: -34.9066, hi: 34.9066, unit: "rad/s"}
68
75
  - id: mdl.companion.jetson-orin-nano
69
76
  family: fam.companion.sbc
70
77
  provenance:
@@ -284,6 +291,26 @@ devices:
284
291
  physical:
285
292
  mass: {value: 59.3, unit: g}
286
293
  mount_frame: x500_frame
294
+ envelopes:
295
+ - context: bench-idle
296
+ signals:
297
+ accel_x:
298
+ band: {lo: -0.196133, hi: 0.196133, unit: m/s^2}
299
+ accel_y:
300
+ band: {lo: -0.196133, hi: 0.196133, unit: m/s^2}
301
+ accel_z:
302
+ band: {lo: -10.002783, hi: -9.610517, unit: m/s^2}
303
+ gyro_x:
304
+ band: {lo: -0.0087266, hi: 0.0087266, unit: rad/s}
305
+ gyro_y:
306
+ band: {lo: -0.0087266, hi: 0.0087266, unit: rad/s}
307
+ gyro_z:
308
+ band: {lo: -0.0087266, hi: 0.0087266, unit: rad/s}
309
+ provenance:
310
+ witness: human
311
+ source: "TDK InvenSense ICM-42688-P Datasheet DS-000347, section 3.1 and 3.2 (read from the datasheet PDF, Revision 1.2 mirror, 2026-10-22); product page https://invensense.tdk.com/products/motion-tracking/6-axis/icm-42688-p/ for the current revision"
312
+ locked_at: 2026-10-22
313
+ note: "The Pixhawk 6C carries the ICM-42688-P as its primary IMU (see mdl.imu.icm-42688-p), so the IMU bands are declared on the flight controller device, which is what the recording reports. Vehicle at rest, level, on the bench (+Z reads -g in the recording's sign convention). Gyro band is the datasheet row 'Initial ZRO Tolerance, Board-level, 25C: +/-0.5 deg/s' = +/-0.0087266 rad/s (0.5 x pi/180). Accel x/y band is the row 'ZERO-G OUTPUT Initial Tolerance, Board-level, all axes: +/-20 mg' = +/-0.196133 m/s^2 (20e-3 x 9.80665); the z band is -9.80665 +/- 0.196133 for the same tolerance. The datasheet figures are at 25 C and board level; ZRO drifts +/-0.005 deg/s per C and zero-g +/-0.15 mg per C (same tables), which this band does not widen for. Assumes a level mount: a tilted airframe reads gravity on x/y and would trip it. Newer datasheet revisions were not readable (TDK site blocks automated fetch); the +/-20 figure matches the TDK product page listing 'Accel ZGO'."
287
314
  owner: avionics-integration
288
315
  provenance:
289
316
  witness: w0
@@ -1103,50 +1103,60 @@ device_models:
1103
1103
  witness: w0
1104
1104
  source: "https://emanual.robotis.com/docs/en/dxl/x/xc330-t288/"
1105
1105
  locked_at: 2026-10-15
1106
- note: "Robotis e-Manual, XC330-T288, fetched 2026-10-15: Stall Torque 1.00 N.m at 12.0 V, 0.88 A; Input Voltage 6.5 ~ 12.0 V (recommended 11.1 V). Stall draw at 12 V = 12 V x 0.88 A = 10.56 W."
1106
+ note: "Robotis e-Manual, XC330-T288, fetched 2026-10-15: Stall Torque 1.00 N.m at 12.0 V, 0.88 A; Input Voltage 6.5 ~ 12.0 V (recommended 11.1 V). Stall draw at 12 V = 12 V x 0.88 A = 10.56 W. temperature_c hard limit -5 to 70 C: Robotis e-Manual (read 2026-10-22) Specifications table, 'Operating Temperature -5 ~ +70 [°C]' (https://emanual.robotis.com/docs/en/dxl/x/xc330-t288/); control table row 'Present Temperature' (address 146, read-only, unit C). The Temperature Limit register (row '31 1 Temperature Limit RW 70 0 ~ 100 1 [℃]') is a user-writable shutdown default; the operating range is used as the limit."
1107
1107
  input_voltage_range:
1108
1108
  min: {value: 6.5, unit: V}
1109
1109
  max: {value: 12.0, unit: V}
1110
+ hard_limits:
1111
+ temperature_c: {lo: -5, hi: 70, unit: degC, why: "Outside the servo's published operating temperature range"}
1110
1112
  - id: mdl.servo.2xc430-w250
1111
1113
  family: fam.servo.dynamixel
1112
1114
  provenance:
1113
1115
  witness: w0
1114
1116
  source: "https://emanual.robotis.com/docs/en/dxl/x/2xc430-w250/"
1115
1117
  locked_at: 2026-10-15
1116
- note: "Robotis e-Manual, 2XC430-W250, fetched 2026-10-15: Stall Torque 1.8 N.m at 12.0 V, 1.4 A; Input Voltage 6.5 ~ 14.8 V (recommended 12.0 V); the page lists one stall figure for the whole two-axis module. Stall draw at 12 V = 12 V x 1.4 A = 16.8 W."
1118
+ note: "Robotis e-Manual, 2XC430-W250, fetched 2026-10-15: Stall Torque 1.8 N.m at 12.0 V, 1.4 A; Input Voltage 6.5 ~ 14.8 V (recommended 12.0 V); the page lists one stall figure for the whole two-axis module. Stall draw at 12 V = 12 V x 1.4 A = 16.8 W. temperature_c hard limit -5 to 72 C: Robotis e-Manual (read 2026-10-22) Specifications table, 'Operating Temperature -5 ~ +72 [°C]' (https://emanual.robotis.com/docs/en/dxl/x/2xc430-w250/); control table row 'Present Temperature' (address 146, read-only, unit C). The Temperature Limit register (row '31 | 1 | Temperature Limit | RW | 80 | 0 ~ 100 | 1 [°C]') is a user-writable shutdown default; the operating range is used as the limit."
1117
1119
  input_voltage_range:
1118
1120
  min: {value: 6.5, unit: V}
1119
1121
  max: {value: 14.8, unit: V}
1122
+ hard_limits:
1123
+ temperature_c: {lo: -5, hi: 72, unit: degC, why: "Outside the servo's published operating temperature range"}
1120
1124
  - id: mdl.servo.xc430-t240bb
1121
1125
  family: fam.servo.dynamixel
1122
1126
  provenance:
1123
1127
  witness: w0
1124
1128
  source: "https://emanual.robotis.com/docs/en/dxl/x/xc430-t240bb/"
1125
1129
  locked_at: 2026-10-15
1126
- note: "Robotis e-Manual, XC430-T240BB, fetched 2026-10-15: Stall Torque 1.9 N.m at 12.0 V, 1.4 A; Input Voltage 6.5 ~ 14.8 V (recommended 12.0 V). Stall draw at 12 V = 12 V x 1.4 A = 16.8 W."
1130
+ note: "Robotis e-Manual, XC430-T240BB, fetched 2026-10-15: Stall Torque 1.9 N.m at 12.0 V, 1.4 A; Input Voltage 6.5 ~ 14.8 V (recommended 12.0 V). Stall draw at 12 V = 12 V x 1.4 A = 16.8 W. temperature_c hard limit -5 to 80 C: Robotis e-Manual (read 2026-10-22) Specifications table, 'Operating Temperature -5 ~ +80 [°C]' (https://emanual.robotis.com/docs/en/dxl/x/xc430-t240bb/); control table row 'Present Temperature' (address 146, read-only, unit C). The Temperature Limit register (row '31 | 1 | Temperature Limit | RW | 80 | 0 ~ 100 | 1 [°C]') is a user-writable shutdown default; the operating range is used as the limit."
1127
1131
  input_voltage_range:
1128
1132
  min: {value: 6.5, unit: V}
1129
1133
  max: {value: 14.8, unit: V}
1134
+ hard_limits:
1135
+ temperature_c: {lo: -5, hi: 80, unit: degC, why: "Outside the servo's published operating temperature range"}
1130
1136
  - id: mdl.servo.xm430-w210
1131
1137
  family: fam.servo.dynamixel
1132
1138
  provenance:
1133
1139
  witness: w0
1134
1140
  source: "https://emanual.robotis.com/docs/en/dxl/x/xm430-w210/"
1135
1141
  locked_at: 2026-10-15
1136
- note: "Robotis e-Manual, XM430-W210, fetched 2026-10-15: Stall Torque 3.0 N.m at 12.0 V, 2.3 A; Input Voltage 10.0 ~ 14.8 V (recommended 12.0 V). Stall draw at 12 V = 12 V x 2.3 A = 27.6 W."
1142
+ note: "Robotis e-Manual, XM430-W210, fetched 2026-10-15: Stall Torque 3.0 N.m at 12.0 V, 2.3 A; Input Voltage 10.0 ~ 14.8 V (recommended 12.0 V). Stall draw at 12 V = 12 V x 2.3 A = 27.6 W. temperature_c hard limit -5 to 80 C: Robotis e-Manual (read 2026-10-22) Specifications table, 'Operating Temperature -5 ~ +80 [°C]' (https://emanual.robotis.com/docs/en/dxl/x/xm430-w210/); control table row 'Present Temperature' (address 146, read-only, unit C). The Temperature Limit register (row '31 | 1 | Temperature Limit | RW | 80 | 0 ~ 100 | 1 [°C]') is a user-writable shutdown default; the operating range is used as the limit."
1137
1143
  input_voltage_range:
1138
1144
  min: {value: 10.0, unit: V}
1139
1145
  max: {value: 14.8, unit: V}
1146
+ hard_limits:
1147
+ temperature_c: {lo: -5, hi: 80, unit: degC, why: "Outside the servo's published operating temperature range"}
1140
1148
  - id: mdl.servo.xm430-w350
1141
1149
  family: fam.servo.dynamixel
1142
1150
  provenance:
1143
1151
  witness: w0
1144
1152
  source: "https://emanual.robotis.com/docs/en/dxl/x/xm430-w350/"
1145
1153
  locked_at: 2026-10-15
1146
- note: "Robotis e-Manual, XM430-W350, fetched 2026-10-15: Stall Torque 4.1 N.m at 12.0 V, 2.3 A; Input Voltage 10.0 ~ 14.8 V (recommended 12.0 V). Stall draw at 12 V = 12 V x 2.3 A = 27.6 W."
1154
+ note: "Robotis e-Manual, XM430-W350, fetched 2026-10-15: Stall Torque 4.1 N.m at 12.0 V, 2.3 A; Input Voltage 10.0 ~ 14.8 V (recommended 12.0 V). Stall draw at 12 V = 12 V x 2.3 A = 27.6 W. temperature_c hard limit -5 to 80 C: Robotis e-Manual (read 2026-10-22) Specifications table, 'Operating Temperature -5 ~ +80 [°C]' (https://emanual.robotis.com/docs/en/dxl/x/xm430-w350/); control table row 'Present Temperature' (address 146, read-only, unit C). The Temperature Limit register (row '31 1 Temperature Limit RW 80 0 ~ 100 -') is a user-writable shutdown default; the operating range is used as the limit."
1147
1155
  input_voltage_range:
1148
1156
  min: {value: 10.0, unit: V}
1149
1157
  max: {value: 14.8, unit: V}
1158
+ hard_limits:
1159
+ temperature_c: {lo: -5, hi: 80, unit: degC, why: "Outside the servo's published operating temperature range"}
1150
1160
  - id: mdl.regulator.pololu-d36v50f12
1151
1161
  family: fam.regulator
1152
1162
  provenance:
@@ -233,10 +233,12 @@ device_models:
233
233
  witness: w0
234
234
  source: "github.com/open-dynamic-robot-initiative/open_robot_actuator_hardware mechanics/actuator_module_v1/README.md; electronics/micro_driver_electronics/README.md; arxiv.org/abs/1910.00093"
235
235
  locked_at: 2026-10-15
236
- note: "T-Motor Antigravity 4004 300KV, dual-stage timing belt 3:1 x 3:1 = 9:1 (actuator README; paper II-A). Paper: k_i = 0.025 N m/A, tau_joint = 0.225 i, tau_max = 2.7 N m at 12 A. Micro-driver board input 5-32 V, 'we operate our robots at 24 V' (micro-driver README), used as the input range."
236
+ note: "T-Motor Antigravity 4004 300KV, dual-stage timing belt 3:1 x 3:1 = 9:1 (actuator README; paper II-A). Paper: k_i = 0.025 N m/A, tau_joint = 0.225 i, tau_max = 2.7 N m at 12 A. Micro-driver board input 5-32 V, 'we operate our robots at 24 V' (micro-driver README), used as the input range. current_a hard limit +/-12 A: the paper (https://arxiv.org/pdf/1910.00093, read 2026-10-22) states 'The actuator can output tau_max = 2.7 N m joint torque at 12 A.'; the 12 A is a rated peak, and the paper gives no lower bound beyond its symmetric reading for reverse torque. The Solo12's IMU and per-motor temperature are NOT declared: the motor driver publishes no temperature figure, and the IMU bias figures could not be sourced to a page that names the Solo12's part."
237
237
  input_voltage_range:
238
238
  min: {value: 5, unit: V}
239
239
  max: {value: 32, unit: V}
240
+ hard_limits:
241
+ current_a: {lo: -12, hi: 12, unit: A, why: "The ODRI paper rates the actuator at 12 A (2.7 N m joint torque); above it the module is outside its published envelope"}
240
242
  devices:
241
243
  - id: dev.motor.front-left-hip-abduction
242
244
  model: mdl.actuator.odri-4004-9to1
@@ -135,7 +135,9 @@ device_models:
135
135
  witness: w0
136
136
  source: "emanual.robotis.com/docs/en/dxl/x/xl430-w250/"
137
137
  locked_at: 2026-10-15
138
- note: "Input 6.5-12.0 V (recommended 11.1 V); stall torque 1.4 N.m at 11.1 V, 1.3 A; standby current 52 mA; mass 57.2 g."
138
+ note: "Input 6.5-12.0 V (recommended 11.1 V); stall torque 1.4 N.m at 11.1 V, 1.3 A; standby current 52 mA; mass 57.2 g. temperature_c hard limit -5 to 72 C: Robotis e-Manual (read 2026-10-22) Specifications table, 'Operating Temperature -5 ~ +72 [°C]' (https://emanual.robotis.com/docs/en/dxl/x/xl430-w250/); control table row 'Present Temperature' (address 146, read-only, unit C). The Temperature Limit register (row '31 1 Temperature Limit RW 72 0 ~ 100 1 [°C]') is a user-writable shutdown default; the operating range is used as the limit."
139
+ hard_limits:
140
+ temperature_c: {lo: -5, hi: 72, unit: degC, why: "Outside the servo's published operating temperature range"}
139
141
  - id: mdl.lidar.lds-02
140
142
  family: fam.lidar
141
143
  provenance:
@@ -519,8 +519,6 @@ class JointLimits(Base):
519
519
 
520
520
  @model_validator(mode="after")
521
521
  def _ordered(self) -> "JointLimits":
522
- if self.lower is not None and self.upper is not None and self.lower >= self.upper:
523
- raise ValueError(f"joint limit lower {self.lower} >= upper {self.upper}")
524
522
  if self.velocity is not None and self.velocity < 0:
525
523
  raise ValueError(f"joint velocity limit must be >= 0, got {self.velocity}")
526
524
  for limit in ("lower", "upper", "effort", "velocity"):
@@ -540,6 +538,21 @@ class Joint(Base):
540
538
  axis: tuple[float, float, float] | None = None
541
539
  limits: JointLimits | None = None
542
540
 
541
+ @model_validator(mode="after")
542
+ def _ordered(self) -> "Joint":
543
+ # Only a moving joint has a range: URDF ignores lower/upper on fixed and continuous
544
+ # joints, and real files carry lower="0" upper="0" there (Kinova's gen3_lite.urdf).
545
+ lim = self.limits
546
+ if (
547
+ self.type in ("revolute", "prismatic")
548
+ and lim
549
+ and lim.lower is not None
550
+ and lim.upper is not None
551
+ and lim.lower >= lim.upper
552
+ ):
553
+ raise ValueError(f"joint {self.name!r} limit lower {lim.lower} >= upper {lim.upper}")
554
+ return self
555
+
543
556
 
544
557
  def _model_structure_hash(links: list[Link], joints: list[Joint]) -> str:
545
558
  """Canonical v0.2 hash; axis and limits are load-bearing joint structure."""
@@ -1485,6 +1498,8 @@ class Budget(Base):
1485
1498
  sources: Literal["devices", "links", "links+devices"] = "devices" # mass_total: what to
1486
1499
  # sum — explicit so double-counting a device that also appears in the URDF is a declared
1487
1500
  # choice, not an accident
1501
+ nominal: Quantity | None = None # mass_total only: the datasheet mass the sum must match
1502
+ tolerance_pct: float | None = None # with nominal; absent means exact
1488
1503
  owner: str
1489
1504
  provenance: Provenance | None = None
1490
1505
 
@@ -1505,6 +1520,14 @@ class Budget(Base):
1505
1520
  "mass_total": "mass",
1506
1521
  }[self.kind]
1507
1522
  _quantity_dimension(expected, f"{self.kind} budget limit")(self.limit)
1523
+ if self.nominal is not None:
1524
+ if self.kind != "mass_total":
1525
+ raise ValueError("nominal is for mass_total budgets only")
1526
+ _quantity_dimension("mass", "budget nominal")(self.nominal)
1527
+ if self.nominal.base() <= 0:
1528
+ raise ValueError("budget nominal must be positive")
1529
+ if self.tolerance_pct is not None and (self.nominal is None or self.tolerance_pct < 0):
1530
+ raise ValueError("tolerance_pct needs nominal and must not be negative")
1508
1531
  return self
1509
1532
 
1510
1533
 
@@ -259,7 +259,8 @@ def _parse_joint(el: ET.Element) -> Joint:
259
259
  )
260
260
  limit_el = el.find("limit")
261
261
  limits = None
262
- if limit_el is not None:
262
+ # A mimic joint follows its parent and has no limits of its own (OnRobot's RG6 ships 0/0/effort 0).
263
+ if limit_el is not None and el.find("mimic") is None:
263
264
  lower_raw, upper_raw = limit_el.get("lower"), limit_el.get("upper")
264
265
  effort_raw, velocity_raw = limit_el.get("effort"), limit_el.get("velocity")
265
266
  limits = JointLimits(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes