Metadata-Version: 2.4
Name: robot-msg-irsl
Version: 0.3.0
Summary: Typed JSON and binary robot messages shared by C++ and Python
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# robot_msg

Typed, transport-independent robot messages for Python and C++. JSON schemas
produce shared models, validation, JSON/binary codecs, a message registry, and
installed schema resources. The runtime has no third-party dependencies.
The distribution is `robot-msg-irsl`; the import package and CMake target remain
`robot_msg` and `robot_msg::robot_msg`. Licensed under the [MIT License](LICENSE).

- [Python usage](internal/docs/python_usage.md)
- [C++ and CMake usage](internal/docs/cpp_usage.md)
- Runnable examples: [Python](example/python), [C++](example/cpp)

## Requirements and distribution

- Python **3.11 or later**, with no upper version bound.
- C++14 or later; CMake 3.10 or later.
- Wheel construction uses setuptools; uv is required only for the optional
  CMake bootstrap and uv-based installation commands.

The wheel includes Python code, C++ sources and headers, CMake configurations,
schemas and typing information. It does **not** contain a platform-specific C++
binary or compile C++ at installation time. The consuming CMake project builds
these sources with its own compiler. Generated sources are included, so ordinary
consumers do not run the generator.

Install a locally built wheel into your virtual environment:

```sh
uv pip install /path/to/robot_msg_irsl-0.3.0-py3-none-any.whl
python -m robot_msg --cmake-dir
```

After a release is published under the `robot-msg-irsl` distribution name, consumers
can use `uv add robot-msg-irsl==0.3.0` instead. Preparing this version's artifacts does
not imply that it has been published or that PyPI name ownership is established.

```sh
cmake -S . -B build \
  -Drobot_msg_DIR="$(uv run python -m robot_msg --cmake-dir)"
cmake --build build
```

```cmake
find_package(robot_msg CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE robot_msg::robot_msg)
```

Source-tree `add_subdirectory` and conventional CMake install/find_package remain
supported. Plain `find_package` does not download dependencies. The opt-in uv
bootstrap described in the C++ guide installs a pinned package into a build-local
environment; it does not change system Python or the application's existing venv.

## Protocol 2 is a breaking release

Distribution version `0.3.0`, envelope protocol version `2`, binary magic `R2`,
and the message's string/FNV-1a wire ID are separate concepts. This release
requires consumer adapters to be updated; it does not transparently replace
protocol 1 submodules.

- Physical geometry uses shared types rather than unrelated arrays/matrices.
- Public `message.metadata` replaces `_robot_msg_timestamp` and the encoder's
  `timestamp=` argument.
- Native SMPL-X parameters use `SmplState`, not the Meta-specific old class.
- Viewer legacy duplicates are removed, batches use paired items, and lifetime
  semantics are explicit.

Both decoders reject protocol 1 before decoding its changed payload. The earlier
`RM` to `R2` magic change already separated the old bit-packed format from the
byte-aligned format; keeping that historical envelope version at 1 did not cause
old binary frames to be interpreted as the new layout.

## Shared geometry and coordinate meaning

Import common models from `robot_msg.types` or include `<robot_msg/types.hpp>`.

| Type | Meaning |
|---|---|
| `Vec2(x,y)`, `Vec3(x,y,z)` | Ordered Cartesian components |
| `Quaternion(x,y,z,w)` | XYZW unit rotation; finite, `abs(norm²-1) <= 1e-6` |
| `Pose(position,orientation)` | Child-to-reference transform, `p_ref = R p_child + position` |
| `Wrench(force,torque)` | Force in N and moment in N·m; the containing field defines axes and moment origin |
| `RotationVector(x,y,z)` | Axis-angle model parameter in radians, not necessarily an absolute physical rotation |
| `ColorRGBA(r,g,b,a)` | Four components in [0,1] |

Quaternions are validated, never silently normalized or sign-flipped. Sender
adapters convert matrices or internal WXYZ values to wire XYZW. Frame identifiers
name coordinate systems; they do not establish calibration, transform discovery,
or a common world origin. Do not relabel an unknown device frame as `world`.

Distances are meters and times are seconds. `q`, `dq`, and viewer `qpos` remain
model-coordinate arrays: nq may differ from nv, and their components are not all
necessarily revolute joint angles. Matrix-shaped numerical auxiliary arrays use
row-major flattening.

### Controller messages

`RobotState` carries `q`, `dq`, `tau_ext`, a publication `sequence`, named joint
and end-effector poses in `frame_id`, and `base_wrench_ext`. The latter's torque
is about the reference-frame origin, not automatically the sensor or center of
mass. A publication counter is not a clock.

`RobotAction` preserves the independent optional real-time and trajectory
branches. End-effector trajectories are `[arm][waypoint]`; configuration
trajectories are `[waypoint][coordinate]`. Contact link IDs are one-based; contact
points and forces are expressed in the referenced joint frame.

Optional command fields preserve presence. In particular, the existing
controller treats the presence of `ctrl_update` as an event; changing its bool
interpretation requires an explicit consumer migration. The message library
adds no acknowledgement or delivery guarantee.

### SMPL-X parameters

`SmplState` separates root orientation, 21 body rotation vectors, 15 left-hand
and 15 right-hand rotation vectors, translation, optional 10 betas and gender.
`hand_pose_convention` distinguishes `absolute` from `mean_offset`. A mean-offset
parameter must not be converted into an absolute quaternion without the model's
hand-mean information.

`root_orientation_valid` and `translation_valid` distinguish measured estimates
from deliberately unspecified model parameters. In particular, a camera pipeline
that zeroes root/translation does not thereby estimate a world pelvis pose.
Translation remains the SMPL-X model translation parameter. Missing betas are
different from explicitly supplied zeros. The prior 169-value storage layout can
be reconstructed at the adapter boundary without changing model assets or
recording formats. No SMPL-X, Torch, NumPy or model files are dependencies here.

### Viewer messages

Geometric messages identify their reference frame. Box dimensions are
`half_extents`, ellipsoid dimensions are axis radii, and arrow `direction` is a
full displacement vector from its origin. The previous viewer multiplied vector
length by two; preserving an old visualization therefore requires an explicit
adapter conversion, not an undocumented factor in the new message.

`Lifetime` has `one_frame`, `persistent`, or `seconds` modes. Only `seconds`
requires a positive `seconds` value. Old marker timeout zero maps to one-frame;
mesh colors that previously persisted map to persistent. Lifetimes begin at
receiver receipt on its local monotonic clock, not at the sender's timestamp.
Joint updates have separate queue-age semantics; expiration does not remove the
last applied robot pose.

Batch `items` keep associated geometry together, with shared appearance where
applicable. Preserve order, ID replacement, `visible=false` removal and empty
batch removal. `Clear` clears markers and message-applied colors, not the robot
model or last joint state. In `MessageBatch`, clear/update order is significant.

## Metadata and optional diagnostic arrays

Every message has independent public metadata. Payload conversion intentionally
excludes it.

```python
import robot_msg as rm
from robot_msg.types import MessageMetadata, NamedArray, Timestamp

message = rm.ControllerCommand(
    ctrl_mode=2,
    metadata=MessageMetadata(
        sample_time=Timestamp(seconds=2.5, clock_kind="simulation", clock_id="sim-run-1"),
        send_time=Timestamp(seconds=1730000000.25, clock_kind="unix"),
        aux=[NamedArray(name="joint_error", shape=[2], data=[0.02, -0.01], unit="rad")],
    ),
)
restored = rm.decode(rm.encode(message))
assert restored.metadata == message.metadata
```

- `sample_time`: source sample time, if known.
- `receive_time`: time at which the sender received upstream data; not the
  recipient's current receive time.
- `send_time`: time selected by the sender for transmission.

Each `Timestamp` declares `unix`, `monotonic`, `simulation`, or `device` clock
kind. Non-Unix clocks require a `clock_id`. Identical IDs do not imply a clock
synchronization accuracy guarantee. Encoding/decoding never stamps automatically
or overwrites metadata. Batch children retain their complete envelopes and
independent metadata; the outer envelope is not an implicit override.

`NamedArray(name,shape,data,unit?,frame_id?)` carries float64 diagnostics without
JSON strings or schema changes for each new name. `shape=[]` means one scalar;
a shape containing zero requires empty data. Names are unique per message.
Limits are 64 arrays, rank 8, 65,536 total values and bounded labels. Dimension
products, finite values and array lengths are validated. Integers and booleans
that require exact typed meaning belong in the payload, not float64 aux.

The aux limit is **not** a guarantee that the full payload or batch fits a
consumer's 1 MiB frame limit. Check the complete encoded frame size at the
transport boundary; batching several individually valid envelopes adds their
sizes.

## Ports, not sockets or hosts

Python: `from robot_msg import ports`. C++: `<robot_msg/ports.hpp>`.

| Constant | TCP port |
|---|---:|
| `ROBOT_STATE` | 28400 |
| `CONTROLLER_COMMAND` | 28401 |
| `ROBOT_ACTION` | 28402 |
| `CONTACT_INFO` | 28403 |
| `VIEWER` | 28410 |
| `META` | 28420 |
| `SMPL_STATE` | 28421 |

`schemas/ports.json` generates both languages. These are project defaults, not
IANA/OS reservations. Consumers choose hosts, bind/connect roles and explicit
overrides. External VR input and Viewer HTTP ports are separate. No port scan,
collision fallback, address discovery or socket dependency is provided.

ZMQ PUB/SUB does not guarantee every message or deadline. CONFLATE keeps the
latest socket message, not the latest message per type. Use complete batch
snapshots when related data must survive together, or separate traffic policies.
Reliable commands require acknowledgements/retries outside this library.

## Installed schema introspection

```python
from robot_msg import message_catalog, get_payload_schema, get_envelope_schema

for info in message_catalog():
    print(info.msg_type, info.wire_id, info.message_class, info.schema_id)
    payload_schema = get_payload_schema(info.msg_type)
    envelope_schema = get_envelope_schema(info.msg_type)
```

The default resolved schemas contain no `$ref`, and defaults are JSON-compatible
objects. `resolved=False` returns the original form. Returned dictionaries are
independent copies. Resources come from the installed package, not a search for
a source checkout. Concrete dataclasses, type hints, `to_payload` and
`from_payload` remain available for editors and adapters.

## Schema development

Canonical message schemas live in `schemas/messages/`, shared types in
`schemas/types/`, and the envelope in `schemas/envelope.json`. The generator uses
only the standard library. Modify schemas, not generated files.

```sh
./internal/tools/generate
./internal/tools/check-generated
```

Supported types are object, array, string, signed int64, float64 and boolean;
constraints include ranges, lengths and string enums. Objects forbid additional
properties. Optional fields without defaults use None/optional; null values are
not accepted. Required fields cannot declare defaults. Recursive object defaults
produce independent objects and are compared bit-exactly for binary omission.

Shared `$ref` is restricted to relative file-root references inside the shared
types directory, with description/default annotations. Remote references,
fragments, path escapes and cycles are rejected. `x-field-order` explicitly fixes
shared constructor/wire component order; otherwise fields are sorted by name.
`x-semantic` selects the small set of built-in cross-field checks. Unsupported
keywords are errors, not silently ignored rules.

The compatibility manifest detects unintended changes to field layout,
constraints/defaults and shared dependencies under the same protocol identity.
Updating a released contract requires an explicit version/release decision;
ordinary generation must not silently accept a new baseline.

## Wire format

JSON uses `{version,msg_type,metadata?,payload}`. Duplicate keys, extra fields,
invalid Unicode, nonfinite numbers and unsupported versions/types are rejected.
`to_payload/from_payload` operate only on the payload. Public `validate` and both
codecs validate the metadata as well.

The byte-aligned little-endian binary format starts with:

```text
2 bytes  magic R2
2 bytes  protocol version 2
4 bytes  FNV-1a message ID
1 byte   metadata presence flags: sample, receive, send, aux (bits 0..3)
N bytes  present metadata fields, in that order
M bytes  schema-driven payload
```

Fields use float64 numbers, bounded integer/enum/length widths, object flag bits
for presence/boolean values, and no names on the wire. Defaults are omitted only
when bit-exactly equal, including signed zero. Unknown flags, truncation, invalid
lengths/values and trailing bytes are rejected. One decoder call consumes exactly
one complete envelope; framing and transport are the caller's responsibility.

## Verification

The test suite additionally needs pip, setuptools and wheel in its selected
Python environment; offline bootstrap tests also need uv. These are development
tools, not runtime dependencies.

```sh
uv venv .venv --python 3.11
uv pip install --python .venv/bin/python pip 'setuptools>=77.0.3' wheel
cmake -S . -B build -DPython3_EXECUTABLE="$(pwd)/.venv/bin/python"
cmake --build build
ctest --test-dir build --output-on-failure
```

Tests cover deterministic generation, shared references/defaults, JSON/binary
validation, cross-language byte equality, metadata/aux boundaries, consumer
semantic contracts, installed schema APIs, wheel/sdist and external CMake
consumption. Packaging tests use isolated environments and the local wheel;
no robot or model inference is required.

The library provides no ZMQ, robot control, calibration, model loading, discovery,
or delivery scheduling. Consumer migrations remain separate changes.
