Skip to content

Telemetry recording and replay for IMU + plate-solve sessions#411

Open
mrosseel wants to merge 22 commits into
brickbots:mainfrom
mrosseel:telemetry
Open

Telemetry recording and replay for IMU + plate-solve sessions#411
mrosseel wants to merge 22 commits into
brickbots:mainfrom
mrosseel:telemetry

Conversation

@mrosseel

@mrosseel mrosseel commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add telemetry recording: IMU samples (quat + raw gyro/accel), plate solves with predicted-vs-solved drift data, and target changes, written as compact JSONL sessions to ~/PiFinder_data/telemetry/; optional per-solve frame capture via a new camera save_image: command. Location is kept in a separate sidecar file so sessions can be shared without leaking it.
  • Add replay: sessions are converted back into SolveResult/ImuSample messages and fed through the integrator's normal apply/advance paths with original timing; the session's clock and location are force-applied for faithful Alt/Az and restored when replay ends. Start/stop from the new Telemetry menu (Record, Images, Load).
  • Robustness: corrupt or truncated sessions are skipped line-wise instead of killing the integrator; IMU is captured from session start (also pre-first-solve), deduped on sample epoch; buffer overflow is logged; header datetime/location late-bind once GPS lock arrives.
  • Tests: test_telemetry.py (record/replay round-trip equivalence) and test_integrator_drift.py.

Test plan

  • nox -s unit_tests passes (includes new test_telemetry.py and test_integrator_drift.py)
  • nox -s lint passes
  • nox -s type_hints passes
  • Record a telemetry session on real hardware and verify replay produces matching pointing output

🤖 Generated with Claude Code

mrosseel and others added 12 commits March 5, 2026 16:29
Replays synthetic telemetry through real ImuDeadReckoning and
integrator wrapper functions, measuring dead-reckoning error vs
ground truth. Three tests: stationary drift, slew tracking, and
solve correction reset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Telemetry recorder captures solve and IMU events with timestamps
- Menu entries for telemetry start/stop/replay
- Integrator wired to record solves and IMU readings
- UI list screen for telemetry sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split integrator.py into three focused modules:
- pointing.py: coordinate math (IMU dead-reckoning, plate-solve
  integration, roll/constellation/altaz finalization)
- telemetry.py: TelemetryManager facade (recording, replay,
  command dispatch, image saving, replay event handling)
- integrator.py: main loop and queue plumbing only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 55 unit tests covering TelemetryRecorder, TelemetryPlayer,
  TelemetryManager, and pointing.py functions
- Fix test_integrator_drift.py import from PiFinder.integrator
  to PiFinder.pointing after refactor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…and analysis tools

- Record raw gyro and accelerometer readings in IMU events
- Record target changes (name, RA/Dec, Alt/Az) for time-to-target analysis
- Fix replay: solve_time, solve_source, imu_pos, failed solves, mount_type header
- Reset integrator state after replay ends
- Move location to separate .location sidecar file for privacy
- Reduce file size with stationary IMU decimation (10x) and float rounding
- Add telemetry analysis scripts: session visualizer, drift analysis,
  truss flex analysis, IMU free-run drift visualization
- Include sample recording (session_20260309.jsonl, location scrubbed)
- 67 unit tests covering all new functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Single conflict in python/PiFinder/integrator.py: telemetry already
refactored the helper functions (update_plate_solve_and_imu, update_imu,
set_cam2scope_alignment, get_roll_by_mount_type) into pointing.py.

Kept telemetry's refactor; dropped the duplicate helpers that upstream
modified in place. Upstream-only additions (get_alt_az, get_constellation,
pointing_updated flag) and the imu_time tweak in update_imu are NOT
incorporated here — apply them to pointing.py as a follow-up if wanted.
Upstream's astro_coords refactor (brickbots#420) moved RaDecRoll from
pointing_model/astro_coords.py to types/coordinates.py and reworked the
API: constructor now takes (ra, dec, roll, deg=...) and replaces
set_from_deg/get_deg with set(...) and get(deg=True).

Resolution:
- integrator.py: kept HEAD; dropped duplicate helpers (now in pointing.py)
- pointing.py: updated import path and ported all RaDecRoll calls to the
  new API (set_from_deg/get_deg → constructor / get(deg=True))
- integrator_classic.py: accepted upstream deletion (brickbots#421 dropped classic
  integrator); telemetry's only change was a trivial **kwargs signature
The clear-on-failure block in solver() reset solved["RA"]/Dec/Matches
but left camera_center and camera_solve pinned to the last successful
solve across every failure path.

Integrator-side downstream is unaffected (integrator.py:96 only merges
new position data when RA is not None), but stale camera_center leaks
into anything reading 'solved' from the solver's perspective. Completes
the existing 'otherwise old values persist' clearing block.
mrosseel added 2 commits May 22, 2026 03:42
solver.py imports tetra3 at module scope, which transitively pulled the
submodule into any caller of get_initialized_solved_dict — including
integrator.py and the integration-drift tests. CI's checkout step does
not init submodules, so tests/test_integrator_drift.py failed to collect:

  PiFinder/solver.py:28: ModuleNotFoundError: No module named 'tetra3'

Move the pure-data dict factory to a tiny new module PiFinder/solved.py.
Update solver.py, integrator.py, and test_integrator_drift.py to import
from there. No runtime behavior change.

@TakKanekoGit TakKanekoGit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! I just reviewed the part I'm familiar with (imu_pi.py). I use telemetry a lot so it'll be great to have it in main.

Comment thread python/PiFinder/imu_pi.py Outdated
Comment thread python/PiFinder/imu_pi.py Outdated
mrosseel and others added 3 commits June 1, 2026 15:42
Port telemetry record/replay onto the refactored Solver/Integrator flow
(upstream brickbots#429): SolveResult DTOs on solver_queue, integrator-owned
PointingEstimate, ImuSample on shared_state.

- integrator.py: take upstream's dataclass integrator, re-add telemetry
  hooks. Replay now converts recorded events back into SolveResult /
  ImuSample messages and feeds them through the same _apply_successful_solve
  / _apply_failed_solve / _advance_with_imu paths as live data (the old
  branch duplicated integrator logic inside TelemetryManager).
- telemetry.py: recorder consumes SolveResult/ImuSample; player produces
  them. JSONL field names unchanged, so existing recordings stay replayable.
- types/positioning.py: ImuSample gains gyro/accel fields for raw-sensor
  telemetry; imu_pi.py populates them.
- Drop pointing.py and solved.py: superseded by types/positioning.py and
  the upstream integrator (incl. the legacy roll-by-mount-type logic that
  upstream replaced with chart-side handling).
- solver.py: take upstream as-is (get_initialized_solved_dict is gone).
- Tests ported to the new types; drift tests now exercise the real
  integrator apply/advance functions.
…corrupt sessions

Port of the fix lost in the worktree resync (was 10e8c0b0), adapted to
the dataclass-based telemetry, plus replay-load hardening:

- Replay's set_datetime call was rejected when the recording predates
  the current clock (set_datetime only moves forward). Pass force=True
  so past sessions take effect; this also blocks GPS time updates from
  clobbering the replayed clock mid-replay.
- Add a "Stop replay" entry to the telemetry list - replay_stop was
  wired in the manager but unreachable from the UI, so a long session
  locked the device into replay until it finished.
- A corrupt or missing session no longer kills the integrator process:
  TelemetryPlayer._load skips unparseable/timestamp-less lines (the
  expected artifact of a power cut mid-recording), the replay command
  catches OSError and restarts the camera the UI just stopped, and
  malformed events are skipped during replay instead of raising.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…giene

Replay no longer hijacks device state permanently:
- Save the pre-replay location and restore it when replay ends (both
  the stop command and natural finish, consolidated in _end_replay);
  reset_datetime() clears the forced replay clock so GPS time resumes.
- Protect source="replay" from GPS fix overwrites in main.py, matching
  WEB/CONFIG/MANUAL - a mid-replay GPS lock clobbered the replayed
  location.
- Shift a header's dt back by (hdr.t - first event t) on apply, so
  late-written headers set the clock to the start of the event stream.

IMU capture now covers the whole session:
- The integrator records IMU samples before the dead-reckoning anchor
  gate, so sessions where the solver never solves (the most
  diagnosis-worthy case) still capture IMU data.
- Records are stamped with the sample epoch (imu.timestamp), not the
  integrator's poll time, and deduped on it - the loop polls faster
  than the IMU updates, so the same sample was recorded repeatedly.

Recorder hygiene:
- Late-bind datetime/location into the session once GPS lock arrives
  after recording started (second hdr record + .location sidecar).
- _poll_target checks target-changed before computing Alt/Az; it was
  running a skyfield transform every integrator loop while recording.
- start() resets per-session dedup state (a second session never
  recorded its initial target).
- Buffer overflow drops are counted and logged instead of silent.
- _do_flush is locked: the background flush thread and the loop's
  time-gated flush could interleave writes and scramble event order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mrosseel mrosseel changed the title Telemetry recording and replay with pointing math refactor Telemetry recording and replay for IMU + plate-solve sessions Jun 9, 2026
mrosseel and others added 2 commits June 9, 2026 22:45
…uaternion

Address review feedback on brickbots#411: the raw-sensor reads for telemetry
lived in the imu_monitor loop, which spins much faster than update()'s
sample-rate gate - two extra I2C transactions per spin, paired with a
quaternion sampled at a different instant, reaching through imu.sensor
in a way that only worked on the fake IMU because of a bare
except-pass.

Now update() reads gyro/accel in the same gated pass as the quaternion
and stamps last_read_time; the monitor just copies imu.gyro/imu.accel
onto the published sample and takes the read epoch as the sample
timestamp. The per-read timestamp also keeps the telemetry recorder's
dedup-by-sample-epoch working while stationary (previously the
timestamp only changed on movement, so stationary heartbeat samples -
gyro/accel noise floor - were never recorded).

Drive-by: remove the stray `imu = Imu()` after the ImuFake fallback,
which re-raised on missing hardware and made the fallback unreachable;
give ImuFake the attributes the monitor reads so the degraded-ops path
actually runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_imu

Address review feedback on brickbots#411: raw-sensor reads add two I2C
transactions per sample on a bus the BNO055 is sensitive about, and
analysis of recorded gyro data showed it wasn't very usable. Default
to quaternion-only sampling; set telemetry_raw_imu=true in config to
record gyro/accel (the recorder already serializes absent values as
null).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants