Extending the Type System#
Sugarcoat uses a type system built on SupportedType to bridge ROS 2 message types with Python-native data. This document explains how the system works and how to extend it with custom types.
SupportedType Base Class#
Every supported message type is a subclass of ros_sugar.io.supported_types.SupportedType. The base class defines three extension points:
class SupportedType:
# The ROS 2 message class (e.g., std_msgs.msg.String)
_ros_type: type
# Callback class for deserializing incoming messages
callback = callbacks.GenericCallback
@classmethod
def convert(cls, output, **_) -> Any:
"""Convert Python data into a ROS message instance."""
return output
@classmethod
def get_ros_type(cls) -> type:
"""Return the underlying ROS 2 message class."""
return cls._ros_type
_ros_type#
Class attribute that holds the ROS 2 message class. This is used to create subscriptions, validate topic compatibility, and generate UI schemas.
callback#
A callback class (typically a subclass of GenericCallback) that handles deserialization of incoming ROS messages. Different types use specialized callbacks – for example, ImageCallback for sensor images, OdomCallback for odometry, StdMsgCallback for simple std_msgs types.
convert#
A classmethod that takes Python-native data and returns a ROS message instance. The first positional argument should be named output. This is called by Publisher when a component publishes output. For example, Image.convert() accepts either a numpy.ndarray or an existing ROSImage and returns a ROSImage.
Built-in Types#
Sugarcoat ships with the following built-in types in ros_sugar.io.supported_types:
Type |
ROS Message |
Callback |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(default) |
Registering Additional Types#
Use add_additional_datatypes() to register custom types at runtime:
from ros_sugar.io.supported_types import add_additional_datatypes
add_additional_datatypes([MyCustomType, AnotherType])
This function maintains a global _additional_types dictionary. When a type with the same __name__ already exists, the function merges callbacks and conversion functions rather than replacing the existing entry. This allows multiple packages to augment the same type with additional callbacks.
Step-by-Step: Adding a Custom Type#
Suppose you want to add support for sensor_msgs/Range:
1. Define the type class#
from sensor_msgs.msg import Range as ROSRange
from ros_sugar.io.supported_types import SupportedType
from ros_sugar.io.callbacks import GenericCallback
class Range(SupportedType):
"""Range sensor message support."""
_ros_type = ROSRange
callback = GenericCallback
@classmethod
def convert(cls, output: float, **_) -> ROSRange:
msg = ROSRange()
msg.range = output
return msg
2. Register the type#
from ros_sugar.io.supported_types import add_additional_datatypes
add_additional_datatypes([Range])
3. Use it in a Topic#
from ros_sugar.io import Topic
range_topic = Topic(name="/front_sonar", msg_type=Range)
The topic can now be used as an input or output on any BaseComponent.
Custom Callbacks#
If the default GenericCallback is insufficient (for example, you need to extract specific fields or perform numpy conversions), create a custom callback class inheriting from GenericCallback:
from ros_sugar.io.callbacks import GenericCallback
class RangeCallback(GenericCallback):
def __call__(self, msg):
# Extract and store just the range value
self.output = msg.range
Then assign it to your type:
class Range(SupportedType):
_ros_type = ROSRange
callback = RangeCallback
# ...
How Derived Packages Register Types#
Packages built on Sugarcoat (such as Kompass or EmbodiedAgents) register their own types by calling add_additional_datatypes() at import time. For example, a navigation package might add:
# In my_nav_package/__init__.py
from ros_sugar.io.supported_types import add_additional_datatypes
from .types import CostMap, Waypoint, TrajectoryArray
add_additional_datatypes([CostMap, Waypoint, TrajectoryArray])
This ensures that when any component from my_nav_package is imported, the types are immediately available for topic wiring and event conditions.
Merging Behavior#
If two packages register a type with the same class name, add_additional_datatypes() merges them:
callback: If the existing type has no callback, the new one is used. If both have callbacks, they are combined into a list.
_ros_type: Only overwritten if the existing type has no
_ros_typeset.convert: Merged using the same list-accumulation logic as callbacks.
This allows, for example, one package to define the _ros_type and another to supply a specialized convert function for the same message type.