Skip to content

Commit 7ab73c4

Browse files
author
Michał Fąferek
committed
feat(mosaico_integration): add 3-robot fleet variant
Extends the Mosaico M0 demo with a fleet compose file so cross-robot forensic queries have more than one sequence to filter on. The single- robot variant proves the plumbing; the fleet variant is what makes the .Q catalog queries interesting. docker-compose.fleet.yml spins up three independent sensor-demos (warehouse-A on 18081, warehouse-B on 18082, outdoor-yard on 18083) each with its own bridge sharing a single mosaicod + postgres. Each bridge tags ingested sequences with distinct robot_id metadata. scripts/trigger-fleet-faults.sh injects three heterogeneous fault signatures so the resulting Mosaico catalog has visible variance: robot-01 warehouse-A LIDAR noise_stddev=0.5 (range std spikes) robot-02 warehouse-B IMU failure (different sensor) robot-03 outdoor-yard LIDAR drift_rate=0.5 (range mean shifts) medkit_params.yaml: widen ring buffer to 15 s pre + 10 s post (was 10 + 2) so each snapshot carries enough pre-fault baseline for drift-vs-noise comparison; lift max_bag_size_mb to 2000 so rosbag2 does not split the recording mid-flight (the gateway only serves the first split). bridge.py: honor POST_FAULT_WAIT_SEC (default 12 s) before downloading, so the longer duration_after_sec still finishes before the bridge grabs the bag. README updated with fleet quick start, file inventory, and refreshed numbers ("25 s snapshot", "~500 MB MCAP") consistent with the new ring buffer sizes. Verified end-to-end on this machine: 3 fault injections produce 3 sequences in mosaicod, all listable via MosaicoClient.list_sequences() and tagged with the expected metadata. Cold start to last verified ingest ~75 s.
1 parent 5d161a5 commit 7ab73c4

5 files changed

Lines changed: 242 additions & 18 deletions

File tree

demos/mosaico_integration/README.md

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
End-to-end pipeline demonstrating that **medkit fault snapshots flow into Mosaico as queryable, structured forensic data** - with no robot hardware and no recording 24/7. Designed as a homework artifact for the [Mosaico Labs](https://github.com/mosaico-labs) team.
44

5-
> Robot stack runs in Docker. Inject high noise on the simulated LiDAR. The medkit gateway detects it via the standard `/diagnostics` ROS topic, confirms the fault, and flushes its 10-second pre-fault + 2-second post-fault ring buffer to an `.mcap` file. A small Python bridge listens on the gateway's `/faults/stream` SSE endpoint, downloads the bag via REST, and ingests it into mosaicod via Apache Arrow Flight using Mosaico's own Python SDK. From `docker compose up` to a queryable `Sequence` in mosaicod takes about a minute.
5+
> Robot stack runs in Docker. Inject high noise on the simulated LiDAR. The medkit gateway detects it via the standard `/diagnostics` ROS topic, confirms the fault, and flushes its 15-second pre-fault + 10-second post-fault ring buffer to an `.mcap` file. A small Python bridge listens on the gateway's `/faults/stream` SSE endpoint, downloads the bag via REST, and ingests it into mosaicod via Apache Arrow Flight using Mosaico's own Python SDK. From `docker compose up` to a queryable `Sequence` in mosaicod takes about a minute.
6+
7+
There are two variants of the stack:
8+
9+
- **Single-robot** (`docker-compose.yml`): one sensor-demo + one bridge. Good for stepping through the pipeline the first time and for the notebook.
10+
- **Fleet** (`docker-compose.fleet.yml`): three sensor-demos (warehouse-A, warehouse-B, outdoor-yard) each with its own bridge, all ingesting into one shared mosaicod. Produces three heterogeneous fault snapshots (LiDAR noise, IMU failure, LiDAR drift) under distinct `robot_id` metadata so cross-robot queries actually have something to filter on.
611

712
## Quick start
813

@@ -29,6 +34,27 @@ jupyter notebook notebooks/mosaico_demo.ipynb
2934

3035
The notebook connects to `localhost:16726` (mosaicod Arrow Flight) and runs four queries against your freshly-ingested fault snapshot, ending with a three-panel time-series plot showing the LiDAR noise spike alongside a stationary IMU - exactly the kind of cross-topic forensic correlation Mosaico is designed for.
3136

37+
### Fleet variant
38+
39+
```bash
40+
# Three robots + three bridges + shared mosaicod. Ports 18081/2/3 for the
41+
# robots, 16726 for mosaicod (same as single-robot).
42+
docker compose -f docker-compose.fleet.yml up -d
43+
44+
# Wait ~25s after all containers are healthy so the ring buffer has a
45+
# pre-fault baseline, then inject three different fault signatures:
46+
# robot-01 warehouse-A : LiDAR noise_stddev = 0.5 (range std spike)
47+
# robot-02 warehouse-B : IMU failure
48+
# robot-03 outdoor-yard: LiDAR drift_rate = 0.5 (range mean shift)
49+
./scripts/trigger-fleet-faults.sh
50+
51+
# Each bridge independently ingests its robot's snapshot. After ~45s you
52+
# will have three Sequences in mosaicod with distinct robot_id metadata.
53+
docker compose -f docker-compose.fleet.yml logs -f bridge-01 bridge-02 bridge-03
54+
55+
docker compose -f docker-compose.fleet.yml down -v
56+
```
57+
3258
## Architecture
3359

3460
```
@@ -92,19 +118,21 @@ The same draft works on the **read** side too, as long as the consumer imports `
92118

93119
## Smart snapshots, not 24/7 recording
94120

95-
This is the value prop: each entry in the Mosaico catalog is **only the 12 seconds around a confirmed fault**, not hours of "nothing is happening" telemetry. With three faults injected the catalog totals ~140 MB, indexed and searchable. A naive 24/7 recording of the same four sensors at the same rates would be roughly 120 GB per robot per day. The pipeline preserves the signal and discards the noise.
121+
This is the value prop: each entry in the Mosaico catalog is **only the 25 seconds around a confirmed fault** (15 s pre-fault baseline + 10 s post-fault), not hours of "nothing is happening" telemetry. Three fleet snapshots together weigh roughly 1.5 GB of MCAP - a naive 24/7 recording of the same four sensors at the same rates would be closer to 120 GB per robot per day. The pipeline preserves the signal and discards the noise.
96122

97123
## Files in this directory
98124

99125
| Path | What |
100126
|---|---|
101-
| `docker-compose.yml` | The four-service stack: postgres + mosaicod + sensor-demo + bridge |
102-
| `bridge/bridge.py` | Subscribes SSE, downloads bag, calls `RosbagInjector` |
127+
| `docker-compose.yml` | Single-robot stack: postgres + mosaicod + sensor-demo + bridge |
128+
| `docker-compose.fleet.yml` | Fleet stack: postgres + mosaicod + 3×(sensor-demo + bridge) |
129+
| `bridge/bridge.py` | Subscribes SSE, downloads bag, calls `RosbagInjector`. Honors `POST_FAULT_WAIT_SEC` (default 12s) before download so the post-fault ring segment is finalized |
103130
| `bridge/Dockerfile` | python:3.11-slim + Mosaico SDK pinned to PR #368 commit `8e090cd` |
104131
| `bridge/requirements.txt` | `httpx>=0.27,<0.30` |
105-
| `medkit_overrides/medkit_params.yaml` | Sensor-demo medkit config with `auto_cleanup: false` defensively flipped |
132+
| `medkit_overrides/medkit_params.yaml` | Sensor-demo medkit config: 15s pre + 10s post ring buffer, single 2 GB bag cap, `auto_cleanup: false` |
106133
| `notebooks/mosaico_demo.ipynb` | Connect, list, query, plot - 7 cells |
107-
| `scripts/trigger-fault.sh` | Calls the medkit Scripts API to inject high noise on lidar-sim |
134+
| `scripts/trigger-fault.sh` | Single-robot: inject high noise on lidar-sim on `localhost:18080` |
135+
| `scripts/trigger-fleet-faults.sh` | Fleet: inject three different fault signatures on robots 01/02/03 |
108136
| `docs/lidar_noise_plot.png` | The output of the notebook's money shot plot |
109137
| `docs/EMAIL_TO_FRANCESCO.md` | Email draft to send Mosaico Labs |
110138

@@ -117,9 +145,9 @@ This is the value prop: each entry in the Mosaico catalog is **only the 12 secon
117145
| medkit gateway responds at `localhost:18080/api/v1/health` ||
118146
| `./scripts/trigger-fault.sh` injects fault, gateway returns CONFIRMED ||
119147
| Bridge SSE connects, picks up `fault_confirmed` event ||
120-
| Bridge resolves entity `apps/diagnostic-bridge` and downloads ~100 MB MCAP ||
148+
| Bridge resolves entity `apps/diagnostic-bridge` and downloads ~500 MB MCAP (25 s of four sensors) ||
121149
| `RosbagInjector` finalizes 4 TopicWriters (`/sensors/{scan,imu,fix,image_raw}`) ||
122-
| `MosaicoClient.list_sequences()` shows the new sequence within ~5s of fault ||
150+
| `MosaicoClient.list_sequences()` shows the new sequence within ~25 s of fault confirmation ||
123151
| Notebook reads back `LaserScan` data with `range_min`, `range_max`, `ranges`, `intensities`, `frame_id` populated ||
124152
| `IMU.Q.acceleration.z.between(...)` filter returns sequences ||
125153

@@ -130,8 +158,9 @@ This is the value prop: each entry in the Mosaico catalog is **only the 12 secon
130158
3. **Faults from the legacy diagnostic path land under `apps/diagnostic-bridge`**, not `apps/lidar-sim`. The diagnostic_bridge node is what owns the snapshot bag in this demo.
131159
4. **Mosaico read-side registry**: even with PR #368 installed, you must `import mosaicolabs.models.futures.laser` before reading `LaserScan` data. Otherwise the topic reader raises `No ontology registered with tag 'laser_scan'`. The bridge does not need this (write side resolves adapters by ROS msg type) but the notebook does.
132160
5. **Not all 5 listed topics actually land in Mosaico**: `/diagnostics` drops silently because no adapter is registered. The medkit ring buffer captures it; Mosaico just does not know what to do with it. See the table above.
133-
6. **Initial post-fault wait**: medkit holds the rosbag2 writer open for `duration_after_sec` (2s) after `fault_confirmed`. The bridge sleeps 2.5s before downloading to make sure the file is finalized.
134-
7. **Gateway port conflict on dev boxes**: we publish on `18080` and `16726` instead of the conventional `8080` and `6726` because dev machines often have other demos running. Adjust if you prefer defaults.
161+
6. **Initial post-fault wait**: medkit holds the rosbag2 writer open for `duration_after_sec` (10s in this config) after `fault_confirmed`. The bridge waits `POST_FAULT_WAIT_SEC` seconds (default 12) before downloading so the trailing ring segment is finalized.
162+
7. **Gateway port conflict on dev boxes**: the single-robot stack publishes on `18080` and `16726`; the fleet stack uses `18081/18082/18083` for the three gateways with `16726` shared by mosaicod. Adjust if you prefer defaults.
163+
8. **`rosbag2` file splitting**: if `max_bag_size_mb` is hit mid-recording, `rosbag2` splits into `_0.mcap`, `_1.mcap`, ... and the medkit gateway serves only the first split. The 2 GB cap in `medkit_params.yaml` is there to prevent splitting for any realistic 25 s snapshot.
135164

136165
## Troubleshooting
137166

demos/mosaico_integration/bridge/bridge.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,10 +346,12 @@ def handle_fault_event(fault: FaultEvent) -> None:
346346
LOG.warning("Confirmed fault has empty fault_code, skipping")
347347
return
348348

349-
# Tiny pause: medkit's post-fault timer keeps the bag writer open for
350-
# `duration_after_sec` (default 2s in sensor_diagnostics demo). Wait
351-
# so the bag is finalized and resolve_rosbag_file_path can find it.
352-
time.sleep(2.5)
349+
# Medkit's post-fault timer keeps the bag writer open for
350+
# `duration_after_sec` (configured to 10s in fleet demo). Wait for
351+
# finalization before trying to download.
352+
post_fault_wait = float(os.environ.get("POST_FAULT_WAIT_SEC", "12"))
353+
LOG.info("Waiting %.0fs for post-fault recording to finalize...", post_fault_wait)
354+
time.sleep(post_fault_wait)
353355

354356
bag = download_bag(MEDKIT_URL, fault)
355357
if bag is None:
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Mosaico M0 Fleet Demo: 3 robots, 3 bridges, 1 Mosaico archive.
2+
#
3+
# Shows cross-robot forensic queries that a folder of .mcap files cannot do.
4+
#
5+
# Robots:
6+
# robot-01 warehouse aisle A port 18081 ROS_DOMAIN_ID=41
7+
# robot-02 warehouse aisle B port 18082 ROS_DOMAIN_ID=42
8+
# robot-03 outdoor yard port 18083 ROS_DOMAIN_ID=43
9+
#
10+
# Each robot gets its own bridge that ingests fault snapshots into
11+
# the shared mosaicod with robot_id metadata.
12+
13+
name: mosaico-fleet
14+
15+
services:
16+
# ── Shared infrastructure ──
17+
postgres:
18+
image: postgres:16-alpine
19+
environment:
20+
POSTGRES_USER: postgres
21+
POSTGRES_PASSWORD: password
22+
POSTGRES_DB: mosaico
23+
volumes:
24+
- fleet_pgdata:/var/lib/postgresql/data
25+
healthcheck:
26+
test: ["CMD-SHELL", "pg_isready -U postgres -d mosaico"]
27+
interval: 5s
28+
timeout: 3s
29+
retries: 20
30+
networks: [fleet_net]
31+
32+
mosaicod:
33+
image: ghcr.io/mosaico-labs/mosaicod:v0.3.0
34+
environment:
35+
MOSAICOD_DB_URL: postgres://postgres:password@postgres:5432/mosaico
36+
RUST_LOG: info
37+
command: [run, --host, 0.0.0.0, --port, "6726", --local-store, /data/store]
38+
volumes:
39+
- fleet_store:/data/store
40+
depends_on:
41+
postgres: { condition: service_healthy }
42+
ports: ["16726:6726"]
43+
networks: [fleet_net]
44+
45+
# ── Robot 01: Warehouse Aisle A ──
46+
robot-01:
47+
build:
48+
context: ../sensor_diagnostics
49+
dockerfile: Dockerfile
50+
container_name: fleet_robot_01
51+
environment:
52+
ROS_DOMAIN_ID: "41"
53+
BEACON_MODE: none
54+
ports: ["18081:8080"]
55+
volumes:
56+
- ./medkit_overrides/medkit_params.yaml:/root/demo_ws/install/sensor_diagnostics_demo/share/sensor_diagnostics_demo/config/medkit_params.yaml:ro
57+
healthcheck:
58+
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/health || exit 1"]
59+
interval: 5s
60+
timeout: 3s
61+
retries: 30
62+
start_period: 30s
63+
networks: [fleet_net]
64+
65+
bridge-01:
66+
build: { context: ./bridge }
67+
container_name: fleet_bridge_01
68+
environment:
69+
MEDKIT_URL: http://robot-01:8080
70+
MOSAICO_HOST: mosaicod
71+
MOSAICO_PORT: "6726"
72+
ROBOT_ID: robot-01-warehouse-A
73+
SOURCE_DEMO: fleet_demo
74+
LOG_LEVEL: INFO
75+
POST_FAULT_WAIT_SEC: "15"
76+
depends_on:
77+
robot-01: { condition: service_healthy }
78+
mosaicod: { condition: service_started }
79+
networks: [fleet_net]
80+
81+
# ── Robot 02: Warehouse Aisle B ──
82+
robot-02:
83+
build:
84+
context: ../sensor_diagnostics
85+
dockerfile: Dockerfile
86+
container_name: fleet_robot_02
87+
environment:
88+
ROS_DOMAIN_ID: "42"
89+
BEACON_MODE: none
90+
ports: ["18082:8080"]
91+
volumes:
92+
- ./medkit_overrides/medkit_params.yaml:/root/demo_ws/install/sensor_diagnostics_demo/share/sensor_diagnostics_demo/config/medkit_params.yaml:ro
93+
healthcheck:
94+
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/health || exit 1"]
95+
interval: 5s
96+
timeout: 3s
97+
retries: 30
98+
start_period: 30s
99+
networks: [fleet_net]
100+
101+
bridge-02:
102+
build: { context: ./bridge }
103+
container_name: fleet_bridge_02
104+
environment:
105+
MEDKIT_URL: http://robot-02:8080
106+
MOSAICO_HOST: mosaicod
107+
MOSAICO_PORT: "6726"
108+
ROBOT_ID: robot-02-warehouse-B
109+
SOURCE_DEMO: fleet_demo
110+
LOG_LEVEL: INFO
111+
POST_FAULT_WAIT_SEC: "15"
112+
depends_on:
113+
robot-02: { condition: service_healthy }
114+
mosaicod: { condition: service_started }
115+
networks: [fleet_net]
116+
117+
# ── Robot 03: Outdoor Yard ──
118+
robot-03:
119+
build:
120+
context: ../sensor_diagnostics
121+
dockerfile: Dockerfile
122+
container_name: fleet_robot_03
123+
environment:
124+
ROS_DOMAIN_ID: "43"
125+
BEACON_MODE: none
126+
ports: ["18083:8080"]
127+
volumes:
128+
- ./medkit_overrides/medkit_params.yaml:/root/demo_ws/install/sensor_diagnostics_demo/share/sensor_diagnostics_demo/config/medkit_params.yaml:ro
129+
healthcheck:
130+
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/health || exit 1"]
131+
interval: 5s
132+
timeout: 3s
133+
retries: 30
134+
start_period: 30s
135+
networks: [fleet_net]
136+
137+
bridge-03:
138+
build: { context: ./bridge }
139+
container_name: fleet_bridge_03
140+
environment:
141+
MEDKIT_URL: http://robot-03:8080
142+
MOSAICO_HOST: mosaicod
143+
MOSAICO_PORT: "6726"
144+
ROBOT_ID: robot-03-outdoor-yard
145+
SOURCE_DEMO: fleet_demo
146+
LOG_LEVEL: INFO
147+
POST_FAULT_WAIT_SEC: "15"
148+
depends_on:
149+
robot-03: { condition: service_healthy }
150+
mosaicod: { condition: service_started }
151+
networks: [fleet_net]
152+
153+
volumes:
154+
fleet_pgdata:
155+
fleet_store:
156+
157+
networks:
158+
fleet_net:
159+
name: mosaico_fleet_net
160+
driver: bridge

demos/mosaico_integration/medkit_overrides/medkit_params.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ fault_manager:
7272
# Rosbag recording configuration
7373
rosbag:
7474
enabled: true
75-
duration_sec: 10.0 # Record 10 seconds before fault confirmation
76-
duration_after_sec: 2.0 # Record 2 seconds after confirmation
75+
duration_sec: 15.0 # Record 15 seconds before fault confirmation
76+
duration_after_sec: 10.0 # Record 10 seconds after confirmation
7777
lazy_start: false # Always recording (ring buffer)
7878
format: "mcap" # MCAP format (recommended for cross-platform)
7979
storage_path: "/var/lib/ros2_medkit/rosbags"
80-
max_bag_size_mb: 100 # Max size per rosbag file
80+
max_bag_size_mb: 2000 # Single-file cap; prevents rosbag2 from splitting (gateway only serves the first file)
8181
max_total_storage_mb: 1000 # 1GB total storage limit
82-
auto_cleanup: false # Cleanup rosbags on fault clear
82+
auto_cleanup: false # Retain rosbag on fault clear so the bridge can ingest it
8383

8484
# Topics to record (use 'config' or 'all')
8585
topics: "config" # Use include/exclude lists below
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
# Fleet fault injection - different fault signatures per robot:
3+
# robot-01: LIDAR noise (noise_stddev=0.5) - range std spikes
4+
# robot-02: IMU failure - different sensor entirely
5+
# robot-03: LIDAR drift (drift_rate=0.5) - range mean shifts over time
6+
set -euo pipefail
7+
8+
echo "=== Fleet Fault Injection ==="
9+
echo ""
10+
11+
echo ">> robot-01 (warehouse-A): LIDAR noise_stddev → 0.5"
12+
curl -sf -X PUT "http://localhost:18081/api/v1/apps/lidar-sim/configurations/noise_stddev" \
13+
-H "Content-Type: application/json" -d '{"value": 0.5}' > /dev/null
14+
15+
sleep 5
16+
17+
echo ">> robot-02 (warehouse-B): IMU failure"
18+
curl -sf -X POST "http://localhost:18082/api/v1/components/compute-unit/scripts/inject-failure/executions" \
19+
-H "Content-Type: application/json" -d '{"execution_type":"now"}' > /dev/null
20+
21+
sleep 5
22+
23+
echo ">> robot-03 (outdoor-yard): LIDAR drift_rate → 0.5"
24+
curl -sf -X PUT "http://localhost:18083/api/v1/apps/lidar-sim/configurations/drift_rate" \
25+
-H "Content-Type: application/json" -d '{"value": 0.5}' > /dev/null
26+
27+
echo ""
28+
echo "=== Injected ==="
29+
echo " robot-01: LIDAR noise (high variance)"
30+
echo " robot-02: IMU failure"
31+
echo " robot-03: LIDAR drift (shifting baseline)"
32+
echo ""
33+
echo "Wait ~60s for post-fault capture + bridge ingest."

0 commit comments

Comments
 (0)