realbench-robotics 0.2.1__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.
- realbench_robotics-0.2.1/LICENSE +21 -0
- realbench_robotics-0.2.1/PKG-INFO +146 -0
- realbench_robotics-0.2.1/README.md +115 -0
- realbench_robotics-0.2.1/pyproject.toml +54 -0
- realbench_robotics-0.2.1/setup.cfg +4 -0
- realbench_robotics-0.2.1/src/realbench/__init__.py +78 -0
- realbench_robotics-0.2.1/src/realbench/acknowledgements.py +216 -0
- realbench_robotics-0.2.1/src/realbench/analyze.py +318 -0
- realbench_robotics-0.2.1/src/realbench/capture.py +208 -0
- realbench_robotics-0.2.1/src/realbench/checks.py +1234 -0
- realbench_robotics-0.2.1/src/realbench/cli.py +847 -0
- realbench_robotics-0.2.1/src/realbench/components.py +641 -0
- realbench_robotics-0.2.1/src/realbench/discovery.py +357 -0
- realbench_robotics-0.2.1/src/realbench/doctor.py +829 -0
- realbench_robotics-0.2.1/src/realbench/export.py +71 -0
- realbench_robotics-0.2.1/src/realbench/gazebo_source.py +395 -0
- realbench_robotics-0.2.1/src/realbench/home_resolve.py +31 -0
- realbench_robotics-0.2.1/src/realbench/messages.py +181 -0
- realbench_robotics-0.2.1/src/realbench/mjcf_model.py +169 -0
- realbench_robotics-0.2.1/src/realbench/orchestrator.py +231 -0
- realbench_robotics-0.2.1/src/realbench/paths.py +35 -0
- realbench_robotics-0.2.1/src/realbench/recorder.py +542 -0
- realbench_robotics-0.2.1/src/realbench/records.py +1007 -0
- realbench_robotics-0.2.1/src/realbench/report.py +613 -0
- realbench_robotics-0.2.1/src/realbench/reset.py +105 -0
- realbench_robotics-0.2.1/src/realbench/robot_model.py +56 -0
- realbench_robotics-0.2.1/src/realbench/ros2_source.py +1924 -0
- realbench_robotics-0.2.1/src/realbench/sensor_extract.py +180 -0
- realbench_robotics-0.2.1/src/realbench/state.py +48 -0
- realbench_robotics-0.2.1/src/realbench/state_source.py +515 -0
- realbench_robotics-0.2.1/src/realbench/urdf_model.py +298 -0
- realbench_robotics-0.2.1/src/realbench/verdict.py +95 -0
- realbench_robotics-0.2.1/src/realbench/versions.py +170 -0
- realbench_robotics-0.2.1/src/realbench/web/__init__.py +1 -0
- realbench_robotics-0.2.1/src/realbench/web/app.py +1280 -0
- realbench_robotics-0.2.1/src/realbench/web/cloud.py +97 -0
- realbench_robotics-0.2.1/src/realbench/web/environment.py +54 -0
- realbench_robotics-0.2.1/src/realbench/web/runs.py +127 -0
- realbench_robotics-0.2.1/src/realbench/web/scan.py +149 -0
- realbench_robotics-0.2.1/src/realbench/web/serve.py +97 -0
- realbench_robotics-0.2.1/src/realbench/web/static/assets/index-Cn25aiky.js +9 -0
- realbench_robotics-0.2.1/src/realbench/web/static/assets/index-RodZfQe3.css +1 -0
- realbench_robotics-0.2.1/src/realbench/web/static/favicon.svg +1 -0
- realbench_robotics-0.2.1/src/realbench/web/static/icons.svg +24 -0
- realbench_robotics-0.2.1/src/realbench/web/static/index.html +16 -0
- realbench_robotics-0.2.1/src/realbench/web/static/realbench_logo.png +0 -0
- realbench_robotics-0.2.1/src/realbench_robotics.egg-info/PKG-INFO +146 -0
- realbench_robotics-0.2.1/src/realbench_robotics.egg-info/SOURCES.txt +78 -0
- realbench_robotics-0.2.1/src/realbench_robotics.egg-info/dependency_links.txt +1 -0
- realbench_robotics-0.2.1/src/realbench_robotics.egg-info/entry_points.txt +2 -0
- realbench_robotics-0.2.1/src/realbench_robotics.egg-info/requires.txt +15 -0
- realbench_robotics-0.2.1/src/realbench_robotics.egg-info/top_level.txt +1 -0
- realbench_robotics-0.2.1/tests/test_analyze.py +121 -0
- realbench_robotics-0.2.1/tests/test_bench_api.py +261 -0
- realbench_robotics-0.2.1/tests/test_capture_home.py +176 -0
- realbench_robotics-0.2.1/tests/test_checks.py +493 -0
- realbench_robotics-0.2.1/tests/test_cli.py +158 -0
- realbench_robotics-0.2.1/tests/test_components.py +618 -0
- realbench_robotics-0.2.1/tests/test_doctor.py +301 -0
- realbench_robotics-0.2.1/tests/test_dwell.py +113 -0
- realbench_robotics-0.2.1/tests/test_error_messages.py +318 -0
- realbench_robotics-0.2.1/tests/test_export.py +106 -0
- realbench_robotics-0.2.1/tests/test_field_loss.py +220 -0
- realbench_robotics-0.2.1/tests/test_gazebo_source.py +59 -0
- realbench_robotics-0.2.1/tests/test_mjcf_model.py +140 -0
- realbench_robotics-0.2.1/tests/test_orchestrator.py +161 -0
- realbench_robotics-0.2.1/tests/test_recorder.py +554 -0
- realbench_robotics-0.2.1/tests/test_recorder_ros2_live.py +99 -0
- realbench_robotics-0.2.1/tests/test_report.py +137 -0
- realbench_robotics-0.2.1/tests/test_ros2_source.py +433 -0
- realbench_robotics-0.2.1/tests/test_sensor_arrival.py +161 -0
- realbench_robotics-0.2.1/tests/test_setup.py +459 -0
- realbench_robotics-0.2.1/tests/test_setup_incomplete.py +345 -0
- realbench_robotics-0.2.1/tests/test_source_capabilities.py +96 -0
- realbench_robotics-0.2.1/tests/test_state_source.py +91 -0
- realbench_robotics-0.2.1/tests/test_timeline.py +202 -0
- realbench_robotics-0.2.1/tests/test_urdf_model.py +95 -0
- realbench_robotics-0.2.1/tests/test_verdict.py +149 -0
- realbench_robotics-0.2.1/tests/test_versions.py +342 -0
- realbench_robotics-0.2.1/tests/test_web.py +214 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Raghavan Madabushi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: realbench-robotics
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Fail-closed robot readiness checks for integration-test reset cycles
|
|
5
|
+
Author-email: Raghavan Madabushi <raghavanmadabushi10@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/realbench-robotics/
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: yourdfpy>=0.0.56
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Requires-Dist: rich>=13
|
|
20
|
+
Requires-Dist: typer>=0.12
|
|
21
|
+
Requires-Dist: fastapi>=0.110
|
|
22
|
+
Requires-Dist: uvicorn[standard]>=0.27
|
|
23
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
24
|
+
Requires-Dist: defusedxml>=0.7.1
|
|
25
|
+
Requires-Dist: mujoco>=3.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: httpx; extra == "dev"
|
|
29
|
+
Provides-Extra: ros2
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# RealBench
|
|
33
|
+
|
|
34
|
+
Fail-closed readiness checks that answer: is this robot verifiably ready for the next run?
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install realbench-robotics
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The import package and CLI remain `realbench`. `pip install realbench-robotics[ros2]` does not install `rclpy`; that comes from your ROS2 distro.
|
|
43
|
+
|
|
44
|
+
## ROS2 warning
|
|
45
|
+
|
|
46
|
+
A positions-only ROS2 connection **cannot reach READY**. Faults and actuator state stay unverifiable, and the run quarantines by design.
|
|
47
|
+
|
|
48
|
+
This is the command that can reach READY:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
realbench check --urdf robot.urdf --source ros2 \
|
|
52
|
+
--ros2-topic /joint_states \
|
|
53
|
+
--diagnostics-topic /diagnostics \
|
|
54
|
+
--controller-manager /controller_manager
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Run `realbench doctor --urdf robot.urdf --source ros2` first if anything is missing.
|
|
58
|
+
|
|
59
|
+
`realbench check --source ros2` with no sensors or nodes declared exits 3
|
|
60
|
+
(usage error) and does not print a readiness verdict. Open the Components
|
|
61
|
+
tab and save, or run `realbench capture-components`.
|
|
62
|
+
|
|
63
|
+
## Quickstart
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
realbench analyze --urdf robot.urdf
|
|
67
|
+
realbench doctor --urdf robot.urdf --source ros2
|
|
68
|
+
realbench capture-home --urdf robot.urdf --source ros2 --repo .
|
|
69
|
+
realbench check --urdf robot.urdf --source ros2 \
|
|
70
|
+
--diagnostics-topic /diagnostics \
|
|
71
|
+
--controller-manager /controller_manager
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`--urdf` accepts a URDF or MJCF file. `analyze` needs only the model. `doctor` diagnoses setup and never produces a readiness verdict. `capture-home` writes `<repo>/.realbench/home.json`. `check` and `cycle` look for that file when `--home` is omitted.
|
|
75
|
+
|
|
76
|
+
## Serve
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
realbench serve --source ros2 --host 0.0.0.0 --no-browser
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
No `--urdf`. The model is read from the latched `*/robot_description` topic
|
|
83
|
+
(TRANSIENT_LOCAL `std_msgs/String`). Joint states, diagnostics, and
|
|
84
|
+
controller manager are inferred from the graph, including namespaced
|
|
85
|
+
paths such as `/JK3/platform/joint_states`. Continuous-joint robots skip
|
|
86
|
+
home pose.
|
|
87
|
+
|
|
88
|
+
Until at least one sensor or node is declared, the stored verdict is
|
|
89
|
+
**SETUP INCOMPLETE**, not QUARANTINE. The Components tab pre-ticks
|
|
90
|
+
discoverable sensors and nodes so save is one click. Saving a LaserScan
|
|
91
|
+
or Imu also writes a default `value_field` and plausible range so those
|
|
92
|
+
checks evaluate (PASS or FAIL), not NOT_APPLICABLE.
|
|
93
|
+
|
|
94
|
+
A failing diagnostic, sensor, or node can be marked a known issue with a
|
|
95
|
+
required note. Acknowledged rows stay recorded and visible; they do not
|
|
96
|
+
drive `classify()` until the reason string changes.
|
|
97
|
+
|
|
98
|
+
Host wall-clock is stored next to robot time. If they disagree by more
|
|
99
|
+
than 5 seconds, `check_clock_sane` fails at HEALTH and the UI shows both
|
|
100
|
+
clocks.
|
|
101
|
+
|
|
102
|
+
`check_state_stream_fresh` uses 3× the observed `joint_states` period and
|
|
103
|
+
3× the recorder sample interval, floored at 2.0s.
|
|
104
|
+
|
|
105
|
+
## Export
|
|
106
|
+
|
|
107
|
+
Do not copy `.realbench/` while serve is running. The recording database
|
|
108
|
+
is WAL-mode; a folder copy drops the `-wal` file.
|
|
109
|
+
|
|
110
|
+
History lives in `<repo>/.realbench/records.db`. There is no separate
|
|
111
|
+
`runs.db` on the serve path.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
realbench export
|
|
115
|
+
realbench export --out capture.db --repo .
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
SQLite's backup API writes one consistent file, prints path and size, and
|
|
119
|
+
only reports success if `integrity_check` passes. Safe while serve is up.
|
|
120
|
+
|
|
121
|
+
## Exit codes
|
|
122
|
+
|
|
123
|
+
| Code | Meaning |
|
|
124
|
+
|------|---------|
|
|
125
|
+
| 0 | READY |
|
|
126
|
+
| 1 | NOT_READY_RECOVERABLE (pose can be reset) |
|
|
127
|
+
| 2 | QUARANTINE (liveness/health failed or unverifiable) |
|
|
128
|
+
| 3 | Could not run (missing URDF, home, source, or undeclared ROS2 components) |
|
|
129
|
+
|
|
130
|
+
`doctor` uses 0 (no blocking setup problems), 1 (problems that would block a useful verdict), and 3 (could not run).
|
|
131
|
+
|
|
132
|
+
## CI
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
realbench check --urdf robot.urdf --source ros2 --diagnostics-topic /diagnostics --controller-manager /controller_manager || { echo "robot not ready, aborting run"; exit 1; }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Read-only
|
|
139
|
+
|
|
140
|
+
Read-only. RealBench never commands motion, never writes firmware, and never modifies your robot.
|
|
141
|
+
|
|
142
|
+
## Known limits
|
|
143
|
+
|
|
144
|
+
- Reset execution is stubbed.
|
|
145
|
+
- Sensor and firmware checks are not implemented.
|
|
146
|
+
- The ROS2 path is validated against a Dockerised Jazzy node, not physical hardware.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# RealBench
|
|
2
|
+
|
|
3
|
+
Fail-closed readiness checks that answer: is this robot verifiably ready for the next run?
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install realbench-robotics
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The import package and CLI remain `realbench`. `pip install realbench-robotics[ros2]` does not install `rclpy`; that comes from your ROS2 distro.
|
|
12
|
+
|
|
13
|
+
## ROS2 warning
|
|
14
|
+
|
|
15
|
+
A positions-only ROS2 connection **cannot reach READY**. Faults and actuator state stay unverifiable, and the run quarantines by design.
|
|
16
|
+
|
|
17
|
+
This is the command that can reach READY:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
realbench check --urdf robot.urdf --source ros2 \
|
|
21
|
+
--ros2-topic /joint_states \
|
|
22
|
+
--diagnostics-topic /diagnostics \
|
|
23
|
+
--controller-manager /controller_manager
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Run `realbench doctor --urdf robot.urdf --source ros2` first if anything is missing.
|
|
27
|
+
|
|
28
|
+
`realbench check --source ros2` with no sensors or nodes declared exits 3
|
|
29
|
+
(usage error) and does not print a readiness verdict. Open the Components
|
|
30
|
+
tab and save, or run `realbench capture-components`.
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
realbench analyze --urdf robot.urdf
|
|
36
|
+
realbench doctor --urdf robot.urdf --source ros2
|
|
37
|
+
realbench capture-home --urdf robot.urdf --source ros2 --repo .
|
|
38
|
+
realbench check --urdf robot.urdf --source ros2 \
|
|
39
|
+
--diagnostics-topic /diagnostics \
|
|
40
|
+
--controller-manager /controller_manager
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`--urdf` accepts a URDF or MJCF file. `analyze` needs only the model. `doctor` diagnoses setup and never produces a readiness verdict. `capture-home` writes `<repo>/.realbench/home.json`. `check` and `cycle` look for that file when `--home` is omitted.
|
|
44
|
+
|
|
45
|
+
## Serve
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
realbench serve --source ros2 --host 0.0.0.0 --no-browser
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
No `--urdf`. The model is read from the latched `*/robot_description` topic
|
|
52
|
+
(TRANSIENT_LOCAL `std_msgs/String`). Joint states, diagnostics, and
|
|
53
|
+
controller manager are inferred from the graph, including namespaced
|
|
54
|
+
paths such as `/JK3/platform/joint_states`. Continuous-joint robots skip
|
|
55
|
+
home pose.
|
|
56
|
+
|
|
57
|
+
Until at least one sensor or node is declared, the stored verdict is
|
|
58
|
+
**SETUP INCOMPLETE**, not QUARANTINE. The Components tab pre-ticks
|
|
59
|
+
discoverable sensors and nodes so save is one click. Saving a LaserScan
|
|
60
|
+
or Imu also writes a default `value_field` and plausible range so those
|
|
61
|
+
checks evaluate (PASS or FAIL), not NOT_APPLICABLE.
|
|
62
|
+
|
|
63
|
+
A failing diagnostic, sensor, or node can be marked a known issue with a
|
|
64
|
+
required note. Acknowledged rows stay recorded and visible; they do not
|
|
65
|
+
drive `classify()` until the reason string changes.
|
|
66
|
+
|
|
67
|
+
Host wall-clock is stored next to robot time. If they disagree by more
|
|
68
|
+
than 5 seconds, `check_clock_sane` fails at HEALTH and the UI shows both
|
|
69
|
+
clocks.
|
|
70
|
+
|
|
71
|
+
`check_state_stream_fresh` uses 3× the observed `joint_states` period and
|
|
72
|
+
3× the recorder sample interval, floored at 2.0s.
|
|
73
|
+
|
|
74
|
+
## Export
|
|
75
|
+
|
|
76
|
+
Do not copy `.realbench/` while serve is running. The recording database
|
|
77
|
+
is WAL-mode; a folder copy drops the `-wal` file.
|
|
78
|
+
|
|
79
|
+
History lives in `<repo>/.realbench/records.db`. There is no separate
|
|
80
|
+
`runs.db` on the serve path.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
realbench export
|
|
84
|
+
realbench export --out capture.db --repo .
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
SQLite's backup API writes one consistent file, prints path and size, and
|
|
88
|
+
only reports success if `integrity_check` passes. Safe while serve is up.
|
|
89
|
+
|
|
90
|
+
## Exit codes
|
|
91
|
+
|
|
92
|
+
| Code | Meaning |
|
|
93
|
+
|------|---------|
|
|
94
|
+
| 0 | READY |
|
|
95
|
+
| 1 | NOT_READY_RECOVERABLE (pose can be reset) |
|
|
96
|
+
| 2 | QUARANTINE (liveness/health failed or unverifiable) |
|
|
97
|
+
| 3 | Could not run (missing URDF, home, source, or undeclared ROS2 components) |
|
|
98
|
+
|
|
99
|
+
`doctor` uses 0 (no blocking setup problems), 1 (problems that would block a useful verdict), and 3 (could not run).
|
|
100
|
+
|
|
101
|
+
## CI
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
realbench check --urdf robot.urdf --source ros2 --diagnostics-topic /diagnostics --controller-manager /controller_manager || { echo "robot not ready, aborting run"; exit 1; }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Read-only
|
|
108
|
+
|
|
109
|
+
Read-only. RealBench never commands motion, never writes firmware, and never modifies your robot.
|
|
110
|
+
|
|
111
|
+
## Known limits
|
|
112
|
+
|
|
113
|
+
- Reset execution is stubbed.
|
|
114
|
+
- Sensor and firmware checks are not implemented.
|
|
115
|
+
- The ROS2 path is validated against a Dockerised Jazzy node, not physical hardware.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "realbench-robotics"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Fail-closed robot readiness checks for integration-test reset cycles"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Raghavan Madabushi", email = "raghavanmadabushi10@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Topic :: Scientific/Engineering",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"yourdfpy>=0.0.56",
|
|
25
|
+
"numpy>=1.24",
|
|
26
|
+
"rich>=13",
|
|
27
|
+
"typer>=0.12",
|
|
28
|
+
"fastapi>=0.110",
|
|
29
|
+
"uvicorn[standard]>=0.27",
|
|
30
|
+
"python-multipart>=0.0.9",
|
|
31
|
+
"defusedxml>=0.7.1",
|
|
32
|
+
"mujoco>=3.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://pypi.org/project/realbench-robotics/"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = ["pytest", "httpx"]
|
|
40
|
+
# rclpy is provided by the ROS2 distro, not pip; this extra is a marker only.
|
|
41
|
+
ros2 = []
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
realbench = "realbench.cli:app"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
realbench = ["web/static/*", "web/static/assets/*"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
pythonpath = ["src"]
|
|
54
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""RealBench: fail-closed readiness checks for robot integration-test reset cycles."""
|
|
2
|
+
|
|
3
|
+
from realbench.orchestrator import CycleReport, evaluate_readiness, run_cycle
|
|
4
|
+
from realbench.report import exit_code_for, render_terminal, to_json
|
|
5
|
+
from realbench.reset import MockResetExecutor, ResetExecutor, ResetOutcome
|
|
6
|
+
from realbench.verdict import ReadinessReport, Verdict, classify
|
|
7
|
+
from realbench.checks import (
|
|
8
|
+
ALL_CHECKS,
|
|
9
|
+
CheckConfig,
|
|
10
|
+
CheckResult,
|
|
11
|
+
CheckStatus,
|
|
12
|
+
CheckTier,
|
|
13
|
+
run_all_checks,
|
|
14
|
+
)
|
|
15
|
+
from realbench.gazebo_source import (
|
|
16
|
+
GazeboStateSource,
|
|
17
|
+
GzLiveStateSource,
|
|
18
|
+
parse_gz_joint_state,
|
|
19
|
+
)
|
|
20
|
+
from realbench.ros2_source import Ros2StateSource, Ros2Unavailable
|
|
21
|
+
from realbench.state import RobotState
|
|
22
|
+
from realbench.state_source import (
|
|
23
|
+
MockStateSource,
|
|
24
|
+
SourceCapabilities,
|
|
25
|
+
StateLoadError,
|
|
26
|
+
StateSource,
|
|
27
|
+
coverage_warning,
|
|
28
|
+
empty_robot_state,
|
|
29
|
+
)
|
|
30
|
+
from realbench.urdf_model import (
|
|
31
|
+
HomePoseError,
|
|
32
|
+
JointLimit,
|
|
33
|
+
ModelProvenance,
|
|
34
|
+
UrdfLoadError,
|
|
35
|
+
UrdfModel,
|
|
36
|
+
load_urdf_model,
|
|
37
|
+
)
|
|
38
|
+
from realbench.robot_model import load_robot_model
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"ALL_CHECKS",
|
|
42
|
+
"CheckConfig",
|
|
43
|
+
"CheckResult",
|
|
44
|
+
"CheckStatus",
|
|
45
|
+
"CheckTier",
|
|
46
|
+
"CycleReport",
|
|
47
|
+
"GazeboStateSource",
|
|
48
|
+
"GzLiveStateSource",
|
|
49
|
+
"HomePoseError",
|
|
50
|
+
"JointLimit",
|
|
51
|
+
"ModelProvenance",
|
|
52
|
+
"MockResetExecutor",
|
|
53
|
+
"MockStateSource",
|
|
54
|
+
"ReadinessReport",
|
|
55
|
+
"ResetExecutor",
|
|
56
|
+
"Ros2StateSource",
|
|
57
|
+
"Ros2Unavailable",
|
|
58
|
+
"ResetOutcome",
|
|
59
|
+
"RobotState",
|
|
60
|
+
"SourceCapabilities",
|
|
61
|
+
"StateLoadError",
|
|
62
|
+
"StateSource",
|
|
63
|
+
"coverage_warning",
|
|
64
|
+
"empty_robot_state",
|
|
65
|
+
"UrdfLoadError",
|
|
66
|
+
"UrdfModel",
|
|
67
|
+
"Verdict",
|
|
68
|
+
"classify",
|
|
69
|
+
"evaluate_readiness",
|
|
70
|
+
"exit_code_for",
|
|
71
|
+
"load_robot_model",
|
|
72
|
+
"load_urdf_model",
|
|
73
|
+
"parse_gz_joint_state",
|
|
74
|
+
"render_terminal",
|
|
75
|
+
"run_all_checks",
|
|
76
|
+
"run_cycle",
|
|
77
|
+
"to_json",
|
|
78
|
+
]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Known-issue acknowledgements. classify() is unchanged; copies are filtered here."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
|
|
8
|
+
from realbench.checks import CheckResult, CheckStatus
|
|
9
|
+
from realbench.components import (
|
|
10
|
+
Acknowledgement,
|
|
11
|
+
ComponentsDeclaration,
|
|
12
|
+
rewrite_acknowledgements,
|
|
13
|
+
)
|
|
14
|
+
from realbench.state import RobotState
|
|
15
|
+
|
|
16
|
+
_BLOCKING = {CheckStatus.FAIL, CheckStatus.UNVERIFIABLE}
|
|
17
|
+
_MEMBER_CHECKS = frozenset(
|
|
18
|
+
{
|
|
19
|
+
"check_sensors_reporting",
|
|
20
|
+
"check_sensors_plausible",
|
|
21
|
+
"check_sensors_changing",
|
|
22
|
+
"check_nodes_alive",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def split_fault(fault: str) -> tuple[str, str]:
|
|
28
|
+
"""Split ``\"name: message\"`` from diagnostics_to_faults."""
|
|
29
|
+
|
|
30
|
+
if ": " in fault:
|
|
31
|
+
name, message = fault.split(": ", 1)
|
|
32
|
+
return name, message
|
|
33
|
+
return fault, ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def current_failing_reasons(
|
|
37
|
+
state: RobotState,
|
|
38
|
+
checks: Sequence[CheckResult],
|
|
39
|
+
) -> dict[str, set[str]]:
|
|
40
|
+
"""Map component id → current failing reason strings.
|
|
41
|
+
|
|
42
|
+
One component can fail in more than one check (reporting vs changing).
|
|
43
|
+
Keeping a set is what lets a written ack be read back instead of
|
|
44
|
+
being overwritten by the later check.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
reasons: dict[str, set[str]] = {}
|
|
48
|
+
for fault in state.faults or ():
|
|
49
|
+
name, _message = split_fault(fault)
|
|
50
|
+
reasons.setdefault(name, set()).add(fault)
|
|
51
|
+
for check in checks:
|
|
52
|
+
for component_id, reason in _failing_members(check):
|
|
53
|
+
reasons.setdefault(component_id, set()).add(reason)
|
|
54
|
+
return reasons
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def fault_is_acknowledged(
|
|
58
|
+
fault: str,
|
|
59
|
+
acknowledgements: Mapping[str, Acknowledgement],
|
|
60
|
+
) -> bool:
|
|
61
|
+
name, message = split_fault(fault)
|
|
62
|
+
for component_id, ack in acknowledgements.items():
|
|
63
|
+
if ack.reason == fault:
|
|
64
|
+
return True
|
|
65
|
+
if component_id == name and ack.reason in (fault, message):
|
|
66
|
+
return True
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def member_is_acknowledged(
|
|
71
|
+
component_id: str,
|
|
72
|
+
reason: str,
|
|
73
|
+
acknowledgements: Mapping[str, Acknowledgement],
|
|
74
|
+
) -> bool:
|
|
75
|
+
del reason
|
|
76
|
+
return component_id in acknowledgements
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def acknowledgement_is_stale(
|
|
80
|
+
ack: Acknowledgement,
|
|
81
|
+
current_reasons: Mapping[str, set[str]],
|
|
82
|
+
faults: Sequence[str],
|
|
83
|
+
) -> bool:
|
|
84
|
+
"""True when this id is currently failing with a different reason."""
|
|
85
|
+
|
|
86
|
+
currents = current_reasons.get(ack.component_id)
|
|
87
|
+
if currents:
|
|
88
|
+
return ack.reason not in currents
|
|
89
|
+
related = [
|
|
90
|
+
fault
|
|
91
|
+
for fault in faults
|
|
92
|
+
if split_fault(fault)[0] == ack.component_id or ack.reason == fault
|
|
93
|
+
]
|
|
94
|
+
if not related:
|
|
95
|
+
return False
|
|
96
|
+
return not any(
|
|
97
|
+
ack.reason == fault or ack.reason == split_fault(fault)[1] for fault in related
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def prune_stale_acknowledgements(
|
|
102
|
+
declaration: ComponentsDeclaration | None,
|
|
103
|
+
state: RobotState,
|
|
104
|
+
checks: Sequence[CheckResult],
|
|
105
|
+
) -> ComponentsDeclaration | None:
|
|
106
|
+
"""Drop acks whose stored reason no longer matches. Writes the file."""
|
|
107
|
+
|
|
108
|
+
if declaration is None or not declaration.acknowledgements:
|
|
109
|
+
return declaration
|
|
110
|
+
reasons = current_failing_reasons(state, checks)
|
|
111
|
+
faults = list(state.faults or ())
|
|
112
|
+
kept: dict[str, Acknowledgement] = {}
|
|
113
|
+
for component_id, ack in declaration.acknowledgements.items():
|
|
114
|
+
if acknowledgement_is_stale(ack, reasons, faults):
|
|
115
|
+
continue
|
|
116
|
+
kept[component_id] = ack
|
|
117
|
+
if kept == dict(declaration.acknowledgements):
|
|
118
|
+
return declaration
|
|
119
|
+
updated = replace(declaration, acknowledgements=kept)
|
|
120
|
+
if declaration.path is not None:
|
|
121
|
+
try:
|
|
122
|
+
rewrite_acknowledgements(declaration.path, kept)
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
return updated
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def active_acknowledgements(
|
|
129
|
+
declaration: ComponentsDeclaration | None,
|
|
130
|
+
state: RobotState,
|
|
131
|
+
checks: Sequence[CheckResult],
|
|
132
|
+
) -> dict[str, Acknowledgement]:
|
|
133
|
+
"""Acks that currently match a failing reason and suppress classify input."""
|
|
134
|
+
|
|
135
|
+
if declaration is None:
|
|
136
|
+
return {}
|
|
137
|
+
reasons = current_failing_reasons(state, checks)
|
|
138
|
+
faults = list(state.faults or ())
|
|
139
|
+
active: dict[str, Acknowledgement] = {}
|
|
140
|
+
for component_id, ack in declaration.acknowledgements.items():
|
|
141
|
+
if acknowledgement_is_stale(ack, reasons, faults):
|
|
142
|
+
continue
|
|
143
|
+
currents = reasons.get(component_id)
|
|
144
|
+
if currents and ack.reason in currents:
|
|
145
|
+
active[component_id] = ack
|
|
146
|
+
continue
|
|
147
|
+
if any(fault_is_acknowledged(fault, {component_id: ack}) for fault in faults):
|
|
148
|
+
active[component_id] = ack
|
|
149
|
+
return active
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def known_issues_acknowledged(
|
|
153
|
+
declaration: ComponentsDeclaration | None,
|
|
154
|
+
state: RobotState,
|
|
155
|
+
checks: Sequence[CheckResult],
|
|
156
|
+
) -> int:
|
|
157
|
+
return len(active_acknowledgements(declaration, state, checks))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def checks_for_verdict(
|
|
161
|
+
checks: Sequence[CheckResult],
|
|
162
|
+
state: RobotState,
|
|
163
|
+
acknowledgements: Mapping[str, Acknowledgement] | None,
|
|
164
|
+
) -> list[CheckResult]:
|
|
165
|
+
"""Copies for classify(). Original check objects are not mutated."""
|
|
166
|
+
|
|
167
|
+
acks = acknowledgements or {}
|
|
168
|
+
out: list[CheckResult] = []
|
|
169
|
+
for check in checks:
|
|
170
|
+
if check.status not in _BLOCKING:
|
|
171
|
+
out.append(check)
|
|
172
|
+
continue
|
|
173
|
+
if check.name == "check_no_latched_faults":
|
|
174
|
+
faults = state.faults
|
|
175
|
+
if (
|
|
176
|
+
isinstance(faults, list)
|
|
177
|
+
and len(faults) > 0
|
|
178
|
+
and all(fault_is_acknowledged(item, acks) for item in faults)
|
|
179
|
+
):
|
|
180
|
+
out.append(replace(check, status=CheckStatus.PASS))
|
|
181
|
+
else:
|
|
182
|
+
out.append(check)
|
|
183
|
+
continue
|
|
184
|
+
if check.name in _MEMBER_CHECKS:
|
|
185
|
+
members = _failing_members(check)
|
|
186
|
+
if members and all(
|
|
187
|
+
member_is_acknowledged(component_id, reason, acks)
|
|
188
|
+
for component_id, reason in members
|
|
189
|
+
):
|
|
190
|
+
out.append(replace(check, status=CheckStatus.PASS))
|
|
191
|
+
else:
|
|
192
|
+
out.append(check)
|
|
193
|
+
continue
|
|
194
|
+
out.append(check)
|
|
195
|
+
return out
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _failing_members(check: CheckResult) -> list[tuple[str, str]]:
|
|
199
|
+
observed = check.observed
|
|
200
|
+
if not isinstance(observed, dict):
|
|
201
|
+
return []
|
|
202
|
+
components = observed.get("components")
|
|
203
|
+
if not isinstance(components, dict):
|
|
204
|
+
return []
|
|
205
|
+
members: list[tuple[str, str]] = []
|
|
206
|
+
for component_id, spec in components.items():
|
|
207
|
+
if not isinstance(spec, dict):
|
|
208
|
+
continue
|
|
209
|
+
if spec.get("required") is False:
|
|
210
|
+
continue
|
|
211
|
+
status = spec.get("status")
|
|
212
|
+
if status in ("FAIL", "UNVERIFIABLE", CheckStatus.FAIL, CheckStatus.UNVERIFIABLE):
|
|
213
|
+
members.append(
|
|
214
|
+
(str(component_id), str(spec.get("reason") or check.reason))
|
|
215
|
+
)
|
|
216
|
+
return members
|