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.
- matterlab_mfcs/__init__.py +25 -0
- matterlab_mfcs/alicat_mfc.py +619 -0
- matterlab_mfcs/base_mfc.py +73 -0
- matterlab_mfcs/enums.py +595 -0
- matterlab_mfcs-0.1.0.dist-info/METADATA +71 -0
- matterlab_mfcs-0.1.0.dist-info/RECORD +8 -0
- matterlab_mfcs-0.1.0.dist-info/WHEEL +5 -0
- matterlab_mfcs-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
matterlab_mfcs/enums.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
matterlab_mfcs
|