NixOS: CM4-based PiFinder v4 boot and display support#540
Open
mrosseel wants to merge 378 commits into
Open
Conversation
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>
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
bcm2711-rpi-4-b.dtbandbcm2711-rpi-cm4.dtbare now processed with the PiFinder overlays, and the forced 4Bhardware.deviceTree.nameis replaced by extlinuxFDTDIR+ u-boot's board-detected${fdtfile}(64-bit u-boot prefixes it withbroadcom/), so one image boots the matching DTB on both boards.boot-splash.cgains an SSD1333 176x176 mode (init sequence ported fromssd1333_device.py, welcome image centered, flush rotated 270° to match the app's lumarotate=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.pifinder.gpsBaudoption emittingGPSD_OPTIONS="-s <baud>". Default stays autobaud, which serves both the rev-3 GPS and the v4 u-blox Gen10 (UBX at 115200).Verified so far
fdtoverlay;uart3/spi0end upokayin the merged tree.-Wall -Wextra; ruff +test_hardware_detectpass.Needs on-device validation
🤖 Generated with Claude Code
https://claude.ai/code/session_01QzvW78i2LxKpgQnXAvzSe3