From bd3cd1b5f878d144c77bf35a3b85c6f8cd37acd1 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Mon, 20 Jul 2026 17:52:18 -0700 Subject: [PATCH 1/3] feat(big_bear): add BigBear orbital shaker driver A daisy-chained orbital shaker: one serial line addresses up to 16 shaker positions ("nests"), each by a 1-based hex id. Adds a single-class driver (BigBearOrbitalShaker) over pylabrobot.io.serial with start/stop shaking, homing, and serial-number readout. Serial: 9600 8N1, no handshake, carriage-return terminated. Commands are "@"; value sets are followed by a "@?" read-back and an "@Q" status poll. A rejected command replies "?:". Not verified against hardware; setup() emits a warning to that effect. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/api/pylabrobot.big_bear.rst | 15 ++ docs/api/pylabrobot.rst | 1 + pylabrobot/big_bear/__init__.py | 5 + pylabrobot/big_bear/orbital_shaker.py | 272 ++++++++++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 docs/api/pylabrobot.big_bear.rst create mode 100644 pylabrobot/big_bear/__init__.py create mode 100644 pylabrobot/big_bear/orbital_shaker.py diff --git a/docs/api/pylabrobot.big_bear.rst b/docs/api/pylabrobot.big_bear.rst new file mode 100644 index 00000000000..16c6cbf99bd --- /dev/null +++ b/docs/api/pylabrobot.big_bear.rst @@ -0,0 +1,15 @@ +.. currentmodule:: pylabrobot.big_bear + +pylabrobot.big_bear package +=========================== + +.. currentmodule:: pylabrobot.big_bear.orbital_shaker + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + :recursive: + + BigBearOrbitalShaker + OrbitalShakerSequence + BigBearError diff --git a/docs/api/pylabrobot.rst b/docs/api/pylabrobot.rst index 5343d347fd4..209d11b6420 100644 --- a/docs/api/pylabrobot.rst +++ b/docs/api/pylabrobot.rst @@ -34,6 +34,7 @@ Manufacturers pylabrobot.agilent pylabrobot.azenta + pylabrobot.big_bear pylabrobot.bmg_labtech pylabrobot.brooks pylabrobot.byonoy diff --git a/pylabrobot/big_bear/__init__.py b/pylabrobot/big_bear/__init__.py new file mode 100644 index 00000000000..908c31b2762 --- /dev/null +++ b/pylabrobot/big_bear/__init__.py @@ -0,0 +1,5 @@ +from .orbital_shaker import ( + BigBearError, + BigBearOrbitalShaker, + OrbitalShakerSequence, +) diff --git a/pylabrobot/big_bear/orbital_shaker.py b/pylabrobot/big_bear/orbital_shaker.py new file mode 100644 index 00000000000..b77677ffaf2 --- /dev/null +++ b/pylabrobot/big_bear/orbital_shaker.py @@ -0,0 +1,272 @@ +import asyncio +import enum +import logging +from typing import Optional + +from pylabrobot.io.serial import Serial + +logger = logging.getLogger(__name__) + + +# Line terminator: commands and replies are carriage-return terminated. +CR = "\r" + +# Reply returned when a command is rejected or the shaker cannot be reached. +ERROR_REPLY = "?:" + +MIN_RPM = 60 +MAX_RPM = 3570 +MIN_ACCELERATION = 1 +MAX_ACCELERATION = 10 +MIN_SHAKERS = 1 +MAX_SHAKERS = 16 + + +class OrbitalShakerSequence(enum.IntEnum): + """Rotation direction of a shake.""" + + CW = 1 + CCW = 2 + + +class BigBearError(Exception): + """Exceptions raised by a BigBear orbital shaker.""" + + def __init__(self, title: str, message: Optional[str] = None) -> None: + self.title = title + self.message = message + + def __str__(self) -> str: + return f"{self.title}: {self.message}" if self.message else self.title + + +class BigBearOrbitalShaker: + """BigBear orbital shaker. + + A daisy-chained orbital shaker: a single serial line drives up to 16 shaker + positions ("nests"), each addressed independently by a 1-based id sent as a + hexadecimal digit. + + Serial settings: + 9600 baud, 8 data bits, no parity, 1 stop bit, no handshake, "\\r" + terminator. + + Commands (each addressed command is "@" followed by the command chars, + where is the nest number in hex; a value-setting command is followed by + a "@?" read-back query): + U enter daisy-chain mode (unaddressed) + ~ report the number of shakers on the chain (unaddressed) + @V set speed (60..3570 rpm) + @A set acceleration (1..10, device scale) + @+ set direction clockwise + @- set direction counter-clockwise + @G start shaking + @S stop shaking + @F find home + @Q poll status (reply is read back) + @Y/X/Z serial-number fields + A rejected command or a comms failure replies "?:". + + Not verified: has NOT been tested against hardware in PyLabRobot. A warning is + emitted at setup. + """ + + def __init__( + self, + port: str, + num_shakers: int = 1, + timeout: float = 10.0, + command_delay: float = 0.1, + daisy_chain_settle: float = 5.0, + stop_settle: float = 6.0, + ): + if not MIN_SHAKERS <= num_shakers <= MAX_SHAKERS: + raise ValueError(f"num_shakers must be {MIN_SHAKERS}..{MAX_SHAKERS}") + self.num_shakers = num_shakers + self.command_delay = command_delay + self.daisy_chain_settle = daisy_chain_settle + self.stop_settle = stop_settle + self.io = Serial( + human_readable_device_name="BigBear Orbital Shaker", + port=port, + baudrate=9600, + bytesize=8, + parity="N", + stopbits=1, + timeout=timeout, + ) + + async def setup(self) -> None: + logger.warning( + "BigBearOrbitalShaker has NOT been tested against hardware in PyLabRobot. " + "Please make a PR to remove this message if you have verified it on your hardware." + ) + await self.io.setup() + await self._enter_daisy_chain() + await self._check_num_shakers() + logger.info("[BigBear %s] connected: num_shakers=%d", self.io.port, self.num_shakers) + + async def stop(self) -> None: + """Stop all shakers and close the serial connection.""" + try: + await self.stop_all() + finally: + await self.io.stop() + + # === Command layer === + + @staticmethod + def _address(device_id: int) -> str: + return "@" + format(device_id, "X") + + def _validate_device_id(self, device_id: int) -> None: + if not 1 <= device_id <= self.num_shakers: + raise ValueError(f"device_id must be 1..{self.num_shakers}") + + async def _send(self, command: str) -> None: + """Write a command and its terminator, then pace the bus.""" + await self.io.write((command + CR).encode("ascii")) + logger.debug("[BigBear] send: %s", command) + await asyncio.sleep(self.command_delay) + + async def _read_reply(self, timeout: Optional[float] = None) -> str: + """Read one CR-terminated reply, skipping empty lines. + + Returns the trimmed reply, or "" if the device sends nothing within the + serial read timeout. + """ + original = self.io.get_read_timeout() + if timeout is not None: + self.io.set_read_timeout(timeout) + try: + buf = bytearray() + while True: + char = await self.io.read(1) + if char == b"": # read timed out + break + if char == b"\r": + if buf: + break + continue # skip a bare terminator / empty line + if char == b"\n": + continue + buf += char + finally: + if timeout is not None: + self.io.set_read_timeout(original) + reply = buf.decode("ascii", errors="replace").strip() + logger.debug("[BigBear] recv: %s", reply) + return reply + + async def _poll(self, device_id: int, read_timeout: Optional[float] = None) -> str: + """Issue the status poll ("Q") for a nest and return its reply.""" + await self._send(self._address(device_id) + "Q") + return await self._read_reply(timeout=read_timeout) + + # === Parameters === + + async def _set_speed(self, device_id: int, rpm: int) -> None: + if not MIN_RPM <= rpm <= MAX_RPM: + raise ValueError(f"rpm must be {MIN_RPM}..{MAX_RPM}") + address = self._address(device_id) + await self._send(f"{address}V{rpm}") + await self._send(f"{address}?V{rpm}") + await self._read_reply() + + async def _set_acceleration(self, device_id: int, acceleration: int) -> None: + if not MIN_ACCELERATION <= acceleration <= MAX_ACCELERATION: + raise ValueError(f"acceleration must be {MIN_ACCELERATION}..{MAX_ACCELERATION}") + address = self._address(device_id) + await self._send(f"{address}A{acceleration}") + await self._send(f"{address}?A{acceleration}") + await self._read_reply() + + async def _set_sequence(self, device_id: int, sequence: OrbitalShakerSequence) -> None: + address = self._address(device_id) + direction = "+" if sequence is OrbitalShakerSequence.CW else "-" + await self._send(f"{address}{direction}") + await self._send(f"{address}?{direction}") + await self._read_reply() + + # === Public API === + + async def start_shaking( + self, + rpm: int, + acceleration: int = 1, + sequence: OrbitalShakerSequence = OrbitalShakerSequence.CW, + device_id: int = 1, + ) -> None: + """Set the parameters for a nest and start it shaking. + + Args: + rpm: shaking speed (60..3570). + acceleration: acceleration on the device's 1..10 scale. + sequence: rotation direction (clockwise or counter-clockwise). + device_id: nest to address (1..num_shakers). + """ + self._validate_device_id(device_id) + await self._set_acceleration(device_id, acceleration) + await self._set_speed(device_id, rpm) + await self._set_sequence(device_id, sequence) + await self._send(self._address(device_id) + "G") + if await self._poll(device_id) == ERROR_REPLY: + raise BigBearError(title="Starting the shaker failed", message=f"nest {device_id}") + logger.info("[BigBear %s] nest %d shaking at %d rpm", self.io.port, device_id, rpm) + + async def stop_shaking(self, device_id: int = 1) -> None: + """Stop shaking on a single nest.""" + self._validate_device_id(device_id) + await self._send(self._address(device_id) + "S") + if await self._poll(device_id, read_timeout=self.stop_settle) == ERROR_REPLY: + raise BigBearError(title="Stopping the shaker failed", message=f"nest {device_id}") + logger.info("[BigBear %s] nest %d stopped", self.io.port, device_id) + + async def stop_all(self) -> None: + """Stop shaking on every nest on the chain.""" + for device_id in range(1, self.num_shakers + 1): + await self._send(self._address(device_id) + "S") + await self._poll(device_id, read_timeout=self.stop_settle) + + async def find_home(self, device_id: int = 1) -> None: + """Home a single nest.""" + self._validate_device_id(device_id) + await self._send(self._address(device_id) + "F") + await self._poll(device_id) + logger.info("[BigBear %s] nest %d homed", self.io.port, device_id) + + async def home_all(self) -> None: + """Home every nest on the chain.""" + for device_id in range(1, self.num_shakers + 1): + await self._send(self._address(device_id) + "F") + await self._poll(device_id) + + async def get_serial_number(self, device_id: int = 1) -> str: + """Read the serial-number fields (Y, X, Z) of a nest and join them. + + The reply format is device-defined; the raw fields are returned as-is. + """ + self._validate_device_id(device_id) + address = self._address(device_id) + fields = [] + for command in ("Y", "X", "Z"): + await self._send(address + command) + fields.append(await self._read_reply()) + return "".join(fields) + + # === Setup helpers === + + async def _enter_daisy_chain(self) -> None: + await self._send("U") + await asyncio.sleep(self.daisy_chain_settle) + await self._read_reply() + + async def _check_num_shakers(self) -> None: + await self._send("U") + await self._send("~") + reply = await self._read_reply() + if str(self.num_shakers) not in reply: + raise BigBearError( + title="Shaker count mismatch", + message=f"expected {self.num_shakers}, device reported {reply!r}", + ) From 1d0238c9e854afecc57f882c608eff1422f36952 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Mon, 20 Jul 2026 17:55:06 -0700 Subject: [PATCH 2/3] docs(big_bear): add user guide for the orbital shaker Add a hello-world notebook covering daisy chaining, setup, per-nest and whole-chain start/stop, homing, and serial-number readout, plus the user-guide index entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/user_guide/big_bear/index.md | 7 + .../big_bear/orbital-shaker/hello-world.ipynb | 131 ++++++++++++++++++ docs/user_guide/index.md | 1 + 3 files changed, 139 insertions(+) create mode 100644 docs/user_guide/big_bear/index.md create mode 100644 docs/user_guide/big_bear/orbital-shaker/hello-world.ipynb diff --git a/docs/user_guide/big_bear/index.md b/docs/user_guide/big_bear/index.md new file mode 100644 index 00000000000..05ec2633e36 --- /dev/null +++ b/docs/user_guide/big_bear/index.md @@ -0,0 +1,7 @@ +# BigBear + +```{toctree} +:maxdepth: 1 + +orbital-shaker/hello-world +``` diff --git a/docs/user_guide/big_bear/orbital-shaker/hello-world.ipynb b/docs/user_guide/big_bear/orbital-shaker/hello-world.ipynb new file mode 100644 index 00000000000..528b5dfeb06 --- /dev/null +++ b/docs/user_guide/big_bear/orbital-shaker/hello-world.ipynb @@ -0,0 +1,131 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bigbear-intro", + "source": "# BigBear Orbital Shaker\n\nThe BigBear orbital shaker is a plate shaker with a serial (RS-232 / ASCII) interface. A single serial line can drive up to 16 shaker positions -- called **nests** -- wired together in a *daisy chain*, each addressed independently.\n\n| Property | Value |\n|---|---|\n| Communication | Serial, ASCII line protocol |\n| Serial settings | 9600 baud, 8 data bits, no parity, 1 stop bit, no handshake |\n| Line terminator | carriage return (`\\r`) |\n| Speed range | 60--3570 rpm |\n| Acceleration | 1--10 (device scale) |\n| Nests per chain | 1--16 |\n\n```{warning}\nThis driver has NOT been tested against hardware in PyLabRobot. The protocol,\nline terminator, and reply formats are reconstructed from the vendor software.\nIf you verify it on a shaker, please open a PR to remove the not-tested warning.\n```", + "metadata": {} + }, + { + "cell_type": "markdown", + "id": "bigbear-daisy-md", + "source": "## Daisy chaining\n\nMultiple shaker units share one serial connection: the host talks to the first unit, which is wired to the second, and so on down the chain. Each unit is a **nest** with a 1-based address.\n\nEvery command is addressed with an `@` prefix, where `` is the nest number written in **hexadecimal** -- so nest 10 is `@A` and nest 16 is `@10`. A command reaches only the nest it names; the others ignore it. This lets one port run up to 16 shakers with independent speed, direction, and start/stop.\n\nAt `setup()` the driver:\n\n1. sends `U` to put the chain into daisy-chain (addressing) mode,\n2. sends `~` to ask how many units are present, and\n3. checks that the reported count matches the `num_shakers` you passed.\n\nIf the counts disagree (a unit is off, unplugged, or `num_shakers` is wrong) setup raises a `BigBearError`. Set `num_shakers` to the number of physical units on the chain.", + "metadata": {} + }, + { + "cell_type": "markdown", + "id": "bigbear-setup-md", + "source": "## Physical setup\n\nConnect the shaker's serial port to your computer, typically through a USB-to-serial adapter. Chain any additional units in series and give the whole chain a single port.\n\nMake sure the shaker's serial parameters match the driver defaults (9600 baud, 8 data bits, no parity, 1 stop bit, no handshake).", + "metadata": {} + }, + { + "cell_type": "markdown", + "id": "bigbear-connect-md", + "source": "## Connect\n\n`setup()` opens the serial port, enters daisy-chain mode, and verifies the shaker count. It also logs the not-tested warning. Pass `num_shakers` to match your chain.", + "metadata": {} + }, + { + "cell_type": "code", + "id": "bigbear-connect-code", + "source": "from pylabrobot.big_bear import BigBearOrbitalShaker, OrbitalShakerSequence\n\nshaker = BigBearOrbitalShaker(port=\"/dev/ttyUSB0\", num_shakers=1) # replace with your port\nawait shaker.setup()", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "bigbear-shake-md", + "source": "## Start shaking\n\n`start_shaking()` sets the acceleration, speed, and direction for a nest and starts it. Shaking continues until you stop it. `device_id` selects which nest (default 1).", + "metadata": {} + }, + { + "cell_type": "code", + "id": "bigbear-shake-code", + "source": "await shaker.start_shaking(\n rpm=750,\n acceleration=3,\n sequence=OrbitalShakerSequence.CW,\n device_id=1,\n)", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "bigbear-stop-md", + "source": "## Stop shaking\n\nStop a single nest with `stop_shaking()`, or every nest on the chain with `stop_all()`.", + "metadata": {} + }, + { + "cell_type": "code", + "id": "bigbear-stop-code", + "source": "await shaker.stop_shaking(device_id=1)\n# or, for the whole chain:\n# await shaker.stop_all()", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "bigbear-multi-md", + "source": "## Driving multiple nests\n\nWith a multi-unit chain, address each nest by its `device_id`. Each runs independently.", + "metadata": {} + }, + { + "cell_type": "code", + "id": "bigbear-multi-code", + "source": "# Example for a 3-nest chain (create with num_shakers=3)\n# await shaker.start_shaking(rpm=500, device_id=1)\n# await shaker.start_shaking(rpm=900, sequence=OrbitalShakerSequence.CCW, device_id=2)\n# await shaker.start_shaking(rpm=1200, device_id=3)", + "metadata": {} + }, + { + "cell_type": "markdown", + "id": "bigbear-home-md", + "source": "## Homing\n\nHome a single nest with `find_home()`, or every nest with `home_all()`.", + "metadata": {} + }, + { + "cell_type": "code", + "id": "bigbear-home-code", + "source": "await shaker.find_home(device_id=1)\n# or, for the whole chain:\n# await shaker.home_all()", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "bigbear-info-md", + "source": "## Device info\n\nRead a nest's serial-number fields (`Y`, `X`, `Z`). The reply format is device-defined; the raw fields are returned joined.", + "metadata": {} + }, + { + "cell_type": "code", + "id": "bigbear-info-code", + "source": "print(\"Serial number:\", await shaker.get_serial_number(device_id=1))", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "id": "bigbear-teardown-md", + "source": "## Teardown\n\n`stop()` stops every nest and closes the serial connection.", + "metadata": {} + }, + { + "cell_type": "code", + "id": "bigbear-teardown-code", + "source": "await shaker.stop()", + "metadata": {}, + "execution_count": null, + "outputs": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index 2a66998a996..b4ecf19f47a 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -32,6 +32,7 @@ definitions agilent/index azenta/index +big_bear/index bmg_labtech/index brooks/index byonoy/index From 6114589dd56bd100ab215db5ad7f867c23c56be3 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Mon, 20 Jul 2026 17:58:59 -0700 Subject: [PATCH 3/3] refactor(big_bear): use a string literal for shake direction Replace the OrbitalShakerSequence enum with a Literal["CW", "CCW"] argument, validated at the call site. Also fix an invalid cell in the user-guide notebook (missing required code-cell fields). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/api/pylabrobot.big_bear.rst | 1 - .../big_bear/orbital-shaker/hello-world.ipynb | 12 +++++----- pylabrobot/big_bear/__init__.py | 6 +---- pylabrobot/big_bear/orbital_shaker.py | 22 ++++++++----------- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/docs/api/pylabrobot.big_bear.rst b/docs/api/pylabrobot.big_bear.rst index 16c6cbf99bd..adb6290553d 100644 --- a/docs/api/pylabrobot.big_bear.rst +++ b/docs/api/pylabrobot.big_bear.rst @@ -11,5 +11,4 @@ pylabrobot.big_bear package :recursive: BigBearOrbitalShaker - OrbitalShakerSequence BigBearError diff --git a/docs/user_guide/big_bear/orbital-shaker/hello-world.ipynb b/docs/user_guide/big_bear/orbital-shaker/hello-world.ipynb index 528b5dfeb06..c9052fa0ea0 100644 --- a/docs/user_guide/big_bear/orbital-shaker/hello-world.ipynb +++ b/docs/user_guide/big_bear/orbital-shaker/hello-world.ipynb @@ -27,7 +27,7 @@ { "cell_type": "code", "id": "bigbear-connect-code", - "source": "from pylabrobot.big_bear import BigBearOrbitalShaker, OrbitalShakerSequence\n\nshaker = BigBearOrbitalShaker(port=\"/dev/ttyUSB0\", num_shakers=1) # replace with your port\nawait shaker.setup()", + "source": "from pylabrobot.big_bear import BigBearOrbitalShaker\n\nshaker = BigBearOrbitalShaker(port=\"/dev/ttyUSB0\", num_shakers=1) # replace with your port\nawait shaker.setup()", "metadata": {}, "execution_count": null, "outputs": [] @@ -41,7 +41,7 @@ { "cell_type": "code", "id": "bigbear-shake-code", - "source": "await shaker.start_shaking(\n rpm=750,\n acceleration=3,\n sequence=OrbitalShakerSequence.CW,\n device_id=1,\n)", + "source": "await shaker.start_shaking(\n rpm=750,\n acceleration=3,\n sequence=\"CW\",\n device_id=1,\n)", "metadata": {}, "execution_count": null, "outputs": [] @@ -69,8 +69,10 @@ { "cell_type": "code", "id": "bigbear-multi-code", - "source": "# Example for a 3-nest chain (create with num_shakers=3)\n# await shaker.start_shaking(rpm=500, device_id=1)\n# await shaker.start_shaking(rpm=900, sequence=OrbitalShakerSequence.CCW, device_id=2)\n# await shaker.start_shaking(rpm=1200, device_id=3)", - "metadata": {} + "source": "# Example for a 3-nest chain (create with num_shakers=3)\n# await shaker.start_shaking(rpm=500, device_id=1)\n# await shaker.start_shaking(rpm=900, sequence=\"CCW\", device_id=2)\n# await shaker.start_shaking(rpm=1200, device_id=3)", + "metadata": {}, + "execution_count": null, + "outputs": [] }, { "cell_type": "markdown", @@ -128,4 +130,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/pylabrobot/big_bear/__init__.py b/pylabrobot/big_bear/__init__.py index 908c31b2762..d9424eab9e2 100644 --- a/pylabrobot/big_bear/__init__.py +++ b/pylabrobot/big_bear/__init__.py @@ -1,5 +1 @@ -from .orbital_shaker import ( - BigBearError, - BigBearOrbitalShaker, - OrbitalShakerSequence, -) +from .orbital_shaker import BigBearError, BigBearOrbitalShaker diff --git a/pylabrobot/big_bear/orbital_shaker.py b/pylabrobot/big_bear/orbital_shaker.py index b77677ffaf2..7c4c870d4f2 100644 --- a/pylabrobot/big_bear/orbital_shaker.py +++ b/pylabrobot/big_bear/orbital_shaker.py @@ -1,10 +1,11 @@ import asyncio -import enum import logging -from typing import Optional +from typing import Literal, Optional from pylabrobot.io.serial import Serial +Sequence = Literal["CW", "CCW"] + logger = logging.getLogger(__name__) @@ -22,13 +23,6 @@ MAX_SHAKERS = 16 -class OrbitalShakerSequence(enum.IntEnum): - """Rotation direction of a shake.""" - - CW = 1 - CCW = 2 - - class BigBearError(Exception): """Exceptions raised by a BigBear orbital shaker.""" @@ -181,9 +175,11 @@ async def _set_acceleration(self, device_id: int, acceleration: int) -> None: await self._send(f"{address}?A{acceleration}") await self._read_reply() - async def _set_sequence(self, device_id: int, sequence: OrbitalShakerSequence) -> None: + async def _set_sequence(self, device_id: int, sequence: Sequence) -> None: + if sequence not in ("CW", "CCW"): + raise ValueError('sequence must be "CW" or "CCW"') address = self._address(device_id) - direction = "+" if sequence is OrbitalShakerSequence.CW else "-" + direction = "+" if sequence == "CW" else "-" await self._send(f"{address}{direction}") await self._send(f"{address}?{direction}") await self._read_reply() @@ -194,7 +190,7 @@ async def start_shaking( self, rpm: int, acceleration: int = 1, - sequence: OrbitalShakerSequence = OrbitalShakerSequence.CW, + sequence: Sequence = "CW", device_id: int = 1, ) -> None: """Set the parameters for a nest and start it shaking. @@ -202,7 +198,7 @@ async def start_shaking( Args: rpm: shaking speed (60..3570). acceleration: acceleration on the device's 1..10 scale. - sequence: rotation direction (clockwise or counter-clockwise). + sequence: rotation direction, "CW" (clockwise) or "CCW" (counter-clockwise). device_id: nest to address (1..num_shakers). """ self._validate_device_id(device_id)