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.py
to start the bridge. - Open foxglove and open a new
Foxglove WebSocket
connection 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
.
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": f"blueye.protocol.{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())