LSEP v0.2 — now a buildable ROS 2 reference implementation: typed lsep_msgs + managed lifecycle node, green CI

Hi everyone,

v0.2.0 now ships a runnable ROS 2 reference implementation — a package you can colcon build in a couple of minutes, with typed messages, a managed lifecycle node, and green CI. Before anything gets locked towards a 1.0, I’d like people who run real robots to poke holes in the architecture.

What’s in v0.2

1. Typed interfaces (lsep_msgs) lsep_msgs/Signal carries the state twice: as a uint8 enum for machines and as a state_name string for debugging via ros2 topic echo. Undefined physics are encoded as NaN instead of magic numbers (e.g. TTC when closing velocity is zero). Light, Sound, and Motion are separate message types — currently nested inside Signal on a single topic. The win today is type reuse and zero parsing overhead versus our previous JSON-in-String transport; whether modalities should get their own topics is an open question I’d like your input on (see below).

2. Managed lifecycle node The core lsep_node is an rclpy.lifecycle.Node following the ROS 2 managed-nodes design, so a safety supervisor can orchestrate configure/activate/deactivate deterministically and keep the signaling layer isolated from the navigation stack.

3. Engine v2.1 — two blind spots in my original state machine, fixed

  • Dwell-based de-escalation: a calmer raw state must remain stable for dwell_de_escalation_s (default 1.5 s) before the machine steps down. Escalation stays immediate; THREAT (< 0.5 s TTC) bypasses everything.
  • Input watchdog: if the sensor stream dies or occludes for longer than input_timeout_s, the reported state degrades to LOW_CONF instead of confidently latching the last danger state on dead sensors.

4. Deprecation policy The old JSON string on /lsep/state is kept as a deprecated mirror behind a publish_json parameter, so nothing existing breaks during migration.

Evidence

CI builds the workspace in a ros:jazzy container on every push, runs the 9/9 unit tests (the engine is testable without a ROS installation), and runs a headless smoke-test where a simulated human approaches, lingers, and retreats. The CI job hard-fails unless the observed state ladder actually reaches THREAT and then de-escalates — so green CI proves the temporal behavior, not just compilation. Ordered ladder from the smoke-test (two full cycles):

INTEGRITY → AWARENESS → INTENT → CARE → CRITICAL → THREAT → AWARENESS → IDLE
          → AWARENESS → INTENT → CARE → CRITICAL → THREAT → AWARENESS
                                            └─ dwell-based de-escalation, 2x ─┘

The THREAT -> AWARENESS step is the one that matters: it shows the dwell timer stepping the robot down only after the danger has genuinely cleared, rather than oscillating.

What I’d genuinely like review on

  1. State representation: uint8 enum + mirror string in one message — sane, or would you model this differently?
  2. NaN convention: NaN in float32 fields for undefined physics — acceptable, or do you prefer explicit validity bools?
  3. QoS: what profile would you expect for a safety-adjacent signaling topic at 10 Hz — reliable vs. best effort, what depth?
  4. Topic layout: one Signal topic with nested modalities (current), or per-modality topics (lsep/light, …) so e.g. an LED driver subscribes only to what it renders?
  5. Naming/namespacing conventions you’d want before this touches real fleets.

Known limits (honestly)

The TTC computation is currently 1D (distance / closing_velocity) — a placeholder; 2D time-to-intercept with trajectory prediction is on the roadmap. No hardware-in-the-loop testing yet — validation so far is the ros:jazzy CI build plus the simulated human, not a physical robot. This is a reference implementation, not certified production safety code.

Repo & 5 minute quick start: GitHub - NemanjaGalic/LSEP: Open protocol for standardized human-robot communication — 9 states, 3 modalities, 1 grammar. Physics-based. EU AI Act ready. · GitHub Release: Release ROS 2 Reference Implementation v0.2.0 · NemanjaGalic/LSEP · GitHub

Thanks for any time you spend tearing this apart.

— Nemanja

That’s all great, but what does LSEP actually do?

2 Likes

LSEP is an open, vendor neutral vocabulary for a robot to show its state to people nearby, mostly through light (sound/motion optional):
AWARENESS (I’ve registered you),
INTENT (I’m about to move — and where),
CARE (I’m slowing/yielding for you),
on up to CRITICAL/THREAT.
The escalation is physics-grounded — time to collision drives how urgent the signal gets — so it’s closer to brake lights/turn signals for a robot’s intent than to every vendor inventing its own blink pattern. The ROS 2 package is just so that’s testable, not a slide.

It’s a proposal, not an adopted standard — which is exactly why I’d rather have someone on the governance side poke holes in the model than nod at it. Where would you expect it to break first?

— Nemanja

Small update since the v0.2 announcement: two safety criticisms from the spec discussion thread are now spec changes — the spec is at v2.1-rc1 on main.

  • Photosensitivity cap: no light signal above the WCAG general-flash threshold may exceed 2 flashes/s (WCAG 2.2 SC 2.3.1 / ISO 9241-391 as a conservative adaptation — conformance is not claimed). CRITICAL is now an asymmetric double pulse instead of a 3 Hz strobe; one state (MED_CONF) is still flagged for revision before v2.1 final.
  • Optional platform_risk_class (candidate): CARE/CRITICAL TTC thresholds scale with a platform’s kinetic energy capability (½·m·v_max²) — a heavy industrial platform escalates earlier than a vacuum robot. The 0.5 s THREAT bypass stays fixed for everyone.
  • TTC thresholds consolidated against one canonical table — the spec had internal inconsistencies; fixed as errata.

Credit and details in the thread where the feedback came from: LSEP: Open protocol for standardized robot-to-human state communication (light + sound + motion) - #8 by NemanjaGalic — if you want to tear into the candidate before it becomes v2.1 final, the open issues are at https://github.com/NemanjaGalic/LSEP/issues (#3 is closed with the fix; objections welcome there too).