Skip to content

NixOS: CM4-based PiFinder v4 boot and display support#540

Open
mrosseel wants to merge 378 commits into
brickbots:mainfrom
mrosseel:nixos-cm4
Open

NixOS: CM4-based PiFinder v4 boot and display support#540
mrosseel wants to merge 378 commits into
brickbots:mainfrom
mrosseel:nixos-cm4

Conversation

@mrosseel

Copy link
Copy Markdown
Collaborator

Adds boot and display support for the CM4-based PiFinder v4 prototype on top of the NixOS branch (#379).

What's in here (the v4-specific delta)

  • CM4 boot: both bcm2711-rpi-4-b.dtb and bcm2711-rpi-cm4.dtb are now processed with the PiFinder overlays, and the forced 4B hardware.deviceTree.name is replaced by extlinux FDTDIR + u-boot's board-detected ${fdtfile} (64-bit u-boot prefixes it with broadcom/), so one image boots the matching DTB on both boards.
  • Boot splash: boot-splash.c gains an SSD1333 176x176 mode (init sequence ported from ssd1333_device.py, welcome image centered, flush rotated 270° to match the app's luma rotate=3). Panel selection reads the device-tree model string (CM4 = v4), which is available in early boot/initrd where the I2C charger probe isn't. Splash service, watchdog failure screens and migration progress bar all get this for free.
  • gpsd: new pifinder.gpsBaud option emitting GPSD_OPTIONS="-s <baud>". Default stays autobaud, which serves both the rev-3 GPS and the v4 u-blox Gen10 (UBX at 115200).
  • hardware_detect: documents that the BQ25895 ACK is a valid whole-board rev-4 marker — a battery is mandatory on v4, so the charger always ACKs on working hardware. (Runtime SSD1333/battery-monitor/buzzer selection off that probe already exists on the NixOS branch.)

Verified so far

  • All overlays (spi0, uart3, i2c-gpio, pwm, gpio-poweroff, imx290 + 74.25 MHz xclk) apply cleanly to the CM4 DTB via fdtoverlay; uart3/spi0 end up okay in the merged tree.
  • Flake evaluates; boot-splash compiles clean under -Wall -Wextra; ruff + test_hardware_detect pass.

Needs on-device validation

  • CM4 selects its own DTB at boot (and 4B regression: FDT → FDTDIR switch affects existing devices; the confirmed-generation watchdog covers a failed boot).
  • SSD1333 splash orientation on the physical panel.
  • Controls matrix, charger/buzzer with battery installed, GPIO14 power-off behaviour, full acceptance pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QzvW78i2LxKpgQnXAvzSe3

github-actions Bot and others added 30 commits February 24, 2026 18:25
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace SD image build/mount/extract cycle with direct closure assembly.
This eliminates ext4 filesystem overhead, reducing tarball size to fit
in Pi 4's 924 MB tmpfs.

Also factors out configTxt and adds migration-boot-firmware package to
deduplicate boot partition assembly across profiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nix store paths are read-only and root-owned; rm -rf without sudo fails
on the copied closure paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge upstream changes including:
- IMU integrator selection (classic/quaternion)
- Eq-mount support under IMU dead-reckoning
- Chinese locale and font support
- Harris Globular Cluster catalog
- T9 input support
- PAM-based web auth (replacing su)
- Stellarium+ Mobile support
- Various bug fixes

NixOS adjustments:
- Add quaternion (numpy-quaternion) to python-packages.nix
- Add quaternion to mypy ignore list in pyproject.toml
- Add get_initialized_solved_dict() lost during merge resolution
- Fix formatting in integrator.py
- Keep requirements.txt and version.txt deleted (managed by nix)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SHA256 is now fetched at runtime from the .sha256 sidecar file
in the GitHub release, eliminating the need for CI to update the
migration branch after each build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: network-online.target was unreliable because
NetworkManager-wait-online was disabled, so pifinder-first-boot
ran before internet was available.

- Add curl-based connectivity check with 5-minute retry loop
- Add Restart=on-failure with 15s delay
- Re-enable NetworkManager-wait-online (with 30s timeout)
- Add sudo permissions for systemctl/journalctl (remote recovery)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Starts boot-splash in animation mode while downloading the full
system closure, so the user sees activity instead of a static screen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
solution() can return None when solve_state() is truthy, causing
TypeError in base.py screen_update. Also fetch first-boot target
from GitHub pifinder-build.json with baked-in fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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/.
- 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>
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>
mrosseel and others added 29 commits July 15, 2026 13:00
SQM photometry is raw-only; the 8-bit processed image is for solving
and display. The PNG path was unreachable in production and measured
strictly worse (0.6-2.4 mag within-sweep swings, errors to 1.7 mag,
frames failing photometry outright) at identical CPU cost.

- solver: create_sqm_calculator always builds the raw calculator;
  update_sqm loses image_processed/use_raw_green and the processed
  fallback branch
- camera_profiles: drop the four _processed profiles and the
  sqm_use_raw_green flag
- CAL wizard: captures, analyzes, verifies and saves the RAW
  calibration only; verification runs the raw pipeline on the raw sky
  frames (green extraction + centroid rescale + 70% saturation cut)
- SQM screen CAL indicator keys off the raw calibration file
- sweep metadata records the raw camera type
- SNR auto-exposure uses the controller defaults (its
  from_camera_profile existed only to consume _processed profiles;
  8-bit defaults 15/30/100 are equivalent)
- noise-floor tests run on a synthetic registered profile instead of
  imx296_processed

Sweep regression after the deletion is bit-identical
(18.275/std 0.070 on the 2026-07-11 reference sweep).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
… deletion)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
The instrument is stable within a night; its only weak number is the
absolute scale, which depends on the night sky spectrum through the
per-sensor band offset (calibrated for one sky regime). The anchor
measures exactly that unknown: aim the reference meter at the camera
field, enter its reading on the new ANCHOR marking-menu entry of the
SQM screen, and the session is shifted by delta = reference - current.

- UISQMAnchor: meter-value entry (sweep-wizard input pattern), shows
  the live PiFinder value, 0 clears the anchor; delta lives in shared
  state only (resets on restart - tonight's spectrum is tonight's)
- solver applies the delta to sqm_final and sqm_altitude_corrected and
  reports it in details; SQM screen shows "anch +/-x.xx" when active
- every anchor event is appended to ~/PiFinder_data/sqm_anchors.jsonl
  (reference, measured, deltas, camera, location) - accumulating the
  per-site band-offset calibration dataset as a side effect of use
- marking menu reworked: ANCHOR (everyday action), CAL (noise
  constants), SWEEP (diagnostic; label was CORRECT which now belongs
  to the anchor)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
The archived imx296 sweep (100 frames, ref 17.8-17.9) replaces three
placeholders with measurements:

- bias_offset 32 -> 60: the on-sky exposure-ramp intercept is 60.3,
  matching the Sony-standard black level (240 @ 12-bit = 60 @ 10-bit,
  same convention as the imx462 at 238). The old 32 was a
  mis-measurement and biased the background subtraction.
- color_coefficient 0 -> +0.21 (460 stars, mzero vs B-V). Small because
  the Pregius mono QE falls through the NIR, unlike the STARVIS colour
  sensors.
- sqm_band_offset 0 -> -0.46, PROVISIONAL: one moonlit sweep, one hand
  reference; the mono band reads darker than the meter. The session
  anchor absorbs the uncertainty until more nights exist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
SQM photometry is raw-only; the 8-bit processed image is for solving
and display. The PNG path was unreachable in production and measured
strictly worse (0.6-2.4 mag within-sweep swings, errors to 1.7 mag,
frames failing photometry outright) at identical CPU cost.

- solver: create_sqm_calculator always builds the raw calculator;
  update_sqm loses image_processed/use_raw_green and the processed
  fallback branch
- camera_profiles: drop the four _processed profiles and the
  sqm_use_raw_green flag
- CAL wizard: captures, analyzes, verifies and saves the RAW
  calibration only; verification runs the raw pipeline on the raw sky
  frames (green extraction + centroid rescale + 70% saturation cut)
- SQM screen CAL indicator keys off the raw calibration file
- sweep metadata records the raw camera type
- SNR auto-exposure uses the controller defaults (its
  from_camera_profile existed only to consume _processed profiles;
  8-bit defaults 15/30/100 are equivalent)
- noise-floor tests run on a synthetic registered profile instead of
  imx296_processed

Sweep regression after the deletion is bit-identical
(18.275/std 0.070 on the 2026-07-11 reference sweep).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
… module

Surface the reference-meter session feature as "SQM Correct" (matching the
CORRECT marking-menu action). Renames UISQMAnchor->UISQMCorrect
(sqm_anchor.py->sqm_correct.py), shared-state sqm_anchor_delta->
sqm_correct_delta, solver._apply_sqm_anchor->_apply_sqm_correct, and the
per-event log sqm_anchors.jsonl->sqm_corrects.jsonl.

Delete the orphaned UISQMCorrection (sqm_correction.py): upstream removed its
menu launcher in brickbots#374 (2026-03), leaving it unreachable dead code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VaxurSrCzxrsfJrSjfA5bo
stdenv's minimal bash lacked programmable completion, breaking
interactive bash launched inside the direnv devshell (shopt progcomp
/ complete errors, raw PS1 escapes).
…tadata

- sqm.py: dark_current_rate/read_noise default to 0.0 so a None (from an
  incomplete calibration file) can't crash the whole SQM calculation on a
  diagnostic-only noise term.
- sqm_sweep.py: always write the sqm/reference block and guard each optional
  enrichment section independently so one failing step no longer discards the
  entire detailed-metadata record.
The solver built the calculator eagerly at startup from
shared_state.camera_type(), which still holds the 'imx296' default until the
camera process finishes libcamera init and reports the real sensor. The
calculator then kept the wrong profile for the whole session, so photometry
subtracted the imx296 pedestal (bias_offset 60) instead of the real sensor's
(238 imx462 / 256 hq), inflating the sky background by ~2-3 mag: devices read
SQM ~15-16 under an ~18 sky (measured: imx296 profile 14.67 vs hq 18.12 on
the same frame).

Create the calculator on first use instead. The camera process records its
real type before capturing its first frame, and a solve requires a captured
frame, so first use is guaranteed to see the real camera type. The
ReloadSqmCalibration command now just invalidates the calculator so the next
solve rebuilds it with fresh calibration, leaving a single creation site.
Coordinate/altitude fields on the web location add & edit forms were
<input type="number">. In a comma-decimal browser locale the browser
returns an empty .value for a period-formatted number (and vice-versa),
so the client-side validation saw the field as empty and silently
blocked submit — the Save button appeared active but did nothing.
Integer entries (no separator) worked, decimals did not.

Switch the numeric inputs to type=text inputmode=decimal, normalise
both separators to a period in JS before validation and submit, and
add a tolerant parse_coordinate() on the server so a stray comma yields
a friendly validation message instead of a 500.

Same bug class as brickbots#291 (equipment focal-length decimal crash).
Offline analysis of every referenced HQ sweep (2025-11-16 x2, 2025-11-18,
2026-07-16; full pipeline incl. per-sweep wing correction) shows a stable
-0.32 mag residual vs the hand-held reference meter (median of -0.28, -0.08,
-0.46, -0.36; spread 0.14). Raise the band offset 0.07 -> 0.39 to absorb it.

Method validated on imx462: the same offline pipeline reproduces on-device
SQM exactly and leaves imx462 residuals at +0.02 +/- 0.04 with its existing
+0.43 offset. Analysis artifacts: myPiFinder/support/dumps/analysis/20260716.
…fsets

When a star's wings sit below the ring noise, the wing-boundary search cuts
at the first ring — at worst the aperture itself, where total==core makes the
enclosed fraction exactly 1.0 regardless of the optics. Faint fields then
fill the rolling window with these tautological 'perfect aperture' samples
and the wing correction collapses (measured: same-night HQ sweeps at f 0.92
vs 0.71, a 0.27 mag SQM split; 20% of the faint sweep's samples were exactly
degenerate).

Require the boundary to sit at least min_wing_radius out: a closer boundary
means the wings were not resolved for that star — a statement about the
star's brightness, not the optics — so the sample is dropped instead of
recorded as f~1. The threshold is one angular cut (~530 arcsec) expressed in
each sensor's photometry-image pixels (CameraProfile.wing_min_radius_px:
7 imx462/imx290 @490px green, 11 hq @760px, 15 imx296 @1088px mono), wired
in the solver when the SQM calculator is created.

Validated offline against all 14 archived sweeps (local cedar pipeline that
reproduces on-device SQM exactly): the same-night HQ pair converges from
f 0.92/0.71 to 0.64/0.67, and per-sweep f spreads tighten on both sensors.
The floor removes samples that previously diluted the correction, so wcorr
rises and both band offsets are recalibrated with it (they absorb the
estimator's mean and must move as a package):

- imx462/imx290: 0.43 -> 0.31 (6 referenced sweeps, residuals +/-0.10)
- hq:            0.39 -> 0.32 (3 independent references; the shared
  2025-11-16 reading sits +0.3 off on both its sweeps — suspect reference)

Analysis artifacts: myPiFinder/support/dumps/analysis/20260716.
- Cache the update manifest on disk and fetch refreshes in a background
  thread: the screen renders the last-known list immediately on entry,
  with a dim bottom-line indicator while checking (and 'update check
  failed' when offline with a cached list).
- Unstable rows lead with the bare PR number (fixed) followed by the PR
  title (scrolls when focused); store hashes no longer appear in the
  list. Trunk rows show the branch name after the dot.
- Focused rows and the confirm screen show build age ('built 5h ago')
  from a new built_at stamp CI now writes into every manifest entry.
- Trunk entries are only offered when their branch actually contains
  flake.nix (checked at refresh time, verdict cached in the manifest
  cache), so a non-NixOS upstream main never shows as installable.
…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).
- Boot: process both the Pi 4B and CM4 DTBs with the PiFinder overlays
  and drop the forced 4B FDT in favour of extlinux FDTDIR + u-boot's
  board-detected ${fdtfile} (64-bit u-boot prefixes it with broadcom/),
  so one image boots the matching DTB on both boards. Verified with
  fdtoverlay: all overlays (spi0, uart3, i2c-gpio, pwm, gpio-poweroff,
  imx290 + xclk) apply cleanly to bcm2711-rpi-cm4.dtb.
- Boot splash: SSD1333 176x176 mode (init per ssd1333_device.py, welcome
  image centered, flush rotated 270 deg to match the app's luma rotate=3)
  selected at runtime from the device-tree model string (CM4 = v4), which
  is available in early boot unlike the charger probe.
- gpsd: new pifinder.gpsBaud option (-s <baud>); default stays autobaud,
  which serves both the rev-3 GPS and the v4 u-blox Gen10 at 115200.
- hardware_detect: document that the BQ25895 ACK is a valid rev-4 marker
  because a battery is mandatory on v4.

Needs on-device validation: CM4 DTB selection at boot, SSD1333 splash
orientation, and 4B regression (FDT -> FDTDIR switch).
@mrosseel mrosseel added the testable Ready for testing via PiFinder software update label Jul 17, 2026
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