Engineering

How We Built the Fleet Adapter Layer: Connecting AMRs and AGVs to a Single Dispatch Brain

10 min read
Close-up of two different robot types side by side on warehouse floor

The most common question we get from operations teams evaluating Botpylon is some version of: "We have robots from three different vendors. Can your system actually talk to all of them?" The answer is yes, but it's worth explaining precisely how, because the architecture involved is not obvious and the naive approach doesn't work at scale.

Robot vendor APIs are not standardized. This is not a new observation, but the practical implications for fleet-agnostic dispatch are deeper than they first appear. It's not just that different vendors use different endpoint naming conventions. The underlying data models, task semantics, position reporting formats, state machine designs, and telemetry frequencies differ in ways that require genuine translation, not just field remapping.

The Problem With Direct Integration

The first version of our integration approach was what you'd expect from an early-stage team: write a specific connector for each robot vendor API and have the dispatch system call each connector directly. This works fine when you have one vendor. It gets complicated fast when you have three, and it becomes a maintenance problem when any of those vendors releases a firmware update that changes API behavior.

More fundamentally, direct integration doesn't solve the semantic problem. Consider position data: one vendor reports robot position as (x, y) coordinates in meters relative to a charging station, another reports coordinates in millimeters relative to a floor map origin point, and a third reports position as a node identifier in their proprietary navigation graph. Before you can build a unified floor graph, you need to translate all three into the same coordinate space. That translation has to be precise, consistent, and fast. If it's slow, your floor graph is stale. If it's inconsistent, the floor graph has phantom positions that produce bad routing decisions.

Task state is even harder. One vendor uses a four-state model (idle, in-transit, at-pick, returning). Another uses a nine-state model with sub-states for charging, error recovery, manual override, and path-blocked. A third vendor distinguishes between "task assigned" and "task accepted" as separate states with different timing characteristics. To dispatch optimally, Botpylon needs a normalized state model that preserves the semantically meaningful distinctions from each vendor's native model while presenting a consistent interface to the dispatch layer above it.

The Adapter Contract: What Every Adapter Must Expose

We settled on an adapter architecture where each vendor integration is implemented as a standalone adapter that fulfills a defined contract. The contract specifies what the adapter must produce, not how it produces it. This lets each adapter use whatever approach makes sense for that vendor's API, while guaranteeing that the dispatch core always receives data in a consistent format.

The adapter contract has four required output streams.

First, a position stream: every adapter must emit robot position updates in normalized floor coordinates (meters, from floor map origin), at a maximum emission interval of 500ms. If the vendor API only pushes position updates on movement events, the adapter is responsible for interpolating between events to maintain freshness. If the vendor API pushes more frequently than 500ms, the adapter buffers and downsamples. The dispatch core expects consistent cadence; adapter implementations are responsible for achieving it.

Second, a state stream: every adapter emits robot state updates using Botpylon's normalized six-state model (idle, assigned, in-transit, at-location, blocked, faulted). The adapter maps from the vendor's native state model to this six-state model. The "blocked" state is important: it means the robot has stopped due to an obstacle or safety event but has not entered an error recovery mode. This distinction matters for congestion-aware routing because a blocked robot is a temporary event that typically resolves in seconds, while a faulted robot needs human intervention. Treating them the same way leads to incorrect floor graph predictions.

Third, a capacity state: payload remaining (as a fraction of max payload), battery level (as percentage), and estimated time to next charge needed. The dispatch core uses this to avoid assigning long-route tasks to robots that will need to charge before completing them.

Fourth, a command channel: the adapter must accept task assignment commands in Botpylon's normalized task format and translate them into the vendor-native API call required to execute that task. The command channel is the output direction of the adapter; the first three streams are the input direction. Both directions are required for the system to function.

Handling the Hard Cases: AGVs and Fixed-Path Systems

AMRs are relatively straightforward to adapt because they're designed for flexible navigation. The bigger challenge is adapting AGVs, which typically operate on fixed wire-guided or magnetic-tape paths, and have fundamentally different task models than AMRs.

An AGV doesn't navigate to a coordinate; it follows a predefined route between fixed nodes. The "task assignment" for an AGV is typically selecting which route to activate, which station to go to next, or which queue to join. The position model is discrete (at node X, between nodes X and Y) rather than continuous. Speed is often fixed or constrained by the guidance system.

For AGV adapters, we maintain a node-to-coordinate mapping that translates between the AGV's node identifiers and the floor coordinate space. When the dispatch layer issues a position update, the adapter converts it to approximate floor coordinates based on the AGV's current path segment position. This approximation is less precise than AMR position reporting, and we reflect that by applying a larger position uncertainty radius to AGV nodes in the floor graph. The routing algorithm accounts for this uncertainty when placing AGVs near tight aisle passages where position accuracy matters most.

Task assignment to AGVs is also different. Instead of sending a destination coordinate (as you would for an AMR), the adapter maps the dispatch system's task to the closest AGV-accessible route endpoint. This means AGV adapters need a capability model that describes which tasks the AGV can execute and which task destinations are reachable given the AGV's fixed-path topology. Tasks that require destinations not on the AGV's path network are not assigned to AGVs, full stop. The dispatch layer treats AGVs and AMRs as members of the same fleet for planning purposes, but the adapter enforces physical constraints that the dispatch core itself doesn't need to know about.

Telemetry Normalization and the Coordinate Problem

Getting all robots into a single coordinate space is the foundational problem for the floor graph. You can't build a unified robot density map if your three robot types report positions in three different reference frames.

We handle this through adapter-level coordinate transforms. During initial setup, we run a mapping calibration step where we drive a robot across the floor and log its reported position versus its known physical position at a set of reference points. This produces a transform matrix for that vendor's coordinate system. The adapter applies this transform to every position update before emitting it to the floor graph system.

The calibration step takes about 30 minutes per robot type and needs to be re-run if the vendor firmware changes coordinate reporting. We've been bitten by firmware updates that silently changed the coordinate origin without documentation. Our adapter now includes a sanity check: if emitted positions jump more than 3 meters between consecutive updates at normal robot speed, the adapter logs an anomaly and flags the robot's position as uncertain until the next update confirms the position is plausible. This hasn't caught many errors in practice, but when it does catch one, it prevents the floor graph from being poisoned with a wrong robot position that could cause incorrect routing decisions for several dispatch cycles.

Latency Budget: Keeping the Adapter Layer Fast

The dispatch core needs to make task assignment decisions in under 300ms. The adapter layer sits between the robot vendor APIs and the dispatch core, so its latency is part of that budget. We allocate roughly 80ms of the budget to the adapter layer: 50ms for the adapter to process an incoming vendor API event and emit a normalized update, plus 30ms for network transit in both directions.

That 50ms processing budget sounds comfortable, but it gets tight when you're running 120+ robots across three adapters simultaneously. Each adapter runs as an independent process with its own connection pool to the vendor API. They emit to a shared message bus (we use a pub/sub system with sub-millisecond internal latency). The floor graph subscriber consumes from that bus and applies updates in arrival order. We run load tests at 3x expected robot density to confirm the adapters don't queue up under high telemetry volume.

The one area where we deliberately trade latency for correctness: state change events. When a robot transitions to the "faulted" state, we do a synchronous confirmation call back to the vendor API before emitting the state change to the floor graph. This adds 20-40ms to that specific event's latency but prevents false fault signals (which could incorrectly remove a healthy robot from the dispatch pool) from reaching the dispatch core. Fault events are rare enough that this doesn't measurably affect overall system latency, and the cost of a false fault is high enough that the confirmation step is worth it.

What the Adapter Layer Doesn't Handle

The adapter layer normalizes data and translates commands. It does not make routing or assignment decisions, and it doesn't attempt to improve on the vendor's native path planning within the vendor's fleet. A vendor's AMR fleet controller handles individual robot navigation, collision avoidance between robots of that fleet, and emergency stop behavior. The adapter doesn't interfere with any of that. It sits above the vendor's fleet controller, communicating through the vendor API, not around it.

We're not saying vendor fleet controllers are unnecessary or redundant. They handle robot-level navigation in ways that would be impractical to replicate at the dispatch layer. What the adapter layer provides is the information the dispatch layer needs to make cross-fleet task assignment decisions intelligently, and the command pathway to influence fleet behavior through the vendor-approved API interface. The boundary between the adapter layer and the vendor fleet controller is intentionally clear, and that clarity is part of why the adapter architecture is maintainable as vendors release firmware updates. The adapter's job is to translate, not to control.