Skip to content

Give the offboard wire's command conversion one owner #597

Description

@vertix

Two modules convert between the offboard policy wire and roboarm.command, and neither owns the boundary:

Site Shape it handles
utils/serialization.py_pack / _unpack the __cmd__ envelope, via to_wire / from_wire
policy/remote.pyRemoteSession._as_command a command served as a bare {'type': ..., ...} mapping

Both answer the same question — a served command becoming the typed object the drivers dispatch on — and splitting them costs twice.

A generic utility carries domain knowledge. utils/serialization.py opens by saying so itself:

This module currently knows about roboarm.command directly. If we accumulate more domain types that need wire treatment (gripper commands, observation packets, etc.), replace the inline dispatch with a generic registry / __to_wire__ protocol so utils stays domain-agnostic.

Nothing answers "where does the wire become a command?" The conversion has since sat in utils/serialization.py, then policy/codec.py, then policy/remote.py. Six separate review findings on #576 flagged comments in four modules, each trying to state this boundary's contract from wherever its author was standing — codec.py, remote.py, keys.py, and the tests. Each finding was right that its site neither states nor enforces the rule. The rule still has no site that does.

The shape that already exists next door

simulator/env_server/protocol.py owns the env-server boundary's wire codec in one module, deliberately positronic-free so a benchmark's interpreter can import it. The offboard boundary has no equivalent: offboard/ holds client.py and server.py, and its encoding lives in utils/.

Scope

  • One owner for the offboard wire's command conversion, accepting both shapes it already accepts
  • utils/serialization.py drops its roboarm import
  • offboard/client.py and offboard/server.py are the users

Both shapes stay supported. The bare mapping is what a partner endpoint sends today; #576 carries tests that pin it through a live socket, and those must keep passing.

Out of scope, and why

  • The env-server wire (Model env-server command types as an enum, parsed at the wire boundary #592). A different vocabulary on purpose: cartesian against cartesian_pos, joint_vel/dq against joint_delta/velocities, hold for None, no reset, and _wire_command raises on a CartesianDelta outside the env control frame rather than encoding it. Its far end must not import positronic, so it cannot share this code.
  • dataset/serializers.py::Serializers.robot_command. A storage schema, unfolding a command into .pose / .joints / .pose_delta columns. Datasets outlive protocols.

What has to be checked before starting

Moving command handling out of utils/serialization.py is safe only if the remote dataset wire (dataset/remote.py, dataset/remote_server/server.py) never carries a CommandType. Serializers.robot_command unfolds commands into arrays at record time, so a stored episode should hold vectors rather than command objects — inferred from the serializer, not verified end to end. This is load-bearing: the two wires share serialise/deserialise.

Separate finding, not part of this scope

policy/recording.py::_command_field_arrays calls roboarm_command.to_wire to name the columns it writes, so recorded column names derive from the network encoding. Renaming a wire field would silently change what a recording is keyed on, and datasets written either side of that change would stop matching. Worth its own issue if it survives a closer look.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions