ROS Messages
============

``ltcodecs`` can encode and decode ROS messages.  The ROS message
path uses the same field configuration format described in
:doc:`codec_configs`, but it can also infer fields from ROS message
definitions.

ROS functionality is implemented using ARMW, so it works with ROS 1 and ROS 2.

Top-Level and Field Codecs
--------------------------

Use :py:class:`ltcodecs.ros_message_codec.RosMessageCodec` at application
boundaries.  It owns the top-level message codec, optional checksum handling,
and metadata returned from ``encode()``.

Use :py:class:`ltcodecs.ros_msg_field_codec.RosMsgFieldCodec` only when a
field inside another codec is itself a ROS message.  In YAML, this is the
``msg`` or ``ros_msg`` codec alias.

.. code-block:: yaml

   pose:
     codec: msg
     ros_type: geometry_msgs/Pose

Automatic Field Inference
-------------------------

If ``RosMessageCodec`` is created without a field dictionary, ``ltcodecs`` uses
ARMW to introspect for the ROS message fields and types:

.. code-block:: python

   codec = ltcodecs.RosMessageCodec("std_msgs/String")

For simple messages this is convenient.  For production acoustic links, prefer
an explicit YAML config so field bounds and lossy encodings are deliberate.
Explicit configs are also the way to change array limits, string lengths, float
resolution, optional fields, and nested message fields.

Explicit Field Configs
----------------------

Pass a field dictionary directly or load one from YAML:

.. code-block:: python

   codec = ltcodecs.RosMessageCodec.from_codec_file(
       "std_msgs/String",
       "string_msg_codec.yaml",
   )

.. code-block:: yaml

   data:
     codec: string
     max_length: 10

When encoding, every configured payload field must exist on the ROS message.
When decoding, ``ltcodecs`` constructs a new ROS message object from the
decoded field values.

ROS 1 and ROS 2 Type Syntax
---------------------------

ROS 1 and ROS 2 expose message type names differently.  ``ltcodecs``
normalizes the common forms before constructing nested message codecs.

==============================  =============================================
Type string                     Meaning
==============================  =============================================
``pkg/Msg``                     Message type, ROS 1 style
``pkg/msg/Msg``                 Message type, ROS 2 introspection style
``pkg/Msg[]``                   Variable-length array, ROS 1 style
``pkg/Msg[3]``                  Fixed-length array, ROS 1 style
``sequence<pkg/msg/Msg>``       Variable-length array, ROS 2 style
``sequence<uint8, 10>``         Bounded variable-length primitive array
``array<pkg/msg/Msg, 3>``       Fixed-length array, ROS 2 style
==============================  =============================================

The ROS 2 form ``pkg/msg/Msg`` is normalized to ``pkg/Msg`` before calling
ARMW's message import helpers.

Arrays
------

When arrays are inferred from ROS field definitions, ``ltcodecs`` maps them to
the array codecs used by explicit configs:

- fixed-size arrays become ``fixed_len_array``
- variable-size arrays become ``variable_len_array``
- primitive element types use their scalar codec alias
- message element types use ``element_type: msg`` and ``element_params``

For example, a ROS 2 field type like:

.. code-block:: text

   sequence<geometry_msgs/msg/Point>

is treated like this explicit configuration:

.. code-block:: yaml

   points:
     codec: variable_len_array
     max_length: 10
     element_type: msg
     element_params:
       ros_type: geometry_msgs/Point

The default maximum length for inferred variable-length arrays is currently
``10``.  Use an explicit config when that limit should be different.

Nested Message Fields
---------------------

Nested ROS message fields can either be inferred or configured explicitly.  An
explicit config is useful when only a subset of the nested fields should be
encoded or when individual nested fields need custom codecs.

.. code-block:: yaml

   point:
     codec: msg
     ros_type: geometry_msgs/Point
     fields:
       x:
         codec: float32
       y:
         codec: float32
       z:
         codec: float32

Without a ``fields`` mapping, ``RosMsgFieldCodec`` infers the nested message's
fields from the ROS message definition.

Metadata
--------

ROS message encoding returns ``(bits, metadata)``.  The metadata value is
usually ``None``.  When a configured field uses a metadata encoder alias, the
field is copied into the returned metadata dictionary instead of being encoded
into the bit stream.

During decoding, ``received_packet`` can provide packet metadata for configured
metadata decoder fields:

.. code-block:: python

   decoded = codec.decode(bits_to_decode, received_packet=packet)

The available metadata aliases are shared with dictionary codecs and are
listed in :doc:`reference/codec_aliases`.

Checksums
---------

``RosMessageCodec`` supports the same top-level checksums as
``DictCodec``:

.. code-block:: python

   codec = ltcodecs.RosMessageCodec(
       "std_msgs/String",
       fields_dict={
           "data": {
               "codec": "string",
               "max_length": 10,
           },
       },
       checksum="crc32",
   )

The checksum is appended after the encoded message payload.  Decoding raises
``ValueError`` if the received checksum does not match the decoded payload.

Testing Without ROS Middleware
------------------------------

ARMW's native backend can load ROS-style ``.msg`` definitions without running
ROS 1 or ROS 2 middleware.  This is useful for fast unit tests that need
standard message classes but do not need a ROS graph.

Set the middleware and interface search path before importing ARMW:

.. code-block:: console

   ARMW_MIDDLEWARE=native
   ROS_PACKAGE_PATH=/path/to/common_interfaces

Then tests can import messages with ARMW:

.. code-block:: python

   import armw

   String = armw.import_message("std_msgs", "String")

Use full ROS 1 and ROS 2 CI jobs for behavior that depends on actual
middleware, generated package layouts, or ROS-specific introspection behavior.
