LEARNED IT · 09/26

Bounded by construction: why an offset goto is safe and a rate command is not

An offset goto carries its own stopping condition — a step count the motor controller counts down to zero. A slew-rate command carries none: it runs until something else stops it. That difference, not the size of the number, is what makes automated mount motion safe to leave running. A step cap protects the first kind and cannot protect the second.

Two ways to make this mount move, and they are not the same kind of thing.

An offset goto is told a distance: stop the axis, set low-speed goto mode and direction, load a 24-bit step count, set the ramp, go. The motor controller counts those steps down and stops itself. The stopping condition travels with the command.

A slew-rate command is told a speed: slew mode, rate, go. It carries no endpoint at all. It runs until something else intervenes — another command, a limit, or me.

Everything about how safe automated motion feels comes from that distinction, and it took two runaways to see it clearly.

The runaways

10 September. Connecting the INDI Skywatcher driver with no site or time set started an uncommanded high-speed azimuth slew of about 75 degrees. Nothing in the software knew where it was meant to stop, because nothing had told it a distance.

17 September. A coordinate bug — get_body("moon", …).transform_to("icrs") returning a position with distance, so the transform re-origined geocentre to barycentre and swung the direction wildly — put the Moon at RA 23.62h Dec −2.46 instead of 16.4h −26.8. Syncing to that and enabling tracking started a 2 degree slew.

Both were continuous motion against a poisoned model. Neither would have happened through an offset goto, because an offset goto cannot travel further than the number of steps it was handed.

The cap that does not protect you

MAX_STEP refuses any single commanded move over 0.40 degrees, and it feels like the safety net. It is not, or rather it is only half of one.

A step cap constrains the command, not the motion. With an offset goto those are the same thing — the axis travels exactly the steps it was given, so capping the steps caps the travel. With a rate command they are unrelated: the rate can be tiny and the motion unbounded, because nothing is counting down.

Worse, a poisoned pointing model turns a small command into a large motion, which is precisely the 17 September failure. That is why the model is validated against raw axis counts before anything is allowed to move — a cap on the number you send is no defence against a wrong frame of reference.

What actually makes it safe

  1. Bounded commands. Every move is a step count that stops itself.
  2. Readback after every move. The encoders are read and the run aborts if a panel landed more than 36″ from target. In practice, ±1″.
  3. An independent stop channel. Raw :K1\r / :K2\r over UDP, which keeps working while INDI holds the connection and froze both axes instantly when tested. The module is not single-client, so this cannot be blocked by whatever else is talking to the mount. That one fact is what turned 10 September from frightening into manageable.
  4. A human in the foreground. The harness refused to let me arm a watcher that would start a run unattended. It was right.

The catch

:K also stops the firmware's tracking, so every goto leaves the axis stopped, and the panels are therefore exposed untracked — which is the whole of [[seventeen-milliseconds-per-pixel]]. The obvious fix is to restart tracking after each goto: slew mode, rate, start.

That fix is a rate command. It reintroduces exactly the unbounded motion this page is about. It is still the right thing to do — the drift problem does not go away on its own — but it belongs in daylight with the mount indoors and a hand on the stop, not at 21:00 on the one clear night. Write the unbounded thing when you can watch it.

The general shape of it

Ask of any command you are about to automate: what stops this, and is the answer part of the command itself? If the stopping condition lives somewhere else — in a timeout, in a watchdog, in you — then the command is not bounded, and no limit on its input will make it so.