From edd502700dbfeecada2107f0d911e4803af251b4 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 08:09:31 +0000 Subject: [PATCH 1/8] Revert "controller: Adjust bubbler (#872)" This reverts commit b7c37db135e5b3ff62f7080184be0f918d8479b3. --- controller/bubbler/main.py | 82 ++++++++++++++------------------------ 1 file changed, 30 insertions(+), 52 deletions(-) diff --git a/controller/bubbler/main.py b/controller/bubbler/main.py index ddcad6efd..0cf37bdf1 100644 --- a/controller/bubbler/main.py +++ b/controller/bubbler/main.py @@ -1,9 +1,7 @@ import asyncio import json -import math import signal import sys -import time from pprint import pprint import aiomqtt # type: ignore @@ -15,24 +13,13 @@ bubbler = None # ============== ADJUST THESE VALUES ============== -# La moyenne exacte entre ton Peak (0.275) et ta Valley (0.265) -OSCILLATION_AVERAGE = 0.264 - -# L'amplitude pour atteindre exactement tes limites -# 0.270 + 0.005 = 0.275 (Peak) -# 0.270 - 0.005 = 0.265 (Valley) -OSCILLATION_AMPLITUDE = 0.005 - -# Vitesse de l'oscillation en Hertz (cycles par seconde). -# 5.0 correspond à ton ancien cycle de 0.2 seconde. -OSCILLATION_FREQUENCY = 4 - -# Fréquence de mise à jour du DAC (en secondes). -# 0.01 offre une courbe très fluide à 100fps. -UPDATE_INTERVAL = 0.02 +# These can probably be improved, but it works nicely around 2 bubbles a second. +RAMP_VALUES = [0.275, 0.265, 0.265, 0.275] # Current levels to ramp through +RAMP_STEP_TIME = 0.1 # Seconds at each level +PAUSE_TIME = 0.2 # Seconds to pause (off) between cycles # ================================================= -oscillation_task = None +ramp_task = None async def start() -> None: @@ -103,60 +90,51 @@ async def handle_action(action: str, payload) -> None: async def on() -> None: - global oscillation_task + global ramp_task assert bubbler is not None - if oscillation_task: - oscillation_task.cancel() + if ramp_task: + ramp_task.cancel() try: - await oscillation_task + await ramp_task except asyncio.CancelledError: pass - oscillation_task = None + ramp_task = None - # Kick-start: high intensity to prime the pump - bubbler.set_value(0.275) - await asyncio.sleep(2) - - # Start oscillation mode - oscillation_task = asyncio.create_task(run_oscillate()) + # Start ramp mode + ramp_task = asyncio.create_task(run_ramp()) await publish_status() -async def run_oscillate(): - """Smoothly oscillate current using a sine wave to pulse the bubbler.""" +async def run_ramp(): + """Ramp up through values at the top, ramp down, pause, repeat.""" assert bubbler is not None - start_time = time.time() try: while True: - # Calculate elapsed time - t = time.time() - start_time - - # Generate sine wave value between -1 and 1 - sine_wave = math.sin(2 * math.pi * OSCILLATION_FREQUENCY * t) - - # Scale to our desired amplitude and shift to our average - current_val = OSCILLATION_AVERAGE + (OSCILLATION_AMPLITUDE * sine_wave) - - # Safety clamp to ensure we never pass invalid values to the DAC - current_val = max(0.0, min(1.0, current_val)) - - bubbler.set_value(current_val) - - await asyncio.sleep(UPDATE_INTERVAL) + # Ramp up + for v in RAMP_VALUES: + bubbler.set_value(v) + await asyncio.sleep(RAMP_STEP_TIME) + # Ramp down + for v in reversed(RAMP_VALUES[:-1]): + bubbler.set_value(v) + await asyncio.sleep(RAMP_STEP_TIME) + # Off and pause + bubbler.set_value(0) + await asyncio.sleep(PAUSE_TIME) except asyncio.CancelledError: pass async def off() -> None: - global oscillation_task - if oscillation_task: - oscillation_task.cancel() + global ramp_task + if ramp_task: + ramp_task.cancel() try: - await oscillation_task + await ramp_task except asyncio.CancelledError: pass - oscillation_task = None + ramp_task = None assert bubbler is not None bubbler.off() await publish_status() From 05fc712d84b409f6f8db8f3c69fb1293055d4745 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 29 Apr 2026 08:11:04 +0000 Subject: [PATCH 2/8] Revert "controller: Use ramp mode for Bubbler (#864)" This reverts commit 7e7737b7405dec1e1a609be2306294bd0ae49460. --- controller/bubbler/README.md | 4 ++ controller/bubbler/main.py | 58 +++++----------------------- controller/bubbler/test.js | 2 +- frontend/src/pages/preview/index.jsx | 31 ++++++--------- 4 files changed, 26 insertions(+), 69 deletions(-) diff --git a/controller/bubbler/README.md b/controller/bubbler/README.md index cb6199b37..aa29578e7 100644 --- a/controller/bubbler/README.md +++ b/controller/bubbler/README.md @@ -31,6 +31,9 @@ just dev ```json { "action": "on", + // float between 0 and 1 + // 0 is same as "action": "off" + "value": 0.5, } ``` @@ -53,6 +56,7 @@ just dev ```json { "status": "On", + "value": 0.5, } ``` diff --git a/controller/bubbler/main.py b/controller/bubbler/main.py index 0cf37bdf1..dcfc536b1 100644 --- a/controller/bubbler/main.py +++ b/controller/bubbler/main.py @@ -12,15 +12,6 @@ loop = asyncio.new_event_loop() bubbler = None -# ============== ADJUST THESE VALUES ============== -# These can probably be improved, but it works nicely around 2 bubbles a second. -RAMP_VALUES = [0.275, 0.265, 0.265, 0.275] # Current levels to ramp through -RAMP_STEP_TIME = 0.1 # Seconds at each level -PAUSE_TIME = 0.2 # Seconds to pause (off) between cycles -# ================================================= - -ramp_task = None - async def start() -> None: # There is no GPIO bubbler on PlanktoScope HAT < 3.3 @@ -68,7 +59,7 @@ async def handle_action(action: str, payload) -> None: assert bubbler is not None if action == "on": - await on() + await on(payload) elif action == "off": await off() # elif action == "settings": @@ -89,52 +80,21 @@ async def handle_action(action: str, payload) -> None: # bubbler.set_current(current) -async def on() -> None: - global ramp_task +async def on(payload) -> None: assert bubbler is not None + value = payload.get("value", 1) + assert 0.0 <= value <= 1.0 - if ramp_task: - ramp_task.cancel() - try: - await ramp_task - except asyncio.CancelledError: - pass - ramp_task = None - - # Start ramp mode - ramp_task = asyncio.create_task(run_ramp()) - await publish_status() + if value == 0: + await off() + return + bubbler.set_value(value) -async def run_ramp(): - """Ramp up through values at the top, ramp down, pause, repeat.""" - assert bubbler is not None - try: - while True: - # Ramp up - for v in RAMP_VALUES: - bubbler.set_value(v) - await asyncio.sleep(RAMP_STEP_TIME) - # Ramp down - for v in reversed(RAMP_VALUES[:-1]): - bubbler.set_value(v) - await asyncio.sleep(RAMP_STEP_TIME) - # Off and pause - bubbler.set_value(0) - await asyncio.sleep(PAUSE_TIME) - except asyncio.CancelledError: - pass + await publish_status() async def off() -> None: - global ramp_task - if ramp_task: - ramp_task.cancel() - try: - await ramp_task - except asyncio.CancelledError: - pass - ramp_task = None assert bubbler is not None bubbler.off() await publish_status() diff --git a/controller/bubbler/test.js b/controller/bubbler/test.js index d9f2d56d1..29e9208d3 100644 --- a/controller/bubbler/test.js +++ b/controller/bubbler/test.js @@ -9,6 +9,6 @@ watch("status/bubbler").then(async (messages) => { await startBubbler() -await setTimeout(4000) +await setTimeout(2000) await stopBubbler() diff --git a/frontend/src/pages/preview/index.jsx b/frontend/src/pages/preview/index.jsx index 3ecedbc58..6cee0c6f7 100644 --- a/frontend/src/pages/preview/index.jsx +++ b/frontend/src/pages/preview/index.jsx @@ -2,12 +2,7 @@ import Stream from "./Stream.jsx" import styles from "./styles.module.css" import "./reader.js" -import { - startLight, - startBubbler, - watch, - stopBubbler, -} from "../../../../lib/scope.js" +import { startLight, startBubbler, watch } from "../../../../lib/scope.js" import { triggerDownload } from "../../helpers.js" import cameraIcon from "./camera.svg" @@ -16,12 +11,14 @@ import NumberInput from "./NumberInput.jsx" import { createSignal } from "solid-js" export default function Preview() { - const [bubbler, setBubbler] = createSignal(false) + const [bubbler_dac, setBubblerDac] = createSignal(0) const [light_dac, setLightDac] = createSignal(0) watch("status/bubbler").then(async (messages) => { for await (const message of messages) { - setBubbler(message.status === "On") + if (message.dac) { + setBubblerDac(message.dac) + } } }) @@ -46,11 +43,9 @@ export default function Preview() {

Bubbler

- -
@@ -92,10 +87,8 @@ function onLightChange(value) { }) } -function onBubblerChange(event) { - if (event.target.checked === true) { - startBubbler() - } else { - stopBubbler() - } +function onBubblerChange(value) { + startBubbler({ + value, + }) } From 93f0874d87cfbf76bf67b1d211d56342262030d7 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 6 May 2026 06:22:08 +0000 Subject: [PATCH 3/8] implement new bubbler --- controller/bubbler/README.md | 4 +- controller/bubbler/main.py | 79 +++++++++++++++++++++++++----------- controller/bubbler/test.js | 2 +- controller/light/main.py | 22 +++------- 4 files changed, 65 insertions(+), 42 deletions(-) diff --git a/controller/bubbler/README.md b/controller/bubbler/README.md index aa29578e7..104e6c0c0 100644 --- a/controller/bubbler/README.md +++ b/controller/bubbler/README.md @@ -31,7 +31,7 @@ just dev ```json { "action": "on", - // float between 0 and 1 + // integer between 0 and 100 // 0 is same as "action": "off" "value": 0.5, } @@ -56,7 +56,7 @@ just dev ```json { "status": "On", - "value": 0.5, + "value": 50, } ``` diff --git a/controller/bubbler/main.py b/controller/bubbler/main.py index dcfc536b1..f66d2d536 100644 --- a/controller/bubbler/main.py +++ b/controller/bubbler/main.py @@ -11,7 +11,42 @@ client = None loop = asyncio.new_event_loop() bubbler = None - +state_value = 0 + +# Calibrated configuration +DAC_MIN_START = 0.2544 # Value at 25% +DAC_MAX_POWER = 0.350 # Value at 100% +KICKSTART_DURATION = 0.1 # 100ms to overcome inertia + +# Translate 0-100% to a DAC value with a special calibration : 25% -> 0.272, 100% -> 0.350. +def map_flow_to_dac(percent): + percent = max(0.0, min(percent, 100.0)) + + # For values between 1% and 24% (e.g. via slider) + # we slowly increase between 0 and DAC_MIN_START + if percent < 25.0: + dac = (percent / 25.0) * DAC_MIN_START + # Strict linear interpolation for range [25% - 100%] + # At 25%, the right hand side is 0, we've got DAC_MIN_START + # At 100%, the fraction equals 1, we've got DAC_MAX_POWER + else: + dac = DAC_MIN_START + ((percent - 25.0) / 75.0) * (DAC_MAX_POWER - DAC_MIN_START) + + # Clamp to eliminate float drift + return max(0.0, min(dac, DAC_MAX_POWER)) + +# And reverse - see map_flow_to_dac +def map_dac_to_flow(dac): + dac = max(0.0, min(dac, DAC_MAX_POWER)) + + if dac < DAC_MIN_START: + percent = (dac / DAC_MIN_START) * 25.0 + else: + percent = 25.0 + ((dac - DAC_MIN_START) / (DAC_MAX_POWER - DAC_MIN_START)) * 75.0 + + # Clamp to eliminate float drift + percent = max(0.0, min(percent, 100.0)) + return int(round(percent)) async def start() -> None: # There is no GPIO bubbler on PlanktoScope HAT < 3.3 @@ -23,6 +58,10 @@ async def start() -> None: import MCP4725 as bubbler bubbler.init(address=0x60) + global state_value + # Restore value from DAC + state_value = map_dac_to_flow(bubbler.get_value()) + global client client = aiomqtt.Client(hostname="localhost", port=1883, protocol=aiomqtt.ProtocolVersion.V5) task_group = asyncio.TaskGroup() @@ -62,53 +101,47 @@ async def handle_action(action: str, payload) -> None: await on(payload) elif action == "off": await off() - # elif action == "settings": - # await handle_settings(payload) elif action == "save": - if hasattr(bubbler, "save"): - bubbler.save() - - -# async def handle_settings(payload) -> None: -# assert bubbler is not None - -# if "current" in payload["settings"]: -# # {"settings":{"current":"20"}} -# current = payload["settings"]["current"] -# if bubbler.is_on(): -# return -# bubbler.set_current(current) - + await save() async def on(payload) -> None: assert bubbler is not None - value = payload.get("value", 1) - assert 0.0 <= value <= 1.0 + value = payload.get("value", 100) + assert 0.0 <= value <= 100.0 if value == 0: await off() return - bubbler.set_value(value) + global state_value + state_value = value + # If pump was off, kickstart + if value >= 25 and bubbler.is_off(): + bubbler.set_value(DAC_MAX_POWER) # Kickstart with max power + await asyncio.sleep(KICKSTART_DURATION) + + bubbler.set_value(map_flow_to_dac(value)) await publish_status() async def off() -> None: assert bubbler is not None bubbler.off() + global state_value + state_value = 0 await publish_status() +async def save() -> None: + bubbler.save() async def publish_status() -> None: assert bubbler is not None assert client is not None - value = bubbler.get_value() - payload = { "status": "Off" if bubbler.is_off() else "On", - "value": value, + "value": state_value, } await client.publish(topic="status/bubbler", payload=json.dumps(payload), retain=True) diff --git a/controller/bubbler/test.js b/controller/bubbler/test.js index 29e9208d3..2c182fdca 100644 --- a/controller/bubbler/test.js +++ b/controller/bubbler/test.js @@ -7,7 +7,7 @@ watch("status/bubbler").then(async (messages) => { } }) -await startBubbler() +await startBubbler({ value: 50 }) await setTimeout(2000) diff --git a/controller/light/main.py b/controller/light/main.py index 8efc893d2..69d431d1d 100644 --- a/controller/light/main.py +++ b/controller/light/main.py @@ -71,23 +71,8 @@ async def handle_action(action: str, payload) -> None: await on(payload) elif action == "off": await off() - # elif action == "settings": - # await handle_settings(payload) elif action == "save": - if hasattr(led, "save"): - led.save() - - -# async def handle_settings(payload) -> None: -# assert led is not None - -# if "current" in payload["settings"]: -# # {"settings":{"current":"20"}} -# current = payload["settings"]["current"] -# if led.is_on(): -# return -# led.set_current(current) - + await save() async def on(payload) -> None: assert led is not None @@ -119,6 +104,11 @@ async def off() -> None: except Exception as e: print(e) +async def save() -> None: + assert led is not None + + if hasattr(led, "save"): + led.save() async def publish_status() -> None: assert client is not None From 94b65382b6616d373c48cf5ed88c14b0edf216c5 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 6 May 2026 06:31:55 +0000 Subject: [PATCH 4/8] f --- controller/bubbler/main.py | 2 +- controller/light/main.py | 2 +- frontend/src/pages/preview/NumberInput.jsx | 12 ++++++------ frontend/src/pages/preview/index.jsx | 6 ++++++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/controller/bubbler/main.py b/controller/bubbler/main.py index f66d2d536..30f4f7970 100644 --- a/controller/bubbler/main.py +++ b/controller/bubbler/main.py @@ -107,7 +107,7 @@ async def handle_action(action: str, payload) -> None: async def on(payload) -> None: assert bubbler is not None value = payload.get("value", 100) - assert 0.0 <= value <= 100.0 + value = max(0.0, min(value, 100)) if value == 0: await off() diff --git a/controller/light/main.py b/controller/light/main.py index 69d431d1d..a75e6934a 100644 --- a/controller/light/main.py +++ b/controller/light/main.py @@ -77,7 +77,7 @@ async def handle_action(action: str, payload) -> None: async def on(payload) -> None: assert led is not None value = payload.get("value", 1) - assert 0.0 <= value <= 1.0 + value = max(0.0, min(value, 1)) if value == 0: await off() diff --git a/frontend/src/pages/preview/NumberInput.jsx b/frontend/src/pages/preview/NumberInput.jsx index 34ca83c37..d4dba02ee 100644 --- a/frontend/src/pages/preview/NumberInput.jsx +++ b/frontend/src/pages/preview/NumberInput.jsx @@ -19,18 +19,18 @@ export default function NumberInput(props) { name={props.name} value={props.value()} onInput={onInput} - min="0" - max="1" - step="0.01" + min={props.min} + max={props.max} + step={props.step} /> ) diff --git a/frontend/src/pages/preview/index.jsx b/frontend/src/pages/preview/index.jsx index 562f3a74c..21a0133e4 100644 --- a/frontend/src/pages/preview/index.jsx +++ b/frontend/src/pages/preview/index.jsx @@ -39,6 +39,9 @@ export default function Preview() { name="light" value={light_dac} onChange={onLightChange} + min="0" + max="1" + step="0.1" />
@@ -47,6 +50,9 @@ export default function Preview() { name="bubler" value={bubbler_dac} onChange={onBubblerChange} + min="0" + max="100" + step="25" />
From 9a26e640b20fbe1080debbbbff8fde597ffe5299 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 6 May 2026 06:48:33 +0000 Subject: [PATCH 5/8] f --- controller/MCP4725.py | 8 ++++ controller/bubbler/README.md | 2 +- controller/bubbler/main.py | 45 ++++++++++------------ controller/bubbler/test.js | 7 +++- frontend/src/pages/preview/NumberInput.jsx | 16 +++++--- frontend/src/pages/preview/index.jsx | 24 +++++------- 6 files changed, 55 insertions(+), 47 deletions(-) diff --git a/controller/MCP4725.py b/controller/MCP4725.py index a25d0850b..52c60e335 100644 --- a/controller/MCP4725.py +++ b/controller/MCP4725.py @@ -67,3 +67,11 @@ def get_value() -> float: def set_value(value: float) -> None: assert dac is not None dac.normalized_value = value + +def get_raw_value() -> int: + assert dac is not None + return dac.raw_value + +def set_raw_value(value: int) -> None: + assert dac is not None + dac.raw_value = value diff --git a/controller/bubbler/README.md b/controller/bubbler/README.md index 104e6c0c0..fbbb1bcc4 100644 --- a/controller/bubbler/README.md +++ b/controller/bubbler/README.md @@ -31,7 +31,7 @@ just dev ```json { "action": "on", - // integer between 0 and 100 + // float between 0 and 1.0 // 0 is same as "action": "off" "value": 0.5, } diff --git a/controller/bubbler/main.py b/controller/bubbler/main.py index 30f4f7970..4b568febe 100644 --- a/controller/bubbler/main.py +++ b/controller/bubbler/main.py @@ -14,39 +14,36 @@ state_value = 0 # Calibrated configuration -DAC_MIN_START = 0.2544 # Value at 25% -DAC_MAX_POWER = 0.350 # Value at 100% +DAC_MIN_START = 1114 # Value at 25% +DAC_MAX_POWER = 1433 # Value at 100% KICKSTART_DURATION = 0.1 # 100ms to overcome inertia -# Translate 0-100% to a DAC value with a special calibration : 25% -> 0.272, 100% -> 0.350. -def map_flow_to_dac(percent): - percent = max(0.0, min(percent, 100.0)) +# Translate a normalized value 0-1 to a DAC value with a special calibration : 0.25 -> 0.272, 1.0 -> 0.350. +def map_value_to_dac(value: float) -> int: + value = max(0.0, min(value, 1.0)) # For values between 1% and 24% (e.g. via slider) # we slowly increase between 0 and DAC_MIN_START - if percent < 25.0: - dac = (percent / 25.0) * DAC_MIN_START + if value < 0.25: + dac = (value / 0.25) * DAC_MIN_START # Strict linear interpolation for range [25% - 100%] # At 25%, the right hand side is 0, we've got DAC_MIN_START # At 100%, the fraction equals 1, we've got DAC_MAX_POWER else: - dac = DAC_MIN_START + ((percent - 25.0) / 75.0) * (DAC_MAX_POWER - DAC_MIN_START) + dac = DAC_MIN_START + ((value - 0.25) / 0.75) * (DAC_MAX_POWER - DAC_MIN_START) - # Clamp to eliminate float drift - return max(0.0, min(dac, DAC_MAX_POWER)) + return int(round(max(0, min(dac, DAC_MAX_POWER)))) -# And reverse - see map_flow_to_dac -def map_dac_to_flow(dac): - dac = max(0.0, min(dac, DAC_MAX_POWER)) +# Reverse of map_value_to_dac +def map_dac_to_value(dac: int) -> float: + dac = max(0, min(dac, DAC_MAX_POWER)) if dac < DAC_MIN_START: - percent = (dac / DAC_MIN_START) * 25.0 + value = (dac / DAC_MIN_START) * 0.25 else: - percent = 25.0 + ((dac - DAC_MIN_START) / (DAC_MAX_POWER - DAC_MIN_START)) * 75.0 + value = 0.25 + ((dac - DAC_MIN_START) / (DAC_MAX_POWER - DAC_MIN_START)) * 0.75 - # Clamp to eliminate float drift - percent = max(0.0, min(percent, 100.0)) - return int(round(percent)) + return max(0.0, min(value, 1.0)) async def start() -> None: # There is no GPIO bubbler on PlanktoScope HAT < 3.3 @@ -60,7 +57,7 @@ async def start() -> None: bubbler.init(address=0x60) global state_value # Restore value from DAC - state_value = map_dac_to_flow(bubbler.get_value()) + state_value = map_dac_to_value(bubbler.get_raw_value()) global client client = aiomqtt.Client(hostname="localhost", port=1883, protocol=aiomqtt.ProtocolVersion.V5) @@ -106,8 +103,8 @@ async def handle_action(action: str, payload) -> None: async def on(payload) -> None: assert bubbler is not None - value = payload.get("value", 100) - value = max(0.0, min(value, 100)) + value = payload.get("value", 1) + value = max(0.0, min(value, 1)) if value == 0: await off() @@ -117,11 +114,11 @@ async def on(payload) -> None: state_value = value # If pump was off, kickstart - if value >= 25 and bubbler.is_off(): - bubbler.set_value(DAC_MAX_POWER) # Kickstart with max power + if value >= 0.25 and bubbler.is_off(): + bubbler.set_raw_value(DAC_MAX_POWER) # Kickstart with max power await asyncio.sleep(KICKSTART_DURATION) - bubbler.set_value(map_flow_to_dac(value)) + bubbler.set_raw_value(map_value_to_dac(value)) await publish_status() diff --git a/controller/bubbler/test.js b/controller/bubbler/test.js index 2c182fdca..f34392d17 100644 --- a/controller/bubbler/test.js +++ b/controller/bubbler/test.js @@ -7,8 +7,11 @@ watch("status/bubbler").then(async (messages) => { } }) -await startBubbler({ value: 50 }) +await stopBubbler() -await setTimeout(2000) +for (const value of [0.25, 0.5, 0.75, 100]) { + await startBubbler({ value }) + await setTimeout(2000) +} await stopBubbler() diff --git a/frontend/src/pages/preview/NumberInput.jsx b/frontend/src/pages/preview/NumberInput.jsx index d4dba02ee..1195f82cb 100644 --- a/frontend/src/pages/preview/NumberInput.jsx +++ b/frontend/src/pages/preview/NumberInput.jsx @@ -11,6 +11,10 @@ export default function NumberInput(props) { props.onChange?.(evt.target.valueAsNumber) } + const min = props.min || "0" + const max = props.max || "1" + const step = props.step || "0.1" + return (
) diff --git a/frontend/src/pages/preview/index.jsx b/frontend/src/pages/preview/index.jsx index 21a0133e4..f3e9371f7 100644 --- a/frontend/src/pages/preview/index.jsx +++ b/frontend/src/pages/preview/index.jsx @@ -11,21 +11,21 @@ import NumberInput from "./NumberInput.jsx" import { createSignal } from "solid-js" export default function Preview() { - const [bubbler_dac, setBubblerDac] = createSignal(0) - const [light_dac, setLightDac] = createSignal(0) + const [bubbler_value, setBubblerValue] = createSignal(0) + const [light_value, setLightValue] = createSignal(0) watch("status/bubbler").then(async (messages) => { for await (const message of messages) { - if (message.dac) { - setBubblerDac(message.dac) + if (message.value) { + setBubblerValue(message.value) } } }) watch("status/light").then(async (messages) => { for await (const message of messages) { - if (message.dac) { - setLightDac(message.dac) + if (message.value) { + setLightValue(message.value) } } }) @@ -37,22 +37,18 @@ export default function Preview() {

Light

Bubbler

From 226ae8e8e8e6cf69b51a66b995a166565a1104d2 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 6 May 2026 06:52:06 +0000 Subject: [PATCH 6/8] f --- controller/bubbler/README.md | 10 +++++----- frontend/src/pages/preview/index.jsx | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/controller/bubbler/README.md b/controller/bubbler/README.md index fbbb1bcc4..ee0bab831 100644 --- a/controller/bubbler/README.md +++ b/controller/bubbler/README.md @@ -31,12 +31,12 @@ just dev ```json { "action": "on", - // float between 0 and 1.0 - // 0 is same as "action": "off" "value": 0.5, } ``` +`value` is a float >= `0` <= `1` that will adjust the bubbler intensity. The default is `1`. + ### Stop the bubbler: **topic** `actuator/bubbler` @@ -52,15 +52,15 @@ just dev **topic** `status/bubbler` -**payload when started:** +**payload when on:** ```json { "status": "On", - "value": 50, + "value": 0.5, } ``` -**payload when stopped:** +**payload when off:** ```json { "status": "Off", diff --git a/frontend/src/pages/preview/index.jsx b/frontend/src/pages/preview/index.jsx index f3e9371f7..b4fef8540 100644 --- a/frontend/src/pages/preview/index.jsx +++ b/frontend/src/pages/preview/index.jsx @@ -5,8 +5,6 @@ import "./reader.js" import { startLight, startBubbler, watch } from "../../../../lib/scope.js" import { triggerDownload, makeUrl } from "../../helpers.js" -import cameraIcon from "./camera.svg" - import NumberInput from "./NumberInput.jsx" import { createSignal } from "solid-js" From b8ed695b8d26994f81ea747b499c0fc2d2f7cd0f Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 6 May 2026 07:40:38 +0000 Subject: [PATCH 7/8] f --- controller/MCP4725.py | 2 ++ controller/bubbler/main.py | 14 ++++++++++---- controller/light/main.py | 3 +++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/controller/MCP4725.py b/controller/MCP4725.py index 52c60e335..f6764c737 100644 --- a/controller/MCP4725.py +++ b/controller/MCP4725.py @@ -68,10 +68,12 @@ def set_value(value: float) -> None: assert dac is not None dac.normalized_value = value + def get_raw_value() -> int: assert dac is not None return dac.raw_value + def set_raw_value(value: int) -> None: assert dac is not None dac.raw_value = value diff --git a/controller/bubbler/main.py b/controller/bubbler/main.py index 4b568febe..8389f6df6 100644 --- a/controller/bubbler/main.py +++ b/controller/bubbler/main.py @@ -14,9 +14,10 @@ state_value = 0 # Calibrated configuration -DAC_MIN_START = 1114 # Value at 25% -DAC_MAX_POWER = 1433 # Value at 100% -KICKSTART_DURATION = 0.1 # 100ms to overcome inertia +DAC_MIN_START = 1114 # Value at 25% +DAC_MAX_POWER = 1433 # Value at 100% +KICKSTART_DURATION = 0.1 # 100ms to overcome inertia + # Translate a normalized value 0-1 to a DAC value with a special calibration : 0.25 -> 0.272, 1.0 -> 0.350. def map_value_to_dac(value: float) -> int: @@ -34,6 +35,7 @@ def map_value_to_dac(value: float) -> int: return int(round(max(0, min(dac, DAC_MAX_POWER)))) + # Reverse of map_value_to_dac def map_dac_to_value(dac: int) -> float: dac = max(0, min(dac, DAC_MAX_POWER)) @@ -45,6 +47,7 @@ def map_dac_to_value(dac: int) -> float: return max(0.0, min(value, 1.0)) + async def start() -> None: # There is no GPIO bubbler on PlanktoScope HAT < 3.3 # only USB powered bubbler @@ -101,6 +104,7 @@ async def handle_action(action: str, payload) -> None: elif action == "save": await save() + async def on(payload) -> None: assert bubbler is not None value = payload.get("value", 1) @@ -115,7 +119,7 @@ async def on(payload) -> None: # If pump was off, kickstart if value >= 0.25 and bubbler.is_off(): - bubbler.set_raw_value(DAC_MAX_POWER) # Kickstart with max power + bubbler.set_raw_value(DAC_MAX_POWER) # Kickstart with max power await asyncio.sleep(KICKSTART_DURATION) bubbler.set_raw_value(map_value_to_dac(value)) @@ -129,9 +133,11 @@ async def off() -> None: state_value = 0 await publish_status() + async def save() -> None: bubbler.save() + async def publish_status() -> None: assert bubbler is not None assert client is not None diff --git a/controller/light/main.py b/controller/light/main.py index a75e6934a..c7e8c3b7b 100644 --- a/controller/light/main.py +++ b/controller/light/main.py @@ -74,6 +74,7 @@ async def handle_action(action: str, payload) -> None: elif action == "save": await save() + async def on(payload) -> None: assert led is not None value = payload.get("value", 1) @@ -104,12 +105,14 @@ async def off() -> None: except Exception as e: print(e) + async def save() -> None: assert led is not None if hasattr(led, "save"): led.save() + async def publish_status() -> None: assert client is not None assert led is not None From b627706a154b8dbac2d845d1b15317fb2295dc68 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Wed, 6 May 2026 07:45:07 +0000 Subject: [PATCH 8/8] f --- controller/MCP4725.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/MCP4725.py b/controller/MCP4725.py index f6764c737..90db8f67c 100644 --- a/controller/MCP4725.py +++ b/controller/MCP4725.py @@ -71,7 +71,7 @@ def set_value(value: float) -> None: def get_raw_value() -> int: assert dac is not None - return dac.raw_value + return int(dac.raw_value) def set_raw_value(value: int) -> None: