At ShitOps we treat every passenger receipt as a first-class billing event. Our Self-driving cars each carry an onboard thermal printer, and when a trip ends the printer emits a receipt that must reconcile into SAP before the customer leaves the curb. We recently discovered that jammed printers were creating silent revenue leaks: the vehicle thought the receipt printed, SAP never received the line item, and support only found out when a passenger posted a photo in discord. This post explains the event-driven AI printer-observability fabric we rolled out to make every print job traceable, billable, and self-healing.

The Incident That Started It All

During a rainy Tuesday rollout, three vehicles in the same geo-cell reported successful print jobs while their paper trays were empty. The rclcpp printer driver kept publishing a cached ok state, the SAP adapter retried for 45 minutes, and the only visible symptom was a spike in support messages. We needed a design that could prove what happened to a single receipt across the vehicle, the message bus, the billing boundary, and the monitoring layer. We also needed a feedback loop that could open a discord thread for the on-call operator without anyone copying IDs by hand.

Architecture Overview

We model each printer as an autonomous edge service. A rclcpp node on the vehicle owns the printer State machine, publishes MQTT events, and annotates every transition with Jaeger trace context. A regional MQTT broker fans events into an AI confidence service, which scores whether a print job is likely complete. Confident jobs flow to SAP; suspicious jobs flow to discord and icinga2. Static ips are assigned per vehicle subnet so the broker ACLs and firewall rules stay deterministic.

stateDiagram-v2 [*] --> Idle Idle --> Printing: mqtt_job_received Printing --> Inspecting: rclcpp_status_ok Inspecting --> Billing: AI_confidence_above_threshold Inspecting --> DiscordAlert: anomaly_detected DiscordAlert --> SAPReconcile: ticket_ack Billing --> SAPReconcile: receipt_created SAPReconcile --> Icinga2Check: sync_complete Icinga2Check --> [*]: healthy

Edge Node: rclcpp and the Printer State machine

Each vehicle runs a rclcpp component named printer_sentinel. It subscribes to the printer device topic, wraps the vendor SDK, and drives a State machine with states for Idle, Printing, Inspecting, Billing, and Faulted. The node publishes MQTT messages on mqtt/printers/{vin}/state and mqtt/printers/{vin}/receipt. Every transition includes a Jaeger span ID, the current paper level, the firmware checksum, and the last known printer error code. If the State machine sees three identical fault transitions inside 90 seconds, it publishes a quarantine event and waits for the AI service to decide whether the receipt should be replayed. This gives us a crisp audit trail without asking the vehicle to understand SAP.

MQTT Topic Topology and IP Plan

The MQTT hierarchy is deliberately explicit: mqtt/fleet/{region}/{vin}/printer/state, mqtt/fleet/{region}/{vin}/printer/receipt, and mqtt/fleet/{region}/{vin}/printer/decision. Vehicles get static ips from a per-region pool, and the broker enforces client certificates plus topic ACLs by IP range. A bridge worker consumes receipt events, enriches them with the vehicle geofence, and forwards them to the AI scorer. We keep a replay topic for receipts that were quarantined, so an operator can republish the original MQTT payload after a paper refill. The IP plan also lets icinga2 probe broker health from inside each region without exposing the cluster to the public internet.

AI Confidence and Jaeger Traces

The AI scorer is a small gradient-boosted model that reads receipt length, print duration, paper-level delta, printer firmware, and the last 20 MQTT events for that vehicle. It returns a confidence score from 0 to 1. Anything above 0.93 is considered billable, anything below 0.80 is sent to discord for human review, and the middle band triggers a second opinion from a rules engine. Jaeger captures every hop: rclcpp transition, MQTT publish, bridge consume, AI inference, and SAP call. When a receipt takes longer than 12 seconds end-to-end, the trace is tagged as a billing-latency outlier. This is how we finally connected a paper jam in one vehicle to a delayed invoice in another region.

SAP, discord, and icinga2 Feedback Loop

The SAP adapter is idempotent by receipt ID. It posts a billing document, stores the SAP document number back on the MQTT decision topic, and emits a completion span to Jaeger. If SAP rejects the payload, the adapter opens a discord thread with the trace ID, the vehicle VIN, the printer serial, and the exact SAP error. The discord bot includes buttons for refill, replay, and escalate. icinga2 watches the whole pipeline with passive checks: broker lag, AI p95 latency, SAP 5xx rate, and unresolved discord threads. A failed check creates a second incident in icinga2 with a link to the same Jaeger trace, which keeps the on-call rotation aligned with the billing team.

Rollout and Results

We rolled the fabric out region by region behind a feature flag. In the first two weeks, the State machine quarantined 1,284 receipts, the AI scorer recovered 917 of them automatically, and the discord bot closed 301 threads after a paper refill. SAP reconciliation dropped from hours to seconds for the happy path, and icinga2 caught two broker partitions before they became customer-visible. The best part is that every receipt now has a Jaeger trace, a discord audit trail, and a SAP document number. We are already planning a second phase that streams printer telemetry into the same rclcpp graph used by our Self-driving cars perception stack, because a unified event model is clearly the right foundation for receipt reliability at fleet scale.