matterlab-mfcs 0.1.0__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.
@@ -0,0 +1,25 @@
1
+ from matterlab_mfcs.enums import (
2
+ GasNumber,
3
+ PressureUnit,
4
+ StandardFlowUnit,
5
+ TemperatureUnit,
6
+ TotalStandardVolumeUnit,
7
+ TotalVolumeUnit,
8
+ TrueMassFlowUnit,
9
+ VolumetricFlowUnit,
10
+ )
11
+ from matterlab_mfcs.alicat_mfc import AlicatMFC
12
+ from matterlab_mfcs.base_mfc import MassFlowController
13
+
14
+ __all__ = [
15
+ "AlicatMFC",
16
+ "GasNumber",
17
+ "MassFlowController",
18
+ "PressureUnit",
19
+ "StandardFlowUnit",
20
+ "TemperatureUnit",
21
+ "TotalStandardVolumeUnit",
22
+ "TotalVolumeUnit",
23
+ "TrueMassFlowUnit",
24
+ "VolumetricFlowUnit",
25
+ ]
@@ -0,0 +1,619 @@
1
+ from __future__ import annotations
2
+
3
+ import struct
4
+ from datetime import date
5
+ from enum import IntEnum
6
+ from logging import Logger
7
+ from typing import Optional, Union
8
+
9
+ from matterlab_serial_device import SerialDevice, open_close
10
+
11
+ from matterlab_mfcs.base_mfc import MassFlowController
12
+ from matterlab_mfcs.enums import GasNumber, PressureUnit, StandardFlowUnit, TemperatureUnit, VolumetricFlowUnit
13
+
14
+
15
+ MODBUS_EXCEPTION_CODES = {
16
+ 0x01: "illegal function",
17
+ 0x02: "illegal data address",
18
+ 0x03: "illegal data value",
19
+ 0x04: "slave device failure",
20
+ 0x05: "acknowledge",
21
+ 0x06: "slave device busy",
22
+ 0x08: "memory parity error",
23
+ 0x0A: "gateway path unavailable",
24
+ 0x0B: "gateway target failed to respond",
25
+ }
26
+
27
+
28
+ class AlicatMFC(MassFlowController, SerialDevice):
29
+ """Alicat Modbus RTU driver built on matterlab_serial_device."""
30
+
31
+ ui_fields = ("com_port", "address")
32
+
33
+ @staticmethod
34
+ def _int_arg(value: Union[int, IntEnum]) -> int:
35
+ return int(value)
36
+
37
+ def __init__(
38
+ self,
39
+ com_port: str,
40
+ address: int = 1,
41
+ baudrate: int = 19200,
42
+ bytesize: int = 8,
43
+ parity: str = "none",
44
+ stopbits: int = 1,
45
+ timeout: float = 0.4,
46
+ connect_hardware: bool = True,
47
+ share_port: bool = False,
48
+ logger: Optional[Logger] = None,
49
+ **kwargs,
50
+ ) -> None:
51
+ MassFlowController.__init__(self, logger=logger)
52
+ SerialDevice.__init__(
53
+ self,
54
+ com_port=com_port,
55
+ baudrate=baudrate,
56
+ bytesize=bytesize,
57
+ parity=parity,
58
+ stopbits=stopbits,
59
+ timeout=timeout,
60
+ connect_hardware=connect_hardware,
61
+ share_port=share_port,
62
+ logger=self.logger,
63
+ **kwargs,
64
+ )
65
+ if address < 1 or address > 247:
66
+ raise ValueError("Modbus slave address must be between 1 and 247.")
67
+ self.address = address
68
+
69
+ @staticmethod
70
+ def crc16_modbus(data: bytes) -> int:
71
+ """Compute the Modbus RTU CRC16 used by Alicat devices."""
72
+ crc = 0xFFFF
73
+ for byte in data:
74
+ crc ^= byte
75
+ for _ in range(8):
76
+ if crc & 1:
77
+ crc = (crc >> 1) ^ 0xA001
78
+ else:
79
+ crc >>= 1
80
+ return crc & 0xFFFF
81
+
82
+ @classmethod
83
+ def _append_crc(cls, payload: bytes) -> bytes:
84
+ return payload + struct.pack("<H", cls.crc16_modbus(payload))
85
+
86
+ def _query_frame_open(self, request: bytes, response_len: int) -> bytes:
87
+ response = self.query(request, read_delay=0, return_bytes=True, num_bytes=response_len)
88
+ self._validate_response(response)
89
+ return response
90
+
91
+ def _build_request(self, function_code: int, payload: bytes) -> bytes:
92
+ return self._append_crc(bytes([self.address, function_code]) + payload)
93
+
94
+ @classmethod
95
+ def _validate_response(cls, response: bytes) -> None:
96
+ if len(response) < 5:
97
+ raise RuntimeError(f"incomplete Modbus RTU response: {response.hex(' ')}")
98
+
99
+ body, crc_bytes = response[:-2], response[-2:]
100
+ got_crc = struct.unpack("<H", crc_bytes)[0]
101
+ want_crc = cls.crc16_modbus(body)
102
+ if got_crc != want_crc:
103
+ raise RuntimeError(f"CRC mismatch: got 0x{got_crc:04X}, expected 0x{want_crc:04X}")
104
+
105
+ function = body[1]
106
+ if function & 0x80:
107
+ code = body[2]
108
+ raise RuntimeError(
109
+ f"Modbus exception 0x{code:02X}: {MODBUS_EXCEPTION_CODES.get(code, 'unknown error')}"
110
+ )
111
+
112
+ def _read_register_payload_open(self, function_code: int, register_address: int, register_count: int) -> bytes:
113
+ request = self._build_request(function_code, struct.pack(">HH", register_address, register_count))
114
+ response = self._query_frame_open(request, 5 + register_count * 2)
115
+ return response[3:-2]
116
+
117
+ def _read_registers_open(self, function_code: int, register_address: int, register_count: int) -> tuple[int, ...]:
118
+ payload = self._read_register_payload_open(function_code, register_address, register_count)
119
+ return struct.unpack(f">{register_count}H", payload)
120
+
121
+ def _read_holding_value_open(self, register_address: int, format_code: str):
122
+ payload = self._read_register_payload_open(0x03, register_address, struct.calcsize(">" + format_code) // 2)
123
+ return struct.unpack(">" + format_code, payload)[0]
124
+
125
+ def _write_registers_open(self, register_address: int, values: tuple[int, ...]) -> tuple[int, int]:
126
+ payload = struct.pack(">HHB", register_address, len(values), len(values) * 2) + struct.pack(f">{len(values)}H", *values)
127
+ response = self._query_frame_open(self._build_request(0x10, payload), 8)
128
+ return struct.unpack(">HH", response[2:6])
129
+
130
+ def _read_scaled_integer_open(self, value_register_address: int, decimals_register_address: int) -> float:
131
+ raw = self._read_holding_value_open(value_register_address, "i")
132
+ decimals = self._read_holding_value_open(decimals_register_address, "H")
133
+ return raw / (10 ** decimals)
134
+
135
+ @open_close
136
+ def _read_registers(self, function_code: int, register_address: int, register_count: int) -> tuple[int, ...]:
137
+ return self._read_registers_open(function_code, register_address, register_count)
138
+
139
+ @open_close
140
+ def _read_holding_value(self, register_address: int, format_code: str):
141
+ return self._read_holding_value_open(register_address, format_code)
142
+
143
+ @open_close
144
+ def _read_scaled_integer(self, value_register_address: int, decimals_register_address: int) -> float:
145
+ return self._read_scaled_integer_open(value_register_address, decimals_register_address)
146
+
147
+ @open_close
148
+ def _write_value(self, register_address: int, format_code: str, value):
149
+ values = struct.unpack(">HH", struct.pack(">" + format_code, value))
150
+ self._write_registers_open(register_address, values)
151
+ return value
152
+
153
+ def _execute_command_integer_open(self, command_id: int, argument: int) -> int:
154
+ command_registers = struct.unpack(">HH", struct.pack(">I", command_id))
155
+ argument_registers = struct.unpack(">HH", struct.pack(">i", argument))
156
+ self._write_registers_open(1001, command_registers + argument_registers)
157
+ while True:
158
+ current_id, current_argument, status, result = struct.unpack(">IiIi", self._read_register_payload_open(0x04, 1001, 8))
159
+ if current_id != command_id or current_argument != argument or status == 1:
160
+ continue
161
+ if status != 0:
162
+ raise RuntimeError(f"Alicat command {command_id} failed with status {status}")
163
+ return result
164
+
165
+ def _execute_command_float_open(self, command_id: int, argument: float) -> float:
166
+ command_registers = struct.unpack(">HH", struct.pack(">I", command_id))
167
+ argument_registers = struct.unpack(">HH", struct.pack(">f", argument))
168
+ self._write_registers_open(1001, command_registers + argument_registers)
169
+ while True:
170
+ current_id, current_argument, status, result = struct.unpack(">IfIf", self._read_register_payload_open(0x04, 1001, 8))
171
+ if current_id != command_id or current_argument != argument or status == 1:
172
+ continue
173
+ if status != 0:
174
+ raise RuntimeError(f"Alicat command {command_id} failed with status {status}")
175
+ return result
176
+
177
+ @open_close
178
+ def _command_integer(self, command_id: int, argument: int) -> int:
179
+ return self._execute_command_integer_open(command_id, argument)
180
+
181
+ @open_close
182
+ def _command_float(self, command_id: int, argument: float) -> float:
183
+ return self._execute_command_float_open(command_id, argument)
184
+
185
+ @property
186
+ def byte_order_test(self) -> float:
187
+ return self._read_holding_value(1087, "f")
188
+
189
+ @property
190
+ def firmware_version(self) -> tuple[int, int]:
191
+ major, minor = self._read_registers(0x03, 1089, 2)
192
+ return major, minor
193
+
194
+ @property
195
+ def serial_number(self) -> int:
196
+ return self._read_holding_value(1093, "I")
197
+
198
+ @property
199
+ def manufacture_date(self) -> date:
200
+ month_day, year = self._read_registers(0x03, 1095, 2)
201
+ return date(year, month_day >> 8, month_day & 0xFF)
202
+
203
+ @property
204
+ def calibration_date(self) -> date:
205
+ month_day, year = self._read_registers(0x03, 1097, 2)
206
+ return date(year, month_day >> 8, month_day & 0xFF)
207
+
208
+ @property
209
+ def gas_number(self) -> GasNumber:
210
+ return GasNumber(self._read_holding_value(1346, "H"))
211
+
212
+ @property
213
+ def device_status(self) -> int:
214
+ return self._read_holding_value(1347, "I")
215
+
216
+ @property
217
+ def setpoint(self) -> float:
218
+ return self._read_holding_value(1349, "f")
219
+
220
+ @setpoint.setter
221
+ def setpoint(self, setpoint: float) -> None:
222
+ self._write_value(1349, "f", setpoint)
223
+
224
+ def write_setpoint_integer(self, setpoint: int) -> int:
225
+ return self._write_value(1299, "i", setpoint)
226
+
227
+ @property
228
+ def valve_drive(self) -> float:
229
+ return self._read_holding_value(1351, "f")
230
+
231
+ @property
232
+ def pressure(self) -> float:
233
+ return self._read_holding_value(1353, "f")
234
+
235
+ @property
236
+ def secondary_pressure(self) -> float:
237
+ return self._read_holding_value(1355, "f")
238
+
239
+ @property
240
+ def barometric_pressure(self) -> float:
241
+ return self._read_holding_value(1357, "f")
242
+
243
+ @property
244
+ def temperature(self) -> float:
245
+ return self._read_holding_value(1359, "f")
246
+
247
+ @property
248
+ def volumetric_flow(self) -> float:
249
+ return self._read_holding_value(1361, "f")
250
+
251
+ @property
252
+ def mass_flow(self) -> float:
253
+ return self._read_holding_value(1363, "f")
254
+
255
+ @property
256
+ def totalizer_1(self) -> float:
257
+ return self._read_holding_value(1365, "f")
258
+
259
+ @property
260
+ def totalizer_2(self) -> float:
261
+ return self._read_holding_value(1367, "f")
262
+
263
+ @property
264
+ def humidity(self) -> float:
265
+ return self._read_holding_value(1369, "f")
266
+
267
+ def setpoint_from_integer(self) -> float:
268
+ return self._read_scaled_integer(1299, 1650)
269
+
270
+ def valve_drive_from_integer(self) -> float:
271
+ return self._read_scaled_integer(1301, 1662)
272
+
273
+ def pressure_from_integer(self) -> float:
274
+ return self._read_scaled_integer(1303, 1674)
275
+
276
+ def secondary_pressure_from_integer(self) -> float:
277
+ return self._read_scaled_integer(1305, 1686)
278
+
279
+ def barometric_pressure_from_integer(self) -> float:
280
+ return self._read_scaled_integer(1307, 1698)
281
+
282
+ def temperature_from_integer(self) -> float:
283
+ return self._read_scaled_integer(1309, 1710)
284
+
285
+ def volumetric_flow_from_integer(self) -> float:
286
+ return self._read_scaled_integer(1311, 1722)
287
+
288
+ def mass_flow_from_integer(self) -> float:
289
+ return self._read_scaled_integer(1313, 1734)
290
+
291
+ def totalizer_1_from_integer(self) -> float:
292
+ return self._read_scaled_integer(1315, 1746)
293
+
294
+ def totalizer_2_from_integer(self) -> float:
295
+ return self._read_scaled_integer(1317, 1758)
296
+
297
+ def humidity_from_integer(self) -> float:
298
+ return self._read_scaled_integer(1319, 1770)
299
+
300
+ @property
301
+ def setpoint_minimum(self) -> float:
302
+ return self._read_holding_value(1645, "f")
303
+
304
+ @property
305
+ def setpoint_maximum(self) -> float:
306
+ return self._read_holding_value(1647, "f")
307
+
308
+ @property
309
+ def setpoint_units(self) -> int:
310
+ return StandardFlowUnit(self._read_holding_value(1649, "H"))
311
+
312
+ @property
313
+ def setpoint_decimal_places(self) -> int:
314
+ return self._read_holding_value(1650, "H")
315
+
316
+ @property
317
+ def pressure_minimum(self) -> float:
318
+ return self._read_holding_value(1669, "f")
319
+
320
+ @property
321
+ def pressure_maximum(self) -> float:
322
+ return self._read_holding_value(1671, "f")
323
+
324
+ @property
325
+ def pressure_units(self) -> PressureUnit:
326
+ return PressureUnit(self._read_holding_value(1673, "H"))
327
+
328
+ @property
329
+ def pressure_decimal_places(self) -> int:
330
+ return self._read_holding_value(1674, "H")
331
+
332
+ @property
333
+ def barometric_pressure_minimum(self) -> float:
334
+ return self._read_holding_value(1693, "f")
335
+
336
+ @property
337
+ def barometric_pressure_maximum(self) -> float:
338
+ return self._read_holding_value(1695, "f")
339
+
340
+ @property
341
+ def barometric_pressure_units(self) -> int:
342
+ return self._read_holding_value(1697, "H")
343
+
344
+ @property
345
+ def barometric_pressure_decimal_places(self) -> int:
346
+ return self._read_holding_value(1698, "H")
347
+
348
+ @property
349
+ def temperature_minimum(self) -> float:
350
+ return self._read_holding_value(1705, "f")
351
+
352
+ @property
353
+ def temperature_maximum(self) -> float:
354
+ return self._read_holding_value(1707, "f")
355
+
356
+ @property
357
+ def temperature_units(self) -> TemperatureUnit:
358
+ return TemperatureUnit(self._read_holding_value(1709, "H"))
359
+
360
+ @property
361
+ def temperature_decimal_places(self) -> int:
362
+ return self._read_holding_value(1710, "H")
363
+
364
+ @property
365
+ def volumetric_flow_minimum(self) -> float:
366
+ return self._read_holding_value(1717, "f")
367
+
368
+ @property
369
+ def volumetric_flow_maximum(self) -> float:
370
+ return self._read_holding_value(1719, "f")
371
+
372
+ @property
373
+ def volumetric_flow_units(self) -> VolumetricFlowUnit:
374
+ return VolumetricFlowUnit(self._read_holding_value(1721, "H"))
375
+
376
+ @property
377
+ def volumetric_flow_decimal_places(self) -> int:
378
+ return self._read_holding_value(1722, "H")
379
+
380
+ @property
381
+ def mass_flow_minimum(self) -> float:
382
+ return self._read_holding_value(1729, "f")
383
+
384
+ @property
385
+ def mass_flow_maximum(self) -> float:
386
+ return self._read_holding_value(1731, "f")
387
+
388
+ @property
389
+ def mass_flow_units(self) -> int:
390
+ return self._read_holding_value(1733, "H")
391
+
392
+ @property
393
+ def mass_flow_decimal_places(self) -> int:
394
+ return self._read_holding_value(1734, "H")
395
+
396
+ @property
397
+ def totalizer_1_minimum(self) -> float:
398
+ return self._read_holding_value(1741, "f")
399
+
400
+ @property
401
+ def totalizer_1_maximum(self) -> float:
402
+ return self._read_holding_value(1743, "f")
403
+
404
+ @property
405
+ def totalizer_1_units(self) -> int:
406
+ return self._read_holding_value(1745, "H")
407
+
408
+ @property
409
+ def totalizer_1_decimal_places(self) -> int:
410
+ return self._read_holding_value(1746, "H")
411
+
412
+ @property
413
+ def totalizer_2_minimum(self) -> float:
414
+ return self._read_holding_value(1753, "f")
415
+
416
+ @property
417
+ def totalizer_2_maximum(self) -> float:
418
+ return self._read_holding_value(1755, "f")
419
+
420
+ @property
421
+ def totalizer_2_units(self) -> int:
422
+ return self._read_holding_value(1757, "H")
423
+
424
+ @property
425
+ def totalizer_2_decimal_places(self) -> int:
426
+ return self._read_holding_value(1758, "H")
427
+
428
+ @property
429
+ def humidity_minimum(self) -> float:
430
+ return self._read_holding_value(1765, "f")
431
+
432
+ @property
433
+ def humidity_maximum(self) -> float:
434
+ return self._read_holding_value(1767, "f")
435
+
436
+ @property
437
+ def humidity_units(self) -> int:
438
+ return self._read_holding_value(1769, "H")
439
+
440
+ @property
441
+ def humidity_decimal_places(self) -> int:
442
+ return self._read_holding_value(1770, "H")
443
+
444
+ def no_operation(self) -> int:
445
+ return self._command_integer(0, 0)
446
+
447
+ def read_serial_number_command(self) -> int:
448
+ return self._command_integer(65570, 0)
449
+
450
+ def read_partial_serial_number(self, which: int) -> int:
451
+ return self._command_integer(81, which)
452
+
453
+ def read_manufacture_date_part(self, which: int) -> int:
454
+ return self._command_integer(82, which)
455
+
456
+ def read_calibration_date_part(self, which: int) -> int:
457
+ return self._command_integer(83, which)
458
+
459
+ def read_firmware_version_part(self, which: int) -> int:
460
+ return self._command_integer(84, which)
461
+
462
+ def query_reading_type(self, which: int) -> int:
463
+ return self._command_integer(28, which)
464
+
465
+ def query_reading_source(self, which: int) -> int:
466
+ return self._command_integer(27, which)
467
+
468
+ def query_reading_minimum_integer(self, which: int) -> int:
469
+ return self._command_integer(65538, which)
470
+
471
+ def query_reading_minimum_float(self, which: int) -> float:
472
+ return self._command_float(65536, float(which))
473
+
474
+ def query_reading_maximum_integer(self, which: int) -> int:
475
+ return self._command_integer(65539, which)
476
+
477
+ def query_reading_maximum_float(self, which: int) -> float:
478
+ return self._command_float(65537, float(which))
479
+
480
+ def query_reading_units(self, which: int) -> int:
481
+ return self._command_integer(29, which)
482
+
483
+ def set_reading_engineering_units(self, which: int, units: Union[int, IntEnum]) -> int:
484
+ return self._command_integer(65300 + which, self._int_arg(units))
485
+
486
+ def query_reading_decimal_places(self, which: int) -> int:
487
+ return self._command_integer(30, which)
488
+
489
+ def flow_averaging_time_constant(self, value: int = 65535) -> int:
490
+ return self._command_integer(50, value)
491
+
492
+ def pressure_averaging_time_constant(self, value: int = 65535) -> int:
493
+ return self._command_integer(47, value)
494
+
495
+ def primary_pressure_averaging_time_constant(self, value: int = 65535) -> int:
496
+ return self._command_integer(48, value)
497
+
498
+ def secondary_pressure_averaging_time_constant(self, value: int = 65535) -> int:
499
+ return self._command_integer(49, value)
500
+
501
+ def closed_loop_active_on_zero_pressure_setpoint(self, mode: int = 65535) -> int:
502
+ return self._command_integer(51, mode)
503
+
504
+ def reading_zero_band(self, value: int = 65535) -> int:
505
+ return self._command_integer(44, value)
506
+
507
+ def autotare_enabled(self, mode: int = 0) -> int:
508
+ return self._command_integer(40, mode)
509
+
510
+ def autotare_delay(self, value: int = 0) -> int:
511
+ return self._command_integer(45, value)
512
+
513
+ def hold_valves(self, mode: int) -> int:
514
+ return self._command_integer(6, mode)
515
+
516
+ def process_variable(self, value: int = 65535) -> int:
517
+ return self._command_integer(43, value)
518
+
519
+ def closed_loop_deadband_integer(self, value: int = -1) -> int:
520
+ return self._command_integer(65554, value)
521
+
522
+ def closed_loop_deadband_float(self, value: float = -1.0) -> float:
523
+ return self._command_float(65555, value)
524
+
525
+ def closed_loop_deadband_mode(self, mode: int = 0) -> int:
526
+ return self._command_integer(41, mode)
527
+
528
+ def query_totalizer_batch_integer(self, which: int) -> int:
529
+ return self._command_integer(65543, which)
530
+
531
+ def query_totalizer_batch_float(self, which: int) -> float:
532
+ return self._command_float(65540, float(which))
533
+
534
+ def set_totalizer_batch_1_integer(self, value: int) -> int:
535
+ return self._command_integer(65544, value)
536
+
537
+ def set_totalizer_batch_1_float(self, value: float) -> float:
538
+ return self._command_float(65541, value)
539
+
540
+ def set_totalizer_batch_2_integer(self, value: int) -> int:
541
+ return self._command_integer(65545, value)
542
+
543
+ def set_totalizer_batch_2_float(self, value: float) -> float:
544
+ return self._command_float(65542, value)
545
+
546
+ def query_setpoint_limit_integer(self, which: int) -> int:
547
+ return self._command_integer(65548, which)
548
+
549
+ def set_minimum_setpoint_limit_integer(self, value: int) -> int:
550
+ return self._command_integer(65550, value)
551
+
552
+ def set_maximum_setpoint_limit_integer(self, value: int) -> int:
553
+ return self._command_integer(65552, value)
554
+
555
+ def query_setpoint_limit_float(self, which: int) -> float:
556
+ return self._command_float(65549, float(which))
557
+
558
+ def set_minimum_setpoint_limit_float(self, value: float) -> float:
559
+ return self._command_float(65551, value)
560
+
561
+ def set_maximum_setpoint_limit_float(self, value: float) -> float:
562
+ return self._command_float(65553, value)
563
+
564
+ def set_power_up_setpoint(self) -> int:
565
+ return self._command_integer(12, 0)
566
+
567
+ def setpoint_maximum_ramp_saved(self, value: int = -1) -> int:
568
+ return self._command_integer(65546, value)
569
+
570
+ def setpoint_maximum_ramp_temporary(self, value: int = -1) -> int:
571
+ return self._command_integer(65547, value)
572
+
573
+ def setpoint_ramp_enabled(self, value: int = 0) -> int:
574
+ return self._command_integer(42, value)
575
+
576
+ def set_active_valve(self, value: int = 65535) -> int:
577
+ return self._command_integer(15, value)
578
+
579
+ def set_loop_control_variable(self, value: int) -> int:
580
+ return self._command_integer(11, value)
581
+
582
+ def set_loop_control_algorithm(self, value: int) -> int:
583
+ return self._command_integer(13, value)
584
+
585
+ def read_closed_loop_control_gain(self, which: int) -> int:
586
+ return self._command_integer(14, which)
587
+
588
+ def set_proportional_closed_loop_control_gain(self, value: int) -> int:
589
+ return self._command_integer(8, value)
590
+
591
+ def set_integral_closed_loop_control_gain(self, value: int) -> int:
592
+ return self._command_integer(10, value)
593
+
594
+ def set_derivative_closed_loop_control_gain(self, value: int) -> int:
595
+ return self._command_integer(9, value)
596
+
597
+ def control_gain_gas_compensation(self, mode: int = 0) -> int:
598
+ return self._command_integer(62, mode)
599
+
600
+ def perform_tare(self, which: int) -> int:
601
+ return self._command_integer(4, which)
602
+
603
+ def perform_pressure_sensor_tare(self, milliseconds: int = 0) -> int:
604
+ return self._command_integer(31, milliseconds)
605
+
606
+ def perform_secondary_pressure_sensor_tare(self, milliseconds: int = 0) -> int:
607
+ return self._command_integer(32, milliseconds)
608
+
609
+ def perform_flow_tare(self, milliseconds: int = 0) -> int:
610
+ return self._command_integer(33, milliseconds)
611
+
612
+ def reset_totalizer(self, which: int) -> int:
613
+ return self._command_integer(80, which)
614
+
615
+ def reset_totalizer_1(self) -> int:
616
+ return self._command_integer(5, 0)
617
+
618
+ def set_selected_gas(self, gas_number: Union[int, IntEnum]) -> GasNumber:
619
+ return GasNumber(self._command_integer(1, self._int_arg(gas_number)))
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from enum import IntEnum
5
+ from logging import Logger, getLogger
6
+ from typing import Optional, Union
7
+
8
+ from matterlab_mfcs.enums import GasNumber
9
+
10
+
11
+ class MassFlowController(ABC):
12
+ """Abstract base class for Matter Lab mass flow controllers."""
13
+
14
+ category = "MassFlowController"
15
+ ui_fields = ("com_port", "address")
16
+
17
+ def __init__(self, logger: Optional[Logger] = None) -> None:
18
+ self.logger = logger if logger is not None else getLogger(
19
+ f"{self.__class__.__module__}.{self.__class__.__name__}"
20
+ )
21
+
22
+ @property
23
+ @abstractmethod
24
+ def serial_number(self) -> int:
25
+ """Return the device serial number."""
26
+
27
+ @property
28
+ @abstractmethod
29
+ def device_status(self) -> int:
30
+ """Return the device status bitfield or device-defined status code."""
31
+
32
+ @property
33
+ @abstractmethod
34
+ def gas_number(self) -> GasNumber:
35
+ """Return the currently selected gas number."""
36
+
37
+ @property
38
+ @abstractmethod
39
+ def setpoint(self) -> float:
40
+ """Return the current setpoint in engineering units."""
41
+
42
+ @setpoint.setter
43
+ @abstractmethod
44
+ def setpoint(self, value: float) -> None:
45
+ """Set the current setpoint in engineering units."""
46
+
47
+ @property
48
+ @abstractmethod
49
+ def volumetric_flow(self) -> float:
50
+ """Return the current volumetric flow reading in engineering units."""
51
+
52
+ @property
53
+ @abstractmethod
54
+ def mass_flow(self) -> float:
55
+ """Return the current mass flow reading in engineering units."""
56
+
57
+ @property
58
+ @abstractmethod
59
+ def pressure(self) -> float:
60
+ """Return the current pressure reading in engineering units."""
61
+
62
+ @property
63
+ @abstractmethod
64
+ def temperature(self) -> float:
65
+ """Return the current temperature reading in engineering units."""
66
+
67
+ @abstractmethod
68
+ def set_selected_gas(self, gas_number: Union[int, IntEnum]) -> GasNumber:
69
+ """Select the active gas number and return the device response."""
70
+
71
+ def stop(self) -> None:
72
+ """Convenience helper for setting the setpoint to zero."""
73
+ self.setpoint = 0.0
@@ -0,0 +1,595 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import IntEnum
4
+
5
+
6
+ class DisplayIntEnum(IntEnum):
7
+ __labels__: dict[int, str] = {}
8
+
9
+ def __str__(self) -> str:
10
+ return self.__labels__.get(int(self), self.name)
11
+
12
+
13
+ class StandardFlowUnit(DisplayIntEnum):
14
+ UNSPECIFIED = 0
15
+ UNKNOWN = 1
16
+ STANDARD_MICROLITER_PER_MINUTE = 2
17
+ STANDARD_MILLILITER_PER_SECOND = 3
18
+ STANDARD_MILLILITER_PER_MINUTE = 4
19
+ STANDARD_MILLILITER_PER_HOUR = 5
20
+ STANDARD_LITER_PER_SECOND = 6
21
+ STANDARD_LITER_PER_MINUTE = 7
22
+ STANDARD_LITER_PER_HOUR = 8
23
+ STANDARD_CUBIC_CENTIMETER_PER_SECOND = 11
24
+ STANDARD_CUBIC_CENTIMETER_PER_MINUTE = 12
25
+ STANDARD_CUBIC_CENTIMETER_PER_HOUR = 13
26
+ STANDARD_CUBIC_METER_PER_MINUTE = 14
27
+ STANDARD_CUBIC_METER_PER_HOUR = 15
28
+ STANDARD_CUBIC_METER_PER_DAY = 16
29
+ STANDARD_CUBIC_INCH_PER_MINUTE = 17
30
+ STANDARD_CUBIC_FOOT_PER_MINUTE = 18
31
+ STANDARD_CUBIC_FOOT_PER_HOUR = 19
32
+ THOUSAND_STANDARD_CUBIC_FEET_PER_MINUTE = 20
33
+ STANDARD_CUBIC_FOOT_PER_DAY = 21
34
+ NORMAL_MICROLITER_PER_MINUTE = 32
35
+ NORMAL_MILLILITER_PER_SECOND = 33
36
+ NORMAL_MILLILITER_PER_MINUTE = 34
37
+ NORMAL_MILLILITER_PER_HOUR = 35
38
+ NORMAL_LITER_PER_SECOND = 36
39
+ NORMAL_LITER_PER_MINUTE = 37
40
+ NORMAL_LITER_PER_HOUR = 38
41
+ NORMAL_CUBIC_CENTIMETER_PER_SECOND = 41
42
+ NORMAL_CUBIC_CENTIMETER_PER_MINUTE = 42
43
+ NORMAL_CUBIC_CENTIMETER_PER_HOUR = 43
44
+ NORMAL_CUBIC_METER_PER_MINUTE = 44
45
+ NORMAL_CUBIC_METER_PER_HOUR = 45
46
+ NORMAL_CUBIC_METER_PER_DAY = 46
47
+ COUNT = 62
48
+ PERCENT_FULL_SCALE = 63
49
+ SLPM = 7
50
+ SCCM = 12
51
+ NLPM = 37
52
+ NCCM = 42
53
+
54
+
55
+ class TrueMassFlowUnit(DisplayIntEnum):
56
+ MILLIGRAM_PER_SECOND = 64
57
+ MILLIGRAM_PER_MINUTE = 65
58
+ GRAM_PER_SECOND = 66
59
+ GRAM_PER_MINUTE = 67
60
+ GRAM_PER_HOUR = 68
61
+ KILOGRAM_PER_MINUTE = 69
62
+ KILOGRAM_PER_HOUR = 70
63
+ OUNCE_PER_SECOND = 71
64
+ OUNCE_PER_MINUTE = 72
65
+ POUND_PER_MINUTE = 73
66
+ POUND_PER_HOUR = 74
67
+
68
+
69
+ class TotalStandardVolumeUnit(DisplayIntEnum):
70
+ UNSPECIFIED = 0
71
+ UNKNOWN = 1
72
+ STANDARD_MICROLITER = 2
73
+ STANDARD_MILLILITER = 3
74
+ STANDARD_LITER = 4
75
+ STANDARD_CUBIC_CENTIMETER = 6
76
+ STANDARD_CUBIC_METER = 7
77
+ STANDARD_CUBIC_INCH = 8
78
+ STANDARD_CUBIC_FOOT = 9
79
+ THOUSAND_STANDARD_CUBIC_FEET = 10
80
+ NORMAL_MICROLITER = 32
81
+ NORMAL_MILLILITER = 33
82
+ NORMAL_LITER = 34
83
+ NORMAL_CUBIC_CENTIMETER = 36
84
+ NORMAL_CUBIC_METER = 37
85
+
86
+
87
+ class VolumetricFlowUnit(DisplayIntEnum):
88
+ UNSPECIFIED = 0
89
+ UNKNOWN = 1
90
+ MICROLITER_PER_MINUTE = 2
91
+ MILLILITER_PER_SECOND = 3
92
+ MILLILITER_PER_MINUTE = 4
93
+ MILLILITER_PER_HOUR = 5
94
+ LITER_PER_SECOND = 6
95
+ LITER_PER_MINUTE = 7
96
+ LITER_PER_HOUR = 8
97
+ US_GALLON_PER_MINUTE = 9
98
+ US_GALLON_PER_HOUR = 10
99
+ CUBIC_CENTIMETER_PER_SECOND = 11
100
+ CUBIC_CENTIMETER_PER_MINUTE = 12
101
+ CUBIC_CENTIMETER_PER_HOUR = 13
102
+ CUBIC_METER_PER_MINUTE = 14
103
+ CUBIC_METER_PER_HOUR = 15
104
+ CUBIC_METER_PER_DAY = 16
105
+ CUBIC_INCH_PER_MINUTE = 17
106
+ CUBIC_FOOT_PER_MINUTE = 18
107
+ CUBIC_FOOT_PER_HOUR = 19
108
+ CUBIC_FOOT_PER_DAY = 21
109
+ COUNT = 62
110
+ PERCENT_FULL_SCALE = 63
111
+ LPM = 7
112
+ SCCM = 12
113
+ CFM = 18
114
+
115
+
116
+ class TotalVolumeUnit(DisplayIntEnum):
117
+ UNSPECIFIED = 0
118
+ UNKNOWN = 1
119
+ MICROLITER = 2
120
+ MILLILITER = 3
121
+ LITER = 4
122
+ US_GALLON = 5
123
+ CUBIC_CENTIMETER = 6
124
+ CUBIC_METER = 7
125
+ CUBIC_INCH = 8
126
+ CUBIC_FOOT = 9
127
+ MICROPOISE = 61
128
+
129
+
130
+ class PressureUnit(DisplayIntEnum):
131
+ UNSPECIFIED = 0
132
+ UNKNOWN = 1
133
+ PASCAL = 2
134
+ HECTOPASCAL = 3
135
+ KILOPASCAL = 4
136
+ MEGAPASCAL = 5
137
+ MILLIBAR = 6
138
+ BAR = 7
139
+ GRAM_FORCE_PER_SQUARE_CENTIMETER = 8
140
+ KILOGRAM_FORCE_PER_SQUARE_CENTIMETER = 9
141
+ PSI = 10
142
+ PSF = 11
143
+ MILLITORR = 12
144
+ TORR = 13
145
+ MILLIMETER_MERCURY_0C = 14
146
+ INCHES_MERCURY_0C = 15
147
+ MILLIMETER_WATER_4C = 16
148
+ MILLIMETER_WATER_60F = 17
149
+ CENTIMETER_WATER_4C = 18
150
+ CENTIMETER_WATER_60F = 19
151
+ INCH_WATER_4C = 20
152
+ INCH_WATER_60F = 21
153
+ ATMOSPHERE = 22
154
+ VOLT = 61
155
+ COUNT = 62
156
+ PERCENT_FULL_SCALE = 63
157
+
158
+
159
+ class TemperatureUnit(DisplayIntEnum):
160
+ UNSPECIFIED = 0
161
+ UNKNOWN = 1
162
+ CELSIUS = 2
163
+ FAHRENHEIT = 3
164
+ KELVIN = 4
165
+ RANKINE = 5
166
+
167
+
168
+ class GasNumber(DisplayIntEnum):
169
+ AIR = 0
170
+ ARGON = 1
171
+ METHANE = 2
172
+ CARBON_MONOXIDE = 3
173
+ CARBON_DIOXIDE = 4
174
+ ETHANE = 5
175
+ HYDROGEN = 6
176
+ HELIUM = 7
177
+ NITROGEN = 8
178
+ NITROUS_OXIDE = 9
179
+ NEON = 10
180
+ OXYGEN = 11
181
+ PROPANE = 12
182
+ NORMAL_BUTANE = 13
183
+ ACETYLENE = 14
184
+ ETHYLENE = 15
185
+ ISOBUTANE = 16
186
+ KRYPTON = 17
187
+ XENON = 18
188
+ SULFUR_HEXAFLUORIDE = 19
189
+ C_25 = 20
190
+ C_10 = 21
191
+ C_8 = 22
192
+ C_2 = 23
193
+ C_75 = 24
194
+ HE_25 = 25
195
+ HE_75 = 26
196
+ A1025 = 27
197
+ STAR29 = 28
198
+ P_5 = 29
199
+ NITRIC_OXIDE = 30
200
+ NITROGEN_TRIFLUORIDE = 31
201
+ AMMONIA = 32
202
+ CHLORINE = 33
203
+ HYDROGEN_SULFIDE = 34
204
+ SULFUR_DIOXIDE = 35
205
+ PROPYLENE = 36
206
+ ONE_BUTENE = 80
207
+ CIS_BUTENE = 81
208
+ ISOBUTENE = 82
209
+ TRANS_2_BUTENE = 83
210
+ CARBONYL_SULFIDE = 84
211
+ DIMETHYL_ETHER = 85
212
+ SILANE = 86
213
+ R_11 = 100
214
+ R_115 = 101
215
+ R_116 = 102
216
+ R_124 = 103
217
+ R_125 = 104
218
+ R_134A = 105
219
+ R_14 = 106
220
+ R_142B = 107
221
+ R_143A = 108
222
+ R_152A = 109
223
+ R_22 = 110
224
+ R_23 = 111
225
+ R_32 = 112
226
+ R_318 = 113
227
+ R_404A = 114
228
+ R_407C = 115
229
+ R_410A = 116
230
+ R_507A = 117
231
+ C_15 = 140
232
+ C_20 = 141
233
+ C_50 = 142
234
+ HE_50 = 143
235
+ HE_90 = 144
236
+ BIO5M = 145
237
+ BIO10M = 146
238
+ BIO15M = 147
239
+ BIO20M = 148
240
+ BIO25M = 149
241
+ BIO30M = 150
242
+ BIO35M = 151
243
+ BIO40M = 152
244
+ BIO45M = 153
245
+ BIO50M = 154
246
+ BIO55M = 155
247
+ BIO60M = 156
248
+ BIO65M = 157
249
+ BIO70M = 158
250
+ BIO75M = 159
251
+ BIO80M = 160
252
+ BIO85M = 161
253
+ BIO90M = 162
254
+ BIO95M = 163
255
+ EAN_32 = 164
256
+ EAN_36 = 165
257
+ EAN_40 = 166
258
+ HEOX20 = 167
259
+ HEOX21 = 168
260
+ HEOX30 = 169
261
+ HEOX40 = 170
262
+ HEOX50 = 171
263
+ HEOX60 = 172
264
+ HEOX80 = 173
265
+ HEOX99 = 174
266
+ EA_40 = 175
267
+ EA_60 = 176
268
+ EA_80 = 177
269
+ METABOLIC_EXHALANT = 178
270
+ LG_4_5 = 179
271
+ LG_6 = 180
272
+ LG_7 = 181
273
+ LG_9 = 182
274
+ HENE_9 = 183
275
+ LG_9_4 = 184
276
+ SYNG_1 = 185
277
+ SYNG_2 = 186
278
+ SYNG_3 = 187
279
+ SYNG_4 = 188
280
+ NATG_1 = 189
281
+ NATG_2 = 190
282
+ NATG_3 = 191
283
+ COAL_GAS = 192
284
+ ENDO = 193
285
+ HHO = 194
286
+ HD_5 = 195
287
+ HD_10 = 196
288
+ OCG_89 = 197
289
+ OCG_93 = 198
290
+ OCG_95 = 199
291
+ FG_1 = 200
292
+ FG_2 = 201
293
+ FG_3 = 202
294
+ FG_4 = 203
295
+ FG_5 = 204
296
+ FG_6 = 205
297
+ P_10 = 206
298
+ DEUTERIUM = 210
299
+ AR = 1
300
+ CH4 = 2
301
+ CO = 3
302
+ CO2 = 4
303
+ H2 = 6
304
+ HE = 7
305
+ N2 = 8
306
+ N2O = 9
307
+ O2 = 11
308
+ C3H8 = 12
309
+ SF6 = 19
310
+ NH3 = 32
311
+ CL2 = 33
312
+ H2S = 34
313
+ SO2 = 35
314
+
315
+
316
+ StandardFlowUnit.__labels__ = {
317
+ 0: "default",
318
+ 1: "---",
319
+ 2: "SuL/m",
320
+ 3: "SmL/s",
321
+ 4: "SmL/m",
322
+ 5: "SmL/h",
323
+ 6: "SL/s",
324
+ 7: "SLPM",
325
+ 8: "SL/h",
326
+ 11: "SCCS",
327
+ 12: "SCCM",
328
+ 13: "Scm^3/h",
329
+ 14: "Sm^3/m",
330
+ 15: "Sm^3/h",
331
+ 16: "Sm^3/d",
332
+ 17: "Sin^3/m",
333
+ 18: "SCFM",
334
+ 19: "SCFH",
335
+ 20: "kSCFM",
336
+ 21: "SCFD",
337
+ 32: "NuL/m",
338
+ 33: "NmL/s",
339
+ 34: "NmL/m",
340
+ 35: "NmL/h",
341
+ 36: "NL/s",
342
+ 37: "NLPM",
343
+ 38: "NL/h",
344
+ 41: "NCCS",
345
+ 42: "NCCM",
346
+ 43: "Ncm^3/h",
347
+ 44: "Nm^3/m",
348
+ 45: "Nm^3/h",
349
+ 46: "Nm^3/d",
350
+ 62: "count",
351
+ 63: "%",
352
+ }
353
+
354
+ TrueMassFlowUnit.__labels__ = {
355
+ 64: "mg/s",
356
+ 65: "mg/m",
357
+ 66: "g/s",
358
+ 67: "g/m",
359
+ 68: "g/h",
360
+ 69: "kg/m",
361
+ 70: "kg/h",
362
+ 71: "oz/s",
363
+ 72: "oz/m",
364
+ 73: "lb/m",
365
+ 74: "lb/h",
366
+ }
367
+
368
+ TotalStandardVolumeUnit.__labels__ = {
369
+ 0: "default",
370
+ 1: "---",
371
+ 2: "SuL",
372
+ 3: "SmL",
373
+ 4: "SL",
374
+ 6: "Scm^3",
375
+ 7: "Sm^3",
376
+ 8: "Sin^3",
377
+ 9: "Sft^3",
378
+ 10: "kSft^3",
379
+ 32: "NuL",
380
+ 33: "NmL",
381
+ 34: "NL",
382
+ 36: "Ncm^3",
383
+ 37: "Nm^3",
384
+ }
385
+
386
+ VolumetricFlowUnit.__labels__ = {
387
+ 0: "default",
388
+ 1: "---",
389
+ 2: "uL/m",
390
+ 3: "mL/s",
391
+ 4: "mL/m",
392
+ 5: "mL/h",
393
+ 6: "L/s",
394
+ 7: "LPM",
395
+ 8: "L/h",
396
+ 9: "US GPM",
397
+ 10: "US GPH",
398
+ 11: "CCS",
399
+ 12: "CCM",
400
+ 13: "cm^3/h",
401
+ 14: "m^3/m",
402
+ 15: "m^3/h",
403
+ 16: "m^3/d",
404
+ 17: "in^3/m",
405
+ 18: "CFM",
406
+ 19: "CFH",
407
+ 21: "CFD",
408
+ 62: "count",
409
+ 63: "%",
410
+ }
411
+
412
+ TotalVolumeUnit.__labels__ = {
413
+ 0: "default",
414
+ 1: "---",
415
+ 2: "uL",
416
+ 3: "mL",
417
+ 4: "L",
418
+ 5: "US GAL",
419
+ 6: "cm^3",
420
+ 7: "m^3",
421
+ 8: "in^3",
422
+ 9: "ft^3",
423
+ 61: "uP",
424
+ }
425
+
426
+ PressureUnit.__labels__ = {
427
+ 0: "default",
428
+ 1: "---",
429
+ 2: "Pa",
430
+ 3: "hPa",
431
+ 4: "kPa",
432
+ 5: "MPa",
433
+ 6: "mbar",
434
+ 7: "bar",
435
+ 8: "g/cm^2",
436
+ 9: "kg/cm^2",
437
+ 10: "PSI",
438
+ 11: "PSF",
439
+ 12: "mTorr",
440
+ 13: "torr",
441
+ 14: "mmHg",
442
+ 15: "inHg",
443
+ 16: "mmH2O",
444
+ 17: "mmH2O(60F)",
445
+ 18: "cmH2O",
446
+ 19: "cmH2O(60F)",
447
+ 20: "inH2O",
448
+ 21: "inH2O(60F)",
449
+ 22: "atm",
450
+ 61: "V",
451
+ 62: "count",
452
+ 63: "%",
453
+ }
454
+
455
+ TemperatureUnit.__labels__ = {
456
+ 0: "default",
457
+ 1: "---",
458
+ 2: "C",
459
+ 3: "F",
460
+ 4: "K",
461
+ 5: "Ra",
462
+ }
463
+
464
+ GasNumber.__labels__ = {
465
+ 0: "Air",
466
+ 1: "Ar",
467
+ 2: "CH4",
468
+ 3: "CO",
469
+ 4: "CO2",
470
+ 5: "C2H6",
471
+ 6: "H2",
472
+ 7: "He",
473
+ 8: "N2",
474
+ 9: "N2O",
475
+ 10: "Ne",
476
+ 11: "O2",
477
+ 12: "C3H8",
478
+ 13: "nC4H10",
479
+ 14: "C2H2",
480
+ 15: "C2H4",
481
+ 16: "iC4H10",
482
+ 17: "Kr",
483
+ 18: "Xe",
484
+ 19: "SF6",
485
+ 20: "C-25",
486
+ 21: "C-10",
487
+ 22: "C-8",
488
+ 23: "C-2",
489
+ 24: "C-75",
490
+ 25: "He-25",
491
+ 26: "He-75",
492
+ 27: "A1025",
493
+ 28: "Star29",
494
+ 29: "P-5",
495
+ 30: "NO",
496
+ 31: "NF3",
497
+ 32: "NH3",
498
+ 33: "Cl2",
499
+ 34: "H2S",
500
+ 35: "SO2",
501
+ 36: "C3H6",
502
+ 80: "1Buten",
503
+ 81: "cButen",
504
+ 82: "iButen",
505
+ 83: "tButen",
506
+ 84: "COS",
507
+ 85: "DME",
508
+ 86: "SiH4",
509
+ 100: "R-11",
510
+ 101: "R-115",
511
+ 102: "R-116",
512
+ 103: "R-124",
513
+ 104: "R-125",
514
+ 105: "R-134A",
515
+ 106: "R-14",
516
+ 107: "R-142b",
517
+ 108: "R-143a",
518
+ 109: "R-152a",
519
+ 110: "R-22",
520
+ 111: "R-23",
521
+ 112: "R-32",
522
+ 113: "R-318",
523
+ 114: "R-404A",
524
+ 115: "R-407C",
525
+ 116: "R-410A",
526
+ 117: "R-507A",
527
+ 140: "C-15",
528
+ 141: "C-20",
529
+ 142: "C-50",
530
+ 143: "He-50",
531
+ 144: "He-90",
532
+ 145: "Bio5M",
533
+ 146: "Bio10M",
534
+ 147: "Bio15M",
535
+ 148: "Bio20M",
536
+ 149: "Bio25M",
537
+ 150: "Bio30M",
538
+ 151: "Bio35M",
539
+ 152: "Bio40M",
540
+ 153: "Bio45M",
541
+ 154: "Bio50M",
542
+ 155: "Bio55M",
543
+ 156: "Bio60M",
544
+ 157: "Bio65M",
545
+ 158: "Bio70M",
546
+ 159: "Bio75M",
547
+ 160: "Bio80M",
548
+ 161: "Bio85M",
549
+ 162: "Bio90M",
550
+ 163: "Bio95M",
551
+ 164: "EAN-32",
552
+ 165: "EAN-36",
553
+ 166: "EAN-40",
554
+ 167: "HeOx20",
555
+ 168: "HeOx21",
556
+ 169: "HeOx30",
557
+ 170: "HeOx40",
558
+ 171: "HeOx50",
559
+ 172: "HeOx60",
560
+ 173: "HeOx80",
561
+ 174: "HeOx99",
562
+ 175: "EA-40",
563
+ 176: "EA-60",
564
+ 177: "EA-80",
565
+ 178: "Metab",
566
+ 179: "LG-4.5",
567
+ 180: "LG-6",
568
+ 181: "LG-7",
569
+ 182: "LG-9",
570
+ 183: "HeNe-9",
571
+ 184: "LG-9.4",
572
+ 185: "SynG-1",
573
+ 186: "SynG-2",
574
+ 187: "SynG-3",
575
+ 188: "SynG-4",
576
+ 189: "NatG-1",
577
+ 190: "NatG-2",
578
+ 191: "NatG-3",
579
+ 192: "CoalG",
580
+ 193: "Endo",
581
+ 194: "HHO",
582
+ 195: "HD-5",
583
+ 196: "HD-10",
584
+ 197: "OCG-89",
585
+ 198: "OCG-93",
586
+ 199: "OCG-95",
587
+ 200: "FG-1",
588
+ 201: "FG-2",
589
+ 202: "FG-3",
590
+ 203: "FG-4",
591
+ 204: "FG-5",
592
+ 205: "FG-6",
593
+ 206: "P-10",
594
+ 210: "D2",
595
+ }
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: matterlab_mfcs
3
+ Version: 0.1.0
4
+ Summary: Python APIs for mass flow controllers used in the Matter Lab.
5
+ Author: Han Hao
6
+ License-Expression: MIT
7
+ Keywords: alicat,massflowcontroller,automation,matterlab
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
13
+ Classifier: Topic :: System :: Hardware
14
+ Classifier: Topic :: System :: Hardware :: Hardware Drivers
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: matterlab_serial_device
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7; extra == "dev"
20
+ Requires-Dist: pytest-timeout; extra == "dev"
21
+ Requires-Dist: ruff; extra == "dev"
22
+
23
+ # Matter Lab Mass Flow Controllers
24
+
25
+ `matterlab_mfcs` provides Python drivers for mass flow controllers used in Matter Lab automation workflows.
26
+
27
+ The first supported device family is Alicat instruments using the Modbus RTU manual bundled with this workspace.
28
+
29
+ ## Installation
30
+
31
+ Use the `matterlab` conda environment and install the package in editable mode during development:
32
+
33
+ ```bash
34
+ pip install -e .[dev]
35
+ ```
36
+
37
+ The package depends on `matterlab_serial_device` for serial communication.
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ from matterlab_mfcs import AlicatMFC
43
+
44
+ mfc = AlicatMFC(com_port="COM5", address=1)
45
+ print(mfc.firmware_version)
46
+ print(mfc.serial_number)
47
+ print(mfc.pressure)
48
+ print(mfc.temperature)
49
+ ```
50
+
51
+ ## Testing
52
+
53
+ Unit tests are the default:
54
+
55
+ ```bash
56
+ pytest
57
+ ```
58
+
59
+ Real hardware validation is opt-in:
60
+
61
+ ```bash
62
+ pytest tests/test_alicat_mfc_real.py --run-real --com-port COM5 --address 1
63
+ ```
64
+
65
+ ## Notes
66
+
67
+ - Source code lives under `src/`
68
+ - Pytest uses `src` as the import root
69
+ - Real hardware tests are skipped unless explicitly enabled
70
+ - The Alicat Modbus CRC implementation matches the Modbus RTU framing validated against the local device on `COM5`
71
+ - Multi-read snapshots should live in scripts or tests, not inside the driver
@@ -0,0 +1,8 @@
1
+ matterlab_mfcs/__init__.py,sha256=pF-PvCvSwF1CQuAuEU4szrcawfFDi1xP_xbGAKdgP3s,558
2
+ matterlab_mfcs/alicat_mfc.py,sha256=goEDoC9E44oGVM1aHF7e9nR9BJUkzm9khBJQc2gJJl8,22701
3
+ matterlab_mfcs/base_mfc.py,sha256=W2hu4-KckdzCBbE4QaPZJqITFbaCTanal0S7N_LUApI,2163
4
+ matterlab_mfcs/enums.py,sha256=WblSL0uy3Q2Um69hvCWGtzXz7Wl_208YHGsuk-2bttg,11113
5
+ matterlab_mfcs-0.1.0.dist-info/METADATA,sha256=ye_yNbQMjMblY96DdEv8txpdwwnmLNoRHrNqSx2JNyA,2116
6
+ matterlab_mfcs-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ matterlab_mfcs-0.1.0.dist-info/top_level.txt,sha256=OEZVQlwF9jRrMVkVQOsT4mADB7R00Ke4O_JurcBUpyo,15
8
+ matterlab_mfcs-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ matterlab_mfcs