How to work here. Architecture and inter-package contracts → ARCHITECTURE.md.
Top rule: draft a short spec + flag sensitive spots before coding non-trivial changes.
- ROS 2 jazzy (Ubuntu 24.04) packages for Husarion ROSbot 2/3 and ROSbot XL (optional OpenMANIPULATOR-X arm).
- Workspace:
~/Husarion/Workspaces/rosbot_ws/, repo assrc/rosbot_ros. - Main / PR target branch:
jazzy(notmain). - Targets: real robot (micro-ROS) or Gazebo Harmonic sim — shared URDF + ros2_control.
- Packages:
rosbot(meta),rosbot_bringup,rosbot_controller,rosbot_description,rosbot_gazebo,rosbot_hardware_interfaces,rosbot_joy,rosbot_localization,rosbot_moveit,rosbot_utils.
For new launches, public args, hardware interfaces, controllers, nodes, URDF/firmware changes — first state:
- Goal (one sentence).
- Impact (packages, public args, firmware/snap compat).
- Sensitive spots —
rosbot_hardware_interfaces(ABI ↔ firmware),controllers.yaml"Based on real measurements" lines, URDF/xacro (MoveIt SRDF regen), public topics in ROS_API.md. - Test plan (pre-commit,
colcon test, manual sim/HW run).
15 min alignment beats a re-do after PR review.
cd ~/Husarion/Workspaces/rosbot_ws
vcs import src < src/rosbot_ros/rosbot/rosbot_hardware.repos
vcs import src < src/rosbot_ros/rosbot/rosbot_simulation.repos
export PIP_BREAK_SYSTEM_PACKAGES=1
sudo rosdep init # only if not previously initialized
rosdep update --rosdistro $ROS_DISTRO
rosdep install -i --from-path src --rosdistro $ROS_DISTRO -y
pip install pre-commit && (cd src/rosbot_ros && pre-commit install)source /opt/ros/jazzy/setup.bash
[ -f install/setup.bash ] && source install/setup.bash
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release --packages-select <pkg>
# or --packages-up-to <pkg> after URDF/dep changes
colcon test --packages-select <pkg> && colcon test-result --verbose
source install/setup.bashSymlink-install symlinks launch/, config/, urdf/, Python scripts/ — edit in src/ works without rebuild. C++ changes still need rebuild.
pre-commit run -a runs all hooks (also fires on git commit). Hooks: black (LL=99), isort (black profile), flake8 (ignore E501,W503), clang-format, cmake-format, codespell, doc8 (LL=100), markdownlint-fix, prettier-package-xml, sort-package-xml, ament_copyright, yamlfmt. Don't add new formatters without agreement.
- No direct commits to
jazzy. Feature branch + PR. - PR template has a
## Changelog descriptionsection — fill it. - Backports via
backport.yamlafter labeling.
ros2 launch rosbot_bringup rosbot.yaml # ROSbot 2/3
ros2 launch rosbot_bringup rosbot_xl.yaml # ROSbot XL
ros2 launch rosbot_gazebo simulation.yaml robot_model:=rosbot_xl
# args: README.md "Launch Arguments" or `ros2 launch <pkg> <file> -s`- Devcontainer:
.devcontainer/compose.yamlmounts repo at/home/husarion/ros2_ws/src/rosbot_ros.runtime: nvidia(needs NVIDIA Container Toolkit). - Production images:
docker/Dockerfile.{hardware,simulation}, built bybuild-docker.yamlon push tojazzy. HW dropsrosbot_gazebo, sim dropsrosbot_bringup. - CI:
ci.yamlorchestratespre-commit→tests.yaml(colcon testoverpackages-select-regex: rosbot*) → (onjazzy)build-docker.yaml.rosbot_bringuptests run with fake HW topics (hardware_bridge:=False);rosbot_gazebokeeps only offline launch/schema tests (real-Gazebo tests removed). - VS Code tasks: inherited from
pantherrepo,build.shuses--packages-up-to panther— prefer manualcolcon buildfrom §3.2.
- License header (Apache 2.0) in every new source file (
ament_copyrightenforced). Maintainer:support@husarion.com. - Python:
blackLL=99, type hints encouraged. Scripts →<pkg>/scripts/, installed viainstall(PROGRAMS …)tolib/${PROJECT_NAME}. Modules →ament_python_install_package. - C++:
clang-format(default ROS),-Wall -Wextra -Wpedantic(+-Wshadow -Wold-style-castinrosbot_joy). C++17 min. HW plugins viapluginlibinrosbot_hardware_interfaces.xml. - Launch: prefer YAML frontend. Use Python only when logic is required (example: microros.launch.py). Keep arg shape — tests, snaps, sibling packages anchor to
namespace,robot_model,config_dir,mecanum,use_sim. - Configs: every package accepts
config_dir. Default =share/<pkg>/config/; withconfig_dirset, reads<config_dir>/<pkg>/config/…(snap relies on this — don't break it). Generate external dir:ros2 run rosbot_utils create_config_dir <dst>. - Naming: packages
rosbot_*; launch verbs/functions (controller.yaml,simulation.yaml,microros.launch.py); public topics — see ROS_API.md. Renaming a public topic = breaking change. /tmp/rosbot_*_<namespace>.{yaml,urdf}are runtime-resolved configs — don't commit.
- Don't change
FIRMWARE_VERSIONin rosbot_utils/firmware_version.py without swappingrosbot_utils/firmware/rosbot[_xl]-${FIRMWARE_VERSION}.binin lockstep. Driver ↔ firmware are tightly coupled;configure_robotrejects mismatched strings. - Don't commit
build/,install/,log/,*.pyc— gitignored. Same for vcstool submodules. - Don't disable pre-commit hooks without approval — CI enforces.
- Don't change
controllers.yaml"Based on real measurements" lines without fresh measurements. - Don't enter
manipulation/manipulation_prowithout reading MANIPULATOR.md — HW pitfalls (power loss → arm falls). - Don't ship new topics/launch args without updating ROS_API.md + table in README.md.
- Don't touch
tf_namespace_bridgeoutside designated cases — it's the only bridge from namespaced TF to global/tf(nav2 / multirobot).
Automation: the checklist is wired into
/feature. Lighter:/quick-fix(≤3 files, no API change),/spec(draft only),/review(optimizer+security+tester on current branch). See .claude/README.md.
Knowledge-base updates required for any public change:
- ARCHITECTURE.md — new inter-package contract / node / topic / plugin.
- ROS_API.md — public topic / node / launch.
- README.md — launch arg added/changed.
- MANIPULATOR.md — arm-related.
- Tests (
test_xacro.py,test_bringup.py,test_launch_offline.py). -
pre-commit run -a,colcon build,colcon test. - §9 — one bullet, one sentence, PR link.
- micro-ROS: ROSbot 2/3 → serial (
/dev/ttySERIAL, 921600). ROSbot XL → udp4 (port 8888). Mode viamicroros_modearg.configure_robotchecks firmware version + sets namespace over serial before agent starts. - Namespace:
ROBOT_NAMESPACEenv →--ros-args. Inside robot TF is namespaced (/<ns>/tf);tf_namespace_bridgere-publishes to global/tf. - XL
configurationarg:basic|telepresence|autonomy|manipulation|manipulation_pro|custom. Drives component list inrosbot_description/config/rosbot_xl/<configuration>.yaml+ whether manipulator launches. - XL
controllers.yamlplaceholder:<manipulator_state>is sed-replaced withactive/inactivebeforecontroller_managerstarts. See controller.yaml. - Sim vs HW ros2_control: same URDF, different
<plugin>—rosbot_hardware_interfaces/RosbotSystem(HW) vsgz_ros2_control/GazeboSimSystem(sim). See ros2_control.urdf.xacro. - EKF fuses
odometry/wheels+imu/data→odometry/filtered.enable_odom_tf: falseon drives — EKF publishesodom→base_linkTF. - Laser filter crops points inside robot body. Per-model: rosbot_utils/config/{rosbot,rosbot_xl}/config.yaml.
rosbot_bringup/rosbot_gazeboCI tests run offline (fake HW topics / launch-schema only) — still do a real sim/HW run after non-trivial launch changes.flash_firmwareprotection unprotect:McuManager{UART,FTDI}.flash_firmwarefixeserror: got nack from device on command 0x44, which is the bootloader (AN3155) refusing the erase command because RDP (read-protection) is active. The two obvious levers,stm32flash -u(Write Unprotect, clears WRP) and-k(Readout Unprotect, clears RDP), existed since the very first commit as commented-out dead code — reactivating "both, always, before every flash" was the first fix tried here, but AN3155 (read directly, section 3.10 vs 3.12) says-uhas no erase side effect while-k"erases all the flash memory sectors" unconditionally, regardless of whether RDP was ever set — so running-kon every flash would mass-erase a chip that was never protected, on every single flash, for no reason. Fixed design:-uruns unconditionally before every flash (cheap, no-op-safe per the doc);-konly runs reactively, triggered by catching the specific0x44/"Failed to erase" signature from the-wattempt, then retried once (_disable_write_protection/_flash_with_read_protection_recoveryin both managers).stm32flashtreats-k/-u/-was mutually exclusive actions (confirmed in its ownmain.c— can't combine flags in one invocation) and each protection op makes the MCU self-reset with no reconnect logic onstm32flash's side — the MCU only comes back up in the bootloader if BOOT0 is still physically driven high at that reset edge. UART/GPIO (mcu_manager_uart.py) holdsboot0_pinviagpiodfor the whole session, so nothing extra is needed. FTDI/USB (mcu_manager_ftdi.py) closes the FTDI handle right after asserting BOOT0 inenter_bootloader_mode(), so CBUS0's level once the handle is closed isn't something the code controls — both_disable_write_protectionand the-krecovery path re-run the fullenter_bootloader_mode()(with itsusbreset) after their op instead of trusting BOOT0 stayed high. FTDI'sflash_firmwareadditionally retries the whole entry+flash attempt up to 3x if it hitsFailed to init device, timeoutspecifically (not other errors) — a separate, pre-existing USB-reenumeration race afterusbreset(reproduced identically on unpatched code, unrelated to the protection fix) that resolved on a bare re-run every time it was hit in testing; UART/GPIO has nousbresetstep and no evidence of this race, so it wasn't given the same retry. HW-verified 2026-08-25 on a real ROSbot 2/3 via--usb/FTDI (STM32F407, device ID0x0413): deliberately enabling read-protection (stm32flash -j) then a raw-wwith no unprotect reproducedGot NACK from device on command 0x44exactly; the patched script then auto-recovered (-uitself got NACK'd on0x73while RDP was active — caught, warned, continued — then-w's0x44NACK triggered the reactive-k+ retry, which succeeded). Confirmed the normal/unprotected path no longer runs-kat all (3 consecutive clean runs, ~58s each, onlyWrite-Protection+Flashing) — the mass-erase regression is gone. TheFailed to init deviceconnect-retry itself never fired live in this session (too rare to force on demand) but was verified by unit-testingflash_firmwarewith the real dependencies monkeypatched: confirmed it retries exactly once and succeeds on that exact error text, and does NOT retry on an unrelated exception (so a genuine hardware fault still fails fast instead of wasting ~3x the time). UART/GPIO path (default forrosbot, no--usb) not HW-tested — only the FTDI/USB path was exercised. On the timeout itself:stm32flash's own init-handshake timeout is hardcoded (strings /usr/bin/stm32flashshows it already does an internal resend+retry -- "Failed to resend init to device" / "Failed to init device, timeout." / "...after retry." -- before giving up) and isn't exposed via any CLI flag, so tuning it would mean patching+vendoring a custom-builtstm32flash, disproportionate for this. What we do control is our own settle delay between "USB re-enumerated" and "first byte sent to the bootloader," which is where this race actually lives (tty node can appear before the MCU's ROM bootloader is ready for a clean auto-baud sync byte) -- bumped inmcu_manager_ftdi.pyonly (nousbreseton the UART/GPIO path, no evidence of the race there): post-usbresetsettle0.3s→0.6s+ a new0.3safter_wait_for_portsucceeds inenter_bootloader_mode(), and the pre-invoke pause inflashing_operation()0.5s→1.0s. Re-verified on HW after the bump: 2 more clean runs, ~60-61s total (was ~58s) -- negligible added latency, no regression. This is a prevention/noise-reduction measure on top of the connect-retry, not a proven fix -- the race is too rare to force on demand, so its actual hit-rate reduction wasn't measured, only that it doesn't cost anything.
Format: date — one-line summary (link). Detail lives in code + .claude/specs/ + .claude/plans/.
- 2026-04-30 — initial CLAUDE.md / ARCHITECTURE.md.
- 2025-04-21 —
led_striparg in rosbot_xl.yaml (commitbbd741b). - 2025-04-XX — firmware →
v1.1.0-jazzy, improved PID (commitsb87b1a4,e5509b2). - 2026-05-04 —
frame_filtersparameter exposed fortf_namespace_bridgevia per-package config (default empty = pass-through). - 2026-05-15 — MoveIt topic namespacing for
joy2servo/servo_nodefixed viaMoveGroupInterface::Options3-arg ctor + relativemonitored_planning_scene_topic./parameter_eventsglobal by design. - 2026-05-15 —
rosbot_moveitrefactor:joy2servomoved fromrosbot_joy; ~1000 LOC of dead code removed;dock/homededuplicated viaarm_pose_movershared lib;ompl_planning.yaml192→41 lines;servo.launch.pytypo + symmetry fix;test_moveit_config.pyregression guards. - 2026-05-15 — manipulation HW noise classified — most accepted upstream (
planner_idempty,OccupancyMapMonitor,controller_managerdeprecated-r, RT scheduling, dynamixel matrix,RosbotSystemstartup race).validate_workspace_boundsfixed viaMoveGroupInterface::setWorkspace(±0.5m)in arm_pose_mover.cpp + joy2servo.cpp. - 2026-05-15 — Cartesian arm rerouted around
moveit_servoPOSE path;joy2servoruns IK locally (KDL +position_only_ik) and publishes JointJog (skips singularity guard). See joy2servo.cpp. - 2026-05-15 — sim-warning cleanup for
simulation.yaml configuration:=manipulation:manipulator_controllerinterpolate_from_desired_state(replaces deprecatedopen_loop_control),default_workspace_bounds: 1.0,default_planner_config: RRTConnectkConfigDefault, home.launch.py wrapper for kinematics injection. Sim residue (OccupancyMapMonitor always-on,ParallelGripperCommandunsupported in MoveIt simple manager) — accepted. - 2026-05-18 —
FIRMWARE_VERSIONdeduplicated → single source inrosbot_utils.firmware_version. - 2026-05-19 — HW
gripper_right_joint-missing-from-/joint_statesfix attempted via xacro override → reverted:dynamixel_hardware_interfacecounts mimic joints againstnumber_of_joints=5, separate<ros2_control>block also rejected. Cosmetic only; needs upstream Robotis + ros2_control changes. - 2026-05-19 — dock.launch.py added mirroring home.launch.py —
MoveItConfigsBuilderinjectssemantic/kinematics/joint_limitssoMGIstops loggingNo kinematics plugins defined.dockexposesnamespace:=arg (homedoesn't — already auto-included frommanipulator.yaml). - 2026-05-19 — duplicated
move()calls injoy2servoMoveToDockPose/MoveToHomePosedropped → reverted (band-aid restored) on 2026-05-19→20:MGIfrom-Dock to-Home staleness reproduces; root cause likely in server-side PSM. See FOLLOWUP_PLAN.md C4. - 2026-05-19 — rviz.launch.py namespace-aware:
namespace:=arg +/tf|/tf_static|/diagnosticsremaps + sed-resolvedmoveit.rvizto/tmp/rosbot_moveit_<ns>.rviz(panel reads ns from rviz config, not node__ns:=). HW-verified. - 2026-05-19 —
gripper_controllerswitched toJointTrajectoryController(wasGripperActionController);joy2servo::ControlGripperpublishes single-point trajectories directly at 20 Hz (no MGI in hot path). Breaking:gripper_cmdaction gone, usefollow_joint_trajectory.gripper_left_jointlimits → physical m/s,max_velocity: 0.08. Testtest_gripper_uses_follow_joint_trajectoryguards moveit_controllers. - 2026-05-20 — Cartesian self-collision regression fix in
joy2servo: newcartesian_max_joint_velocity(rad/s, default 1.0) — scales whole velocity vector when any joint exceeds.collision_check_rate: 10→30 Hz,self_collision_proximity_thresholdupstream default 0.02 m. servo.launch.py dropsjoy2servodefault log_level to WARN, whitelistsjoy2servo:=infoto silencekdl_kinematics_plugin"Using position only ik" spam. Per-logger filter formoveit.kinematics.kdl_kinematics_plugindoesn't work in jazzy (auto-named plugin nodes). - 2026-05-21 — namespace audit (.claude/plans/namespace-audit.md): sim CM services namespaced via 14 URDF
<remapping>entries in gazebo.urdf.xacro (push_ros_namespace can't reachgz_ros2_control-hosted CM).micro_ros_agentdefensivenamespace=attempted then reverted — produced__ns:=/<ns>/<ns>double-stack (push_ros_namespace IS active insideOnProcessExitcallback). EKF services land at/<ns>/{set_pose,enable,toggle}(relative, notekf_node/-prefixed). Regression guards: test_namespace_isolation.py, test_xacro::test_gazebo_urdf_namespace_remappings, test_launch_offline::test_microros_agent_node_has_no_explicit_namespace. HW + sim retest: zero leaks (only/tf,/tf_static,/parameter_events,/rosouton root, plus sim-only/gz_bridge/*+/launch_ros_*). Snapshots in .claude/scratch/. Caveat: xacro comments going to gz_sim must have NO:(gz's URDF→SDF uses yaml.safe_load). - 2026-05-21 — log-noise audit Phase A/B/C (spec, classification, proposals): 4 baseline captures, 86 unique groups, 21 proposals tiered. Surprises: (1) Dynamixel SDK raw
cout≈105 lines hw-manipulation, NOT silencable via--log-level(onlyoutput='log', high UX cost — see [[feedback-log-level-misses-cout]]). (2)led_strip_{ready,rainbow}KeyboardInterrupt traceback (18 lines on Ctrl+C) fixed viatry/except + try_shutdown. (3)process has died exit code -2is launch-core ERROR on benign Ctrl+C — needs launch hook. (4)gz_ros_controlplugin INFO lives inside gazebo process —--log-levelmay not propagate. 60% target revised → ~26% hw-m / ~21% sim-m with wariant A alone. - 2026-05-21 — log noise Phase D partial: pretty-header
log:actions in rosbot.yaml+rosbot_xl.yaml (jazzy YAML frontend action islognotlog_info, attribute ismessagenotmsg).tf_namespace_bridgesignal_handlerINFO fixed at source (sibling repof6f32bc):SignalHandlerOptions::None+std::signalquiet handler → exit code 0 (was -2), launch core no longer prints redprocess has died.led_strip_{ready,rainbow}PythonKeyboardInterrupttraceback wrapped (try/except +try_shutdown). micro_ros_agent silencing skipped (-v 3flag tried, reverted). - 2026-05-21 — MAVLink as default
backend:=mavlink|microrosarg onrosbot[_xl].yaml(paired withrosbot_mavlink_bridge). Renamed legacymicrorosbool →hardware_bridge. - 2026-05-22 — runtime-switch firmware handshake:
configure_robot --backendemitsBACKEND:line beforeNS:; missing ACK is fatal so legacy single-protocol firmware fails loud rather than silently defaulting the namespace.link_layerarg renamed tobackendto match firmware-sideCommBackend(breaking, no alias). - 2026-05-22 — added justfile with
just <recipe>helpers (build, test, hw, sim, precommit) — handles workspace auto-detect for both main checkout and.claude/worktrees/<name>/worktrees, with idempotentvcs importof sibling repos. Firmware quirk worth remembering: ROSbot 2/3 firmware leavessensor_msgs/BatteryState.power_supply_statusatUNKNOWNregardless of charger state; only ROSbot XL fills it in (CHARGING/FULL/DISCHARGING). Any future code touchingpower_supply_statusmust either restrict torosbot_xlor includeUNKNOWNin the allowlist. - 2026-05-29 — pinned
RCUTILS_CONSOLE_OUTPUT_FORMAT="[{severity}] [{time}] [{name}]: {message}"viaset_envin rosbot.yaml, rosbot_xl.yaml, simulation.yaml (next to existingRCUTILS_COLORIZED_OUTPUT). Value equals the ROS default — purpose is to override any shell-exported format so logs render consistently. Dead end confirmed:RCUTILS_LOGGING_SEVERITY_THRESHOLDdoes NOT exist in rcutils on any distro (incl. Rolling); no env var sets the default log level — only--ros-args --log-level. So nolog_levelarg was added. - 2026-07-23 —
husarion_asset_serverfolded intorosbot_bringup's own launch (node:underpush_ros_namespacein rosbot.yaml/rosbot_xl.yaml, newasset_serverarg, defaultTrue) instead of running as a separate snap daemon — gets the correct namespace at startup, no more restart-to-re-announce hack. Vendored as a new husarion_asset_server package that fetches + sha256-verifies the upstream prebuilt release binary (not built viacolcon-ros-cargo/cargo-ament-build— avoids a Rust/clang toolchain in dev/CI/therosbot-snapbuild).husarion_asset_msgsadded torosbot_hardware.repos. CI test_bringup passesasset_server:=Falseto stay offline. - 2026-06-22 —
led_strip/enablestd_srvs/SetBoolservice added to bothled_strip_{ready,rainbow}nodes —data:=falsecancels the 40ms timer (no image compute, no publish),data:=trueresets it (resumes from current phase/position). Documented in ROS_API.md "Available Services"; regression guardtest_led_strip_enable_servicein test_led_strip.py. - 2026-08-11 —
led_strip/enableSetBoolrestored onanimation_publisher. The service was documented in ROS_API.md but had been lost whenled_strip_{ready,rainbow}were retired in favour of the PNG-driven node, so the docs described an API nothing served. Publishing is now gated byenabled_ && active_ != nonein a singleRestartTimer(), which both the service and thecurrent_animationcallback go through — so switching animation while disabled stores the selection without resuming, andenable=truebrings back whatever was picked.std_srvswas already apackage.xmldepend (leftover from the retired nodes), only theament_target_dependenciesline needed it. Guard:test_led_strip_enable_service. - 2026-08-11 — animations are now selectable at runtime from
config_dir, not just from the startup scan.current_animationre-reads<name>.png+<name>.yamlfrom disk on every set (TryLoadFromDisk, user dir first) and only falls back to the cached copy, so dropping a PNG into<config_dir>/rosbot_utils/animationsand selecting it needs no driver restart; the rejection message now names the directories that were searched. rosbot_xl.yaml gained ananimations_dirlet deriving that path fromconfig_dir— the snap needs no change,create_config_diralready copiedrosbot_utils/animationsanddaemon_start.shalready passesconfig_dir:=. Gotcha found while testing:create_config_dirused bareshutil.copytree, which aborts on a dangling symlink — routine in a--symlink-installtree whenever a source file is deleted without a rebuild, and it left a half-populatedconfig_dir. Nowignore_dangling_symlinks=True+ per-foldershutil.Errorhandling. Guard:test_animation_loaded_from_user_dir_at_runtime. - 2026-08-11 —
animation_publisherfollowstwist_mux_controller/source, so the strip shows who is driving:autonomous→navigation_animation(defaultnavigation),manual/unknown/not_published→ready_animation(defaultready). The subscription must betransient_local+reliableto match the controller's latched publisher — it republishes only on handover, so a volatile subscriber would show nothing until the next change. The switch goes throughset_parameter("current_animation", …)rather thanActivate()directly, soros2 param getkeeps reporting what is actually on the strip and the disk re-read path (henceconfig_diranimations) is reused;Node::set_parameterreturns a rejection inSetParametersResultinstead of throwing, verified againstrclcpp/node.hpp. All three parameters are read live in the callback, sofollow_cmd_vel_source:=Falsehands the strip back at runtime with no subscription juggling. Follow mode ownscurrent_animationwhile enabled — including over a hand-setnone; useled_strip/enableto silence instead. Guard:test_animation_follows_cmd_vel_source. The three parameters plusled_countlive in rosbot_utils/config/rosbot_xl/config.yaml next tobattery_alert, loaded via ananimation_publisher_configlet that mirrors battery_alert.yaml'sconfig_dir-or-share resolution. Thefrom:entry is listed before thename:/value:params in the node block — later entries win in the launch frontend, soled_animationand theconfig_dir-deriveduser_animations_dirstay authoritative over the file. - 2026-05-25 — bumped firmware to
v2.0.0-jazzy(runtime-switch — one binary covers both micro-ROS and MAVLink, picked at boot via theBACKEND:handshake line). Bundledrosbot[_xl]_mavlink-v0.1.1.binandrosbot[_xl]-v1.1.0-jazzy.bindropped;--variantflag and--expected-firmwarearg removed (single binary,FIRMWARE_VERSIONis the only source of truth). - 2026-08-10 —
twist_mux_controller(chainable, from the already-pinnedhusarion_controllers) added to both models'controllers.yaml+ spawner, mirroringhusarion_ugv_ros. Priority inputsmanual/cmd_vel(100) >autonomous/cmd_vel(10) >cmd_vel(1), 0.2 s fallthrough;rosbot_joy'sjoy_veldefault moved tomanual/cmd_velso the pad overrides nav2. Gotcha: base controllers disagree on reference-interface naming —diff_drive_controllerexports<name>/linear|angular/velocitywhilemecanum_drive_controller/omni_wheel_drive_controller/husarion_mecanum_drive_controllerkeep the axis (<name>/linear/{x,y}/velocity,<name>/angular/z/velocity). Rather than encode that per robot,twist_mux_controllergained adrive_controllerparam (husarion_controllers6181d6d, pin bumped inrosbot_hardware.repos— the two must land together) that derives the names from the controller name +holonomic; empty keeps the legacy explicitcommand_interface_*path.controllers.yamltherefore only substitutes<drive_controller>+<mecanum>, both from themecanumarg via a shareddrive_controllerlet that also feeds the spawner.holonomictakes the rawmecanumarg — the ROS yaml parser accepts capitalisedTrue/Falseas bool, verified viarclpy.parameter.parameter_dict_from_yaml_file. Jazzy-only: Humble'sdiff_drive_controlleris not chainable, so the humble branch keeps the directcmd_velpath. Sim-verified on both drive types (chained interfaces + manual-overrides-autonomous + fallthrough on release);cmd_velstays functional at priority 1. Follow-up outside this repo:rosbot-snap'steleop_launcher.shstill publishes tocmd_vel, so keyboard teleop now loses to nav2 — point it atmanual/cmd_vel. - 2026-08-12 — Attempted composing
tf_namespace_bridge+robot_state_publisherinto onerclcpp_componentsnode_containerinrosbot_description(skip a serialize/DDS hop on the highest-frequency/tfpublisher + its direct subscriber) — reverted, do not retry without reading this first.LoadComposableNodes/ComposableNodeContainerlazily creates a process-wide singletonROSAdapter(launch_ros/ros_adapters.py,get_ros_node()) the first time anything needs a service client, and registers anOnShutdownhandler that callsros_adapter.shutdown().rosbot_controller/controller.yaml'sros2_control_nodehason_exit: shutdown(deliberate fail-fast — see below on why that stays) — every exit of that node, including a completely normal shutdown, fires an extraShutdownaction that races the planned one. TwoShutdowns in flight →OnShutdownfires twice → the secondros_adapter.shutdown()raisesRuntimeError: Cannot shutdown a ROS adapter that is not running→[ERROR] [launch]: Caught exception in launch→ the SIGINT→SIGTERM escalation for the remaining processes gets interrupted, so unrelated ones (husarion_asset_server, the controllerspawner) die via raw signal instead of a clean exit. 100% reproducible (8/8test_bringupparametrizations +test_namespace_isolationfailed in CI, run 31591651133), not flaky — happens on every bringup shutdown once anynode_containerexists anywhere in the tree, not just on an actual controller crash. Same symptom (Cannot shutdown a ROS adapter that is not running+component_container) independently confirmed in navigation2#4722 — alaunch_rosframework interaction, not something fixable from our YAML. Whyon_exit: shutdownstays despite this: without it, a controller_manager crash leaves the rest of the stack (RSP, EKF, joy, LED, asset_server, HW/MAVLink bridge) running in a silent degraded state — topics still flow, robot looks alive, nothing actually drives it. The snap'sdaemonservice (snapcraft_template.yaml.jinja2) hasrestart-condition: always+restart-delay: 5sspecifically so a full fail-fast shutdown is cheap to recover from — the two mechanisms are meant to work together. Kept from this attempt (harmless on their own, nonode_containerinvolved):rosbot_descriptionis now the single owner oftf_namespace_bridge.yaml(was duplicated identically acrossrosbot_bringup/rosbot_gazebo/rosbot_description); its path is an internallet(matchingcomponents_config's pattern), not a public arg. Don't retry composable nodes anywhereon_exit: shutdown(or any otherOnProcessExit-triggeredShutdown) is in the same launch tree until upstreamlaunch_rosfixes the double-shutdown race. - 2026-08-17 — CPU-cost analysis of the manipulator stack on the Jetson Orin Nano identified
moveit_servo'scollision_monitorloop as the single hottest thread (~91% of one core, live-measured) anddynamixel_hardware_interface'sread()as a real-time-budget risk (2.7–4.9 ms of a 10 ms/100 Hz cycle, ~200–300x costlier than the base's HW interface, per/controller_manager/statistics/full) rather than a CPU-% cost (blocking serial I/O). Fixed moveit_servo.yaml:is_primary_planning_scene_monitortrue→false(move_group.launch.py already runs the canonical primary PSM — servo was duplicating its own world/scene monitor against the yaml's own documented guidance) andcollision_check_rate30→20 Hz(still 1.33x margin under the 20 mm self-collision threshold at the 1 rad/s joy2servo clamp, ~33% cut on the always-on dual self+scene FCL check loop). Needs an HW re-verify (fast jog toward self-collision/an obstacle, confirm it still stops before contact) before trusting in the field — not yet HW-tested.
| what | command |
|---|---|
| Build single package | colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release --packages-select <pkg> |
| Build with dependents | --packages-up-to <pkg> |
| Test single package | colcon test --packages-select <pkg> && colcon test-result --verbose |
| Pre-commit | pre-commit run -a |
| List launch args | ros2 launch <pkg> <file> -s |
| Flash firmware | ros2 run rosbot_utils flash_firmware --robot-model rosbot[|_xl] |
| Activate manipulator | ros2 run rosbot_controller arm_control active |
| External config dir | ros2 run rosbot_utils create_config_dir <dst> |
| Spawn another robot in sim | ros2 launch rosbot_gazebo spawn_robot.yaml robot_model:=… namespace:=robotN x:=… y:=… |