ROS 2 package to bridge an Arduino-based emergency stop button to:
- ROS 2 topics:
/estop/triggered(std_msgs/Bool)/estop/plugged_in(std_msgs/Bool)
- Optional raw CycloneDDS topic:
h12/estop_status_raw- type
h12.estop.EstopStatusRaw - fields:
triggered(bool),plugged_in(bool),sequence_id(int),stamp_ms(int, epoch time)
- type
- Continuously attempts to connect to
/dev/ttyACM0(configurable). - Parses serial lines from Arduino in format
ESTOP,0/ESTOP,1. - Publishes status at fixed rate (
100 Hzdefault) even when disconnected. - Applies software debounce (
state_debounce_sec=0.08default) to suppress spurious state flip-flops. plugged_in=falsewhen serial cannot be opened or data goes stale.- Applies startup stale grace (
stale_connect_grace_sec=1.5) to avoid false stale warnings right after connect. - NOTE: Fail-safe mode forces
triggered=truewhile disconnected.
estop/serial_interface.py: serial discovery, probe, parsing, stale handling, and reconnect.estop/estop_node.py: ROS 2 + raw DDS publisher only.estop/serial_status_test.py: standalone serial parser test executable.
Flash arduino/estop_serial.ino to your UNO.
Default configuration:
- Digital pin:
13 - Serial baud:
115200 - Message format:
ESTOP,0|1
ROS dependencies are declared in package.xml.
Python runtime dependencies:
- required:
pyserial - optional for raw DDS publishing:
cyclonedds
If you are using a venv without pip in this workspace, use uv pip:
uv pip install --python .venv310/bin/python pyserial cycloneddsRun the following command to grant the current user access to the serial port:
# allow current user to access USB serial devices
sudo usermod -a -G dialout $USER
# then log out and back inFrom ros2 workspace root (one level above src):
colcon build --paths src/estop
source install/setup.bashros2 launch estop estop.launch.pyOr run node directly:
ros2 run estop estop_nodeOverride publish rate if needed:
ros2 run estop estop_node --ros-args -p publish_rate_hz:=200.0If your wiring is noisy or the button bounces, increase debounce window:
ros2 run estop estop_node --ros-args -p state_debounce_sec:=0.15If you see package 'estop' not found, source the workspace in the same shell you launch from:
cd /home/humanoid/ws_ctrl
source install/setup.bash
ros2 pkg list | grep '^estop$'ros2 topic echo /estop/triggered
ros2 topic echo /estop/plugged_inIf raw_dds_enabled is true and cyclonedds is installed, the node also writes raw DDS samples on h12/estop_status_raw.
Run the ROS-side publisher first, then in another shell:
cd /home/humanoid/ws_ctrl
source install/setup.bash
ros2 run estop estop_raw_dds_subscriberOptional topic override:
ros2 run estop estop_raw_dds_subscriber -- --topic h12/estop_status_rawThis directly exercises estop serial discovery + parsing:
cd /home/humanoid/ws_ctrl/src/estop
conda deactivate
python3 scripts/estop_serial_test.py \
--serial-port /dev/serial/by-id/usb-Arduino__www.arduino.cc__0043_55338343539351817262-if00This script is fully outside ROS 2 (no ros2 run, no workspace sourcing).
The node now supports robust matching with probe validation so it does not attach to the wrong serial device.
Recommended order:
- Use
/dev/serial/by-id/*if available. - Set exact USB
serial_numberif your adapter exposes one. - Optionally constrain by
usb_vidandusb_pid. - Keep
require_estop_signature=trueso the node only accepts devices emittingESTOP,BOOTorESTOP,0|1.
Defaults tuned for unplug/replug robustness:
probe_timeout_sec=3.0publish_rate_hz=100.0line_stale_timeout_sec=0.1stale_connect_grace_sec=1.5stale_reopen_timeout_sec=3.0reconnect_interval_sec=1.0
Example launch overrides:
ros2 run estop estop_node --ros-args \
-p serial_port:=/dev/serial/by-id/usb-Arduino__www.arduino.cc__0043_XXXXXXXX-if00 \
-p require_estop_signature:=trueor auto-discovery with USB filters:
ros2 run estop estop_node --ros-args \
-p serial_port:="" \
-p usb_vid:=9025 \
-p usb_pid:=67 \
-p require_estop_signature:=true