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.
Files changed (17) hide show
  1. opentron_glulac_readout_module-2.0.3/LICENCE +21 -0
  2. opentron_glulac_readout_module-2.0.3/PKG-INFO +245 -0
  3. opentron_glulac_readout_module-2.0.3/README.md +230 -0
  4. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/__init__.py +5 -0
  5. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentron_glulac_readout/__init__.py +0 -0
  6. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentron_glulac_readout/jobst_sensor_module.py +118 -0
  7. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentron_glulac_readout/opentron_readout.py +1232 -0
  8. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentron_glulac_readout/volume_manager.py +76 -0
  9. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentrons_photo_module/__init__.py +0 -0
  10. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module/opentrons_photo_module/opentron_photo.py +83 -0
  11. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/PKG-INFO +245 -0
  12. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/SOURCES.txt +15 -0
  13. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/dependency_links.txt +1 -0
  14. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/requires.txt +2 -0
  15. opentron_glulac_readout_module-2.0.3/opentron_glulac_readout_module.egg-info/top_level.txt +1 -0
  16. opentron_glulac_readout_module-2.0.3/pyproject.toml +42 -0
  17. 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"
@@ -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