Source code for irsim.util.message

"""Helpers for applying :mod:`irsim.msg` messages to simulation objects.

These functions resolve which local objects an incoming message addresses.
They only read the objects they are given, so a caller can validate a whole
batch of updates before applying any of them.
"""

from __future__ import annotations

from collections.abc import Callable, Iterable
from typing import Any

from irsim.msg import ObjectState, WorldState
from irsim.util.util import find_object_by_identity


[docs] def resolve_message_targets( objects: Iterable[Any], msg: WorldState | ObjectState | Any, object_name: str | None = None, object_id: int | None = None, *, default_target: Callable[[], Any], ) -> list[tuple[Any, Any]]: """ Pair each incoming odometry with the distinct local object it addresses. A ``WorldState`` addresses every object it contains, an ``ObjectState`` uses its embedded name and id, and a standalone odometry message goes to ``default_target`` unless a name or id selects another object. Args: objects (Iterable): Local objects the message can address. msg: A :class:`~irsim.msg.WorldState`, an :class:`~irsim.msg.ObjectState`, or an odometry-shaped message. object_name (str): Explicit target name for a standalone odometry or object message. object_id (int): Explicit target id for the same messages. Names take precedence when both are given. default_target (Callable): Called only when a standalone odometry selects no object; returns the object that receives it. Returns: list: ``(object, odometry)`` pairs, one per addressed object. Raises: ValueError: If a target cannot be found, a name or id is combined with a ``WorldState``, or one object is addressed more than once. """ objects = list(objects) if isinstance(msg, WorldState): if object_name is not None or object_id is not None: raise ValueError( "object_name and object_id cannot be used with WorldState." ) updates = [ (_require_object(objects, obj_msg.name, obj_msg.id), obj_msg.odom) for obj_msg in msg.objects ] elif isinstance(msg, ObjectState): if object_name is None and object_id is None: object_name, object_id = msg.name, msg.id updates = [(_require_object(objects, object_name, object_id), msg.odom)] elif object_name is None and object_id is None: updates = [(default_target(), msg)] else: updates = [(_require_object(objects, object_name, object_id), msg)] seen: set[int] = set() for target, _ in updates: if id(target) in seen: raise ValueError( f"Received more than one state for object '{target.name}'." ) seen.add(id(target)) return updates
def _require_object( objects: list[Any], object_name: str | None, object_id: int | None ) -> Any: """Find an incoming message target, or report the identity that failed.""" target = find_object_by_identity(objects, object_name, object_id) if target is not None: return target identity = ( f"name={object_name!r}, id={object_id}" if object_name or object_id is not None else "without a name or id" ) raise ValueError(f"No simulation object matches received message {identity}.")