From 9481194ed0f0d7132c7579f84ef52bfeb4292a41 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 21 Jul 2026 23:09:36 -0700 Subject: [PATCH 1/5] docs: add device driver contributor guide Add a guide covering how to write a PyLabRobot device driver and its hello-world tutorial notebook: recovering the protocol, driver structure (single plain class, idempotent public API, OS-agnostic), code style, the notebook shape and cell rules, and verify/ship steps. Replace the new-machine-type and new-concrete-backend pages with this guide, wire it into the contributor-guide toctree, and repoint their redirects. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/conf.py | 4 +- docs/contributor_guide/device-driver-guide.md | 90 +++++++++++++++++++ docs/contributor_guide/index.md | 3 +- .../contributor_guide/new-concrete-backend.md | 42 --------- docs/contributor_guide/new-machine-type.md | 68 -------------- 5 files changed, 93 insertions(+), 114 deletions(-) create mode 100644 docs/contributor_guide/device-driver-guide.md delete mode 100644 docs/contributor_guide/new-concrete-backend.md delete mode 100644 docs/contributor_guide/new-machine-type.md diff --git a/docs/conf.py b/docs/conf.py index d5676a7e9b2..ff4d9971b99 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -176,8 +176,8 @@ "installation.html": "user_guide/installation.html", "contributing.html": "contributor_guide/index.html", "configuration.html": "user_guide/configuration.html", - "new-machine-type.html": "contributor_guide/new_machine_type.html", - "new-concrete-backend.html": "contributor_guide/new_concrete_backend.html", + "new-machine-type.html": "contributor_guide/device-driver-guide.html", + "new-concrete-backend.html": "contributor_guide/device-driver-guide.html", "how-to-open-source.html": "contributor_guide/how_to_open_source.html", "basic.html": "user_guide/basic.html", "using-the-visualizer.html": "user_guide/using_the_visualizer.html", diff --git a/docs/contributor_guide/device-driver-guide.md b/docs/contributor_guide/device-driver-guide.md new file mode 100644 index 00000000000..07a2bc0f014 --- /dev/null +++ b/docs/contributor_guide/device-driver-guide.md @@ -0,0 +1,90 @@ +# Writing PyLabRobot Device Drivers & Hello-World Guides + +High level instructions for adding a new hardware device driver to PyLabRobot, plus the runnable tutorial that ships alongside it, for humans and agents. + +Before you start, it's worth posting on the [PyLabRobot forum](https://discuss.pylabrobot.org) to say what you're working on — it helps avoid duplicated effort and is a good place to get support. + +--- + +## 1. Understanding the device + +Before writing a line of driver code, recover the protocol. + +- **Extract, don't guess.** Work from whatever authoritative source you have — firmware/protocol documentation, log files produced by the manufacturer's software, or a reference binary. Pull out the real material: command frames, error codes, status values, and the exact human-readable text of every message. Don't leave placeholders — capture the *complete* set (e.g. every entry in an error-code switch), each filled with its real text. +- **Nail the wire format.** Establish transport (serial params, USB endpoint, socket), framing (start/end bytes, length fields, checksums), and the request/response handshake (echo? ack? busy→ok transitions?). Get it byte-exact. +- **Distinguish blocking from non-blocking operations.** Know which commands return immediately and which report `BUSY` until motion completes, and how a fault (e-stop, jam) surfaces. + +--- + +## 2. Driver structure + +Keep it small and idiomatic to PyLabRobot. + +- **Default to one file, one plain class.** While some older drivers use a Driver/Backend split or other capability machinery, that is the old architecture and shouldn't be used anymore. Instead, write a single plain class whose public methods are the device's operations, talking to the hardware through a PyLabRobot `io` transport. Folder = vendor, file = device (`pylabrobot//.py`); `__init__.py` re-exports. Promote the single `.py` module to a `/` package (a directory with `__init__.py`) split across several modules when it genuinely helps: a distinct subsystem, the wire protocol / framing layer, a large constant or command table. But only when warranted, not by reflex. +- **Use PyLabRobot's transport primitives** (`pylabrobot.io.serial.Serial`, etc.) rather than talking to the OS directly. Expose async `setup()` / `stop()` and public operation methods. +- **Stay OS-agnostic:** no OS-specific libraries or DLLs. The driver must work on Windows, Mac, and Linux — this is what keeps experiments portable and reproducible. +- **Prefer string literals over enums** for user-facing modes. A `Literal["standard", "head", "pump"]` argument plus an internal dict mapping those to wire codes reads better at the call site than an enum import. +- **Wire up the docs** the same way every other device does: add `docs/api/pylabrobot..rst` plus a line in `docs/api/pylabrobot.rst`. + +### Idempotent public API + +The public surface must expose **no non-idempotent commands.** + +- If the hardware only offers a raw toggle/flip primitive, keep it private (`_toggle_x`) and expose move-to-state methods (`move_x_out` / `move_x_in`) that read current state, act only if needed, then confirm. +- This makes the API safe to call repeatedly and safe to reason about — the caller states intent ("be open"), not a blind toggle. + +### Unverified drivers + +If the driver has not been checked against real hardware, say so loudly. `setup()` should log a warning that the driver is untested and invite a change once someone verifies it on their device. Don't quietly present untested code as ready. + +--- + +## 3. Code style + +- **Comments document what the code does, not its history.** No "NEW", "now", "previously", "used to", "we changed". The code is not a diary or a changelog. State behavior as fact. Plain rationale ("why") is welcome; before/after narration and emphasis-caps are not. +- **No provenance stories.** Describe what the thing *is* and what it does. The archaeology of how it was produced belongs in your own notes, not the shipped repo. +- **No dead code.** Drop unused constants and leftover scaffolding. +- **No one-time scripts in the repo.** The codebase holds permanent software only. Run backfills, migrations, and throwaway jobs ad-hoc — as thin inline invocations that reuse the module's own functions — never as committed scripts. Keep reusable primitives in the main module so a one-off run is a one-liner. + +--- + +## 4. The hello-world tutorial + +Every device ships with a runnable "hello-world" notebook so a user can go from cabling to first successful command. They all follow one shape. + +**Location & wiring:** + +- File: `docs/user_guide///hello-world.ipynb`. +- Add `/index.md` with a `{toctree}` listing `/hello-world`. +- Add `/index` to the Manufacturers `{toctree}` in `docs/user_guide/index.md` (keep it alphabetical). +- The API-reference `.rst` is separate from the user-guide notebook. + +**Structure** (alternating markdown cell → code cell): + +1. **Title + description + property table** (comms, serial settings, framing, value ranges) + a warning admonition if the driver is untested. +2. **"How it talks"** — a brief protocol overview. Do not get into details as the users care about using the machine and not about what happens on the wire. +3. **"Physical setup"** — cabling and connection parameters. +4. **"Connect"** — `setup()`. +5. **One section per operation** — status, load, prime, run, home, disconnect, and so on. + +**Cell rules:** + +- **One concept per code cell.** The deciding question: *would a user run this as one action or two?* If a physical action happens in between (place a plate, attach a tube), it's two cells. Loading a plate = `move_tray_out()` → *(place plate)* → `move_tray_in()` is two cells. Closely related read-only checks can share a cell. +- **Every code cell gets a preceding markdown cell** that explains it. +- **Use the idempotent public API** in every example — `move_tray_out()`, never a raw toggle. +- **Notebook mechanics:** edit with a notebook-aware tool (plain text replacement is blocked on `.ipynb`). Code cells use `execution_count: null` and empty `outputs`; `nbformat: 4`, `nbformat_minor: 5`; every cell has an `id`. Validate the JSON parses before shipping. + +--- + +## 5. Verify & ship + +- **Lint and type-check** with the repo's own tooling (ruff + mypy, 2-space indent). +- **Show the commit and PR text and get confirmation before committing.** +- **Never touch real hardware without an explicit, per-run go-ahead.** A previous approval does not carry to the next run. + +--- + +## 6. Working style + +- **Do exactly what's asked and nothing more.** No unrequested extras bundled in — no rewriting existing comments, no changing timeouts or constants, no redesigning logic, no docs/config side-quests. If a fix seems to need more, state the minimal fact and let the person decide. +- **Run at full speed.** Don't artificially throttle or slow-roll work; parallelize independent steps. diff --git a/docs/contributor_guide/index.md b/docs/contributor_guide/index.md index 06292a05ccd..4f3f8feb1fb 100644 --- a/docs/contributor_guide/index.md +++ b/docs/contributor_guide/index.md @@ -14,8 +14,7 @@ contributing-to-docs :maxdepth: 2 :caption: Adding Backends/Drivers -new-machine-type -new-concrete-backend +device-driver-guide ```
diff --git a/docs/contributor_guide/new-concrete-backend.md b/docs/contributor_guide/new-concrete-backend.md deleted file mode 100644 index 49420e133d0..00000000000 --- a/docs/contributor_guide/new-concrete-backend.md +++ /dev/null @@ -1,42 +0,0 @@ -# Adding Support for a New Machine of an Existing Type - -This guide explains how to add support for a new machine of an existing type. For example, if you want to add support for a new liquid handler, you should read this guide. If you want to add support for a new type of machine, you should read {doc}`this guide ` first. - -The machine types that are currently can be found [here](/user_guide/machines). - -Two documents that you can read before you start are: - -- [CONTRIBUTING.md](https://github.com/PyLabRobot/pylabrobot/blob/main/CONTRIBUTING.md): This document contains general information about contributing to PyLabRobot, and covers things like installation and testing. -- [How to Open Source](https://docs.pylabrobot.org/how-to-open-source.html): This document contains step-by-step instructions for contributing to an open source project. It is not specific to PyLabRobot, and serves as a reference. - -Thank you for contributing to PyLabRobot! - -## Background - -Backends are minimal classes that are responsible for communicating with a machine and are thus specific to one machine. Frontends are higher level classes that are responsible for orchestrating higher-level state and providing nice interfaces to users, and should work with any machine. For example, the STAR liquid handler backend is responsible for executing the liquid handling operations on a Hamilton STAR, while the LiquidHandler frontend is responsible for making sure a requested operation is valid given the current state of the deck. - -Backends should contain minimal state. We prefer to manage the state in the frontend, because this allows us to share the code across all machines of a type. For example, the liquid handler backend does not contain any information about the deck, because this is managed by the frontend. If a certain machine has a specific state that needs to be managed, like whether the gripper arm is parked on a liquid handling robot, that should be done by the backend because it is specific to the machine. - -## 0. Get in touch - -Please make a post on [the PyLabRobot Development forum](https://discuss.pylabrobot.org) to let us know what you are working on. This will help you avoid duplicating work, and it is also a good place to get support. - -## 1. Creating a new concrete backend class - -It is easiest to start by copying the abstract base class for the machine type to a new file. You will find this in `backend.py` in the module for the machine type. For example, the liquid handling abstract base class is located at `pylabrobot.liquid_handling.backends.backend`. You should copy this file to a new file called `.py` in the same directory. For example, the liquid handling backend for the Hamilton STAR is located at `pylabrobot.liquid_handling.backends.hamilton.STAR`. - -## 2. Implementing the abstract methods - -The abstract base class contains a number of abstract methods. These are the methods that are expected to be implemented by the concrete backend. You should implement these methods in the concrete backend. You can use the abstract base class as a reference for what the methods should do. - -PyLabRobot aims to be OS-agnostic, meaning that it should work on Windows, Mac, and Linux. This maximizes flexibility for users and the reproducibility of experiments. However, this also means that you should not use any OS-specific libraries or dlls in the backend. - -If an operation is not supported by the machine, you should raise a `NotImplementedError`. - -The actual process of implementing the methods varies widely from machine to machine. It is generally useful to search for firmware documents, search for log files generated by a manufacturer's software, or find other open source projects that have implemented the same machine. - -## 3. Adding documentation (recommended) - -Find the relevant module in the `docs` directory. For example, the liquid handling backends module is located at `docs/pylabrobot.liquid_handling.backends.rst`. Then, add the name of the new backend to make sure that the new backend is automatically documented in the API reference. - -If you want, you can also add a new page to the `docs` directory that explains how to use the new backend. This is not required, but it is strongly recommended. Experience has shown that this is the best way to get people to actually use the new backend. diff --git a/docs/contributor_guide/new-machine-type.md b/docs/contributor_guide/new-machine-type.md deleted file mode 100644 index 165eef0ca24..00000000000 --- a/docs/contributor_guide/new-machine-type.md +++ /dev/null @@ -1,68 +0,0 @@ -# Contributing a New Type of Machine to PLR - -PyLabRobot supports a number of different types of machines. They can be found [here](/user_guide/machines). - -If you want to add support for a new type of machine, this guide will explain the process. If you want to add a new machine for a type that already exists, you should read {doc}`this guide ` instead. - -This guide is not a definitive step-by-step guide (otherwise we would have automated it), but rather a collection of high-level ideas and suggestions. Often, it only becomes clear what the best abstractions are after two or more machines for a type have been implemented, so it is totally valid (and encouraged) to make some assumptions and then refactor later. - -Two documents that you can read before you start are: - -- [CONTRIBUTING.md](https://github.com/PyLabRobot/pylabrobot/blob/main/CONTRIBUTING.md): This document contains general information about contributing to PyLabRobot, and covers things like installation and testing. -- [How to Open Source](https://docs.pylabrobot.org/how-to-open-source.html): This document contains step-by-step instructions for contributing to an open source project. It is not specific to PyLabRobot, and serves as a reference. - -Thank you for contributing to PyLabRobot! - -## 0. Get in touch - -Please make a post on [the PyLabRobot Development forum](https://discuss.pylabrobot.org) to let us know what you are working on. This will help you avoid duplicating work, and it is also a good place to get support. - -## 1. Creating a new module - -Each machine type has its own module in PLR. For example, the liquid handling module is located at `pylabrobot.liquid_handling`. This module contains: - -- the machine front end: the user-facing API for the machine type. Example: `LiquidHandler`. -- the abstract base class for the machine type: the minimal set of atomic commands that the machine type is expected to support. Example: `LiquidHandlerBackend`. -- the concrete backends: the actual implementations of the abstract base class for specific machines. See {doc}`the concrete backends guide ` for more information. Example: `STAR`. - -## 2. Creating a new abstract backend class - -Abstract backends are used to define the interface for a type of machine in terms of the minimal set of atomic commands. For example, all liquid handlers should have an `aspirate` method. - -The commands should be interactive and minimal. Interactive means that the command is expected to be executed immediately when its method is called. Minimal means the commands cannot be broken into sub-commands that a user would reasonably want to use. For example, the abstract liquid handler backend contains commands for `aspirate` and `dispense`, but not `transfer` (a convenience method that exists on the frontend). At the same time, `aspirate` does move the pipetting head to a certain location because this move and the actual aspiration are reasonably expected to always occur together. For new machines, it is fine to make some assumptions and revisit them later. - -The purpose of minimality is to make adding new concrete backends as easy as possible. The purpose of interactivity is to make the iteration cycle when developing new methods as short as possible. - -You must put the abstract base class in `backend.py` in the module you created in step 1. The abstract class {class}`~pylabrobot.machine.MachineBackend` must be used as the base class for all backends. This class defines the `setup` and `stop` methods, which are used to initialize and stop the machine. - -## 3. Creating a new front end class - -Front ends are used to define the user-facing interface for a specific machine type, and shared across all machines of this type in PLR. They expose the atomic backend commands in addition to providing higher level utilities and orchestrating state. For example, `LiquidHandler` has a `transfer` method that is not defined in the abstract backend (it is not minimal), but instead simply calls `aspirate` and `dispense` on the backend. This way, the `transfer` implementation is shared across all supported liquid handling robots. `LiquidHandler` also maintains a reference to the deck, to make sure the requested operations are valid given the current state of the deck. - -The abstract class {class}`~pylabrobot.machine.MachineFrontend` must be used as the base class for all front ends. This class defines the `setup` and `stop` methods, which are used to initialize and stop the machine. It also defines the `backend` attribute, which is used to access the backend. - -You should put the front end in a file called `.py` in the module you created in step 1. For example, the liquid handling front end is located at `pylabrobot.liquid_handling.liquid_handler.py`. - -If your devices updates the resource tree or its state, the front end should handle this. See [the resources guide](/resources/introduction.md) for more information. - -## 4. Creating a new concrete backend for a specific machine - -Refer to the {doc}`the concrete backends guide `. - -## 5. Adding documentation (strongly recommended) - -Each module should have a corresponding documentation page in the `docs` directory. Experience has shown that this is the best way to get people to actually use the new module. - -### API documentation - -API documentation is generated automatically based on docstrings, but has to be linked to from the main API documentation. - -1. Create a new file in the `docs` directory called `pylabrobot..rst`. You can look at the existing files for examples. -2. Add a link to this file to the API documentation in [`docs/pylabrobot.rst`](https://github.com/PyLabRobot/pylabrobot/blob/main/docs/pylabrobot.rst). - -### Brief introduction - -It is also recommended to add a brief introduction to the module which explains what it is and how to use it. You can write this introduction in Markdown, reStructuredText, or a Jupyter notebook (recommended). You can look at [`basic.ipynb`](https://github.com/PyLabRobot/pylabrobot/blob/main/docs/basic.ipynb) for an example. - -1. Put the introduction in the `docs` folder. -2. Link to the new file from [`docs/index.rst`](https://github.com/PyLabRobot/pylabrobot/blob/main/docs/index.rst). From 55401b9116e0fde5629d734c4ed92c8679d0a387 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 21 Jul 2026 23:15:14 -0700 Subject: [PATCH 2/5] docs: add AGENTS.md pointing to the device driver guide Add a short root AGENTS.md orienting agents to the contributor guides and the core working principles (tight scope, no diary/provenance comments, no one-time scripts, lint with ruff+mypy, hardware needs approval). Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..6ba2db8b33f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# Agent guide + +Orientation for agents (and humans) working in this repo. + +## Guides + +- **Writing a device driver + its hello-world notebook:** [`docs/contributor_guide/device-driver-guide.md`](docs/contributor_guide/device-driver-guide.md) + +## Working principles + +- **Scope tightly.** Make the change that was asked for and nothing else — no drive-by rewrites of comments, constants, or logic, no unrequested docs/config side-quests. +- **Code documents what it does, not its history.** No diary/changelog comments ("NEW", "now", "previously"), no provenance/origin stories in code, commits, or PRs. Describe what the thing *is*. +- **Keep the repo permanent.** No one-time/backfill/migration scripts committed — run those ad-hoc. +- **Lint before shipping:** ruff + mypy (2-space indent), matching the repo's config. +- **Never drive real hardware without explicit, per-run approval.** From 213b759b1c6787152ab20d0f4d05428e0628cd16 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 21 Jul 2026 23:22:19 -0700 Subject: [PATCH 3/5] docs: tighten device driver guide Condense the guide to be more concise and directive while keeping the framing and rationale. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/contributor_guide/device-driver-guide.md | 91 ++++++------------- 1 file changed, 29 insertions(+), 62 deletions(-) diff --git a/docs/contributor_guide/device-driver-guide.md b/docs/contributor_guide/device-driver-guide.md index 07a2bc0f014..fb04b9ec446 100644 --- a/docs/contributor_guide/device-driver-guide.md +++ b/docs/contributor_guide/device-driver-guide.md @@ -1,90 +1,57 @@ # Writing PyLabRobot Device Drivers & Hello-World Guides -High level instructions for adding a new hardware device driver to PyLabRobot, plus the runnable tutorial that ships alongside it, for humans and agents. +How to add a device driver and its hello-world notebook, for humans and agents. Post on the [PyLabRobot forum](https://discuss.pylabrobot.org) before starting to avoid duplicated effort and get support. -Before you start, it's worth posting on the [PyLabRobot forum](https://discuss.pylabrobot.org) to say what you're working on — it helps avoid duplicated effort and is a good place to get support. +## 1. Understand the device ---- +Recover the protocol before writing code. -## 1. Understanding the device +- **Extract, don't guess.** Work from an authoritative source — firmware/protocol docs, manufacturer log files, or a reference binary. Capture the *complete* set of command frames, error codes, status values, and exact message text, filled with real values, not placeholders. +- **Get the wire format byte-exact:** transport (serial params / USB endpoint / socket), framing (delimiters, length fields, checksums), handshake (echo? ack? busy→ok?). +- **Note blocking vs non-blocking commands** and how faults (e-stop, jam) surface. -Before writing a line of driver code, recover the protocol. - -- **Extract, don't guess.** Work from whatever authoritative source you have — firmware/protocol documentation, log files produced by the manufacturer's software, or a reference binary. Pull out the real material: command frames, error codes, status values, and the exact human-readable text of every message. Don't leave placeholders — capture the *complete* set (e.g. every entry in an error-code switch), each filled with its real text. -- **Nail the wire format.** Establish transport (serial params, USB endpoint, socket), framing (start/end bytes, length fields, checksums), and the request/response handshake (echo? ack? busy→ok transitions?). Get it byte-exact. -- **Distinguish blocking from non-blocking operations.** Know which commands return immediately and which report `BUSY` until motion completes, and how a fault (e-stop, jam) surfaces. - ---- - -## 2. Driver structure +## 2. Structure the driver Keep it small and idiomatic to PyLabRobot. -- **Default to one file, one plain class.** While some older drivers use a Driver/Backend split or other capability machinery, that is the old architecture and shouldn't be used anymore. Instead, write a single plain class whose public methods are the device's operations, talking to the hardware through a PyLabRobot `io` transport. Folder = vendor, file = device (`pylabrobot//.py`); `__init__.py` re-exports. Promote the single `.py` module to a `/` package (a directory with `__init__.py`) split across several modules when it genuinely helps: a distinct subsystem, the wire protocol / framing layer, a large constant or command table. But only when warranted, not by reflex. -- **Use PyLabRobot's transport primitives** (`pylabrobot.io.serial.Serial`, etc.) rather than talking to the OS directly. Expose async `setup()` / `stop()` and public operation methods. -- **Stay OS-agnostic:** no OS-specific libraries or DLLs. The driver must work on Windows, Mac, and Linux — this is what keeps experiments portable and reproducible. -- **Prefer string literals over enums** for user-facing modes. A `Literal["standard", "head", "pump"]` argument plus an internal dict mapping those to wire codes reads better at the call site than an enum import. -- **Wire up the docs** the same way every other device does: add `docs/api/pylabrobot..rst` plus a line in `docs/api/pylabrobot.rst`. +- **One file, one plain class.** The old Driver/Backend split and capability machinery are deprecated — don't use them. Instead write a single plain class whose public methods are the device's operations, talking to hardware through a `pylabrobot.io` transport. Path `pylabrobot//.py`, re-exported from `__init__.py`. Promote to a `/` package only when it genuinely helps — a distinct subsystem, the protocol/framing layer, or a large command table — not by reflex. +- **Stay OS-agnostic:** no OS-specific libraries or DLLs. Running on Windows, Mac, and Linux is what keeps experiments portable and reproducible. +- **Async `setup()` / `stop()`** plus public operation methods, over PyLabRobot's transport primitives (`pylabrobot.io.serial.Serial`, etc.) — never the OS directly. +- **Modes as `Literal[...]`, not enums.** A `Literal["standard", "head", "pump"]` argument plus an internal dict mapping to wire codes reads better at the call site than an enum import. +- **API docs:** add `docs/api/pylabrobot..rst` plus a line in `docs/api/pylabrobot.rst`. ### Idempotent public API -The public surface must expose **no non-idempotent commands.** - -- If the hardware only offers a raw toggle/flip primitive, keep it private (`_toggle_x`) and expose move-to-state methods (`move_x_out` / `move_x_in`) that read current state, act only if needed, then confirm. -- This makes the API safe to call repeatedly and safe to reason about — the caller states intent ("be open"), not a blind toggle. +The public surface must expose **no non-idempotent commands.** If the hardware only offers a raw toggle/flip, keep it private (`_toggle_x`) and expose move-to-state methods (`move_x_out` / `move_x_in`) that read current state, act only if needed, then confirm. This keeps the API safe to call repeatedly — the caller states intent ("be open"), not a blind toggle. ### Unverified drivers -If the driver has not been checked against real hardware, say so loudly. `setup()` should log a warning that the driver is untested and invite a change once someone verifies it on their device. Don't quietly present untested code as ready. - ---- +If the driver hasn't been checked against real hardware, say so loudly: `setup()` should `logger.warning(...)` that it's untested and invite a change once someone verifies it. Don't quietly present untested code as ready. ## 3. Code style -- **Comments document what the code does, not its history.** No "NEW", "now", "previously", "used to", "we changed". The code is not a diary or a changelog. State behavior as fact. Plain rationale ("why") is welcome; before/after narration and emphasis-caps are not. -- **No provenance stories.** Describe what the thing *is* and what it does. The archaeology of how it was produced belongs in your own notes, not the shipped repo. -- **No dead code.** Drop unused constants and leftover scaffolding. -- **No one-time scripts in the repo.** The codebase holds permanent software only. Run backfills, migrations, and throwaway jobs ad-hoc — as thin inline invocations that reuse the module's own functions — never as committed scripts. Keep reusable primitives in the main module so a one-off run is a one-liner. - ---- +- **Comments document what the code does, not its history.** No "NEW", "now", "previously", no emphasis-caps — the code is not a diary. State behavior as fact; plain rationale ("why") is welcome. +- **No provenance stories.** Describe what the thing *is*, not where it came from — in code, commits, or PRs. +- **No dead code**, and **no one-time scripts in the repo.** The codebase holds permanent software only; run backfills/migrations ad-hoc as thin inline invocations of the module's own functions. -## 4. The hello-world tutorial +## 4. Hello-world notebook -Every device ships with a runnable "hello-world" notebook so a user can go from cabling to first successful command. They all follow one shape. +Every device ships a runnable notebook that takes a user from cabling to first command. Path `docs/user_guide///hello-world.ipynb`; wire it in via the `/index.md` `{toctree}` and add `/index` to the Manufacturers `{toctree}` in `docs/user_guide/index.md` (alphabetical). -**Location & wiring:** +Sections (markdown cell then code cell): (1) title + property table + untested-warning; (2) how it talks — brief, since users care about the machine, not the wire; (3) physical setup; (4) `setup()`; (5) one section per operation. -- File: `docs/user_guide///hello-world.ipynb`. -- Add `/index.md` with a `{toctree}` listing `/hello-world`. -- Add `/index` to the Manufacturers `{toctree}` in `docs/user_guide/index.md` (keep it alphabetical). -- The API-reference `.rst` is separate from the user-guide notebook. - -**Structure** (alternating markdown cell → code cell): - -1. **Title + description + property table** (comms, serial settings, framing, value ranges) + a warning admonition if the driver is untested. -2. **"How it talks"** — a brief protocol overview. Do not get into details as the users care about using the machine and not about what happens on the wire. -3. **"Physical setup"** — cabling and connection parameters. -4. **"Connect"** — `setup()`. -5. **One section per operation** — status, load, prime, run, home, disconnect, and so on. - -**Cell rules:** - -- **One concept per code cell.** The deciding question: *would a user run this as one action or two?* If a physical action happens in between (place a plate, attach a tube), it's two cells. Loading a plate = `move_tray_out()` → *(place plate)* → `move_tray_in()` is two cells. Closely related read-only checks can share a cell. -- **Every code cell gets a preceding markdown cell** that explains it. -- **Use the idempotent public API** in every example — `move_tray_out()`, never a raw toggle. -- **Notebook mechanics:** edit with a notebook-aware tool (plain text replacement is blocked on `.ipynb`). Code cells use `execution_count: null` and empty `outputs`; `nbformat: 4`, `nbformat_minor: 5`; every cell has an `id`. Validate the JSON parses before shipping. - ---- +Cell rules: +- **One concept per code cell.** If a physical action happens between steps, that's two cells: `move_tray_out()` → place plate → `move_tray_in()`. Every code cell gets a preceding markdown cell. +- **Use the idempotent public API** in examples, never a raw toggle. +- **Notebook JSON:** edit with a notebook-aware tool (plain-text replace is blocked on `.ipynb`); code cells `execution_count: null`, empty `outputs`; `nbformat: 4`, `nbformat_minor: 5`; every cell has an `id`; validate it parses. ## 5. Verify & ship -- **Lint and type-check** with the repo's own tooling (ruff + mypy, 2-space indent). -- **Show the commit and PR text and get confirmation before committing.** -- **Never touch real hardware without an explicit, per-run go-ahead.** A previous approval does not carry to the next run. - ---- +- **Lint + type-check:** ruff + mypy, 2-space indent. +- **Show commit + PR text and get confirmation before committing.** +- **Never drive real hardware without explicit per-run approval** — a previous OK doesn't carry to the next run. ## 6. Working style -- **Do exactly what's asked and nothing more.** No unrequested extras bundled in — no rewriting existing comments, no changing timeouts or constants, no redesigning logic, no docs/config side-quests. If a fix seems to need more, state the minimal fact and let the person decide. -- **Run at full speed.** Don't artificially throttle or slow-roll work; parallelize independent steps. +- **Do exactly what's asked, nothing more** — no drive-by edits to comments/constants/logic, no side-quests. If a fix seems to need more, state the minimal fact and let the person decide. +- **Run at full speed;** parallelize independent work. From 704ca3f5caf5c94cbb5662a03c80e1bf692a88cb Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 21 Jul 2026 23:24:47 -0700 Subject: [PATCH 4/5] docs: broaden enum guidance in device driver guide Prefer string Literals over enums generally, especially user-facing ones; allow IntEnum in narrow internal cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/contributor_guide/device-driver-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributor_guide/device-driver-guide.md b/docs/contributor_guide/device-driver-guide.md index fb04b9ec446..604fe164fd1 100644 --- a/docs/contributor_guide/device-driver-guide.md +++ b/docs/contributor_guide/device-driver-guide.md @@ -17,7 +17,7 @@ Keep it small and idiomatic to PyLabRobot. - **One file, one plain class.** The old Driver/Backend split and capability machinery are deprecated — don't use them. Instead write a single plain class whose public methods are the device's operations, talking to hardware through a `pylabrobot.io` transport. Path `pylabrobot//.py`, re-exported from `__init__.py`. Promote to a `/` package only when it genuinely helps — a distinct subsystem, the protocol/framing layer, or a large command table — not by reflex. - **Stay OS-agnostic:** no OS-specific libraries or DLLs. Running on Windows, Mac, and Linux is what keeps experiments portable and reproducible. - **Async `setup()` / `stop()`** plus public operation methods, over PyLabRobot's transport primitives (`pylabrobot.io.serial.Serial`, etc.) — never the OS directly. -- **Modes as `Literal[...]`, not enums.** A `Literal["standard", "head", "pump"]` argument plus an internal dict mapping to wire codes reads better at the call site than an enum import. +- **Prefer string `Literal[...]` over enums**, especially anything user-facing. A `Literal["standard", "head", "pump"]` argument plus an internal dict mapping to wire codes reads better at the call site than an enum import. `IntEnum` is fine in narrow internal cases (e.g. a fixed set of wire/register codes never exposed to callers). - **API docs:** add `docs/api/pylabrobot..rst` plus a line in `docs/api/pylabrobot.rst`. ### Idempotent public API From 1a87001789239e73c8f69ce59d344eb1c52e6fde Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Tue, 21 Jul 2026 23:24:59 -0700 Subject: [PATCH 5/5] docs: drop redundant idempotent-API cell rule The idempotent public API is already covered in the driver-structure section. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/contributor_guide/device-driver-guide.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/contributor_guide/device-driver-guide.md b/docs/contributor_guide/device-driver-guide.md index 604fe164fd1..0d1351a1e56 100644 --- a/docs/contributor_guide/device-driver-guide.md +++ b/docs/contributor_guide/device-driver-guide.md @@ -42,7 +42,6 @@ Sections (markdown cell then code cell): (1) title + property table + untested-w Cell rules: - **One concept per code cell.** If a physical action happens between steps, that's two cells: `move_tray_out()` → place plate → `move_tray_in()`. Every code cell gets a preceding markdown cell. -- **Use the idempotent public API** in examples, never a raw toggle. - **Notebook JSON:** edit with a notebook-aware tool (plain-text replace is blocked on `.ipynb`); code cells `execution_count: null`, empty `outputs`; `nbformat: 4`, `nbformat_minor: 5`; every cell has an `id`; validate it parses. ## 5. Verify & ship