Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions device-backend/control/planktoscopehat/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import time
import signal
import os
import json

from loguru import logger

Expand Down Expand Up @@ -53,7 +54,7 @@ def handler_stop_signals(signum, frame):
if __name__ == "__main__":
logger.info("Welcome!")
logger.info(
"Initialising signals handling and sanitizing the directories (step 1/5)"
"Initialising configuration, signals handling and sanitizing the directories (step 1/5)"
)
signal.signal(signal.SIGINT, handler_stop_signals)
signal.signal(signal.SIGTERM, handler_stop_signals)
Expand Down Expand Up @@ -84,25 +85,34 @@ def handler_stop_signals(signum, frame):
f"This PlanktoScope's machine name is {planktoscope.identity.load_machine_name()}"
)

try:
with open("/home/pi/PlanktoScope/hardware.json", "r") as config_file:
configuration = json.load(config_file)
Copy link
Copy Markdown
Collaborator

@ethanjli ethanjli May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in #578 (which this PR is linked to as a closing PR), this would be a great opportunity to log something (using the loguru-based logger) when json.load fails to parse the open file and throws some kind of exception (which is not a FileNotFoundError and thus is not caught by any exception handler which would write the exception traceback to logger, as far as I can tell).

Maybe one option would be to move everything currently under if __name__ == '__main__': into a new main function, and then to just have a top-level catch-all exception handler around main(), e.g. like the following code which might not be syntactically correct:

if __name__ == '__main__':
  try:
    main()
  except e:
    logger.exception('uncaught exception:', e)
    raise e

Adding this kind of top-level exception handler into this PR might be sufficient to mark #553 as resolved once this PR is merged.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, now that I've started #602 I think I'd like to make the change described above in that PR instead of this PR, so that the top-level exception handler will be truly top-level (i.e. in the if __name__ == '__main__' block of ~/PlanktoScope/device-backend/control/main.py); in that case, #602 will resolve #553. Once you merge this PR, then in #602 I can move code in the adafruithat/main.py and planktoscopehat/main.py scripts into def main() function blocks so that those main functions can be invoked via module imports from ~/PlanktoScope/device-backend/control/main.py.

except FileNotFoundError:
logger.info(
"The hardware configuration file doesn't exists, using defaults"
)
configuration = {}

# Prepare the event for a graceful shutdown
shutdown_event = multiprocessing.Event()
shutdown_event.clear()

# Starts the pump process
logger.info("Starting the pump control process (step 2/5)")
pump_thread = planktoscope.pump.PumpProcess(shutdown_event)
pump_thread = planktoscope.pump.PumpProcess(shutdown_event, configuration)
pump_thread.start()

# Starts the focus process
logger.info("Starting the focus control process (step 3/5)")
focus_thread = planktoscope.focus.FocusProcess(shutdown_event)
focus_thread = planktoscope.focus.FocusProcess(shutdown_event, configuration)
focus_thread.start()

# TODO try to isolate the imager thread (or another thread)
# Starts the imager control process
logger.info("Starting the imager control process (step 4/5)")
try:
imager_thread = imager.Worker(shutdown_event)
imager_thread = imager.Worker(shutdown_event, configuration)
except Exception as e:
logger.error(f"The imager control process could not be started: {e}")
imager_thread = None
Expand All @@ -112,7 +122,7 @@ def handler_stop_signals(signum, frame):
# Starts the light process
logger.info("Starting the light control process (step 5/5)")
try:
light_thread = planktoscope.light.LightProcess(shutdown_event)
light_thread = planktoscope.light.LightProcess(shutdown_event, configuration)
except Exception:
logger.error("The light control process could not be started")
light_thread = None
Expand Down
21 changes: 6 additions & 15 deletions device-backend/control/planktoscopehat/planktoscope/camera/mqtt.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""mqtt provides an MJPEG+MQTT API for camera supervision and interaction."""

import json
import os
import threading
import time
import typing
Expand All @@ -17,7 +16,11 @@
class Worker(threading.Thread):
"""Runs a camera with live MJPEG preview and an MQTT API for adjusting camera settings."""

def __init__(self, mjpeg_server_address: tuple[str, int] = ("", 8000)) -> None:
def __init__(
self,
configuration: dict[str, typing.Any],
mjpeg_server_address: tuple[str, int] = ("", 8000),
) -> None:
"""Initialize the backend.

Args:
Expand Down Expand Up @@ -50,19 +53,7 @@ def __init__(self, mjpeg_server_address: tuple[str, int] = ("", 8000)) -> None:
sharpness=0, # disable the default "normal" sharpening level
jpeg_quality=95, # maximize image quality
)
if os.path.exists("/home/pi/PlanktoScope/hardware.json"):
# load hardware.json
with open("/home/pi/PlanktoScope/hardware.json", "r", encoding="utf-8") as config_file:
hardware_config = json.load(config_file)
loguru.logger.debug(
f"Loaded hardware configuration file: {hardware_config}",
)
settings = settings.overlay(hardware.config_to_settings_values(hardware_config))
else:
loguru.logger.info(
"The hardware configuration file doesn't exist, using default settings: "
+ f"{settings}"
)
settings = settings.overlay(hardware.config_to_settings_values(configuration))

# I/O
self._preview_stream: hardware.PreviewStream = hardware.PreviewStream()
Expand Down
13 changes: 1 addition & 12 deletions device-backend/control/planktoscopehat/planktoscope/focus.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import multiprocessing
import os
import time
Expand All @@ -22,23 +21,13 @@ class FocusProcess(multiprocessing.Process):
# focus max speed is in mm/sec and is limited by the maximum number of pulses per second the PlanktoScope can send
focus_max_speed = 5

def __init__(self, event):
def __init__(self, event, configuration):
super(FocusProcess, self).__init__()
logger.info("Initialising the focus process")

self.stop_event = event
self.focus_started = False

if os.path.exists("/home/pi/PlanktoScope/hardware.json"):
# load hardware.json
with open("/home/pi/PlanktoScope/hardware.json", "r") as config_file:
# TODO #100 insert guard for config_file empty
configuration = json.load(config_file)
logger.debug(f"Hardware configuration loaded is {configuration}")
else:
logger.info("The hardware configuration file doesn't exists, using defaults")
configuration = {}

# parse the config data. If the key is absent, we are using the default value
self.focus_steps_per_mm = configuration.get("focus_steps_per_mm", self.focus_steps_per_mm)
self.focus_max_speed = configuration.get("focus_max_speed", self.focus_max_speed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Worker(multiprocessing.Process):
# TODO(ethanjli): instead of passing in a stop_event, just expose a `close()` method! This
# way, we don't give any process the ability to stop all other processes watching the same
# stop_event!
def __init__(self, stop_event: threading.Event) -> None:
def __init__(self, stop_event: threading.Event, configuration) -> None:
"""Initialize the worker's internal state, but don't start anything yet.

Args:
Expand All @@ -52,6 +52,8 @@ def __init__(self, stop_event: threading.Event) -> None:
# constructor.
self._camera: typing.Optional[camera.Worker] = None

self.configuration = configuration

loguru.logger.success("planktoscope.imager is initialized and ready to go!")

@loguru.logger.catch
Expand All @@ -74,7 +76,7 @@ def run(self) -> None:
loguru.logger.success("Pump RPC client is ready!")

loguru.logger.info("Starting the camera...")
self._camera = camera.Worker()
self._camera = camera.Worker(self.configuration)
self._camera.start()
if self._camera.camera is None:
loguru.logger.error(
Expand Down
18 changes: 4 additions & 14 deletions device-backend/control/planktoscopehat/planktoscope/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
################################################################################
# Logger library compatible with multiprocessing
from loguru import logger
import json

from gpiozero import DigitalOutputDevice

Expand Down Expand Up @@ -40,22 +39,13 @@ class Register(enum.IntEnum):
# This constant defines the current (mA) sent to the LED, 10 allows the use of the full ISO scale and results in a voltage of 2.77v
DEFAULT_CURRENT = 10

def __init__(self):
try:
with open("/home/pi/PlanktoScope/hardware.json", "r") as config_file:
configuration = json.load(config_file)
except FileNotFoundError:
logger.info(
"The hardware configuration file doesn't exists, using defaults"
)
configuration = {}

def __init__(self, configuration):
hat_type = configuration.get("hat_type") or ""
hat_version = float(configuration.get("hat_version") or 0)

# The led is controlled by LM36011
# but on version 1.2 of the PlanktoScope HAT (PlanktoScope v2.6)
# the circuit is connected to that pin so it needs to be high
# the circuit is connected to the pin 18 so it needs to be high
# pin is assigned to self to prevent gpiozero from immediately releasing it
if hat_type != "planktoscope" or hat_version < 3.1:
self.__pin = DigitalOutputDevice(pin=18, initial_value=True)
Expand Down Expand Up @@ -176,7 +166,7 @@ def _read_byte(self, address):
class LightProcess(multiprocessing.Process):
"""This class contains the main definitions for the light of the PlanktoScope"""

def __init__(self, event):
def __init__(self, event, configuration):
"""Initialize the Light class

Args:
Expand All @@ -189,7 +179,7 @@ def __init__(self, event):
self.stop_event = event
self.light_client = None
try:
self.led = i2c_led()
self.led = i2c_led(configuration)
self.led.set_torch_current(self.led.DEFAULT_CURRENT)
self.led.activate_torch_ramp()
self.led.activate_torch()
Expand Down
13 changes: 1 addition & 12 deletions device-backend/control/planktoscopehat/planktoscope/pump.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import multiprocessing
import os
import time
Expand All @@ -24,23 +23,13 @@ class PumpProcess(multiprocessing.Process):
# pump max speed is in ml/min
pump_max_speed = 50

def __init__(self, event):
def __init__(self, event, configuration):
super(PumpProcess, self).__init__()
logger.info("Initialising the pump process")

self.stop_event = event
self.pump_started = False

if os.path.exists("/home/pi/PlanktoScope/hardware.json"):
# load hardware.json
with open("/home/pi/PlanktoScope/hardware.json", "r") as config_file:
# TODO #100 insert guard for config_file empty
configuration = json.load(config_file)
logger.debug(f"Hardware configuration loaded is {configuration}")
else:
logger.info("The hardware configuration file doesn't exists, using defaults")
configuration = {}

# parse the config data. If the key is absent, we are using the default value
self.pump_steps_per_ml = configuration.get("pump_steps_per_ml", self.pump_steps_per_ml)
self.pump_max_speed = configuration.get("pump_max_speed", self.pump_max_speed)
Expand Down