pychemstation 0.10.4__py3-none-any.whl → 0.10.5__py3-none-any.whl
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.
- pychemstation/analysis/chromatogram.py +20 -0
- pychemstation/analysis/process_report.py +125 -63
- pychemstation/control/__init__.py +2 -0
- pychemstation/control/controllers/__init__.py +2 -3
- pychemstation/control/controllers/comm.py +37 -22
- pychemstation/control/controllers/devices/device.py +56 -31
- pychemstation/control/controllers/devices/injector.py +19 -7
- pychemstation/control/controllers/tables/__init__.py +4 -0
- pychemstation/control/controllers/tables/method.py +50 -93
- pychemstation/control/controllers/tables/sequence.py +134 -95
- pychemstation/control/controllers/tables/table.py +102 -78
- pychemstation/control/hplc.py +11 -17
- pychemstation/utils/injector_types.py +1 -1
- pychemstation/utils/macro.py +2 -2
- pychemstation/utils/method_types.py +1 -1
- {pychemstation-0.10.4.dist-info → pychemstation-0.10.5.dist-info}/METADATA +4 -4
- pychemstation-0.10.5.dist-info/RECORD +36 -0
- pychemstation/control/controllers/tables/ms.py +0 -24
- pychemstation-0.10.4.dist-info/RECORD +0 -37
- {pychemstation-0.10.4.dist-info → pychemstation-0.10.5.dist-info}/WHEEL +0 -0
- {pychemstation-0.10.4.dist-info → pychemstation-0.10.5.dist-info}/licenses/LICENSE +0 -0
@@ -3,6 +3,7 @@
|
|
3
3
|
import os
|
4
4
|
import time
|
5
5
|
from dataclasses import dataclass
|
6
|
+
from typing import Dict
|
6
7
|
|
7
8
|
import numpy as np
|
8
9
|
|
@@ -114,3 +115,22 @@ class AgilentChannelChromatogramData:
|
|
114
115
|
F: AgilentHPLCChromatogram
|
115
116
|
G: AgilentHPLCChromatogram
|
116
117
|
H: AgilentHPLCChromatogram
|
118
|
+
|
119
|
+
@classmethod
|
120
|
+
def from_dict(cls, chroms: Dict[str, AgilentHPLCChromatogram]):
|
121
|
+
keys = chroms.keys()
|
122
|
+
class_keys = vars(AgilentChannelChromatogramData)["__annotations__"].keys()
|
123
|
+
if set(class_keys) == set(keys):
|
124
|
+
return AgilentChannelChromatogramData(
|
125
|
+
A=chroms["A"],
|
126
|
+
B=chroms["B"],
|
127
|
+
C=chroms["C"],
|
128
|
+
D=chroms["D"],
|
129
|
+
E=chroms["E"],
|
130
|
+
F=chroms["F"],
|
131
|
+
G=chroms["G"],
|
132
|
+
H=chroms["H"],
|
133
|
+
)
|
134
|
+
else:
|
135
|
+
err = f"{keys} don't match {class_keys}"
|
136
|
+
raise KeyError(err)
|
@@ -1,10 +1,12 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import abc
|
2
4
|
import os
|
3
5
|
import re
|
4
6
|
from abc import abstractmethod
|
5
7
|
from dataclasses import dataclass
|
6
8
|
from enum import Enum
|
7
|
-
from typing import AnyStr, Dict, List, Optional, Pattern
|
9
|
+
from typing import AnyStr, Dict, List, Optional, Pattern, Union
|
8
10
|
|
9
11
|
import pandas as pd
|
10
12
|
from aghplctools.ingestion.text import (
|
@@ -15,6 +17,7 @@ from aghplctools.ingestion.text import (
|
|
15
17
|
_signal_table_re,
|
16
18
|
chunk_string,
|
17
19
|
)
|
20
|
+
from pandas.errors import EmptyDataError
|
18
21
|
from result import Err, Ok, Result
|
19
22
|
|
20
23
|
from ..analysis.chromatogram import AgilentHPLCChromatogram
|
@@ -43,7 +46,7 @@ class Signals:
|
|
43
46
|
class AgilentReport:
|
44
47
|
vial_location: Optional[Tray]
|
45
48
|
signals: List[Signals]
|
46
|
-
solvents: Optional[Dict[
|
49
|
+
solvents: Optional[Dict[str, str]]
|
47
50
|
|
48
51
|
|
49
52
|
class ReportType(Enum):
|
@@ -69,6 +72,37 @@ class CSVProcessor(ReportProcessor):
|
|
69
72
|
"""
|
70
73
|
super().__init__(path)
|
71
74
|
|
75
|
+
def find_csv_prefix(self) -> str:
|
76
|
+
files = [
|
77
|
+
f
|
78
|
+
for f in os.listdir(self.path)
|
79
|
+
if os.path.isfile(os.path.join(self.path, f))
|
80
|
+
]
|
81
|
+
for file in files:
|
82
|
+
if "00" in file:
|
83
|
+
name, _, file_extension = file.partition(".")
|
84
|
+
if "00" in name and file_extension.lower() == "csv":
|
85
|
+
prefix, _, _ = name.partition("00")
|
86
|
+
return prefix
|
87
|
+
raise FileNotFoundError("Couldn't find the prefix for CSV")
|
88
|
+
|
89
|
+
def report_contains(self, labels: List[str], want: List[str]):
|
90
|
+
for label in labels:
|
91
|
+
if label in want:
|
92
|
+
want.remove(label)
|
93
|
+
|
94
|
+
all_labels_seen = False
|
95
|
+
if len(want) != 0:
|
96
|
+
for want_label in want:
|
97
|
+
label_seen = False
|
98
|
+
for label in labels:
|
99
|
+
if want_label in label or want_label == label:
|
100
|
+
label_seen = True
|
101
|
+
all_labels_seen = label_seen
|
102
|
+
else:
|
103
|
+
return True
|
104
|
+
return all_labels_seen
|
105
|
+
|
72
106
|
def process_report(self) -> Result[AgilentReport, AnyStr]:
|
73
107
|
"""
|
74
108
|
Method to parse details from CSV report.
|
@@ -76,15 +110,30 @@ class CSVProcessor(ReportProcessor):
|
|
76
110
|
:return: subset of complete report details, specifically the sample location, solvents in pumps,
|
77
111
|
and list of peaks at each wavelength channel.
|
78
112
|
"""
|
79
|
-
|
80
|
-
|
81
|
-
|
113
|
+
prefix = self.find_csv_prefix()
|
114
|
+
labels = os.path.join(self.path, f"{prefix}00.CSV")
|
115
|
+
if not os.path.exists(labels):
|
116
|
+
raise ValueError(
|
117
|
+
"CSV reports do not exist, make sure to turn on the post run CSV report option!"
|
118
|
+
)
|
119
|
+
elif os.path.exists(labels):
|
120
|
+
LOCATION = "Location"
|
121
|
+
NUM_SIGNALS = "Number of Signals"
|
122
|
+
SOLVENT = "Solvent"
|
123
|
+
df_labels: Dict[int, Dict[int, str]] = pd.read_csv(
|
82
124
|
labels, encoding="utf-16", header=None
|
83
125
|
).to_dict()
|
84
|
-
vial_location =
|
85
|
-
signals = {}
|
86
|
-
solvents = {}
|
87
|
-
|
126
|
+
vial_location: str = ""
|
127
|
+
signals: Dict[int, list[AgilentPeak]] = {}
|
128
|
+
solvents: Dict[str, str] = {}
|
129
|
+
report_labels: Dict[int, str] = df_labels[0]
|
130
|
+
|
131
|
+
if not self.report_contains(
|
132
|
+
list(report_labels.values()), [LOCATION, NUM_SIGNALS, SOLVENT]
|
133
|
+
):
|
134
|
+
return Err(f"Missing one of: {LOCATION}, {NUM_SIGNALS}, {SOLVENT}")
|
135
|
+
|
136
|
+
for pos, val in report_labels.items():
|
88
137
|
if val == "Location":
|
89
138
|
vial_location = df_labels[1][pos]
|
90
139
|
elif "Solvent" in val:
|
@@ -93,22 +142,29 @@ class CSVProcessor(ReportProcessor):
|
|
93
142
|
elif val == "Number of Signals":
|
94
143
|
num_signals = int(df_labels[1][pos])
|
95
144
|
for s in range(1, num_signals + 1):
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
145
|
+
try:
|
146
|
+
df = pd.read_csv(
|
147
|
+
os.path.join(self.path, f"{prefix}0{s}.CSV"),
|
148
|
+
encoding="utf-16",
|
149
|
+
header=None,
|
150
|
+
)
|
151
|
+
peaks = df.apply(lambda row: AgilentPeak(*row), axis=1)
|
152
|
+
except EmptyDataError:
|
153
|
+
peaks = []
|
154
|
+
try:
|
155
|
+
wavelength = df_labels[1][pos + s].partition(",4 Ref=off")[
|
156
|
+
0
|
157
|
+
][-3:]
|
158
|
+
signals[int(wavelength)] = list(peaks)
|
159
|
+
except (IndexError, ValueError):
|
160
|
+
# TODO: Ask about the MS signals
|
161
|
+
pass
|
106
162
|
break
|
107
163
|
|
108
164
|
return Ok(
|
109
165
|
AgilentReport(
|
110
166
|
signals=[
|
111
|
-
Signals(wavelength=
|
167
|
+
Signals(wavelength=w, peaks=s, data=None)
|
112
168
|
for w, s in signals.items()
|
113
169
|
],
|
114
170
|
vial_location=FiftyFourVialPlate.from_int(int(vial_location)),
|
@@ -154,7 +210,7 @@ class TXTProcessor(ReportProcessor):
|
|
154
210
|
path: str,
|
155
211
|
min_ret_time: int = 0,
|
156
212
|
max_ret_time: int = 999,
|
157
|
-
target_wavelength_range
|
213
|
+
target_wavelength_range=None,
|
158
214
|
):
|
159
215
|
"""
|
160
216
|
Class to process reports in CSV form.
|
@@ -164,12 +220,14 @@ class TXTProcessor(ReportProcessor):
|
|
164
220
|
:param max_ret_time: peaks will only be returned up to this time (min)
|
165
221
|
:param target_wavelength_range: range of wavelengths to return
|
166
222
|
"""
|
223
|
+
if target_wavelength_range is None:
|
224
|
+
target_wavelength_range = list(range(200, 300))
|
167
225
|
self.target_wavelength_range = target_wavelength_range
|
168
226
|
self.min_ret_time = min_ret_time
|
169
227
|
self.max_ret_time = max_ret_time
|
170
228
|
super().__init__(path)
|
171
229
|
|
172
|
-
def process_report(self) -> Result[AgilentReport, AnyStr]:
|
230
|
+
def process_report(self) -> Result[AgilentReport, Union[AnyStr, Exception]]:
|
173
231
|
"""
|
174
232
|
Method to parse details from CSV report.
|
175
233
|
If you want more functionality, use `aghplctools`.
|
@@ -179,44 +237,48 @@ class TXTProcessor(ReportProcessor):
|
|
179
237
|
:return: subset of complete report details, specifically the sample location, solvents in pumps,
|
180
238
|
and list of peaks at each wavelength channel.
|
181
239
|
"""
|
182
|
-
|
183
|
-
with open(
|
184
|
-
os.path.join(self.path, "REPORT.TXT"), "r", encoding="utf-16"
|
185
|
-
) as openfile:
|
186
|
-
text = openfile.read()
|
187
|
-
|
188
240
|
try:
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
241
|
+
with open(
|
242
|
+
os.path.join(self.path, "REPORT.TXT"), "r", encoding="utf-16"
|
243
|
+
) as openfile:
|
244
|
+
text = openfile.read()
|
245
|
+
|
246
|
+
try:
|
247
|
+
signals = self.parse_area_report(text)
|
248
|
+
except ValueError as e:
|
249
|
+
return Err("No peaks found: " + str(e))
|
250
|
+
|
251
|
+
signals = {
|
252
|
+
key: signals[key]
|
253
|
+
for key in self.target_wavelength_range
|
254
|
+
if key in signals
|
255
|
+
}
|
256
|
+
|
257
|
+
parsed_signals = []
|
258
|
+
for wavelength, wavelength_dict in signals.items():
|
259
|
+
current_wavelength_signals = Signals(
|
260
|
+
wavelength=int(wavelength), peaks=[], data=None
|
261
|
+
)
|
262
|
+
for ret_time, ret_time_dict in wavelength_dict.items():
|
263
|
+
if self.min_ret_time <= ret_time <= self.max_ret_time:
|
264
|
+
current_wavelength_signals.peaks.append(
|
265
|
+
AgilentPeak(
|
266
|
+
retention_time=ret_time,
|
267
|
+
area=ret_time_dict["Area"],
|
268
|
+
width=ret_time_dict["Width"],
|
269
|
+
height=ret_time_dict["Height"],
|
270
|
+
peak_number=None,
|
271
|
+
peak_type=ret_time_dict["Type"],
|
272
|
+
area_percent=None,
|
273
|
+
)
|
213
274
|
)
|
214
|
-
|
215
|
-
parsed_signals.append(current_wavelength_signals)
|
275
|
+
parsed_signals.append(current_wavelength_signals)
|
216
276
|
|
217
|
-
|
218
|
-
|
219
|
-
|
277
|
+
return Ok(
|
278
|
+
AgilentReport(vial_location=None, solvents=None, signals=parsed_signals)
|
279
|
+
)
|
280
|
+
except Exception as e:
|
281
|
+
return Err(e)
|
220
282
|
|
221
283
|
def parse_area_report(self, report_text: str) -> Dict:
|
222
284
|
"""
|
@@ -234,7 +296,7 @@ class TXTProcessor(ReportProcessor):
|
|
234
296
|
if re.search(_no_peaks_re, report_text): # There are no peaks in Report.txt
|
235
297
|
raise ValueError("No peaks found in Report.txt")
|
236
298
|
blocks = _header_block_re.split(report_text)
|
237
|
-
signals = {} # output dictionary
|
299
|
+
signals: Dict[int, dict] = {} # output dictionary
|
238
300
|
for ind, block in enumerate(blocks):
|
239
301
|
# area report block
|
240
302
|
if _area_report_re.match(block): # match area report block
|
@@ -247,7 +309,7 @@ class TXTProcessor(ReportProcessor):
|
|
247
309
|
# some error state (e.g. 'not found')
|
248
310
|
if si.group("error") != "":
|
249
311
|
continue
|
250
|
-
wavelength =
|
312
|
+
wavelength = int(si.group("wavelength"))
|
251
313
|
if wavelength in signals:
|
252
314
|
# placeholder error raise just in case (this probably won't happen)
|
253
315
|
raise KeyError(
|
@@ -270,15 +332,14 @@ class TXTProcessor(ReportProcessor):
|
|
270
332
|
for key in self._column_re_dictionary:
|
271
333
|
if key in peak.re.groupindex:
|
272
334
|
try: # try float conversion, otherwise continue
|
273
|
-
|
335
|
+
current[key] = float(peak.group(key))
|
274
336
|
except ValueError:
|
275
|
-
|
276
|
-
current[key] = value
|
337
|
+
current[key] = peak.group(key)
|
277
338
|
else: # ensures defined
|
278
339
|
current[key] = None
|
279
340
|
return signals
|
280
341
|
|
281
|
-
def build_peak_regex(self, signal_table: str) -> Pattern[
|
342
|
+
def build_peak_regex(self, signal_table: str) -> Pattern[str] | None:
|
282
343
|
"""
|
283
344
|
Builds a peak regex from a signal table. Courtesy of Veronica Lai.
|
284
345
|
|
@@ -311,6 +372,7 @@ class TXTProcessor(ReportProcessor):
|
|
311
372
|
f'The header/unit combination "{header}" "{unit}" is not defined in the peak regex '
|
312
373
|
f"dictionary. Let Lars know."
|
313
374
|
)
|
375
|
+
|
314
376
|
return re.compile(
|
315
377
|
"[ ]+".join(
|
316
378
|
peak_re_string
|
@@ -3,7 +3,6 @@
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from .comm import CommunicationController
|
6
|
-
from .
|
7
|
-
from .tables.sequence import SequenceController
|
6
|
+
from . import tables
|
8
7
|
|
9
|
-
__all__ = ["CommunicationController", "
|
8
|
+
__all__ = ["CommunicationController", "tables"]
|
@@ -39,6 +39,7 @@ class CommunicationController:
|
|
39
39
|
comm_dir: str,
|
40
40
|
cmd_file: str = "cmd",
|
41
41
|
reply_file: str = "reply",
|
42
|
+
offline: bool = False,
|
42
43
|
debug: bool = False,
|
43
44
|
):
|
44
45
|
"""
|
@@ -47,26 +48,27 @@ class CommunicationController:
|
|
47
48
|
:param reply_file: Name of reply file
|
48
49
|
:param debug: whether to save log of sent commands
|
49
50
|
"""
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
51
|
+
if not offline:
|
52
|
+
self.debug = debug
|
53
|
+
if os.path.isdir(comm_dir):
|
54
|
+
self.cmd_file = os.path.join(comm_dir, cmd_file)
|
55
|
+
self.reply_file = os.path.join(comm_dir, reply_file)
|
56
|
+
self.cmd_no = 0
|
57
|
+
else:
|
58
|
+
raise FileNotFoundError(f"comm_dir: {comm_dir} not found.")
|
58
59
|
|
59
|
-
|
60
|
-
|
61
|
-
|
60
|
+
# Create files for Chemstation to communicate with Python
|
61
|
+
open(self.cmd_file, "a").close()
|
62
|
+
open(self.reply_file, "a").close()
|
62
63
|
|
63
|
-
|
64
|
+
self.reset_cmd_counter()
|
64
65
|
|
65
|
-
|
66
|
-
|
66
|
+
# Initialize row counter for table operations
|
67
|
+
self._most_recent_hplc_status: Status = self.get_status()
|
68
|
+
self.send("Local Rows")
|
67
69
|
|
68
70
|
def get_num_val(self, cmd: str) -> Union[int, float]:
|
69
|
-
tries =
|
71
|
+
tries = 10
|
70
72
|
for _ in range(tries):
|
71
73
|
self.send(Command.GET_NUM_VAL_CMD.value.format(cmd=cmd))
|
72
74
|
res = self.receive()
|
@@ -75,7 +77,7 @@ class CommunicationController:
|
|
75
77
|
raise RuntimeError("Failed to get number.")
|
76
78
|
|
77
79
|
def get_text_val(self, cmd: str) -> str:
|
78
|
-
tries =
|
80
|
+
tries = 10
|
79
81
|
for _ in range(tries):
|
80
82
|
self.send(Command.GET_TEXT_VAL_CMD.value.format(cmd=cmd))
|
81
83
|
res = self.receive()
|
@@ -92,9 +94,15 @@ class CommunicationController:
|
|
92
94
|
time.sleep(1)
|
93
95
|
|
94
96
|
try:
|
95
|
-
|
96
|
-
|
97
|
-
|
97
|
+
res = self.receive()
|
98
|
+
if res.is_err():
|
99
|
+
return HPLCErrorStatus.NORESPONSE
|
100
|
+
if res.is_ok():
|
101
|
+
parsed_response = self.receive().value.string_response
|
102
|
+
self._most_recent_hplc_status = str_to_status(parsed_response)
|
103
|
+
return self._most_recent_hplc_status
|
104
|
+
else:
|
105
|
+
raise RuntimeError("Failed to get status")
|
98
106
|
except IOError:
|
99
107
|
return HPLCErrorStatus.NORESPONSE
|
100
108
|
except IndexError:
|
@@ -145,7 +153,8 @@ class CommunicationController:
|
|
145
153
|
:raises IOError: Could not read reply file.
|
146
154
|
:return: Potential ChemStation response
|
147
155
|
"""
|
148
|
-
err: Optional[Union[OSError, IndexError]] = None
|
156
|
+
err: Optional[Union[OSError, IndexError, ValueError]] = None
|
157
|
+
err_msg = ""
|
149
158
|
for _ in range(num_attempts):
|
150
159
|
time.sleep(1)
|
151
160
|
|
@@ -158,7 +167,11 @@ class CommunicationController:
|
|
158
167
|
|
159
168
|
try:
|
160
169
|
first_line = response.splitlines()[0]
|
161
|
-
|
170
|
+
try:
|
171
|
+
response_no = int(first_line.split()[0])
|
172
|
+
except ValueError as e:
|
173
|
+
err = e
|
174
|
+
err_msg = f"Caused by {first_line}"
|
162
175
|
except IndexError as e:
|
163
176
|
err = e
|
164
177
|
continue
|
@@ -169,7 +182,9 @@ class CommunicationController:
|
|
169
182
|
else:
|
170
183
|
continue
|
171
184
|
else:
|
172
|
-
return Err(
|
185
|
+
return Err(
|
186
|
+
f"Failed to receive reply to command #{cmd_no} due to {err} caused by {err_msg}."
|
187
|
+
)
|
173
188
|
|
174
189
|
def sleepy_send(self, cmd: Union[Command, str]):
|
175
190
|
self.send("Sleep 0.1")
|
@@ -1,49 +1,74 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import abc
|
2
|
-
from typing import
|
4
|
+
from typing import Union
|
3
5
|
|
4
|
-
from result import
|
6
|
+
from result import Err, Ok
|
5
7
|
|
6
|
-
from ....analysis.process_report import AgilentReport, ReportType
|
7
8
|
from ....control.controllers import CommunicationController
|
8
|
-
from ....
|
9
|
-
from
|
10
|
-
AgilentChannelChromatogramData,
|
11
|
-
AgilentHPLCChromatogram,
|
12
|
-
)
|
13
|
-
from ....utils.table_types import T, Table
|
9
|
+
from ....utils.macro import Command, Response
|
10
|
+
from ....utils.table_types import RegisterFlag, Table, TableOperation
|
14
11
|
|
15
12
|
|
16
|
-
class DeviceController(
|
13
|
+
class DeviceController(abc.ABC):
|
17
14
|
def __init__(
|
18
15
|
self, controller: CommunicationController, table: Table, offline: bool
|
19
16
|
):
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
self.table_locator = table
|
18
|
+
self.controller = controller
|
19
|
+
self.offline = offline
|
23
20
|
|
24
21
|
@abc.abstractmethod
|
25
22
|
def get_row(self, row: int):
|
26
23
|
pass
|
27
24
|
|
28
|
-
def
|
29
|
-
|
25
|
+
def get_text(self, row: int, col_name: RegisterFlag) -> str:
|
26
|
+
return self.controller.get_text_val(
|
27
|
+
TableOperation.GET_ROW_TEXT.value.format(
|
28
|
+
register=self.table_locator.register,
|
29
|
+
table_name=self.table_locator.name,
|
30
|
+
row=row,
|
31
|
+
col_name=col_name.value,
|
32
|
+
)
|
33
|
+
)
|
30
34
|
|
31
|
-
def
|
32
|
-
|
35
|
+
def get_num(self, row: int, col_name: RegisterFlag) -> Union[int, float]:
|
36
|
+
return self.controller.get_num_val(
|
37
|
+
TableOperation.GET_ROW_VAL.value.format(
|
38
|
+
register=self.table_locator.register,
|
39
|
+
table_name=self.table_locator.name,
|
40
|
+
row=row,
|
41
|
+
col_name=col_name.value,
|
42
|
+
)
|
43
|
+
)
|
33
44
|
|
34
|
-
def
|
35
|
-
self
|
36
|
-
|
37
|
-
|
45
|
+
def get_num_rows(self) -> Ok[Response] | Err[str]:
|
46
|
+
self.send(
|
47
|
+
TableOperation.GET_NUM_ROWS.value.format(
|
48
|
+
register=self.table_locator.register,
|
49
|
+
table_name=self.table_locator.name,
|
50
|
+
col_name=RegisterFlag.NUM_ROWS,
|
51
|
+
)
|
52
|
+
)
|
53
|
+
self.send(
|
54
|
+
Command.GET_ROWS_CMD.value.format(
|
55
|
+
register=self.table_locator.register,
|
56
|
+
table_name=self.table_locator.name,
|
57
|
+
col_name=RegisterFlag.NUM_ROWS,
|
58
|
+
)
|
59
|
+
)
|
60
|
+
res = self.controller.receive()
|
38
61
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
62
|
+
if res.is_ok():
|
63
|
+
self.send("Sleep 0.1")
|
64
|
+
self.send("Print Rows")
|
65
|
+
return res
|
66
|
+
else:
|
67
|
+
return Err("No rows could be read.")
|
45
68
|
|
46
|
-
def
|
47
|
-
self
|
48
|
-
|
49
|
-
|
69
|
+
def send(self, cmd: Union[Command, str]):
|
70
|
+
if not self.controller:
|
71
|
+
raise RuntimeError(
|
72
|
+
"Communication controller must be initialized before sending command. It is currently in offline mode."
|
73
|
+
)
|
74
|
+
self.controller.send(cmd)
|
@@ -10,6 +10,7 @@ from ....utils.injector_types import (
|
|
10
10
|
SourceType,
|
11
11
|
Wait,
|
12
12
|
)
|
13
|
+
from ....utils.macro import Response
|
13
14
|
from ....utils.table_types import RegisterFlag, Table
|
14
15
|
from ....utils.tray_types import Tray
|
15
16
|
from .device import DeviceController
|
@@ -23,7 +24,12 @@ class InjectorController(DeviceController):
|
|
23
24
|
|
24
25
|
def get_row(self, row: int) -> InjectorFunction:
|
25
26
|
def return_tray_loc() -> Tray:
|
26
|
-
|
27
|
+
raise NotImplementedError
|
28
|
+
# unit = self.get_text(row, RegisterFlag.DRAW_LOCATION_UNIT)
|
29
|
+
# tray = self.get_text(row, RegisterFlag.DRAW_LOCATION_TRAY)
|
30
|
+
# x = self.get_text(row, RegisterFlag.DRAW_LOCATION_ROW)
|
31
|
+
# y = self.get_text(row, RegisterFlag.DRAW_LOCATION_COLUMN)
|
32
|
+
# return FiftyFourVialPlate.from_str("P1-A1")
|
27
33
|
|
28
34
|
function = self.get_text(row, RegisterFlag.FUNCTION)
|
29
35
|
if function == "Wait":
|
@@ -48,14 +54,20 @@ class InjectorController(DeviceController):
|
|
48
54
|
elif function == "Remote":
|
49
55
|
return Remote(
|
50
56
|
command=RemoteCommand(self.get_text(row, RegisterFlag.REMOTE)),
|
51
|
-
duration=self.get_num(row, RegisterFlag.REMOTE_DUR),
|
57
|
+
duration=int(self.get_num(row, RegisterFlag.REMOTE_DUR)),
|
52
58
|
)
|
59
|
+
raise ValueError("No valid function found.")
|
53
60
|
|
54
61
|
def load(self) -> InjectorTable:
|
55
62
|
rows = self.get_num_rows()
|
56
63
|
if rows.is_ok():
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
64
|
+
row_response = rows.value
|
65
|
+
if isinstance(row_response, Response):
|
66
|
+
return InjectorTable(
|
67
|
+
functions=[
|
68
|
+
self.get_row(i) for i in range(int(row_response.num_response))
|
69
|
+
]
|
70
|
+
)
|
71
|
+
elif rows.is_err():
|
72
|
+
return InjectorTable(functions=[])
|
73
|
+
raise ValueError("Unexpected error")
|