cyberwave-stress-test on the real robot. The tool ships with the Python SDK. It asks the twin what it can do, runs only the test sections that match, and decides pass or fail from what the robot measures, never from what was commanded. You write no per-robot test code.
Run it
python -m cyberwave.test.driver_testing.cli is the same command.
cyberwave-stress-test --help lists the step sizes, tolerances and timeouts.
How sections are chosen
Discovery reads four things from the twin before anything moves:- capabilities:
twin.capabilities; - command catalog:
twin.commands.get_schema(), itscommands.supportedlist and served flat MQTT topic paths (such ascyberwave/joint/{twin_uuid}/update); - observed state streams: subscribed
state/motionandstate/kinematicsmessages, even when the served catalog omits those topics; - joints and limits: the universal schema’s controllable joints, with their limits and home positions. Joints named like a gripper part (
grip,finger,jaw,knuckle, …), or prismatic joints after the arm, are the gripper; - planned-motion backends: the
kinematics_backendthatstate/motionreports, plus any--backends.
catalog lacks base_rotate_left, base_rotate_right. A section can also skip itself at run time, for example when a backend refuses with backend_unavailable.
What your driver needs, in SDK terms
Each section exercises what an SDK driver mixin gives your driver. Compose the mixin and its sections apply:
A driver that exposes
ee_pose should also compose CartesianStateMixin with use_cartesian_state = True. Without the measured TCP, nothing can check where an absolute pose landed, and ee_pose is reported untested.
Every command is tested
Every command in the driver’scommands.supported must be exercised by at least one section. --list ends with a table of every catalog command:
- tested: the sections that exercise it;
- not selected: only
--sectionsor--skipleft it out; - untested: no section exercises it, or every one that does is skipped for this twin, with the reason.
--allow-untested. After the run the table is recomputed: a section that skipped itself at run time (for example, a backend refusing with backend_unavailable) tested nothing, so its commands count as untested and the run fails. report.json carries the table under coverage.
A driver-specific command needs a section of its own: see Adding a section. Declare what it exercises with covers= when that differs from the commands it requires.
Sections
Every run starts with a preflight and ends with a return to the measured start.
Measured feedback and result matching
- Only
source_type=edgejoints and poses count. SDK target caches and teleop echoes never complete a section. - The pose is
state/kinematicswhen the driver publishes it (CartesianStateMixin: the TCP in the arm’s base frame), elsetwin/pose. With onlytwin/pose, which may be a flange rather than the TCP, sections move by relative displacements (mode: relativein the planning frame) and check the measured displacement.cartesian.ee_poseandplanned.replaceneed an absolute TCP target, so they are skipped, andee_posecounts as untested. - Commands carry no request id. A command’s motion is the first new
motion_idthatstate/motionreports for that command after it was sent. Its result is the one naming thatmotion_id. A refusal before a motion started has nomotion_idand is matched by command name. Every result is recognized by its uniquetimestamp_ms.
Safety model
- Preflight at the measured start: complete joint feedback, every joint inside its limits, the pose inside the workspace box, then
stopand a measured still arm. That posture is where the run returns to. - Joint limits: every joint target is clamped to the schema limits minus
--joint-limit-margin-rad(0.05). Steps turn toward the middle of the range. - Workspace box: a Cartesian target outside
--workspace-min/--workspace-maxis never sent; the section is skipped with the reason. - Speed limits: the tool refuses to start when the sine’s peak joint speed exceeds
--max-joint-speed-rad-s(0.25), a Cartesian step exceeds--max-cartesian-step-m(0.08), or a rotation step exceeds--max-rotation-step-rad(0.3). Joint teleop is capped at 10 Hz. The driver’s own velocity limits still apply. - After a failed section:
stop, then a measured hold, before anything else. - Skipped means nothing moved: a section checks everything that can skip it before it sends a motion. If a section, including one you add, skips after sending one, the harness still sends
stopand verifies a measured hold, and the report lists what was sent undersent_before_skip. - Every exit, Ctrl-C included, cancels any active waypoint mission, joins the teleop stream and sends
stop.
scene.collision_guardruns only when no task scene is active, or the active one is from an earlier stress run (its revision starts withstress-). It leaves an empty task scene behind.scene.payloadruns only when no payload is attached.
scene_set with no objects) or detach the payload to test them. obstacles.roundtrip restores the persisted world obstacle list. A killed process can skip that: check the scene before the next run.
With only twin/pose, the fixture box is centred on the reported link. Make --probe-box-m larger than twice the link-to-TCP distance so the box contains the TCP.
Reading the report
--report writes three files:
report.json:outcome(passed,failedorinterrupted) anderror;config, the arguments;profile, what discovery found;plan, every section withplannedorskippedand the reason;phases, every section as it ran:passedwith its metrics,failedwith its error, orskippedwith its reason;skipped, the sections that never ran;coverage, every catalog command as tested, not selected or untested.
events.jsonl: the trace. Every measured sample, sent target and command, and motion status, with local monotonic and UTC receipt times.joints.csv: measured joints and transmitted targets for plotting. A blank field in a target row is an axis that target did not address, not zero.
--allow-untested).
Setpoint RMSE and feedback gaps in the sine metrics are diagnostics, not pass gates: MQTT can delay samples while the arm keeps moving.
Adding a section for a new capability
A section is one decorated function. It declares what it needs and drives the twin through aStressHarness:
resolve_plan runs, and the section appears in --list. Use the harness’s primitives so the section gets the same guarantees:
commandsends a catalog command;set_jointssends clamped, rate-limited joint targets;move/stepsendset_pose;outcome/resultmatch a command’s result bymotion_id;wait_joints,wait_poseandwait_stillwait for fresh measured feedback;check_workspacerefuses a target outside the box.
StressHarness and overrides what only that robot has, for example a fixture of its own workcell, on top of the generic sections.
Testing the test
The registry and discovery are pure:profile_from builds a TwinProfile from a capabilities dict, a catalog and a universal schema, and resolve_plan turns it into a plan. Unit-test your own sections the same way the SDK does: give the harness a fake twin that publishes measured feedback.
Related: Arm motion building blocks, Integrating MoveIt.