pigeon-transmission 0.0.2__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.
- pigeon_transmission/__init__.py +4 -0
- pigeon_transmission/device_h4.py +996 -0
- pigeon_transmission/device_h5.py +1056 -0
- pigeon_transmission-0.0.2.dist-info/METADATA +116 -0
- pigeon_transmission-0.0.2.dist-info/RECORD +8 -0
- pigeon_transmission-0.0.2.dist-info/WHEEL +5 -0
- pigeon_transmission-0.0.2.dist-info/licenses/LICENSE +692 -0
- pigeon_transmission-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
"""
|
|
2
|
+
device_h5.py
|
|
3
|
+
|
|
4
|
+
This library provides a high-level interface for communicating with IMT Analytics CITREX measurement
|
|
5
|
+
devices via serial connection. It supports executing commands, reading and writing settings,
|
|
6
|
+
retrieving measurements, system states, system information and using the imt protocol.
|
|
7
|
+
It is not an official library provided or endorsed by IMT Analytics.
|
|
8
|
+
|
|
9
|
+
Author: Dennis Schramm
|
|
10
|
+
Date: 06.02.2026
|
|
11
|
+
Version: 0.0.2
|
|
12
|
+
Python-version: 3.10
|
|
13
|
+
Coding conventions: Is complied with PEP8 Style Guide for Python Code. E501 max line length is ignored.
|
|
14
|
+
"""
|
|
15
|
+
import serial
|
|
16
|
+
import copy
|
|
17
|
+
import time
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
message_core_structure = { # the standard syntax for a msg
|
|
22
|
+
"start_token": ['%', 1],
|
|
23
|
+
"operation": [None, None],
|
|
24
|
+
"separator_token": ['#', 1],
|
|
25
|
+
"operation_identifier": [None, None]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
message_value_optional = { # the syntax when a msg or response has a value
|
|
29
|
+
"value_prefix": ['$', 1],
|
|
30
|
+
"value": [None, None]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
message_terminator = { # the standard syntax for terminator
|
|
34
|
+
"end_token": ['\r', 1]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
message_value_mapping = { # Overview of whether a command and a response can contain a value
|
|
38
|
+
"CM": {"command_has_value": None, "response_has_value": None}, # None mean its variable (exist or not)
|
|
39
|
+
"WS": {"command_has_value": True, "response_has_value": True}, # True means a value exist
|
|
40
|
+
"RS": {"command_has_value": False, "response_has_value": True}, # False mean a value do not exist
|
|
41
|
+
"RM": {"command_has_value": False, "response_has_value": True},
|
|
42
|
+
"RI": {"command_has_value": False, "response_has_value": True},
|
|
43
|
+
"ST": {"command_has_value": False, "response_has_value": True}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
stream_protocol_specs = {
|
|
47
|
+
"stream": {"baudrate": 19200, "msg length": 9},
|
|
48
|
+
"fast stream": {"baudrate": 115200, "msg length": 27}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
execute_command_dictionary = { # this dict translate the string for the operation identifier and contain the conversion factor
|
|
52
|
+
"start_pressure/flow_offset_adjust": {"operation_identifier": 1, "conversion_factor": None},
|
|
53
|
+
"start_oxygen_calibration": {"operation_identifier": 2, "conversion_factor": None},
|
|
54
|
+
"next_calibration_step": {"operation_identifier": 3, "conversion_factor": None},
|
|
55
|
+
"stop_running_calibration": {"operation_identifier": 4, "conversion_factor": None},
|
|
56
|
+
"switch_echo": {"operation_identifier": 5, "conversion_factor": None, "value": {
|
|
57
|
+
"off": 0,
|
|
58
|
+
"on": 1}},
|
|
59
|
+
"start_stream": {"operation_identifier": 64, "conversion_factor": None},
|
|
60
|
+
"stop_stream": {"operation_identifier": 65, "conversion_factor": None},
|
|
61
|
+
"start_automated_zero_calibration": {"operation_identifier": 66, "conversion_factor": None, "range": {"min": 1, "max": 1}},
|
|
62
|
+
"screen": {"operation_identifier": 67, "conversion_factor": None, "value": {
|
|
63
|
+
"unlocked": 0,
|
|
64
|
+
"locked": 1}},
|
|
65
|
+
"touch": {"operation_identifier": 68, "conversion_factor": None, "value": {
|
|
66
|
+
"unlocked": 0,
|
|
67
|
+
"locked": 1}}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
read_measurement_dictionary = { # this dict translate the string for the operation identifier and contain the conversion factor
|
|
71
|
+
"high_flow": {"operation_identifier": 0, "conversion_factor": 10, "range": {"min": -3000, "max": 3000}},
|
|
72
|
+
"differential_pressure": {"operation_identifier": 3, "conversion_factor": 100, "range": {"min": -15000, "max": 15000}},
|
|
73
|
+
"pressure_high_flow": {"operation_identifier": 4, "conversion_factor": 100, "range": {"min": 0, "max": 15000}},
|
|
74
|
+
"volume_high_flow": {"operation_identifier": 6, "conversion_factor": 10, "range": {"min": 0, "max": 65536}},
|
|
75
|
+
"current_breath_phase": {"operation_identifier": 8, "conversion_factor": 1, "range": {"min": 0, "max": 1}, "value": {
|
|
76
|
+
"expiration": 0,
|
|
77
|
+
"inspiration": 1}},
|
|
78
|
+
"oxygen": {"operation_identifier": 9, "conversion_factor": 10, "range": {"min": 0, "max": 1100}},
|
|
79
|
+
"temperature": {"operation_identifier": 11, "conversion_factor": 10, "range": {"min": 0, "max": 1000}},
|
|
80
|
+
"high_pressure": {"operation_identifier": 13, "conversion_factor": 1, "range": {"min": 0, "max": 10000}},
|
|
81
|
+
"ambient_pressure": {"operation_identifier": 14, "conversion_factor": 1, "range": {"min": 0, "max": 1150}},
|
|
82
|
+
"inspiration_time": {"operation_identifier": 19, "conversion_factor": 100, "range": {"min": 20, "max": 6000}},
|
|
83
|
+
"expiration_time": {"operation_identifier": 20, "conversion_factor": 100, "range": {"min": 20, "max": 6000}},
|
|
84
|
+
"i_e_ratio": {"operation_identifier": 21, "conversion_factor": 10, "range": {"min": 1, "max": 3000}},
|
|
85
|
+
"breath_rate": {"operation_identifier": 22, "conversion_factor": 10, "range": {"min": 0, "max": 1500}},
|
|
86
|
+
"volume_inspiration": {"operation_identifier": 23, "conversion_factor": 1, "range": {"min": 0, "max": 10000}},
|
|
87
|
+
"volume_expiration": {"operation_identifier": 24, "conversion_factor": 1, "range": {"min": 0, "max": 10000}},
|
|
88
|
+
"volume_flow_inspiration": {"operation_identifier": 25, "conversion_factor": 10, "range": {"min": 0, "max": 3000}},
|
|
89
|
+
"volume_flow_expiration": {"operation_identifier": 26, "conversion_factor": 10, "range": {"min": 0, "max": 3000}},
|
|
90
|
+
"peak_pressure": {"operation_identifier": 27, "conversion_factor": 10, "range": {"min": 0, "max": 1500}},
|
|
91
|
+
"mean_pressure": {"operation_identifier": 28, "conversion_factor": 10, "range": {"min": 0, "max": 1500}},
|
|
92
|
+
"peep": {"operation_identifier": 29, "conversion_factor": 10, "range": {"min": 0, "max": 1500}},
|
|
93
|
+
"inspiratory_time_fraction": {"operation_identifier": 30, "conversion_factor": 10, "range": {"min": 0, "max": 1000}},
|
|
94
|
+
"peak_flow_inspiration": {"operation_identifier": 31, "conversion_factor": 10, "range": {"min": -3000, "max": 3000}},
|
|
95
|
+
"peak_flow_expiration": {"operation_identifier": 32, "conversion_factor": 10, "range": {"min": -3000, "max": 3000}},
|
|
96
|
+
"plateau_pressure": {"operation_identifier": 41, "conversion_factor": 10, "range": {"min": 0, "max": 1500}},
|
|
97
|
+
# ## the range is not possible
|
|
98
|
+
"compliance": {"operation_identifier": 42, "conversion_factor": 10, "range": {"min": 0, "max": 1000000}},
|
|
99
|
+
"ipap": {"operation_identifier": 43, "conversion_factor": 10, "range": {"min": 0, "max": 1500}}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
read_write_setting_dictionary = { # this dict translate the string for the operation identifier and contain the conversion factor
|
|
103
|
+
"gas_type": {"operation_identifier": 1, "conversion_factor": None, "value": {
|
|
104
|
+
"air": 0,
|
|
105
|
+
"air_o2_manually": 1,
|
|
106
|
+
"air_o2_automatically": 2,
|
|
107
|
+
"n2o_o2_manually": 3,
|
|
108
|
+
"n2o_o2_automatically": 4,
|
|
109
|
+
"heliox": 5,
|
|
110
|
+
"he_o2_manually": 6,
|
|
111
|
+
"he_o2_automatically": 7,
|
|
112
|
+
"n2": 8,
|
|
113
|
+
"co2": 9,
|
|
114
|
+
"custom": 10}},
|
|
115
|
+
"manual_oxygen_concentration": {"operation_identifier": 2, "conversion_factor": 1, "range": {"min": 21, "max": 100}, "value": True}, # Note: "value" = True mean do not exist a translation - The value must be taken directly
|
|
116
|
+
"gas_standard": {"operation_identifier": 3, "conversion_factor": None, "value": {
|
|
117
|
+
"atp": 0,
|
|
118
|
+
"stp": 1,
|
|
119
|
+
"btps": 2,
|
|
120
|
+
"btpd": 3,
|
|
121
|
+
"0/1013": 4,
|
|
122
|
+
"20/981": 5,
|
|
123
|
+
"15/1013": 6,
|
|
124
|
+
"20/1013": 7,
|
|
125
|
+
"25/991": 8,
|
|
126
|
+
"ap21": 9,
|
|
127
|
+
"stph": 10,
|
|
128
|
+
"atpd": 11,
|
|
129
|
+
"atps": 12,
|
|
130
|
+
"btps-a": 13,
|
|
131
|
+
"btpd-a": 14,
|
|
132
|
+
"ntpd": 15,
|
|
133
|
+
"ntps": 16}},
|
|
134
|
+
"vol_trigger_response_mode": {"operation_identifier": 4, "conversion_factor": None, "value": {
|
|
135
|
+
"adult": 0,
|
|
136
|
+
"pediatric": 1,
|
|
137
|
+
"high_frequency": 2,
|
|
138
|
+
"auto": 3}},
|
|
139
|
+
"vol_trigger_trigger_source": {"operation_identifier": 5, "conversion_factor": None, "value": {
|
|
140
|
+
"internal_high_flow_channel": 1,
|
|
141
|
+
"external_high_flow_channel": 3}},
|
|
142
|
+
"vol_trigger_start_trigger_signal": {"operation_identifier": 6, "conversion_factor": None, "value": {
|
|
143
|
+
"flow": 0,
|
|
144
|
+
"pressure_flow_channel": 1}},
|
|
145
|
+
"vol_trigger_start_trigger_edge": {"operation_identifier": 7, "conversion_factor": None, "value": {
|
|
146
|
+
"rising_edge": 0,
|
|
147
|
+
"falling_edge": 1}},
|
|
148
|
+
"vol_trigger_start_trigger_signal_value": {"operation_identifier": 8, "conversion_factor": 10, "range": {"min": -2500, "max": 2500}, "value": True}, # Note: "value" = True mean do not exist a translation - The value must be taken directly
|
|
149
|
+
"vol_trigger_end_trigger_signal": {"operation_identifier": 9, "conversion_factor": None, "value": {
|
|
150
|
+
"flow": 0,
|
|
151
|
+
"pressure": 1}},
|
|
152
|
+
"vol_trigger_end_trigger_edge": {"operation_identifier": 10, "conversion_factor": None, "value": {
|
|
153
|
+
"rising_edge": 0,
|
|
154
|
+
"falling_edge": 1}},
|
|
155
|
+
"vol_trigger_end_trigger_signal_value": {"operation_identifier": 11, "conversion_factor": 10, "range": {"min": -2500, "max": 2500}, "value": True}, # Note: "value" = True mean do not exist a translation - The value must be taken directly
|
|
156
|
+
"trigger_delay": {"operation_identifier": 12, "conversion_factor": 1, "range": {"min": 10, "max": 120}, "value": True},
|
|
157
|
+
"baseflow": {"operation_identifier": 13, "conversion_factor": None, "value": {
|
|
158
|
+
"disabled": 0,
|
|
159
|
+
"enabled": 1}},
|
|
160
|
+
"baseflow_volume_trigger": {"operation_identifier": 14, "conversion_factor": 10, "range": {"min": -3000, "max": 3000}, "value": True}, # Note: "value" = True mean do not exist a translation - The value must be taken directly
|
|
161
|
+
"filter_type": {"operation_identifier": 15, "conversion_factor": None, "value": {
|
|
162
|
+
"None": 0,
|
|
163
|
+
"filter_low": 1,
|
|
164
|
+
"filter_medium": 2,
|
|
165
|
+
"filter_high": 3}},
|
|
166
|
+
"start_trigger_delay": {"operation_identifier": 19, "conversion_factor": 1, "range": {"min": 10, "max": 120}, "value": True}, # Note: "value" = True mean do not exist a translation - The value must be taken directly
|
|
167
|
+
"end_trigger_delay": {"operation_identifier": 20, "conversion_factor": 1, "range": {"min": 10, "max": 120}, "value": True}, # Note: "value" = True mean do not exist a translation - The value must be taken directly
|
|
168
|
+
"gas_humidity": {"operation_identifier": 21, "conversion_factor": 1, "range": {"min": 0, "max": 100}, "value": True}, # Note: "value" = True mean do not exist a translation - The value must be taken directly
|
|
169
|
+
"respiratory_parameter_pressure_source": {"operation_identifier": 22, "conversion_factor": None, "value": {
|
|
170
|
+
"pressure_channel": 0,
|
|
171
|
+
"differential_pressure": 1,
|
|
172
|
+
"high_pressure": 2}},
|
|
173
|
+
"dynamic_pressure_compensation": {"operation_identifier": 23, "conversion_factor": None, "value": {
|
|
174
|
+
"disable": 0,
|
|
175
|
+
"enable": 1}},
|
|
176
|
+
"stream_first_value": {"operation_identifier": 64, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}}, # use the operation identifier from read_measurement_dictionary
|
|
177
|
+
"stream_second_value": {"operation_identifier": 65, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}}, # use the operation identifier from read_measurement_dictionary
|
|
178
|
+
"stream_third_value": {"operation_identifier": 66, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}}, # use the operation identifier from read_measurement_dictionary
|
|
179
|
+
"stream_fourth_value": {"operation_identifier": 160, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}},
|
|
180
|
+
"stream_fifth_value": {"operation_identifier": 161, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}},
|
|
181
|
+
"stream_sixth_value": {"operation_identifier": 162, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}},
|
|
182
|
+
"stream_seventh_value": {"operation_identifier": 163, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}},
|
|
183
|
+
"stream_eighth_value": {"operation_identifier": 164, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}},
|
|
184
|
+
"stream_ninth_value": {"operation_identifier": 165, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}},
|
|
185
|
+
"stream_tenth_value": {"operation_identifier": 166, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}},
|
|
186
|
+
"stream_eleventh_value": {"operation_identifier": 167, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}},
|
|
187
|
+
"stream_twelfth_value": {"operation_identifier": 168, "conversion_factor": None, "value": {key: value["operation_identifier"] for key, value in read_measurement_dictionary.items()}},
|
|
188
|
+
"usb_mass_storage": {"operation_identifier": 70, "conversion_factor": None, "value": {
|
|
189
|
+
"disabled": 0,
|
|
190
|
+
"enabled": 1}}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
read_system_information_dictionary = { # this dict translate the string for the operation identifier and contain the conversion factor
|
|
194
|
+
"hardware_version": {"operation_identifier": 1, "conversion_factor": None},
|
|
195
|
+
"software_major_version": {"operation_identifier": 2, "conversion_factor": None},
|
|
196
|
+
"software_minor_version": {"operation_identifier": 3, "conversion_factor": None},
|
|
197
|
+
"software_release": {"operation_identifier": 4, "conversion_factor": None},
|
|
198
|
+
"last_calibration_day": {"operation_identifier": 5, "conversion_factor": None},
|
|
199
|
+
"last_calibration_month": {"operation_identifier": 6, "conversion_factor": None},
|
|
200
|
+
"last_calibration_year": {"operation_identifier": 7, "conversion_factor": None},
|
|
201
|
+
"serial_number": {"operation_identifier": 8, "conversion_factor": None},
|
|
202
|
+
"next_calibration_day": {"operation_identifier": 9, "conversion_factor": None},
|
|
203
|
+
"next_calibration_month": {"operation_identifier": 10, "conversion_factor": None},
|
|
204
|
+
"next_calibration_year": {"operation_identifier": 11, "conversion_factor": None},
|
|
205
|
+
"device_type": {"operation_identifier": 12, "conversion_factor": None, "value": {
|
|
206
|
+
"invalid": 0,
|
|
207
|
+
"Citrex H4": 1,
|
|
208
|
+
"VT305": 2,
|
|
209
|
+
"Citrex H5": 3,
|
|
210
|
+
"Citrex H3": 4,
|
|
211
|
+
"PF-300 Pro": 5,
|
|
212
|
+
"Flowmeter F1": 6,
|
|
213
|
+
"Flowmeter F2": 7}}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
read_state_dictionary = { # this dict translate the string for the operation identifier and contain the conversion factor
|
|
217
|
+
"calibration_state": {"operation_identifier": 1, "conversion_factor": None, "range": {"min": 0, "max": 27}, "value": {
|
|
218
|
+
"Calibration state: idle": 0,
|
|
219
|
+
"Calibration state: error during calibration": 1,
|
|
220
|
+
"Oxygen calibration: waiting for 100pct oxygen": 2,
|
|
221
|
+
"Oxygen calibration: reading 100pct oxygen": 3,
|
|
222
|
+
"Oxygen calibration: waiting for 21pct oxygen": 4,
|
|
223
|
+
"Oxygen calibration: reading 21pct oxygen": 5,
|
|
224
|
+
"Oxygen calibration: finished, waiting for user acknowledge": 6,
|
|
225
|
+
"Oxygen calibration: finished": 7,
|
|
226
|
+
"Pressure/flow offset adjust: wait for user acknowledge": 8,
|
|
227
|
+
"Pressure/flow offset adjust: reading offset": 9,
|
|
228
|
+
"Pressure/flow offset adjust: finished, waiting for user acknowledge": 10,
|
|
229
|
+
"Pressure/flow offset adjust: finished": 11,
|
|
230
|
+
"Pressure sensor gain adjust: read offset, waiting for user acknowledge": 12,
|
|
231
|
+
"Pressure sensor gain adjust: reading offset": 13,
|
|
232
|
+
"Pressure sensor gain adjust: read high adjustment pressure 1, waiting for user acknowledge": 14,
|
|
233
|
+
"Pressure sensor gain adjust: read high adjustment pressure 1": 15,
|
|
234
|
+
"Pressure sensor gain adjust: read high adjustment pressure 2, waiting for user acknowledge": 16,
|
|
235
|
+
"Pressure sensor gain adjust: read high adjustment pressure 2": 17,
|
|
236
|
+
"Pressure sensor gain adjust: finished, waiting for user acknowledge": 18,
|
|
237
|
+
"Pressure sensor gain adjust: finished": 19,
|
|
238
|
+
"Flow calibration: next flow, waiting for user acknowledge": 20,
|
|
239
|
+
"Flow calibration: read next flow": 21,
|
|
240
|
+
"Flow calibration: finished, waiting for user acknowledge": 22,
|
|
241
|
+
"Flow calibration: finished": 23,
|
|
242
|
+
"Drift compensation: started": 24,
|
|
243
|
+
"Drift compensation: read reference": 25,
|
|
244
|
+
"Drift compensation: waiting for next temperature": 26,
|
|
245
|
+
"Drift compensation: read temperature and offset": 27}}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class DeviceH5:
|
|
250
|
+
def __init__(self, port: str | None = None, baudrate: int | None = None):
|
|
251
|
+
"""
|
|
252
|
+
This method is used to initialize the class.
|
|
253
|
+
"""
|
|
254
|
+
self.serial_interface = SerialInterface()
|
|
255
|
+
self.io_validator = IOValidator()
|
|
256
|
+
self.port = port # Set the argument for port – Note: if it is not called with a with block, it will be None
|
|
257
|
+
self.baudrate = baudrate # set the argument for baud rate
|
|
258
|
+
self.serial_object = None # set the serial object
|
|
259
|
+
|
|
260
|
+
def __enter__(self):
|
|
261
|
+
"""
|
|
262
|
+
Context manager entry method.
|
|
263
|
+
|
|
264
|
+
This method is automatically called when entering a `with` block.
|
|
265
|
+
The serial interface is opened automatically
|
|
266
|
+
|
|
267
|
+
Return:
|
|
268
|
+
self (object): The current instance of the class
|
|
269
|
+
"""
|
|
270
|
+
self.open_serial_interface(self.port, self.baudrate) # open the serial object
|
|
271
|
+
return self # return
|
|
272
|
+
|
|
273
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
274
|
+
"""
|
|
275
|
+
Context manager exit method.
|
|
276
|
+
|
|
277
|
+
This method is automatically called when leaving a `with` block.
|
|
278
|
+
It ensures that the serial interface is properly closed, even if
|
|
279
|
+
an exception occurs during execution inside the block.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
exc_type (type): The exception type if an error occurred, else None
|
|
283
|
+
exc_val (Exception): The exception instance if an error occurred, else None
|
|
284
|
+
exc_tb (traceback): The traceback object if an error occurred, else None
|
|
285
|
+
"""
|
|
286
|
+
self.close_serial_interface() # close the serial port
|
|
287
|
+
|
|
288
|
+
def open_serial_interface(self, port: str, baudrate: int) -> serial.Serial:
|
|
289
|
+
"""
|
|
290
|
+
This method is used to open a serial interface.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
port (str): The port where serial communication is to be established
|
|
294
|
+
baudrate (int): The baudrate for the serial communication
|
|
295
|
+
|
|
296
|
+
Return:
|
|
297
|
+
serial_object (serial.Serial): The serial object of the session
|
|
298
|
+
"""
|
|
299
|
+
allowed = [spec["baudrate"] for spec in stream_protocol_specs.values()]
|
|
300
|
+
if baudrate not in allowed:
|
|
301
|
+
raise ValueError(f"Invalid baudrate '{baudrate}'. Allowed baudrates are {allowed}")
|
|
302
|
+
serial_object, e = self.serial_interface.open_port(port=port, baudrate=baudrate) # call method for open serial port
|
|
303
|
+
if serial_object is None: # if serial Object is None
|
|
304
|
+
raise ConnectionError(f"Error while open the serial port: {e}") # raise a connection error
|
|
305
|
+
self.serial_interface._get_echo_state(serial_object=serial_object) # call the method for read the echo mode state and save as attribute of serial object
|
|
306
|
+
self.serial_object = serial_object
|
|
307
|
+
return serial_object # return the serial object
|
|
308
|
+
|
|
309
|
+
def close_serial_interface(self, serial_object: serial.Serial | None = None) -> None:
|
|
310
|
+
"""
|
|
311
|
+
This method closed the serial object.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
315
|
+
"""
|
|
316
|
+
serial_object = serial_object or self.serial_object
|
|
317
|
+
self.serial_interface.close_port(serial_object=serial_object) # call the method for close the serial port
|
|
318
|
+
|
|
319
|
+
def execute_command(self, operation_name: str, value: str | int | None = None, serial_object: serial.Serial | None = None) -> str | None: # CM
|
|
320
|
+
"""
|
|
321
|
+
This method is used to execute command(CM).
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
operation_name (str): The operation name of the command
|
|
325
|
+
value (str | int | None): The value for the setting
|
|
326
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
327
|
+
|
|
328
|
+
Return:
|
|
329
|
+
value (str | None): The return value of the response
|
|
330
|
+
"""
|
|
331
|
+
serial_object = serial_object or self.serial_object
|
|
332
|
+
result = self._handle_communication(operation="CM", operation_name=operation_name, value=value, dictionary=execute_command_dictionary, serial_object=serial_object) # call the method
|
|
333
|
+
if operation_name == "switch_echo":
|
|
334
|
+
serial_object.echo_state = True if value == "on" else False
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
def write_setting(self, operation_name: str, value: str | int | None = None, serial_object: serial.Serial | None = None) -> str: # WS
|
|
338
|
+
"""
|
|
339
|
+
This method is used to write setting (WS).
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
operation_name (str): The operation name of the command
|
|
343
|
+
value (str | int | None): The value for the setting
|
|
344
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
345
|
+
|
|
346
|
+
Return:
|
|
347
|
+
value (str): The return value of the response
|
|
348
|
+
"""
|
|
349
|
+
serial_object = serial_object or self.serial_object
|
|
350
|
+
return self._handle_communication(operation="WS", operation_name=operation_name, value=value, dictionary=read_write_setting_dictionary, serial_object=serial_object) # call the method
|
|
351
|
+
|
|
352
|
+
def read_setting(self, operation_name: str, serial_object: serial.Serial | None = None) -> str: # RS
|
|
353
|
+
"""
|
|
354
|
+
This method is used to read setting (RS).
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
operation_name (str): The operation name of the command
|
|
358
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
359
|
+
|
|
360
|
+
Return:
|
|
361
|
+
value (str): The return value of the response
|
|
362
|
+
"""
|
|
363
|
+
serial_object = serial_object or self.serial_object
|
|
364
|
+
return self._handle_communication(operation="RS", operation_name=operation_name, value=None, dictionary=read_write_setting_dictionary, serial_object=serial_object) # call the method
|
|
365
|
+
|
|
366
|
+
def read_measurement(self, operation_name: str, serial_object: serial.Serial | None = None) -> str: # RM
|
|
367
|
+
"""
|
|
368
|
+
This method is used to read measurement (RM).
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
operation_name (str): The operation name of the command
|
|
372
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
373
|
+
|
|
374
|
+
Return:
|
|
375
|
+
value (str): The return value of the response
|
|
376
|
+
"""
|
|
377
|
+
serial_object = serial_object or self.serial_object
|
|
378
|
+
return self._handle_communication(operation="RM", operation_name=operation_name, value=None, dictionary=read_measurement_dictionary, serial_object=serial_object) # call the method
|
|
379
|
+
|
|
380
|
+
def read_system_information(self, operation_name: str, serial_object: serial.Serial | None = None) -> str: # RI
|
|
381
|
+
"""
|
|
382
|
+
This method is used to read system information (RI).
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
operation_name (str): The operation name of the command
|
|
386
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
387
|
+
|
|
388
|
+
Return:
|
|
389
|
+
value (str): The return value of the response
|
|
390
|
+
"""
|
|
391
|
+
serial_object = serial_object or self.serial_object
|
|
392
|
+
return self._handle_communication(operation="RI", operation_name=operation_name, value=None, dictionary=read_system_information_dictionary, serial_object=serial_object) # call the method
|
|
393
|
+
|
|
394
|
+
def read_state(self, operation_name: str, serial_object: serial.Serial | None = None) -> str: # ST
|
|
395
|
+
"""
|
|
396
|
+
This method is used to read state (ST).
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
operation_name (str): The operation name of the command
|
|
400
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
401
|
+
|
|
402
|
+
Return:
|
|
403
|
+
value (str): The return value of the response
|
|
404
|
+
"""
|
|
405
|
+
serial_object = serial_object or self.serial_object
|
|
406
|
+
return self._handle_communication(operation="ST", operation_name=operation_name, value=None, dictionary=read_state_dictionary, serial_object=serial_object) # call the method
|
|
407
|
+
|
|
408
|
+
def read_multiple_measurements(self, operation_name: str, sample_count: int | str | float, sample_rate: int | str | float, serial_object: serial.Serial | None = None) -> list: # RM multiple
|
|
409
|
+
"""
|
|
410
|
+
This method is used to read measurements (RM).
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
operation_name (str): The operation name of the command
|
|
414
|
+
sample_count (int | str | float): The number of measured values
|
|
415
|
+
sample_rate (int | str | float): The sample rate to use in Hz (min: 0.01 | max: 20)
|
|
416
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
417
|
+
|
|
418
|
+
Return:
|
|
419
|
+
value (list): The return values of the response as list
|
|
420
|
+
"""
|
|
421
|
+
serial_object = serial_object or self.serial_object
|
|
422
|
+
sample_count = self.io_validator.validate_and_cast(value=sample_count, target_types=int, allowed_types=(str, float, int), min_value=1, max_value=999999) # call the method for validate and cast the sample count
|
|
423
|
+
sample_rate = self.io_validator.validate_and_cast(value=sample_rate, target_types=float, allowed_types=(str, float, int), min_value=0.01, max_value=20) # call the method for validate and cast the sample rate
|
|
424
|
+
value_list = [] # initialize an empty list
|
|
425
|
+
wait_time = 0 if (result := 1 / float(sample_rate) - 0.05) < 0 else result # if the sample rate is less than 50 ms, use 0 else result - Note: each command with a response takes an average of 50 ms
|
|
426
|
+
for i in range(sample_count): # start for loop
|
|
427
|
+
value = self.read_measurement(operation_name=operation_name, serial_object=serial_object) # read the measurement
|
|
428
|
+
value_list.append(value) # append the value to list
|
|
429
|
+
if i < sample_count - 1: # if no is the last iteration
|
|
430
|
+
time.sleep(wait_time) # wait
|
|
431
|
+
return value_list # return the list
|
|
432
|
+
|
|
433
|
+
def start_stream(self, first_value: str, second_value: str, third_value: str, data_queue, stop_queue, streaming_duration: int | float | str | None, serial_object: serial.Serial | None = None) -> None:
|
|
434
|
+
"""
|
|
435
|
+
This method is used to start the stream.\n
|
|
436
|
+
Note: Please read the imt protocol stream documentation before using this feature.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
first_value (str): The first value of stream
|
|
440
|
+
second_value (str): The second value of stream
|
|
441
|
+
third_value (str): The third value of stream
|
|
442
|
+
data_queue (queue): The data queue where the measurement data is saved
|
|
443
|
+
stop_queue (queue): The stop queue that handles cancellation and termination signals
|
|
444
|
+
streaming_duration (int | float | str | None): The maximum duration of the stream in seconds
|
|
445
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
446
|
+
"""
|
|
447
|
+
serial_object = serial_object or self.serial_object
|
|
448
|
+
baudrate = stream_protocol_specs["stream"]["baudrate"]
|
|
449
|
+
msg_length = stream_protocol_specs["stream"]["msg length"]
|
|
450
|
+
if serial_object.baudrate != baudrate: # check that the correct baudrate is use
|
|
451
|
+
raise ValueError(f"Invalid baudrate '{serial_object.baudrate}'. " f"This method requires {baudrate} for stream.")
|
|
452
|
+
streaming_duration = self.io_validator.validate_and_cast(value=streaming_duration, target_types=float, allowed_types=(str, float, int, type(None)), min_value=1, max_value=999999) # call the method to validate and cast the streaming duration
|
|
453
|
+
self._set_stream_pre_configuration(serial_object, first_value, second_value, third_value) # call method for set stream configuration
|
|
454
|
+
self.execute_command(operation_name="start_stream", value=None, serial_object=serial_object) # call the method to start the stream
|
|
455
|
+
self._handle_read_stream(serial_object, data_queue, stop_queue, streaming_duration, msg_length, first_value, second_value, third_value) # call the method for handle the stream
|
|
456
|
+
self.execute_command(operation_name="stop_stream", value=None, serial_object=serial_object) # call the method to stop the stream
|
|
457
|
+
self.execute_command(operation_name="screen", value="unlocked", serial_object=serial_object) # call method for unlock the screen
|
|
458
|
+
|
|
459
|
+
def start_fast_stream(self, first_value: str, second_value: str, third_value: str,
|
|
460
|
+
fourth_value: str, fifth_value: str, sixth_value: str,
|
|
461
|
+
seventh_value: str, eighth_value: str, ninth_value: str,
|
|
462
|
+
tenth_value: str, eleventh_value: str, twelfth_value: str,
|
|
463
|
+
data_queue, stop_queue, streaming_duration: int | float | str | None, serial_object: serial.Serial | None = None) -> None:
|
|
464
|
+
"""
|
|
465
|
+
This method is used to start the fast stream.\n
|
|
466
|
+
Note: Please read the imt fast protocol stream documentation before using this feature.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
first_value (str): The first value of fast stream
|
|
470
|
+
second_value (str): The second value of fast stream
|
|
471
|
+
third_value (str): The third value of fast stream
|
|
472
|
+
fourth_value (str): The fourth value of the fast stream
|
|
473
|
+
fifth_value (str): The fifth value of the fast stream
|
|
474
|
+
sixth_value (str):The sixth value of the fast stream
|
|
475
|
+
seventh_value (str): The seventh value of the fast stream
|
|
476
|
+
eighth_value (str): The eighth value of the fast stream
|
|
477
|
+
ninth_value (str): The ninth value of the fast stream
|
|
478
|
+
tenth_value (str): The tenth value of the fast stream
|
|
479
|
+
eleventh_value (str): The eleventh value of the fast stream
|
|
480
|
+
twelfth_value (str): The twelfth value of the fast stream
|
|
481
|
+
data_queue (queue): The data queue where the measurement data is saved
|
|
482
|
+
stop_queue (queue): The stop queue that handles cancellation and termination signals
|
|
483
|
+
streaming_duration (int | float | str | None): The maximum duration of the fast stream in seconds
|
|
484
|
+
serial_object (serial.Serial | None): Optional - uses the instances serial object if omitted.
|
|
485
|
+
"""
|
|
486
|
+
serial_object = serial_object or self.serial_object
|
|
487
|
+
baudrate = stream_protocol_specs["fast stream"]["baudrate"]
|
|
488
|
+
msg_length = stream_protocol_specs["fast stream"]["msg length"]
|
|
489
|
+
if serial_object.baudrate != stream_protocol_specs["fast stream"]["baudrate"]: # check that the correct baudrate is use - Note: only 115200 else raise an error
|
|
490
|
+
raise ValueError(f"Invalid baudrate '{serial_object.baudrate}'. " f"This method requires {baudrate} for fast stream.")
|
|
491
|
+
streaming_duration = self.io_validator.validate_and_cast(value=streaming_duration, target_types=float, allowed_types=(str, float, int, type(None)), min_value=1, max_value=999999) # call the method to validate and cast the streaming duration
|
|
492
|
+
self._set_stream_pre_configuration(serial_object, first_value, second_value, third_value, fourth_value, fifth_value, sixth_value, seventh_value, eighth_value, ninth_value, tenth_value, eleventh_value, twelfth_value) # call method for set fast stream configuration
|
|
493
|
+
self.execute_command(operation_name="start_stream", value=None, serial_object=serial_object) # call the method to start the fast stream
|
|
494
|
+
self._handle_read_stream(serial_object, data_queue, stop_queue, streaming_duration, msg_length, first_value, second_value, third_value, fourth_value, fifth_value, sixth_value, seventh_value, eighth_value, ninth_value, tenth_value, eleventh_value, twelfth_value) # call the method for handle the fast stream
|
|
495
|
+
self.execute_command(operation_name="stop_stream", value=None, serial_object=serial_object) # call the method to stop the fast stream
|
|
496
|
+
self.execute_command(operation_name="screen", value="unlocked", serial_object=serial_object) # call method for unlock the screen
|
|
497
|
+
|
|
498
|
+
def _handle_communication(self, operation: str, operation_name: str, value: str | int | None, dictionary: dict, serial_object: serial.Serial) -> str | None:
|
|
499
|
+
"""
|
|
500
|
+
This method is used to handle the communication with the device.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
operation (str): The execute operation - RM/CM/RS...
|
|
504
|
+
operation_name (str): The operation name of the command
|
|
505
|
+
value (str | int | None): The value to be setting
|
|
506
|
+
dictionary (dict): The dictionary to use for the command
|
|
507
|
+
serial_object (serial.Serial): The serial interface for communication
|
|
508
|
+
|
|
509
|
+
Return:
|
|
510
|
+
value (str | None): The return value of the response
|
|
511
|
+
"""
|
|
512
|
+
operation_name = operation_name.lower().replace(" ", "") # all characters lower case and remove spaces
|
|
513
|
+
operation_identifier = self.io_validator.check_operation_name(dictionary=dictionary, operation_name=operation_name) # call the method to verify the operation_name and get the operation identifier
|
|
514
|
+
conversion_factor = dictionary.get(operation_name, None).get("conversion_factor") # get the conversion factor
|
|
515
|
+
if value is not None: # if the value is not none
|
|
516
|
+
value = str(value).lower().replace(" ", "") # delete spaces
|
|
517
|
+
if conversion_factor is not None: # if the conversion fac tor is not none
|
|
518
|
+
value = int(float(value) * conversion_factor) # calculate the needed value for the device
|
|
519
|
+
searched_value = self.io_validator.check_values_argument(dictionary=dictionary, operation=operation, operation_name=operation_name, value=value) # call the method to verify the value
|
|
520
|
+
if serial_object is None: # if the serial object is None (not valid)
|
|
521
|
+
raise ConnectionError("The serial object is not valid")
|
|
522
|
+
for _ in range(5): # start for loop
|
|
523
|
+
value, error_occurred = self._execute_communication(operation=operation, operation_identifier=operation_identifier, value=searched_value, serial_object=serial_object) # call the method to execute the communication
|
|
524
|
+
if error_occurred: # if an error occurred
|
|
525
|
+
continue # continue the loop
|
|
526
|
+
if operation == "WS": # if the operation is WS - command value and response value must be the same, otherwise something has gone wrong
|
|
527
|
+
if str(searched_value) != str(value): # if is not same
|
|
528
|
+
time.sleep(2) # wait 2 seconds
|
|
529
|
+
continue # continue the loop
|
|
530
|
+
if (dict_range := execute_command_dictionary.get(operation_name, {}).get("range")) is not None: # if range exist
|
|
531
|
+
if not (dict_range.get("min") <= int(value) <= dict_range.get("max")): # if the value is in range
|
|
532
|
+
time.sleep(2) # wait 2 seconds
|
|
533
|
+
continue # continue the loop
|
|
534
|
+
if dictionary.get(operation_name, {}).get("value") not in (None, True) and value != "": # if a translation for the value exist
|
|
535
|
+
value = next((k for k, v in dictionary.get(operation_name, {}).get("value", {}).items() if str(v) == value), None) # translate the response value
|
|
536
|
+
if value is None:
|
|
537
|
+
continue # continue the loop
|
|
538
|
+
break # break the loop no error
|
|
539
|
+
else: # only is called if the for loop runs without a break command
|
|
540
|
+
raise ValueError("Invalid device response") # raise an error
|
|
541
|
+
if (conversion_factor := dictionary.get(operation_name, {}).get("conversion_factor")) is not None: # if conversion factor exist
|
|
542
|
+
value = str(float(value) / float(conversion_factor)) # convert the value
|
|
543
|
+
return value # return the value
|
|
544
|
+
|
|
545
|
+
def _execute_communication(self, operation: str, operation_identifier: str, value: str | int | None, serial_object: serial.Serial) -> tuple[str | None, bool]:
|
|
546
|
+
"""
|
|
547
|
+
This method is used to handle the communication with the device.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
operation (str): The execute operation - RM/CM/RS...
|
|
551
|
+
operation_identifier (str): The operation name of the command
|
|
552
|
+
value (str | int | None): The value to be setting
|
|
553
|
+
serial_object (serial.Serial): The serial interface for communication
|
|
554
|
+
|
|
555
|
+
Return:
|
|
556
|
+
value (str | None): The return value of the response
|
|
557
|
+
error (bool): The error
|
|
558
|
+
"""
|
|
559
|
+
command = self.io_validator.build_command(operation=operation, operation_identifier=operation_identifier, value=value) # call the method for build command
|
|
560
|
+
# print(f"sent: {command}")
|
|
561
|
+
self.serial_interface.write_command(command=command, serial_object=serial_object) # call the method for write the command
|
|
562
|
+
chunk = self.serial_interface.read_command(length=1, serial_object=serial_object) # call the method for read the command
|
|
563
|
+
if chunk == b'': # if the chunk is empty
|
|
564
|
+
return "", True # bool mean error occurred - A timeout often occurs then.
|
|
565
|
+
response_dict = self.io_validator.build_message_structure(operation, operation_identifier, value, has_value_key="response_has_value") # call the method for build the structure of response
|
|
566
|
+
response, error_occurred = self.serial_interface.read_packet(chunk=chunk, serial_object=serial_object, response_dict=response_dict) # call the method for read the msg
|
|
567
|
+
# print(f"received: {response}")
|
|
568
|
+
if error_occurred: # if an error occurred
|
|
569
|
+
return response, True # bool mean error occurred
|
|
570
|
+
value, error_occurred = self.io_validator.check_response(response, response_dict) # call the method for check the response
|
|
571
|
+
if error_occurred:
|
|
572
|
+
return value, True # return the value and True for msg no valid
|
|
573
|
+
return value, False # return the value and false for no error
|
|
574
|
+
|
|
575
|
+
def _set_stream_pre_configuration(self, serial_object: serial.Serial, *values: str) -> None:
|
|
576
|
+
"""
|
|
577
|
+
This method is used to set the configuration of the stream.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
serial_object (serial.Serial): The serial interface for communication
|
|
581
|
+
*values (str): All values to be measured
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
execute_command_list = [
|
|
585
|
+
("screen", "locked") # only for citrex - Note: The temporal resolution can only be ensured when the screen is locked
|
|
586
|
+
]
|
|
587
|
+
for operation_name, value in execute_command_list: # get all commands
|
|
588
|
+
self.execute_command(operation_name=operation_name, value=value, serial_object=serial_object) # call method to execute the command
|
|
589
|
+
|
|
590
|
+
names = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth"]
|
|
591
|
+
write_setting_list = [
|
|
592
|
+
(f"stream_{names[i]}_value", values[i]) for i in range(len(values))
|
|
593
|
+
] # create the list for set all values
|
|
594
|
+
for operation_name, value in write_setting_list: # get all commands
|
|
595
|
+
self.write_setting(operation_name=operation_name, value=value, serial_object=serial_object) # call method to execute the command
|
|
596
|
+
|
|
597
|
+
def _handle_read_stream(self, serial_object: serial.Serial, data_queue: list, stop_queue: bool, streaming_duration: int | float | None, msg_length: int, *values: str) -> None:
|
|
598
|
+
"""
|
|
599
|
+
This method is used to handle the communication with the device.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
serial_object (serial.Serial): The serial interface for communication
|
|
603
|
+
data_queue (queue): The data queue where the measurement data is saved
|
|
604
|
+
stop_queue (queue): The stop queue that handles cancellation and termination signals
|
|
605
|
+
streaming_duration (int | float | str | None): The maximum duration of the stream in seconds
|
|
606
|
+
msg_length (int): The length of the streaming message
|
|
607
|
+
*value (str): The values which are inside in the stream message
|
|
608
|
+
"""
|
|
609
|
+
last_raw_timestamp, timestamp_rollover_count = None, 0 # initialize the variables
|
|
610
|
+
streaming_duration = float('inf') if streaming_duration is None else streaming_duration # if streaming duration is None set infinity else use the values of streaming duration
|
|
611
|
+
conversion_factors = [read_measurement_dictionary.get(value, {}).get("conversion_factor") for value in values] # get all conversion factor
|
|
612
|
+
ranges = [(float(read_measurement_dictionary.get(value, {}).get("range").get("min")), float(read_measurement_dictionary.get(value, {}).get("range").get("max"))) for value in values] # get the limit of the values min and max
|
|
613
|
+
while stop_queue.empty(): # while stop_queue is empty
|
|
614
|
+
response = self.serial_interface.read_streaming_msg(serial_object=serial_object, msg_length=msg_length) # call method for read the msg x byte of stream
|
|
615
|
+
if response is True: # if response is True the device is without reaction
|
|
616
|
+
stop_queue.put((True)) # write true to stop_queue
|
|
617
|
+
return # return
|
|
618
|
+
timestamp, last_raw_timestamp, timestamp_rollover_count = self.io_validator.calculate_real_timestamp(response=response, last_raw_timestamp=last_raw_timestamp, timestamp_rollover_count=timestamp_rollover_count) # calculate the real time step with overflow with 16bit
|
|
619
|
+
error_occurred = self.io_validator.check_range(response, msg_length, ranges) # call the method for check the range if all values
|
|
620
|
+
if error_occurred: # if error occurred
|
|
621
|
+
continue
|
|
622
|
+
measured_values = self.io_validator.extract_measured_values(response, msg_length, conversion_factors) # get measured values
|
|
623
|
+
data_queue.put((timestamp, *measured_values)) # write the data to data_queue
|
|
624
|
+
if timestamp >= streaming_duration: # when the maximum streaming duration has expired
|
|
625
|
+
stop_queue.put((True)) # write True to stop_queue
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class SerialInterface:
|
|
629
|
+
@staticmethod
|
|
630
|
+
def open_port(port: str, baudrate: int) -> serial.Serial:
|
|
631
|
+
"""
|
|
632
|
+
This method is used to open serial ports.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
port (str): The target port
|
|
636
|
+
baudrate (int): The target baudrate
|
|
637
|
+
|
|
638
|
+
Return:
|
|
639
|
+
serial_port (serial.Serial): The session for this serial communication
|
|
640
|
+
"""
|
|
641
|
+
for _ in range(3): # start for loop
|
|
642
|
+
try: # try
|
|
643
|
+
serial_port = serial.Serial(port, baudrate, timeout=11) # open serial port with 11 seconds because the device need 10 second plus transport
|
|
644
|
+
return serial_port, None # return serial_port, error
|
|
645
|
+
except serial.SerialException as e: # except
|
|
646
|
+
last_exception = e # save the error
|
|
647
|
+
time.sleep(2) # retry after 2 seconds - but max 3 times
|
|
648
|
+
return None, last_exception # return None, error exception - that means error occurred
|
|
649
|
+
|
|
650
|
+
@staticmethod
|
|
651
|
+
def close_port(serial_object: serial.Serial) -> None:
|
|
652
|
+
"""
|
|
653
|
+
This static method closed the serial object.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
serial_object (serial.Serial): The serial object to be closed
|
|
657
|
+
"""
|
|
658
|
+
try:
|
|
659
|
+
serial_object.close() # close the serial port
|
|
660
|
+
except Exception:
|
|
661
|
+
pass
|
|
662
|
+
|
|
663
|
+
@staticmethod
|
|
664
|
+
def write_command(command: bytes, serial_object: serial.Serial) -> None:
|
|
665
|
+
"""
|
|
666
|
+
This static method write a command to serial object.
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
command (bytes): The command to be written
|
|
670
|
+
serial_object (serial.Serial): The serial object to be closed
|
|
671
|
+
"""
|
|
672
|
+
serial_object.write(command) # write the command
|
|
673
|
+
|
|
674
|
+
def read_command(self, length: int, serial_object: serial.Serial) -> bytes:
|
|
675
|
+
"""
|
|
676
|
+
This method read a response from serial interface.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
length (int): The length of read response
|
|
680
|
+
serial_object (serial.Serial): The serial object where is read
|
|
681
|
+
|
|
682
|
+
Return:
|
|
683
|
+
response (bytes): The read response
|
|
684
|
+
"""
|
|
685
|
+
return serial_object.read(length) # return the response
|
|
686
|
+
|
|
687
|
+
def read_packet(self, chunk: bytes, serial_object: serial.Serial, response_dict: dict) -> tuple[bytes, bool]:
|
|
688
|
+
"""
|
|
689
|
+
This method read a response from serial interface.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
chunk (bytes): The chunk of bytes msg
|
|
693
|
+
serial_object (serial.Serial): The serial object where is read
|
|
694
|
+
response_dict: The dict for get the end_token
|
|
695
|
+
|
|
696
|
+
Return:
|
|
697
|
+
message (bytes): The read response
|
|
698
|
+
error (bool): The error as bool
|
|
699
|
+
"""
|
|
700
|
+
start_token = response_dict["start_token"][0].encode().decode('unicode-escape').encode('utf-8') # convert to bytes (b'%')
|
|
701
|
+
separator_token = response_dict["separator_token"][0].encode().decode('unicode-escape').encode('utf-8') # convert to bytes (b'#')
|
|
702
|
+
end_token = response_dict["end_token"][0].encode().decode('unicode-escape').encode('utf-8') # convert to bytes (b'\r')
|
|
703
|
+
pattern = re.compile(rb'.*' + start_token + rb'(.{1,3})' + separator_token + rb'(.{0,18})' + end_token) # build pattern b'{not important} % {1-3bytes} # {0-18 bytes} \r {not important}'
|
|
704
|
+
message = chunk # set the first chunk as message
|
|
705
|
+
echo_message = None
|
|
706
|
+
while True: # run until the flag is false - only for term character
|
|
707
|
+
chunk = self.read_command(1, serial_object) # read the first byte
|
|
708
|
+
message += chunk # add the byte to message
|
|
709
|
+
if b'' == chunk: # if the chunk is empty
|
|
710
|
+
return message, True # mean timeout occurred
|
|
711
|
+
if chunk == b'?': # if the chunk is ?
|
|
712
|
+
original_timeout = serial_object.timeout # get the original _timeout
|
|
713
|
+
serial_object.timeout = 1 # set timeout to 1 sec
|
|
714
|
+
next_chunk = self.read_command(1, serial_object) # read the next chunk
|
|
715
|
+
serial_object.timeout = original_timeout # set timeout to original
|
|
716
|
+
if not next_chunk: # if next chunk not exist
|
|
717
|
+
return message, True # the response is this error
|
|
718
|
+
message += next_chunk # add next chunk to message
|
|
719
|
+
if pattern.search(message):
|
|
720
|
+
message.replace(b'\x00', b'') # replace all nul in the msg with nothing - the device send some times 0x00 (nul)
|
|
721
|
+
if serial_object.echo_state and echo_message is None:
|
|
722
|
+
echo_message = message # write message to echo_messages
|
|
723
|
+
message = b'' # clear the message
|
|
724
|
+
# print(f"echo: {echo_message}") # echo_message
|
|
725
|
+
continue
|
|
726
|
+
return message, False # return the complete message incl. term_character as bytes - bool meaning no error occurred
|
|
727
|
+
|
|
728
|
+
@staticmethod
|
|
729
|
+
def read_streaming_msg(serial_object: serial.Serial, msg_length: int) -> bytes | bool:
|
|
730
|
+
"""
|
|
731
|
+
This method read a nine byte response from serial interface.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
serial_object (serial.Serial): The serial object where is read
|
|
735
|
+
msg_length (int): The length of the message
|
|
736
|
+
|
|
737
|
+
Return:
|
|
738
|
+
message (bytes | bool): The read response or True if not enough bytes were read
|
|
739
|
+
"""
|
|
740
|
+
response = serial_object.read(msg_length) # read x byte - note: every complete msg has x bytes
|
|
741
|
+
if len(response) < msg_length: # if less than x bytes were read
|
|
742
|
+
return True # return true for time out und less than x bytes
|
|
743
|
+
while sum(response) % 256 != 0: # while the chk sum is not equal to 0 - The message is invalid - use a shift register to shift one byte further. The division may simply have been shifted.
|
|
744
|
+
response = response[1:] + serial_object.read(1) # shift 1 byte
|
|
745
|
+
return response # return the msg ist valid
|
|
746
|
+
|
|
747
|
+
def _get_echo_state(self, serial_object):
|
|
748
|
+
for _ in range(5):
|
|
749
|
+
self.write_command(b'%RS#1\r', serial_object)
|
|
750
|
+
# print(f'echo test - sent: {b'%RS#1\r'}')
|
|
751
|
+
chunk = self.read_command(1, serial_object)
|
|
752
|
+
if chunk == b'?': # if the first chunk is ? mean error
|
|
753
|
+
continue # continue
|
|
754
|
+
response = chunk + serial_object.read_until(b"\r") # read until '\r' plus the chunk
|
|
755
|
+
echo = b'$' not in response # b'$' not in response? - that mean we have an echo
|
|
756
|
+
serial_object.echo_state = echo # set the echo state
|
|
757
|
+
if echo:
|
|
758
|
+
# print(f'echo test - echo: {response}')
|
|
759
|
+
response = serial_object.read_until(b"\r")
|
|
760
|
+
break
|
|
761
|
+
# print(f'echo test - received: {response}')
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class IOValidator:
|
|
765
|
+
def build_command(self, operation: str, operation_identifier: str, value: str | int | None) -> bytes:
|
|
766
|
+
"""
|
|
767
|
+
This method is used to build the command.
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
operation (str): The operation to be performed example CM/RM....
|
|
771
|
+
operation_identifier (str): The operation identifier for the command
|
|
772
|
+
value (str | int | None): The value for the command if exist
|
|
773
|
+
|
|
774
|
+
Return:
|
|
775
|
+
command_as_byte (bytes): The built command
|
|
776
|
+
"""
|
|
777
|
+
command_dict = self.build_message_structure(operation, operation_identifier, value, has_value_key="command_has_value") # call the method for build the message structure
|
|
778
|
+
command_as_byte = (''.join(str(value[0]) for value in command_dict.values() if value[0] is not None)).encode('utf-8') # convert to command as bytes - get all entries of dict
|
|
779
|
+
return command_as_byte # return the command as bytes
|
|
780
|
+
|
|
781
|
+
def build_message_structure(self, operation: str, operation_identifier: str, value: str | int | None, has_value_key: str) -> dict:
|
|
782
|
+
"""
|
|
783
|
+
This method is used to build the structure of msg.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
operation (str): The operation to be performed example CM/RM....
|
|
787
|
+
operation_identifier (str): The operation identifier for the command
|
|
788
|
+
value (str | int | None): The value for the command if exist
|
|
789
|
+
has_value_key (str): only command_has_value and response_has_value
|
|
790
|
+
|
|
791
|
+
Return:
|
|
792
|
+
command_as_dict (dict): The built command as dict
|
|
793
|
+
"""
|
|
794
|
+
update_core = {"operation": operation, "operation_identifier": operation_identifier} # create the update dictionary for core structure
|
|
795
|
+
core = self._update_dict(copy.deepcopy(message_core_structure), update_core) # call the method for update the dict as core
|
|
796
|
+
terminator = copy.deepcopy(message_terminator) # get the edn token as deep copy dict
|
|
797
|
+
has_value = message_value_mapping[operation][has_value_key] # check if the message has a value - possible True/False and None that mean is variable
|
|
798
|
+
if has_value or has_value is None: # if (has_value is True) or (has_value is None and value is not None) or (has_value is None and has_value_key is True)
|
|
799
|
+
if has_value_key == "response_has_value" or value is not None: # if the out put dict ist for the response
|
|
800
|
+
update_value = {"value": has_value if has_value_key == "response_has_value" else value} # create update dict and if has_value_key is response use None else the value
|
|
801
|
+
value_optional = self._update_dict(copy.deepcopy(message_value_optional), update_value) # get the optional value as dict
|
|
802
|
+
return {**core, **value_optional, **terminator} # merge the dict
|
|
803
|
+
return {**core, **terminator} # merge the dict without value because no has value
|
|
804
|
+
|
|
805
|
+
def check_response(self, response: bytes, response_dict: dict) -> tuple[str | None, bool]:
|
|
806
|
+
"""
|
|
807
|
+
This method is used to build the structure of msg.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
response (bytes): The response of device as bytes (b'%RM#0$11\r')
|
|
811
|
+
response_dict (dict): The structure dict of response
|
|
812
|
+
|
|
813
|
+
Return:
|
|
814
|
+
value (str | None): The value of response
|
|
815
|
+
error (bool): If an error occurred return True
|
|
816
|
+
"""
|
|
817
|
+
response_str = response.decode('utf-8', errors='ignore') # decode the response
|
|
818
|
+
start_token = response_dict["start_token"][0] # get the start token from the structure dict.
|
|
819
|
+
index = response_str.rfind(start_token) # get the position of start token in the msg - Note search the last start token in he msg
|
|
820
|
+
if index == -1: # if start token no exist
|
|
821
|
+
return response_str, True # return - true means msg no valid - start token not found
|
|
822
|
+
response_str = response_str[index + 1:] # get the response without start token and - delete "%"
|
|
823
|
+
for key in ["operation", "separator_token", "operation_identifier"]: # check if the token exist in the msg (RM and # and 0)
|
|
824
|
+
expected_value, length = response_dict[key] # get the token and the length
|
|
825
|
+
if str(response_str[:length]) != str(expected_value): # exist if exist
|
|
826
|
+
return response_str, True # return the response and True for msg no valid
|
|
827
|
+
response_str = response_str[length:] # delete the found and checked token
|
|
828
|
+
response_str = response_str.replace(response_dict["end_token"][0], "") # delete the end token - normally \r
|
|
829
|
+
if not response_str: # if response is empty
|
|
830
|
+
expected_state = response_dict.get("value")[0] # get expected state
|
|
831
|
+
if expected_state is None or expected_state is False: # if is none or false
|
|
832
|
+
return "", False # no error
|
|
833
|
+
else:
|
|
834
|
+
return response, True # error
|
|
835
|
+
value_prefix, length = response_dict["value_prefix"]
|
|
836
|
+
if response_str[:length] != value_prefix:
|
|
837
|
+
return response, True # error occurred
|
|
838
|
+
return response_str[length:], False # return value, no error
|
|
839
|
+
|
|
840
|
+
@staticmethod
|
|
841
|
+
def calculate_real_timestamp(response: bytes, last_raw_timestamp: int | None, timestamp_rollover_count: int) -> tuple[float, int, int]:
|
|
842
|
+
"""
|
|
843
|
+
This method is used to calculate the real timestamp with rollover.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
response (bytes): The stream response
|
|
847
|
+
last_raw_timestamp (int | None): The last raw timestamp
|
|
848
|
+
timestamp_rollover (int): The counter of rollover
|
|
849
|
+
|
|
850
|
+
Return:
|
|
851
|
+
real_timestamp (int): The real calculated timestamp
|
|
852
|
+
last_raw_timestamp (int): The last raw timestamp
|
|
853
|
+
timestamp_rollover (int): The counter of rollover
|
|
854
|
+
"""
|
|
855
|
+
raw_timestamp = int.from_bytes(response[0:2], byteorder='big') # 2-byte timestamp max int is 65536 - The device then starts again at 0.
|
|
856
|
+
if last_raw_timestamp is not None and raw_timestamp < last_raw_timestamp: # check if the last timestamp is bigger then the new one
|
|
857
|
+
timestamp_rollover_count += 1 # Rollover detected
|
|
858
|
+
last_raw_timestamp = raw_timestamp # write the new timestamp to the old timestamp
|
|
859
|
+
full_ticks = timestamp_rollover_count * 65536 + raw_timestamp # biggest integer with 16 bit is (2**16 -1) = 65536 - calculate the full tick
|
|
860
|
+
timestamp = (full_ticks * 5) / 1000 # calculate the full time stamp - note 5 is every 5 ms I get a new value
|
|
861
|
+
return timestamp, last_raw_timestamp, timestamp_rollover_count # return
|
|
862
|
+
|
|
863
|
+
@staticmethod
|
|
864
|
+
def check_range(response: bytes, msg_length: int, ranges: list) -> bool | None:
|
|
865
|
+
"""
|
|
866
|
+
This method is used to check the range of the values in the stream response.
|
|
867
|
+
|
|
868
|
+
Args:
|
|
869
|
+
response (bytes): The stream response
|
|
870
|
+
msg_length (int): The length of message
|
|
871
|
+
ranges (list): The ranges for all measured values (min & max)
|
|
872
|
+
Return:
|
|
873
|
+
error_occurred (True | None): If True return an error occurred else not
|
|
874
|
+
"""
|
|
875
|
+
start = 2 # the first 2 byte are timestamp
|
|
876
|
+
end = msg_length - 1 # the last byte is the check sum
|
|
877
|
+
measured_values = [] # create empty list
|
|
878
|
+
for i in range(start, end, 2): # extract all measured values
|
|
879
|
+
value = int.from_bytes(response[i:i + 2], byteorder='big', signed=True) # get the value
|
|
880
|
+
measured_values.append(value) # append the measured value
|
|
881
|
+
for value, (min_val, max_val) in zip(measured_values, ranges): # get the measured value und the range (max min)
|
|
882
|
+
if not (min_val <= value <= max_val): # if not in range
|
|
883
|
+
return True # the value is not in range
|
|
884
|
+
return None
|
|
885
|
+
|
|
886
|
+
@staticmethod
|
|
887
|
+
def extract_measured_values(response: bytes, msg_length: int, conversion_factors: list[float]):
|
|
888
|
+
"""
|
|
889
|
+
This method is used to extract the measured values.
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
response (bytes): The stream response
|
|
893
|
+
msg_length (int): The length of message
|
|
894
|
+
conversion_factors (list[float]): The list with all conversion factors
|
|
895
|
+
|
|
896
|
+
Return:
|
|
897
|
+
converted_values (tuple): All measured values as tuple
|
|
898
|
+
"""
|
|
899
|
+
start = 2 # the first 2 byte are timestamp
|
|
900
|
+
end = msg_length - 1 # the last byte is the check sum
|
|
901
|
+
measured_values = [] # create empty list
|
|
902
|
+
for i in range(start, end, 2): # extract all measured values
|
|
903
|
+
value = int.from_bytes(response[i:i + 2], byteorder='big', signed=True) # get the value
|
|
904
|
+
measured_values.append(value) # append the measured value
|
|
905
|
+
converted_values = [
|
|
906
|
+
raw / factor for raw, factor in zip(measured_values, conversion_factors)
|
|
907
|
+
] # calculate all measured values
|
|
908
|
+
return tuple(converted_values)
|
|
909
|
+
|
|
910
|
+
@staticmethod
|
|
911
|
+
def _update_dict(dict: dict, update_dict: dict) -> dict:
|
|
912
|
+
"""
|
|
913
|
+
This static method is used to update a dict.
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
dict (dict): The dictionary that needs to be updated
|
|
917
|
+
update_dict (dict): The dictionary with the new entries
|
|
918
|
+
|
|
919
|
+
Return:
|
|
920
|
+
dict (dict): The updated dictionary
|
|
921
|
+
"""
|
|
922
|
+
for key, value in update_dict.items(): # get all items
|
|
923
|
+
if key in dict: # if the key exist in the dict
|
|
924
|
+
dict[key][0] = value # the dict is updated
|
|
925
|
+
dict[key][1] = None if value is None or isinstance(value, bool) else len(str(value)) # update the length if is bool or none the length is None else the length
|
|
926
|
+
return dict # return dict
|
|
927
|
+
|
|
928
|
+
def check_values_argument(self, dictionary: dict, operation: str, operation_name: str, value: str | None) -> str | None: # value is None or str
|
|
929
|
+
"""
|
|
930
|
+
This method is used to check the values of the command.
|
|
931
|
+
|
|
932
|
+
Args:
|
|
933
|
+
dictionary (dict): The dictionary for this operation (CM/RM etc....)
|
|
934
|
+
operation (str): The operation (CM/RM.....)
|
|
935
|
+
operation_name (str): The operation name (pressure_low)
|
|
936
|
+
value (str | None): The value that is set
|
|
937
|
+
|
|
938
|
+
Return:
|
|
939
|
+
value (str | None): The value of response
|
|
940
|
+
"""
|
|
941
|
+
command_has_value = message_value_mapping.get(operation, {}).get("command_has_value") # get if the command_has_value
|
|
942
|
+
entry_for_value = dictionary.get(operation_name, {}).get("value") # get the entry for value
|
|
943
|
+
if command_has_value is False and value is None: # Case 1: No value required and none given
|
|
944
|
+
return None # return None
|
|
945
|
+
if command_has_value is True and value is None: # Case 2: Value required, but none given
|
|
946
|
+
self._raise_error(dictionary, operation_name, value) # call the method for raise the error
|
|
947
|
+
if command_has_value is False and value is not None: # Case 3: No value required, but one given
|
|
948
|
+
self._raise_error(dictionary, operation_name, value) # call the method for raise the error
|
|
949
|
+
if command_has_value is None and value is None: # Case 4: Unclear whether value is required, and none given
|
|
950
|
+
if entry_for_value is None: # if no value required
|
|
951
|
+
return None # return None
|
|
952
|
+
self._raise_error(dictionary, operation_name, value) # call the method for raise the error
|
|
953
|
+
if value is not None: # Cases 5 & 6: Value given, check whether checking the range or mapping for translation
|
|
954
|
+
if entry_for_value is True: # if entry_for_value is True
|
|
955
|
+
min_value, max_value = dictionary.get(operation_name, {}).get("range").get("min"), dictionary.get(operation_name, {}).get("range").get("max") # extract min_value, max_value
|
|
956
|
+
try: # try
|
|
957
|
+
if not (min_value <= int(value) <= max_value): # if value if not in range
|
|
958
|
+
self._raise_error(dictionary, operation_name, value) # raise an value error
|
|
959
|
+
except ValueError:
|
|
960
|
+
self._raise_error(dictionary, operation_name, value) # is impossible to convert in int
|
|
961
|
+
return value # return value
|
|
962
|
+
elif entry_for_value is not None: # if entry_for_value is not None
|
|
963
|
+
translation = entry_for_value.get(value) # get the translation
|
|
964
|
+
if translation is None: # if the translation is none
|
|
965
|
+
self._raise_error(dictionary, operation_name, value) # call the method for raise the error
|
|
966
|
+
return translation # return translation
|
|
967
|
+
else:
|
|
968
|
+
self._raise_error(dictionary, operation_name, value) # call the method for raise the error
|
|
969
|
+
|
|
970
|
+
def _raise_error(self, dictionary: dict, operation_name: str, value: str | None) -> None:
|
|
971
|
+
"""
|
|
972
|
+
This method is used to generate the raise error with msg
|
|
973
|
+
|
|
974
|
+
Args:
|
|
975
|
+
dictionary (dict): The dictionary for this operation (CM/RM etc....)
|
|
976
|
+
operation_name (str): The operation name (pressure_low)
|
|
977
|
+
value (str | None): The value that is set
|
|
978
|
+
"""
|
|
979
|
+
entry_for_value = dictionary.get(operation_name, {}).get("value") # get the entry for the value in the dict
|
|
980
|
+
conversion_factor = dictionary.get(operation_name, None).get("conversion_factor") # get the conversion factor
|
|
981
|
+
if entry_for_value is None: # if entry is none
|
|
982
|
+
valid_values = None # the valid values is none
|
|
983
|
+
elif entry_for_value is True: # if entry is True
|
|
984
|
+
min_value, max_value = dictionary.get(operation_name, {}).get("range").get("min") / conversion_factor, dictionary.get(operation_name, {}).get("range").get("max") / conversion_factor # get max and min value
|
|
985
|
+
valid_values = f"values from {min_value} to {max_value}" # set the valid range
|
|
986
|
+
else:
|
|
987
|
+
valid_values = list(dictionary.get(operation_name, {}).get("value")) # set the list with valid operation_names
|
|
988
|
+
if value is not None and conversion_factor is not None:
|
|
989
|
+
value = value / conversion_factor
|
|
990
|
+
raise ValueError(f"Value is not valid: {value}\nThe following values are valid: {valid_values}") # raise an value error
|
|
991
|
+
|
|
992
|
+
@staticmethod
|
|
993
|
+
def check_operation_name(dictionary: dict, operation_name: str) -> str:
|
|
994
|
+
"""
|
|
995
|
+
This method is used to generate the raise error with msg
|
|
996
|
+
|
|
997
|
+
Args:
|
|
998
|
+
dictionary (dict): The dictionary for this operation (CM/RM etc....)
|
|
999
|
+
operation_name (str): The operation name (pressure_low)
|
|
1000
|
+
|
|
1001
|
+
Return:
|
|
1002
|
+
operation_identifier (str): The translated operation name
|
|
1003
|
+
"""
|
|
1004
|
+
if (operation_identifier := dictionary.get(operation_name, {}).get("operation_identifier")) is None: # if operation identifier is not found
|
|
1005
|
+
raise ValueError(f"Operation name is not valid: {operation_name}\nThe following operation names are valid {list(dictionary.keys())}") # raise value error
|
|
1006
|
+
return operation_identifier # return the operation identifier
|
|
1007
|
+
|
|
1008
|
+
@staticmethod
|
|
1009
|
+
def validate_and_cast(value, target_types, allowed_types, min_value=None, max_value=None) -> tuple[str | int | float | bool | None]:
|
|
1010
|
+
"""
|
|
1011
|
+
This method validates whether the input value can be converted to the expected data type and whether it falls within the allowed range.
|
|
1012
|
+
|
|
1013
|
+
Args:
|
|
1014
|
+
value (str | int | float | bool | None): The value to be checked
|
|
1015
|
+
target_types (str | int | float | bool | None | tuple): The target types
|
|
1016
|
+
allowed_types (tuple |str | int | float | bool | None): The allowed types
|
|
1017
|
+
min_value (int | float | None): The lower limit of the permissible range
|
|
1018
|
+
max_value (int | float | None): The upper limit of the permissible range
|
|
1019
|
+
|
|
1020
|
+
Return:
|
|
1021
|
+
value (str | int | float | bool | None): the converted and verified value
|
|
1022
|
+
"""
|
|
1023
|
+
if not isinstance(allowed_types, tuple): # if the input in not a tuple
|
|
1024
|
+
allowed_types = (allowed_types,) # convert to tuple
|
|
1025
|
+
if type(value) not in allowed_types: # if the type of input value is not in the allowed types
|
|
1026
|
+
raise ValueError(f"The input '{value}' is invalid. Please provide data that is convertible to {target_types} in range of {min_value} to {max_value}") # raise an error
|
|
1027
|
+
if type(value) is type(None) or type(value) is bool: # if the type of value is bool or None return it directly
|
|
1028
|
+
return value
|
|
1029
|
+
try: # try
|
|
1030
|
+
if target_types == str: # if the target type is str
|
|
1031
|
+
return str(value) # convert to str and return
|
|
1032
|
+
elif target_types == int: # if the target type is int
|
|
1033
|
+
value_as_float = float(value) # change to float - Note for convert string to float (directly from string to int not possible)
|
|
1034
|
+
if value_as_float.is_integer(): # check if is integer if not pass for raise error (maybe now is 3.4)
|
|
1035
|
+
value_as_int = int(value_as_float) # convert the float to int
|
|
1036
|
+
if max_value is not None and min_value is not None: # if max and min are not none
|
|
1037
|
+
if not (min_value <= value_as_int <= max_value): # if the sample_count is not in range
|
|
1038
|
+
1 / 0 # for trigger the exception
|
|
1039
|
+
return value_as_int # return value as int
|
|
1040
|
+
elif target_types == float: # convert in float
|
|
1041
|
+
value_as_float = float(value) # convert to float
|
|
1042
|
+
if max_value is not None and min_value is not None: # if are not None
|
|
1043
|
+
if not (min_value <= value_as_float <= max_value): # if the sample_count is not in range
|
|
1044
|
+
1 / 0 # for trigger the exception
|
|
1045
|
+
return value_as_float # return
|
|
1046
|
+
elif target_types is type(None): # check if is None type
|
|
1047
|
+
return value # return it
|
|
1048
|
+
elif target_types is bool: # check if is bool
|
|
1049
|
+
return value # return the value
|
|
1050
|
+
except Exception: # Exception
|
|
1051
|
+
pass
|
|
1052
|
+
raise ValueError(f"The input '{value}' is invalid. Please provide data that is convertible to {target_types} in range of {min_value} to {max_value}") # raise error
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
if __name__ == "__main__":
|
|
1056
|
+
pass
|