From 8321a99121c9690abcc664f3b7b5a39b0a93ac2c Mon Sep 17 00:00:00 2001 From: DeeJanuz <45082401+DeeJanuz@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:32:32 -0600 Subject: [PATCH] hand recorder: take.json keeps camera clock samples sets.bin's capture_ns is CLOCK_MONOTONIC_RAW; poses.jsonl and prompts.jsonl are CLOCK_MONOTONIC. On 2026-10-03 the two were 0.80 s apart during a session and 1.11 s apart five hours later, so images can't be paired with poses by capture_ns. take.json now samples RAW minus MONOTONIC as each recording part starts and stops (as ft-hands' raw_minus_mono_ns), so readers can put each exposure on the poses' clock. Co-Authored-By: Claude Opus 5.5 --- hands/rec/DESIGN.md | 5 +++-- hands/rec/session.py | 14 ++++++++++++++ hands/rec/tests/test_session.py | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/hands/rec/DESIGN.md b/hands/rec/DESIGN.md index aba2cda..740975e 100644 --- a/hands/rec/DESIGN.md +++ b/hands/rec/DESIGN.md @@ -57,11 +57,12 @@ Test hooks: poses.jsonl head and controller poses from ft-handpanel (below) take.json {"section", "title", "started_ns", "ended_ns", "status": "complete"|"stopped"|"skipped", "deleted": [[from_ns, to_ns], ...], "notes": "", - "parts": {"sets.bin": {"names_swapped": false}, "sets-2.bin": {...}}} + "parts": {"sets.bin": {"names_swapped": false}, "sets-2.bin": {...}}, + "clock": [[mono_ns, raw_minus_mono_ns], ...]} exports// what export writes (below) ``` -All `_ns` times are CLOCK_MONOTONIC nanoseconds, the clock of `dqbuf_ns` in sets.bin and of `capture_ns` in the hands file. sets.bin's `capture_ns` is the camera clock (CLOCK_MONOTONIC_RAW). +All `_ns` times are CLOCK_MONOTONIC nanoseconds, the clock of `dqbuf_ns` in sets.bin and of `capture_ns` in the hands file. sets.bin's `capture_ns` is the camera clock (CLOCK_MONOTONIC_RAW). The two drift apart with NTP's corrections: on 2026-10-03 RAW ran 0.80 s ahead, gaining about 10 ppm. take.json `"clock"` samples the difference (RAW minus MONOTONIC, as ft-hands' `raw_minus_mono_ns()`) when each part starts and stops, so a set's exposure time on the poses' clock is `capture_ns - raw_minus_mono_ns`, interpolated between samples. `dqbuf_ns` is on the right clock already, but a few ms after the exposure. Takes recorded before 2026-10-03 have no `"clock"`. ### profile.json diff --git a/hands/rec/session.py b/hands/rec/session.py index 4bc1811..33ab5d2 100755 --- a/hands/rec/session.py +++ b/hands/rec/session.py @@ -99,6 +99,18 @@ def mono_ns(): return time.clock_gettime_ns(time.CLOCK_MONOTONIC) +def clock_sample(): + """[CLOCK_MONOTONIC ns, CLOCK_MONOTONIC_RAW - CLOCK_MONOTONIC ns], as ft-hands' raw_minus_mono_ns(). + sets.bin's capture_ns is on the RAW clock and everything else on MONOTONIC; the two drift + apart with NTP's corrections (0.8 s apart and ~10 ppm on 2026-10-03), so take.json keeps + samples to turn capture_ns into MONOTONIC: capture_ns - offset, interpolated by time.""" + a = time.clock_gettime_ns(time.CLOCK_MONOTONIC) + r = time.clock_gettime_ns(time.CLOCK_MONOTONIC_RAW) + b = time.clock_gettime_ns(time.CLOCK_MONOTONIC) + mid = (a + b) // 2 + return [mid, r - mid] + + def run_dir(): return "/run/user/%d/frametop-hands" % os.getuid() @@ -2010,6 +2022,7 @@ class Session: def _start_recording(self): t = self._take t["part"] += 1 + t["json"].setdefault("clock", []).append(clock_sample()) if not self.dry_run: # a safety net only: the session ends the recording itself remaining = section_seconds(t["section"], worst=True, auto=self.auto) / self.speed @@ -2031,6 +2044,7 @@ class Session: self._log("recording part %d ended (%s)" % (rec.part, code)) if self._take: # how this part's side cameras are named (sides.py) self._take["json"].setdefault("parts", {})[rec.file] = {"names_swapped": rec.names_swapped} + self._take["json"].setdefault("clock", []).append(clock_sample()) write_json(os.path.join(self._take["dir"], "take.json"), self._take["json"]) def _end_take(self, status): diff --git a/hands/rec/tests/test_session.py b/hands/rec/tests/test_session.py index 82ffa72..037b33d 100644 --- a/hands/rec/tests/test_session.py +++ b/hands/rec/tests/test_session.py @@ -89,6 +89,12 @@ class SessionTest(SessionBase): self.assertGreater(ev[2]["t"] - ev[1]["t"], 0.25e9) with open(os.path.join(s.session_dir, "session.json")) as f: self.assertEqual(json.load(f)["mode"], "step") + # a clock sample as each part starts (2 steps: 2 parts), for capture_ns (RAW) -> MONOTONIC + with open(os.path.join(s.session_dir, "takes", "01-poses", "take.json")) as f: + clock = json.load(f)["clock"] + self.assertEqual(len(clock), 2) + self.assertTrue(all(len(c) == 2 and all(isinstance(v, int) for v in c) for c in clock)) + self.assertLess(clock[0][0], clock[1][0]) # the panel: Ready?, then the countdown, then the section's word for the hold, the diagram self.assertIn("panel: action " + session.READY_TEXT, self.panel) self.assertLess(self.panel.index("panel: big 3"), self.panel.index("panel: big 1"))