robot_msgs v1 — one domain model for robot data

2026-10-28 · /home/bam/robot_msgs · commit {{COMMIT}} · built from real runs by docs/build_report.py

robot_msgs exists and works: 30+ standard types, a unified Trajectory, plots, and real ROS 2 messages, all tested. It's a new local repo (main, no remote, not pushed). Nothing in bam_ws, traj_compare or like_keras uses it yet; migrating them is your call (below).
{{HERO}}

Recorded from robot_msgs.viz.animate(joints, tool) on the UR5e pick example: one JointTrajectory (626 samples, 8 ms grid) and the PoseTrajectory of tool0 from FK over its batch view. The cursor is traj.at(t).

What it is

Kept from ROSDropped
The domain model: geometry / sensor / trajectory / vision msgsBehaviourless structs. Offsetting, interpolation and projection were separate libraries; here they are methods.
Field names verbatim (JointState.name/position, CameraInfo.k)Array-of-structs as the only storage: a JointTrajectory is not 626 point messages
Every stamped message carries time + frame (Header)Codegen, colcon and a ROS install just to have a Pose
Explicit frame_id / child_frame_idMutable messages and msg.pose.pose.position.x = …

The Trajectory: one entity, many views

Storage is struct-of-arrays: one read-only array per field, with time on axis 0. Every other shape is a view (free, same memory) or a computed new value. Your instinct was right, and the July tuple-backed version couldn't deliver it: its to_numpy() view was a copy.

JointTrajectory frozen · numpy, read-only · time on axis 0 t (N,) positions (N, J) velocities (N, J) channels["kp"] (N, J) per-trajectory: joint_names, limits, header (stamp, frame_id) traj.positions → (N, J) array traj[i] / for p in traj → point (row views) traj[a:b], window(t0, t1) → trajectory traj.joint("elbow") → JointTrajectory traj.at(t), resample(dt=…) → new values to_points(), to_ros() → ROS-style points

Solid = view (no copy, measured with np.shares_memory in the tests) · dashed = computes a new value. joint() copies its columns (numpy fancy indexing); its row slices are views.

{{TOUR}}

How general should a type be? The function decides

Your framing is now rule 8 in docs/DESIGN.md. A function that works on anything with samples over time takes a Trajectory: resample, window, plot, concatenate. One that needs joint angles takes a JointTrajectory: joint(), limit_violations(), joint_state_at(). One that needs poses takes a PoseTrajectory: transformed(), shifted(), path_length(). The type is the validation. A new domain subclasses Trajectory and declares its sample() fields in its own file. It gets indexing, iteration, interpolation, plotting and serialisation for free, plus a generated point type (tested with a GripperTrajectory).

The UR5e pick example, end to end

examples/ur5e_pick.py turns named waypoints into a minimum-jerk JointTrajectory with UR5e limits and a gripper channel, then runs FK over the batch view into the tool's PoseTrajectory. Real output:

{{PICK_OUTPUT}}

Six trajectory shapes in your repos → one

TodayWhereIn robot_msgsBehaviour that must survive
NumpyTrajectory (frozen, ndarray, t0 = 0)mdp/env/sim/bam_arm/bam_arm/kinematics/contracts.py:181 + 2 byte-identical copies in like_kerasJointTrajectory, rebased() for t0 = 0joint_names; frame_id → header.frame_id; waypoint names/times → a sparse waypoint trajectory; trajectory_id digest is not modelled yet
"NpTrajectory" (mutable, all-optional, kp/kd/tau, ik_tip_path)archived bam/msgs, bam_traj_gen/docs/refJointTrajectory + channels, PoseTrajectorytau → effort; kp/kd/max_tau → channels; ik_tip_path (N,4,4) → PoseTrajectory.from_matrices; path metrics → channels
bam_traj_gen.JointTrajectory (tuples, tau_ff, waypoints)bam_traj_gen/src/bam_traj_gen/joint_trajectory.py:38JointTrajectoryq/qd/qdd → positions/velocities/accelerations; tau_ff → effort; waypoints → JointTrajectory with a name channel (tested); the numpy view becomes free
ROS-style JointTrajectory(header, joint_names, points)like_keras/lk/msgs/archive/ros_msgsto_points() / robot_msgs.ros.to_rosarray-of-structs on demand, never as storage
sim2real measured-run Trajectorysim2real/diagnostics_ref/robotdx/diagnostics/data.py:23JointTrajectory + channelsq_target, i_q, temp_c, voltage interpolate linearly; mode/fault hold (ints never blend, tested)
traj_compare.Situationtraj_compare/src/traj_compare/model.py:273JointTrajectory + PoseTrajectoryposes (N,4,4) → from_matrices; missing qd/qdd → with_derivatives()

The packages

ImportTypesHelpers that ROS left to other libraries
robot_msgs.stdHeadernow(), stamp_ns, sec/nanosec split, time_shifted()
robot_msgs.geometryVector3, Point, Quaternion, Pose, Pose2D, Transform, Twist, Wrench, Accel, PoseStamped, TransformStamped@ compose, inverse(), shifted()/rotated() (world or local), from_xyz_rpy, matrices, slerp, interpolate; batch kernel geometry.rotations
robot_msgs.sensorJointState, Image, CameraInfo, PointCloud, Imuproject/unproject, PointCloud.from_depth, transformed, downsampled, Image.from_array/to_rgb/crop
robot_msgs.visionBoundingBox2D/3D, ObjectHypothesis(WithPose), Detection2D/3D(+Array)iou, nms, filtered, as_arrays() batch view, corners/contains
robot_msgs.trajectoryTrajectory, JointTrajectory, PoseTrajectory, their points, JointLimitsviews, at/resample/window, with_derivatives, time_scaled, limit_violations, concatenate, from_joint_states
robot_msgs.viz [viz]—plot(msg) for every type, animate(...)
robot_msgs.ros [ros]—to_ros/from_ros (ROS 2 Jazzy via rosbags), write_bag/read_bag (mcap)

Visualisation: plot(msg) for every standard type

ROS 2 is an export, and it is real

robot_msgs.ros converts every type with a ROS counterpart to the actual ROS 2 Jazzy message classes (rosbags' typestore, no ROS install; vision_msgs 4.1.1 registered from upstream) and writes standard rosbag2 mcap bags that Foxglove opens. What ROS has no slot for is never dropped silently.

{{ROS_OUTPUT}}

Proof

Tests: {{TEST_TOTAL}} collected, {{TEST_FAILURES}} failing, {{TEST_SKIPPED}} skipped

pytest summary line: {{TEST_TAIL}}

{{TEST_ROWS}}
FileTests

Dependency boundary

Built wheel {{WHEEL}}, metadata:

{{REQUIRES}}

Third-party modules loaded by python -I -c "import robot_msgs" (everything outside the standard library, minus robot_msgs itself and the interpreter's own sitecustomize hook): {{PROBE}}

Decisions I made (each reversible)

DecisionWhyAlternative
Name robot_msgs, not lk_msgsIt sits below like_keras. Something that changes more slowly shouldn't carry the faster thing's prefix. The helpers don't need a framework name.lk_msgs (your 2026-07-21 idea), which inverts the dependency
JointTrajectory, not NpTrajectoryIt's ROS's name. Storage is an implementation detail and shouldn't be in the name.—
numpy storage (overrides the July tuple-backed FR)"Most compact, efficient storage with views." Tuple storage makes every numpy view a copy, at 4× the memory.Tuples plus an optional numpy view: stdlib-only, but views copy
Header.stamp is float secondsWhat all your trajectory and controller code already uses. Resolves 0.24 µs at today's epoch; stamp_ns is there for bags.int nanoseconds (lossless, clunkier)
xyzw quaternions everywhere; wxyz only via explicit convertersYour vault's stance (ROS/Pinocchio). MuJoCo and viser get xyzw_to_wxyz.—
Own 200-line rotation kernel instead of bam_ws's tf_transformations forkThe fork pulls transforms3d and pytest at runtime and lives inside bam_ws. The kernel is scipy-oracle-tested.Depend on the fork by path
shifted(), not shift()A frozen value returns a new value, so the name says so. Matches normalized/rebased/time_scaled.shift()
Floats print exactly in reprThe compact format printed stamp 1761600000.25 as 1.7616e+09. Truthful rendering wins over pretty.Rounded display
ROS export refuses to drop channels/limits silentlyROS has no slot for them. drop_extras=True makes the loss explicit.Drop them silently

What needs you

  1. Domain: robotmsgs.com is unregistered (registry RDAP; price and premium status unknown, since there's no Cloudflare registrar token). Buy it on Cloudflare yourself. Default if silent: nothing bought.
  2. Which consumer migrates first? I recommend traj_compare, whose Situation is already JointTrajectory + PoseTrajectory in disguise, then bam_arm's NumpyTrajectory. Default: nothing migrates until you say.
  3. Publish? A private GitHub repo, plus reserving robot-msgs on PyPI (free today). Default: local only.
  4. numpy over tuples reverses the July FR-TRAJ-JOINT-TRAJECTORY-VIEWS acceptance ("tuple-backed"). Default: numpy, as built.

Left, and deliberately not done