From ef19a1caddcb57e91dcfacd5e0ed8e54f8212a2b Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 10:50:44 +0000 Subject: [PATCH 1/8] os: Move /home/pi/PlanktoScope to /opt/PlanktoScope --- .github/workflows/build-os.yml | 2 +- backend/planktoscope-org.backend.service | 2 +- ...lanktoscope-org.controller.bubbler.service | 2 +- ...lanktoscope-org.controller.display.service | 2 +- controller/focus/main.py | 2 +- .../planktoscope-org.controller.focus.service | 2 +- controller/helpers.py | 2 +- controller/imager/main.py | 2 +- ...planktoscope-org.controller.imager.service | 2 +- controller/light/LM36011.py | 2 +- .../planktoscope-org.controller.light.service | 2 +- .../planktoscope-org.controller.service | 2 +- .../planktoscope-org.controller.pump.service | 2 +- docs/calibration-matrix.md | 2 +- .../community/contribute/tips-and-tricks.md | 4 ++-- .../docs/operation/software-upgrades.md | 2 +- justfile | 5 +++-- lib/cockpit.js | 2 +- lib/file-config.js | 19 ++++++++++--------- lib/mediamtx.js | 4 ++-- lib/nodered.js | 2 +- node-red/30-override.conf | 2 +- node-red/projects/dashboard | 2 +- os/caddy/Caddyfile | 2 +- os/image/mount-firmware.service | 2 +- os/machine-name/generate-hostname.service | 2 +- os/machine-name/generate-machine-name.service | 2 +- os/mediamtx/justfile | 2 +- os/mediamtx/mediamtx.service | 2 +- .../planktoscope-org.firstboot.service | 2 +- os/rauc/rauc.ini | 4 ++-- segmenter/planktoscope-org.segmenter.service | 2 +- 32 files changed, 46 insertions(+), 44 deletions(-) diff --git a/.github/workflows/build-os.yml b/.github/workflows/build-os.yml index 3ffaa6a1c..32bb3678a 100644 --- a/.github/workflows/build-os.yml +++ b/.github/workflows/build-os.yml @@ -200,7 +200,7 @@ jobs: user: pi run: | export DEBIAN_FRONTEND=noninteractive - cd /home/pi/PlanktoScope + cd /opt/PlanktoScope just ci # UPLOAD OS IMAGE diff --git a/backend/planktoscope-org.backend.service b/backend/planktoscope-org.backend.service index 448bf1af3..eac9cac08 100644 --- a/backend/planktoscope-org.backend.service +++ b/backend/planktoscope-org.backend.service @@ -5,7 +5,7 @@ After=mosquitto.service [Service] Type=simple -ExecStart=/home/pi/PlanktoScope/backend/src/service.js +ExecStart=/opt/PlanktoScope/backend/src/service.js Restart=on-failure User=pi diff --git a/controller/bubbler/planktoscope-org.controller.bubbler.service b/controller/bubbler/planktoscope-org.controller.bubbler.service index 5976a09a0..0ee3655d8 100644 --- a/controller/bubbler/planktoscope-org.controller.bubbler.service +++ b/controller/bubbler/planktoscope-org.controller.bubbler.service @@ -5,7 +5,7 @@ After=mosquitto.service [Service] Type=simple Environment=HOME=/home/pi -WorkingDirectory=/home/pi/PlanktoScope/controller +WorkingDirectory=/opt/PlanktoScope/controller ExecStart=/usr/local/bin/uv run python -m bubbler.main User=pi Group=pi diff --git a/controller/display/planktoscope-org.controller.display.service b/controller/display/planktoscope-org.controller.display.service index ec33a6af4..2dbc52e8b 100644 --- a/controller/display/planktoscope-org.controller.display.service +++ b/controller/display/planktoscope-org.controller.display.service @@ -5,7 +5,7 @@ After=mosquitto.service [Service] Type=simple Environment=HOME=/home/pi -WorkingDirectory=/home/pi/PlanktoScope/controller +WorkingDirectory=/opt/PlanktoScope/controller ExecStart=/usr/local/bin/uv run python -m display.main User=pi Group=pi diff --git a/controller/focus/main.py b/controller/focus/main.py index 30a50f7e0..104f8acf3 100644 --- a/controller/focus/main.py +++ b/controller/focus/main.py @@ -34,7 +34,7 @@ async def start() -> None: hardware_config = None try: - async with aiofiles.open("/home/pi/PlanktoScope/hardware.json", mode="r") as file: + async with aiofiles.open("/opt/PlanktoScope/hardware.json", mode="r") as file: hardware_config = json.loads(await file.read()) except FileNotFoundError: return None diff --git a/controller/focus/planktoscope-org.controller.focus.service b/controller/focus/planktoscope-org.controller.focus.service index 9ed72f79b..c0b589579 100644 --- a/controller/focus/planktoscope-org.controller.focus.service +++ b/controller/focus/planktoscope-org.controller.focus.service @@ -5,7 +5,7 @@ After=mosquitto.service [Service] Type=simple Environment=HOME=/home/pi -WorkingDirectory=/home/pi/PlanktoScope/controller +WorkingDirectory=/opt/PlanktoScope/controller ExecStart=/usr/local/bin/uv run python -m focus.main User=pi Group=pi diff --git a/controller/helpers.py b/controller/helpers.py index d4b8bf50a..a1618b85d 100644 --- a/controller/helpers.py +++ b/controller/helpers.py @@ -7,7 +7,7 @@ import aiomqtt import paho -HARDWARE_CONFIG_PATH = "/home/pi/PlanktoScope/hardware.json" +HARDWARE_CONFIG_PATH = "/opt/PlanktoScope/hardware.json" hardwre_config_lock = asyncio.Lock() diff --git a/controller/imager/main.py b/controller/imager/main.py index 163c714bf..b7da6e504 100644 --- a/controller/imager/main.py +++ b/controller/imager/main.py @@ -529,7 +529,7 @@ def close(self) -> None: def read_config() -> typing.Any: config = {} try: - with open("/home/pi/PlanktoScope/hardware.json", "r") as file: + with open("/opt/PlanktoScope/hardware.json", "r") as file: try: config = json.load(file) except Exception: diff --git a/controller/imager/planktoscope-org.controller.imager.service b/controller/imager/planktoscope-org.controller.imager.service index b7a109d36..3a2448d5c 100644 --- a/controller/imager/planktoscope-org.controller.imager.service +++ b/controller/imager/planktoscope-org.controller.imager.service @@ -7,7 +7,7 @@ After=mediamtx.service [Service] Type=simple Environment=HOME=/home/pi -WorkingDirectory=/home/pi/PlanktoScope/controller +WorkingDirectory=/opt/PlanktoScope/controller # FIXME: https://github.com/fairscope/PlanktoScope/issues/842 ExecStartPre=/bin/sleep 5 ExecStart=/usr/local/bin/uv run python -m imager.main diff --git a/controller/light/LM36011.py b/controller/light/LM36011.py index 3216e0eba..1eb1d6214 100644 --- a/controller/light/LM36011.py +++ b/controller/light/LM36011.py @@ -24,7 +24,7 @@ class Register(enum.IntEnum): DEFAULT_CURRENT = 10 def __init__(self): - with open("/home/pi/PlanktoScope/hardware.json", "r") as file: + with open("/opt/PlanktoScope/hardware.json", "r") as file: config = json.load(file) hat_version = float(config.get("hat_version") or 0) # The led is controlled by LM36011 diff --git a/controller/light/planktoscope-org.controller.light.service b/controller/light/planktoscope-org.controller.light.service index 3279f00f4..1ff79794b 100644 --- a/controller/light/planktoscope-org.controller.light.service +++ b/controller/light/planktoscope-org.controller.light.service @@ -5,7 +5,7 @@ After=mosquitto.service [Service] Type=simple Environment=HOME=/home/pi -WorkingDirectory=/home/pi/PlanktoScope/controller +WorkingDirectory=/opt/PlanktoScope/controller ExecStart=/usr/local/bin/uv run python -m light.main User=pi Group=pi diff --git a/controller/planktoscope-org.controller.service b/controller/planktoscope-org.controller.service index 91ea393b2..a53c5aa78 100644 --- a/controller/planktoscope-org.controller.service +++ b/controller/planktoscope-org.controller.service @@ -8,7 +8,7 @@ After=mediamtx.service [Service] Type=simple Environment=HOME=/home/pi -WorkingDirectory=/home/pi/PlanktoScope/controller +WorkingDirectory=/opt/PlanktoScope/controller ExecStart=/usr/local/bin/uv run main.py User=pi Group=pi diff --git a/controller/pump/planktoscope-org.controller.pump.service b/controller/pump/planktoscope-org.controller.pump.service index 794dea573..69d02f5bb 100644 --- a/controller/pump/planktoscope-org.controller.pump.service +++ b/controller/pump/planktoscope-org.controller.pump.service @@ -5,7 +5,7 @@ After=mosquitto.service [Service] Type=simple Environment=HOME=/home/pi -WorkingDirectory=/home/pi/PlanktoScope/controller +WorkingDirectory=/opt/PlanktoScope/controller ExecStart=/usr/local/bin/uv run python -m pump.main User=pi Group=pi diff --git a/docs/calibration-matrix.md b/docs/calibration-matrix.md index d7aeb3a77..4d854d291 100644 --- a/docs/calibration-matrix.md +++ b/docs/calibration-matrix.md @@ -108,7 +108,7 @@ else: #### `lib/file-config.js` Added: -- `CALIBRATION_PATH` — `/home/pi/PlanktoScope/calibration.json` +- `CALIBRATION_PATH` — `/opt/PlanktoScope/calibration.json` - `readCalibrationConfig()` — read the calibration matrix - `updateCalibrationConfig()` — update it - `hasCalibrationConfig()` — check existence diff --git a/documentation/docs/community/contribute/tips-and-tricks.md b/documentation/docs/community/contribute/tips-and-tricks.md index 50e7f66f3..7da9eea3c 100644 --- a/documentation/docs/community/contribute/tips-and-tricks.md +++ b/documentation/docs/community/contribute/tips-and-tricks.md @@ -39,7 +39,7 @@ PlanktoScope OS is ready. Type the following commands ```sh -cd /home/pi/PlanktoScope +cd /opt/PlanktoScope just update # don't forget to copy default configs if needed/wanted # cp default-configs/v3.0.hardware.json hardware.json @@ -107,7 +107,7 @@ git status -We recommend developping directly from the PlanktoScope using [Visual Studio Code and the Remote - SSH extension](https://code.visualstudio.com/docs/remote/ssh) or [Zed - Remote Development](https://zed.dev/docs/remote-development). Use `$planktoscope` as the host to connect to and open the `/home/pi/PlanktoScope` directory. +We recommend developping directly from the PlanktoScope using [Visual Studio Code and the Remote - SSH extension](https://code.visualstudio.com/docs/remote/ssh) or [Zed - Remote Development](https://zed.dev/docs/remote-development). Use `$planktoscope` as the host to connect to and open the `/opt/PlanktoScope` directory. ## Connect to router diff --git a/documentation/docs/operation/software-upgrades.md b/documentation/docs/operation/software-upgrades.md index 60fb12d9f..9abe5cd87 100644 --- a/documentation/docs/operation/software-upgrades.md +++ b/documentation/docs/operation/software-upgrades.md @@ -16,7 +16,7 @@ Before you reset/upgrade/downgrade the software installed on your PlanktoScope, Advanced users may also want to take the following actions, depending on what changes they have made: -- If you don't want to write down your white balance gains and hardware settings/calibrations, you can instead back up your PlanktoScope's hardware settings file, which is saved at `/home/pi/PlanktoScope/hardware.json`, for example in the file browser at . This file includes some hidden settings not exposed in the PlanktoScope's Node-RED dashboard - so if you have changed any such settings by editing this file, then you may want to back up this file. +- If you don't want to write down your white balance gains and hardware settings/calibrations, you can instead back up your PlanktoScope's hardware settings file, which is saved at `/opt/PlanktoScope/hardware.json`, for example in the file browser at . This file includes some hidden settings not exposed in the PlanktoScope's Node-RED dashboard - so if you have changed any such settings by editing this file, then you may want to back up this file. ## Reset the PlanktoScope OS diff --git a/justfile b/justfile index 899a25ce3..96a4d40ad 100644 --- a/justfile +++ b/justfile @@ -73,8 +73,9 @@ developer-mode: setup-dev ./os/developer-mode/configure.mjs reset: base setup - rm /home/pi/PlanktoScope/config.json - rm /home/pi/PlanktoScope/hardware.json + rm /opt/PlanktoScope/config.json + rm /opt/PlanktoScope/hardware.json + rm /opt/PlanktoScope/calibration.json sudo reboot install-uv: diff --git a/lib/cockpit.js b/lib/cockpit.js index 32dff399b..2ee4e4bac 100644 --- a/lib/cockpit.js +++ b/lib/cockpit.js @@ -6,7 +6,7 @@ import { Systemctl } from "systemctl.js" import { randomUUID } from "node:crypto" import { queue } from "./helpers.js" -const config_template_path = "/home/pi/PlanktoScope/os/cockpit/cockpit.ini" +const config_template_path = "/opt/PlanktoScope/os/cockpit/cockpit.ini" const config_path = "/etc/cockpit/cockpit.conf" async function configureCockpit({ hostname, address } = {}) { diff --git a/lib/file-config.js b/lib/file-config.js index a62c10f28..e2f564d3e 100644 --- a/lib/file-config.js +++ b/lib/file-config.js @@ -1,10 +1,11 @@ import { rm, writeFile } from "fs/promises" import { readFile, access, constants, copyFile } from "fs/promises" -const HARDWARE_PATH = "/home/pi/PlanktoScope/hardware.json" -const SOFTWARE_PATH = "/home/pi/PlanktoScope/config.json" -const CALIBRATION_PATH = "/home/pi/PlanktoScope/calibration.json" -const CALIBRATION_DEFAULTS_PATH = "/home/pi/PlanktoScope/default-configs/calibration.json" +const HARDWARE_PATH = "/opt/PlanktoScope/hardware.json" +const SOFTWARE_PATH = "/opt/PlanktoScope/config.json" +const CALIBRATION_PATH = "/opt/PlanktoScope/calibration.json" +const CALIBRATION_DEFAULTS_PATH = + "/opt/PlanktoScope/default-configs/calibration.json" async function hasConfig(path) { try { @@ -18,12 +19,12 @@ async function hasConfig(path) { export async function initConfigFiles(hardware_version) { await Promise.all([ copyFile( - `/home/pi/PlanktoScope/default-configs/${hardware_version}.config.json`, - "/home/pi/PlanktoScope/config.json", + `/opt/PlanktoScope/default-configs/${hardware_version}.config.json`, + "/opt/PlanktoScope/config.json", ), copyFile( - `/home/pi/PlanktoScope/default-configs/${hardware_version}.hardware.json`, - "/home/pi/PlanktoScope/hardware.json", + `/opt/PlanktoScope/default-configs/${hardware_version}.hardware.json`, + "/opt/PlanktoScope/hardware.json", ), // Create calibration.json from defaults if it doesn't exist yet. // Unlike config/hardware, this never overwrites — preserving user calibrations. @@ -62,6 +63,7 @@ export async function removeConfig() { await Promise.all([ rm(HARDWARE_PATH, { force: true }), rm(SOFTWARE_PATH, { force: true }), + rm(CALIBRATION_PATH, { forrce: true }), ]) } @@ -100,4 +102,3 @@ export async function updateCalibrationConfig(...args) { export async function hasCalibrationConfig() { return hasConfig(CALIBRATION_PATH) } - diff --git a/lib/mediamtx.js b/lib/mediamtx.js index b6d8a1370..e05155b11 100644 --- a/lib/mediamtx.js +++ b/lib/mediamtx.js @@ -3,8 +3,8 @@ import os from "os" import { queue } from "./helpers.js" const config_template_path = - "/home/pi/PlanktoScope/os/mediamtx/mediamtx.template.yml" -const config_path = "/home/pi/PlanktoScope/os/mediamtx/mediamtx.yml" + "/opt/PlanktoScope/os/mediamtx/mediamtx.template.yml" +const config_path = "/opt/PlanktoScope/os/mediamtx/mediamtx.yml" async function configureMediaMTX({ hostname, address } = {}) { let content = await readFile(config_template_path, "utf8") diff --git a/lib/nodered.js b/lib/nodered.js index 8f0bb156c..854a6bc3c 100644 --- a/lib/nodered.js +++ b/lib/nodered.js @@ -3,7 +3,7 @@ import { createRequire } from "node:module" const require = createRequire(import.meta.url) -const node_red_settings_path = "/home/pi/PlanktoScope/node-red/settings.cjs" +const node_red_settings_path = "/opt/PlanktoScope/node-red/settings.cjs" export async function promiseDashboardOnline() { const { uiPort: port } = require(node_red_settings_path) diff --git a/node-red/30-override.conf b/node-red/30-override.conf index 3c0ac6765..25cb6dc2e 100644 --- a/node-red/30-override.conf +++ b/node-red/30-override.conf @@ -8,4 +8,4 @@ Before=mosquitto.service [Service] ExecStart= -ExecStart=/home/pi/.local/bin/node-red-pi $NODE_OPTIONS --settings /home/pi/PlanktoScope/node-red/settings.cjs $NODE_RED_OPTIONS +ExecStart=/home/pi/.local/bin/node-red-pi $NODE_OPTIONS --settings /opt/PlanktoScope/node-red/settings.cjs $NODE_RED_OPTIONS diff --git a/node-red/projects/dashboard b/node-red/projects/dashboard index 41122091e..1f8c7f024 160000 --- a/node-red/projects/dashboard +++ b/node-red/projects/dashboard @@ -1 +1 @@ -Subproject commit 41122091e7fcd49f75773e711a5b8998e6d49792 +Subproject commit 1f8c7f0249611621028b8fa3c6e643f409ad44a8 diff --git a/os/caddy/Caddyfile b/os/caddy/Caddyfile index e7f07d416..6d8f16fa7 100644 --- a/os/caddy/Caddyfile +++ b/os/caddy/Caddyfile @@ -42,7 +42,7 @@ # We want everyrthing else /* to frontend handle /* { - root * /home/pi/PlanktoScope/frontend/dist + root * /opt/PlanktoScope/frontend/dist try_files {path} /index.html file_server } diff --git a/os/image/mount-firmware.service b/os/image/mount-firmware.service index a5b5a2e2c..c673661b6 100644 --- a/os/image/mount-firmware.service +++ b/os/image/mount-firmware.service @@ -3,7 +3,7 @@ Description=Mount firmware partition [Service] Type=oneshot -ExecStart=/home/pi/PlanktoScope/os/image/mount-firmware.js +ExecStart=/opt/PlanktoScope/os/image/mount-firmware.js RemainAfterExit=yes [Install] diff --git a/os/machine-name/generate-hostname.service b/os/machine-name/generate-hostname.service index d5670c001..c0a3a889f 100644 --- a/os/machine-name/generate-hostname.service +++ b/os/machine-name/generate-hostname.service @@ -10,7 +10,7 @@ Before=sysinit.target [Service] Type=oneshot -ExecStart=/home/pi/PlanktoScope/os/machine-name/generate-hostname.sh +ExecStart=/opt/PlanktoScope/os/machine-name/generate-hostname.sh [Install] WantedBy=sysinit.target diff --git a/os/machine-name/generate-machine-name.service b/os/machine-name/generate-machine-name.service index ff953862c..3a08b99dc 100644 --- a/os/machine-name/generate-machine-name.service +++ b/os/machine-name/generate-machine-name.service @@ -9,7 +9,7 @@ Before=systemd-hostnamed.service [Service] Type=oneshot ExecStartPre=sh -c "echo 'unknown' >/run/machine-name" -ExecStart=/home/pi/PlanktoScope/os/machine-name/generate-machine-name.sh +ExecStart=/opt/PlanktoScope/os/machine-name/generate-machine-name.sh [Install] WantedBy=sysinit.target diff --git a/os/mediamtx/justfile b/os/mediamtx/justfile index 27a4279db..0a4a27961 100644 --- a/os/mediamtx/justfile +++ b/os/mediamtx/justfile @@ -1,7 +1,7 @@ setup: sudo ./setup_h264_sysctl.sh wget https://github.com/bluenviron/mediamtx/releases/download/v1.16.2/mediamtx_v1.16.2_linux_arm64.tar.gz -P /tmp - wget https://raw.githubusercontent.com/bluenviron/mediamtx/refs/tags/v1.16.2/internal/servers/webrtc/reader.js -O /home/pi/PlanktoScope/frontend/src/pages/preview/reader.js + wget https://raw.githubusercontent.com/bluenviron/mediamtx/refs/tags/v1.16.2/internal/servers/webrtc/reader.js -O /opt/PlanktoScope/frontend/src/pages/preview/reader.js cd /tmp && tar -xf /tmp/mediamtx_v1.16.2_linux_arm64.tar.gz -sudo systemctl stop mediamtx sudo cp /tmp/mediamtx /usr/local/bin/mediamtx diff --git a/os/mediamtx/mediamtx.service b/os/mediamtx/mediamtx.service index a8d9d55cd..4314089ce 100644 --- a/os/mediamtx/mediamtx.service +++ b/os/mediamtx/mediamtx.service @@ -5,7 +5,7 @@ After=network.target Wants=network.target [Service] -ExecStart=/usr/local/bin/mediamtx /home/pi/PlanktoScope/os/mediamtx/mediamtx.yml +ExecStart=/usr/local/bin/mediamtx /opt/PlanktoScope/os/mediamtx/mediamtx.yml [Install] WantedBy=multi-user.target diff --git a/os/raspberry/planktoscope-org.firstboot.service b/os/raspberry/planktoscope-org.firstboot.service index ec9dd5d33..787d55450 100644 --- a/os/raspberry/planktoscope-org.firstboot.service +++ b/os/raspberry/planktoscope-org.firstboot.service @@ -4,7 +4,7 @@ ConditionFirstBoot=yes [Service] Type=oneshot -ExecStart=/home/pi/PlanktoScope/os/raspberry/firstboot.sh +ExecStart=/opt/PlanktoScope/os/raspberry/firstboot.sh [Install] WantedBy=sysinit.target diff --git a/os/rauc/rauc.ini b/os/rauc/rauc.ini index 46f8c7fd0..8d770aa6f 100644 --- a/os/rauc/rauc.ini +++ b/os/rauc/rauc.ini @@ -9,14 +9,14 @@ data-directory=/data/rauc # https://rauc.readthedocs.io/en/latest/using.html#system-based-customization-handlers [handlers] # https://rauc.readthedocs.io/en/latest/integration.html#custom -bootloader-custom-backend=/home/pi/PlanktoScope/os/rauc/custom-bootloader-script +bootloader-custom-backend=/opt/PlanktoScope/os/rauc/custom-bootloader-script # https://rauc.readthedocs.io/en/latest/reference.html#handlers-section # TODO # system-info=/usr/lib/raspberrypi-firmware-rauc-bootloader-backend/system-info # https://rauc.readthedocs.io/en/latest/reference.html#keyring-section [keyring] -path=/home/pi/PlanktoScope/os/rauc/demo.cert.pem +path=/opt/PlanktoScope/os/rauc/demo.cert.pem # https://rauc.readthedocs.io/en/latest/reference.html#slot-slot-class-idx-sections # Generated by rauc.js diff --git a/segmenter/planktoscope-org.segmenter.service b/segmenter/planktoscope-org.segmenter.service index 177370ddc..ea851b554 100644 --- a/segmenter/planktoscope-org.segmenter.service +++ b/segmenter/planktoscope-org.segmenter.service @@ -8,7 +8,7 @@ After=nodered.service Type=simple Environment=HOME=/home/pi Environment=PLANKTOSCOPE_DATA_PATH=/home/pi/data -WorkingDirectory=/home/pi/PlanktoScope/segmenter +WorkingDirectory=/opt/PlanktoScope/segmenter ExecStart=/usr/local/bin/uv run main.py User=pi Group=pi From af50ca7d34a93b52965c037e5af483754f79e98e Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 11:51:13 +0000 Subject: [PATCH 2/8] f --- docs/calibration-matrix.md | 189 ----------------------- docs/fix-pixel-size-management.md | 244 ------------------------------ justfile | 6 +- lib/file-config.js | 10 +- os/setup-ci.sh | 6 +- 5 files changed, 11 insertions(+), 444 deletions(-) delete mode 100644 docs/calibration-matrix.md delete mode 100644 docs/fix-pixel-size-management.md diff --git a/docs/calibration-matrix.md b/docs/calibration-matrix.md deleted file mode 100644 index 4d854d291..000000000 --- a/docs/calibration-matrix.md +++ /dev/null @@ -1,189 +0,0 @@ -# Per-Preset Calibration Matrix - -## Problem - -The original pixel size management used a single global `process_pixel` value. When a user switched magnification presets, the factory default for the new preset overwrote any user calibration. A stage-micrometer calibration (the gold standard in microscopy) could be silently discarded. - -| Scenario | Expected | Old behavior | -|---|---|---| -| User calibrates medium optics to 0.762 µm/px, starts acquisition on "medium" | Uses 0.762 | Uses 0.75 (factory overwrites calibration) | -| User calibrates high, switches to medium, switches back to high | Restores high calibration | Uses 0.53 (factory value, calibration lost) | - -## Solution - -Each magnification preset (high/medium/low) stores both its factory default and an optional user-calibrated override. The active pixel size is always: **user calibration if it exists for that preset, otherwise factory default**. - -``` -calibration_matrix = { - "high": { "factory": 0.53, "user_calibrated": null }, - "medium": { "factory": 0.75, "user_calibrated": null }, - "low": { "factory": 1.34, "user_calibrated": null } -} -``` - -Resolution happens in Node-RED before sending to Python. Python is a consumer, not a resolver. - -## Data Flow - -``` -User selects preset or completes calibration - │ - ▼ -┌──────────────────────────┐ -│ Node-RED (body template)│ -│ │ -│ calibrationMatrix │ -│ resolves: user_cal ?? │ -│ factory default │ -│ │ -│ sends process_pixel │ -│ via MQTT │ -└──────────┬───────────────┘ - │ MQTT: imager/image/update_config - ▼ -┌──────────────────────────┐ -│ Python Imager │ -│ (controller/imager/ │ -│ main.py) │ -│ │ -│ Uses process_pixel │ -│ directly. Fallback: │ -│ process_pixel_fixed │ -│ from hardware.json │ -│ │ -│ Writes → metadata.json │ -└──────────┬───────────────┘ - │ - ▼ -┌──────────────────────────┐ -│ Segmenter │ -│ (__init__.py) │ -│ │ -│ Reads process_pixel │ -│ from metadata.json │ -│ │ -│ • ESD threshold (µm→px) │ -│ • Measurement scaling │ -│ • Writes to TSV │ -└──────────┬───────────────┘ - │ - ▼ -┌──────────────────────────┐ -│ Gallery / Explorer │ -│ (Node-RED templates) │ -│ │ -│ Reads process_pixel │ -│ from TSV column │ -│ → meta.resolution │ -│ │ -│ • Scale bar rendering │ -│ • Thumbnail sizing │ -│ • Measurement display │ -└──────────────────────────┘ -``` - -## Files Changed - -### Python - -#### `controller/imager/main.py` (lines 179–193) - -**Before:** 3-tier resolution cascade: -1. `process_pixel_size` (preset from acquisition page) — wins -2. `process_pixel` (from calibration page) — loses if preset set -3. `process_pixel_fixed` (hardware.json fallback) - -**After:** Node-RED always sends the resolved value. Python just uses it: - -```python -pixel_size = metadata.get("process_pixel") -if pixel_size is not None: - metadata["process_pixel"] = float(pixel_size) -else: - loguru.logger.warning("process_pixel missing from config — measurements will be in pixels") -``` - -### JavaScript - -#### `lib/file-config.js` - -Added: -- `CALIBRATION_PATH` — `/opt/PlanktoScope/calibration.json` -- `readCalibrationConfig()` — read the calibration matrix -- `updateCalibrationConfig()` — update it -- `hasCalibrationConfig()` — check existence -- Calibration file init moved into `initConfigFiles()` with copy-if-missing semantics (never overwrites user calibrations) - -#### `default-configs/calibration.json` - -Factory defaults for the calibration matrix. Created during hardware setup if it doesn't already exist. - -### Node-RED Flow Changes - -Changes made directly in the Node-RED flow editor. - -#### `Get Global Variables` (id: `31fab063b7078fe6`) - -Added initialization of `calibration_matrix` in persistent global context on first boot. If the global doesn't exist, creates it with factory defaults. This matrix is then sent to the body template along with all other globals when the acquisition page loads. - -#### `set calibration_pixel_size` (id: `133c27ef75317205`) - -**Before:** Wrote calibration result to three bare globals (`process_pixel`, `process_pixel_size`, `calibration_pixel_size`). - -**After:** Reads `acq_magnification` from global context to determine current preset, then writes the calibrated value to `calibration_matrix[preset].user_calibrated` in persistent global context. Still writes legacy globals for backward compatibility. Logs which preset was calibrated. - -#### `set acq_params` (id: `74aa092817adc6f0`) - -**Before:** Stored `process_pixel_size` in global context and synced it to `process_pixel` via a manual hack. - -**After:** Stores `process_pixel` directly (the resolved value from the calibration matrix). Removed `process_pixel_size` from the key list. Removed the sync hack. - -#### Body template (id: `79bccc0355eb5d87`) - -**`data()` section:** -Added `calibrationMatrix` property initialized with factory defaults. - -**Message watcher:** -- Loads `calibration_matrix` from incoming globals into `this.calibrationMatrix` -- Changed `process_pixel_size` handler to `process_pixel` - -**`applyMagChange()` method:** -Instead of always setting `this.calibration_pixel_size = config.pixel_size` (factory), now resolves from the calibration matrix: -```javascript -const calEntry = this.calibrationMatrix[this.currentMag]; -this.calibration_pixel_size = (calEntry && calEntry.user_calibrated !== null) - ? calEntry.user_calibrated - : config.pixel_size; -``` - -**`sendSettings()` method:** -Sends `process_pixel` (resolved value) instead of `process_pixel_size` (factory value). - -### Unchanged - -- **Segmenter** (`segmenter/planktoscope/segmenter/__init__.py`) — reads `process_pixel` from metadata.json as before. ESD filtering, measurement scaling, and metadata prefix filtering are unchanged. -- **EcoTaxa export** (`ecotaxa.py`) — unchanged, uses scaled values from segmenter. -- **Gallery/Explorer function nodes** — unchanged. They parse `process_pixel` from TSV columns, not from live MQTT globals. -- **Gallery/Explorer UI templates** — unchanged. Scale bar rendering (`getScaleBar`, `adaptiveScaleMicrons`, `scaleBarPercent`, `displayPx`) reads `meta.resolution` which comes from TSV parsing. - -## MQTT Contract - -| Field | Before | After | -|---|---|---| -| `process_pixel_size` | Sent by acquisition page (factory preset value) | No longer sent | -| `process_pixel` | Set by calibration page via global context | Resolved value from calibration matrix | - -## Backward Compatibility - -**Existing installations without `calibration.json`:** `Get Global Variables` initializes `calibration_matrix` in global context with factory defaults on first boot. `initConfigFiles()` creates the JSON file from defaults during hardware setup without overwriting existing calibrations. - -## Scale Bar Impact - -The Gallery and Explorer scale bars read `process_pixel` from the EcoTaxa TSV file at display time — not from live MQTT globals. The data path is: - -1. `process_pixel` written to `metadata.json` during acquisition -2. Segmenter reads it, writes it to TSV as a column -3. Gallery/Explorer function nodes parse TSV → `meta.resolution` -4. UI templates use `meta.resolution` for `getScaleBar()`, `displayPx()`, `scaleBarPercent()` - -Since `process_pixel` still reaches metadata.json with the correct value, scale bars are unaffected. diff --git a/docs/fix-pixel-size-management.md b/docs/fix-pixel-size-management.md deleted file mode 100644 index f4f70ad64..000000000 --- a/docs/fix-pixel-size-management.md +++ /dev/null @@ -1,244 +0,0 @@ -# Fix: Pixel Size Management System - -## Problem Statement - -Users reported three related issues when using different flowcell sizes (100µm, 300µm, 500µm): - -1. **Wrong ESD filtering thresholds** — The segmenter was not respecting the 20µm minimum ESD target. The 300µm flowcell filtered at ~15µm instead of 20µm; the 100µm and 500µm flowcells had similarly incorrect cutoffs. -2. **Pixel size missing from TSV output** — All exported EcoTaxa TSV files had the same (absent) pixel size, regardless of configuration. Downstream biovolume and size calculations were therefore incorrect. -3. **Measurements in pixels instead of µm** — All morphological measurements (area, major/minor axis, equivalent diameter, etc.) were exported in raw pixel units, not physical units. - -## Optical Configurations - -The PlanktoScope supports three magnification presets, each with a different lens configuration and pixel-to-µm ratio: - -| Magnification | Tube Lens | Objective | Pixel Size | Flowcell | Min ESD (20µm) | -|---|---|---|---|---|---| -| **high** | 35mm F2.4 | 12mm F2 | **0.53 µm/px** | 100µm | 37.7 px | -| **medium** | 25mm F1.8 | 12mm F1.8 | **0.75 µm/px** | 300µm | 26.7 px | -| **low** | 25mm F2.0 | 21.8mm F2.8 | **1.34 µm/px** | 500µm | 14.9 px | - -Users can also run interactive pixel calibration to measure the exact µm/px ratio for their specific hardware. - -## Root Cause - -Two distinct issues prevented `process_pixel` from reaching the segmenter: - -### Issue 1: Naming mismatch between Node-RED and Python - -The Node-RED acquisition page sends the pixel size as **`process_pixel_size`** in the MQTT config. The Python segmenter expects **`process_pixel`**. These are different field names: - -- The calibration page sets `global("process_pixel")` — correct name, but only when user runs calibration -- The acquisition page magnification presets set `this.calibration_pixel_size` and send it as `process_pixel_size` — wrong name for the segmenter -- The `set acq_params` Node-RED function stored `process_pixel_size` in globals but never synced it to `process_pixel` -- When the user changed magnification on the acquisition page, the `process_pixel` global remained stale at whatever value was last set by calibration (or never set at all) - -### Issue 2: No fallback when both fields were missing - -If the user never ran calibration and the Node-RED globals contained neither field with the right name, `process_pixel` never reached `metadata.json` at all. The segmenter fell back to treating all measurements as raw pixels. - -### Data flow before the fix - -``` -Node-RED Acquisition Page Calibration Page - sends: process_pixel_size: 0.53 sets global("process_pixel") = 0.82 - | | - ▼ ▼ - set acq_params: set calibration_pixel_size: - global("process_pixel_size") = 0.53 global("process_pixel") = 0.82 - ✗ process_pixel NOT updated ✗ dead end (no wires out) - | | - ▼ ▼ - update_config gathers all process_* globals: - → process_pixel_size = 0.53 (current) - → process_pixel = 0.82 (stale from calibration, or absent) - | - ▼ - MQTT: imager/image/update_config - → config includes BOTH fields (or only process_pixel_size) - | - ▼ - Imager: self._metadata → metadata.json - → process_pixel = 0.82 (stale!) or absent - → segmenter uses wrong value or no value -``` - -### Cascade of failures per magnification - -| Flowcell | Pixel Size | Bug: ESD threshold | Effective ESD | Expected | -|---|---|---|---|---| -| 100µm (high) | 0.53 µm/px | 20 px (treated as pixels) | **10.6 µm** | 20 µm | -| 300µm (medium) | 0.75 µm/px | 20 px | **15.0 µm** | 20 µm | -| 500µm (low) | 1.34 µm/px | 20 px | **26.8 µm** (loses 20-27µm objects) | 20 µm | - -### Why 15µm instead of 20µm (the math) - -The segmenter's min ESD filter works like this: - -```python -# With process_pixel = 0.75: -min_esd_pixels = 20 / 0.75 = 26.67 pixels ← CORRECT (matches 20µm) - -# Without process_pixel (fallback): -min_esd_pixels = 20 ← treats µm value as pixels -# 20 pixels × 0.75 µm/px = 15µm effective threshold ← BUG -``` - -## Fix - -### Change 1: Use `process_pixel` from Node-RED calibration matrix - -**File:** `controller/imager/main.py` - -Node-RED resolves the correct pixel size from a per-preset calibration matrix (user calibration wins over factory default) and sends it as `process_pixel`. The imager simply uses it: - -```python -pixel_size = metadata.get("process_pixel") -if pixel_size is not None: - metadata["process_pixel"] = float(pixel_size) -else: - loguru.logger.warning("process_pixel missing from config — measurements will be in pixels") -``` - -**Key design decisions:** - -- **Node-RED is the resolver** — The calibration matrix (factory defaults + per-preset user calibrations) lives in Node-RED. Python is a consumer, not a resolver. -- **No fallback** — Backend and frontend are shipped together, so Node-RED always sends `process_pixel`. No need for `process_pixel_fixed` from `hardware.json`. -- **Stateless** — The resolution happens at metadata-creation time for each acquisition. No state accumulates across `update_config` calls. - -### Change 1b: Node-RED calibration matrix - -**File:** `node-red/projects/dashboard/flows.json` (changes made in flow editor) - -- **`set acq_params`** — stores `process_pixel` (resolved value) directly -- **`set calibration_pixel_size`** — writes user calibration to `calibration_matrix[preset].user_calibrated` in global context -- **Body template `applyMagChange()`** — resolves from calibration matrix: `user_calibrated ?? factory` -- **Body template `sendUpdate()`** — sends `process_pixel` (resolved) instead of `process_pixel_size` -- **`Get Global Variables`** — initializes `calibration_matrix` on first boot - -### Change 2: Update EcoTaxa archive naming convention - -**Files:** `segmenter/planktoscope/segmenter/__init__.py`, `segmenter/planktoscope/segmenter/ecotaxa.py` - -Archive and internal TSV filenames now follow the format: - -| Component | Old Format | New Format | -|---|---|---| -| ZIP archive | `ecotaxa_{acquisition}.zip` | `Ecotaxa_{project}_{acquisition}.zip` | -| TSV inside archive | `ecotaxa_{acquisition_id}.tsv` | `Ecotaxa_{project}_{acquisition_id}.tsv` | - -Both `project` (from `sample_project`) and `acquisition` (from `acq_id`) are sanitized by replacing spaces with underscores, consistent with existing behavior. - -## What did NOT need changing - -The segmenter's core logic was already correct — it just wasn't receiving the data it needed: - -- **ESD filtering** (`__init__.py:460-462`) — Correctly uses `region.equivalent_diameter_area` (derived from object area, NOT vignette/bounding box area) -- **Measurement scaling** (`__init__.py:334-399`) — Correctly multiplies linear measurements by `px` and area measurements by `px²` -- **Metadata filter** (`__init__.py:806-812`) — `process_pixel` starts with "process" and correctly passes the prefix filter -- **TSV export** (`ecotaxa.py:247-256`) — Correctly includes all global metadata fields in the output - -## Verification - -With `process_pixel` now correctly resolved for all magnifications: - -| Magnification | Pixel Size | Min ESD Threshold | Effective ESD | Correct? | -|---|---|---|---|---| -| high (100µm) | 0.53 µm/px | `20 / 0.53 = 37.7 px` | 20.0 µm | ✓ | -| medium (300µm) | 0.75 µm/px | `20 / 0.75 = 26.7 px` | 20.0 µm | ✓ | -| low (500µm) | 1.34 µm/px | `20 / 1.34 = 14.9 px` | 20.0 µm | ✓ | - -Additional verifications: - -| Step | Value | Correct? | -|---|---|---| -| Area measurement | `prop.area × pixel_size²` µm² | ✓ | -| Equivalent diameter | `prop.equivalent_diameter × pixel_size` µm | ✓ | -| `process_pixel` in TSV | Matches selected magnification | ✓ | -| ESD uses object area | `equivalent_diameter_area` (not bbox) | ✓ | -| Calibration page value | Flows through and overrides presets | ✓ | -| Magnification change | `process_pixel_size` overrides stale `process_pixel` | ✓ | - -### Change 3: Visualizer TSV file discovery - -**File:** `lib/db.js` - -The `getSegmentationFromPath()` function scans directories for EcoTaxa TSV files to populate the visualizer. It previously only matched the lowercase `ecotaxa_` prefix, so segmentations created with the new `Ecotaxa_` naming were invisible. - -```javascript -// Before -if (extension === ".tsv" && file.startsWith("ecotaxa_")) { - -// After — accepts both naming conventions -if (extension === ".tsv" && (file.startsWith("Ecotaxa_") || file.startsWith("ecotaxa_"))) { -``` - -This ensures backward compatibility with existing datasets while supporting the new naming format. - -### Change 4: Node-RED flow function patches - -**File:** `node-red/projects/dashboard/flows.json` (tracked in dashboard subrepo) - -Four function nodes in the Visualizer tab referenced hardcoded lowercase `ecotaxa_` patterns: - -#### "Get tsv path" (×2 nodes) - -The `expectedTsv` lookup now tries the new naming format first, falling back to legacy: - -```javascript -// Before -const expectedTsv = `${cleanPath}/ecotaxa_${acqId}.tsv`; - -// After — try new format first, then legacy -const newTsv = `${cleanPath}/Ecotaxa_${acqId}.tsv`; -const legacyTsv = `${cleanPath}/ecotaxa_${acqId}.tsv`; -const expectedTsv = fs.existsSync(newTsv) ? newTsv : legacyTsv; -``` - -The fallback `files.find()` also accepts both prefixes: - -```javascript -// Before -const tsvFile = files.find(f => f.startsWith('ecotaxa_') && f.endsWith('.tsv')); - -// After -const tsvFile = files.find(f => (f.startsWith('Ecotaxa_') || f.startsWith('ecotaxa_')) && f.endsWith('.tsv')); -``` - -#### "Insert export column" (×2 nodes) - -The export ZIP URL now includes the project name and uses the new prefix: - -```javascript -// Before -item.export = `/ps/data/browse/api/raw/export/ecotaxa/ecotaxa_${acq}.zip`; - -// After -const project = (item.project_name || '').replace(/ /g, '_'); -item.export = `/ps/data/browse/api/raw/export/ecotaxa/Ecotaxa_${project}_${acq}.zip`; -``` - -## Files Changed - -| File | Lines | Description | -|---|---|---| -| `controller/imager/main.py` | 179-187 | Use `process_pixel` from Node-RED; warn if missing | -| `segmenter/planktoscope/segmenter/__init__.py` | 855-858 | Update archive filename to `Ecotaxa_{project}_{acquisition}.zip` | -| `segmenter/planktoscope/segmenter/ecotaxa.py` | 272-275 | Update TSV filename to `Ecotaxa_{project}_{acquisition_id}.tsv` | -| `lib/db.js` | 192 | Accept both `Ecotaxa_` and `ecotaxa_` prefixes in visualizer TSV discovery | -| `lib/file-config.js` | 18-35 | Calibration file init consolidated into `initConfigFiles()` | -| `default-configs/calibration.json` | — | Per-preset factory defaults for the calibration matrix | -| `segmenter/tests/test_pixel_size.py` | — | Pytest tests for pixel size pipeline | -| `node-red/.../flows.json` | — | Calibration matrix + Ecotaxa_ naming (via flow editor) | - -## Backward Compatibility - -All file-discovery code (db.js, Node-RED flow functions) accepts **both** the new `Ecotaxa_` and legacy `ecotaxa_` prefixes. Existing datasets produced before this fix remain fully visible and functional. Only newly created archives and TSV files use the new naming convention. - -## Related Context - -- Three magnification presets exist: high (0.53), medium (0.75), low (1.34) — each corresponds to a different lens pair and flowcell thickness -- Per-preset calibration matrix in `calibration.json` stores factory defaults and optional user calibrations; see `docs/calibration-matrix.md` -- Users can override factory presets via the interactive pixel calibration page (per-preset, preserved across magnification switches) -- The segmenter's `process_min_ESD` defaults to 20µm (configurable via MQTT segment command's `settings.process_min_ESD`) -- The old filtering approach (comparing `acq_minimum_mesh` against `filled_area`) was previously replaced with the current ESD-based approach per the CHANGELOG diff --git a/justfile b/justfile index 96a4d40ad..4b402b717 100644 --- a/justfile +++ b/justfile @@ -73,9 +73,9 @@ developer-mode: setup-dev ./os/developer-mode/configure.mjs reset: base setup - rm /opt/PlanktoScope/config.json - rm /opt/PlanktoScope/hardware.json - rm /opt/PlanktoScope/calibration.json + rm /home/pi/config.json + rm /home/pi/hardware.json + rm /home/pi/calibration.json sudo reboot install-uv: diff --git a/lib/file-config.js b/lib/file-config.js index e2f564d3e..6f7e5fd75 100644 --- a/lib/file-config.js +++ b/lib/file-config.js @@ -1,9 +1,9 @@ import { rm, writeFile } from "fs/promises" import { readFile, access, constants, copyFile } from "fs/promises" -const HARDWARE_PATH = "/opt/PlanktoScope/hardware.json" -const SOFTWARE_PATH = "/opt/PlanktoScope/config.json" -const CALIBRATION_PATH = "/opt/PlanktoScope/calibration.json" +const HARDWARE_PATH = "/home/pi/hardware.json" +const SOFTWARE_PATH = "/home/pi/config.json" +const CALIBRATION_PATH = "/home/pi/calibration.json" const CALIBRATION_DEFAULTS_PATH = "/opt/PlanktoScope/default-configs/calibration.json" @@ -20,11 +20,11 @@ export async function initConfigFiles(hardware_version) { await Promise.all([ copyFile( `/opt/PlanktoScope/default-configs/${hardware_version}.config.json`, - "/opt/PlanktoScope/config.json", + "/home/pi/config.json", ), copyFile( `/opt/PlanktoScope/default-configs/${hardware_version}.hardware.json`, - "/opt/PlanktoScope/hardware.json", + "/home/pi/hardware.json", ), // Create calibration.json from defaults if it doesn't exist yet. // Unlike config/hardware, this never overwrites — preserving user calibrations. diff --git a/os/setup-ci.sh b/os/setup-ci.sh index f0ee2cc7b..8556fe0b4 100755 --- a/os/setup-ci.sh +++ b/os/setup-ci.sh @@ -7,10 +7,10 @@ export LANG="en_US.UTF-8" # The PlanktoScope monorepo is used for running and iterating on software components # https://github.com/fairscope/planktoscope -sudo cp -r "$build_scripts_root"/.. "$HOME/PlanktoScope" -sudo chown -R "$USER:$USER" "$HOME/PlanktoScope" +sudo cp -r "$build_scripts_root"/.. "/opt/PlanktoScope" +sudo chown -R "$USER:$USER" "/opt/PlanktoScope" sudo apt install -y just -just --justfile "$HOME"/PlanktoScope/justfile +just --justfile /opt/PlanktoScope/justfile ./postinstall.sh ./preimage.sh From 2c2dab1ca845d8c85b18296706d7391e9f28faa8 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 12:10:54 +0000 Subject: [PATCH 3/8] f --- os/raspberry/bootloader.sh | 2 ++ os/raspberry/firmware.sh | 2 ++ os/setup.sh | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/os/raspberry/bootloader.sh b/os/raspberry/bootloader.sh index 925f17323..8bafbd9fc 100755 --- a/os/raspberry/bootloader.sh +++ b/os/raspberry/bootloader.sh @@ -12,7 +12,9 @@ cp /usr/lib/firmware/raspberrypi/bootloader-2712/latest/recovery.bin /tmp rpi-eeprom-config /tmp/pieeprom-2025-11-05.bin --config boot.ini --out /tmp/pieeprom.upd rpi-eeprom-digest -i /tmp/pieeprom.upd -o /tmp/pieeprom.sig +sudo mount -o remount,rw /boot/firmware sudo cp /tmp/pieeprom.upd /tmp/pieeprom.sig /tmp/recovery.bin /boot/firmware/ +sudo mount -o remount,ro /boot/firmware # The bootloader will be installed on first boot and the files removed # see https://github.com/fairscope/PlanktoScope/pull/589 diff --git a/os/raspberry/firmware.sh b/os/raspberry/firmware.sh index ad1299706..011d5b91a 100755 --- a/os/raspberry/firmware.sh +++ b/os/raspberry/firmware.sh @@ -2,7 +2,9 @@ # Configure firmware # https://www.raspberrypi.com/documentation/computers/config_txt.html +sudo mount -o remount,rw /boot/firmware sudo bash -c "cat \"config.ini\" >> \"/boot/firmware/config.txt\"" +sudo mount -o remount,ro /boot/firmware # Disable the 4 Raspberry logo in the top left corner # more space for kernel and system logs diff --git a/os/setup.sh b/os/setup.sh index 48dbe8d45..b455bcfa7 100755 --- a/os/setup.sh +++ b/os/setup.sh @@ -12,11 +12,13 @@ if [ "$line" != "$expected" ]; then exit 1 fi -cd /home/pi sudo apt install -y git just +cd /opt if cd PlanktoScope; then git pull else + sudo mkdir PlanktoScope + sudo chown pi:pi PlanktoScope git clone https://github.com/fairscope/PlanktoScope.git cd PlanktoScope fi From 3a6024b2d987cd1c1d1117114c80178e91177727 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 12:21:09 +0000 Subject: [PATCH 4/8] f --- documentation/docs/community/contribute/tips-and-tricks.md | 2 +- node-red/justfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/community/contribute/tips-and-tricks.md b/documentation/docs/community/contribute/tips-and-tricks.md index 7da9eea3c..3ac5ebb42 100644 --- a/documentation/docs/community/contribute/tips-and-tricks.md +++ b/documentation/docs/community/contribute/tips-and-tricks.md @@ -99,7 +99,7 @@ You can now SSH into your PlanktoScope without username / password (using `ssh $ On the PlanktoScope ```sh -cd ~/PlanktoScope +cd /opt/PlanktoScope just developer-mode git checkout main git status diff --git a/node-red/justfile b/node-red/justfile index aa547c05d..59334c64d 100644 --- a/node-red/justfile +++ b/node-red/justfile @@ -16,7 +16,7 @@ setup-dev: dev: -sudo systemctl stop nodered - node --watch-path ~/PlanktoScope/node-red/nodes/ ~/.local/bin/node-red --settings ~/PlanktoScope/node-red/settings.cjs + node --watch-path /opt/PlanktoScope/node-red/nodes/ ~/.local/bin/node-red --settings /opt/PlanktoScope/node-red/settings.cjs test: validator projects/dashboard/flows.json From e083a726be885c105583c6e22ade1995db101c52 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 12:22:50 +0000 Subject: [PATCH 5/8] f --- docs/calibration-matrix.md | 189 +++++++++++++++++++++++ docs/fix-pixel-size-management.md | 244 ++++++++++++++++++++++++++++++ 2 files changed, 433 insertions(+) create mode 100644 docs/calibration-matrix.md create mode 100644 docs/fix-pixel-size-management.md diff --git a/docs/calibration-matrix.md b/docs/calibration-matrix.md new file mode 100644 index 000000000..d7aeb3a77 --- /dev/null +++ b/docs/calibration-matrix.md @@ -0,0 +1,189 @@ +# Per-Preset Calibration Matrix + +## Problem + +The original pixel size management used a single global `process_pixel` value. When a user switched magnification presets, the factory default for the new preset overwrote any user calibration. A stage-micrometer calibration (the gold standard in microscopy) could be silently discarded. + +| Scenario | Expected | Old behavior | +|---|---|---| +| User calibrates medium optics to 0.762 µm/px, starts acquisition on "medium" | Uses 0.762 | Uses 0.75 (factory overwrites calibration) | +| User calibrates high, switches to medium, switches back to high | Restores high calibration | Uses 0.53 (factory value, calibration lost) | + +## Solution + +Each magnification preset (high/medium/low) stores both its factory default and an optional user-calibrated override. The active pixel size is always: **user calibration if it exists for that preset, otherwise factory default**. + +``` +calibration_matrix = { + "high": { "factory": 0.53, "user_calibrated": null }, + "medium": { "factory": 0.75, "user_calibrated": null }, + "low": { "factory": 1.34, "user_calibrated": null } +} +``` + +Resolution happens in Node-RED before sending to Python. Python is a consumer, not a resolver. + +## Data Flow + +``` +User selects preset or completes calibration + │ + ▼ +┌──────────────────────────┐ +│ Node-RED (body template)│ +│ │ +│ calibrationMatrix │ +│ resolves: user_cal ?? │ +│ factory default │ +│ │ +│ sends process_pixel │ +│ via MQTT │ +└──────────┬───────────────┘ + │ MQTT: imager/image/update_config + ▼ +┌──────────────────────────┐ +│ Python Imager │ +│ (controller/imager/ │ +│ main.py) │ +│ │ +│ Uses process_pixel │ +│ directly. Fallback: │ +│ process_pixel_fixed │ +│ from hardware.json │ +│ │ +│ Writes → metadata.json │ +└──────────┬───────────────┘ + │ + ▼ +┌──────────────────────────┐ +│ Segmenter │ +│ (__init__.py) │ +│ │ +│ Reads process_pixel │ +│ from metadata.json │ +│ │ +│ • ESD threshold (µm→px) │ +│ • Measurement scaling │ +│ • Writes to TSV │ +└──────────┬───────────────┘ + │ + ▼ +┌──────────────────────────┐ +│ Gallery / Explorer │ +│ (Node-RED templates) │ +│ │ +│ Reads process_pixel │ +│ from TSV column │ +│ → meta.resolution │ +│ │ +│ • Scale bar rendering │ +│ • Thumbnail sizing │ +│ • Measurement display │ +└──────────────────────────┘ +``` + +## Files Changed + +### Python + +#### `controller/imager/main.py` (lines 179–193) + +**Before:** 3-tier resolution cascade: +1. `process_pixel_size` (preset from acquisition page) — wins +2. `process_pixel` (from calibration page) — loses if preset set +3. `process_pixel_fixed` (hardware.json fallback) + +**After:** Node-RED always sends the resolved value. Python just uses it: + +```python +pixel_size = metadata.get("process_pixel") +if pixel_size is not None: + metadata["process_pixel"] = float(pixel_size) +else: + loguru.logger.warning("process_pixel missing from config — measurements will be in pixels") +``` + +### JavaScript + +#### `lib/file-config.js` + +Added: +- `CALIBRATION_PATH` — `/home/pi/PlanktoScope/calibration.json` +- `readCalibrationConfig()` — read the calibration matrix +- `updateCalibrationConfig()` — update it +- `hasCalibrationConfig()` — check existence +- Calibration file init moved into `initConfigFiles()` with copy-if-missing semantics (never overwrites user calibrations) + +#### `default-configs/calibration.json` + +Factory defaults for the calibration matrix. Created during hardware setup if it doesn't already exist. + +### Node-RED Flow Changes + +Changes made directly in the Node-RED flow editor. + +#### `Get Global Variables` (id: `31fab063b7078fe6`) + +Added initialization of `calibration_matrix` in persistent global context on first boot. If the global doesn't exist, creates it with factory defaults. This matrix is then sent to the body template along with all other globals when the acquisition page loads. + +#### `set calibration_pixel_size` (id: `133c27ef75317205`) + +**Before:** Wrote calibration result to three bare globals (`process_pixel`, `process_pixel_size`, `calibration_pixel_size`). + +**After:** Reads `acq_magnification` from global context to determine current preset, then writes the calibrated value to `calibration_matrix[preset].user_calibrated` in persistent global context. Still writes legacy globals for backward compatibility. Logs which preset was calibrated. + +#### `set acq_params` (id: `74aa092817adc6f0`) + +**Before:** Stored `process_pixel_size` in global context and synced it to `process_pixel` via a manual hack. + +**After:** Stores `process_pixel` directly (the resolved value from the calibration matrix). Removed `process_pixel_size` from the key list. Removed the sync hack. + +#### Body template (id: `79bccc0355eb5d87`) + +**`data()` section:** +Added `calibrationMatrix` property initialized with factory defaults. + +**Message watcher:** +- Loads `calibration_matrix` from incoming globals into `this.calibrationMatrix` +- Changed `process_pixel_size` handler to `process_pixel` + +**`applyMagChange()` method:** +Instead of always setting `this.calibration_pixel_size = config.pixel_size` (factory), now resolves from the calibration matrix: +```javascript +const calEntry = this.calibrationMatrix[this.currentMag]; +this.calibration_pixel_size = (calEntry && calEntry.user_calibrated !== null) + ? calEntry.user_calibrated + : config.pixel_size; +``` + +**`sendSettings()` method:** +Sends `process_pixel` (resolved value) instead of `process_pixel_size` (factory value). + +### Unchanged + +- **Segmenter** (`segmenter/planktoscope/segmenter/__init__.py`) — reads `process_pixel` from metadata.json as before. ESD filtering, measurement scaling, and metadata prefix filtering are unchanged. +- **EcoTaxa export** (`ecotaxa.py`) — unchanged, uses scaled values from segmenter. +- **Gallery/Explorer function nodes** — unchanged. They parse `process_pixel` from TSV columns, not from live MQTT globals. +- **Gallery/Explorer UI templates** — unchanged. Scale bar rendering (`getScaleBar`, `adaptiveScaleMicrons`, `scaleBarPercent`, `displayPx`) reads `meta.resolution` which comes from TSV parsing. + +## MQTT Contract + +| Field | Before | After | +|---|---|---| +| `process_pixel_size` | Sent by acquisition page (factory preset value) | No longer sent | +| `process_pixel` | Set by calibration page via global context | Resolved value from calibration matrix | + +## Backward Compatibility + +**Existing installations without `calibration.json`:** `Get Global Variables` initializes `calibration_matrix` in global context with factory defaults on first boot. `initConfigFiles()` creates the JSON file from defaults during hardware setup without overwriting existing calibrations. + +## Scale Bar Impact + +The Gallery and Explorer scale bars read `process_pixel` from the EcoTaxa TSV file at display time — not from live MQTT globals. The data path is: + +1. `process_pixel` written to `metadata.json` during acquisition +2. Segmenter reads it, writes it to TSV as a column +3. Gallery/Explorer function nodes parse TSV → `meta.resolution` +4. UI templates use `meta.resolution` for `getScaleBar()`, `displayPx()`, `scaleBarPercent()` + +Since `process_pixel` still reaches metadata.json with the correct value, scale bars are unaffected. diff --git a/docs/fix-pixel-size-management.md b/docs/fix-pixel-size-management.md new file mode 100644 index 000000000..f4f70ad64 --- /dev/null +++ b/docs/fix-pixel-size-management.md @@ -0,0 +1,244 @@ +# Fix: Pixel Size Management System + +## Problem Statement + +Users reported three related issues when using different flowcell sizes (100µm, 300µm, 500µm): + +1. **Wrong ESD filtering thresholds** — The segmenter was not respecting the 20µm minimum ESD target. The 300µm flowcell filtered at ~15µm instead of 20µm; the 100µm and 500µm flowcells had similarly incorrect cutoffs. +2. **Pixel size missing from TSV output** — All exported EcoTaxa TSV files had the same (absent) pixel size, regardless of configuration. Downstream biovolume and size calculations were therefore incorrect. +3. **Measurements in pixels instead of µm** — All morphological measurements (area, major/minor axis, equivalent diameter, etc.) were exported in raw pixel units, not physical units. + +## Optical Configurations + +The PlanktoScope supports three magnification presets, each with a different lens configuration and pixel-to-µm ratio: + +| Magnification | Tube Lens | Objective | Pixel Size | Flowcell | Min ESD (20µm) | +|---|---|---|---|---|---| +| **high** | 35mm F2.4 | 12mm F2 | **0.53 µm/px** | 100µm | 37.7 px | +| **medium** | 25mm F1.8 | 12mm F1.8 | **0.75 µm/px** | 300µm | 26.7 px | +| **low** | 25mm F2.0 | 21.8mm F2.8 | **1.34 µm/px** | 500µm | 14.9 px | + +Users can also run interactive pixel calibration to measure the exact µm/px ratio for their specific hardware. + +## Root Cause + +Two distinct issues prevented `process_pixel` from reaching the segmenter: + +### Issue 1: Naming mismatch between Node-RED and Python + +The Node-RED acquisition page sends the pixel size as **`process_pixel_size`** in the MQTT config. The Python segmenter expects **`process_pixel`**. These are different field names: + +- The calibration page sets `global("process_pixel")` — correct name, but only when user runs calibration +- The acquisition page magnification presets set `this.calibration_pixel_size` and send it as `process_pixel_size` — wrong name for the segmenter +- The `set acq_params` Node-RED function stored `process_pixel_size` in globals but never synced it to `process_pixel` +- When the user changed magnification on the acquisition page, the `process_pixel` global remained stale at whatever value was last set by calibration (or never set at all) + +### Issue 2: No fallback when both fields were missing + +If the user never ran calibration and the Node-RED globals contained neither field with the right name, `process_pixel` never reached `metadata.json` at all. The segmenter fell back to treating all measurements as raw pixels. + +### Data flow before the fix + +``` +Node-RED Acquisition Page Calibration Page + sends: process_pixel_size: 0.53 sets global("process_pixel") = 0.82 + | | + ▼ ▼ + set acq_params: set calibration_pixel_size: + global("process_pixel_size") = 0.53 global("process_pixel") = 0.82 + ✗ process_pixel NOT updated ✗ dead end (no wires out) + | | + ▼ ▼ + update_config gathers all process_* globals: + → process_pixel_size = 0.53 (current) + → process_pixel = 0.82 (stale from calibration, or absent) + | + ▼ + MQTT: imager/image/update_config + → config includes BOTH fields (or only process_pixel_size) + | + ▼ + Imager: self._metadata → metadata.json + → process_pixel = 0.82 (stale!) or absent + → segmenter uses wrong value or no value +``` + +### Cascade of failures per magnification + +| Flowcell | Pixel Size | Bug: ESD threshold | Effective ESD | Expected | +|---|---|---|---|---| +| 100µm (high) | 0.53 µm/px | 20 px (treated as pixels) | **10.6 µm** | 20 µm | +| 300µm (medium) | 0.75 µm/px | 20 px | **15.0 µm** | 20 µm | +| 500µm (low) | 1.34 µm/px | 20 px | **26.8 µm** (loses 20-27µm objects) | 20 µm | + +### Why 15µm instead of 20µm (the math) + +The segmenter's min ESD filter works like this: + +```python +# With process_pixel = 0.75: +min_esd_pixels = 20 / 0.75 = 26.67 pixels ← CORRECT (matches 20µm) + +# Without process_pixel (fallback): +min_esd_pixels = 20 ← treats µm value as pixels +# 20 pixels × 0.75 µm/px = 15µm effective threshold ← BUG +``` + +## Fix + +### Change 1: Use `process_pixel` from Node-RED calibration matrix + +**File:** `controller/imager/main.py` + +Node-RED resolves the correct pixel size from a per-preset calibration matrix (user calibration wins over factory default) and sends it as `process_pixel`. The imager simply uses it: + +```python +pixel_size = metadata.get("process_pixel") +if pixel_size is not None: + metadata["process_pixel"] = float(pixel_size) +else: + loguru.logger.warning("process_pixel missing from config — measurements will be in pixels") +``` + +**Key design decisions:** + +- **Node-RED is the resolver** — The calibration matrix (factory defaults + per-preset user calibrations) lives in Node-RED. Python is a consumer, not a resolver. +- **No fallback** — Backend and frontend are shipped together, so Node-RED always sends `process_pixel`. No need for `process_pixel_fixed` from `hardware.json`. +- **Stateless** — The resolution happens at metadata-creation time for each acquisition. No state accumulates across `update_config` calls. + +### Change 1b: Node-RED calibration matrix + +**File:** `node-red/projects/dashboard/flows.json` (changes made in flow editor) + +- **`set acq_params`** — stores `process_pixel` (resolved value) directly +- **`set calibration_pixel_size`** — writes user calibration to `calibration_matrix[preset].user_calibrated` in global context +- **Body template `applyMagChange()`** — resolves from calibration matrix: `user_calibrated ?? factory` +- **Body template `sendUpdate()`** — sends `process_pixel` (resolved) instead of `process_pixel_size` +- **`Get Global Variables`** — initializes `calibration_matrix` on first boot + +### Change 2: Update EcoTaxa archive naming convention + +**Files:** `segmenter/planktoscope/segmenter/__init__.py`, `segmenter/planktoscope/segmenter/ecotaxa.py` + +Archive and internal TSV filenames now follow the format: + +| Component | Old Format | New Format | +|---|---|---| +| ZIP archive | `ecotaxa_{acquisition}.zip` | `Ecotaxa_{project}_{acquisition}.zip` | +| TSV inside archive | `ecotaxa_{acquisition_id}.tsv` | `Ecotaxa_{project}_{acquisition_id}.tsv` | + +Both `project` (from `sample_project`) and `acquisition` (from `acq_id`) are sanitized by replacing spaces with underscores, consistent with existing behavior. + +## What did NOT need changing + +The segmenter's core logic was already correct — it just wasn't receiving the data it needed: + +- **ESD filtering** (`__init__.py:460-462`) — Correctly uses `region.equivalent_diameter_area` (derived from object area, NOT vignette/bounding box area) +- **Measurement scaling** (`__init__.py:334-399`) — Correctly multiplies linear measurements by `px` and area measurements by `px²` +- **Metadata filter** (`__init__.py:806-812`) — `process_pixel` starts with "process" and correctly passes the prefix filter +- **TSV export** (`ecotaxa.py:247-256`) — Correctly includes all global metadata fields in the output + +## Verification + +With `process_pixel` now correctly resolved for all magnifications: + +| Magnification | Pixel Size | Min ESD Threshold | Effective ESD | Correct? | +|---|---|---|---|---| +| high (100µm) | 0.53 µm/px | `20 / 0.53 = 37.7 px` | 20.0 µm | ✓ | +| medium (300µm) | 0.75 µm/px | `20 / 0.75 = 26.7 px` | 20.0 µm | ✓ | +| low (500µm) | 1.34 µm/px | `20 / 1.34 = 14.9 px` | 20.0 µm | ✓ | + +Additional verifications: + +| Step | Value | Correct? | +|---|---|---| +| Area measurement | `prop.area × pixel_size²` µm² | ✓ | +| Equivalent diameter | `prop.equivalent_diameter × pixel_size` µm | ✓ | +| `process_pixel` in TSV | Matches selected magnification | ✓ | +| ESD uses object area | `equivalent_diameter_area` (not bbox) | ✓ | +| Calibration page value | Flows through and overrides presets | ✓ | +| Magnification change | `process_pixel_size` overrides stale `process_pixel` | ✓ | + +### Change 3: Visualizer TSV file discovery + +**File:** `lib/db.js` + +The `getSegmentationFromPath()` function scans directories for EcoTaxa TSV files to populate the visualizer. It previously only matched the lowercase `ecotaxa_` prefix, so segmentations created with the new `Ecotaxa_` naming were invisible. + +```javascript +// Before +if (extension === ".tsv" && file.startsWith("ecotaxa_")) { + +// After — accepts both naming conventions +if (extension === ".tsv" && (file.startsWith("Ecotaxa_") || file.startsWith("ecotaxa_"))) { +``` + +This ensures backward compatibility with existing datasets while supporting the new naming format. + +### Change 4: Node-RED flow function patches + +**File:** `node-red/projects/dashboard/flows.json` (tracked in dashboard subrepo) + +Four function nodes in the Visualizer tab referenced hardcoded lowercase `ecotaxa_` patterns: + +#### "Get tsv path" (×2 nodes) + +The `expectedTsv` lookup now tries the new naming format first, falling back to legacy: + +```javascript +// Before +const expectedTsv = `${cleanPath}/ecotaxa_${acqId}.tsv`; + +// After — try new format first, then legacy +const newTsv = `${cleanPath}/Ecotaxa_${acqId}.tsv`; +const legacyTsv = `${cleanPath}/ecotaxa_${acqId}.tsv`; +const expectedTsv = fs.existsSync(newTsv) ? newTsv : legacyTsv; +``` + +The fallback `files.find()` also accepts both prefixes: + +```javascript +// Before +const tsvFile = files.find(f => f.startsWith('ecotaxa_') && f.endsWith('.tsv')); + +// After +const tsvFile = files.find(f => (f.startsWith('Ecotaxa_') || f.startsWith('ecotaxa_')) && f.endsWith('.tsv')); +``` + +#### "Insert export column" (×2 nodes) + +The export ZIP URL now includes the project name and uses the new prefix: + +```javascript +// Before +item.export = `/ps/data/browse/api/raw/export/ecotaxa/ecotaxa_${acq}.zip`; + +// After +const project = (item.project_name || '').replace(/ /g, '_'); +item.export = `/ps/data/browse/api/raw/export/ecotaxa/Ecotaxa_${project}_${acq}.zip`; +``` + +## Files Changed + +| File | Lines | Description | +|---|---|---| +| `controller/imager/main.py` | 179-187 | Use `process_pixel` from Node-RED; warn if missing | +| `segmenter/planktoscope/segmenter/__init__.py` | 855-858 | Update archive filename to `Ecotaxa_{project}_{acquisition}.zip` | +| `segmenter/planktoscope/segmenter/ecotaxa.py` | 272-275 | Update TSV filename to `Ecotaxa_{project}_{acquisition_id}.tsv` | +| `lib/db.js` | 192 | Accept both `Ecotaxa_` and `ecotaxa_` prefixes in visualizer TSV discovery | +| `lib/file-config.js` | 18-35 | Calibration file init consolidated into `initConfigFiles()` | +| `default-configs/calibration.json` | — | Per-preset factory defaults for the calibration matrix | +| `segmenter/tests/test_pixel_size.py` | — | Pytest tests for pixel size pipeline | +| `node-red/.../flows.json` | — | Calibration matrix + Ecotaxa_ naming (via flow editor) | + +## Backward Compatibility + +All file-discovery code (db.js, Node-RED flow functions) accepts **both** the new `Ecotaxa_` and legacy `ecotaxa_` prefixes. Existing datasets produced before this fix remain fully visible and functional. Only newly created archives and TSV files use the new naming convention. + +## Related Context + +- Three magnification presets exist: high (0.53), medium (0.75), low (1.34) — each corresponds to a different lens pair and flowcell thickness +- Per-preset calibration matrix in `calibration.json` stores factory defaults and optional user calibrations; see `docs/calibration-matrix.md` +- Users can override factory presets via the interactive pixel calibration page (per-preset, preserved across magnification switches) +- The segmenter's `process_min_ESD` defaults to 20µm (configurable via MQTT segment command's `settings.process_min_ESD`) +- The old filtering approach (comparing `acq_minimum_mesh` against `filled_area`) was previously replaced with the current ESD-based approach per the CHANGELOG From a7510b62fc0b1caa04a4db20ad8777724c54c607 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 12:28:57 +0000 Subject: [PATCH 6/8] f --- justfile | 6 +++--- lib/file-config.js | 14 ++++++++------ os/preimage.sh | 3 ++- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/justfile b/justfile index 4b402b717..0200a7f40 100644 --- a/justfile +++ b/justfile @@ -73,9 +73,9 @@ developer-mode: setup-dev ./os/developer-mode/configure.mjs reset: base setup - rm /home/pi/config.json - rm /home/pi/hardware.json - rm /home/pi/calibration.json + rm /home/pi/PlanktoScope/config.json + rm /home/pi/PlanktoScope/hardware.json + rm /home/pi/PlanktoScope/calibration.json sudo reboot install-uv: diff --git a/lib/file-config.js b/lib/file-config.js index 6f7e5fd75..57d2abd67 100644 --- a/lib/file-config.js +++ b/lib/file-config.js @@ -1,9 +1,9 @@ -import { rm, writeFile } from "fs/promises" +import { rm, writeFile, mkdir } from "fs/promises" import { readFile, access, constants, copyFile } from "fs/promises" -const HARDWARE_PATH = "/home/pi/hardware.json" -const SOFTWARE_PATH = "/home/pi/config.json" -const CALIBRATION_PATH = "/home/pi/calibration.json" +const HARDWARE_PATH = "/home/pi/PlanktoScope/hardware.json" +const SOFTWARE_PATH = "/home/pi/PlanktoScope/config.json" +const CALIBRATION_PATH = "/home/pi/PlanktoScope/calibration.json" const CALIBRATION_DEFAULTS_PATH = "/opt/PlanktoScope/default-configs/calibration.json" @@ -17,14 +17,16 @@ async function hasConfig(path) { } export async function initConfigFiles(hardware_version) { + await mkdir("/home/pi/PlanktoScope") + await Promise.all([ copyFile( `/opt/PlanktoScope/default-configs/${hardware_version}.config.json`, - "/home/pi/config.json", + SOFTWARE_PATH, ), copyFile( `/opt/PlanktoScope/default-configs/${hardware_version}.hardware.json`, - "/home/pi/hardware.json", + HARDWARE_PATH, ), // Create calibration.json from defaults if it doesn't exist yet. // Unlike config/hardware, this never overwrites — preserving user calibrations. diff --git a/os/preimage.sh b/os/preimage.sh index 9ec913c28..8901ba4a4 100755 --- a/os/preimage.sh +++ b/os/preimage.sh @@ -18,8 +18,9 @@ rm -f "$HOME"/.gitconfig rm -rf "$HOME"/.ssh rm -rf "$HOME"/data rm -f "$HOME"/filebrowser.db -rm -f "$HOME"/planktoScope/hardware.json +rm -f "$HOME"/PlanktoScope/hardware.json rm -f "$HOME"/PlanktoScope/config.json +rm -f "$HOME"/PlanktoScope/calibartion.json # Clear machine-id so that it will be regenerated on the next boot # This is also the condition for ConditionFirstBoot=yes From 1ebe4c8d6a051efd7c94c76d0cdd722fc341c3b4 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 13:01:22 +0000 Subject: [PATCH 7/8] f --- node-red/projects/dashboard | 2 +- os/image/README.md | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/node-red/projects/dashboard b/node-red/projects/dashboard index 1f8c7f024..539a102f1 160000 --- a/node-red/projects/dashboard +++ b/node-red/projects/dashboard @@ -1 +1 @@ -Subproject commit 1f8c7f0249611621028b8fa3c6e643f409ad44a8 +Subproject commit 539a102f133da23d61aac34bf8e248721bd9cc3d diff --git a/os/image/README.md b/os/image/README.md index c815bdc50..8af3c7182 100644 --- a/os/image/README.md +++ b/os/image/README.md @@ -4,9 +4,6 @@ This folder contains scripts and documentation to build the PlanktoScope OS imag The scripts should work on standard Linux installations, in case of doubt use Raspberry Pi OS. - - - ## How to use ### Bootstrap Raspberry Pi OS From e4c754bd1a2550382b53aef7c353b0c26abc0fed Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 14:25:43 +0000 Subject: [PATCH 8/8] f --- os/raspberry/firmware.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/os/raspberry/firmware.sh b/os/raspberry/firmware.sh index 011d5b91a..3b2009bc5 100755 --- a/os/raspberry/firmware.sh +++ b/os/raspberry/firmware.sh @@ -1,10 +1,10 @@ #!/bin/bash -eux +sudo mount -o remount,rw /boot/firmware + # Configure firmware # https://www.raspberrypi.com/documentation/computers/config_txt.html -sudo mount -o remount,rw /boot/firmware sudo bash -c "cat \"config.ini\" >> \"/boot/firmware/config.txt\"" -sudo mount -o remount,ro /boot/firmware # Disable the 4 Raspberry logo in the top left corner # more space for kernel and system logs @@ -19,3 +19,5 @@ sudo mount -o remount,ro /boot/firmware [ ! -f /boot/firmware/cmdline.txt ] || \ grep -qw logo.nologo /boot/firmware/cmdline.txt || \ sudo sed -i 's/$/ logo.nologo/' /boot/firmware/cmdline.txt + +sudo mount -o remount,ro /boot/firmware