blacknode-robot/core owns stable robot contracts, profiles, discovery,
hardware-identity calibration, capability bindings, and process descriptors.
Physical driver implementations are selectable blacknode-drivers components.
The existing Feetech script path remains as a compatibility launcher for saved
profiles and delegates to the ros2 adapter nested under the feetech
component in blacknode-drivers.
Blackndoerobotvisiondemo2-github.mp4
Generic robot setup nodes for Blacknode.
Install this Blacknode extension package to add robot hardware setup to the visual workflow editor: find USB serial robot devices, fix Linux serial permissions, launch and stop a driver process, and emit reusable robot profiles for downstream control nodes — drivable from workflows or AI agents over MCP.
This package owns the user-facing robot abstraction:
- find USB serial robot devices
- explain Linux serial permissions
- describe how a robot driver should start
- start/stop a driver process
- build, save, duplicate, load, and calibrate reusable robot profiles
- bind semantic capabilities to replaceable package components
- inspect each provider as available, unavailable, or unhealthy
Robot-specific packages provide protocol driver descriptors and hardware
bridges. Transport packages such as
blacknode-ros2 verify and control the standard interface exposed by the
driver.
Coding agents should read AGENTS.md before changing this package.
It defines the package boundary, calibration identity contract, motion safety,
and verification commands.
| Node | What it does |
|---|---|
RobotDriverDescriptor |
Declares a driver command template and standard topics |
Robot |
Selects a robot, automatically finds its connection, applies calibration, and optionally starts its driver |
RobotJointDefinition |
Defines one named joint, servo ID, range, zero, and direction |
RobotJointList |
Combines any number of joint definitions; another socket appears as the list fills |
RobotDefinition |
Builds a reusable robot profile and driver contract visually |
RobotProfileSave |
Saves a profile under robots/<profile_id>/profile.json |
RobotProfileLoad |
Loads a profile and the calibration for a connected hardware ID |
RobotProfileList |
Lists built-in and locally saved profiles |
RobotProfileDuplicate |
Copies a built-in or local profile under a new editable name |
RobotCalibrationRecorder |
Safely records released-arm limits and a home pose for one physical robot |
RobotCapabilityBinding |
Binds one semantic capability to a replaceable package component, adapter, configuration, and optional hardware identity |
RobotAttachment |
Describes one mounted camera, depth camera, LiDAR, IMU, GPS, microphone, or custom peripheral with its ROS 2 interface, frame, transform, provider, and hardware identity |
RobotAttachmentList |
Collects physical attachments for storage in a reusable robot profile |
RobotCapabilityList |
Collects capability bindings for a robot profile |
RobotCapabilityProfile |
Attaches capability providers and stable hardware identity to a reusable robot profile |
RobotCapabilityInspect |
Resolves a profile against installed components and live provider reports as available, unavailable, or unhealthy |
RobotDriverLauncher |
Starts/stops a driver process from the descriptor |
RobotConnectionDashboard |
Shows USB, driver, ROS interface, live joint positions, home references, safe ranges, and calibration source in one view |
RobotMonitor |
Opens a read-only live canvas view for a registered robot's connection, motion state, telemetry, streams, and joints |
RobotROSInterfaceCheck |
Matches a live ROS graph to a supported robot interface profile without publishing commands |
Open Complete Robot Bringup and press Run. Its Robot node discovers
the connected hardware, selects the profile already bound to that physical
device, applies its calibration, and starts the driver with motion disarmed.
The remaining nodes inventory ROS 2 and report each declared capability as
available, unavailable, or unhealthy.
Automatic profile selection prefers a calibration saved for the connected hardware, then an exact physical identity, then an exact USB match. When only one profile exists, Blacknode selects it directly. If multiple profiles remain equally possible, the report asks for one profile selection instead of choosing a motion-capable definition by guesswork.
Joint-based profiles automatically expose position_feedback and
joint_group capability bindings using their configured state and command
topics. Other robot shapes declare only their own capabilities and ROS 2
interfaces. An arm does not imply a base, and a camera does not imply LiDAR.
The bringup workflow inventories and checks the declared interfaces; it never
authorizes or sends motion.
Changing the generic Robot.profile_id invalidates the old dashboard. Press
Run to apply it: if its generated driver command differs, Blacknode safely
stops the prior managed process before starting the selected profile. A
PROFILE DEFAULTS dashboard has no saved calibration for that profile and
hardware ID.
For one robot, use only Robot; the default profile_id=auto discovers the
available hardware and uses selection: 0. Duplicate it and choose
selection: 1 for a second robot. Bind each physical device to its saved
profile or calibration so selection remains stable when USB enumeration order
changes. Camera and future sensor facades use the same selection convention.
The selected entry's serial number (or port path when no serial is available)
becomes the robot's hardware_id; discovery's index-0 shortcut values never
override a different selected entry.
For remote deployment, declare the device capabilities and choose calibration
in the editor's Deployments panel. Activate on device binds the saved
calibration to the paired device while it is connected and disarmed. Staging
then embeds that exact robot profile and calibration in the workflow. The
remote Robot node applies the embedded home positions and safe ranges only
when discovery returns the same physical hardware identity.
The Properties panel keeps transport controls under Advanced. They are not
required for normal setup: probe_open actively opens candidate serial ports
for diagnostics, vendor/product IDs narrow discovery to a USB adapter model,
and hardware_filter pins a node to one stable adapter identity. Stable
identity is recommended after calibration or for unattended motion so two
identical robots cannot silently exchange roles. Hidden compatibility nodes
still provide low-level diagnostics for old workflows, but USB is an
implementation of a robot connection rather than the public abstraction.
The SO-ARM101 Leader Follower template uses robot indexes 0 and 1,
separate driver run IDs, and /leader and /follower ROS topic prefixes. It
releases only the leader, starts the follower controller in disarmed preview,
and requires saved calibration for both physical devices. For a permanent
installation, promote hardware_filter from Advanced and bind each role to its
adapter serial.
Its default configuration uses tracking_mode=direct at 60 Hz with no
deadband or relative step limiter. The Feetech driver batches
all joint reads and all goal writes into synchronized bus transactions, while
calibration limits, stale-stream suppression, and explicit arming still apply.
The generic pipeline is:
Robot -> discovered connection + calibrated profile + managed driver
Driver descriptors use {serial_port} as the placeholder for the discovered
USB path:
python scripts/my_robot_driver.py --port {serial_port}
The resulting robot profile carries:
state_topiccommand_topic- optional
config_topic usbdriverinterfaceattachments, including stable attachment IDs, provider bindings, ROS 2 topics and message types, TF parent/child frames, mount transforms, and physical hardware identity
Robot-specific packages should fill in the descriptor. Transport packages should verify and use the interface.
Use the generic Robot node in new workflows. Its dropdown starts with
Auto, followed by built-in and locally saved profiles. It discovers the
selected connection, applies the matching calibration, and checks, starts, or
stops the driver itself. The old discovery and profile-loader types remain
hidden for workflow compatibility.
RobotDefinition.driver_script is also a dropdown populated from installed
drivers/*_driver.py files when Blacknode starts. Adding a driver file and
restarting Blacknode makes it selectable without changing the node. A custom
executable or non-Python launch path can still use protocol=custom with an
explicit command_template.
The curated SO-ARM101 preset uses the same profile schema as a visual custom robot. Supporting another arm on the bundled Feetech protocol normally means assembling joint nodes and saving a profile; it does not require changing Python. Only a genuinely new wire protocol needs another bus driver.
Open Editable SO-ARM101 Profile as a working example. Each
RobotJointDefinition names a stable joint and sets its servo ID, provisional
range, center tick, and direction. RobotJointList preserves their order;
RobotDefinition creates the profile; and RobotProfileSave makes it reusable.
To reuse the same mechanical definition under another identity, use
RobotProfileDuplicate with source_profile_id=so_arm101 and choose a new ID.
For structural changes, copy the Editable SO-ARM101 Profile workflow and
edit its visible joint nodes before saving. Profile and joint IDs normalize to lowercase
snake_case, limited to 64 characters, and must be unique. Display names are
free-form and can change without breaking workflows.
Open Robot Sensor Attachments to describe a camera, LiDAR, and IMU mounted
on one robot. Each RobotAttachment stores its physical identity, replaceable
provider, ROS 2 topic and message type, TF parent/child frames, and translation
plus roll/pitch/yaw mount transform. Add more attachment sockets through
RobotAttachmentList, then save the resulting profile. Edit the example topic
and frame names to match the live graph before checking or starting providers.
Connect Robot.hardware to RobotDefinition.hardware. The definition copies
the real USB vendor ID and product ID reported internally; these four-digit
values identify the hardware manufacturer/product and are not random robot IDs.
Manual vendor_id and product_id values remain advanced overrides. The
adapter serial—or its device path when no serial exists—selects the calibration
for one physical assembly.
Local robot data is deliberately separate from the package source:
robots/
my_robot/
profile.json
calibrations/
usb_serial_or_device_id.json
Set BLACKNODE_ROBOTS_DIR to move this library elsewhere. The default
robots/ directory is ignored by Git because calibrations describe a specific
physical assembly. Copy or version it deliberately when sharing a machine
configuration.
Open Robot Guided Calibration after saving a profile:
- Enter a clear Calibration name, such as
Workshop armorLeft SO-ARM101. - Load the profile and start discovery with the robot connected.
- Press Release + live pose on Manual Move and physically support the arm.
- Confirm live joint values are changing, then press Start recording.
- Slowly move every joint through the safe physical range you intend to use. Do not force a hard stop.
- Put the robot in the pose that should read as zero and press Capture Home.
- Press Stop recording whenever you want to pause extrema collection without losing samples. Current pose remains live; press Resume recording to continue.
- Press Save calibration. The recorder applies the configured safety margin inside the observed extrema and saves the name with its hardware ID.
- Press Hold position only while the arm is supported and the workspace is clear.
Recording never commands movement. It refuses to start while torque is on, and
it will not save until every configured joint has been observed and a home pose
has been captured. Its CURRENT pose, observed ranges, sample count, dashboard,
report, and connected Output nodes update through the live runtime. Robot
automatically applies the matching
device calibration when given the discovery hardware output; another physical
robot with the same profile keeps a separate calibration.
While recording, the most strongly moving joint is labeled CAPTURING. Its
row turns blue, and a newly extended limit flashes amber with MIN ↓, MAX ↑,
or RANGE. This distinguishes ordinary motion inside an already observed range
from a sample that actually changed the saved extrema.
Drives a real SO-ARM101 —
6x Feetech STS3215 serial-bus servos (shoulder_pan, shoulder_lift,
elbow_flex, wrist_flex, wrist_roll, gripper; servo IDs 1-6; 1 Mbps)
— through drivers/feetech_bus_driver.py. The preset defaults to
transport=auto: it uses native rclpy when available and otherwise uses
rosbridge. The transport can still be forced for advanced deployments. In
rosbridge mode the serial connection remains local to the driver machine.
pip install -r packages/blacknode-robot/requirements.txt # servo SDK + roslibpy- Plug in the arm, then run
Robotwithaction=checkto confirm its connection is found and accessible. This confirms the adapter is enumerated, not that robot power or servo communication is healthy; use the driver connection state for that. - Load the SO-ARM101 Motion Test template
(
templates/so-arm101-motion-test.json):Robot(so_arm101, starts the driver) →ROS2Status→ROS2JointState→ROS2SetJoint(armed=falseby default) →ROS2MotionDashboard. - Press Run. With
armed=falsethis only proves the pipeline: the driver starts,ROS2Statusselects native ROS 2 or rosbridge, andROS2JointStateshows the arm's live pose. The arm must not move or twitch during this step — see Safety below for why. - Set a
jointname andarmed=trueonROS2SetJoint, recook. It syncs to the current pose, ramps to the target, and the dashboard shows before/after.
The motion-test template includes ROS2ManualMove between connection status and
motion. Its safe default is Monitor only, which changes no torque state and
starts an explicitly labeled live pose monitor.
- Press Release + live pose to disable servo torque while the driver keeps publishing joint positions. Support the arm before pressing it; it may go limp.
- Move the supported arm by hand. The Teach node's unconnected dashboard output refreshes from the runtime monitor and shows the latest pose.
- Press Hold position to hold again. The driver reads every servo and writes those exact positions as goals before enabling torque. Motion is blocked during the transition. The selected button and dashboard report the actual current mode rather than only the last requested action.
- Stop all remains the emergency-safe shutdown: it stops the driver and disables torque, so live state publishing also stops.
- Torque-enable sequencing. Feetech STS servos snap toward whatever is
already sitting in
Goal_Positionthe instantTorque_Enableswitches on — that register is not guaranteed to already equal the physical position.feetech_bus_driver.pyalways reads every servo's current position first, writes that same value back asGoal_Positionwhile torque is still off, and only then enables torque — so there is nothing for the servo to snap toward. If the arm moves or twitches the moment the driver starts, stop and investigate before arming anything. - Shutdown behavior. By default the driver disables torque on every
servo when it stops (clean shutdown, crash, or
RobotDriverLauncheraction=stop) — the arm goes limp rather than holding its last position indefinitely with no watchdog. Override with--no-torque-off-on-exitin the preset'scommand_template(or a customRobotDriverDescriptor) only if holding position is actually the safer failure mode for your specific mounting. - Rosbridge reconnects without restarting the hardware driver. A dropped
WebSocket no longer leaves an alive-but-silent process. The driver keeps the
serial session open, waits for
roslibpyto reconnect, then republishes its safety configuration and current joint pose before resuming state updates. Command writes and state reads share one bus lock so Feetech packet transactions cannot overlap. Confirmed command writes consume each servo's status response, and malformed short read packets retain the last valid pose instead of terminating the driver. Late driver exits retain their error text in runtime status instead of disappearing as a generic offline process. - The editor's "Stop all" reaches the driver process too.
robot.pyexposesruntime_status()/stop_runtime_services()(registered in the main Blacknode editor's_RUNTIME_MODULES), so pressing "Stop all" sendsSIGTERMto every driver this session launched — which is what actually triggers the torque-off-on-exit shutdown above. Before this was wired in, "Stop all" only stopped camera/tracking/reasoning stream helpers and left the robot driver (and torque) running silently in the background. - Joint limits are placeholders until calibrated. The
min_deg/max_degvalues in the SO-ARM101 base profile are not a verified safe envelope for your assembly. Use Robot Guided Calibration with torque released to record intended physical ranges and a safety margin before commanding broad motion. Never discover limits by driving an armed joint into its hard stop. - Calibration details.
feetech_bus_driver.pyinitially assumes each servo's raw center tick (2048 of 4095) is that joint's mechanical zero and that none of the joints are mirror-mounted. Saved profiles carry direction and center information, while device calibrations supply measured home ticks and safe ranges automatically. The--home-ticksand--invertCLI flags remain available as advanced driver-level overrides. - Verify the control-table addresses before ever writing. Run
python packages/blacknode-robot/drivers/feetech_bus_driver.py --dry-run --port <serial_port> --joints "<name:id:min:max,...>"first: it only readsPresent_Positionfor every servo ID and prints the result — it never touchesGoal_PositionorTorque_Enable. Confirm every servo responds with a plausible tick (0-4095) before trusting the driver with real writes.
Keep a physical power cutoff within reach and clear the workspace before arming.
Apache-2.0, same as Blacknode.