Lift Control¶
In production since 04.08.2026. Built and operated by Eric Cordero
(site/elevator-control, accessibility/multilevel-accessibility).
A robot changing floors is the hardest thing this system does, because it hands control of its own movement to somebody else for a while — and has to get it back, in every case, including the ones where the lift never answers.
Ownership, not scheduling¶
The lift service does not plan robot movements. It owns them for a while.
A move carries an owner and an ownerToken. The same owner may replace its
own move; a different owner is refused with 409 robot_owned, and the refusal
names owner, moveId and since, so the caller can see who holds it and for
how long.
That distinction matters because it decides what happens when two systems want the same robot. Scheduling would need a shared plan. Ownership needs only a token and an answer.
The four verbs¶
The interface IRobotMotionAuthority is an outbound port of the lift
service, not a service it offers. Whoever implements it provides the server
side of these four routes for their own chassis. The lift service is the
client.
| Verb | Route | Success | Also not an error |
|---|---|---|---|
currentMission |
GET /robots/:robotId/ownership |
2xx | 404 — robot is free |
pause |
POST /moves/:moveId/pause |
202 |
— |
resume |
POST /moves/:moveId/resume |
202 |
409 paused_by_others |
cancel |
DELETE /moves/:moveId?reason=… |
204 |
404 — already finished |
Five details that cannot be read from a specification and that cost one defect each:
The answer sits inside a ServiceResponse envelope. The ownership record
is under responseObject, not at the root. Reading the root yields undefined,
which reads as "no mission" for every robot — and then the door hold can never
engage, while the bench shows a steady stream of "robot-1 is running no
mission" with a voice mission plainly running. The flat form is still accepted.
moveId: null or absent means the robot is free. Not a 404, not an empty
body.
The reason for DELETE travels as a query parameter, because DELETE carries
no body. Without it every ending reads "Cancelled by request", and a door that
never opened cannot be told apart from an operator pressing cancel. That
distinction is what anybody reading the log afterwards needs.
pause takes an optional standoff {x, y, reason?} — a place to bring
the robot to before it parks, for the case where it would otherwise stop
inside a door area. Only the door service knows that geometry, so it travels
with the call.
409 paused_by_others means our hold is released and somebody else still
holds one. Treated as success. A 409 with a different reason throws.
Operating figures¶
| Value | Setting |
|---|---|
| Request timeout | 3000 ms |
| Ownership polling | at most once per second per robot |
| Lift queue re-decided | every second, without any latch |
If the authority does not answer, the door service does nothing — there is deliberately no local fallback onto the chassis. The consequence is stated openly: the robot keeps driving towards the door, and the chassis obstacle avoidance is what stops it. A slow server is therefore visible behaviour, not a silent failure.
The pause is emulated¶
The chassis has no pause verb, only PATCH /chassis/moves/current
{state:"cancelled"}. A resume issues a new move to the same target from
the current pose.
Three consequences follow, and every caller has to know them: the planned path is gone; the route after the resume may differ from the route before it; and the cost of a pause is a replan, not a standstill.
A door that never reacts is a cancellation, not a longer pause. After the
timeout the way out is DELETE /moves/:moveId, not more holding.
A cancellation inside a floor-change sequence takes effect at the next safe boundary. The robot is never left standing in the cabin.
Order of precedence¶
(priority, claimedAtMs, serial) — priority first (smaller goes first, absent
means 0), then the moment of claiming, and the serial number only to break a
tie.
The middle term is the important one. Ordering by serial number alone starves whoever starts furthest away: it loses every race it enters, systematically. Both teams found this independently — the lift queue from the building side, the fleet from the corridor side. See Fleet.
This makes a demand on clocks
claimedAtMs is only usable if the clocks agree. Today the claim time comes
from the gateway host, not from the chassis, and the hosts measured within
two seconds of each other. The moment claims arrive from a system with a
drifting clock, that drift becomes somebody else's starvation. An inbound
interface must therefore stamp the time of receipt or require a
synchronised source. The ±5-minute frame check does not help here: it is
replay protection, not a freshness test. See
Time and Time Source.