diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e2f688a..357c927f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: hooks: - id: mypy additional_dependencies: [ - msgspec==0.17.0, + msgspec==0.18.0, types-pkg_resources==0.1.3, types-paho-mqtt==1.6.0.6 ] diff --git a/pioreactor/actions/od_blank.py b/pioreactor/actions/od_blank.py index 902730e3..199ca53f 100644 --- a/pioreactor/actions/od_blank.py +++ b/pioreactor/actions/od_blank.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from __future__ import annotations from collections import defaultdict diff --git a/pioreactor/automations/dosing/base.py b/pioreactor/automations/dosing/base.py index d0293689..cfcd3696 100644 --- a/pioreactor/automations/dosing/base.py +++ b/pioreactor/automations/dosing/base.py @@ -455,7 +455,10 @@ def execute_io_action( self.remove_waste_from_bioreactor( unit=self.unit, experiment=self.experiment, - ml=waste_ml * 2, + ml=waste_ml + * config.getfloat( + "dosing_automation", "waste_removal_multiplier", fallback=2.0 + ), source_of_event=source_of_event, ) briefer_pause() diff --git a/pioreactor/background_jobs/monitor.py b/pioreactor/background_jobs/monitor.py index 2fdab58c..21ea4ae3 100644 --- a/pioreactor/background_jobs/monitor.py +++ b/pioreactor/background_jobs/monitor.py @@ -16,6 +16,7 @@ from pioreactor import version from pioreactor import whoami from pioreactor.background_jobs.base import BackgroundJob +from pioreactor.hardware import is_HAT_present from pioreactor.hardware import PCB_BUTTON_PIN as BUTTON_PIN from pioreactor.hardware import PCB_LED_PIN as LED_PIN from pioreactor.hardware import TEMP @@ -77,6 +78,7 @@ def off(): # functions don't take any arguments, nothing is passed in "button_down": {"datatype": "boolean", "settable": False}, "versions": {"datatype": "json", "settable": False}, "voltage_on_pwm_rail": {"datatype": "Voltage", "settable": False}, + "ipv4": {"datatype": "string", "settable": False}, } computer_statistics: Optional[dict] = None led_in_use: bool = False @@ -178,12 +180,19 @@ def _setup_GPIO(self) -> None: self.logger.warning("Failed to add button detect.") def check_for_network(self) -> None: - ip = get_ip() - while (not whoami.is_testing_env()) and ((ip == "127.0.0.1") or (ip is None)): - # no wifi connection? Sound the alarm. - self.logger.warning("Unable to connect to network...") - self.flicker_led_with_error_code(error_codes.NO_NETWORK_CONNECTION) - ip = get_ip() + if whoami.is_testing_env(): + self.ipv4 = "127.0.0.1" + else: + ipv4 = get_ip() + while ipv4 == "127.0.0.1" or ipv4 is None: + # no wifi connection? Sound the alarm. + self.logger.warning("Unable to connect to network...") + self.flicker_led_with_error_code(error_codes.NO_NETWORK_CONNECTION) + ipv4 = get_ip() + + self.ipv4 = ipv4 + + self.logger.debug(f"IPv4 address: {self.ipv4}") def self_checks(self) -> None: # check active network connection @@ -202,6 +211,7 @@ def self_checks(self) -> None: self.check_for_webserver() if whoami.am_I_active_worker(): + self.check_for_HAT() # check the PCB temperature self.check_heater_pcb_temperature() @@ -276,6 +286,10 @@ def check_for_required_jobs_running(self): "watchdog and mqtt_to_db_streaming should be running on leader. Double check." ) + def check_for_HAT(self) -> None: + if not is_HAT_present(): + self.logger.warning("HAT is not detected.") + def check_heater_pcb_temperature(self) -> None: """ Originally from #220 @@ -296,6 +310,7 @@ def check_heater_pcb_temperature(self) -> None: tmp_driver = TMP1075(address=TEMP) except ValueError: # No PCB detected using i2c - fine to exit. + self.logger.warning("Heater PCB is not detected.") return observed_tmp = tmp_driver.get_temperature() diff --git a/pioreactor/background_jobs/stirring.py b/pioreactor/background_jobs/stirring.py index cf54bc5a..4b851bf9 100644 --- a/pioreactor/background_jobs/stirring.py +++ b/pioreactor/background_jobs/stirring.py @@ -22,6 +22,7 @@ from pioreactor.utils import clamp from pioreactor.utils import is_pio_job_running from pioreactor.utils import local_persistant_storage +from pioreactor.utils import retry from pioreactor.utils.gpio_helpers import set_gpio_availability from pioreactor.utils.pwm import PWM from pioreactor.utils.streaming_calculations import PID @@ -73,9 +74,11 @@ def setup(self) -> None: # ignore any changes that occur within 15ms - at 1000rpm (very fast), the # delta between changes is ~60ms, so 15ms is good enough. - # TODO: sometimes this fails with `RuntimeError: Failed to add edge detection` - self.GPIO.add_event_detect( - self.hall_sensor_pin, self.GPIO.FALLING, callback=self.callback, bouncetime=15 + # sometimes this fails with `RuntimeError: Failed to add edge detection`, so we retry. + retry( + self.GPIO.add_event_detect, + args=(self.hall_sensor_pin, self.GPIO.FALLING), + kwargs={"callback": self.callback, "bouncetime": 15}, ) self.turn_off_collection() diff --git a/pioreactor/cli/pio.py b/pioreactor/cli/pio.py index 27f7b0c1..18f094ab 100644 --- a/pioreactor/cli/pio.py +++ b/pioreactor/cli/pio.py @@ -141,11 +141,6 @@ def log(message: str, level: str, name: str, local_only: bool): ) getattr(logger, level)(message) - # flush and close handlers - for handler in logger.logger.handlers: - handler.flush() - handler.close() - except Exception as e: # don't let a logging error bring down a script... print(e) @@ -465,7 +460,9 @@ def update_app( if source is not None: version_installed = source - commands_and_priority.append((f"sudo pip3 install -U --force-reinstall {source}", 1)) + commands_and_priority.append( + (f"sudo pip3 install --force-reinstall --no-index {source}", 1) + ) elif branch is not None: version_installed = quote(branch) diff --git a/pioreactor/utils/__init__.py b/pioreactor/utils/__init__.py index 0287a4f7..91b45759 100644 --- a/pioreactor/utils/__init__.py +++ b/pioreactor/utils/__init__.py @@ -4,7 +4,9 @@ import os import signal import tempfile +import time from contextlib import contextmanager +from functools import wraps from threading import Event from typing import Callable from typing import cast @@ -375,3 +377,46 @@ def __getitem__(self, key): return 0 else: return dict.__getitem__(self, key) + + +def retry(func: Callable, retries=3, delay=0.5, args=(), kwargs={}): + """ + Retries a function upon encountering an exception until it succeeds or the maximum number of retries is exhausted. + + This function executes the provided function and handles any exceptions it raises. If an exception is raised, + the function will wait for a specified delay before attempting to execute the function again. This process repeats + until either the function execution is successful or the specified maximum number of retries is exhausted. + On the final attempt, if the function still raises an exception, that exception will be re-raised to the caller. + + Parameters + ----------- + func (callable): The function to be retried. + retries (int, optional): The maximum number of times to retry the function. Defaults to 3. + delay (float, optional): The number of seconds to wait between retries. Defaults to 0.5. + args (tuple, optional): The positional arguments to pass to the function. Defaults to an empty tuple. + kwargs (dict, optional): The keyword arguments to pass to the function. Defaults to an empty dictionary. + + Returns + -------- + The return value of the function call, if the function call is successful. + + Raises + -------- + Exception: The exception raised by the function call if the function call is unsuccessful after the specified number of retries. + + Example + -------- + + > def risky_function(x, y): + > return x / y + > + > # Call the function with retry + > result = retry(risky_function, retries=5, delay=1, args=(10, 0)) + """ + for i in range(retries): + try: + return func(*args, **kwargs) + except Exception as e: + if i == retries - 1: # If this was the last attempt + raise e + time.sleep(delay) diff --git a/pioreactor/utils/networking.py b/pioreactor/utils/networking.py index 57569cb8..e4c7f011 100644 --- a/pioreactor/utils/networking.py +++ b/pioreactor/utils/networking.py @@ -79,6 +79,7 @@ def is_connected_to_network() -> bool: def get_ip() -> Optional[str]: + # TODO: is this always ipv4?? from psutil import net_if_addrs # type: ignore # Check for IP address of wireless network interface 'wlan0' diff --git a/requirements/requirements.txt b/requirements/requirements.txt index bb0fe39b..de89fad7 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,5 +5,5 @@ sh==1.14.2 JSON-log-formatter==0.4.0 rpi_hardware_pwm==0.1.3 colorlog==6.6.0 -msgspec==0.17.0 +msgspec==0.18.0 diskcache==5.6.1 diff --git a/setup.py b/setup.py index 598361b8..88fe6d09 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ "sh==1.14.3", "JSON-log-formatter==0.5.1", "colorlog==6.7.0", - "msgspec==0.17.0", + "msgspec==0.18.0", "diskcache==5.6.1", "wheel==0.38.4", "crudini==0.9.4",