Skip to content

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.