ROS 2 Foxy robot package for the Deep Robotics Lynx M20: the robot's inspection protocol as a driver with standard messages only, the official URDF with its camera and LiDAR frames, and one launch for both.
Packages · Network · Quick start · Interface · Usage · Verify on the real robot · Safety · Develop
The Lynx M20 takes requests and reports its state over UDP with JSON messages (Deep Robotics
Inspection Communication Protocol V1.3.0-0). This repository turns that into ROS 2 Foxy topics
and services on any computer that reaches the robot: velocity, odometry, IMU, joints, battery,
GPS, errors, and every motion state, gait, mode, light, charging and sleep command. The robot's
own ROS 2 topics (custom drdds types, on its hosts only) are not needed.
Docs: driver · URDF and frames · bringup · changelog
| Package | |
|---|---|
m20_driver |
the protocol driver: cmd_vel through a velocity gate (holds, takeover latch, E006, watchdog) → axis commands in regular mode; reports → odom, imu, joint_states, battery, gps/fix, state, diagnostics; every robot command as a std_srvs/Trigger service that succeeds only when the robot confirmed it; a simulated robot for trying it without one |
m20_description |
URDF: Deep Robotics' model with its meshes (BSD-3-Clause), front/rear camera and LiDAR frames, imu_link |
m20_bringup |
one launch for the driver and the URDF, RViz on request |
From the protocol document V1.3.0-0:
| robot | UDP server 10.21.31.103:30000 on the wired payload network (the TCP server on 30001 is not used); the official remote app talks to 10.21.33.103 (SDK IpConfig) |
| messages | 16-byte header EB 91 EB 90, ASDU length and message ID (little endian), 0x01 = JSON; then {"PatrolDevice": {"Type", "Command", "Time", "Items"}} |
| heartbeat | Type 100 / Command 100, at least 1 Hz (the driver sends 5 Hz) |
| reports | to whoever sends the heartbeat: basic status 2 Hz, motion status 10 Hz, device status 2 Hz, abnormal status 2 Hz |
| answers | a call status response (ErrorCode, ErrorMessage) to every request |
| velocity | axis command Type 2 / Command 21, ratios in [-1, 1], 20 Hz recommended, regular usage mode only, moves the robot in RL control; from one client within 2 s (E006) |
Nothing has to be configured on the robot: it reports to the address and port the heartbeats come
from. This computer only needs a route to 10.21.31.103 (ping 10.21.31.103).
Ubuntu 20.04, ROS 2 Foxy.
# 1. Sources and dependencies (Foxy is end-of-life: rosdep needs --include-eol-distros)
mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
git clone https://github.com/DarmawanRobotics/m20_ros2.git
cd ~/ros2_ws
rosdep update --rosdistro foxy --include-eol-distros
rosdep install --from-paths src --ignore-src -y --rosdistro foxy
# 2. Build
colcon build && source install/setup.bash
# 3. Launch: the driver and the URDF in the namespace m20
ros2 launch m20_bringup bringup.launch.py
# 4. In a second terminal
ros2 topic echo /m20/state
ros2 service call /m20/stand_up std_srvs/srv/TriggerNo robot at hand: start the simulated M20 and point the driver at it.
python3 $(ros2 pkg prefix m20_driver)/share/m20_driver/test/fake_robot.py --port 30000
ros2 launch m20_bringup bringup.launch.py robot_ip:=127.0.0.1 rviz:=trueThen work through Verify on the real robot, including the
emergency stop, the takeover, the speed calibration and the cmd_vel signs (steps 4 to 10),
before any autonomy drives the robot.
Everything under /m20 (launch argument namespace).
| Name | Type | ||
|---|---|---|---|
| sub | cmd_vel |
geometry_msgs/Twist |
REP-103 in base_link; sent only while armed and moving, then zeros for 0.5 s, then nothing (also while zero Twists keep coming); held by services, a takeover or E006 (velocity gate) |
| pub | odom |
nav_msgs/Odometry |
odom → base_link, dead-reckoned from the reported velocities and heading; no TF unless publish_tf |
| pub | imu |
sensor_msgs/Imu |
attitude once the angle unit is known, rates (and acceleration on newer firmware), frame imu_link |
| pub | joint_states |
sensor_msgs/JointState |
12 leg angles and 4 wheels, named as in the URDF |
| pub | battery |
sensor_msgs/BatteryState |
both batteries, percentage 0–1 |
| pub | gps/fix |
sensor_msgs/NavSatFix |
the robot's GPS |
| pub | state |
diagnostic_msgs/DiagnosticStatus |
connected, motion state, velocity (driving, idle, hold, disarmed, takeover, axis_owner_other), gait, usage mode, charge, e-stop, sleep, battery ... as key/values |
| pub | diagnostics |
diagnostic_msgs/DiagnosticArray |
driver and link, robot errors (the document's error table), temperatures, power |
Services (std_srvs/Trigger): success only when the robot confirmed the command (its call
status response, and for motion states the reported state); no answer is unconfirmed, a refusal
fails with the robot's error code.
| posture | stand_up (Stand, then RL control), sit_down, idle, damping, rl_control |
| stop | emergency_stop (soft, confirmed or an ERROR), release_emergency_stop (from the SDK, not in V1.3.0) |
| gaits | gait_basic, gait_stair (standard), gait_flat_agile, gait_stair_agile (agile); in RL control and regular mode |
| usage modes | regular_mode, navigation_mode, assist_mode |
| other | lights_on, lights_off, charge_start, charge_stop, charge_cancel (from the SDK), sleep, wake, sync_time |
# stand up and walk
ros2 service call /m20/stand_up std_srvs/srv/Trigger
ros2 service call /m20/gait_flat_agile std_srvs/srv/Trigger # agile gait: for autonomy
ros2 topic pub -r 10 /m20/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.3}}"
# stop (cmd_vel stays held until stand_up or rl_control), sit, lights, docking
ros2 service call /m20/emergency_stop std_srvs/srv/Trigger
ros2 service call /m20/release_emergency_stop std_srvs/srv/Trigger
ros2 service call /m20/sit_down std_srvs/srv/Trigger
ros2 service call /m20/lights_on std_srvs/srv/Trigger
ros2 service call /m20/charge_start std_srvs/srv/Trigger
# state (velocity: why cmd_vel does or does not drive), errors, temperatures, speed estimates
ros2 topic echo /m20/state
ros2 topic echo /m20/diagnosticsYour own settings go into a copy of
driver_params.yaml:
ros2 launch m20_bringup bringup.launch.py driver_params:=$HOME/m20_params.yaml.
The protocol document leaves some things open; the driver takes the safe or the documented choice and these steps settle them. On a new robot, in an open area, with someone at the remote and the hard emergency stop:
- Reports:
ros2 topic hz /m20/odomshows ~10 Hz and/m20/statesaysconnected: truewith the real motion state and gait. - Angle unit: drive a turn faster than 0.3 rad/s:
state.angle_unitbecomesrad (auto)ordeg (auto)(the document says rad, the official client degrees). Turn 90° by remote: the yaw of/m20/imuchanges by about 1.57 rad. Then setangle_unitto that unit. - Stand:
ros2 service call /m20/stand_up std_srvs/srv/Triggeranswersstanding in RL control. Note from which states the robot accepts Stand. - Emergency stop: while driving,
emergency_stopanswerssoft emergency stop confirmedand the robot stops. Then find out what releases it:release_emergency_stop(MotionParam −2, from the SDK) orstand_upstraight from state 2 (the SDK allows Stand there).cmd_velstays held untilstand_uporrl_control, and then needs a zero first. - Regular mode and takeover: with
auto_regular_mode(default) a robot that was in navigation mode since boot is switched to regular whencmd_velstarts. While streaming, switch to navigation in the app: the driver stops,state.velocitysaystakeover (navigation mode)and it never switches back by itself. Hand back to regular in the app (or callregular_mode): it drives again aftercmd_velwas zero once. - One axis client: while the driver streams, the remote's joystick is locked out (E006, 2 s
rule); after the stop burst the driver is quiet and the remote works again. Use the remote's
joystick while
cmd_velstreams:state.velocityshowsaxis_owner_otherand the driver yields untilcmd_velpauses. - Forward:
ros2 topic pub -r 10 /m20/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.2}}"moves it forward, slowly (uncalibrated). Stop the publisher: it stops within 0.5 s. - Calibrate speeds per gait: in each gait, drive steadily (e.g.
x: 0.5, theny: 0.2, thenangular: {z: 0.5}) and readfull_scale.<gait>.vx_estimate(vy,wz) in/m20/diagnostics. Put them intofull_scale.<gait>.vx,.vy,.wzof your parameters;odom's twist then matchescmd_vel. A gait far faster than its full scale trips the overspeed guard (disarmed:overspeed). - Turn and strafe (REP-103):
{angular: {z: 0.3}}must turn the robot left (counter-clockwise seen from above), else setinvert_wz: true;{linear: {y: 0.1}}must move it left, elseinvert_vy: true. - Direction Back: set the robot's forward direction to Back (
state.direction):x: 0.2must still move it towards its front andy: 0.1to its left (the driver negates X and Y,follow_direction), andodom's twist must still read positive x. If it reads negative, setdirection_flips_reports: true(the diagnostics warn about it after 2 s). - Joints and frames:
ros2 launch m20_bringup bringup.launch.py rviz:=true: the legs of the model move like the real ones (joint signs against the URDF axes) and the wheels turn the right way. The model gives the rear camera and rear LiDAR no rotation: check their frames against their data. - Batteries and GPS:
battery.locationlistsrear, front(the official client's order ofBatteryList); compare the percentages with the app. Outdoors,gps/fixshows a fix (FixQualityread as NMEA) and an altitude: find out whether it is above sea level or the ellipsoid. - Sleep and charging:
sleep/wakekeep the robot's own auto-sleep settings (read with 1101/7 every time);charge_startnear the dock;charge_cancelwhile it goes to the dock,charge_stopwhile charging or on the dock (the SDK's meaning of Charge 0). - Lost controller: stop the driver while it streams: three zeros go out. Its heartbeat also pauses when the driver hangs: check what the robot does then (the SDK knows error 0x8309, controller heartbeat timeout).
- The driver stops on its own: no
cmd_velfor 0.5 s gives zeros for 0.5 s, then silence, and a stream of zero Twists sends nothing. Only one node (your control mux) should publish on/m20/cmd_vel. emergency_stopholdscmd_velfirst, sends a zero and the soft emergency stop three times, and succeeds only when the robot confirms it; otherwise it fails withsoft e-stop NOT confirmedand logs an ERROR. It aborts any service that is still waiting. The remote and the robot's hard emergency stop always work too.emergency_stop,sit_down,idle,damping,sleep,charge_start,navigation_modeandassist_modeholdcmd_vel; onlystand_up/rl_control(andregular_modefor the mode holds) lift it, and a running stream then has to be zero once: a stale stream never drives off.- A takeover (the robot leaves regular mode after it was in regular mode: the app, the remote, the robot) latches: no axis command and no mode request until the robot reports regular mode again. The driver never switches a robot back that someone else switched away.
- While the driver streams, the remote's joystick is locked out (the robot takes axis commands
from one client within 2 s); after the stop burst the driver is quiet. When another client
holds the axis channel (E006) the driver yields until
cmd_velpauses. - No service reports success without the robot's answer (and for motion states the reported state).
- Until the speeds are calibrated the robot runs slower than
cmd_velasks (highfull_scale.<gait>.*defaults);max_vx,max_vy,max_wzclamp every command, and a reported speed far above them stops the stream (overspeed). sit_down,idleanddampingare refused while the robot walks;idleanddampingonly from a sitting state.stand_upis refused on a hard emergency stop or while charging.- Invalid parameters stop the driver before it contacts the robot.
charge_startmakes the robot dock by itself.
colcon build && colcon test --executor sequential && colcon test-result --verboseThe codec is tested against the document's header layout and JSON examples and the official
Android client's framing; every rule of the velocity gate has a unit test; the UDP link and the
node run against a simulated M20 on the loopback interface (m20_driver/test/fake_robot.py),
which can also answer with wrong message IDs, stay silent, refuse with E006, change its usage
mode on its own and report degrees; the URDF against the official model's numbers and the
driver's joint names. CI runs the same in a ros:foxy container, plus
an end-to-end smoke test of the installed launch file against the simulator, check_urdf and
shellcheck.
The URDF and its meshes are Deep Robotics' model (DeepRoboticsLab/deep_robotics_model, BSD-3-Clause, license). m20_ros2 is released under the Apache License 2.0.