opentron-glulac-readout-module 2.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opentron_glulac_readout_module-2.0.3/LICENCE +21 -0
- opentron_glulac_readout_module-2.0.3/PKG-INFO +245 -0
- opentron_glulac_readout_module-2.0.3/README.md +230 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/__init__.py +5 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentron_glulac_readout/__init__.py +0 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentron_glulac_readout/jobst_sensor_module.py +118 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentron_glulac_readout/opentron_readout.py +1232 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentron_glulac_readout/volume_manager.py +76 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentrons_photo_module/__init__.py +0 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentrons_photo_module/opentron_photo.py +83 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/PKG-INFO +245 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/SOURCES.txt +15 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/dependency_links.txt +1 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/requires.txt +2 -0
- opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/top_level.txt +1 -0
- opentron_glulac_readout_module-2.0.3/pyproject.toml +42 -0
- opentron_glulac_readout_module-2.0.3/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 WisTex TechSero Ltd. Co.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opentron_glulac_readout_module
|
|
3
|
+
Version: 2.0.3
|
|
4
|
+
Summary: A TU Vienna university project, building a system that allow plugging a Jobst SIX Biotransmitter into the Opentron OT2 automated liquid handler system and then automates measurements with a custom printed labware
|
|
5
|
+
Author-email: Michael Höller <pypi.capably513@passmail.net>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.cellchipgroup.org/
|
|
8
|
+
Keywords: tuvienna,opentron,cellchip
|
|
9
|
+
Requires-Python: <3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENCE
|
|
12
|
+
Requires-Dist: pyserial>=3.5
|
|
13
|
+
Requires-Dist: opentrons>=9.0.0
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# opentron_glulac_readout_module
|
|
17
|
+
|
|
18
|
+
A Python library for reading amperometric sensor data from the **Jobst Six Biotransmitter** when connected to an **Opentrons OT-2** automatic liquid handler (ALH).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Table of Contents
|
|
23
|
+
|
|
24
|
+
- [Overview](#overview)
|
|
25
|
+
- [Installation](#installation)
|
|
26
|
+
- [Modules](#modules)
|
|
27
|
+
- [opentrons_photo_module](#opentrons_photo_module)
|
|
28
|
+
- [opentron_glulac_readout](#opentron_glulac_readout)
|
|
29
|
+
- [Usage](#usage)
|
|
30
|
+
- [Quick Start](#quick-start)
|
|
31
|
+
- [Typical Protocol Flow](#typical-protocol-flow)
|
|
32
|
+
- [API Reference](#api-reference)
|
|
33
|
+
- [SensorHandlerModule](#sensorhandlermodule)
|
|
34
|
+
- [opentrons_photo_module](#opentrons_photo_module-1)
|
|
35
|
+
- [Simulation](#simulation)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
This library bridges the Opentrons OT-2 liquid handler with the Jobst Six Biotransmitter, enabling automated amperometric measurements within OT-2 protocols.
|
|
42
|
+
|
|
43
|
+
**Key features:**
|
|
44
|
+
|
|
45
|
+
- Continuous background sensor readout written incrementally to CSV — data is preserved even if the protocol is interrupted.
|
|
46
|
+
- Built-in wash cycles with configurable buffer sources and waste destinations.
|
|
47
|
+
- Replicate measurement support with consistent UUID grouping for downstream analysis.
|
|
48
|
+
- Integrated camera capture via the OT-2's built-in video device.
|
|
49
|
+
- Simulation-safe: all functions degrade gracefully when run in Opentrons simulation mode.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
> **Requirement:** Python 3.10 — the Opentrons library only supports this version.
|
|
56
|
+
|
|
57
|
+
### 1. Create and activate a virtual environment
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python3.10 -m venv .venv
|
|
61
|
+
source .venv/bin/activate
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Install the package
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install opentron_glulac_readout_module
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Modules
|
|
73
|
+
|
|
74
|
+
| Module | Purpose |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `opentrons_photo_module` | Capture images using the OT-2's built-in camera |
|
|
77
|
+
| `opentron_glulac_readout` | Manage sensor readout, measurement cycles, and wash steps |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
### Quick Start
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from opentron_glulac_readout_module import SensorHandlerModule
|
|
87
|
+
from opentrons import protocol_api
|
|
88
|
+
|
|
89
|
+
# ... opentron definitions ...
|
|
90
|
+
def run(protocol: protocol_api.ProtocolContext):
|
|
91
|
+
|
|
92
|
+
# custom manufactured measurement holder for the sensor that can be placed within the OT-2
|
|
93
|
+
glulac = protocol.load_labware(...)
|
|
94
|
+
pipette300 = protocol.load_instrument(...)
|
|
95
|
+
|
|
96
|
+
with SensorHandlerModule(
|
|
97
|
+
protocol=protocol,
|
|
98
|
+
measuring_well=glulac["A1"],
|
|
99
|
+
pipette=pipette300,
|
|
100
|
+
) as sensor_handler:
|
|
101
|
+
|
|
102
|
+
sensor_handler.add_trash_volume(trash_well)
|
|
103
|
+
sensor_handler.add_buffer_volume_for_washing(buffer_well)
|
|
104
|
+
sensor_handler.validate_pipette_clearance_and_sensor()
|
|
105
|
+
|
|
106
|
+
csv_path = sensor_handler.start_measurement()
|
|
107
|
+
|
|
108
|
+
sensor_handler.measure(source_well=plate["A1"])
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Typical Protocol Flow
|
|
112
|
+
|
|
113
|
+
The following order must be respected when setting up a measurement session:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
1. Create SensorHandlerModule (via context manager)
|
|
117
|
+
2. add_trash_volume() — register waste wells
|
|
118
|
+
3. add_buffer_volume_for_washing() — register buffer wells
|
|
119
|
+
4. validate_pipette_clearance_and_sensor() — optional, but must precede start_measurement()
|
|
120
|
+
5. start_measurement() — opens CSV and starts background readout
|
|
121
|
+
6. measure() / measure_replicate() — one call per sample
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> **Note:** `validate_pipette_clearance_and_sensor()` cannot be called after `start_measurement()` — doing so raises a `RuntimeError`.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## API Reference
|
|
129
|
+
|
|
130
|
+
### SensorHandlerModule
|
|
131
|
+
|
|
132
|
+
The central class for managing sensor readout. Must be used as a **context manager** to ensure background threads and CSV writers are properly started and stopped.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
with SensorHandlerModule(
|
|
136
|
+
protocol=protocol,
|
|
137
|
+
measuring_well=glulac["A1"],
|
|
138
|
+
pipette=pipette300,
|
|
139
|
+
) as sensor_handler:
|
|
140
|
+
...
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
#### `add_trash_volume(well, ...)`
|
|
146
|
+
|
|
147
|
+
Registers a well as a waste destination for discarded liquid.
|
|
148
|
+
|
|
149
|
+
Multiple trash wells can be registered; they are filled in registration order as capacity is consumed. Must be called before `start_measurement()`.
|
|
150
|
+
|
|
151
|
+
> Not required if only `start_measurement()` is used with no wash steps.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
#### `add_buffer_volume_for_washing(well, ...)`
|
|
156
|
+
|
|
157
|
+
Registers a well as a buffer source for wash steps.
|
|
158
|
+
|
|
159
|
+
Multiple buffer wells can be registered; they are drawn from in registration order as volume is consumed. Must be called before `start_measurement()`.
|
|
160
|
+
|
|
161
|
+
> Not required if only `start_measurement()` is used with no wash steps.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
#### `validate_pipette_clearance_and_sensor(...)`
|
|
166
|
+
|
|
167
|
+
Moves the pipette through all configured positions so the operator can visually confirm there are no crash risks before the protocol runs.
|
|
168
|
+
|
|
169
|
+
Optionally verifies that:
|
|
170
|
+
- The sensor produces a valid readout.
|
|
171
|
+
- The camera module is functional.
|
|
172
|
+
|
|
173
|
+
Each individual check can be disabled if only a subset of positions need to be verified.
|
|
174
|
+
|
|
175
|
+
**Constraints:**
|
|
176
|
+
- Must be called before `start_measurement()`.
|
|
177
|
+
- Calling it after `start_measurement()` raises a `RuntimeError`.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
#### `start_measurement() -> str`
|
|
182
|
+
|
|
183
|
+
Opens the output CSV file and starts continuous background sensor readout.
|
|
184
|
+
|
|
185
|
+
The CSV is written incrementally (one row per sensor reading), so data is preserved even if the protocol is interrupted mid-run.
|
|
186
|
+
|
|
187
|
+
**Returns:** The absolute path of the CSV file being written to.
|
|
188
|
+
|
|
189
|
+
**Must be called after:**
|
|
190
|
+
- `add_buffer_volume_for_washing()` / `add_trash_volume()` (if used)
|
|
191
|
+
- `validate_pipette_clearance_and_sensor()` (if used)
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
#### `measure(source_well, ...)`
|
|
196
|
+
|
|
197
|
+
Performs a single complete measurement cycle:
|
|
198
|
+
|
|
199
|
+
1. Aspirates sample from `source_well` and dispenses it into the measuring well.
|
|
200
|
+
2. Waits for the sensor signal to stabilize (slope detection followed by rolling standard deviation threshold).
|
|
201
|
+
3. Washes the measuring well with buffer for `SensorSettings.wash_iterations` cycles, waiting for signal stability after each wash.
|
|
202
|
+
|
|
203
|
+
The background CSV is annotated with `is_measuring_sample` and `is_washing` flags on each row, enabling precise time-windowing in downstream analysis.
|
|
204
|
+
|
|
205
|
+
Each call generates a unique `sample_uuid` that groups all CSV rows for the measurement (including wash rows).
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
#### `measure_replicate(source_well, ...)`
|
|
210
|
+
|
|
211
|
+
Runs a replicate measurement series on the sample.
|
|
212
|
+
|
|
213
|
+
The number of replicates defaults to `3` and can be configured via `SensorSettings.replicate_iterations`.
|
|
214
|
+
|
|
215
|
+
All repetitions share the same `sample_uuid` for grouping in downstream analysis and are stamped with an incrementing `replicate_count` column (starting at `1`).
|
|
216
|
+
|
|
217
|
+
This is equivalent to calling `measure()` multiple times manually, but guarantees consistent UUID grouping and replicate bookkeeping.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### opentrons_photo_module
|
|
222
|
+
|
|
223
|
+
Photography functions do **not** require a `SensorHandlerModule` instance.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
#### `take_picture(directory, filename, ...)`
|
|
228
|
+
|
|
229
|
+
Captures a single JPEG frame using the OT-2's built-in video device (`/dev/video0`) via FFmpeg.
|
|
230
|
+
|
|
231
|
+
- The output directory is created automatically if it does not exist.
|
|
232
|
+
- In **simulation mode**, no image is captured; a comment is logged to the protocol indicating where the photo would have been saved.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Simulation
|
|
237
|
+
|
|
238
|
+
To test whether a protocol will run correctly without executing it on real hardware, use the `opentrons_simulate` CLI from within your virtual environment:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
opentrons_simulate -L /Local_Path/toLabwareFolder protocol_name.py
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> The `-L` flag points to your custom labware directory. Using the same path as the Opentrons App reduces the risk of labware definition mismatches.
|
|
245
|
+
> You find this Location by going to the Opentrons App, press on preferences (bottom left), Advanced 'Additional Custom Labware Source Folder'. Here you can see the current folder, fill this one here
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# opentron_glulac_readout_module
|
|
2
|
+
|
|
3
|
+
A Python library for reading amperometric sensor data from the **Jobst Six Biotransmitter** when connected to an **Opentrons OT-2** automatic liquid handler (ALH).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Modules](#modules)
|
|
12
|
+
- [opentrons_photo_module](#opentrons_photo_module)
|
|
13
|
+
- [opentron_glulac_readout](#opentron_glulac_readout)
|
|
14
|
+
- [Usage](#usage)
|
|
15
|
+
- [Quick Start](#quick-start)
|
|
16
|
+
- [Typical Protocol Flow](#typical-protocol-flow)
|
|
17
|
+
- [API Reference](#api-reference)
|
|
18
|
+
- [SensorHandlerModule](#sensorhandlermodule)
|
|
19
|
+
- [opentrons_photo_module](#opentrons_photo_module-1)
|
|
20
|
+
- [Simulation](#simulation)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
This library bridges the Opentrons OT-2 liquid handler with the Jobst Six Biotransmitter, enabling automated amperometric measurements within OT-2 protocols.
|
|
27
|
+
|
|
28
|
+
**Key features:**
|
|
29
|
+
|
|
30
|
+
- Continuous background sensor readout written incrementally to CSV — data is preserved even if the protocol is interrupted.
|
|
31
|
+
- Built-in wash cycles with configurable buffer sources and waste destinations.
|
|
32
|
+
- Replicate measurement support with consistent UUID grouping for downstream analysis.
|
|
33
|
+
- Integrated camera capture via the OT-2's built-in video device.
|
|
34
|
+
- Simulation-safe: all functions degrade gracefully when run in Opentrons simulation mode.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
> **Requirement:** Python 3.10 — the Opentrons library only supports this version.
|
|
41
|
+
|
|
42
|
+
### 1. Create and activate a virtual environment
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python3.10 -m venv .venv
|
|
46
|
+
source .venv/bin/activate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Install the package
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install opentron_glulac_readout_module
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Modules
|
|
58
|
+
|
|
59
|
+
| Module | Purpose |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `opentrons_photo_module` | Capture images using the OT-2's built-in camera |
|
|
62
|
+
| `opentron_glulac_readout` | Manage sensor readout, measurement cycles, and wash steps |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Quick Start
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from opentron_glulac_readout_module import SensorHandlerModule
|
|
72
|
+
from opentrons import protocol_api
|
|
73
|
+
|
|
74
|
+
# ... opentron definitions ...
|
|
75
|
+
def run(protocol: protocol_api.ProtocolContext):
|
|
76
|
+
|
|
77
|
+
# custom manufactured measurement holder for the sensor that can be placed within the OT-2
|
|
78
|
+
glulac = protocol.load_labware(...)
|
|
79
|
+
pipette300 = protocol.load_instrument(...)
|
|
80
|
+
|
|
81
|
+
with SensorHandlerModule(
|
|
82
|
+
protocol=protocol,
|
|
83
|
+
measuring_well=glulac["A1"],
|
|
84
|
+
pipette=pipette300,
|
|
85
|
+
) as sensor_handler:
|
|
86
|
+
|
|
87
|
+
sensor_handler.add_trash_volume(trash_well)
|
|
88
|
+
sensor_handler.add_buffer_volume_for_washing(buffer_well)
|
|
89
|
+
sensor_handler.validate_pipette_clearance_and_sensor()
|
|
90
|
+
|
|
91
|
+
csv_path = sensor_handler.start_measurement()
|
|
92
|
+
|
|
93
|
+
sensor_handler.measure(source_well=plate["A1"])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Typical Protocol Flow
|
|
97
|
+
|
|
98
|
+
The following order must be respected when setting up a measurement session:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
1. Create SensorHandlerModule (via context manager)
|
|
102
|
+
2. add_trash_volume() — register waste wells
|
|
103
|
+
3. add_buffer_volume_for_washing() — register buffer wells
|
|
104
|
+
4. validate_pipette_clearance_and_sensor() — optional, but must precede start_measurement()
|
|
105
|
+
5. start_measurement() — opens CSV and starts background readout
|
|
106
|
+
6. measure() / measure_replicate() — one call per sample
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> **Note:** `validate_pipette_clearance_and_sensor()` cannot be called after `start_measurement()` — doing so raises a `RuntimeError`.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## API Reference
|
|
114
|
+
|
|
115
|
+
### SensorHandlerModule
|
|
116
|
+
|
|
117
|
+
The central class for managing sensor readout. Must be used as a **context manager** to ensure background threads and CSV writers are properly started and stopped.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
with SensorHandlerModule(
|
|
121
|
+
protocol=protocol,
|
|
122
|
+
measuring_well=glulac["A1"],
|
|
123
|
+
pipette=pipette300,
|
|
124
|
+
) as sensor_handler:
|
|
125
|
+
...
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
#### `add_trash_volume(well, ...)`
|
|
131
|
+
|
|
132
|
+
Registers a well as a waste destination for discarded liquid.
|
|
133
|
+
|
|
134
|
+
Multiple trash wells can be registered; they are filled in registration order as capacity is consumed. Must be called before `start_measurement()`.
|
|
135
|
+
|
|
136
|
+
> Not required if only `start_measurement()` is used with no wash steps.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
#### `add_buffer_volume_for_washing(well, ...)`
|
|
141
|
+
|
|
142
|
+
Registers a well as a buffer source for wash steps.
|
|
143
|
+
|
|
144
|
+
Multiple buffer wells can be registered; they are drawn from in registration order as volume is consumed. Must be called before `start_measurement()`.
|
|
145
|
+
|
|
146
|
+
> Not required if only `start_measurement()` is used with no wash steps.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
#### `validate_pipette_clearance_and_sensor(...)`
|
|
151
|
+
|
|
152
|
+
Moves the pipette through all configured positions so the operator can visually confirm there are no crash risks before the protocol runs.
|
|
153
|
+
|
|
154
|
+
Optionally verifies that:
|
|
155
|
+
- The sensor produces a valid readout.
|
|
156
|
+
- The camera module is functional.
|
|
157
|
+
|
|
158
|
+
Each individual check can be disabled if only a subset of positions need to be verified.
|
|
159
|
+
|
|
160
|
+
**Constraints:**
|
|
161
|
+
- Must be called before `start_measurement()`.
|
|
162
|
+
- Calling it after `start_measurement()` raises a `RuntimeError`.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
#### `start_measurement() -> str`
|
|
167
|
+
|
|
168
|
+
Opens the output CSV file and starts continuous background sensor readout.
|
|
169
|
+
|
|
170
|
+
The CSV is written incrementally (one row per sensor reading), so data is preserved even if the protocol is interrupted mid-run.
|
|
171
|
+
|
|
172
|
+
**Returns:** The absolute path of the CSV file being written to.
|
|
173
|
+
|
|
174
|
+
**Must be called after:**
|
|
175
|
+
- `add_buffer_volume_for_washing()` / `add_trash_volume()` (if used)
|
|
176
|
+
- `validate_pipette_clearance_and_sensor()` (if used)
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
#### `measure(source_well, ...)`
|
|
181
|
+
|
|
182
|
+
Performs a single complete measurement cycle:
|
|
183
|
+
|
|
184
|
+
1. Aspirates sample from `source_well` and dispenses it into the measuring well.
|
|
185
|
+
2. Waits for the sensor signal to stabilize (slope detection followed by rolling standard deviation threshold).
|
|
186
|
+
3. Washes the measuring well with buffer for `SensorSettings.wash_iterations` cycles, waiting for signal stability after each wash.
|
|
187
|
+
|
|
188
|
+
The background CSV is annotated with `is_measuring_sample` and `is_washing` flags on each row, enabling precise time-windowing in downstream analysis.
|
|
189
|
+
|
|
190
|
+
Each call generates a unique `sample_uuid` that groups all CSV rows for the measurement (including wash rows).
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
#### `measure_replicate(source_well, ...)`
|
|
195
|
+
|
|
196
|
+
Runs a replicate measurement series on the sample.
|
|
197
|
+
|
|
198
|
+
The number of replicates defaults to `3` and can be configured via `SensorSettings.replicate_iterations`.
|
|
199
|
+
|
|
200
|
+
All repetitions share the same `sample_uuid` for grouping in downstream analysis and are stamped with an incrementing `replicate_count` column (starting at `1`).
|
|
201
|
+
|
|
202
|
+
This is equivalent to calling `measure()` multiple times manually, but guarantees consistent UUID grouping and replicate bookkeeping.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### opentrons_photo_module
|
|
207
|
+
|
|
208
|
+
Photography functions do **not** require a `SensorHandlerModule` instance.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
#### `take_picture(directory, filename, ...)`
|
|
213
|
+
|
|
214
|
+
Captures a single JPEG frame using the OT-2's built-in video device (`/dev/video0`) via FFmpeg.
|
|
215
|
+
|
|
216
|
+
- The output directory is created automatically if it does not exist.
|
|
217
|
+
- In **simulation mode**, no image is captured; a comment is logged to the protocol indicating where the photo would have been saved.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Simulation
|
|
222
|
+
|
|
223
|
+
To test whether a protocol will run correctly without executing it on real hardware, use the `opentrons_simulate` CLI from within your virtual environment:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
opentrons_simulate -L /Local_Path/toLabwareFolder protocol_name.py
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
> The `-L` flag points to your custom labware directory. Using the same path as the Opentrons App reduces the risk of labware definition mismatches.
|
|
230
|
+
> You find this Location by going to the Opentrons App, press on preferences (bottom left), Advanced 'Additional Custom Labware Source Folder'. Here you can see the current folder, fill this one here
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from opentron_glulac_readout_module.opentron_glulac_readout.opentron_readout import SensorHandlerModule, SensorExpectedConcentrations, SensorMetadata, SensorType, SensorSettings
|
|
2
|
+
from opentron_glulac_readout_module.opentrons_photo_module.opentron_photo import take_picture
|
|
3
|
+
|
|
4
|
+
# Version of the opentron_glulac_readout_module package
|
|
5
|
+
__version__ = "2.0.3"
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import serial
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class JobstSensorModule:
|
|
6
|
+
HEADER = bytes([0x68, 0x13, 0x13, 0x68, 0x04]) # wire order
|
|
7
|
+
PAYLOAD_LENGTH = 18
|
|
8
|
+
FOOTER_LENGTH = 2 # [checksum, 0x16]
|
|
9
|
+
GAIN = 50 / (2 ** 15 - 1) # nA per LSB
|
|
10
|
+
INTER_FRAME_LENGTH = 15
|
|
11
|
+
|
|
12
|
+
def __init__(self, com_port: str = "/dev/ttyUSB0", baud: int = 9600):
|
|
13
|
+
self.com_port = com_port
|
|
14
|
+
self.baud = baud
|
|
15
|
+
|
|
16
|
+
def readout_stream(self):
|
|
17
|
+
"""
|
|
18
|
+
A generator that yields processed sensor data frames.
|
|
19
|
+
|
|
20
|
+
Each yielded dict contains:
|
|
21
|
+
- timestamp (str) : wall-clock time of the frame
|
|
22
|
+
- currents (list) : 6 current readings in nanoamperes
|
|
23
|
+
- temp (float) : temperature in degrees Celsius
|
|
24
|
+
- frames_received (int) : running count of valid frames
|
|
25
|
+
- frames_skipped (int) : running count of invalid/incomplete frames
|
|
26
|
+
"""
|
|
27
|
+
with serial.Serial(self.com_port, baudrate=self.baud, timeout=2.0) as ser:
|
|
28
|
+
# Double-flush: discard any partial frame that arrived before open
|
|
29
|
+
ser.reset_input_buffer()
|
|
30
|
+
time.sleep(1.6) # wait one full sensor cycle
|
|
31
|
+
ser.reset_input_buffer() # flush bytes that arrived during the wait
|
|
32
|
+
|
|
33
|
+
frames_received = 0
|
|
34
|
+
frames_skipped = 0
|
|
35
|
+
|
|
36
|
+
while True:
|
|
37
|
+
# ── 1. seek header ─────────────────────────────────────────
|
|
38
|
+
buf = ser.read_until(self.HEADER)
|
|
39
|
+
|
|
40
|
+
if not buf.endswith(self.HEADER):
|
|
41
|
+
frames_skipped += 1
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# ── 2. read payload + footer ───────────────────────────────
|
|
45
|
+
frame = ser.read(self.PAYLOAD_LENGTH + self.FOOTER_LENGTH)
|
|
46
|
+
|
|
47
|
+
if len(frame) < self.PAYLOAD_LENGTH + self.FOOTER_LENGTH:
|
|
48
|
+
frames_skipped += 1
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
payload = frame[:self.PAYLOAD_LENGTH]
|
|
52
|
+
checksum_byte = frame[self.PAYLOAD_LENGTH] # second-to-last
|
|
53
|
+
footer_byte = frame[self.PAYLOAD_LENGTH + 1] # last byte = 0x16
|
|
54
|
+
|
|
55
|
+
# ── 3. validate footer ─────────────────────────────────────
|
|
56
|
+
if footer_byte != 0x16:
|
|
57
|
+
frames_skipped += 1
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# ── 4. validate checksum ───────────────────────────────────
|
|
61
|
+
# Covers the 18 payload bytes plus the trailing 0x04 header byte
|
|
62
|
+
cks = (sum(payload) + 0x04) & 0xFF
|
|
63
|
+
if cks != checksum_byte:
|
|
64
|
+
frames_skipped += 1
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# ── 5. decode payload ──────────────────────────────────────
|
|
68
|
+
it = iter(payload)
|
|
69
|
+
out_data = [
|
|
70
|
+
int.from_bytes(bytes([x, next(it)]), 'big', signed=True)
|
|
71
|
+
for x in it
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
currents = [round(x * self.GAIN, 3) for x in out_data[0:6]]
|
|
75
|
+
temperature = round(out_data[6] / 16, 3)
|
|
76
|
+
frames_received += 1
|
|
77
|
+
|
|
78
|
+
yield {
|
|
79
|
+
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
|
|
80
|
+
"currents": currents,
|
|
81
|
+
"temp": temperature,
|
|
82
|
+
"frames_received": frames_received,
|
|
83
|
+
"frames_skipped": frames_skipped,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ── 6. consume inter-frame bytes (natural ~1.5s pause) ─────
|
|
87
|
+
ser.read(self.INTER_FRAME_LENGTH)
|
|
88
|
+
|
|
89
|
+
def validate_sensor(self, frames_required: int = 3, timeout_seconds: float = 10.0) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Opens the serial connection briefly, attempts to read `frames_required`
|
|
92
|
+
valid frames, then closes it. Returns True if successful.
|
|
93
|
+
Raises SerialException if the port cannot be opened at all.
|
|
94
|
+
"""
|
|
95
|
+
received = 0
|
|
96
|
+
deadline = time.time() + timeout_seconds
|
|
97
|
+
package_length = 25
|
|
98
|
+
data_block = [b'\x00'] * package_length
|
|
99
|
+
|
|
100
|
+
with serial.Serial(self.com_port, baudrate=self.baud, timeout=2) as ser:
|
|
101
|
+
while time.time() < deadline:
|
|
102
|
+
data = ser.read() if ser.inWaiting() else None
|
|
103
|
+
if data:
|
|
104
|
+
data_block.insert(0, data)
|
|
105
|
+
data_block.pop()
|
|
106
|
+
|
|
107
|
+
header = [b'\x04', b'\x68', b'\x13', b'\x13', b'\x68']
|
|
108
|
+
cks = sum(int.from_bytes(x, 'big') for x in data_block[2:-4]) & 0xFF
|
|
109
|
+
|
|
110
|
+
if (data_block[-5:] == header
|
|
111
|
+
and data_block[0] == b'\x16'
|
|
112
|
+
and int.from_bytes(data_block[1], 'big') == cks):
|
|
113
|
+
received += 1
|
|
114
|
+
if received >= frames_required:
|
|
115
|
+
return True
|
|
116
|
+
time.sleep(0.01)
|
|
117
|
+
|
|
118
|
+
return False # timed out before receiving enough valid frames
|