Visualize live sensor data - Blueye SDK

Skip to content

# Visualize live sensor data from the drone with Foxglove

With some simple steps you can visualize live sensor data from the drone in Foxglove.

1. Download foxglove here and create an account.
2. Power on the drone and connect your computer to the Blueye wifi.
3. Run `pip install "blueye.sdk[examples]"` to get the necessary dependencies, if you have not done so already.
4. Clone the blueye.sdk repository to get the examples, or copy the script below into a file. In the examples folder you simply run `python foxglove_bridge_ws.py` to start the bridge.
5. Open foxglove and open a new `Foxglove WebSocket` connection and leave it on default (`ws://localhost:8765`).
6. Add panel, Raw message, or Plot and select the topic you want to display.

### Alternative with Docker

We have also provided a docker container that you can use to automatically starts the blueye-foxglove server.

1. Pull the image: `docker pull blueyerobotics/foxglove-bridge`.
2. Run the image in a container with port 8765 open: `docker run --rm -p 8765:8765 blueyerobotics/foxglove-bridge`.
3. Connect as above in step 5.

### How it works

The script below uses the Blueye SDK to subscribe to the drone telemetry messages with `ZeroMQ`. Then the foxglove websocket server is forwarding the protobuf messages so they can be subscribed to in the `Foxglove GUI`.

### Visualize entire dive logs

With our `.bez` to `.mcap` log file converter you can easily visualize all the logged data from a dive. Check out the `Logfile to MCAP converter` to get started.

### Example of a websocket bridge

    #!/usr/bin/env python3
    import time
    import logging

    from blueye.sdk import Drone

    import asyncio
    import time
    import base64
    from foxglove_websocket import run_cancellable
    from foxglove_websocket.server import FoxgloveServer
    import sys
    import inspect
    from google.protobuf import descriptor_pb2
    import blueye.protocol

    # Declare the global variable
    channel_ids = {}
    global_server = None

    logger = logging.getLogger("FoxgloveBridge")
    logger.setLevel(logging.DEBUG)
    handler = logging.StreamHandler()
    handler.setFormatter(logging.Formatter("%(asctime)s: [%(levelname)s] <%(name)s> %(message)s"))
    logger.addHandler(handler)
    logger.info("Starting Foxglove bridge")

    logger_sdk = logging.getLogger(blueye.sdk.__name__)
    logger_sdk.setLevel(logging.DEBUG)
    logger_sdk.addHandler(handler)

    def parse_message(payload_msg_name, data):
        global global_server
        global channel_ids

        if payload_msg_name in channel_ids:
            try:
                asyncio.run(
                    global_server.send_message(channel_ids[payload_msg_name], time.time_ns(), data)
                )
            except TypeError as e:
                logger.info(f"Error sending message for {payload_msg_name}: {e}")
        else:
            logger.info(f"Warning: Channel ID not found for message type: {payload_msg_name}")

    def add_file_descriptor_and_dependencies(file_descriptor, file_descriptor_set):
        """Recursively add descriptors and their dependencies to the FileDescriptorSet"""
        # Check if the descriptor is already in the FileDescriptorSet
        if file_descriptor.name not in [fd.name for fd in file_descriptor_set.file]:
            # Add the descriptor to the FileDescriptorSet
            file_descriptor.CopyToProto(file_descriptor_set.file.add())

            # Recursively add dependencies
            for file_descriptor_dep in file_descriptor.dependencies:
                add_file_descriptor_and_dependencies(file_descriptor_dep, file_descriptor_set)

    def get_protobuf_descriptors(namespace):
        descriptors = {}

        # Get the module corresponding to the namespace
        module = sys.modules[namespace]

        # Iterate through all the attributes of the module
        for name, obj in inspect.getmembers(module):
            # Check if the object is a class, ends with 'Tel', and has a _meta attribute with pb
            if (
                inspect.isclass(obj)
                and name.endswith("Tel")
                and hasattr(obj, "_meta")
                and hasattr(obj._meta, "pb")
            ):
                try:
                    # Access the DESCRIPTOR
                    descriptor = obj._meta.pb.DESCRIPTOR

                    # Create a FileDescriptorSet
                    file_descriptor_set = descriptor_pb2.FileDescriptorSet()

                    # Add the descriptor and its dependencies
                    add_file_descriptor_and_dependencies(descriptor.file, file_descriptor_set)

                    # Serialize the FileDescriptorSet to binary
                    serialized_data = file_descriptor_set.SerializeToString()

                    # Base64 encode the serialized data
                    schema_base64 = base64.b64encode(serialized_data).decode("utf-8")

                    # Store the serialized data in the dictionary
                    descriptors[name] = schema_base64
                except AttributeError as e:
                    logger.info(f"Skipping message: {name}: {e}")
                    # Skip non-message types
                    raise e

        return descriptors

    async def main():
        # Initialize the drone
        myDrone = Drone(connect_as_observer=True)
        myDrone.telemetry.add_msg_callback([], parse_message, raw=True)

        # Specify the server's host, port, and a human-readable name
        async with FoxgloveServer("0.0.0.0", 8765, "Blueye SDK bridge") as server:
            global global_server
            global_server = server

            # Get Protobuf descriptors for all relevant message types
            namespace = "blueye.protocol"
            descriptors = get_protobuf_descriptors(namespace)

            # Register each message type as a channel
            for message_name, schema_base64 in descriptors.items():
                chan_id = await global_server.add_channel(
                    {
                        "topic": message_name,  # Using the message name as the topic
                        "encoding": "protobuf",
                        "schemaName": f"blueye.protocol.{message_name}",
                        "schema": schema_base64,
                    }
                )
                # Store the chan_id in the map
                channel_ids[message_name] = chan_id

            for name, chan_id in channel_ids.items():
                logger.info(f"Registered topic: blueye.protocol.{name}")

            # Keep the server running
            while True:
                await asyncio.sleep(1)

    if __name__ == "__main__":
        asyncio.run(main())