diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0a09d6..8c064096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ #### Enhancements - new OD calibration using standards (requires multiple vials). Try `pio calibrations run --device od`. This was inspired by the plugin by @odcambc. - - new RP2040 firmware improvements (v0.4) => faster response over i2c. - Improved chart colors in the UI - The OD reading CLI has a new option, `--snapshot`, that will start the job, take a single reading, and exit. This is useful for scripting purposes. - A new CLI for pumps: `pio run pumps`. Add pumps as options: @@ -18,7 +17,7 @@ - Run multiple experiment profiles per experiment! - Specify which Pioreactor to update on the Updates page (option is only available with release archives.) - Choose the level of detail on the new Event Logs page. - - Added index to stirring_rates table. + - Previously, when a worker's web server is down, it would halt an update from proceeding (since it can't send the command). Now, leader will try the webserver, and if it observes a 5xx error, will attempt an SSH communication. #### Web API changes diff --git a/pioreactor/calibrations/od_calibration_using_standards.py b/pioreactor/calibrations/od_calibration_using_standards.py index 4222c50d..0c91ac39 100644 --- a/pioreactor/calibrations/od_calibration_using_standards.py +++ b/pioreactor/calibrations/od_calibration_using_standards.py @@ -11,7 +11,6 @@ """ from __future__ import annotations -from math import log2 from time import sleep from typing import cast @@ -56,12 +55,10 @@ def introduction() -> None: clear() echo( - """This routine will calibrate the current Pioreactor to (offline) OD600 readings. You'll need: - 1. The Pioreactor you wish to calibrate (the one you are using) - 2. At least 10mL of a culture with the highest density you'll ever observe, and its OD600 measurement - 3. A micro-pipette, or accurate tool to dispense 1ml of liquid. - 4. Accurate 10mL measurement tool (ex: graduated cylinder) - 5. Sterile media, amount to be determined shortly. + """This routine will calibrate the current Pioreactor to (offline) OD600 readings using a set of standards. You'll need: + 1. A Pioreactor + 2. A set of OD600 standards in Pioreactor vials (at least 10 mL in each vial), each with a stirbar + 3. One of the standards should be a blank (no cells, only media). """ ) @@ -91,7 +88,7 @@ def get_name_from_user() -> str: return name -def get_metadata_from_user() -> tuple[pt.OD600, pt.OD600, pt.mL, pt.PdAngle, pt.PdChannel]: +def get_metadata_from_user() -> tuple[pt.PdAngle, pt.PdChannel]: if config.get("od_reading.config", "ir_led_intensity") == "auto": echo( red( @@ -100,49 +97,6 @@ def get_metadata_from_user() -> tuple[pt.OD600, pt.OD600, pt.mL, pt.PdAngle, pt. ) raise click.Abort() - initial_od600 = prompt( - green("Provide the OD600 measurement of your initial, high density, culture"), - type=click.FloatRange(min=0.01, clamp=False), - ) - - minimum_od600 = prompt( - green("Provide the minimum OD600 measurement you wish to calibrate to"), - type=click.FloatRange(min=0, max=initial_od600, clamp=False), - ) - - while minimum_od600 >= initial_od600: - minimum_od600 = cast( - pt.OD600, - prompt( - "The minimum OD600 measurement must be less than the initial OD600 culture measurement", - type=click.FloatRange(min=0, max=initial_od600, clamp=False), - ), - ) - - if minimum_od600 == 0: - minimum_od600 = 0.01 - - dilution_amount = prompt( - green("Provide the volume to be added to your vial each iteration (default = 1 mL)"), - default=1, - type=click.FloatRange(min=0.01, max=10, clamp=False), - ) - - number_of_points = int(log2(initial_od600 / minimum_od600) * (10 / dilution_amount)) - - echo(f"This will require {number_of_points} data points.") - echo(f"You will need at least {number_of_points * dilution_amount + 10}mL of media available.") - confirm(green("Continue?"), abort=True, default=True) - - if "REF" not in config["od_config.photodiode_channel_reverse"]: - echo( - red( - "REF required for OD calibration. Set an input to REF in [od_config.photodiode_channel] in your config." - ) - ) - raise click.Abort() - # technically it's not required? we just need a specific PD channel to calibrate from. - ref_channel = config["od_config.photodiode_channel_reverse"]["REF"] pd_channel = cast(pt.PdChannel, "1" if ref_channel == "2" else "2") @@ -154,7 +108,7 @@ def get_metadata_from_user() -> tuple[pt.OD600, pt.OD600, pt.mL, pt.PdAngle, pt. default=True, ) angle = cast(pt.PdAngle, config["od_config.photodiode_channel"][pd_channel]) - return initial_od600, minimum_od600, dilution_amount, angle, pd_channel + return angle, pd_channel def setup_HDC_instructions() -> None: @@ -243,13 +197,14 @@ def get_voltage_from_adc() -> float: od_readings2 = od_reader.record_from_adc() return 0.5 * (od_readings1.ods[signal_channel].od + od_readings2.ods[signal_channel].od) - for _ in range(4): + for _ in range(3): # warm up od_reader.record_from_adc() while True: - click.echo("Recording next standard.") - standard_od = click.prompt("Enter OD600 measurement", type=float) + click.clear() + click.echo("Recording new standard.") + standard_od = click.prompt(green("Enter OD600 measurement of current vial"), type=float) for i in range(4): click.echo(".", nl=False) sleep(0.5) @@ -276,16 +231,17 @@ def get_voltage_from_adc() -> float: ) click.echo() - if not click.confirm("Record another OD600 standard?", default=True): + if not click.confirm(green("Record another OD600 standard?"), default=True): break click.echo() - click.echo(click.style("Stop❗", fg="red")) - click.echo("Carefully remove vial and replace with next standard.") - click.echo("Confirm vial outside is dry and clean.") - while not click.confirm("Continue?", default=True): + click.echo("Remove old vial.") + click.echo("Replace with new vial: confirm vial is dry and clean.") + click.echo() + while not click.confirm(green("Confirm vial is placed in Pioreactor?"), default=True): pass st.set_state("ready") + click.echo("Starting stirring.") st.block_until_rpm_is_close_to_target(abs_tolerance=120) sleep(1.0) @@ -300,9 +256,9 @@ def get_voltage_from_adc() -> float: y_label="Voltage", ) click.echo("Add media blank standard.") - od600_blank = click.prompt("What is the OD600 of your blank?", type=float) - click.echo("Confirm vial outside is dry and clean. Place into Pioreactor.") - while not click.confirm("Continue?", default=True): + od600_blank = click.prompt(green("What is the OD600 of your blank?"), type=float) + click.echo("Confirm blank vial outside is dry and clean. Place into Pioreactor.") + while not click.confirm(green("Continue?"), default=True): pass voltages.append(get_voltage_from_adc()) @@ -326,9 +282,6 @@ def run_od_calibration() -> structs.ODCalibration: raise click.Abort() ( - initial_od600, - minimum_od600, - dilution_amount, angle, pd_channel, ) = get_metadata_from_user() diff --git a/pioreactor/cli/calibrations.py b/pioreactor/cli/calibrations.py index aadb7b9f..067a10a6 100644 --- a/pioreactor/cli/calibrations.py +++ b/pioreactor/cli/calibrations.py @@ -17,6 +17,10 @@ from pioreactor.calibrations.utils import plot_data +def green(string: str) -> str: + return click.style(string, fg="green") + + @click.group(short_help="calibration utils") def calibration() -> None: """ @@ -86,12 +90,14 @@ def run_calibration(ctx, device: str, protocol_name: str | None, y: bool) -> Non click.clear() click.echo() click.echo(f"Available protocols for {device}:") + click.echo() for protocol in calibration_protocols.get(device, {}).values(): click.echo(click.style(f" • {protocol.protocol_name}", bold=True)) click.echo(f" Description: {protocol.description}") click.echo() protocol_name = click.prompt( - "Choose a protocol", type=click.Choice(list(calibration_protocols.get(device, {}).keys())) + green("Choose a protocol"), + type=click.Choice(list(calibration_protocols.get(device, {}).keys())), ) assistant = calibration_protocols.get(device, {}).get(protocol_name) @@ -111,7 +117,8 @@ def run_calibration(ctx, device: str, protocol_name: str | None, y: bool) -> Non if not y: if click.confirm( - f"Do you want to set this calibration as the active calibration for {device}?", default=True + green(f"Do you want to set this calibration as the active calibration for {device}?"), + default=True, ): calibration_struct.set_as_active_calibration_for_device(device) click.echo(f"Set {calibration_struct.calibration_name} as the active calibration for {device}.") @@ -122,6 +129,7 @@ def run_calibration(ctx, device: str, protocol_name: str | None, y: bool) -> Non else: calibration_struct.set_as_active_calibration_for_device(device) + click.echo() click.echo( f"Calibration '{calibration_struct.calibration_name}' of device '{device}' saved to {out_file} ✅" ) @@ -186,7 +194,7 @@ def set_active_calibration(device: str, calibration_name: str | None) -> None: @calibration.command(name="delete") @click.option("--device", required=True, help="Which calibration device to delete from.") @click.option("--name", "calibration_name", required=True, help="Which calibration name to delete.") -@click.confirmation_option(prompt="Are you sure you want to delete this calibration?") +@click.confirmation_option(prompt=green("Are you sure you want to delete this calibration?")) def delete_calibration(device: str, calibration_name: str) -> None: """ Delete a calibration file from local storage. diff --git a/pioreactor/cli/pios.py b/pioreactor/cli/pios.py index 86fc5ea7..462c0e00 100644 --- a/pioreactor/cli/pios.py +++ b/pioreactor/cli/pios.py @@ -286,11 +286,14 @@ def update( logger = create_logger("update", unit=get_unit_name(), experiment=UNIVERSAL_EXPERIMENT) options: dict[str, str | None] = {} + args = "" - if source is not None: - options["source"] = source - elif branch is not None: + if branch is not None: options["branch"] = branch + args = f"--branch {branch}" + elif source is not None: + options["source"] = source + args = f"--source {source}" def _thread_function(unit: str) -> tuple[bool, dict]: try: @@ -304,12 +307,6 @@ def _thread_function(unit: str) -> tuple[bool, dict]: f"Unable to update on {unit} due to server error: {e}. Attempting SSH method..." ) try: - args: str - if source is not None: - args = f"--source {source}" - elif branch is not None: - args = f"--branch {branch}" - ssh(resolve_to_address(unit), f"pio update {args}") return True, {"unit": unit} except SSHError as e: @@ -362,14 +359,18 @@ def update_app( logger = create_logger("update", unit=get_unit_name(), experiment=UNIVERSAL_EXPERIMENT) options: dict[str, str | None] = {} + args = "" # only one of these three is possible, mutually exclusive if version is not None: options["version"] = version + args = f"--version {version}" elif branch is not None: options["branch"] = branch + args = f"--branch {branch}" elif source is not None: options["source"] = source + args = f"--source {source}" if repo is not None: options["repo"] = repo @@ -384,14 +385,6 @@ def _thread_function(unit: str) -> tuple[bool, dict]: except HTTPException as e: logger.error(f"Unable to update on {unit} due to server error: {e}. Attempting SSH method...") try: - args: str - if source is not None: - args = f"--source {source}" - elif branch is not None: - args = f"--branch {branch}" - elif version is not None: - args = f"--version {version}" - ssh(resolve_to_address(unit), f"pio update app {args}") return True, {"unit": unit} except SSHError as e: @@ -443,14 +436,18 @@ def update_ui( logger = create_logger("update", unit=get_unit_name(), experiment=UNIVERSAL_EXPERIMENT) options: dict[str, str | None] = {} + args = "" # only one of these three is possible, mutually exclusive if version is not None: options["version"] = version + args = f"--version {version}" elif branch is not None: options["branch"] = branch + args = f"--branch {branch}" elif source is not None: options["source"] = source + args = f"--source {source}" if repo is not None: options["repo"] = repo @@ -465,14 +462,6 @@ def _thread_function(unit: str) -> tuple[bool, dict]: except HTTPException as e: logger.error(f"Unable to update on {unit} due to server error: {e}. Attempting SSH method...") try: - args: str - if source is not None: - args = f"--source {source}" - elif branch is not None: - args = f"--branch {branch}" - elif version is not None: - args = f"--version {version}" - ssh(resolve_to_address(unit), f"pio update ui {args}") return True, {"unit": unit} except SSHError as e: diff --git a/pioreactor/utils/adcs.py b/pioreactor/utils/adcs.py index 22cfdd2d..ca96fa23 100644 --- a/pioreactor/utils/adcs.py +++ b/pioreactor/utils/adcs.py @@ -94,7 +94,8 @@ class Pico_ADC(_ADC): def __init__(self) -> None: # set up i2c connection to hardware.ADC self.i2c = I2C(hardware.SCL, hardware.SDA) - assert self.get_firmware_version() == (0, 4), "Firmware version mismatch." + self.scale = 16 + # assert self.get_firmware_version() == (0, 4), "Firmware version mismatch." def read_from_channel(self, channel: pt.AdcChannel) -> pt.AnalogValue: assert 0 <= channel <= 3 @@ -115,13 +116,13 @@ def get_firmware_version(self) -> tuple[int, int]: return (result[1], result[0]) def from_voltage_to_raw(self, voltage: pt.Voltage) -> pt.AnalogValue: - return int((voltage / 3.3) * 4095 * 32) + return int((voltage / 3.3) * 4095 * self.scale) def from_voltage_to_raw_precise(self, voltage: pt.Voltage) -> float: - return (voltage / 3.3) * 4095 * 32 + return (voltage / 3.3) * 4095 * self.scale def from_raw_to_voltage(self, raw: pt.AnalogValue) -> pt.Voltage: - return (raw / 4095 / 32) * 3.3 + return (raw / 4095 / self.scale) * 3.3 def check_on_gain(self, value: pt.Voltage, tol: float = 0.85) -> None: # pico has no gain. diff --git a/update_scripts/upcoming/update.sql b/update_scripts/upcoming/update.sql deleted file mode 100644 index c98ec899..00000000 --- a/update_scripts/upcoming/update.sql +++ /dev/null @@ -1,10 +0,0 @@ -# update.sql - -# Actually, I'm not going to do this. These can take a _long_ time to execute. - -# CREATE INDEX IF NOT EXISTS stirring_rates_ix -# ON stirring_rates (experiment, pioreactor_unit); -# -# CREATE INDEX IF NOT EXISTS pwm_dcs_ix -# ON pwm_dcs (experiment, pioreactor_unit); -#