RobotWebTools / rosbridge_suite

Server Implementations of the rosbridge v2 Protocol
https://robotwebtools.github.io
BSD 3-Clause "New" or "Revised" License
924 stars 519 forks source link

Error on CBOR compression of custom messages #965

Open pfarinha91 opened 1 month ago

pfarinha91 commented 1 month ago

Description

By using Roslibjs to subscribe to topics using CBOR compression, I'm having a Rosbridge error with some custom messages. I can subscribe to standard messages with the compression, but not some of my custom ones, particularly when containing vectors of other custom messages.

Expected Behavior

All messages arriving through Roslibjs on the wep application, when CBOR compression is requested.

Actual Behavior

The messages don't arrive, and Rosbridge prints the following error:

[rosbridge_websocket-4]     for slot, slot_type in msg.get_fields_and_field_types().items():
[rosbridge_websocket-4] AttributeError: 'numpy.ndarray' object has no attribute 'get_fields_and_field_types'

I don't know where this numpy.array comes from since my nodes are C++. I'm guessing is some conversion to Python on the message generators in between.

Possible solution

I was able to got it to work, but I'm unsure of the solution. Or, if the problem is caused by another completely different issue and this workaround is not the best approach.

On cbor_conversion.py, I added to the beginning of extract_cbor_values function:

if isinstance(msg, np.ndarray):
    return msg.tolist() 

And imported numpy. The final result being:

# (...)

import numpy as np

# (...)

def extract_cbor_values(msg):
    """Extract a dictionary of CBOR-friendly values from a ROS message.

    Primitive values will be casted to specific Python primitives.

    Typed arrays will be tagged and packed into byte arrays.
    """

    if isinstance(msg, np.ndarray):
        return msg.tolist() 

    out = {}
    for slot, slot_type in msg.get_fields_and_field_types().items():
        val = getattr(msg, slot)

        # string
        if slot_type in STRING_TYPES:
            out[slot] = str(val)

        # bool
        elif slot_type in BOOL_TYPES:
            out[slot] = bool(val)

        # integers
        elif slot_type in INT_TYPES:
            out[slot] = int(val)

        # floats
        elif slot_type in FLOAT_TYPES:
            out[slot] = float(val)

        # time/duration
        elif slot_type in TIME_TYPES:
            out[slot] = {
                "sec": int(val.sec),
                "nanosec": int(val.nanosec),
            }

        # byte array
        elif slot_type in BYTESTREAM_TYPES:
            out[slot] = bytes(val)

        # bool array
        elif slot_type in BOOL_ARRAY_TYPES:
            out[slot] = [bool(i) for i in val]

        elif slot_type in STRING_ARRAY_TYPES:
            out[slot] = [str(i) for i in val]

        # numeric arrays
        elif slot_type in TAGGED_ARRAY_FORMATS:
            tag, fmt = TAGGED_ARRAY_FORMATS[slot_type]
            fmt_to_length = fmt.format(len(val))
            packed = struct.pack(fmt_to_length, *val)
            out[slot] = Tag(tag=tag, value=packed)

        # array of messages
        elif type(val) in LIST_TYPES:
            out[slot] = [extract_cbor_values(i) for i in val]

        # message
        else:
            out[slot] = extract_cbor_values(val)

    return out

It seems to be working for every message I request, and my custom messages are being processed much faster with the compression='cbor' flag now.

Any tips or suggestions?

Thanks!

sea-bass commented 1 month ago

What does your custom message look like?

Because the function does check if the types are lists of primitive types, and then recursively calls itself on individual elements.

I'd also first confirm under which conditions the data becomes a NumPy array here, and see where there is a fixable bug before adding this special case.

pfarinha91 commented 1 month ago

Thanks for your answer.

This happens with a message type that defines an array of another custom message:

# ObjectArray.msg
std_msgs/Header header
test_msgs/Object[] objects
# Object.msg
uint32 id
std_msgs/Header header
geometry_msgs/Pose pose
test_msgs/Classification[] classification
test_msgs/Dynamics dynamics
uint32[] observers
bool is_observer

The test_msgs here is just a dummy name for my interface package, but this is basically its structure.

I'd also first confirm under which conditions the data becomes a NumPy array here, and see where there is a fixable bug before adding this special case.

I don't know where it becomes a numpy.array, the messages are generated with rosidl_generate_interfaces.

From what I can see in message_conversion.py that has the default extract_values / extract_json_values function, the list_types definition includes np.ndarray, deals with it in the code and converts to list.

list_types = [list, tuple, np.ndarray, array.array]

But on extract_cbor_values on cbor_conversion.py the list_types only include list and tuple, and it does not deal with numpy arrays.

LIST_TYPES = [list, tuple]