Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,15 @@
[submodule "vendor/mathematics_toolbox"]
path = vendor/mathematics_toolbox
url = git@github.com:dlr-ts/mathematics_toolbox.git

[submodule "vendor/ros2_observer"]
path = vendor/ros2_observer
url = git@github.com:dlr-ts/ros2_observer.git
[submodule "tools/lichtblick/lichtblick"]
path = tools/lichtblick/lichtblick
url = https://github.com/Lichtblick-Suite/lichtblick.git
[submodule "vendor/libOpenDRIVE/libOpenDRIVE"]
path = vendor/libOpenDRIVE/libOpenDRIVE
url = git@github.com:pageldev/libOpenDRIVE.git
[submodule "ros2_workspace/src/vendor/libOpenDRIVE"]
path = ros2_workspace/src/vendor/libOpenDRIVE
url = https://github.com/DLR-TS/libopendrive.git
[submodule "ros2_workspace/src/vendor/osqp-eigen"]
path = ros2_workspace/src/vendor/osqp_eigen
url = https://github.com/robotology/osqp-eigen.git
Expand All @@ -102,9 +101,6 @@
[submodule "ros2_workspace/src/vendor/osqp"]
path = ros2_workspace/src/vendor/osqp/osqp
url = https://github.com/osqp/osqp.git
[submodule "ros2_workspace/src/vendor/libOpenDRIVE"]
path = ros2_workspace/src/vendor/libOpenDRIVE
url = git@github.com:pageldev/libOpenDRIVE.git
[submodule "ros2_workspace/src/vendor/qdldl"]
path = ros2_workspace/src/vendor/osqp/qdldl
url = git@github.com:osqp/qdldl.git
Expand Down
48 changes: 46 additions & 2 deletions adore.env
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,62 @@ MQTT_BRIDGE_ENABLE=false

# MQTT_BROKER_HOST: Hostname or IP of the MQTT broker the bridge connects to.
# - Default: localhost
MQTT_BROKER_HOST=localhost
#MQTT_BROKER_HOST=localhost

# MQTT_BROKER_PORT: Port of the MQTT broker.
# - Default: 1883
MQTT_BROKER_PORT=1883
#MQTT_BROKER_PORT=1883

# MQTT_BRIDGE_CONFIG: Path to the mqtt_message_bridge config file.
# - Single source of truth for the bridge connection (host, port, TLS, certs,
# topic mappings). Relative cert names resolve against MQTT_BRIDGE_CERT_DIR and
# env_file resolves relative to this file.
# - Default: ${SOURCE_DIRECTORY}/ros2_workspace/src/adore_interfaces/mqtt_message_bridge/bridge_config.yaml
MQTT_BRIDGE_CONFIG=${SOURCE_DIRECTORY}/ros2_workspace/src/adore_interfaces/mqtt_message_bridge/bridge_config.yaml

# MQTT_BRIDGE_CERT_DIR: Directory holding the broker CA and client certs.
# - Relative cert names in MQTT_BRIDGE_CONFIG resolve against this directory.
# - Default: ${SOURCE_DIRECTORY}/ros2_workspace/src/adore_interfaces/mqtt_message_bridge/certs
MQTT_BRIDGE_CERT_DIR=${SOURCE_DIRECTORY}/ros2_workspace/src/adore_interfaces/mqtt_message_bridge/certs

# MQTT_LOCAL_BROKER_ENABLE: Start a local Mosquitto broker in Docker on the host.
# - Set to "true" to launch eclipse-mosquitto alongside the CLI container.
# - Set to "false" or leave unset to use an external broker.
# - Default: false
MQTT_LOCAL_BROKER_ENABLE=false

# gRPC Bridge Configuration
# =========================
# The ADORe CLI optionally runs the grpc_message_bridge ROS node as a host
# process. It is a gRPC CLIENT that dials a remote endpoint over an
# OAuth2-authenticated channel and bridges ROS2 topics to/from it. Logs are
# written to ${LOG_DIRECTORY}/grpc/.
#
# GRPC_BRIDGE_ENABLE: Start the grpc_message_bridge ROS node on the host.
# - Default: false
GRPC_BRIDGE_ENABLE=false

# GRPC_BRIDGE_CONFIG: Path to the stream/topic mapping config.
GRPC_BRIDGE_CONFIG=${SOURCE_DIRECTORY}/ros2_workspace/src/adore_interfaces/grpc_message_bridge/config/bridge_config.yaml

# GRPC_BRIDGE_PROTO_PATH: Generated python stubs added to PYTHONPATH at runtime.
# - Produced by the package build from the proto submodule.
GRPC_BRIDGE_PROTO_PATH=${SOURCE_DIRECTORY}/ros2_workspace/src/adore_interfaces/grpc_message_bridge/proto/generated

# GRPC_ENDPOINT: host:port of the remote gRPC service the bridge dials.
#GRPC_ENDPOINT=some_grpc_service.com:443

# AUTH_ENDPOINT: OAuth2 token URL. Leave unset for an insecure local channel.
#AUTH_ENDPOINT=https://some_oath_provider.com/auth/token

# GRPC_BRIDGE_SECRETS: Optional env file holding CLIENT_ID and CLIENT_SECRET.
# - Sourced by the start script if present; keeps OAuth secrets out of adore.env.
GRPC_BRIDGE_SECRETS=${SOURCE_DIRECTORY}/ros2_workspace/src/adore_interfaces/grpc_message_bridge/.secrets.env

# GRPC_BRIDGE_HOST / GRPC_BRIDGE_PORT: local grpc server bind for server-side streams.
#GRPC_BRIDGE_HOST=0.0.0.0
#GRPC_BRIDGE_PORT=50051

# RMW_IMPLEMENTATION: ROS2 middleware to use.
# - rmw_fastrtps_cpp: FastDDS (default, no router needed)
# - rmw_cyclonedds_cpp: CycloneDDS (no router needed)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.secrets.env
proto/generated
proto/
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AUTH_ENDPOINT=https://some_oath_provider.com/auth/token
GRPC_ENDPOINT=some_grpc_service.com:443
CLIENT_ID=some_client_id
CLIENT_SECRET=some_super_secret_secret
23 changes: 23 additions & 0 deletions ros2_workspace/src/adore_interfaces/grpc_message_bridge/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
ARG ROS_DISTRO=jazzy
FROM ros:${ROS_DISTRO}-ros-base

RUN apt-get update && apt-get install -y --no-install-recommends \
python3-pip \
python3-grpcio \
python3-yaml \
&& rm -rf /var/lib/apt/lists/*

RUN pip3 install --no-cache-dir --break-system-packages grpcio-tools protobuf

WORKDIR /ws
COPY . /ws/src/grpc_message_bridge/

ENV PYTHONPATH=/ws/src/grpc_message_bridge/proto/generated

RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \
cd /ws && \
colcon build --packages-select grpc_message_bridge

COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
123 changes: 123 additions & 0 deletions ros2_workspace/src/adore_interfaces/grpc_message_bridge/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
PACKAGE ?= grpc_message_bridge
PROTO_ROOT ?= $(PWD)/proto
PROTO_OUT ?= $(PWD)/proto/generated

PROTO_SOURCES := \
$(PROTO_ROOT)/messages/common.proto \
$(PROTO_ROOT)/messages/events.proto \
$(PROTO_ROOT)/messages/telemetry.proto \
$(PROTO_ROOT)/messages/signaling.proto \
$(PROTO_ROOT)/messages/commands.proto \
$(PROTO_ROOT)/messages/config/sensors.proto \
$(PROTO_ROOT)/messages/config/vehicle.proto \
$(PROTO_ROOT)/vehicle/stream.proto \
$(PROTO_ROOT)/client/notifications.proto \
$(PROTO_ROOT)/client/stream.proto

IMAGE_NAME ?= grpc_message_bridge
IMAGE_TAG ?= latest
ROS_DISTRO ?= jazzy
GRPC_HOST ?= 0.0.0.0
GRPC_PORT ?= 50051
BRIDGE_CONFIG ?= $(PWD)/config/bridge_config.yaml
SECRETS_ENV ?= $(PWD)/.secrets.env

.PHONY: help
help:
@echo "Targets:"
@echo " gen_proto Regenerate Python stubs from proto/"
@echo " build Build Docker image"
@echo " start Start bridge container (alias for run)"
@echo " run Start bridge container"
@echo " stop Stop bridge container"
@echo " restart Stop then start"
@echo " logs Tail bridge container logs (Ctrl-C to exit)"
@echo " crash Show logs from last exited container"
@echo " test Run integration test suite inside the container"
@echo " inspect Subscribe and print live data (DURATION=30 VEHICLE=MV-001 TYPE=ALL)"
@echo " SCENARIO=N to run a single scenario"
@echo " clean Stop + remove image"

.PHONY: gen_proto
gen_proto:
@mkdir -p \
$(PROTO_OUT)/messages/config \
$(PROTO_OUT)/vehicle \
$(PROTO_OUT)/client
python3 -m grpc_tools.protoc \
-I$(PROTO_ROOT) \
$(shell python3 -m grpc_tools.protoc --version > /dev/null 2>&1 && \
python3 -c "import grpc_tools,os; print('-I'+os.path.join(os.path.dirname(grpc_tools.__file__),'_proto'))") \
--python_out=$(PROTO_OUT) \
--grpc_python_out=$(PROTO_OUT) \
$(PROTO_SOURCES)
@for d in $(PROTO_OUT) \
$(PROTO_OUT)/messages \
$(PROTO_OUT)/messages/config \
$(PROTO_OUT)/vehicle \
$(PROTO_OUT)/client; do \
touch $$d/__init__.py; \
done
@echo "Stubs written to $(PROTO_OUT)"

.PHONY: build
build:
docker build \
--build-arg ROS_DISTRO=$(ROS_DISTRO) \
-t $(IMAGE_NAME):$(IMAGE_TAG) .

.PHONY: start run
start run:
@test -f $(SECRETS_ENV) || (echo "ERROR: $(SECRETS_ENV) not found"; exit 1)
-docker stop grpc-bridge 2>/dev/null || true
docker run --rm -d \
--name grpc-bridge \
--network host \
--env-file $(SECRETS_ENV) \
-v $(BRIDGE_CONFIG):/config/bridge_config.yaml:ro \
-e BRIDGE_CONFIG_PATH=/config/bridge_config.yaml \
-e GRPC_HOST=$(GRPC_HOST) \
-e GRPC_PORT=$(GRPC_PORT) \
$(IMAGE_NAME):$(IMAGE_TAG)

.PHONY: stop
stop:
-docker stop grpc-bridge 2>/dev/null || true

.PHONY: restart
restart: stop start

.PHONY: logs
logs:
docker logs -f grpc-bridge

.PHONY: crash
crash:
docker logs $$(docker ps -aq --filter name=grpc-bridge | head -1) 2>&1 || \
echo "No exited grpc-bridge container found"

.PHONY: inspect
inspect:
@test -f $(SECRETS_ENV) || (echo "ERROR: $(SECRETS_ENV) not found"; exit 1)
docker run --rm -it \
--network host \
--env-file $(SECRETS_ENV) \
-e PYTHONPATH=/ws/src/grpc_message_bridge/proto/generated \
$(IMAGE_NAME):$(IMAGE_TAG) \
python3 /ws/src/grpc_message_bridge/tools/test_integration.py \
--inspect --duration $(or $(DURATION),30) $(if $(VEHICLE),--vehicle $(VEHICLE),) $(if $(TYPE),--type $(TYPE),)

.PHONY: test
test:
@test -f $(SECRETS_ENV) || (echo "ERROR: $(SECRETS_ENV) not found"; exit 1)
docker run --rm \
--network host \
--env-file $(SECRETS_ENV) \
-e PYTHONPATH=/ws/src/grpc_message_bridge/proto/generated \
$(IMAGE_NAME):$(IMAGE_TAG) \
python3 /ws/src/grpc_message_bridge/tools/test_integration.py

.PHONY: clean
clean: stop
docker rmi $(IMAGE_NAME):$(IMAGE_TAG) 2>/dev/null || true
docker builder prune -f
106 changes: 106 additions & 0 deletions ros2_workspace/src/adore_interfaces/grpc_message_bridge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# grpc_message_bridge

## Setup

```bash
cp .secrets.env.template .secrets.env
# fill in CLIENT_SECRET
make build
```

## Run

```bash
make start
make stop
make restart
make logs
```

## Inspect live data

Subscribe and pretty-print incoming messages from the supervision backend.

```bash
# All message types, 30s (default)
make inspect

# Telemetry only, 60s
make inspect TYPE=TELEMETRY DURATION=60

# Notifications only, 120s
make inspect TYPE=NOTIFICATIONS DURATION=120

# All types, specific vehicles
make inspect TYPE=ALL VEHICLE="MV-001 MV-002"

# Combine
make inspect TYPE=TELEMETRY DURATION=300 VEHICLE=MV-001
```

`TYPE` options: `ALL` (default) · `TELEMETRY` · `NOTIFICATIONS`

## Test

```bash
# All 7 scenarios
make test

# Single scenario
make test SCENARIO=4
```

## ROS topics

All messages are JSON strings on `std_msgs/msg/String`.

### Incoming (supervision backend → ROS)

| Topic | Content |
|---|---|
| `/supervision/telemetry` | `VehicleTelemetryUpdate` -- vehicle id, state, position, velocity, battery, obstacles |
| `/supervision/notifications` | `NotificationMessage` -- id, vehicle id, type, severity, title, message |
| `/supervision/ack/rx` | `Ack` -- success, error_msg |
| `/supervision/signaling/rx` | `SignalMessage` -- WebRTC signaling (offer/answer/ICE) |

### Outgoing (ROS → supervision backend)

| Topic | Content |
|---|---|
| `/supervision/subscription` | `SubscriptionRequest` -- type (TELEMETRY/NOTIFICATIONS/ALL), vehicle_ids |
| `/supervision/ack/tx` | `Ack` |
| `/supervision/signaling/tx` | `SignalMessage` |

### Examples

Subscribe to telemetry:
```bash
ros2 topic echo /supervision/telemetry
```

Subscribe to notifications:
```bash
ros2 topic echo /supervision/notifications
```

Send a subscription request (all vehicles, telemetry only):
```bash
ros2 topic pub --once /supervision/subscription std_msgs/msg/String \
'{"data": "{\"type\": \"TELEMETRY\", \"vehicleIds\": []}"}'
```

Send a subscription request filtered to specific vehicles:
```bash
ros2 topic pub --once /supervision/subscription std_msgs/msg/String \
'{"data": "{\"type\": \"ALL\", \"vehicleIds\": [\"MV-001\", \"MV-002\"]}"}'
```

List all active bridge topics:
```bash
ros2 topic list | grep supervision
```

Check message rate:
```bash
ros2 topic hz /supervision/telemetry
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
enabled_streams:
- client.ClientService.ClientStream

client_streams:
- stream: client.ClientService.ClientStream
# The gateway streams nothing until it receives a SubscriptionRequest.
# Replayed on every reconnect. An empty payload leaves the oneof unset.
initial_send:
- field: subscription
payload:
type: ALL
vehicle_ids: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
bridge:
build: .
image: grpc_message_bridge:latest
network_mode: host
env_file:
- .secrets.env
environment:
BRIDGE_CONFIG_PATH: /config/bridge_config.yaml
GRPC_HOST: "0.0.0.0"
GRPC_PORT: "50051"
volumes:
- ./config/bridge_config.yaml:/config/bridge_config.yaml:ro
restart: unless-stopped
Loading
Loading