Skip to content
C.W.K.
Lesson 03 of 05 · published

Close Parks First

~16 min · ordering, gimbal, verification, uvc, honest-status

Level 0Closed Lens
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

The Order Matters

Two of Oriel's cameras are on motorized gimbals. When Pippa looks around, she may turn one toward something. Then the look ends and the gate closes. Which happens first: the camera turns back to rest, or the gate closes? It sounds like a detail. It is the difference between a camera that ends every session looking at the floor and one that sits there pointed at the room, with its lens open, until someone notices.

The first version closed the gate and then tried to park. But once the gate is closed, nothing may move the camera, including a park. Dad saw the consequence and stated the order in one sentence on 2026-11-03: "카메라를 다 조작을 한 다음에 게이트를 닫아야 되는데", finish operating every camera, then close the gate. Since then every close (Dad's, an expiry, the end of a session) runs one at a time and in his order.

The Close Order

  1. Refuse new work. The closing inputs enter a "closing" state at once: every new look, move and read on them is refused with "being parked away from the room before the gate closes".
  2. Stop what is no longer covered. A recording that only the closing approval was covering stops. A recording the eyes mode still covers keeps going.
  3. Park while the approval is still open. Each connected look-around camera that is not known to be at rest turns to its rest pose: its saved rest bookmark if it has one, otherwise its own front with its head all the way down. One camera after another, each tried once more if it does not check out.
  4. Only then close the approvals.

"Not known to be at rest" is a precise set: a camera opened through the gate, moved, or whose last park failed, since its last checked park. A camera already head-down and closed is left alone, a rule Dad added the next day ("어떤 카메라든 고개 숙이고 닫혀 있으면 된 거야", any camera that is head-down and closed is fine). It matters more than it looks: opening one webcam's stream just to park it again lifts its head as the stream starts.

A Park Is Checked, Not Assumed

A gimbal has a read-back: you can ask it what pose it is in. Oriel does not trust it alone, for a measured reason. On the pocket camera, a pan sent while no video stream was open read back as set, and the next frame showed the camera had not moved at all. The gimbal only answers while the camera streams. So a move runs inside a stream (the input's recording if one is running, else a stream opened just for the move with a short warm-up), and it is confirmed two ways:

  • By the pose it reads back, within six degrees (one camera reads −85 at its −90 rest), read again if the first read lags.
  • By frames. The recorder keeps a frame before the move and one after it settles. The engine lines them up on a small grey copy (bars cut first) and measures the shift: a larger pan slides the picture left, a larger tilt slides it down, both measured on the real cameras. When the move is wider than the frames' overlap, the whole picture changing counts. A move that should have happened and shows no shift fails the park.

The park's frames are deleted once measured. Nothing about the room is kept from a park, and a park returns numbers only, never a picture.

Never Say "Parked" for a Camera That Was Only Asked

The answer to close_eyes, the page's gate bar and the activity log (park_failed) name every camera that did not park. The word "parked" is never used for a camera that was only asked to park. And because a camera can still end up facing the room (a failed park, a power cut mid-move), the park is the one move allowed while the gate is closed: park_cameras and the page's "Turn cameras away" turn cameras away with the gate shut, open nothing and return no picture. Recovery must not require loosening the very gate whose failure you are recovering from.

Code

Dad's order for every close (trimmed excerpt, backend/engine.py Engine._close_in_order)·python
def _close_in_order(self, closing, close, *, actor, reason, input_ids=None, every=True):
    """Dad's order for every close (2026-11-03, todo #830): the closing inputs
    take no new look or move; a recording they no longer cover stops; every
    movable camera parks and is checked while its approval is still open;
    then the approvals close."""
    with self._closing:
        closing_ids = {i for a in closing for i in a["inputs"]}
        ...  # which cameras stay covered by another approval or the mode
        pool = [s.id for s in self.pool_sources() if s.kind == "uvc"]
        unsettled = self.unsettled(pool)        # opened, moved, or a failed park
        targets = [i for i in pool if i not in staying and (i in closing_ids or i in unsettled)]
        with self.gate.closing(sorted((closing_ids | set(targets)) - staying)):
            self._stop_uncovered(staying, reason=f"the eyes are closing ({reason})")
            parks = list(self.parker(targets) or []) if self.parker is not None and targets else []
            closed = close()                    # only now do the approvals close
        self.last_close = {"parks": parks,
                           "failed": [p for p in parks if not p.get("parked") and not p.get("absent")]}
        return closed, parks
Confirm a move by the picture, not the read-back: measure the shift between two frames (runnable; needs Pillow and numpy)·python
import numpy as np
from PIL import Image


def grey(path, width=96):
    image = Image.open(path).convert("L")
    height = round(image.height * width / image.width)
    return np.asarray(image.resize((width, height)), dtype=float)


def shift(before, after, search=20):
    """The horizontal and vertical shift (in grey pixels) that best lines up two frames."""
    best = (None, 0, 0)
    h, w = before.shape
    for dy in range(-search, search + 1):
        for dx in range(-search, search + 1):
            a = before[max(0, dy):h + min(0, dy), max(0, dx):w + min(0, dx)]
            b = after[max(0, -dy):h + min(0, -dy), max(0, -dx):w + min(0, -dx)]
            err = np.mean(np.abs(a - b))
            # ties go to the smaller shift, so two identical frames read as (0, 0)
            if best[0] is None or (err, abs(dx) + abs(dy)) < (best[0], abs(best[1]) + abs(best[2])):
                best = (err, dx, dy)
    return best


err, dx, dy = shift(grey("before.jpg"), grey("after.jpg"))
print(f"picture moved dx={dx} dy={dy} (error {err:.1f})")
print("moved" if abs(dx) + abs(dy) >= 2 else "did NOT move: the park is not confirmed")

External links

Exercise

Take two photos with your phone or webcam a moment apart, panning slightly between them, and run the shift script. Then take two without moving. Use the results to pick a threshold for "moved". Finally, write the close order for a pretend gimbal camera as five calls in the right order, and a test that fails if close_approvals() is ever called before park() has been checked.
Hint
A recording mock that logs each call's name and checks the order is enough: assert the log ends with ['refuse_new', 'stop_uncovered', 'park', 'check_park', 'close_approvals']. The point of the test is the order, not the motor.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.