Kachaka Isaac Sim — design decisions¶
Agreed design for the simulator (2026-10-18). Numbers marked (est.) are not
published by Preferred Robotics; they come from the community Gazebo model
(CyberAgentAILab/kachaka_ros2_dev_kit) and live in config/robot.yaml.
Goal¶
A drop-in replacement for a real Kachaka when developing and testing ROS 2 applications. Synthetic data generation is a secondary use.
Scope¶
- MVP: differential drive, odometry/TF, LiDAR, front/back RGB cameras,
ToF depth, IMU, and stubs (battery, version,
set_enabledservices,robot_description,joint_states). Kachaka Shelf placed as a static obstacle. - Phase 2 (done):
goal_poseand thekachaka_command/executecommands, charger, battery, RETURN_HOME and shelf docking/carrying. - One robot; namespace and frame prefix are parameters.
Runtime¶
- Isaac Sim 6.1 in a project uv venv (
isaacsim[all,extscache]==6.1.0.0). scripts/run_sim.pystarts the sim, GUI or--headless.- Isaac Sim publishes with its bundled ROS 2 Jazzy libraries. Nav2, slam_toolbox,
RViz and teleop run in a
ros:jazzycontainer (host network, Fast DDS,/clock+use_sim_time).
Robot model¶
- Official URDF from
pf-robotics/kachaka-api(Apache-2.0), pinned commit, fetched byscripts/fetch_assets.py, converted to USD byscripts/build_usd.py. Generated USD is not committed. - Added after import: three low-friction caster spheres (two front on the floor, one rear 6 mm above it so the rigid body can tilt over 4–5 mm door sills), sensors, physics params. Drive wheels use sphere colliders (smooth rolling on any floor mesh).
- Wheels are articulation joints driven by velocity; torque limited to a realistic value (Kachaka Pro motor max 2 Nm).
- Velocity limits: manual cmd_vel 0.3 m/s, 1.57 rad/s (real bridge full
scale); autonomous motion (
sim/autonomous/cmd_vel) 0.8 m/s, the spec top speed (est.); both at most 0.5 m/s while towing a shelf, the spec top speed with a shelf (est.). Accel 2 m/s², 3 rad/s² (est.). See Speed limits below.
ROS 2 interface (namespace kachaka)¶
Same names, types and QoS as kachaka_grpc_ros2_bridge; optional remap to
generic names (/cmd_vel, /scan, /odom).
| Topic | Type | Notes |
|---|---|---|
manual_control/cmd_vel |
Twist (sub) | clamped to the manual limits |
sim/autonomous/cmd_vel |
Twist (sub) | sim-only, clamped to the autonomous limits, see Speed limits below |
lidar/scan |
LaserScan | PhysX raycasts, 240°, 400 pts, 10 Hz, 0.1–10 m (est.) |
front_camera/image_raw, camera_info |
Image, CameraInfo | 640×480, 10 Hz (lowered from 1280×720 for real-time) |
back_camera/... |
same | |
tof_camera/image_raw, camera_info |
depth Image | 160×120, 0.01–5 m (est.), 16UC1 mm (real format unknown) |
imu/imu |
Imu | |
odometry/odometry |
Odometry | ground truth + configurable noise; drives TF odom→base_footprint |
wheel_odometry/wheel_odometry |
Odometry | integrated from wheel joints (slip shows) |
sim/ground_truth |
Odometry | sim-only |
robot_info/battery_state |
BatteryState | SensorDataQoS, 1 Hz, CHARGING or DISCHARGING; voltage, current, charge, capacity, temperature (the real bridge: voltage −1 only, battery.bridge_fields_only) |
robot_info/version |
stub | |
manual_control/set_enabled |
SetBool | stub, always succeeds |
sim/charger_pose, sim/charger_detection |
PoseStamped | sim-only, see Charger below |
sim/layout, sim/shelf_poses, sim/shelf_detection, sim/shelf_docked, sim/shelf_dock (srv) |
sim-only, see Kachaka Shelf below | |
sim/front_stop/state, sim/front_stop/set_enabled (srv), sim/front_stop/mute (srv) |
sim-only, see Front obstacle stop below | |
sim/reset (srv), sim/reset/event |
SetBool, String | sim-only, see Reset below |
kachaka_sim_bringup's command_server adds the parts the real robot runs
itself: kachaka_command/execute (all command types, see Commands below), goal_pose,
layout/locations/list (charger id home, shelf homes <shelf>_home),
layout/shelves/list and auto_homing/set_enabled.
| robot_description, joint_states | | |
Speed limits¶
The real robot has two speed regimes: its bridge scales manual cmd_vel to
0.3 m/s and 1.57 rad/s, while its own autonomous motion reaches the spec top
speed, 0.8 m/s alone and 0.5 m/s with a shelf. In the simulator, Nav2's
commands reach the robot through nav_bridge, so on manual_control/cmd_vel
they could not be told apart from manual ones.
- Two inputs.
ros_nodealso subscribes the simulator-onlysim/autonomous/cmd_veland hands the sim loop(v, w, autonomous).VelocityCommand.set_target(..., autonomous=...)clamps the linear speed withDriveLimits.linear_cap(autonomous, towing):max_linear_vel(0.3) for manual commands,max_linear_vel_auto(0.8 (est.)) for autonomous ones, and at mostmax_linear_vel_towing(0.5 (est.)) for both while the limiter'stowingflag is set (set_shelf_dockedinrobot.pyandfake_sim.py). The angular cap and the accelerations stay the same. - Who uses which.
nav_bridgeforwards Nav2's/cmd_veltosim/autonomous/cmd_velwhenuse_sim_timeis true (bridge.launch.pysets itscmd_vel_topic), and tomanual_control/cmd_velfor a real robot.command_server(simulator only) drives MOVE_FORWARD, ROTATE_IN_PLACE, undocking and the like onsim/autonomous/cmd_vel. The gRPCSetRobotVelocity, teleop and anything else onmanual_control/cmd_velstay at 0.3 m/s. - Nav2 while towing. MPPI's
vx_maxand the smoother's maximum are 0.8 m/s. Onsim/shelf_docked, and every second since the controller server subscribes volatile,command_serverpublishesnav2_msgs/SpeedLimitonspeed_limit: 0.5 m/s absolute while towing, 0 (no limit) otherwise. MPPI scalesvx_maxandwz_maxby the same ratio, so Nav2 also turns slower with a shelf (0.98 rad/s); the docking servers have their own slow speeds. - Wheel joint limit. The official URDF limits the drive wheels to 10
rad/s, 0.45 m/s at the 0.045 m radius, which would cap the physics below
0.5 and 0.8 m/s.
_configure_drivesinrobot.pyraises the wheels' max joint velocity to 1.2 x (the top linear cap + full-rate turning) / radius. - Unknowns. Whether the real robot lowers its manual cap while towing is not published (the towing cap is above 0.3 m/s anyway), and all autonomous values are estimates until measured (#4).
Front obstacle stop¶
The real robot stops by itself in front of an obstacle (an app setting turns
it off). Every velocity source ends on manual_control/cmd_vel or
sim/autonomous/cmd_vel, so the
simulator applies the stop where it limits the velocity (robot.py,
safety.py):
- At each LiDAR scan,
front_clearancetakes the exact raycast distances (no noise, nomin_rangecut-off, since on the real robot the ToF camera covers the LiDAR's blind range) and finds the nearest point in a lane ahead of the body (front_x0.23 m,half_width0.15 m). - Each physics step, forward motion brakes (
VelocityCommand.update(..., forward_blocked=True), atmax_linear_accel) while the clearance is at mostmargin + v * scan period + v^2 / (2 * max_linear_accel). It is released once the clearance grows by 3 cm more, so the robot does not creep up in starts and stops. The command is kept, so the robot drives on when the way clears; backing up and turning are never blocked. - The lane (+-0.15 m) is inside a shelf's legs (inner faces at +-0.172 m),
so docking under a shelf works without an exemption; the towed shelf is
ignored by the LiDAR already. On the charger with
sensors_off, the clearance is treated as free. command_servermutes it (sim/front_stop/mute) for MOVE_BY_VELOCITY_WITH_MUTED_SENSORS and MOVE_FORWARD withmute_sensors, and fails a drive with −9 after the stop holds it for 1 s.
Torch lights¶
torches.py puts a USD SphereLight (radius 1 cm) with a cone (ShapingAPI)
at camera_front and camera_back, turned like the USD cameras so it shines
along the optical axis. This placement is an assumption: the real torches'
position is not documented and the URDF has no torch frame. Product photos
show a small unlabelled round window a few cm beside the front camera, which
may be the torch; a few cm makes no visible difference here. The lights cast no
shadows, otherwise the body shell in front of the lenses would block them.
SetFrontTorchIntensity / SetBackTorchIntensity arrive on gRPC threads, so
KachakaSimNode.set_torch changes the intensity in the simulation loop
(run_in_sim) and publishes the levels on sim/torch. Level 255 maps to
torch.max_intensity, linearly; brightness, cone and colour are estimates.
Reset¶
Tests used to restart the simulator to get back to the start state, which
takes about a minute. sim/reset (SetBool; data = also clear the SLAM map)
does it in place. The service callback runs in the simulation loop between
steps, so it changes the stage directly:
SimRobot.resetlets go of a towed shelf, sends every shelf home (Shelves.reset), teleports the articulation to the spawn pose with zero root and joint velocities, zeroes the velocity limiter and ignorescmd_velfor 1 s (Nav2's controller may still be sending), resets the wheel odometry, the battery and the front stop.- The fused odometry (the one SLAM uses) keeps its drift: the teleport is
added to it as an exact move, with no new drift. SLAM's map -> odom
already corrects that drift, and the robot then stands still, so SLAM has
nothing to correct a change with. Zeroing the drift instead left the map
pose off by a few cm for good, enough to fail docking (measured). With
data: truethe fused odometry restarts at the spawn pose with no drift, matching slam_toolbox's new map. - The node releases the emergency stop, turns the torches off and publishes
sim/reset/event({"id", "clear_map"}); the gRPC server hears it throughtap("reset"). command_serveron the event: bumpsrequest_id(stops the running and queued commands) andreset_epoch(theirrun()then publishes no result), publishessim/command/statewithrunning/lastcleared and"reset": id, clears the costmaps, and with clear_map callsslam_toolbox/reset.- The gRPC server clears history, last result, errors, sounds, volume and
auto homing. It clears history again when
sim/command/statefirst carries the new reset id, which drops a result that command_server sent before it heard of the reset. Under AMCL (navigation.launch.py) it publishes/initialposeat the last map -> odom, where the robot now is.
No gRPC call: kachaka-api has none for this, and a test can call the ROS service from the ROS side.
TF follows the real robot: URDF frames plus laser_link, camera_front,
camera_back, tof_camera (placed at the URDF sensor poses until real values
are known).
World¶
Isaac Sim bundled indoor assets plus Kachaka Shelf models from
GAI-313/kachaka_shelf_description (MIT) that the robot can dock with and carry.
Acceptance criteria (MVP)¶
- Teleop drives the robot at the commanded speed.
- RViz shows all sensors and TF with real-robot frame names.
- slam_toolbox builds a map and Nav2 reaches a goal.
- A single headless smoke-test script passes.
- GUI run at real-time factor ≈ 1.0 on an RTX 2080 Ti.
Implementation notes (deviations found while building)¶
- LiDAR via PhysX raycasts, not RTX LiDAR. The sensor sits inside the body mesh and RTX rays cannot ignore the robot's own geometry. Raycasts skip the robot's colliders and only see collision shapes, which suits a 2D scan.
- Physics 120 Hz, render 10 Hz. Control, odometry, LiDAR and IMU run in
physics callbacks; rendering (cameras) runs at
--render-hz(default 10, a multiple of the camera rate)./persistent/simulation/minFrameRateis set to 1 so each update runs all its physics substeps. Measured RTF on an RTX 2080 Ti, headless, 640×480 cameras: 1.45 at 10 Hz, 0.96 at 20 Hz, 0.71 at 30 Hz. - QoS. Odometry, wheel odometry, scan, IMU and cameras use SensorDataQoS
like the real bridge. Nav2 expects reliable
/odom, sokachaka_sim_bringuprelays it (nav_bridge), together with/cmd_vel→sim/autonomous/cmd_vel(manual_control/cmd_velwithuse_sim_time:=false, for a real robot). - No reversing in Nav2. The LiDAR covers 240° to the front and nothing
senses range behind the robot, so MPPI
vx_minand the velocity smoother's minimum x are 0, and the behavior trees drop theBackUprecovery.tests/ros_nav_check.pyfails on any backwardcmd_vel. - DDS buffers. Receiving 640×480 RGB at 10 Hz needs larger socket buffers
on the client:
FASTDDS_BUILTIN_TRANSPORTS=UDPv4?sockets_size=4194304. - ToF camera points 45° upward, as in the official URDF.
- Camera near clip is short (2.5 cm RGB, 1 cm ToF, set in
config/robot.yaml) so close objects stay visible; the lenses sit just behind the body shell. - The default room has 2.4 m walls so the upward ToF sees them.
- Callbacks drained each frame. The simulator spins its ROS node once per
render frame (10 Hz) but runs every ready callback, not just one. With one,
cmd_velqueued up and the robot kept moving ~0.5 s after Nav2 stopped it.
Charger, battery and RETURN_HOME¶
- Dock model. An L-shaped dark grey dock after the photos in Kachaka's
support pages: a back panel (collider) with a green LED, a thin floor plate
and two contacts (visual only). Sizes are estimates (
config/robot.yaml). - Charging is decided from ground truth: the robot charges when its
base_footprintis within 4 cm and 0.15 rad of the docked pose. - Battery (
power.py), updated each second from the mean power since the last update. Off the charger: idle power + losses proportional to the wheel rim speed (×towing_factorwith a shelf) + the wheels' mechanical power over the motor efficiency. The torque is the velocity drive's own,damping × (target − speed)capped atwheel_max_torque, computed rather than read back from PhysX.fake_sim.pyhas no torques, only the first two parts. On the charger: constant current up tocv_level(0.8), then a rate proportional to what is left, with a floor so it reaches 1.0. Voltage from a level-to-voltage table; current = power / voltage. All values (est.). - Backward docking, the one exception to "never reverse": the real robot
backs onto its charger and drives forward to leave. Nav2's docking server
(
opennav_docking,dock_backwards: true) drives to a staging pose 0.9 m in front of the dock, then reverses slowly (≤ 0.1 m/s). Leaving is an ordinary forward Nav2 goal. - Detection. The SLAM map drifts a few cm from the world, too much for the
contacts. The simulator publishes the dock pose relative to the robot within
1.5 m (
sim/charger_detection);command_serverrelays it to the docking server'sdetected_dock_pose(reliable QoS, which the server requires). How the real robot finds its dock is not published. - Sensors on the charger. The real robot stops its LiDAR and ToF while
charging.
charger.sensors_offdoes the same but is off by default: slam_toolbox stampsmap→odomwith the last scan, so without scans Nav2's TF lookups fail and it cannot drive off the charger. The real robot localizes itself and does not have this problem. - Auto homing in
command_server: afterauto_homing_idle_timeout(300 s) without velocity commands, or below 15 % battery. Retries wait 60 s. - Result error codes of
kachaka_command/executeare the simulator's own (negative); the real ones are not published.
Commands¶
- Moving. MOVE_TO_POSE is a Nav2
navigate_to_posegoal in the map frame. MOVE_TO_LOCATION uses the location's pose; for the charger it runs RETURN_HOME, since driving forward onto the charger would hit it. A towed shelf comes along with its footprint. - SPEAK has no audio. The text and
tts_on_successgo to the log and the simulator-onlysim/speechtopic, so tests can check them. - Queue. As on the real robot (kachaka-api docs),
cancel_allstops the running command and drops queued ones; without it the command waits for the earlier ones. The ROS action leavescancel_allfalse by default, andgoal_posesends MOVE_TO_POSE that way, as the real bridge does. - Canceling a Nav2 or docking goal waits until the goal has ended. Nav2 servers take a new goal as a preemption, and a late cancel request then stopped the next command's goal.
- Error −10: navigation failed.
Kachaka Shelf¶
- Model. The 3-layer shelf mesh from
GAI-313/kachaka_shelf_descriptionas a rigid body (6 kg (est.)) on four low-friction caster spheres. Its pose is thebase_footprintpose of a robot docked under it. - Docking.
sim/shelf_dockraises the pin when the robot is within 5 cm and 0.2 rad of that pose (either direction along the shelf's x axis) and joins robot and shelf with a fixed joint; lowering the pin removes it. While towing, the speed is capped atdrive.max_linear_vel_towing(0.5 m/s, see Speed limits below) and the LiDAR ignores the towed shelf. - LiDAR sees the legs only. The scan plane (z 0.105 m) lies just under the bottom plate, so its collider starts at 0.107 m. Otherwise the shelf showed up in the map as a closed box and Nav2 could not plan out from under it.
- Forward docking with a second docking server. Jazzy's
opennav_dockingtakes the docking direction per server, soshelf_docking.launch.pyrunsshelf_docking_server(dock_backwards: false, SimpleNonChargingDock with external detection) next to the charger's. Staging is 0.9 m in front of the docked pose. The server aims 0.25 m past the dock pose and checks collisions up to that point, sodock_collision_thresholdis 0.85 m: far enough that the shelf's own legs do not count. - Side. A shelf can be entered from either end.
command_serverpicks the end facing the robot, switches to the other one if the staging pose is in an occupied or unknown costmap cell, and retries the other end when staging fails. - Detection. As with the charger, the simulator publishes shelf poses
relative to the robot (
sim/shelf_detection) andcommand_serverrelays the chosen one toshelf_detected_dock_pose. To put a shelf down at a location, it relays the location's pose (from SLAM) instead, so placement is as good as SLAM: 3–10 cm in the test room. - Footprint. On
sim/shelf_docked,command_serverpublishes the shelf's footprint to both costmaps'footprinttopics (and back on undock) and clears the costmaps. - Shelf legs in the map. slam_toolbox mapped the legs while the shelf
stood still, and they stay in
/mapafter the robot docks under it. With the towing footprint the inflation around them covered the robot's own cell, so NavFn found no path until SLAM cleared them (up to a minute).footprint_map_filterrelays/mapto/nav_mapwith the cells under the robot's current footprint (plus 5 cm) set free, and the global costmap's static layer reads/nav_map. A robot is never inside an obstacle, so this holds without a shelf too. - Undocking lowers the pin and drives 0.50 m forward at 0.15 m/s on
sim/autonomous/cmd_vel. It refuses (error −9) when the LiDAR sees something in that path, since the robot does not reverse. At 0.40 m the robot's front corners (0.28 m from its centre) hit the shelf's legs when it then turned around, and it stayed stuck there (#39). - Parked shelves in the costmaps. The LiDAR sees only the four legs, and
the robot fits between them, so Nav2 planned straight through a parked
shelf and the robot ran into its legs.
command_serverpublishes the outlines of the shelves not on the robot on/parked_shelves(PointCloud2, map frame, 2 Hz), a marking-only source for both costmaps. The shelf being docked to stays in: the docking controller skips its collision checks near it. A towed shelf's old outline is cleared with the costmaps when it is docked, or by the LiDAR's rays later. - Loop closures under a shelf. slam_toolbox's
loop_search_space_dimensionis 3 m, so a loop closure moves the pose by at most 1.5 m. At the default 8 m, a scan taken while turning on the spot with a shelf just docked matched the wrong end of the 5 x 4 m room and threw the map pose 3.7 m off (#39). - Squaring up. The docking controller stops on position alone and put shelves down up to ~0.13 rad off the location's heading. MOVE_SHELF and RETURN_SHELF turn on the spot onto the location's axis (either direction) before lowering the pin when the robot is more than 0.02 rad off.
- RViz.
robot_descriptionstays the official URDF without the shelf, as on the real robot.shelf_markersdraws the shelves as a MarkerArray on/shelf_markers: standing shelves in the map frame fromlayout/shelves/list, the towed one inbase_footprintfromsim/shelf_detection, so it moves with the robot. - Not modelled. The real robot finds shelves with its cameras and shows each shelf's speed mode; here detection is handed over by the simulator and speed mode is always the default.
kachaka-api gRPC server¶
- In the simulator process.
kachaka_isaac_sim/api_server.pyserves theKachakaApiservice from the pip package's generated stubs (kachaka-api 3.17.5.0, grpcio 1.66.1) on--api-port(26400). The ROS node hands each message it publishes to the server (KachakaSimNode.tap), which keeps the latest one and converts it to the proto type on request, so sensor RPCs need no ROS round trip. - Cursors. Each stream keeps its latest value with a cursor (monotonic
ns). A request whose cursor equals the latest one waits for the next
value, as on the real robot; the client's
start_commandrelies on this. - Commands go through
command_server. Isaac Sim's bundled rclpy cannot load thekachaka_interfacestypes, so the server cannot callkachaka_command/execute. It publishes JSON onsim/command/start(with a fresh command id) andsim/command/cancelinstead.command_serversends the command to its own action under that id as the goal UUID, so queueing,cancel_allandtts_on_successbehave as for ROS clients. It publishes the running command and the last result of every command (ROS, gRPC, auto homing) on the latchedsim/command/state, which backsGetCommandStateandGetLastCommandResult. Withoutcommand_server,StartCommandfails with −6. - Pose and map.
GetRobotPosecomposes map→odom (slam_toolbox or AMCL, from/tf) with odometry;GetPngMapconverts/mapto the robot's PNG format (occupied 174, free 253, unknown 234, top row = max y). - Saved maps (
kachaka_isaac_sim/maps.py) are Nav2 yaml + pgm pairs inmaps/, somap_saver_clioutput and the maps the API writes are the same files. Under SLAM the only current map issim, the live/map. The server tells the modes apart by map_server's parameter service: with it (navigation.launch.py),GetCurrentMapIdis the launch map untilSwitchMap.SwitchMappublishessim/map/load;command_servercalls map_server'sload_map, gives AMCL the pose in the request ("pose":initial_poseor the map's charger pose) on/initialposeuntil AMCL reports it (position and heading) and stays quiet for 1.5 s, clears the costmaps and answers onsim/map/loaded. AMCL re-initialises when the new map reaches it, so an/initialposepublished after the answer alone could be overtaken (seen: ~20° off after an elevator ride).SetRobotPosepublishes/initialposedirectly. - Locations are the charger (
home, the default), each shelf's home (<id>_home) and the layout's extra locations. - Manual control.
SetRobotVelocitytakes values normalized to the speed limits and publishes onmanual_control/cmd_vel; it fails with −12 while manual control is disabled, so the client enables it first, as on the robot. - Odometry-driven commands. MOVE_FORWARD, ROTATE_IN_PLACE,
MOVE_BY_VELOCITY_WITH_MUTED_SENSORS, LOCK and DEPART_FROM_CHARGER run in
command_serveroncmd_veland odometry, without Nav2. MOVE_FORWARD checks the LiDAR 0.15 m ahead as it goes and stops with −9. The simulated robot follows a velocity command about 0.35 s late, so ROTATE_IN_PLACE slows down toward the end and stops when the turn left equals what the robot will still coast at its current rate; it lands within about 0.03 rad and returns once the robot stands still. - Shortcuts and history. Shortcuts come from
config/robot.yaml;StartShortcutCommandstarts their commands one after another. The history is the last 100 results fromsim/command/state, in memory. - Emergency stop.
SetEmergencyStopcancels every command ({"all": true}onsim/command/cancelalso drops the queue), zeroes the wheel command in the sim node and refusesStartCommand(−14) untilRestartRobot. - Shelf reset runs in the simulation loop:
KachakaSimNode.run_in_simqueues the call, which runs after the nextspin_once, and the gRPC thread waits on its future. - Sounds have no audio:
PlaySoundpublishes the name onsim/sound. - Checked in CI without Isaac Sim.
scripts/fake_sim.pyrunsKachakaSimNodeand this server with a kinematic robot in a 2D copy of the default room (kachaka_isaac_sim/flat_world.py: walls, boxes, people and shelf legs as boxes for the LiDAR and for collisions), and mirrorsSimRobot's limiter, front stop, odometry, IMU, battery, charger, shelf towing and reset. The robot follows the limited command; only the yaw rate lags (first order, 0.3 s,--turn-lag) so thatROTATE_IN_PLACEends where it does in Isaac Sim (the command server stops early byROTATE_LAG). It stops at obstacles instead of bumping into them; when it already overlaps one (a shelf reset onto it), it moves freely until clear, as if physics had pushed them apart.--tracelogs the pose and the command once a second.tests/api_fake_check.pybrings it up with SLAM, Nav2 andcommand_serverand runstests/api_check.py --no-cameras. Cameras, object detection, the hazard sensors, physics effects, Isaac scenes and saved-map switching are checked with the simulator only.
Web dashboard¶
- A separate process that only talks kachaka-api.
scripts/dashboard.pywraps aKachakaApiClientin a stdlibThreadingHTTPServer(kachaka_isaac_sim/dashboard.py), so it needs no ROS, does not load the simulator, and works against a real robot. It is off unless started. - Polling, no websockets. The page (
dashboard.html, plain JS and a canvas) polls/api/stateevery 0.5 s,/api/historyevery 3 s and the map and layout every 10 s, since the SLAM map grows. The server caches the layout and the map between those refreshes. - Idle. With no command the robot reports PENDING and an empty command; the dashboard shows that as "idle".
- Commands are sent with
wait_for_completion=False, so a request returns at once and the page follows the command through the state. Only the commands the page has buttons for are accepted (parse_command); anything else is a 400. - No path. kachaka-api has no RPC for the planned path, so the page draws the trail of poses it has seen.
Object detection¶
The real robot runs a detector network on the front camera and reports
ObjectDetection {label, roi, score, distance_median} (kachaka-api) and
/kachaka/object_detection/result (ROS 2 bridge). The simulator has no
network; it reads the answer from the renderer.
- Ground truth from Replicator. The front camera's render product has two
more annotators:
instance_id_segmentation(which prim each pixel shows) anddistance_to_camera. Each target (a person, a shelf, the charger) is a prim subtree;detection.detectmaps instance ids to targets and works on the pixels of each. Only visible pixels count, so occlusion needs no extra work, and the boxes and distances match the image they are stamped with. - Noise and limits are guesses. Range, minimum size, the score curve, box
noise and the miss rate are in
config/robot.yamlobject_detection, all marked est.; the real values are not published. - Features are empty.
GetObjectDetectionFeaturesreturns the stamp and no features: there is no network whose embeddings to fake. - ROS 2 through a relay. Isaac Sim's rclpy cannot load
kachaka_interfaces, so the simulator publishes JSON onsim/object_detectionandobject_detection_relay(inbridge.launch.py) republishes it asObjectDetectionListStampedwith the real bridge's label numbers (PERSON 0, SHELF 1, CHARGER 2, DOOR 3; UNSPECIFIED is dropped, as the bridge does). - People. Isaac Sim's characters (
Isaac/People/Characters,people.characters, taken in turn;world.add_person) with one invisible box collider, so the PhysX LiDAR, Nav2 and the front stop see them as obstacles. Two stand in the room by default (people.room); they do not move. The characters' rest pose is a T-pose, and the bundled animations (Isaac/People/Animations) are for another rig, played through the animation graph's retargeting; soworld.add_actorbinds a one-frame SkelAnimation of the rest pose with the upper arms turned down. The characters face −y, so they are turned 90° to face along the pose's yaw. Without the asset server, people are capsule mannequins.
Steps and collisions¶
Sources: the real error codes and texts are in kachaka-api
docs/api/ERROR_HANDLING.md (21051 "Kachaka is paused", 21052 "Step
detected", resumed with the power button). That the robot has two step
sensors at the front comes from secondary sources; their positions, height
and range are not published, so cliff in config/robot.yaml is assumed.
No bumper is published.
- Step sensors (
physics_sensors.CliffSensors,hazards.Cliff): two downward PhysX raycasts from the base link every physics step, ignoring the robot and a towed shelf. A sensor sees a drop when no floor is withinheight + threshold. A floor-to-drop transition pauses the robot: the velocity limiter is zeroed at once (the wheels' torque limit does the braking),{"all": true}goes tosim/command/cancel(command_server and the API server drop their commands),take_cmdreturns zero and the API refuses commands with 21051. After the power button, a sensor that still sees the drop only blocks forward motion (the same limiter input as the front obstacle stop), so the robot can back away without pausing again. At 0.3 m/s the casters stop about 1.5 cm before the edge. - Errors:
KachakaSimNode.error_codes()derives the list from the state (emergency stop, cliff pause, collision stop) andpublish_errors()hands it to GetError andsim/errors, so SetEmergencyStop, RestartRobot, the power button and sim/reset cannot leave stale codes behind. - Collisions (
physics_sensors.ContactSensor,hazards.CollisionCounter):PhysxContactReportAPIon the base link (applied before play) andsubscribe_contact_report_events. The wheels are separate links and are not reported; the casters are colliders of the base link and are filtered by collider path. The floor and a towed shelf never count. Events are queued in the callback and handled inpost_step. A contact with an object counts when it starts 0.5 s after the last event with that object, so one push against a wall is one collision. - Drops (
world.add_floor_with_drops,hazards.floor_tiles): the floor is a set of static boxes around the holes, the ground plane liesdrop.depthbelow. The boxes are triangle-mesh colliders: cooked convex hulls of 40 m × 20 m × 2 cm boxes came out a few millimetres short at the edges, and the step sensors saw a drop at the seam between two tiles.
Doors¶
Issue #26. A door is only the moving panel; the doorway's walls are plain
static boxes (--wall, world.add_wall).
- Panel (
world.add_door,doors.Doors): a kinematic rigid body with a box collider.Doors.stepruns before each physics step and sets the panel's USD translate and orient ops; PhysX follows them as kinematic targets.RigidPrim.set_world_poses(tensor API) moved the collider but not the rendered panel, so it is not used. A kinematic panel is not stopped by the robot: a closing door pushes it (no safety edge, assumed). - Geometry (
doors.panel_pose): a sliding panel is 2 × 5 cm wider than the doorway, runs just off the wall's −y face and slides +x until it clears the doorway. A hinged panel is 1 cm narrower than the doorway and turns about its −x end by up to 90° into the −y side. - State (
doors.DoorState):openinggoes from 0 (closed) to 1 (open) atsliding_speed/hinged_speed(doorinconfig/robot.yaml). A--door-cyclefollows the simulation time since the start or the lastsim/reset, starting in the start state; aset_opencall stops the cycle until the next reset.sim/doorsis published when a state changes (open, closed, opening, closing), not on every step. - Nav2: nothing door-specific. The panel is in the LiDAR scan, so the
obstacle layer marks a closed door and clears it again once it is open
(the scan raytraces through the doorway); the BT replans at 1 Hz. The
static layer is another matter while mapping: slam_toolbox learns a door
it sees closed, and with
occupancy_threshold0.1 a cell needs about nine scans through it for each one that hit it before it is free again (and it adds scans only while the robot moves). Sotests/ros_door_check.pyfreezes the SLAM map (slam_toolbox/pause_new_measurements) after driving through the open door, as when navigating on a saved map with the door open. A door that was closed when the map was saved stays a wall for the planner. - Automatic doors (#63,
doors.AutoSpec,DoorSpec.senses): the sensor area is a rectangle in the doorway's frame,|along| <= width / 2 + marginand|across| <= range, on both sides at once.Doorsgets the robot's ground-truth pose and the movers' poses as callables and checks them before each step;DoorState.stepkeeps the door's target open while the last sensing is less thanholds ago, so a door that is closing turns round as soon as something comes in. Only sliding doors (the usual kind), and not with a cycle. Aset_opencommand overrides the sensor untilsim/reset, as it stops a cycle. The sensor sees the true pose, not a scan: no blind spots, no reaction delay (assumed). Nav2 still sees a closed automatic door as an obstacle, so a goal behind the only doorway may not be planned until the door has opened (not tested);tests/ros_auto_door_check.pysends the robot up to the door first. - Elevators: see Elevator.
Elevator¶
Issue #48. kachaka_isaac_sim/elevator.py, floor_maps.py,
scripts/make_maps.py, layouts/elevator.yaml, layouts/elevator_vertical.yaml.
- Floors side by side: floor k is the room moved k × 10 m along x
(
FLOOR_SPACING) by default. A layout'sfloorsentries are sub-layouts (walls, doors, boxes, people) moved to their floor; shelves, the charger and the spawn pose stay on the first floor. These side-by-side coordinates are the "logical" ones that layouts,floor_maps,floor_ofandElevatorStatealways use. - Stacked floors (
elevator.stack: vertical): the stage is built in logical coordinates, then each upper floor's groups (its room, look and car) and each wall, box and person on it (byfloor_ofof its place) get an extra translate op put first (world.lift,stage_offset): back to x of the first floor andFLOOR_HEIGHT(3 m, assumed) up per floor. Each upper floor has a slab (add_slab, exact triangle meshes, top at the floor) over the room and car with the car's inside left open (slab_tiles). Car walls areFLOOR_HEIGHTtall, so they line the shaft. A floor under another gets a ceiling light (add_ceiling_light, a rect light atCEILING_LIGHTx the dome light), as the slab keeps the dome light out. The car's floor is a kinematic box (/World/Elevator/Platform) thatElevatormoves every pre-physics step toElevatorState.position(smoothstep between floors while riding); it reaches 2 mm under the walls and slab (kinematic and static bodies do not collide), so the step sensors see no gap at the doorway. Friction carries the robot and any shelf; nothing is teleported,odomand the map frame need no shift. The robot's logical pose (logical_pose: floor from its height above the start, rounded) is whatElevator,sim/elevatorandto_mapuse;sim/elevatoraddsstackandrobot_z. Doors and movers (they set world poses) on upper floors are not lifted, so the layout rejects them; the car's yaw must be a multiple of 90° (inside_rect). Object detection does not see people on upper floors (its targets are logical poses). The recording camera follows the robot's height (--record-follow), and a far near clip cuts the floor above. - Car: walls behind a doorway in a room wall, and a sliding door
(
doors.Doors, its own instance, not listed onsim/doors) on each floor.ElevatorStateis plain Python:gosets the target,stepcloses the door, waitsrides per floor once it is closed, then reports the arrival and opens the door on the new floor. While closing, the door reopens as long as the robot's base_footprint is withindoor_sensorof the doorway (assumed door sensor). - The ride (side by side) happens between physics steps (main loop,
take_ride): the robot articulation and any shelf inside the car (and the towed shelf) are moved by the floor offset with their velocities kept.NoisyOdometry.shiftmoves its origin by the same amount, soodomdoes not jump; wheel odometry integrates the wheels and never sees it.SimRobot.map_originmoves too, so sim-only map poses stay in the current floor's map frame. - Maps: SLAM's map frame is the spawn pose, so each floor's map frame is
the spawn pose moved to that floor; a pose in the car is the same on every
floor's map, and after a ride the next floor's map is loaded and AMCL is
given the pose it had.
floor_maprasterises the layout's static geometry (doors open, movers and people left out); the origin is snapped so that cell centres never sit on a wall face (otherwise thin walls came out one cell jagged). - Floor looks: each floor's room gets a floor tint and its name in
seven-segment strokes (
FLOOR_LOOKS,label_segments), thin boxes without colliders, so the PhysX-raycast LiDAR and step sensors miss them. - ROS:
sim/elevator/go(String) and latchedsim/elevator(JSON). kachaka-api has no elevator calls; on the real robot the building's system drives the ride (sources in worlds.md). - Not done: RViz markers for the car; people riding; more than one car.
Layout files¶
Issue #25. --layout FILE (kachaka_isaac_sim/layout.py, examples in
layouts/) places the spawn pose, charger, shelves, locations, people,
boxes, walls, doors and drops on top of any world.
- Plain Python: the file is read and every pose is settled before Isaac
Sim starts, so a bad file fails fast (
layout: ..., exit 1) and the parser is unit-tested without the simulator (tests/unit/test_layout.py). - Defaults: a key that is left out keeps what the world has without a
layout; a key that is given replaces it (
shelves: [],charger: null). The command-line options keep their meaning on top: the placement options (--shelf,--charger,--person,--no-*,--x/--y/--yaw) replace the layout's, the additive ones (--wall,--door,--drop) add to it. - Randomisation: any pose number can be
[lo, hi]. Fixed objects are placed first, then the randomised ones in file order, each drawn up to 200 times until its footprint circle (half its outline's diagonal) keepsclearancefrom the objects already placed, the layout's walls and drops, and in the room from its walls and boxes. The seed comes from--layout-seed, then the file, then a random one, and is printed so a run can be repeated. Circles are coarse (a shelf's is wide) but simple; they cannot see the furniture of an Isaac Sim scene. - Overlap check: so after
playeach object's box is put to a PhysXoverlap_boxquery against the stage (excluding the object itself and the robot, and for the spawn pose the charger and shelves it starts on), and a hit prints[kachaka] layout warning. It warns rather than fails, since a person standing against a table is often what was meant. - Not done:
fake_sim.py(the tests without Isaac Sim) does not read layouts.