Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Bridge a VDA 5050 v3.0.0 fleet-control interface (MQTT/JSON) onto Nav 2 under Clean Architecture — domain entities, MQTT/Nav 2 adapters behind ports, order→NavigateThroughPoses mapping, state aggregation, action handlers. Trigger when the user asks to build or extend a VDA 5050 connector / fleet bridge.
.claude/skills/harunkurtdev-vda5050-integration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 19% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 235% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 69% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 259% | 0% |
How to bridge a VDA 5050 v3.0.0 fleet-control interface (MQTT/JSON) onto a Nav 2 stack without breaking Clean Architecture.
rules/vda5050_protocol.md.rules/vda5050_messages.md.ROS .msg+bridge / C++ structs): rules/vda5050_implementation_formats.md. Real reference repos in ~/nav2_ws/src/: isaac_mission_dispatch (fleet/pydantic), isaac_ros_cloud_control (robot/ROS 2), vda5050_core (C++ core).
~/nav2_ws/src/VDA5050/(VDA5050_EN.md, json_schemas/*.schema).
~/nav2_ws/src/vda5050_connector/. Study it before writing a new one.
VDA 5050 is an outer-world protocol — MQTT topics, JSON payloads, schema versions. By the layer rules in rules/clean_architecture.md:
> The protocol does not belong in the domain. Domain entities model > orders, nodes, edges, actions, state as plain Python/C++ — no MQTT, > no json, no *_msgs. MQTT and Nav 2 are infrastructure adapters > behind domain ports.
PRESENTATION launch entrypoint, CLI, RViz
│
APPLICATION OrderHandler · StateAssembler · InstantActionHandler
│ (graph traversal, action queue, state aggregation)
DOMAIN entities: Order/Node/Edge/Action/State/Connection/Factsheet
▲ ports: MqttClientInterface · NavigationAdapterInterface
│ implements
INFRASTRUCTURE MqttBridge (paho) · Nav2Adapter (rclpy + NavigateToPose)The connector mirrors this exactly:
vda5050_connector/
├── domain/
│ ├── entities/ order.py state.py action.py connection.py
│ │ factsheet.py visualization.py header.py graph.py
│ └── interfaces/ mqtt_client.py navigation_adapter.py ← PORTS
├── application/ order_handler.py state_assembler.py
│ action_handler.py
└── infrastructure/ mqtt_bridge.py nav2_adapter.py
fleet_adapter.py vda5050_node.py ← composition rootModel each message as a dataclass tree. Keep field names aligned with the JSON schema so (de)serialization is mechanical, but do not put json.loads here — that is an adapter concern.
python# domain/entities/order.py (excerpt of the real connector) @dataclass class NodePosition: x: float = 0.0 y: float = 0.0 theta: float = 0.0 map_id: str = '' @dataclass class OrderNode: node_id: str = '' sequence_id: int = 0 released: bool = False node_position: Optional[NodePosition] = None actions: List[Action] = field(default_factory=list) @dataclass class Order: order_id: str = '' order_update_id: int = 0 nodes: List[OrderNode] = field(default_factory=list) edges: List[OrderEdge] = field(default_factory=list)
Ports (abstract) live in domain/interfaces/:
pythonclass NavigationAdapterInterface(ABC): @abstractmethod def navigate_to(self, node: OrderNode, on_done) -> None: ... @abstractmethod def cancel(self) -> None: ... @abstractmethod def get_position(self) -> AGVPosition: ... class MqttClientInterface(ABC): @abstractmethod def publish(self, topic, payload, qos=0, retain=False) -> None: ... @abstractmethod def subscribe(self, topic, callback, qos=0) -> None: ... @abstractmethod def set_last_will(self, topic, payload, qos=1, retain=True) -> None: ...
OrderHandler — validates an incoming Order, enforcesbase/horizon, runs the node/edge graph in sequenceId order, resolves missing nodePosition from a NavigationGraph (VDA 5050 allows omitting position when both MC and robot know the node), tracks nodeStates/edgeStates, and calls NavigationAdapterInterface. Handles order updates / stitching (same orderId, next orderUpdateId, first node = lastNodeId).
StateAssembler — aggregates position, velocity, battery, errors,actionStates, nodeStates/edgeStates into a State entity for periodic publish.
InstantActionHandler — cancelOrder, startPause/stopPause,stateRequest, factsheetRequest, pick/drop, etc.; drives the actionStatus state machine (WAITING → … → FINISHED/FAILED/RETRIABLE).
MqttBridge (paho-mqtt) — topic vda5050/v3/<mfr>/<serial>/<topic>,QoS 0 for everything except connection (QoS 1), MQTT last will on connection = CONNECTION_BROKEN. (De)serializes JSON ↔ domain entities here, and validates against json_schemas/*.schema.
Nav2Adapter (rclpy) — maps navigate_to(node) →NavigateToPose action; reads telemetry from running topics (/amcl_pose → position, /odom → velocity, /battery_state, /scan → safety, /diagnostics → errors/info).
vda5050_node.py — composition root: instantiates adapters,injects ports into the application services, runs the publish timers.
| VDA 5050 | Nav 2 / ROS 2 | |----------|----------------| | order node (released base) | NavigateToPose goal (per node) or NavigateThroughPoses (whole base) | | nodePosition {x,y,theta,mapId} | geometry_msgs/PoseStamped in map frame | | edge.maximumSpeed | controller setSpeedLimit / velocity-smoother cap | | edge.trajectory (NURBS) | sample → nav_msgs/Path (or let the planner replan) | | cancelOrder instant action | cancel the Nav 2 action goal | | startPause/stopPause | cancel/resume goal or velocity smoother gate | | pick/drop/finePositioning | custom behavior / action server (your hardware) | | state.mobileRobotPosition | /amcl_pose (PoseWithCovarianceStamped) | | state.velocity | /odom (nav_msgs/Odometry) | | state.powerSupply | /battery_state (sensor_msgs/BatteryState) | | state.errors / information | /diagnostics (DiagnosticArray) | | state.driving / paused | derived from goal status / pause flag | | state.safetyState | E-stop topic + /scan proximity | | initializePosition instant action | publish /initialpose to AMCL |
VDA 5050 prescribes MQTT QoS, which maps onto ROS 2 QoS at the bridge edge (see rules/ros2_communication.md):
| Data | MQTT QoS | ROS 2 QoS | |------|----------|-----------| | order / instantActions (commands) | 0 | RELIABLE, depth 10 | | state / visualization (telemetry) | 0 | BEST_EFFORT, depth 5 | | connection | 1 | RELIABLE, TRANSIENT_LOCAL |
/ JSON / *_msgs imports.
MqttClientInterface + NavigationAdapterInterface ports.actionStatus state machine in application services.
outbound JSON against json_schemas/*.schema.
connection last will to CONNECTION_BROKEN; publishONLINE on connect.
state on a timer (default ~1 Hz) and on every event(node reached, action status change); visualization faster (~10 Hz).
factsheet on connect and on factsheetRequest.adapters with launch_testing + a local MQTT broker (see rules/testing.md).
paho, json, or rclpy → it's not domain. Moveit to an adapter behind a port.
released=true) arecommitted; only extend the horizon or send an order update.
lastNodeId → must be rejected with anerror carrying the orderId/orderUpdateId.
headerId per-topic increment or non-UTC timestamps.loads: [] when the load state is unknown — omit thefield entirely instead (empty array means confirmed unloaded).
~/nav2_ws/src/VDA5050/json_schemas/; the VDA PDF is authoritative if they ever differ.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 28,864 | 26,133 | -9% | 1 | 1 | 0% | 6,231 | 8,351 | +34% | 0 | 0 | — |
case-02 | fail→fail | 37,236 | 31,309 | -16% | 1 | 1 | 0% | 6,224 | 8,771 | +41% | 0 | 0 | — |
case-03 | fail→pass | 31,676 | 23,917 | -24% | 1 | 1 | 0% | 6,234 | 7,393 | +19% | 0 | 0 | — |
case-04 | pass→pass | 13,978 | 8,305 | -41% | 1 | 1 | 0% | 2,356 | 4,055 | +72% | 0 | 0 | — |
case-05 | fail→pass | 5,482 | 5,848 | +7% | 1 | 1 | 0% | 1,087 | 3,643 | +235% | 0 | 0 | — |
case-06 | pass→pass | 14,125 | 14,868 | +5% | 1 | 1 | 0% | 2,188 | 5,251 | +140% | 0 | 0 | — |
case-07 | pass→pass | 12,761 | 6,022 | -53% | 1 | 1 | 0% | 2,286 | 3,662 | +60% | 0 | 0 | — |
case-08 | pass→pass | 12,077 | 7,123 | -41% | 1 | 1 | 0% | 2,176 | 3,843 | +77% | 0 | 0 | — |
case-09 | pass→pass | 9,462 | 7,331 | -23% | 1 | 1 | 0% | 1,684 | 3,935 | +134% | 0 | 0 | — |
case-10 | pass→pass | 8,624 | 3,856 | -55% | 1 | 1 | 0% | 1,464 | 3,204 | +119% | 0 | 0 | — |
case-11 | pass→pass | 17,751 | 15,328 | -14% | 1 | 1 | 0% | 2,820 | 5,165 | +83% | 0 | 0 | — |
case-12 | pass→pass | 14,185 | 9,639 | -32% | 1 | 1 | 0% | 2,145 | 4,150 | +93% | 0 | 0 | — |
case-13 | fail→pass | 13,765 | 9,232 | -33% | 1 | 1 | 0% | 2,125 | 4,102 | +93% | 0 | 0 | — |
case-14 | fail→pass | 12,437 | 3,251 | -74% | 1 | 1 | 0% | 1,785 | 3,025 | +69% | 0 | 0 | — |
case-15 | pass→pass | 16,637 | 17,144 | +3% | 1 | 1 | 0% | 2,467 | 5,371 | +118% | 0 | 0 | — |
case-16 | pass→pass | 7,949 | 5,034 | -37% | 1 | 1 | 0% | 1,353 | 3,414 | +152% | 0 | 0 | — |
case-17 | pass→pass | 14,770 | 5,190 | -65% | 1 | 1 | 0% | 2,314 | 3,361 | +45% | 0 | 0 | — |
case-18 | pass→pass | 14,416 | 11,410 | -21% | 1 | 1 | 0% | 2,312 | 4,428 | +92% | 0 | 0 | — |
case-19 | fail→pass | 6,043 | 6,383 | +6% | 1 | 1 | 0% | 999 | 3,584 | +259% | 0 | 0 | — |
case-20 | fail→pass | 8,039 | 3,641 | -55% | 1 | 1 | 0% | 1,208 | 3,082 | +155% | 0 | 0 | — |
case-21 | pass→pass | 16,975 | 16,428 | -3% | 1 | 1 | 0% | 3,330 | 5,935 | +78% | 0 | 0 | — |
case-22 | pass→fail | 14,826 | 14,111 | -5% | 1 | 1 | 0% | 2,812 | 5,261 | +87% | 0 | 0 | — |
case-23 | pass→pass | 8,949 | 8,964 | +0% | 1 | 1 | 0% | 1,687 | 4,147 | +146% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.