Visualize live sensor data from the drone with Foxglove
With some simple steps you can visualize live sensor data from the drone in Foxglove.
- Download foxglove here and create an account.
- Power on the drone and connect your computer to the Blueye wifi.
- Run
pip install "blueye.sdk[examples]"to get the necessary dependencies, if you have not done so already. - 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.pyto start the bridge. - Open foxglove and open a new
Foxglove WebSocketconnection and leave it on default (ws://localhost:8765). - 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.
- Pull the image:
docker pull blueyerobotics/foxglove-bridge. - Run the image in a container with port 8765 open:
docker run --rm -p 8765:8765 blueyerobotics/foxglove-bridge. - 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())