Skip to content

NixOS support#379

Open
mrosseel wants to merge 396 commits into
brickbots:mainfrom
mrosseel:nixos
Open

NixOS support#379
mrosseel wants to merge 396 commits into
brickbots:mainfrom
mrosseel:nixos

Conversation

@mrosseel

Copy link
Copy Markdown
Collaborator

Summary

  • Full NixOS-based system for PiFinder (replaces Raspbian)
  • Declarative system configuration via Nix flake
  • SD card image, netboot, and migration bootstrap tarball builds
  • Software update via nixos-rebuild with GitHub release/PR channels

Test plan

  • Flash SD image and verify boot
  • Test WiFi AP and client mode switching
  • Test software update UI channels
  • Test hostname rename via web UI

🤖 Generated with Claude Code

@mrosseel mrosseel added the testable Ready for testing via PiFinder software update label Feb 12, 2026
@mrosseel mrosseel added testable Ready for testing via PiFinder software update and removed testable Ready for testing via PiFinder software update labels Feb 14, 2026
Merges upstream PiFinder changes including:
- Bottle → Flask/Jinja webserver migration (.tpl → .html)
- Harris and Lynga catalog loaders + data + DB update
- Selenium-based webserver test suite

NixOS-specific decisions:
- Keep deletion of requirements.txt, requirements_dev.txt, noxfile.py,
  version.txt (managed via flake.nix / pifinder-build.json)
- Keep Python 3.13 target (was reset to py39 by upstream)
- Keep stub sys_utils_fake (uses sys_utils_base)
- Replace bottle/cheroot with flask/flask-babel/waitress in
  nixos/pkgs/python-packages.nix; add selenium to devPackages
- Reimplement Bottle-era network status message in Jinja syntax
Includes nixos/pkgs/pifinder-src.nix fix: drop fetchurl for hip_main.dat,
which upstream now ships as a committed file in astro_data/.
github-actions Bot and others added 3 commits May 25, 2026 08:44
- nixos/RELEASE.md: document version flow + release/dev pipelines
- software.py: MIN_NIXOS_VERSION 2.5.0 → 3.0.0
- python-packages.nix: add pyerfa (used by calc_utils since upstream brickbots#423,
  silently dropped during upstream merge because requirements.txt is not
  mirrored into the Nix env)
- python-packages.nix: include hardwarePackages in devEnv so nix develop
  matches the runtime import surface
- python-packages.nix: select simplejpeg wheel by host arch (was hard-pinned
  to aarch64; failed to import on x86_64 dev shells)
- flake.nix: apply libcamera -Dpycamera=enabled overlay to the x86_64
  devShell and export PYTHONPATH so picamera2 finds the python bindings

Verified: nix develop --command python -c 'import …' on x86_64
succeeds for all 34 imports (erfa, picamera2, libcamera, PyHotKey,
pynput, hardware packages, etc.). RPi.GPIO still raises its own
"only on a Raspberry Pi" RuntimeError at import time — expected,
matches upstream pip behavior on non-Pi hardware.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. gpsd-add-uart: rename /dev/ttyAMA3 → /dev/ttyAMA1 (6 sites). The uart3
   overlay surfaces as ttyAMA1, matching hardware.nix's udev rule and the
   Debian image's gpsd.conf.

2. /etc/default/gpsd: drop the custom USBAUTO+GPSD_SOCKET pair, write
   upstream pi_config_files/gpsd.conf's three lines verbatim. DEVICES now
   opens the on-board UART at startup. gpsd-add-uart kept as the boot-time
   socket-activation kick; can retire after on-Pi confirmation.

3. pifinder-upgrade: replace fragile `nix build --dry-run | grep` progress
   with `nix --log-format internal-json build … --max-jobs 0` parsed by
   gawk, counting type=100 (actCopyPath) start/stop events. Stable across
   Nix ≥ 2.4. Validated against a real cache.nixos.org substitute (5/5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot and others added 6 commits May 27, 2026 14:54
build.yml was defaulting VERSION=2.5.0 on push triggers (and the
workflow_dispatch default also read 2.5.0), so this branch's auto-build
was publishing v2.5.0-migration tarballs while the migration branch's
downloader (software.py _MIGRATION_VERSION_INFO and the brickbots/PiFinder
release branch's migration_gate.json) points at v3.0.0-migration. Bump
both the workflow_dispatch default and the push-trigger fallback to 3.0.0
so a normal push to nixos publishes the artifact at the URL the migration
branch actually downloads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Lint & Test workflow was using DeterminateSystems/magic-nix-cache-action,
which is backed by GitHub's Actions Cache and gets HTTP-418 rate-limited
under sustained traffic — exactly the failure mode that just broke
type-check ("--install-types failed: substituter disabled, rate limit
exceeded"). The Nix substituter is then disabled mid-run and dependent
commands like mypy --install-types fall over.

Replace it with cachix/cachix-action@v17 pointed at the pifinder cache
(read-only, no auth token needed). Same backing as build.yml, so dev-shell
substitutes hit the same store paths the system closure was built against.
cache.nixos.org remains the default fallback.

Also bump actions/checkout@v4 → @v6 in this file to align with the Node 24
migration in build.yml/release.yml.

This is a stop-gap. The real fix is standing up Attic with an S3 backend
so both build.yml and lint.yml can retire cachix.org and MNC together —
tracked separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rong)

Previous commit on this branch swapped DeterminateSystems/magic-nix-cache-action
for cachix/cachix-action@v17 thinking the MNC HTTP-418 rate-limit was the
root cause of the failed lint/type-check. That swap made things worse:
the pifinder Cachix only contains the NixOS *system closure*, not the
*dev shell* (cedar-detect-server's Rust crate builds). With MNC removed,
the dev shell had to rebuild from source, which fetched crate tarballs
from a crates.io mirror and hit 403s.

MNC was carrying real weight by caching locally-built derivations
between runs. Restoring it. The original MNC rate-limit was a transient
flake — re-runs work around it. Real fix is standing up Attic with
S3-backed storage so both build.yml and lint.yml can retire MNC and
cachix.org together.

The checkout@v4 → @v6 bump from the swap commit is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot and others added 5 commits May 27, 2026 16:04
The Nix derivation was overwriting pifinder-build.json with
"nix-${gitRev}" at build time, so even released devices reported a
random short-sha instead of the release version. Three writers became
two, with consistent semantics everywhere:

- pifinder-src.nix: drop the cat > pifinder-build.json block and the
  gitRev arg — the derivation now copies the source file through
  verbatim, no version invention.
- flake.nix: drop the pifinderGitRev _module.args plumbing.
- services.nix: drop pifinderGitRev / gitRev from the pifinder-src
  import.
- release.yml: reorder so the version stamp is written into the
  working tree BEFORE the nix build (so the store path bakes in the
  release version, not the previous stamp), then re-stamp with the
  resulting store_path after the build, commit, push, tag.

Result: SD image, cachix closure, and committed JSON all agree on
the released version. Matches the flow already documented in
nixos/RELEASE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the decision to self-host Attic at cache.pifinder.eu, backed by
SQLite + local disk initially with Cloudflare R2 as the eventual chunk
store. Covers considered alternatives (cachix.org, Magic Nix Cache,
nix-casync, harmonia) and the operational consequences for CI publishing,
on-device updates, and failure fall-through to cache.nixos.org.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces pifinder.cachix.org with the self-hosted Attic instance at
https://cache.pifinder.eu/pifinder (ADR 0004) on both the device-side
substituter list and the CI publish path. cachix.org is removed
entirely with no fallback, so this is the first build that proves
Attic stands on its own.

services.nix:
  substituters       = ["https://cache.pifinder.eu/pifinder"
                        "https://cache.nixos.org"]
  trusted-public-keys = ["pifinder:8UU/O3oLkaJHHUyqEcPGl+9F1m4MqDca39Ewl49jBmE="
                         "cache.nixos.org-1:..."]
  (pifinder.cachix.org and its key removed.)

build.yml — build-native, build-emulated, build-migration-tarball:
  - remove cachix/cachix-action steps
  - remove `cachix push` (replaced by `attic push pifinder:pifinder`)
  - add a "Setup Attic substituter" step that runs
      nix profile install nixpkgs#attic-client
      attic login pifinder https://cache.pifinder.eu \"\$ATTIC_TOKEN\"
      attic use pifinder:pifinder
    before the build, so the build itself substitutes from
    cache.pifinder.eu.
  build-emulated swaps cachix/install-nix-action for
  DeterminateSystems/nix-installer-action — no cachix dependencies left.

First post-cachix build is expected to be slow: pifinder.cachix.org's
warmed-up paths are gone. Once it completes and `attic push` lands,
subsequent builds substitute from cache.pifinder.eu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mrosseel added 10 commits July 16, 2026 18:14
…ection; centroid-excluded annuli

Three defects found live on-sky (mr2, 2026-07-17), each verified against the
raw frames before and after:

1. ROTATION (the 3-magnitude bug): the camera process rotates the solve/
   display image (90 or 270 deg CCW depending on screen_direction /
   camera_rotation) but stores the raw frame unrotated. The raw-photometry
   SQM path mapped solve-image centroids straight onto the raw, so every
   star aperture landed on the wrong sky and SQM read ~15-16 under an ~18
   sky on every device with a rotated display (which is all of them).
   The camera now publishes its rotation (SharedState.solve_image_rotation)
   and update_sqm counter-rotates matched + detected centroids onto the raw
   grid (_derotate_centroids, validated against PIL rotation for 0/90/180/
   270 and arbitrary angles). Measured live: matched-star flux recovered
   ~4x; SQM 15.4 -> 17.6 agreeing with the offline pipeline to 0.05.

2. Wing correction was an artifact: the stacked curve of growth of 21
   bright live stars shows the imx462 PSF fully enclosed by r~4 (FWHM
   2.6px) -- no wings. The per-star boundary search integrated noise and
   reported f~0.72-0.75, a spurious +0.3-0.5 mag correction whose variance
   polluted the calibration. WingEstimator now median-stacks
   aperture-normalized bright stars per frame and reads the enclosed
   fraction off the stack's growth-curve plateau: wingless optics measure
   f=1.0 (correction 0); hq's mild focus-dependent wings (f 0.87-1.0)
   are measured honestly. min_wing_radius and
   CameraProfile.wing_min_radius_px are gone.

3. Background annuli in dense fields: every *detected* centroid (cedar
   finds them all; the parameter was previously unused) is now masked out
   of each star's background annulus, with a 3-sigma clip as backstop for
   undetected sources. A neighbour star can no longer inflate the local
   sky estimate.

Band offsets recalibrated against all referenced clear-night sweeps with
this pipeline: imx462/imx290 0.64 (residuals +/-0.06 over 6 sweeps), hq
0.63 (3 independent references). Cloudy-night sweeps excluded and
documented: star-calibrated SQM under bright clouds reads ~0.7-0.9 low vs
a radiometric meter because the zero point is measured through cloud
extinction while the glow largely is not.

Analysis artifacts: myPiFinder/support/dumps/analysis/20260716.
The mono sweep (2025-10-31) rerun through the final pipeline (growth-curve
aperture correction: f=0.985, wingless like its siblings; centroid-excluded
annuli) computes 17.45 against the recorded 17.8-17.9 hand-held reference.
Refit: -0.46 -> -0.06. The +0.40 shift matches the phantom-wing retirement
on imx462/hq (+0.33/+0.31), and a near-zero offset is physically consistent
with the Pregius mono passband being the closest of the three sensors to the
meter's. Single moonlit reference night: confidence +/-0.2; re-anchor with
Session Correct or a reference sweep when this camera flies again.
…mp logging

The titlebar SQM runs under PID auto-exposure (10 ms - 1 s), and the sweep
ramps showed the frame zero point drifting ~-0.2 mag/decade of exposure.
Per-star tracking across the ramps (860 star tracks) exonerates the sensor:
response is linear to ±1% below 0.2 full-scale, no compression. The drift is
population composition: matched-star sets slide ~4 magnitudes across the
exposure range, and per-star zero points carry a mild magnitude dependence,
so every exposure-dependent selection (all-star median, top-K, brighter
half) inherits drift.

Three changes:

- mzero is now the median over stars in a fixed catalog-magnitude band
  (3.5-6.5, fallback to all stars below 5 in band): the same physical stars
  vote at every exposure. Measured drift on the imx462 ramps:
  -0.10 -> -0.01 mag/decade. hq keeps a ~-0.15 residual consistent with its
  real focus-dependent wings (tracked per session by the growth estimator).

- Background annulus moved 6-14 -> 10-18, outside the PSF-wing zone that
  inflated bright stars' local sky at long exposure (~35% of the drift);
  safe against neighbours now that all detected centroids are masked out
  of annuli.

- Sensor die temperature (picamera2 SensorTemperature) recorded into
  last_image_metadata and sweep metadata, diagnostic only: the residual
  black level wanders ±2 ADU night-to-night and temperature is the prime
  suspect; normal sweeps now collect the correlation data.

Band offsets refit against all referenced clear-night sweeps with this
pipeline (they absorb the annulus/selection level shifts):
imx462/imx290 0.61, hq 0.60, imx296 -0.10. Clear-night residuals:
imx462 ±0.08 (6 refs), hq -0.16..+0.18 (3 refs, the shared 2025-11-16
reading remains the outlier). Cloud bias unchanged (-0.4..-0.6, physics).
mrosseel and others added 19 commits July 17, 2026 23:44
…tion

Publish SQM as a fixed-calibration radiometer (factory zero point x measured
sky flux) at 1 Hz from every camera frame, no plate solve required. Stellar
photometry becomes a 10-second transmission diagnostic that establishes,
maintains and audits the calibration:

- sqm/radiometer.py: camera-side sparse-central-median sample; 12-sample/15 s
  accumulator; factory radiometric zero point + FOV per sensor profile.
- sqm/clouds.py: cloud/dew discrimination. Cloud dims stars AND brightens a
  light-polluted sky (radiometer immune, no correction); dew/optics dim stars
  and sky together (stellar deficit compensates the radiometer once a clear
  session baseline exists). Asymmetric median baselines, factory-seeded.
- sqm/black_level.py: sensor pedestal tracked from the sky-vs-exposure
  intercept of ordinary frames (the true black level wanders +/-2 ADU night
  to night); fed at 1 Hz, gated on transmission stability, zero-touch only.
- sqm/gaia_ref.py + astro_data/hip_gaia_g.npz: Gaia G reference band with
  BP-RP trim for bare sensors (24-29% less star scatter than Hipparcos V).
- Robust stellar zero point: fixed catalog-magnitude band + 3-MAD rejection;
  centroid-excluded background annuli; growth-curve aperture correction.
- Calibration wizard rework: temporal read noise, multi-exposure dark fit,
  exact frame/solve pairing, typed reload command, transient exposures.

Archive replay (16 sweeps, 800 frames): imx462 MAE 0.05 mag over six
SQM-L-referenced clear nights; bright-cloud errors 0.35-0.84 collapse to
under 0.10 with zero corrections on clear skies; 800/800 frames published
including 318 without usable stars.
Each sweep image pair now gets img_NNN_XXms_metadata.json carrying the
driver-applied exposure/gain, sensor die temperature, sensor black levels,
Lux, colour gains, and raw-frame ADU statistics (mean/median/std/percentiles,
saturated fraction). frame_metadata.json aggregates the same records.

Also gate the sweep UI COMPLETE transition on sweep_metadata.json existing:
the camera process writes it a few seconds after the last image, and
enriching before that silently skipped the SQM/solve/noise-floor details
on every sweep.
Each tracker (BlackLevelTracker, CloudEstimator, WingEstimator,
RadiometerAccumulator) gains dump(): config, derived stats, and the full
window sample contents. The solver publishes these into sqm_details as
window_black_level / window_clouds / window_wings / window_radiometer on
every update, and each sweep image JSON snapshots the whole sqm_details,
so a sweep records the pipeline state frame by frame.
…ot the old base

activate_system compared the persisted camera-type against --default-camera,
i.e. the RUNNING generation base camera. On a device whose build had that
camera as its base (mr: imx477-base build), upgrading onto an imx462-base
build concluded camera is the base, nothing to do and rebooted into the
wrong DTB, killing the camera. Ask the new store path for a matching
specialisation instead, and run the NEW build set-extlinux-default so the
camera-to-entry mapping matches the entries just written.
…erization

Plan B for the stuck-pedestal incident (2026-07-17):
- BlackLevelTracker: an accepted pedestal is now a lease (max_age_seconds,
  default 900 s) instead of a latch — without a fresh accepting refit it
  expires and callers fall back to the profile constant, the same state
  every session boots in. Regression test replays the incident.
- max_intercept_stderr tightened 1.0 -> 0.6 ADU: honest ramp fits on the
  27-sweep archive run 0.10-0.69; the cloud-poisoned fit passed at 0.97.

Calibration wizard (dark-frame path the imx296 tester will run):
- persists a per-frame report JSON (median/MAD/percentiles, requested AND
  driver-reported exposure, sensor temp) beside the fitted scalars — the
  ramp shape is where short-exposure pedestal elevation shows, and three
  fitted numbers cannot preserve it
- dark-current fit uses driver-reported exposures (the imx477 delivers
  intermittent half-exposure frames; a requested-exposure fit is silently
  corrupted)
- dark ladder extended from one decade to near sensor minimum, covering
  the elevation zone (imx462: +2.9 ADU at 25 ms, 0 at ~300 ms)

Sweep instrumentation:
- per-image records now carry the full driver metadata dict (new sensors
  report different keys) and the live camera-side radiometer sample
  (the sqm_details snapshot freezes while the solver starves mid-sweep)
- camera_pi keeps last_frame_metadata; both image_metadata sites publish
  actual_exposure_us
…mat stragglers

mypy: the B-V cache globals are Optional, so assignments cannot narrow
them across statements; parse into locals and use those for savez/len.
format: 4 files the CI format session was silently reformatting on every
run (timeentry, three tests).
Telemetry captured only IMU and solve streams — blind to the radiometric
side. A third event type records the camera-side radiometer sample at
~1 Hz: t (capture epoch, s), exp (driver-reported exposure, s), bg
(background median, ADU), mad (median absolute deviation, ADU), grad
(quadrant gradient, ADU), seq (frame sequence). Dedupes on sequence;
replay players skip unknown event types, so old builds read new files.

A full-night radio stream answers what sweep snapshots cannot: how
auto-exposure actually behaves over a session (lever-arm availability
for the black-level fit), a continuous sky-steadiness timeline, and
real-night input for replaying pedestal estimators.
The marking menu down slot is the one readable at the eyepiece without
moving your head — it goes to SWEEP, the everyday reference-meter action.

SQM Correct is deleted rather than kept: a magnitude-additive session
knob silently absorbs ADU-space, brightness-dependent errors (tonight:
a stuck pedestal), masking exactly the fault class it appears to fix,
and staling whenever the layers beneath it change. Its legitimate
output — the unit-vs-reference-meter difference — is already recorded
as evidence by every sweep run with a reference reading. The feature
never shipped upstream (fork-only), so now is the cheapest moment to
remove it.

Removed: UISQMCorrect screen, menu entry and corrected indicator,
_apply_sqm_correct and both solver call sites, the sqm_correct_delta
shared state, and all test references. CONTEXT.md records the decision.
python/result and python/result-lib are nix build output symlinks that were
committed before .gitignore's result/result-* rule existed, so they stayed
tracked and kept getting re-snapshotted into unrelated commits. Untrack them;
the existing ignore rule keeps them out from now on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXDxJkU5CzAegv3FcMEwEq
The NixOS migration UI flow (UIMigrationConfirm/UIMigrationProgress) was removed
from ui/software.py in 'refactor(software): delete the NixOS migration UI flow',
but a later sqm-fix merge reintroduced the test's import and spec builders for
those deleted classes, breaking test collection. Drop the stale references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXDxJkU5CzAegv3FcMEwEq
test_radec_entry exercised an older dependency-injection API (BlinkingCursor
time_provider; CoordinateConverter/CoordinateEntryLogic calc_utils_provider;
LayoutConfig no-arg) that radec_entry dropped for module-level calc_utils/time.
Patch calc_utils/time at the module level and pass a display stub to
LayoutConfig so the suite matches the implementation. 6 stale tests were failing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXDxJkU5CzAegv3FcMEwEq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testable Ready for testing via PiFinder software update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants