syndesi 0.4.2__py3-none-any.whl → 0.5.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.
Files changed (57) hide show
  1. syndesi/__init__.py +22 -2
  2. syndesi/adapters/adapter.py +332 -489
  3. syndesi/adapters/adapter_worker.py +820 -0
  4. syndesi/adapters/auto.py +58 -25
  5. syndesi/adapters/descriptors.py +38 -0
  6. syndesi/adapters/ip.py +203 -71
  7. syndesi/adapters/serialport.py +154 -25
  8. syndesi/adapters/stop_conditions.py +354 -0
  9. syndesi/adapters/timeout.py +58 -21
  10. syndesi/adapters/visa.py +236 -11
  11. syndesi/cli/console.py +51 -16
  12. syndesi/cli/shell.py +95 -47
  13. syndesi/cli/terminal_tools.py +8 -8
  14. syndesi/component.py +315 -0
  15. syndesi/protocols/delimited.py +92 -107
  16. syndesi/protocols/modbus.py +2368 -868
  17. syndesi/protocols/protocol.py +186 -33
  18. syndesi/protocols/raw.py +45 -62
  19. syndesi/protocols/scpi.py +65 -102
  20. syndesi/remote/remote.py +188 -0
  21. syndesi/scripts/syndesi.py +12 -2
  22. syndesi/tools/errors.py +49 -31
  23. syndesi/tools/log_settings.py +21 -8
  24. syndesi/tools/{log.py → logmanager.py} +24 -13
  25. syndesi/tools/types.py +9 -7
  26. syndesi/version.py +5 -1
  27. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/METADATA +1 -1
  28. syndesi-0.5.0.dist-info/RECORD +41 -0
  29. syndesi/adapters/backend/__init__.py +0 -0
  30. syndesi/adapters/backend/adapter_backend.py +0 -438
  31. syndesi/adapters/backend/adapter_manager.py +0 -48
  32. syndesi/adapters/backend/adapter_session.py +0 -346
  33. syndesi/adapters/backend/backend.py +0 -438
  34. syndesi/adapters/backend/backend_status.py +0 -0
  35. syndesi/adapters/backend/backend_tools.py +0 -66
  36. syndesi/adapters/backend/descriptors.py +0 -153
  37. syndesi/adapters/backend/ip_backend.py +0 -149
  38. syndesi/adapters/backend/serialport_backend.py +0 -241
  39. syndesi/adapters/backend/stop_condition_backend.py +0 -219
  40. syndesi/adapters/backend/timed_queue.py +0 -39
  41. syndesi/adapters/backend/timeout.py +0 -252
  42. syndesi/adapters/backend/visa_backend.py +0 -197
  43. syndesi/adapters/ip_server.py +0 -102
  44. syndesi/adapters/stop_condition.py +0 -90
  45. syndesi/cli/backend_console.py +0 -96
  46. syndesi/cli/backend_status.py +0 -274
  47. syndesi/cli/backend_wrapper.py +0 -61
  48. syndesi/scripts/syndesi_backend.py +0 -37
  49. syndesi/tools/backend_api.py +0 -175
  50. syndesi/tools/backend_logger.py +0 -64
  51. syndesi/tools/exceptions.py +0 -16
  52. syndesi/tools/internal.py +0 -0
  53. syndesi-0.4.2.dist-info/RECORD +0 -60
  54. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/WHEEL +0 -0
  55. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/entry_points.txt +0 -0
  56. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/licenses/LICENSE +0 -0
  57. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,57 @@
1
1
  # File : modbus.py
2
2
  # Author : Sébastien Deriaz
3
3
  # License : GPL
4
+ """
5
+ Modbus TCP and Modbus RTU implementation
6
+ """
7
+
8
+ # PDU (Protocol Data Unit) format
9
+ #
10
+ # Modbus TCP
11
+ # ┌────────────────┬─────────────┬────────┬─────────┬───────┐
12
+ # │ Transaction ID │ Protocol ID │ Length │ Unit ID │ Data │
13
+ # └────────────────┴─────────────┴────────┴─────────┴───────┘
14
+ # 2 bytes 2 bytes 2 bytes 1 byte N bytes
15
+ #
16
+ #
17
+ # Modbus RTU
18
+ # ┌───────────────┬────────┬────────┐
19
+ # │ Slave address │ Data │ CRC │
20
+ # └───────────────┴────────┴────────┘
21
+ # 1 byte N bytes 2 bytes
4
22
  #
23
+ # Modbus ASCII
24
+ # ┌────────┬───────────────┬────────┬───────┬─────────┐
25
+ # │ HEADER │ Slave address │ Data │ CRC │ TRAILER │
26
+ # └────────┴───────────────┴────────┴───────┴─────────┘
27
+ # 1 byte 1 byte N bytes 2 bytes 1 byte
5
28
  #
6
- # Modbus TCP and Modbus RTU implementation
29
+ #
30
+ # The PDU is built and parsed by the Modbus class, each data block (SDU)
31
+ # is constructed by its corresponding ModbusRequestSDU class. SDUs are parsed
32
+ # by ModbusResponseSDU classes
33
+
34
+ # pylint: disable=too-many-lines
35
+
36
+ from __future__ import annotations
7
37
 
8
38
  import struct
39
+ from abc import abstractmethod
40
+ from dataclasses import dataclass
9
41
  from enum import Enum
10
42
  from math import ceil
43
+ from types import EllipsisType
11
44
  from typing import cast
12
- from unittest.mock import DEFAULT
45
+
46
+ from syndesi.adapters.adapter_worker import AdapterEvent
47
+ from syndesi.component import AdapterFrame
13
48
 
14
49
  from ..adapters.adapter import Adapter
15
50
  from ..adapters.ip import IP
16
51
  from ..adapters.serialport import SerialPort
17
- from ..adapters.stop_condition import Continuation, Length
18
52
  from ..adapters.timeout import Timeout
19
- from .protocol import Protocol
53
+ from ..tools.errors import ProtocolError, ProtocolReadError
54
+ from .protocol import Protocol, ProtocolFrame
20
55
 
21
56
  MODBUS_TCP_DEFAULT_PORT = 502
22
57
 
@@ -29,13 +64,18 @@ MAX_DISCRETE_INPUTS = (
29
64
  # This value has been checked and going up to 1976 seems to work but sticking to the
30
65
  # spec is safer
31
66
 
32
- # Specification says 125, but write_multiple_registers would exceed the allow number of bytes in that case
67
+ # Specification says 125, but write_multiple_registers would exceed the allow number
68
+ # of bytes in that case
33
69
  # MAX_NUMBER_OF_REGISTERS = 123
34
70
 
35
71
  ExceptionCodesType = dict[int, str]
36
72
 
37
73
 
38
74
  class Endian(Enum):
75
+ """
76
+ Endian enum
77
+ """
78
+
39
79
  BIG = "big"
40
80
  LITTLE = "little"
41
81
 
@@ -43,6 +83,64 @@ class Endian(Enum):
43
83
  endian_symbol = {Endian.BIG: ">", Endian.LITTLE: "<"}
44
84
 
45
85
 
86
+ def _dm_to_pdu_address(dm_address: int) -> int:
87
+ """
88
+ Convert Modbus data model address to Modbus PDU address
89
+
90
+ - Modbus data model starts at address 1
91
+ - Modbus PDU addresses start at 0
92
+
93
+ Modbus data model is the one specified in the devices datasheets
94
+
95
+ Parameters
96
+ ----------
97
+ dm_address : int
98
+ """
99
+ if dm_address == 0:
100
+ raise ValueError("Address 0 is not valid in Modbus data model")
101
+
102
+ return dm_address - 1
103
+
104
+
105
+ def _pdu_to_dm_address(pdu_address: int) -> int:
106
+ """
107
+ Convert Modbus PDU address to Modbus data model address
108
+
109
+ - Modbus data model starts at address 1
110
+ - Modbus PDU addresses start at 0
111
+
112
+ Modbus data model is the one specified in the devices datasheets
113
+
114
+ Parameters
115
+ ----------
116
+ pdu_address : int
117
+ """
118
+ return pdu_address + 1
119
+
120
+
121
+ def _check_discrete_input_output_count(discrete_inputs: int) -> None:
122
+ if not 1 <= discrete_inputs <= MAX_DISCRETE_INPUTS:
123
+ raise ValueError(
124
+ f"Invalid number of inputs/outputs : {discrete_inputs}, it must be in "
125
+ f"the range [1, {MAX_DISCRETE_INPUTS}]"
126
+ )
127
+
128
+
129
+ def _check_address(address: int, max_address: int | None = None) -> None:
130
+ if not MIN_ADDRESS <= address <= MAX_ADDRESS:
131
+ raise ValueError(
132
+ f"Invalid address : {address}, it must be in the range "
133
+ f"[{MIN_ADDRESS},{MAX_ADDRESS}]"
134
+ )
135
+
136
+ if max_address is not None:
137
+ if address > max_address:
138
+ raise ValueError(
139
+ f"Invalid address : {address}, it cannot exceed {max_address}"
140
+ " in the current context"
141
+ )
142
+
143
+
46
144
  ENDIAN = endian_symbol[Endian.BIG]
47
145
 
48
146
  # TCP
@@ -54,14 +152,31 @@ ENDIAN = endian_symbol[Endian.BIG]
54
152
  # Size limitation only apply to Modbus RTU
55
153
  AVAILABLE_PDU_SIZE = 255 - 3
56
154
 
155
+ _ASCII_HEADER = b":"
156
+ _PROTOCO_ID = 0
157
+ _UNIT_ID = 0
158
+ _ASCII_TRAILER = b"\r\n"
159
+
57
160
 
58
161
  class ModbusType(Enum):
162
+ """
163
+ Modbus type
164
+
165
+ - TCP : Modbus over TCP
166
+ - RTU : Modbus over serial
167
+ - ASCII : Modbus using text based encoding
168
+ """
169
+
59
170
  RTU = "RTU"
60
171
  ASCII = "ASCII"
61
172
  TCP = "TCP"
62
173
 
63
174
 
64
175
  class FunctionCode(Enum):
176
+ """
177
+ Modbus function codes enum
178
+ """
179
+
65
180
  # Public function codes 1 to 64
66
181
  READ_COILS = 0x01
67
182
  READ_DISCRETE_INPUTS = 0x02
@@ -89,6 +204,10 @@ class FunctionCode(Enum):
89
204
 
90
205
 
91
206
  class DiagnosticsCode(Enum):
207
+ """
208
+ Modbus Diagnostics codes enum
209
+ """
210
+
92
211
  RETURN_QUERY_DATA = 0x00
93
212
  RESTART_COMMUNICATIONS_OPTION = 0x01
94
213
  RETURN_DIAGNOSTIC_REGISTER = 0x02
@@ -110,11 +229,19 @@ class DiagnosticsCode(Enum):
110
229
 
111
230
 
112
231
  class EncapsulatedInterfaceTransportSubFunctionCodes(Enum):
232
+ """
233
+ Encapsulated interface transport subfunction codes enum
234
+ """
235
+
113
236
  CANOPEN_GENERAL_REFERENCE_REQUEST_AND_RESPONSE_PDU = 0x0D
114
237
  READ_DEVICE_IDENTIFICATION = 0x0E
115
238
 
116
239
 
117
240
  class DeviceIndentificationObjects(Enum):
241
+ """
242
+ Device identification objects enum
243
+ """
244
+
118
245
  VENDOR_NAME = 0x00
119
246
  PRODUCT_CODE = 0x01
120
247
  MAJOR_MINOR_REVISION = 0x02
@@ -135,20 +262,28 @@ SERIAL_LINE_ONLY_CODES = [
135
262
 
136
263
 
137
264
  def bool_list_to_bytes(lst: list[bool]) -> bytes:
265
+ """
266
+ Convert a list of bool to bytes, LSB first
267
+ """
138
268
  byte_count = ceil(len(lst) / 8)
139
- result: bytes = sum([2**i * int(v) for i, v in enumerate(lst)]).to_bytes(
269
+ result: bytes = sum(2**i * int(v) for i, v in enumerate(lst)).to_bytes(
140
270
  byte_count, byteorder="little"
141
271
  )
142
272
  return result
143
273
 
144
274
 
145
- def bytes_to_bool_list(_bytes: bytes, N: int) -> list[bool]:
146
- return [
147
- True if c == "1" else False for c in "".join([f"{x:08b}"[::-1] for x in _bytes])
148
- ][:N]
275
+ def bytes_to_bool_list(_bytes: bytes, n: int) -> list[bool]:
276
+ """
277
+ Convert bytes to a list of bool, one per bit, LSB first
278
+ """
279
+ return [c == "1" for c in "".join([f"{x:08b}"[::-1] for x in _bytes])][:n]
149
280
 
150
281
 
151
282
  class TypeCast(Enum):
283
+ """
284
+ Type of cast when storing values in modbus registers
285
+ """
286
+
152
287
  INT = "int"
153
288
  UINT = "uint"
154
289
  FLOAT = "float"
@@ -156,467 +291,1506 @@ class TypeCast(Enum):
156
291
  ARRAY = "array"
157
292
 
158
293
  def is_number(self) -> bool:
294
+ """
295
+ Return True if the type is a number
296
+ """
159
297
  return self in [TypeCast.INT, TypeCast.UINT, TypeCast.FLOAT]
160
298
 
161
299
 
162
- def struct_format(type: TypeCast, length: int) -> str:
163
- if type == TypeCast.INT:
164
- if length == 1:
165
- return "b"
166
- elif length == 2:
167
- return "h"
168
- elif length == 4:
169
- return "i" # or 'l'
170
- elif length == 8:
171
- return "q"
172
- elif type == TypeCast.UINT:
173
- if length == 1:
174
- return "B"
175
- elif length == 2:
176
- return "H"
177
- elif length == 4:
178
- return "I" # or 'L'
179
- elif length == 8:
180
- return "Q"
181
- elif type == TypeCast.FLOAT:
182
- if length == 4:
183
- return "f"
184
- elif length == 8:
185
- return "d"
186
- elif type == TypeCast.STRING or type == TypeCast.ARRAY:
300
+ def struct_format(_type: TypeCast, length: int) -> str:
301
+ """
302
+ Convert typecast+length to python struct character
303
+ """
304
+ struct_characters = {
305
+ (TypeCast.INT, 1): "b",
306
+ (TypeCast.INT, 2): "h",
307
+ (TypeCast.INT, 4): "i",
308
+ (TypeCast.INT, 8): "q",
309
+ (TypeCast.UINT, 1): "B",
310
+ (TypeCast.UINT, 2): "H",
311
+ (TypeCast.UINT, 4): "I", # or 'L'
312
+ (TypeCast.UINT, 8): "Q",
313
+ (TypeCast.FLOAT, 4): "f",
314
+ (TypeCast.FLOAT, 8): "d",
315
+ }
316
+
317
+ if _type in [TypeCast.STRING, TypeCast.ARRAY]:
187
318
  return f"{length}s"
188
- else:
189
- raise ValueError(f"Unknown cast type : {type}")
190
- raise ValueError(f"Invalid type cast / length combination : {type} / {length}")
191
-
319
+ try:
320
+ return struct_characters[_type, length]
321
+ except KeyError:
322
+ pass
323
+ raise ValueError(f"Invalid type cast / length combination : {_type} / {length}")
192
324
 
193
- class ModbusException(Exception):
194
- pass
195
325
 
326
+ class ModbusError(Exception):
327
+ """
328
+ Generic modbus exception
329
+ """
196
330
 
197
- class Modbus(Protocol):
198
- _ASCII_HEADER = b":"
199
- _ASCII_TRAILER = b"\r\n"
200
331
 
201
- def __init__(
202
- self,
203
- adapter: Adapter,
204
- timeout: Timeout = DEFAULT,
205
- _type: str = ModbusType.RTU.value,
206
- slave_address: int | None = None,
207
- ) -> None:
208
- """
209
- Modbus protocol
332
+ def modbus_crc(_bytes: bytes) -> int:
333
+ """Calculate modbus CRC from the given buffer"""
334
+ # TODO : Implement
335
+ return 0
210
336
 
211
- Parameters
212
- ----------
213
- adapter : Adapter
214
- SerialPort or IP
215
- timeout : Timeout
216
- _type : str
217
- Only used with SerialPort adapter
218
- 'RTU' : Modbus RTU (default)
219
- 'ASCII' : Modbus ASCII
220
- """
221
- super().__init__(adapter, timeout)
222
- self._logger.debug("Initializing Modbus protocol...")
223
337
 
224
- if isinstance(adapter, IP):
225
- self._adapter: IP
226
- self._adapter.set_default_port(MODBUS_TCP_DEFAULT_PORT)
227
- self._modbus_type = ModbusType.TCP
228
- elif isinstance(adapter, SerialPort):
229
- self._modbus_type = ModbusType(_type)
230
- assert slave_address is not None, "slave_address must be set"
231
- raise NotImplementedError("Serialport (Modbus RTU) is not supported yet")
232
- else:
233
- raise ValueError("Invalid adapter")
338
+ def _raise_if_error(sdu: bytes, exceptions: dict[int, str]) -> None:
339
+ if sdu == b"":
340
+ raise ModbusError("Empty response")
341
+ if sdu[0] & 0x80:
342
+ # There is an error
343
+ code = sdu[1]
344
+ if code not in exceptions:
345
+ raise ModbusError(f"Unexpected modbus error code: {code}")
346
+ raise ModbusError(f"{code:02X} : {exceptions[code]}")
234
347
 
235
- self._slave_address = slave_address
236
348
 
237
- self._transaction_id = 0
349
+ class ModbusSDU:
350
+ """
351
+ Modbus Service Data Unit
238
352
 
239
- # Connect the adapter if it wasn't done already
240
- self._adapter.connect()
353
+ Subclasses of this contain either request or response fields to a modbus
354
+ frame
355
+ """
241
356
 
242
- def _dm_to_pdu_address(self, dm_address: int) -> int:
243
- """
244
- Convert Modbus data model address to Modbus PDU address
357
+ def make_sdu(self) -> bytes:
358
+ """Generate a bytes array containing the SDU"""
359
+ raise ProtocolError("make_sdu() is not supported for this SDU")
245
360
 
246
- - Modbus data model starts at address 1
247
- - Modbus PDU addresses start at 0
361
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
362
+ """Generate a ModbusSDU instance from a given sdu buffer"""
363
+ raise ProtocolError("parse_sdu() is not supported for this SDU")
248
364
 
249
- Modbus data model is the one specified in the devices datasheets
365
+ def exceptions(self) -> dict[int, str]:
366
+ """Return a dictionary of exceptions based on their integer code"""
367
+ return {}
250
368
 
251
- Parameters
252
- ----------
253
- dm_address : int
254
- """
255
- if dm_address == 0:
256
- raise ValueError("Address 0 is not valid in Modbus data model")
369
+ def _check_for_error(self, sdu: bytes) -> None:
370
+ """Check the given sdu buffer for an exception"""
371
+ _raise_if_error(sdu, self.exceptions())
257
372
 
258
- return dm_address - 1
259
373
 
260
- def _pdu_to_dm_address(self, pdu_address: int) -> int:
261
- """
262
- Convert Modbus PDU address to Modbus data model address
374
+ class ModbusRequestSDU(ModbusSDU):
375
+ """
376
+ Base class for modbus request SDUs (Service Data Unit)
377
+ """
263
378
 
264
- - Modbus data model starts at address 1
265
- - Modbus PDU addresses start at 0
379
+ @abstractmethod
380
+ def function_code(self) -> FunctionCode:
381
+ """Return the function code of this modbus request"""
266
382
 
267
- Modbus data model is the one specified in the devices datasheets
383
+ @classmethod
384
+ def expected_length(cls, pdu_length: int, modbus_type: ModbusType) -> int:
385
+ """
386
+ Return the length of the modbus SDU based on the length of the PDU and modbus type
268
387
 
269
388
  Parameters
270
389
  ----------
271
- pdu_address : int
272
- """
273
- return pdu_address + 1
274
-
275
- def _crc(self, _bytes: bytes) -> int:
276
- # TODO : Implement
277
- return 0
278
-
279
- def _make_pdu(self, _bytes: bytes) -> bytes:
390
+ pdu_length : int
391
+ modbus_type : ModbusType
280
392
  """
281
- Return PDU generated from bytes data
282
- """
283
- PROTOCOL_ID = 0
284
- UNIT_ID = 0
285
-
286
- if self._modbus_type == ModbusType.TCP:
287
- # Return raw data
288
- # output = _bytes
289
- # Temporary :
290
- length = len(_bytes) + 1 # unit_id is included
291
- output = (
292
- struct.pack(
293
- ENDIAN + "HHHB", self._transaction_id, PROTOCOL_ID, length, UNIT_ID
294
- )
295
- + _bytes
296
- )
297
- self._transaction_id += 1
393
+ if modbus_type == ModbusType.TCP:
394
+ output = pdu_length + 8
298
395
  else:
299
- # Add slave address and error check
300
- error_check = self._crc(_bytes)
301
- output = (
302
- struct.pack(ENDIAN + "B", self._slave_address)
303
- + _bytes
304
- + struct.pack(ENDIAN + "H", error_check)
305
- )
306
- if self._modbus_type.ASCII:
396
+ output = 1 + pdu_length + 2
397
+ if modbus_type == ModbusType.ASCII:
307
398
  # Add header and trailer
308
- output = self._ASCII_HEADER + output + self._ASCII_TRAILER
309
-
399
+ output += len(_ASCII_HEADER) + len(_ASCII_TRAILER)
310
400
  return output
311
401
 
312
- def _raise_if_error(
313
- self, response: bytes, exception_codes: ExceptionCodesType
314
- ) -> None:
315
- if response == b"":
316
- raise RuntimeError("Empty response")
317
- if response[0] & 0x80:
318
- # There is an error
319
- code = response[1]
320
- if code not in exception_codes:
321
- raise RuntimeError(f"Unexpected modbus error code: {code}")
322
- else:
323
- raise ModbusException(f"{code:02X} : {exception_codes[code]}")
324
-
325
- def _is_error(self, response: bytes) -> bool:
402
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
326
403
  raise NotImplementedError()
327
404
 
328
- def _error_code(self, response: bytes) -> int:
329
- return response[1]
330
405
 
331
- def _parse_pdu(self, _pdu: bytes | None) -> bytes:
332
- """
333
- Return data from PDU
334
- """
335
- if _pdu is None:
336
- raise RuntimeError("Failed to read modbus data")
337
- if self._modbus_type == ModbusType.TCP:
338
- # Return raw data
339
- # data = _pdu
340
- data = _pdu[7:]
406
+ # class ModbusResponseSDU(ModbusSDU):
407
+ # def make_sdu(self) -> bytes:
408
+ # """Generate a bytes array containing the SDU"""
409
+ # raise NotImplementedError("make_sdu() not implemented for this SDU")
341
410
 
342
- else:
343
- if self._modbus_type.ASCII:
344
- # Remove header and trailer
345
- _pdu = _pdu[len(self._ASCII_HEADER) : -len(self._ASCII_TRAILER)]
346
- # Remove slave address and CRC and check CRC
347
411
 
348
- data = _pdu[1:-2]
349
- # Check CRC
350
- # error_check = _pdu[-2:]
351
- # TODO : Check here and raise exception
412
+ class SerialLineOnlySDU(ModbusRequestSDU):
413
+ """
414
+ Marker class for serial line only Modbus function codes
415
+ """
352
416
 
353
- return data
417
+ @abstractmethod
418
+ def exceptions(self) -> dict[int, str]: ...
354
419
 
355
- def _length(self, pdu_length: int) -> int:
356
- dummy_pdu = self._make_pdu(b"")
357
- return len(dummy_pdu) + pdu_length
358
420
 
359
- # Read Coils - 0x01
360
- def read_coils(self, start_address: int, number_of_coils: int) -> list[bool]:
361
- """
362
- Read a defined number of coils starting at a set address
421
+ # Read Coils - 0x01
422
+ @dataclass
423
+ class ReadCoilsSDU(ModbusRequestSDU):
424
+ """Read coils request."""
363
425
 
364
- Parameters
365
- ----------
366
- start_address : int
367
- number_of_coils : int
426
+ start_address: int
427
+ number_of_coils: int
368
428
 
369
- Returns
370
- -------
371
- coils : list
372
- """
429
+ @dataclass
430
+ class Response(ModbusSDU):
431
+ """Response data."""
432
+
433
+ coils: list[bool]
373
434
 
374
- EXCEPTIONS: ExceptionCodesType = {
435
+ def function_code(self) -> FunctionCode:
436
+ return FunctionCode.READ_COILS
437
+
438
+ def exceptions(self) -> dict[int, str]:
439
+ return {
375
440
  1: "Function code not supported",
376
441
  2: "Invalid Start or end addresses",
377
442
  3: "Invalid quantity of outputs",
378
443
  4: "Couldn't read coils",
379
444
  }
380
445
 
381
- assert (
382
- 1 <= number_of_coils <= MAX_DISCRETE_INPUTS
383
- ), f"Invalid number of coils : {number_of_coils}"
384
- assert (
385
- MIN_ADDRESS <= start_address <= MAX_ADDRESS - number_of_coils + 1
386
- ), f"Invalid start address : {start_address}"
446
+ def __post_init__(self) -> None:
447
+ _check_discrete_input_output_count(self.number_of_coils)
448
+ _check_address(self.start_address, MAX_ADDRESS - self.number_of_coils + 1)
387
449
 
388
- query = struct.pack(
450
+ def make_sdu(self) -> bytes:
451
+ data = struct.pack(
389
452
  ENDIAN + "BHH",
390
453
  FunctionCode.READ_COILS.value,
391
- self._dm_to_pdu_address(start_address),
392
- number_of_coils,
454
+ _dm_to_pdu_address(self.start_address),
455
+ self.number_of_coils,
393
456
  )
394
457
 
395
- n_coil_bytes = ceil(number_of_coils / 8)
396
- pdu: bytes | None = self._adapter.query(
397
- self._make_pdu(query),
398
- # timeout=Timeout(continuation=1),
399
- stop_conditions=[
400
- Length(self._length(n_coil_bytes + 2)),
401
- Continuation(time=1),
402
- ], # TODO : convert to multiple stop conditions here
403
- )
404
- response = self._parse_pdu(pdu)
405
- self._raise_if_error(response, exception_codes=EXCEPTIONS)
458
+ return data
406
459
 
407
- _, _, coil_bytes = struct.unpack(ENDIAN + f"BB{n_coil_bytes}s", response)
408
- coils = bytes_to_bool_list(coil_bytes, number_of_coils)
409
- return coils
460
+ def parse_sdu(self, sdu: bytes) -> Response:
461
+ super()._check_for_error(sdu)
462
+ _, n_bytes = struct.unpack(ENDIAN + "BB", sdu[:2])
463
+ coil_bytes = struct.unpack(ENDIAN + f"{n_bytes}s", sdu[2:])[0]
464
+ coils = bytes_to_bool_list(coil_bytes, self.number_of_coils)
465
+ return self.Response(coils)
410
466
 
411
- def read_single_coil(self, address: int) -> bool:
412
- """
413
- Read a single coil at a specified address.
414
- This is a wrapper for the read_coils method with the number of coils set to 1
415
467
 
416
- Parameters
417
- ----------
418
- address : int
468
+ # Read discrete inputs - 0x02
469
+ @dataclass
470
+ class ReadDiscreteInputs(ModbusRequestSDU):
471
+ """Read discrete inputs request."""
419
472
 
420
- Returns
421
- -------
422
- coil : bool
423
- """
424
- coil = self.read_coils(start_address=address, number_of_coils=1)[0]
425
- return coil
473
+ start_address: int
474
+ number_of_inputs: int
426
475
 
427
- # Read Discrete inputs - 0x02
428
- def read_discrete_inputs(
429
- self, start_address: int, number_of_inputs: int
430
- ) -> list[bool]:
431
- """
432
- Read a defined number of discrete inputs at a set starting address
476
+ @dataclass
477
+ class Response(ModbusSDU):
478
+ """Response data."""
433
479
 
434
- Parameters
435
- ----------
436
- start_address : int
437
- number_of_inputs : int
480
+ inputs: list[bool]
438
481
 
439
- Returns
440
- -------
441
- inputs : list
442
- List of booleans
443
- """
482
+ def function_code(self) -> FunctionCode:
483
+ return FunctionCode.READ_DISCRETE_INPUTS
444
484
 
445
- EXCEPTIONS = {
485
+ def exceptions(self) -> dict[int, str]:
486
+ return {
446
487
  1: "Function code not supported",
447
488
  2: "Invalid Start or end addresses",
448
489
  3: "Invalid quantity of inputs",
449
490
  4: "Couldn't read inputs",
450
491
  }
451
492
 
452
- assert (
453
- 1 <= number_of_inputs <= MAX_DISCRETE_INPUTS
454
- ), f"Invalid number of inputs : {number_of_inputs}"
455
- assert (
456
- MIN_ADDRESS <= start_address <= MAX_ADDRESS - number_of_inputs + 1
457
- ), f"Invalid start address : {start_address}"
493
+ def __post_init__(self) -> None:
494
+ _check_discrete_input_output_count(self.number_of_inputs)
495
+ _check_address(self.start_address, MAX_ADDRESS - self.number_of_inputs + 1)
458
496
 
459
- query = struct.pack(
497
+ def make_sdu(self) -> bytes:
498
+ sdu = struct.pack(
460
499
  ENDIAN + "BHH",
461
- FunctionCode.READ_DISCRETE_INPUTS.value,
462
- self._dm_to_pdu_address(start_address),
463
- number_of_inputs,
500
+ self.function_code().value,
501
+ _dm_to_pdu_address(self.start_address),
502
+ self.number_of_inputs,
464
503
  )
504
+ return sdu
505
+
506
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
507
+ self._check_for_error(sdu)
508
+
465
509
  byte_count = ceil(
466
- number_of_inputs / 8
510
+ self.number_of_inputs / 8
467
511
  ) # pre-calculate the number of returned coil value bytes
468
- response = self._parse_pdu(
469
- self._adapter.query(
470
- self._make_pdu(query),
471
- stop_conditions=Length(self._length(byte_count + 2)),
472
- )
473
- )
474
- self._raise_if_error(response, exception_codes=EXCEPTIONS)
475
- _, _, data = struct.unpack(ENDIAN + f"BB{byte_count}s", response)
476
- inputs = bytes_to_bool_list(data, number_of_inputs)
477
- return inputs
478
512
 
479
- # Read Holding Registers - 0x03
480
- def read_holding_registers(
481
- self, start_address: int, number_of_registers: int
482
- ) -> list[int]:
483
- """
484
- Reads a defined number of registers starting at a set address
513
+ _, _, data = struct.unpack(ENDIAN + f"BB{byte_count}s", sdu)
514
+ inputs = bytes_to_bool_list(data, self.number_of_inputs)
485
515
 
486
- Parameters
487
- ----------
488
- start_address : int
489
- number_of_registers : int
490
- 1 to 125
516
+ return self.Response(inputs)
491
517
 
492
- Returns
493
- -------
494
- registers : list
495
- """
496
- EXCEPTIONS = {
518
+
519
+ # Read discrete inputs - 0x03
520
+ @dataclass
521
+ class ReadHoldingRegisters(ModbusRequestSDU):
522
+ """Read holding registers request."""
523
+
524
+ start_address: int
525
+ number_of_registers: int
526
+
527
+ @dataclass
528
+ class Response(ModbusSDU):
529
+ """Response data."""
530
+
531
+ registers: list[int]
532
+
533
+ def function_code(self) -> FunctionCode:
534
+ return FunctionCode.READ_HOLDING_REGISTERS
535
+
536
+ def exceptions(self) -> dict[int, str]:
537
+ return {
497
538
  1: "Function code not supported",
498
539
  2: "Invalid Start or end addresses",
499
540
  3: "Invalid quantity of registers",
500
541
  4: "Couldn't read registers",
501
542
  }
502
- # Specification says 125, but the size would be exceeded over TCP. So 123 is safer
503
- MAX_NUMBER_OF_REGISTERS = (AVAILABLE_PDU_SIZE - 2) // 2
504
543
 
505
- assert (
506
- MIN_ADDRESS <= start_address <= MAX_ADDRESS - number_of_registers + 1
507
- ), f"Invalid start address : {start_address}"
544
+ def __post_init__(self) -> None:
545
+ _check_address(self.start_address, MAX_ADDRESS - self.number_of_registers + 1)
508
546
 
509
- assert (
510
- 1 <= number_of_registers <= MAX_NUMBER_OF_REGISTERS
511
- ), f"Invalid number of registers : {number_of_registers}"
512
- query = struct.pack(
513
- ENDIAN + "BHH",
514
- FunctionCode.READ_HOLDING_REGISTERS.value,
515
- self._dm_to_pdu_address(start_address),
516
- number_of_registers,
517
- )
518
- response = self._parse_pdu(
519
- self._adapter.query(
520
- self._make_pdu(query),
521
- stop_conditions=Length(self._length(2 + number_of_registers * 2)),
547
+ max_number_of_registers = (AVAILABLE_PDU_SIZE - 2) // 2
548
+
549
+ if not 1 <= self.number_of_registers <= max_number_of_registers:
550
+ raise ValueError(
551
+ f"Invalid number of registers : {self.number_of_registers}"
522
552
  )
553
+
554
+ def make_sdu(self) -> bytes:
555
+ sdu = struct.pack(
556
+ ENDIAN + "BHH",
557
+ self.function_code().value,
558
+ _dm_to_pdu_address(self.start_address),
559
+ self.number_of_registers,
523
560
  )
524
- self._raise_if_error(response, exception_codes=EXCEPTIONS)
561
+ return sdu
562
+
563
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
564
+ self._check_for_error(sdu)
565
+
525
566
  _, _, registers_data = struct.unpack(
526
- ENDIAN + f"BB{number_of_registers * 2}s", response
567
+ ENDIAN + f"BB{self.number_of_registers * 2}s", sdu
527
568
  )
528
569
  registers = list(
529
- struct.unpack(ENDIAN + "H" * number_of_registers, registers_data)
570
+ struct.unpack(ENDIAN + "H" * self.number_of_registers, registers_data)
530
571
  )
531
- return registers
532
572
 
533
- def read_multi_register_value(
534
- self,
535
- address: int,
536
- n_registers: int,
537
- value_type: str,
538
- byte_order: str = Endian.BIG.value,
539
- word_order: str = Endian.BIG.value,
540
- encoding: str = "utf-8",
541
- padding: int | None = 0,
542
- ) -> str | bytes | int | float:
543
- """
544
- Read an integer, a float, or a string over multiple registers
573
+ return self.Response(registers)
545
574
 
546
- Parameters
547
- ----------
548
- address : int
549
- Address of the first register
550
- n_registers : int
551
- Number of registers (half the number of bytes)
552
- value_type : str
553
- Type to which the value will be cast
554
- 'int' : signed integer
555
- 'uint' : unsigned integer
556
- 'float' : float or double
557
- 'string' : string
558
- 'array' : Bytes array
559
- Each type will be adapted based on the number of bytes (_bytes parameter)
560
- byte_order : str
561
- Byte order, 'big' means the high bytes will come first, 'little' means the low bytes will come first
562
- Byte order inside a register (2 bytes) is always big as per Modbus specification (4.2 Data Encoding)
563
- encoding : str
564
- String encoding (if used). UTF-8 by default
565
- padding : int | None
566
- String padding, None to return the raw string
567
- Returns
568
- -------
569
- data : any
570
- """
571
- type_cast = TypeCast(value_type)
572
- _byte_order = Endian(byte_order)
573
- if type_cast.is_number():
574
- _word_order = Endian(word_order)
575
- else:
576
- _word_order = Endian.BIG
577
- # Read N registers
578
- registers = self.read_holding_registers(
579
- start_address=address, number_of_registers=n_registers
580
- )
581
- # Create a buffer
582
- to_bytes_endian = Endian.BIG if _byte_order == _word_order else Endian.LITTLE
583
- buffer = b"".join(
584
- [x.to_bytes(2, byteorder=to_bytes_endian.value) for x in registers]
585
- )
586
- # Use struct_format to convert to the corresponding value directly
587
- # Swap the buffer accordingly
588
- data: bytes | int | float = struct.unpack(
589
- endian_symbol[_word_order] + struct_format(type_cast, n_registers * 2),
590
- buffer,
591
- )[0]
592
575
 
593
- # If data is a string, do additionnal processing
594
- output: bytes | int | float | str
595
- if type_cast == TypeCast.STRING:
596
- data = cast(bytes, data)
597
- # If null termination is enabled, remove any \0
598
- if padding is not None and padding in data:
599
- data = data[: data.index(padding)]
600
- # Cast
601
- output = data.decode(encoding)
602
- else:
603
- output = data
576
+ # Write Single coil - 0x05
577
+ @dataclass
578
+ class WriteSingleCoilSDU(ModbusRequestSDU):
579
+ """Write single coil request."""
604
580
 
605
- return output
581
+ address: int
582
+ status: bool
606
583
 
607
- def write_multi_register_value(
608
- self,
609
- address: int,
610
- n_registers: int,
611
- value_type: str,
612
- value: str | bytes | int | float,
613
- byte_order: str = Endian.BIG.value,
614
- word_order: str = Endian.BIG.value,
615
- encoding: str = "utf-8",
616
- padding: int = 0,
617
- ) -> None:
618
- """
619
- Write an integer, a float, or a string over multiple registers
584
+ _ON_VALUE = 0xFF00
585
+ _OFF_VALUE = 0x0000
586
+
587
+ @dataclass
588
+ class Response(ModbusSDU):
589
+ """Response data."""
590
+
591
+ def function_code(self) -> FunctionCode:
592
+ return FunctionCode.WRITE_SINGLE_COIL
593
+
594
+ def exceptions(self) -> dict[int, str]:
595
+ return {
596
+ 1: "Function code not supported",
597
+ 2: "Invalid address",
598
+ 3: "Invalid value",
599
+ 4: "Couldn't set coil output",
600
+ }
601
+
602
+ def __post_init__(self) -> None:
603
+ _check_address(self.address)
604
+
605
+ def make_sdu(self) -> bytes:
606
+ sdu = struct.pack(
607
+ ENDIAN + "BHH",
608
+ FunctionCode.WRITE_SINGLE_COIL.value,
609
+ _dm_to_pdu_address(self.address),
610
+ self._ON_VALUE if self.status else self._OFF_VALUE,
611
+ )
612
+ return sdu
613
+
614
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
615
+ self._check_for_error(sdu)
616
+ return self.Response()
617
+
618
+
619
+ @dataclass
620
+ class ReadInputRegistersSDU(ModbusRequestSDU):
621
+ """Read input registers request."""
622
+
623
+ start_address: int
624
+ number_of_registers: int
625
+
626
+ @dataclass
627
+ class Response(ModbusSDU):
628
+ """Response data."""
629
+
630
+ registers: list[int]
631
+
632
+ def function_code(self) -> FunctionCode:
633
+ return FunctionCode.READ_INPUT_REGISTERS
634
+
635
+ def exceptions(self) -> dict[int, str]:
636
+ return {
637
+ 1: "Function code not supported",
638
+ 2: "Invalid Start or end addresses",
639
+ 3: "Invalid quantity of registers",
640
+ 4: "Couldn't read registers",
641
+ }
642
+
643
+ def __post_init__(self) -> None:
644
+ _check_address(self.start_address, MAX_ADDRESS - self.number_of_registers + 1)
645
+
646
+ max_number_of_registers = (AVAILABLE_PDU_SIZE - 2) // 2
647
+
648
+ if not 1 <= self.number_of_registers <= max_number_of_registers:
649
+ raise ValueError(
650
+ f"Invalid number of registers : {self.number_of_registers}"
651
+ )
652
+
653
+ def make_sdu(self) -> bytes:
654
+ sdu = struct.pack(
655
+ ENDIAN + "BHH",
656
+ self.function_code().value,
657
+ _dm_to_pdu_address(self.start_address),
658
+ self.number_of_registers,
659
+ )
660
+ return sdu
661
+
662
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
663
+ self._check_for_error(sdu)
664
+
665
+ _, _, registers_data = struct.unpack(
666
+ ENDIAN + f"BB{self.number_of_registers * 2}s", sdu
667
+ )
668
+ registers = list(
669
+ struct.unpack(ENDIAN + "H" * self.number_of_registers, registers_data)
670
+ )
671
+
672
+ return self.Response(registers)
673
+
674
+
675
+ @dataclass
676
+ class WriteSingleRegisterSDU(ModbusRequestSDU):
677
+ """Write single register request."""
678
+
679
+ address: int
680
+ value: int
681
+
682
+ @dataclass
683
+ class Response(ModbusSDU):
684
+ """Response data."""
685
+
686
+ def function_code(self) -> FunctionCode:
687
+ return FunctionCode.WRITE_SINGLE_REGISTER
688
+
689
+ def exceptions(self) -> dict[int, str]:
690
+ return {
691
+ 1: "Function code not supported",
692
+ 2: "Invalid address",
693
+ 3: "Invalid register value",
694
+ 4: "Couldn't write register",
695
+ }
696
+
697
+ def __post_init__(self) -> None:
698
+ _check_address(self.address)
699
+ if not 0 <= self.value <= 0xFFFF:
700
+ raise ValueError(f"Invalid register value : {self.value}")
701
+
702
+ def make_sdu(self) -> bytes:
703
+ sdu = struct.pack(
704
+ ENDIAN + "BHH",
705
+ self.function_code().value,
706
+ _dm_to_pdu_address(self.address),
707
+ self.value,
708
+ )
709
+ return sdu
710
+
711
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
712
+ self._check_for_error(sdu)
713
+ if sdu != self.make_sdu():
714
+ raise ProtocolReadError(
715
+ f"Response ({sdu!r}) should match query ({self.make_sdu()!r})"
716
+ )
717
+ return self.Response()
718
+
719
+
720
+ @dataclass
721
+ class ReadExceptionStatusSDU(SerialLineOnlySDU):
722
+ """Read exception status request (serial only)."""
723
+
724
+ @dataclass
725
+ class Response(ModbusSDU):
726
+ """Response data."""
727
+
728
+ status: int
729
+
730
+ def function_code(self) -> FunctionCode:
731
+ return FunctionCode.READ_EXCEPTION_STATUS
732
+
733
+ def exceptions(self) -> dict[int, str]:
734
+ return {
735
+ 1: "Function code not supported",
736
+ 4: "Couldn't read exception status",
737
+ }
738
+
739
+ def make_sdu(self) -> bytes:
740
+ return struct.pack(ENDIAN + "B", self.function_code().value)
741
+
742
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
743
+ self._check_for_error(sdu)
744
+ _, status = cast(tuple[int, int], struct.unpack(ENDIAN + "BB", sdu))
745
+ return self.Response(status)
746
+
747
+
748
+ @dataclass
749
+ class DiagnosticsSDU(SerialLineOnlySDU):
750
+ """Diagnostics request (serial only)."""
751
+
752
+ code: DiagnosticsCode
753
+ subfunction_data: bytes
754
+ return_subfunction_bytes: int
755
+ check_response: bool = True
756
+
757
+ @dataclass
758
+ class Response(ModbusSDU):
759
+ """Response data."""
760
+
761
+ data: bytes
762
+
763
+ def function_code(self) -> FunctionCode:
764
+ return FunctionCode.DIAGNOSTICS
765
+
766
+ def exceptions(self) -> dict[int, str]:
767
+ return {
768
+ 1: "Unsuported function code or sub-function code",
769
+ 3: "Invalid data value",
770
+ 4: "Diagnostic error",
771
+ }
772
+
773
+ def make_sdu(self) -> bytes:
774
+ return (
775
+ struct.pack(ENDIAN + "BH", self.function_code().value, self.code.value)
776
+ + self.subfunction_data
777
+ )
778
+
779
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
780
+ self._check_for_error(sdu)
781
+
782
+ if self.return_subfunction_bytes == 0:
783
+ returned_function, returned_subfunction_integer = struct.unpack(
784
+ ENDIAN + "BH", sdu
785
+ )
786
+ subfunction_returned_data = b""
787
+ else:
788
+ (
789
+ returned_function,
790
+ returned_subfunction_integer,
791
+ subfunction_returned_data,
792
+ ) = cast(
793
+ tuple[int, int, bytes],
794
+ struct.unpack(ENDIAN + f"BH{self.return_subfunction_bytes}s", sdu),
795
+ )
796
+
797
+ if self.check_response:
798
+ returned_subfunction = DiagnosticsCode(returned_subfunction_integer)
799
+
800
+ if returned_function != self.function_code().value:
801
+ raise ProtocolReadError(
802
+ f"Invalid returned function code : {returned_function}"
803
+ )
804
+ if returned_subfunction != self.code:
805
+ raise ProtocolReadError(
806
+ f"Invalid returned subfunction code : {returned_subfunction}"
807
+ )
808
+
809
+ return self.Response(subfunction_returned_data)
810
+
811
+
812
+ @dataclass
813
+ class GetCommEventCounterSDU(SerialLineOnlySDU):
814
+ """Get communication event counter request (serial only)."""
815
+
816
+ @dataclass
817
+ class Response(ModbusSDU):
818
+ """Response data."""
819
+
820
+ status: int
821
+ event_count: int
822
+
823
+ def function_code(self) -> FunctionCode:
824
+ return FunctionCode.GET_COMM_EVENT_COUNTER
825
+
826
+ def exceptions(self) -> dict[int, str]:
827
+ return {
828
+ 1: "Function code not supported",
829
+ 4: "Couldn't get comm event counter",
830
+ }
831
+
832
+ def make_sdu(self) -> bytes:
833
+ return struct.pack(ENDIAN + "B", self.function_code().value)
834
+
835
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
836
+ self._check_for_error(sdu)
837
+ _, status, event_count = struct.unpack(ENDIAN + "BHH", sdu)
838
+ return self.Response(status, event_count)
839
+
840
+
841
+ @dataclass
842
+ class GetCommEventLogSDU(SerialLineOnlySDU):
843
+ """Get communication event log request (serial only)."""
844
+
845
+ @dataclass
846
+ class Response(ModbusSDU):
847
+ """Response data."""
848
+
849
+ status: int
850
+ event_count: int
851
+ message_count: int
852
+ events: bytes
853
+
854
+ def function_code(self) -> FunctionCode:
855
+ return FunctionCode.GET_COMM_EVENT_LOG
856
+
857
+ def exceptions(self) -> dict[int, str]:
858
+ return {
859
+ 1: "Function code not supported",
860
+ 4: "Couldn't get comm event log",
861
+ }
862
+
863
+ def make_sdu(self) -> bytes:
864
+ return struct.pack(ENDIAN + "B", self.function_code().value)
865
+
866
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
867
+ self._check_for_error(sdu)
868
+ _, byte_count = struct.unpack(ENDIAN + "BB", sdu[:2])
869
+ status, event_count, message_count = struct.unpack(ENDIAN + "HHH", sdu[2:8])
870
+ events = sdu[8 : 8 + (byte_count - 6)]
871
+ return self.Response(status, event_count, message_count, events)
872
+
873
+
874
+ @dataclass
875
+ class WriteMultipleCoilsSDU(ModbusRequestSDU):
876
+ """Write multiple coils request."""
877
+
878
+ start_address: int
879
+ values: list[bool]
880
+
881
+ @dataclass
882
+ class Response(ModbusSDU):
883
+ """Response data."""
884
+
885
+ def function_code(self) -> FunctionCode:
886
+ return FunctionCode.WRITE_MULTIPLE_COILS
887
+
888
+ def exceptions(self) -> dict[int, str]:
889
+ return {
890
+ 1: "Function code not supported",
891
+ 2: "Invalid start and/or end addresses",
892
+ 3: "Invalid number of outputs and/or byte count",
893
+ 4: "Couldn't write outputs",
894
+ }
895
+
896
+ def __post_init__(self) -> None:
897
+ number_of_coils = len(self.values)
898
+ _check_discrete_input_output_count(number_of_coils)
899
+ _check_address(self.start_address, MAX_ADDRESS - number_of_coils + 1)
900
+
901
+ def make_sdu(self) -> bytes:
902
+ number_of_coils = len(self.values)
903
+ byte_count = ceil(number_of_coils / 8)
904
+ sdu = struct.pack(
905
+ ENDIAN + f"BHHB{byte_count}s",
906
+ self.function_code().value,
907
+ _dm_to_pdu_address(self.start_address),
908
+ number_of_coils,
909
+ byte_count,
910
+ bool_list_to_bytes(self.values),
911
+ )
912
+ return sdu
913
+
914
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
915
+ self._check_for_error(sdu)
916
+
917
+ _, start_address, coils_written = struct.unpack(ENDIAN + "BHH", sdu)
918
+ if coils_written != len(self.values):
919
+ raise ProtocolError(
920
+ f"Number of coils written ({coils_written}) doesn't match expected "
921
+ f"value : {len(self.values)}"
922
+ )
923
+ if start_address != _dm_to_pdu_address(self.start_address):
924
+ raise ProtocolReadError(
925
+ f"Start address mismatch : {start_address} != "
926
+ "{_dm_to_pdu_address(self.start_address)}"
927
+ )
928
+ return self.Response()
929
+
930
+
931
+ @dataclass
932
+ class WriteMultipleRegistersSDU(ModbusRequestSDU):
933
+ """Write multiple registers request."""
934
+
935
+ start_address: int
936
+ values: list[int]
937
+
938
+ @dataclass
939
+ class Response(ModbusSDU):
940
+ """Response data."""
941
+
942
+ def function_code(self) -> FunctionCode:
943
+ return FunctionCode.WRITE_MULTIPLE_REGISTERS
944
+
945
+ def exceptions(self) -> dict[int, str]:
946
+ return {
947
+ 1: "Function code not supported",
948
+ 2: "Invalid start and/or end addresses",
949
+ 3: "Invalid number of outputs and/or byte count",
950
+ 4: "Couldn't write outputs",
951
+ }
952
+
953
+ def __post_init__(self) -> None:
954
+ max_number_of_registers = (AVAILABLE_PDU_SIZE - 6) // 2
955
+
956
+ if len(self.values) == 0:
957
+ raise ValueError("Empty register list")
958
+
959
+ if len(self.values) > max_number_of_registers:
960
+ raise ValueError(
961
+ f"Cannot set more than {max_number_of_registers} registers at a time"
962
+ )
963
+
964
+ _check_address(self.start_address, MAX_ADDRESS - len(self.values) + 1)
965
+
966
+ def make_sdu(self) -> bytes:
967
+ byte_count = 2 * len(self.values)
968
+ sdu = struct.pack(
969
+ ENDIAN + f"BHHB{byte_count // 2}H",
970
+ self.function_code().value,
971
+ _dm_to_pdu_address(self.start_address),
972
+ byte_count // 2,
973
+ byte_count,
974
+ *self.values,
975
+ )
976
+ return sdu
977
+
978
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
979
+ self._check_for_error(sdu)
980
+
981
+ _, start_address, registers_written = struct.unpack(ENDIAN + "BHH", sdu)
982
+ if registers_written != len(self.values):
983
+ raise ProtocolError(
984
+ f"Number of registers written ({registers_written}) doesn't match expected "
985
+ f"value : {len(self.values)}"
986
+ )
987
+ if start_address != _dm_to_pdu_address(self.start_address):
988
+ raise ProtocolReadError(
989
+ f"Start address mismatch : {start_address} != "
990
+ "{_dm_to_pdu_address(self.start_address)}"
991
+ )
992
+ return self.Response()
993
+
994
+
995
+ @dataclass
996
+ class ReportServerIdSDU(SerialLineOnlySDU):
997
+ """Report server ID request (serial only)."""
998
+
999
+ server_id_length: int
1000
+ additional_data_length: int
1001
+
1002
+ @dataclass
1003
+ class Response(ModbusSDU):
1004
+ """Response data."""
1005
+
1006
+ server_id: bytes
1007
+ run_indicator_status: bool
1008
+ additional_data: bytes
1009
+
1010
+ def function_code(self) -> FunctionCode:
1011
+ return FunctionCode.REPORT_SERVER_ID
1012
+
1013
+ def exceptions(self) -> dict[int, str]:
1014
+ return {
1015
+ 1: "Function code not supported",
1016
+ 4: "Couldn't report slave ID",
1017
+ }
1018
+
1019
+ def make_sdu(self) -> bytes:
1020
+ return struct.pack(ENDIAN + "B", self.function_code().value)
1021
+
1022
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
1023
+ self._check_for_error(sdu)
1024
+ _, byte_count = struct.unpack(ENDIAN + "BB", sdu[:2])
1025
+ expected = self.server_id_length + 1 + self.additional_data_length
1026
+ if byte_count != expected:
1027
+ raise ProtocolReadError(
1028
+ f"Invalid byte count : {byte_count}, expected {expected}"
1029
+ )
1030
+ start = 2
1031
+ end = start + self.server_id_length
1032
+ server_id = sdu[start:end]
1033
+ run_indicator_status = sdu[end] == 0xFF
1034
+ additional_data = sdu[end + 1 : end + 1 + self.additional_data_length]
1035
+ return self.Response(server_id, run_indicator_status, additional_data)
1036
+
1037
+
1038
+ @dataclass
1039
+ class ReadFileRecordSDU(ModbusRequestSDU):
1040
+ """Read file record request."""
1041
+
1042
+ records: list[tuple[int, int, int]]
1043
+
1044
+ @dataclass
1045
+ class Response(ModbusSDU):
1046
+ """Response data."""
1047
+
1048
+ records_data: list[bytes]
1049
+
1050
+ def function_code(self) -> FunctionCode:
1051
+ return FunctionCode.READ_FILE_RECORD
1052
+
1053
+ def exceptions(self) -> dict[int, str]:
1054
+ return {
1055
+ 1: "Function code not supported",
1056
+ 2: "Invalid parameters",
1057
+ 3: "Invalid byte count",
1058
+ 4: "Couldn't read records",
1059
+ }
1060
+
1061
+ def __post_init__(self) -> None:
1062
+ size_limit = 253
1063
+ query_size = 2 + 7 * len(self.records)
1064
+ response_size = 2 + len(self.records) + sum(2 * r[2] for r in self.records)
1065
+ if query_size > size_limit:
1066
+ raise ValueError(f"Number of records is too high : {len(self.records)}")
1067
+ if response_size > size_limit:
1068
+ raise ValueError(
1069
+ f"Sum of records lenghts is too high : {sum(r[2] for r in self.records)}"
1070
+ )
1071
+
1072
+ def make_sdu(self) -> bytes:
1073
+ reference_type = 6
1074
+ sub_req_buffer = b""
1075
+ for file_number, record_number, record_length in self.records:
1076
+ sub_req_buffer += struct.pack(
1077
+ ENDIAN + "BHHH",
1078
+ reference_type,
1079
+ file_number,
1080
+ record_number,
1081
+ record_length,
1082
+ )
1083
+
1084
+ byte_count = len(sub_req_buffer)
1085
+ sdu = struct.pack(
1086
+ ENDIAN + f"BB{byte_count}s",
1087
+ self.function_code().value,
1088
+ byte_count,
1089
+ sub_req_buffer,
1090
+ )
1091
+ return sdu
1092
+
1093
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
1094
+ self._check_for_error(sdu)
1095
+ _, byte_count = struct.unpack(ENDIAN + "BB", sdu[:2])
1096
+ records_data: list[bytes] = []
1097
+ offset = 2
1098
+ end = 2 + byte_count
1099
+ while offset < end:
1100
+ length = sdu[offset]
1101
+ reference_type = sdu[offset + 1]
1102
+ if reference_type != 6:
1103
+ raise ProtocolReadError(f"Invalid reference type : {reference_type}")
1104
+ data_start = offset + 2
1105
+ data_end = data_start + length - 1
1106
+ records_data.append(sdu[data_start:data_end])
1107
+ offset = data_end
1108
+
1109
+ return self.Response(records_data)
1110
+
1111
+
1112
+ @dataclass
1113
+ class WriteFileRecordSDU(ModbusRequestSDU):
1114
+ """Write file record request."""
1115
+
1116
+ records: list[tuple[int, int, bytes]]
1117
+
1118
+ @dataclass
1119
+ class Response(ModbusSDU):
1120
+ """Response data."""
1121
+
1122
+ def function_code(self) -> FunctionCode:
1123
+ return FunctionCode.WRITE_FILE_RECORD
1124
+
1125
+ def exceptions(self) -> dict[int, str]:
1126
+ return {
1127
+ 1: "Function code not supported",
1128
+ 2: "Invalid parameters",
1129
+ 3: "Invalid byte count",
1130
+ 4: "Couldn't write records",
1131
+ }
1132
+
1133
+ def __post_init__(self) -> None:
1134
+ if isinstance(self.records, tuple):
1135
+ self.records = [self.records]
1136
+ elif not isinstance(self.records, list):
1137
+ raise TypeError(f"Invalid records type : {self.records}")
1138
+
1139
+ def make_sdu(self) -> bytes:
1140
+ reference_type = 6
1141
+ sub_req_buffer = b""
1142
+
1143
+ for file_number, record_number, data in self.records:
1144
+ sub_req_buffer += struct.pack(
1145
+ ENDIAN + f"BHHH{len(data)}s",
1146
+ reference_type,
1147
+ file_number,
1148
+ record_number,
1149
+ len(data) // 2,
1150
+ data,
1151
+ )
1152
+
1153
+ sdu = struct.pack(
1154
+ ENDIAN + f"BB{len(sub_req_buffer)}s",
1155
+ self.function_code().value,
1156
+ len(sub_req_buffer),
1157
+ sub_req_buffer,
1158
+ )
1159
+ return sdu
1160
+
1161
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
1162
+ self._check_for_error(sdu)
1163
+ if sdu != self.make_sdu():
1164
+ raise ProtocolReadError("Response different from query")
1165
+ return self.Response()
1166
+
1167
+
1168
+ @dataclass
1169
+ class MaskWriteRegisterSDU(ModbusRequestSDU):
1170
+ """Mask write register request."""
1171
+
1172
+ address: int
1173
+ and_mask: int
1174
+ or_mask: int
1175
+
1176
+ @dataclass
1177
+ class Response(ModbusSDU):
1178
+ """Response data."""
1179
+
1180
+ def function_code(self) -> FunctionCode:
1181
+ return FunctionCode.MASK_WRITE_REGISTER
1182
+
1183
+ def exceptions(self) -> dict[int, str]:
1184
+ return {
1185
+ 1: "Function code not supported",
1186
+ 2: "Invalid register address",
1187
+ 3: "Invalid AND/OR mask",
1188
+ 4: "Couldn't write register",
1189
+ }
1190
+
1191
+ def __post_init__(self) -> None:
1192
+ _check_address(self.address)
1193
+
1194
+ def make_sdu(self) -> bytes:
1195
+ sdu = struct.pack(
1196
+ ENDIAN + "BHHH",
1197
+ self.function_code().value,
1198
+ _dm_to_pdu_address(self.address),
1199
+ self.and_mask,
1200
+ self.or_mask,
1201
+ )
1202
+ return sdu
1203
+
1204
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
1205
+ self._check_for_error(sdu)
1206
+ if sdu != self.make_sdu():
1207
+ raise ProtocolReadError(
1208
+ f"Response ({sdu!r}) should match query ({self.make_sdu()!r})"
1209
+ )
1210
+ return self.Response()
1211
+
1212
+
1213
+ @dataclass
1214
+ class ReadWriteMultipleRegistersSDU(ModbusRequestSDU):
1215
+ """Read/write multiple registers request."""
1216
+
1217
+ read_starting_address: int
1218
+ number_of_read_registers: int
1219
+ write_starting_address: int
1220
+ write_values: list[int]
1221
+
1222
+ @dataclass
1223
+ class Response(ModbusSDU):
1224
+ """Response data."""
1225
+
1226
+ read_values: list[int]
1227
+
1228
+ def function_code(self) -> FunctionCode:
1229
+ return FunctionCode.READ_WRITE_MULTIPLE_REGISTERS
1230
+
1231
+ def exceptions(self) -> dict[int, str]:
1232
+ return {
1233
+ 1: "Function code not supported",
1234
+ 2: "Invalid read/write start/end address",
1235
+ 3: "Invalid quantity of read/write and/or byte count",
1236
+ 4: "Couldn't read and/or write registers",
1237
+ }
1238
+
1239
+ def __post_init__(self) -> None:
1240
+ max_number_read_registers = (AVAILABLE_PDU_SIZE - 2) // 2
1241
+ max_numbers_write_registers = (AVAILABLE_PDU_SIZE - 10) // 2
1242
+
1243
+ if not 1 <= self.number_of_read_registers <= max_number_read_registers:
1244
+ raise ValueError(
1245
+ f"Invalid number of read registers : {self.number_of_read_registers}"
1246
+ )
1247
+
1248
+ if not 1 <= len(self.write_values) <= max_numbers_write_registers:
1249
+ raise ValueError(
1250
+ f"Invalid number of write registers : {self.number_of_read_registers}"
1251
+ )
1252
+
1253
+ _check_address(
1254
+ self.read_starting_address,
1255
+ MAX_ADDRESS - self.number_of_read_registers + 1,
1256
+ )
1257
+ _check_address(
1258
+ self.write_starting_address, MAX_ADDRESS - len(self.write_values) + 1
1259
+ )
1260
+
1261
+ def make_sdu(self) -> bytes:
1262
+ sdu = struct.pack(
1263
+ ENDIAN + f"BHHHHB{len(self.write_values)}H",
1264
+ self.function_code().value,
1265
+ _dm_to_pdu_address(self.read_starting_address),
1266
+ self.number_of_read_registers,
1267
+ _dm_to_pdu_address(self.write_starting_address),
1268
+ len(self.write_values),
1269
+ len(self.write_values) * 2,
1270
+ *self.write_values,
1271
+ )
1272
+ return sdu
1273
+
1274
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
1275
+ self._check_for_error(sdu)
1276
+ _, byte_count = struct.unpack(ENDIAN + "BB", sdu[:2])
1277
+ expected = self.number_of_read_registers * 2
1278
+ if byte_count != expected:
1279
+ raise ProtocolReadError(
1280
+ f"Invalid byte count : {byte_count}, expected {expected}"
1281
+ )
1282
+ read_values = list(
1283
+ struct.unpack(ENDIAN + f"{self.number_of_read_registers}H", sdu[2:])
1284
+ )
1285
+ return self.Response(read_values)
1286
+
1287
+
1288
+ @dataclass
1289
+ class ReadFifoQueueSDU(ModbusRequestSDU):
1290
+ """Read FIFO queue request."""
1291
+
1292
+ fifo_address: int
1293
+
1294
+ @dataclass
1295
+ class Response(ModbusSDU):
1296
+ """Response data."""
1297
+
1298
+ values: list[int]
1299
+
1300
+ def function_code(self) -> FunctionCode:
1301
+ return FunctionCode.READ_FIFO_QUEUE
1302
+
1303
+ def exceptions(self) -> dict[int, str]:
1304
+ return {
1305
+ 1: "Function code not supported",
1306
+ 2: "Invalid FIFO address",
1307
+ 3: "Invalid FIFO count (>31)",
1308
+ 4: "Couldn't read FIFO queue",
1309
+ }
1310
+
1311
+ def __post_init__(self) -> None:
1312
+ _check_address(self.fifo_address)
1313
+
1314
+ def make_sdu(self) -> bytes:
1315
+ return struct.pack(
1316
+ ENDIAN + "BH",
1317
+ self.function_code().value,
1318
+ _dm_to_pdu_address(self.fifo_address),
1319
+ )
1320
+
1321
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
1322
+ self._check_for_error(sdu)
1323
+ _, byte_count = struct.unpack(ENDIAN + "BH", sdu[:3])
1324
+ fifo_count = struct.unpack(ENDIAN + "H", sdu[3:5])[0]
1325
+ register_count = byte_count // 2 - 1
1326
+ if fifo_count != register_count:
1327
+ raise ProtocolReadError(
1328
+ f"FIFO count mismatch : {fifo_count} != {register_count}"
1329
+ )
1330
+ values = list(struct.unpack(ENDIAN + f"{register_count}H", sdu[5:]))
1331
+ return self.Response(values)
1332
+
1333
+
1334
+ @dataclass
1335
+ class EncapsulatedInterfaceTransportSDU(ModbusRequestSDU):
1336
+ """Encapsulated interface transport request."""
1337
+
1338
+ mei_type: int
1339
+ mei_data: bytes
1340
+ extra_exceptions: dict[int, str] | None = None
1341
+
1342
+ @dataclass
1343
+ class Response(ModbusSDU):
1344
+ """Response data."""
1345
+
1346
+ data: bytes
1347
+
1348
+ def function_code(self) -> FunctionCode:
1349
+ return FunctionCode.ENCAPSULATED_INTERFACE_TRANSPORT
1350
+
1351
+ def exceptions(self) -> dict[int, str]:
1352
+ exceptions = {1: "Function code not supported"}
1353
+ if self.extra_exceptions is not None:
1354
+ exceptions.update(self.extra_exceptions)
1355
+ return exceptions
1356
+
1357
+ def make_sdu(self) -> bytes:
1358
+ return struct.pack(
1359
+ ENDIAN + f"BB{len(self.mei_data)}s",
1360
+ self.function_code().value,
1361
+ self.mei_type,
1362
+ self.mei_data,
1363
+ )
1364
+
1365
+ def parse_sdu(self, sdu: bytes) -> ModbusSDU:
1366
+ self._check_for_error(sdu)
1367
+ return self.Response(sdu[2:])
1368
+
1369
+
1370
+ class ModbusFrame(ProtocolFrame[ModbusSDU]):
1371
+ """Modbus frame containing a ModbusSDU"""
1372
+
1373
+ payload: ModbusSDU
1374
+
1375
+ def __str__(self) -> str:
1376
+ return f"ModbusFrame({self.payload})"
1377
+
1378
+
1379
+ # pylint: disable=too-many-public-methods
1380
+ class Modbus(Protocol[ModbusSDU]):
1381
+ """
1382
+ Modbus protocol
1383
+
1384
+ Parameters
1385
+ ----------
1386
+ adapter : Adapter
1387
+ SerialPort or IP
1388
+ timeout : Timeout
1389
+ _type : str
1390
+ Only used with SerialPort adapter
1391
+ 'RTU' : Modbus RTU (default)
1392
+ 'ASCII' : Modbus ASCII
1393
+ """
1394
+
1395
+ def __init__(
1396
+ self,
1397
+ adapter: Adapter,
1398
+ timeout: Timeout | None | EllipsisType = ...,
1399
+ _type: str = ModbusType.RTU.value,
1400
+ slave_address: int | None = None,
1401
+ ) -> None:
1402
+ super().__init__(adapter, timeout)
1403
+ self._logger.debug("Initializing Modbus protocol...")
1404
+
1405
+ if isinstance(adapter, IP):
1406
+ self._adapter: IP
1407
+ self._adapter.set_default_port(MODBUS_TCP_DEFAULT_PORT)
1408
+ self._modbus_type = ModbusType.TCP
1409
+ elif isinstance(adapter, SerialPort):
1410
+ self._modbus_type = ModbusType(_type)
1411
+ if slave_address is None:
1412
+ raise ValueError("slave_address must be set")
1413
+ raise NotImplementedError("Serialport (Modbus RTU) is not supported yet")
1414
+ else:
1415
+ raise ValueError("Invalid adapter")
1416
+
1417
+ self._slave_address = slave_address
1418
+
1419
+ self._last_sdu: ModbusSDU | None = None
1420
+ self._transaction_id = 0
1421
+
1422
+ def _default_timeout(self) -> Timeout | None:
1423
+ return Timeout(response=1, action="error")
1424
+
1425
+ def _on_event(self, event: AdapterEvent) -> None: ...
1426
+
1427
+ def _protocol_to_adapter(self, protocol_payload: ModbusSDU) -> bytes:
1428
+ if isinstance(protocol_payload, SerialLineOnlySDU):
1429
+ if self._modbus_type == ModbusType.TCP:
1430
+ raise ProtocolError("This function cannot be used with Modbus TCP")
1431
+
1432
+ sdu = protocol_payload.make_sdu()
1433
+
1434
+ if self._modbus_type == ModbusType.TCP:
1435
+ # Return raw data
1436
+ length = len(sdu) + 1 # unit_id is included
1437
+ output = (
1438
+ struct.pack(
1439
+ ENDIAN + "HHHB", self._transaction_id, _PROTOCO_ID, length, _UNIT_ID
1440
+ )
1441
+ + sdu
1442
+ )
1443
+ else:
1444
+ # Add slave address and error check
1445
+ error_check = modbus_crc(sdu)
1446
+ output = (
1447
+ struct.pack(ENDIAN + "B", self._slave_address)
1448
+ + sdu
1449
+ + struct.pack(ENDIAN + "H", error_check)
1450
+ )
1451
+ if self._modbus_type == ModbusType.ASCII:
1452
+ # Add header and trailer
1453
+ output = _ASCII_HEADER + output + _ASCII_TRAILER
1454
+
1455
+ self._transaction_id += 1
1456
+
1457
+ self._last_sdu = protocol_payload
1458
+ return output
1459
+
1460
+ def _adapter_to_protocol(
1461
+ self, adapter_frame: AdapterFrame
1462
+ ) -> ProtocolFrame[ModbusSDU]:
1463
+ pdu = adapter_frame.get_payload()
1464
+
1465
+ if self._modbus_type == ModbusType.TCP:
1466
+ # transaction_id, protocol_id, length, unit_id = struct.unpack(
1467
+ # "HHHB",
1468
+ # pdu[:7]
1469
+ # )
1470
+ data = pdu[7:]
1471
+ # len(data) should match length variable
1472
+ else:
1473
+ if self._modbus_type == ModbusType.ASCII:
1474
+ # Remove header and trailer
1475
+ pdu = pdu[len(_ASCII_HEADER) : -len(_ASCII_TRAILER)]
1476
+ # Remove slave address and CRC and check CRC
1477
+
1478
+ # slave_address = pdu[0]
1479
+ data = pdu[1:-2]
1480
+ # crc = pdu[-2:] # TODO : Check CRC
1481
+
1482
+ if self._last_sdu is None:
1483
+ raise ModbusError("Cannot read without prior write")
1484
+
1485
+ # It is necessary to know the previous SDU because some information cannot
1486
+ # be parsed from the response only (like the number of coils from a read_coils
1487
+ # command)
1488
+ sdu = self._last_sdu.parse_sdu(data)
1489
+
1490
+ return ModbusFrame(
1491
+ stop_timestamp=adapter_frame.stop_timestamp,
1492
+ stop_condition_type=adapter_frame.stop_condition_type,
1493
+ previous_read_buffer_used=adapter_frame.previous_read_buffer_used,
1494
+ response_delay=adapter_frame.response_delay,
1495
+ payload=sdu,
1496
+ )
1497
+
1498
+ # ┌────────────┐
1499
+ # │ Public API │
1500
+ # └────────────┘
1501
+
1502
+ # Read Coils - 0x01
1503
+ def read_coils(self, start_address: int, number_of_coils: int) -> list[bool]:
1504
+ """
1505
+ Read a defined number of coils starting at a set address
1506
+
1507
+ Parameters
1508
+ ----------
1509
+ start_address : int
1510
+ number_of_coils : int
1511
+
1512
+ Returns
1513
+ -------
1514
+ coils : list
1515
+ """
1516
+ payload = ReadCoilsSDU(
1517
+ start_address=start_address, number_of_coils=number_of_coils
1518
+ )
1519
+
1520
+ output = cast(ReadCoilsSDU.Response, self.query(payload))
1521
+
1522
+ return output.coils
1523
+
1524
+ async def aread_coils(self, start_address: int, number_of_coils: int) -> list[bool]:
1525
+ """
1526
+ Asynchronously read a defined number of coils starting at a set address
1527
+
1528
+ Parameters
1529
+ ----------
1530
+ start_address : int
1531
+ number_of_coils : int
1532
+
1533
+ Returns
1534
+ -------
1535
+ coils : list
1536
+ """
1537
+
1538
+ payload = ReadCoilsSDU(
1539
+ start_address=start_address, number_of_coils=number_of_coils
1540
+ )
1541
+
1542
+ output = cast(ReadCoilsSDU.Response, await self.aquery(payload))
1543
+
1544
+ return output.coils
1545
+
1546
+ # This is a wrapper for the read_coils method
1547
+ def read_single_coil(self, address: int) -> bool:
1548
+ """
1549
+ Read a single coil at a specified address.
1550
+ This is a wrapper for the read_coils method with the number of coils set to 1
1551
+
1552
+ Parameters
1553
+ ----------
1554
+ address : int
1555
+
1556
+ Returns
1557
+ -------
1558
+ coil : bool
1559
+ """
1560
+ coil = self.read_coils(start_address=address, number_of_coils=1)[0]
1561
+ return coil
1562
+
1563
+ async def aread_single_coil(self, address: int) -> bool:
1564
+ """
1565
+ Asynchronously read a single coil at a specified address.
1566
+ This is a wrapper for the read_coils method with the number of coils set to 1
1567
+
1568
+ Parameters
1569
+ ----------
1570
+ address : int
1571
+
1572
+ Returns
1573
+ -------
1574
+ coil : bool
1575
+ """
1576
+ coil = (await self.aread_coils(start_address=address, number_of_coils=1))[0]
1577
+ return coil
1578
+
1579
+ # Read Discrete inputs - 0x02
1580
+ def read_discrete_inputs(
1581
+ self, start_address: int, number_of_inputs: int
1582
+ ) -> list[bool]:
1583
+ """
1584
+ Read a defined number of discrete inputs at a set starting address
1585
+
1586
+ Parameters
1587
+ ----------
1588
+ start_address : int
1589
+ number_of_inputs : int
1590
+
1591
+ Returns
1592
+ -------
1593
+ inputs : list
1594
+ List of booleans
1595
+ """
1596
+
1597
+ payload = ReadDiscreteInputs(
1598
+ start_address=start_address, number_of_inputs=number_of_inputs
1599
+ )
1600
+
1601
+ output = cast(ReadDiscreteInputs.Response, self.query(payload))
1602
+
1603
+ return output.inputs
1604
+
1605
+ async def aread_discrete_inputs(
1606
+ self, start_address: int, number_of_inputs: int
1607
+ ) -> list[bool]:
1608
+ """
1609
+ Read a defined number of discrete inputs at a set starting address
1610
+
1611
+ Parameters
1612
+ ----------
1613
+ start_address : int
1614
+ number_of_inputs : int
1615
+
1616
+ Returns
1617
+ -------
1618
+ inputs : list
1619
+ List of booleans
1620
+ """
1621
+
1622
+ payload = ReadDiscreteInputs(
1623
+ start_address=start_address, number_of_inputs=number_of_inputs
1624
+ )
1625
+
1626
+ output = cast(ReadDiscreteInputs.Response, await self.aquery(payload))
1627
+
1628
+ return output.inputs
1629
+
1630
+ # Read Holding Registers - 0x03
1631
+ def read_holding_registers(
1632
+ self, start_address: int, number_of_registers: int
1633
+ ) -> list[int]:
1634
+ """
1635
+ Reads a defined number of registers starting at a set address
1636
+
1637
+ Parameters
1638
+ ----------
1639
+ start_address : int
1640
+ number_of_registers : int
1641
+ 1 to 125
1642
+
1643
+ Returns
1644
+ -------
1645
+ registers : list
1646
+ """
1647
+
1648
+ payload = ReadHoldingRegisters(
1649
+ start_address=start_address, number_of_registers=number_of_registers
1650
+ )
1651
+
1652
+ output = cast(ReadHoldingRegisters.Response, self.query(payload))
1653
+
1654
+ return output.registers
1655
+
1656
+ async def aread_holding_registers(
1657
+ self, start_address: int, number_of_registers: int
1658
+ ) -> list[int]:
1659
+ """
1660
+ Asynchronously Reads a defined number of registers starting at a set address
1661
+
1662
+ Parameters
1663
+ ----------
1664
+ start_address : int
1665
+ number_of_registers : int
1666
+ 1 to 125
1667
+
1668
+ Returns
1669
+ -------
1670
+ registers : list
1671
+ """
1672
+
1673
+ payload = ReadHoldingRegisters(
1674
+ start_address=start_address, number_of_registers=number_of_registers
1675
+ )
1676
+
1677
+ output = cast(ReadHoldingRegisters.Response, await self.aquery(payload))
1678
+
1679
+ return output.registers
1680
+
1681
+ def _parse_multi_register_value(
1682
+ self,
1683
+ n_registers: int,
1684
+ registers: list[int],
1685
+ value_type: str,
1686
+ *,
1687
+ byte_order: str = Endian.BIG.value,
1688
+ word_order: str = Endian.BIG.value,
1689
+ encoding: str = "utf-8",
1690
+ padding: int | None = 0,
1691
+ ) -> str | bytes | int | float:
1692
+
1693
+ type_cast = TypeCast(value_type)
1694
+ _byte_order = Endian(byte_order)
1695
+ if type_cast.is_number():
1696
+ _word_order = Endian(word_order)
1697
+ else:
1698
+ _word_order = Endian.BIG
1699
+ # Create a buffer
1700
+ to_bytes_endian = Endian.BIG if _byte_order == _word_order else Endian.LITTLE
1701
+ buffer = b"".join(
1702
+ [x.to_bytes(2, byteorder=to_bytes_endian.value) for x in registers]
1703
+ )
1704
+ # Use struct_format to convert to the corresponding value directly
1705
+ # Swap the buffer accordingly
1706
+ data: bytes | int | float = struct.unpack(
1707
+ endian_symbol[_word_order] + struct_format(type_cast, n_registers * 2),
1708
+ buffer,
1709
+ )[0]
1710
+
1711
+ # If data is a string, do additionnal processing
1712
+ output: bytes | int | float | str
1713
+ if type_cast == TypeCast.STRING:
1714
+ data = cast(bytes, data)
1715
+ # If null termination is enabled, remove any \0
1716
+ if padding is not None and padding in data:
1717
+ data = data[: data.index(padding)]
1718
+ # Cast
1719
+ output = data.decode(encoding)
1720
+ else:
1721
+ output = data
1722
+
1723
+ return output
1724
+
1725
+ def read_multi_register_value(
1726
+ self,
1727
+ address: int,
1728
+ n_registers: int,
1729
+ value_type: str,
1730
+ *,
1731
+ byte_order: str = Endian.BIG.value,
1732
+ word_order: str = Endian.BIG.value,
1733
+ encoding: str = "utf-8",
1734
+ padding: int | None = 0,
1735
+ ) -> str | bytes | int | float:
1736
+ """
1737
+ Read an integer, a float, or a string over multiple registers
1738
+
1739
+ Parameters
1740
+ ----------
1741
+ address : int
1742
+ Address of the first register
1743
+ n_registers : int
1744
+ Number of registers (half the number of bytes)
1745
+ value_type : str
1746
+ Type to which the value will be cast
1747
+ 'int' : signed integer
1748
+ 'uint' : unsigned integer
1749
+ 'float' : float or double
1750
+ 'string' : string
1751
+ 'array' : Bytes array
1752
+ Each type will be adapted based on the number of bytes (_bytes parameter)
1753
+ byte_order : str
1754
+ Byte order, 'big' means the high bytes will come first, 'little' means the low bytes
1755
+ will come first
1756
+ Byte order inside a register (2 bytes) is always big as per
1757
+ Modbus specification (4.2 Data Encoding)
1758
+ encoding : str
1759
+ String encoding (if used). UTF-8 by default
1760
+ padding : int | None
1761
+ String padding, None to return the raw string
1762
+ Returns
1763
+ -------
1764
+ data : any
1765
+ """
1766
+ # Read N registers
1767
+ registers = self.read_holding_registers(
1768
+ start_address=address, number_of_registers=n_registers
1769
+ )
1770
+
1771
+ return self._parse_multi_register_value(
1772
+ n_registers=n_registers,
1773
+ registers=registers,
1774
+ value_type=value_type,
1775
+ byte_order=byte_order,
1776
+ word_order=word_order,
1777
+ encoding=encoding,
1778
+ padding=padding,
1779
+ )
1780
+
1781
+ async def aread_multi_register_value(
1782
+ self,
1783
+ address: int,
1784
+ n_registers: int,
1785
+ value_type: str,
1786
+ *,
1787
+ byte_order: str = Endian.BIG.value,
1788
+ word_order: str = Endian.BIG.value,
1789
+ encoding: str = "utf-8",
1790
+ padding: int | None = 0,
1791
+ ) -> str | bytes | int | float:
1792
+ """
1793
+ Asynchronously read an integer, a float, or a string over multiple registers
620
1794
 
621
1795
  Parameters
622
1796
  ----------
@@ -624,26 +1798,53 @@ class Modbus(Protocol):
624
1798
  Address of the first register
625
1799
  n_registers : int
626
1800
  Number of registers (half the number of bytes)
627
- value_type : str | bytes | int
1801
+ value_type : str
628
1802
  Type to which the value will be cast
629
1803
  'int' : signed integer
630
1804
  'uint' : unsigned integer
631
1805
  'float' : float or double
632
1806
  'string' : string
633
1807
  'array' : Bytes array
634
- value : any
635
- The value to write, can be any of the following : str, int, float, str, bytearray
1808
+ Each type will be adapted based on the number of bytes (_bytes parameter)
636
1809
  byte_order : str
637
- Byte order, 'big' means the high bytes will come first, 'little' means the low bytes will come first
638
- Byte order inside a register (2 bytes) is always big as per Modbus specification (4.2 Data Encoding)
1810
+ Byte order, 'big' means the high bytes will come first, 'little' means the low bytes
1811
+ will come first
1812
+ Byte order inside a register (2 bytes) is always big as per
1813
+ Modbus specification (4.2 Data Encoding)
639
1814
  encoding : str
640
- String encoding (if used)
641
- padding : int
642
- Padding in case the value (str / bytes) is not long enough
1815
+ String encoding (if used). UTF-8 by default
1816
+ padding : int | None
1817
+ String padding, None to return the raw string
643
1818
  Returns
644
1819
  -------
645
1820
  data : any
646
1821
  """
1822
+ # Read N registers
1823
+ registers = await self.aread_holding_registers(
1824
+ start_address=address, number_of_registers=n_registers
1825
+ )
1826
+
1827
+ return self._parse_multi_register_value(
1828
+ n_registers=n_registers,
1829
+ registers=registers,
1830
+ value_type=value_type,
1831
+ byte_order=byte_order,
1832
+ word_order=word_order,
1833
+ encoding=encoding,
1834
+ padding=padding,
1835
+ )
1836
+
1837
+ def _make_multi_register_value(
1838
+ self,
1839
+ n_registers: int,
1840
+ value_type: str,
1841
+ value: str | bytes | int | float,
1842
+ *,
1843
+ byte_order: str = Endian.BIG.value,
1844
+ word_order: str = Endian.BIG.value,
1845
+ encoding: str = "utf-8",
1846
+ padding: int = 0,
1847
+ ) -> list[int]:
647
1848
  _type = TypeCast(value_type)
648
1849
  n_bytes = n_registers * 2
649
1850
  if _type.is_number():
@@ -653,7 +1854,7 @@ class Modbus(Protocol):
653
1854
  _byte_order = Endian(byte_order)
654
1855
 
655
1856
  array = b""
656
- if _type == TypeCast.INT or _type == TypeCast.UINT or _type == TypeCast.FLOAT:
1857
+ if _type in [TypeCast.INT, TypeCast.UINT, TypeCast.FLOAT]:
657
1858
  # Make one big array using word_order endian
658
1859
  array = struct.pack(
659
1860
  endian_symbol[_word_order] + struct_format(_type, n_bytes), value
@@ -693,8 +1894,121 @@ class Modbus(Protocol):
693
1894
  ]
694
1895
  for i in range(len(array) // 2)
695
1896
  ]
1897
+
1898
+ return registers
1899
+
1900
+ def write_multi_register_value(
1901
+ self,
1902
+ address: int,
1903
+ n_registers: int,
1904
+ value_type: str,
1905
+ value: str | bytes | int | float,
1906
+ *,
1907
+ byte_order: str = Endian.BIG.value,
1908
+ word_order: str = Endian.BIG.value,
1909
+ encoding: str = "utf-8",
1910
+ padding: int = 0,
1911
+ ) -> None:
1912
+ """
1913
+ Write an integer, a float, or a string over multiple registers
1914
+
1915
+ Parameters
1916
+ ----------
1917
+ address : int
1918
+ Address of the first register
1919
+ n_registers : int
1920
+ Number of registers (half the number of bytes)
1921
+ value_type : str | bytes | int
1922
+ Type to which the value will be cast
1923
+ 'int' : signed integer
1924
+ 'uint' : unsigned integer
1925
+ 'float' : float or double
1926
+ 'string' : string
1927
+ 'array' : Bytes array
1928
+ value : any
1929
+ The value to write, can be any of the following : str, int, float, str, bytearray
1930
+ byte_order : str
1931
+ Byte order, 'big' means the high bytes will come first, 'little' means the low
1932
+ bytes will come first
1933
+ Byte order inside a register (2 bytes) is always big as per
1934
+ Modbus specification (4.2 Data Encoding)
1935
+ encoding : str
1936
+ String encoding (if used)
1937
+ padding : int
1938
+ Padding in case the value (str / bytes) is not long enough
1939
+ Returns
1940
+ -------
1941
+ data : any
1942
+ """
1943
+
1944
+ registers = self._make_multi_register_value(
1945
+ n_registers=n_registers,
1946
+ value_type=value_type,
1947
+ value=value,
1948
+ byte_order=byte_order,
1949
+ word_order=word_order,
1950
+ encoding=encoding,
1951
+ padding=padding,
1952
+ )
1953
+
696
1954
  self.write_multiple_registers(start_address=address, values=registers)
697
1955
 
1956
+ async def awrite_multi_register_value(
1957
+ self,
1958
+ address: int,
1959
+ n_registers: int,
1960
+ value_type: str,
1961
+ value: str | bytes | int | float,
1962
+ *,
1963
+ byte_order: str = Endian.BIG.value,
1964
+ word_order: str = Endian.BIG.value,
1965
+ encoding: str = "utf-8",
1966
+ padding: int = 0,
1967
+ ) -> None:
1968
+ """
1969
+ Asynchronously write an integer, a float, or a string over multiple registers
1970
+
1971
+ Parameters
1972
+ ----------
1973
+ address : int
1974
+ Address of the first register
1975
+ n_registers : int
1976
+ Number of registers (half the number of bytes)
1977
+ value_type : str | bytes | int
1978
+ Type to which the value will be cast
1979
+ 'int' : signed integer
1980
+ 'uint' : unsigned integer
1981
+ 'float' : float or double
1982
+ 'string' : string
1983
+ 'array' : Bytes array
1984
+ value : any
1985
+ The value to write, can be any of the following : str, int, float, str, bytearray
1986
+ byte_order : str
1987
+ Byte order, 'big' means the high bytes will come first, 'little' means the low
1988
+ bytes will come first
1989
+ Byte order inside a register (2 bytes) is always big as per
1990
+ Modbus specification (4.2 Data Encoding)
1991
+ encoding : str
1992
+ String encoding (if used)
1993
+ padding : int
1994
+ Padding in case the value (str / bytes) is not long enough
1995
+ Returns
1996
+ -------
1997
+ data : any
1998
+ """
1999
+
2000
+ registers = self._make_multi_register_value(
2001
+ n_registers=n_registers,
2002
+ value_type=value_type,
2003
+ value=value,
2004
+ byte_order=byte_order,
2005
+ word_order=word_order,
2006
+ encoding=encoding,
2007
+ padding=padding,
2008
+ )
2009
+
2010
+ await self.awrite_multiple_registers(start_address=address, values=registers)
2011
+
698
2012
  # Read Input Registers - 0x04
699
2013
  def read_input_registers(
700
2014
  self, start_address: int, number_of_registers: int
@@ -713,72 +2027,67 @@ class Modbus(Protocol):
713
2027
  registers : list
714
2028
  List of integers
715
2029
  """
716
- # Same implementation as read_holding_registers
717
- EXCEPTIONS = {
718
- 1: "Function code not supported",
719
- 2: "Invalid Start or end addresses",
720
- 3: "Invalid quantity of registers",
721
- 4: "Couldn't read registers",
722
- }
2030
+ payload = ReadInputRegistersSDU(
2031
+ start_address=start_address,
2032
+ number_of_registers=number_of_registers,
2033
+ )
723
2034
 
724
- MAX_NUMBER_OF_REGISTERS = (AVAILABLE_PDU_SIZE - 2) // 2
2035
+ output = cast(ReadInputRegistersSDU.Response, self.query(payload))
725
2036
 
726
- assert (
727
- 1 <= number_of_registers <= MAX_NUMBER_OF_REGISTERS
728
- ), f"Invalid number of registers : {number_of_registers}"
729
- query = struct.pack(
730
- ENDIAN + "BHH",
731
- FunctionCode.READ_INPUT_REGISTERS.value,
732
- self._dm_to_pdu_address(start_address),
733
- number_of_registers,
734
- )
735
- response = self._parse_pdu(
736
- self._adapter.query(self._make_pdu(self._make_pdu(query)))
737
- )
738
- self._raise_if_error(response, exception_codes=EXCEPTIONS)
739
- _, _, registers_data = struct.unpack(
740
- ENDIAN + f"BB{number_of_registers * 2}", response
741
- )
742
- registers = list(
743
- struct.unpack(ENDIAN + "2s" * number_of_registers, registers_data)
2037
+ return output.registers
2038
+
2039
+ async def aread_input_registers(
2040
+ self, start_address: int, number_of_registers: int
2041
+ ) -> list[int]:
2042
+ """
2043
+ Asynchronously read a defined number of input registers starting at a set address
2044
+
2045
+ Parameters
2046
+ ----------
2047
+ start_address : int
2048
+ number_of_registers : int
2049
+ 1 to 125
2050
+
2051
+ Returns
2052
+ -------
2053
+ registers : list
2054
+ List of integers
2055
+ """
2056
+ payload = ReadInputRegistersSDU(
2057
+ start_address=start_address,
2058
+ number_of_registers=number_of_registers,
744
2059
  )
745
- return registers
2060
+
2061
+ output = cast(ReadInputRegistersSDU.Response, await self.aquery(payload))
2062
+
2063
+ return output.registers
746
2064
 
747
2065
  # Write Single coil - 0x05
748
- def write_single_coil(self, address: int, enabled: bool) -> None:
2066
+ def write_single_coil(self, address: int, status: bool) -> None:
749
2067
  """
750
2068
  Write a single output to either ON or OFF
751
2069
 
752
2070
  Parameters
753
2071
  ----------
754
2072
  address : int
755
- enabled : bool
2073
+ status : bool
756
2074
  """
757
- ON_VALUE = 0xFF00
758
- OFF_VALUE = 0x0000
759
- EXCEPTIONS = {
760
- 1: "Function code not supported",
761
- 2: "Invalid address",
762
- 3: "Invalid value",
763
- 4: "Couldn't set coil output",
764
- }
765
- assert MIN_ADDRESS <= address <= MAX_ADDRESS, f"Invalid address : {address}"
2075
+ payload = WriteSingleCoilSDU(address=address, status=status)
766
2076
 
767
- query = struct.pack(
768
- ENDIAN + "BHH",
769
- FunctionCode.WRITE_SINGLE_COIL.value,
770
- self._dm_to_pdu_address(address),
771
- ON_VALUE if enabled else OFF_VALUE,
772
- )
773
- response = self._parse_pdu(
774
- self._adapter.query(
775
- self._make_pdu(query), stop_conditions=Length(self._length(len(query)))
776
- )
777
- )
778
- self._raise_if_error(response, EXCEPTIONS)
779
- assert (
780
- query == response
781
- ), f"Write single coil response should match query {query!r} != {response!r}"
2077
+ self.query(payload)
2078
+
2079
+ async def awrite_single_coil(self, address: int, status: bool) -> None:
2080
+ """
2081
+ Asynchronously write a single output to either ON or OFF
2082
+
2083
+ Parameters
2084
+ ----------
2085
+ address : int
2086
+ status : bool
2087
+ """
2088
+ payload = WriteSingleCoilSDU(address=address, status=status)
2089
+
2090
+ await self.aquery(payload)
782
2091
 
783
2092
  # Write Single Register - 0x06
784
2093
  def write_single_register(self, address: int, value: int) -> None:
@@ -791,30 +2100,29 @@ class Modbus(Protocol):
791
2100
  value : int
792
2101
  value between 0x0000 and 0xFFFF
793
2102
  """
794
- EXCEPTIONS = {
795
- 1: "Function code not supported",
796
- 2: "Invalid address",
797
- 3: "Invalid register value",
798
- 4: "Couldn't write register",
799
- }
2103
+ payload = WriteSingleRegisterSDU(
2104
+ address=address,
2105
+ value=value,
2106
+ )
800
2107
 
801
- assert MIN_ADDRESS <= address <= MAX_ADDRESS, f"Invalid address : {address}"
2108
+ self.query(payload)
802
2109
 
803
- query = struct.pack(
804
- ENDIAN + "BHH",
805
- FunctionCode.WRITE_SINGLE_REGISTER.value,
806
- self._dm_to_pdu_address(address),
807
- value,
808
- )
809
- response = self._parse_pdu(
810
- self._adapter.query(
811
- self._make_pdu(query), stop_conditions=Length(self._length(len(query)))
812
- )
2110
+ async def awrite_single_register(self, address: int, value: int) -> None:
2111
+ """
2112
+ Asynchronously write a single register
2113
+
2114
+ Parameters
2115
+ ----------
2116
+ address : int
2117
+ value : int
2118
+ value between 0x0000 and 0xFFFF
2119
+ """
2120
+ payload = WriteSingleRegisterSDU(
2121
+ address=address,
2122
+ value=value,
813
2123
  )
814
- self._raise_if_error(response, EXCEPTIONS)
815
- assert (
816
- query == response
817
- ), f"Response ({response!r}) should match query ({query!r})"
2124
+
2125
+ await self.aquery(payload)
818
2126
 
819
2127
  def read_single_register(self, address: int) -> int:
820
2128
  """
@@ -831,7 +2139,7 @@ class Modbus(Protocol):
831
2139
  return self.read_holding_registers(address, 1)[0]
832
2140
 
833
2141
  # Read Exception Status - 0x07
834
- def read_exception_status(self) -> int: # TODO : Check the return type
2142
+ def read_exception_status(self) -> int:
835
2143
  """
836
2144
  Read exeption status
837
2145
 
@@ -839,75 +2147,56 @@ class Modbus(Protocol):
839
2147
  -------
840
2148
  exceptions : int
841
2149
  """
842
- EXCEPTIONS = {
843
- 1: "Function code not supported",
844
- 4: "Couldn't read exception status",
845
- }
846
- if self._modbus_type == ModbusType.TCP:
847
- raise RuntimeError("read_exception_status cannot be used with Modbus TCP")
848
- query = struct.pack(ENDIAN + "B", FunctionCode.READ_EXCEPTION_STATUS.value)
849
- response = self._parse_pdu(self._adapter.query(self._make_pdu(query)))
850
- self._raise_if_error(response, EXCEPTIONS)
851
- _, exception_status = cast(
852
- tuple[int, int], struct.unpack(ENDIAN + "BB", response)
853
- ) # TODO : Check this
854
- return exception_status
2150
+ payload = ReadExceptionStatusSDU()
2151
+
2152
+ output = cast(ReadExceptionStatusSDU.Response, self.query(payload))
2153
+
2154
+ return output.status
2155
+
2156
+ async def aread_exception_status(self) -> int:
2157
+ """
2158
+ Asynchronously read exeption status
2159
+
2160
+ Returns
2161
+ -------
2162
+ exceptions : int
2163
+ """
2164
+ payload = ReadExceptionStatusSDU()
2165
+
2166
+ output = cast(ReadExceptionStatusSDU.Response, await self.aquery(payload))
2167
+
2168
+ return output.status
855
2169
 
856
2170
  # Diagnostics - 0x08
857
- # One function for each to simplify parameters and returns
858
- def _diagnostics(
859
- self,
860
- code: DiagnosticsCode,
861
- subfunction_data: bytes,
862
- return_subfunction_bytes: int,
863
- check_response: bool = True,
864
- ) -> bytes:
2171
+ def diagnostics_return_query_data(self, data: int = 0x1234) -> bool:
865
2172
  """
866
- Diagnostics wrapper function
2173
+ Run "Return Query Data" diagnostic
2174
+
2175
+ A query is sent and should be return identical
867
2176
 
868
2177
  Parameters
869
2178
  ----------
870
- code : DiagnosticsCode
871
- check_response : bool
872
- Check response function code and subfunction code for equality with query
873
- True by default
2179
+ data : int
2180
+ data to send (16 bits integer)
874
2181
 
875
2182
  Returns
876
2183
  -------
877
- response
2184
+ success : bool
878
2185
  """
879
-
880
- EXCEPTIONS = {
881
- 1: "Unsuported function code or sub-function code",
882
- 3: "Invalid data value",
883
- 4: "Diagnostic error",
884
- }
885
-
886
- query = (
887
- struct.pack(ENDIAN + "BH", FunctionCode.DIAGNOSTICS.value, code)
888
- + subfunction_data
2186
+ subfunction_data = struct.pack(ENDIAN + "H", data)
2187
+ payload = DiagnosticsSDU(
2188
+ code=DiagnosticsCode.RETURN_QUERY_DATA,
2189
+ subfunction_data=subfunction_data,
2190
+ return_subfunction_bytes=2,
889
2191
  )
890
- response = self._parse_pdu(self._adapter.query(self._make_pdu(query)))
891
2192
 
892
- self._raise_if_error(response, EXCEPTIONS)
2193
+ output = cast(DiagnosticsSDU.Response, self.query(payload))
893
2194
 
894
- returned_function, returned_subfunction_integer, subfunction_returned_data = (
895
- cast(
896
- tuple[int, int, bytes],
897
- struct.unpack(ENDIAN + f"BH{return_subfunction_bytes}s", response),
898
- )
899
- )
900
- if check_response:
901
- returned_subfunction = DiagnosticsCode(returned_subfunction_integer)
902
- assert (
903
- returned_function == FunctionCode.DIAGNOSTICS.value
904
- ), f"Invalid returned function code : {returned_function}"
905
- assert returned_subfunction == code
906
- return subfunction_returned_data
2195
+ return output.data == subfunction_data
907
2196
 
908
- def diagnostics_return_query_data(self, data: int = 0x1234) -> bool:
2197
+ async def adiagnostics_return_query_data(self, data: int = 0x1234) -> bool:
909
2198
  """
910
- Run "Return Query Data" diagnostic
2199
+ Asynchronously run "Return Query Data" diagnostic
911
2200
 
912
2201
  A query is sent and should be return identical
913
2202
 
@@ -921,13 +2210,15 @@ class Modbus(Protocol):
921
2210
  success : bool
922
2211
  """
923
2212
  subfunction_data = struct.pack(ENDIAN + "H", data)
924
- try:
925
- response = self._diagnostics(
926
- DiagnosticsCode.RETURN_QUERY_DATA, subfunction_data, 2
927
- )
928
- return response == subfunction_data
929
- except AssertionError:
930
- return False
2213
+ payload = DiagnosticsSDU(
2214
+ code=DiagnosticsCode.RETURN_QUERY_DATA,
2215
+ subfunction_data=subfunction_data,
2216
+ return_subfunction_bytes=2,
2217
+ )
2218
+
2219
+ output = cast(DiagnosticsSDU.Response, await self.aquery(payload))
2220
+
2221
+ return output.data == subfunction_data
931
2222
 
932
2223
  # TODO : Check how this function interracts with Listen Only mode
933
2224
  def diagnostics_restart_communications_option(
@@ -944,41 +2235,121 @@ class Modbus(Protocol):
944
2235
  subfunction_data = struct.pack(
945
2236
  ENDIAN + "H", 0xFF00 if clear_communications_event_log else 0x0000
946
2237
  )
947
- self._diagnostics(
948
- DiagnosticsCode.RESTART_COMMUNICATIONS_OPTION, subfunction_data, 2
2238
+ payload = DiagnosticsSDU(
2239
+ code=DiagnosticsCode.RESTART_COMMUNICATIONS_OPTION,
2240
+ subfunction_data=subfunction_data,
2241
+ return_subfunction_bytes=2,
2242
+ )
2243
+
2244
+ self.query(payload)
2245
+
2246
+ async def adiagnostics_restart_communications_option(
2247
+ self, clear_communications_event_log: bool = False
2248
+ ) -> None:
2249
+ """
2250
+ Asynchronously initialize and restart serial line port.
2251
+ Brings the device out of Listen Only Mode
2252
+
2253
+ Parameters
2254
+ ----------
2255
+ clear_communications_event_log : bool
2256
+ False by default
2257
+ """
2258
+ subfunction_data = struct.pack(
2259
+ ENDIAN + "H", 0xFF00 if clear_communications_event_log else 0x0000
2260
+ )
2261
+ payload = DiagnosticsSDU(
2262
+ code=DiagnosticsCode.RESTART_COMMUNICATIONS_OPTION,
2263
+ subfunction_data=subfunction_data,
2264
+ return_subfunction_bytes=2,
949
2265
  )
950
2266
 
2267
+ await self.aquery(payload)
2268
+
951
2269
  def diagnostics_return_diagnostic_register(self) -> int:
952
2270
  """
953
2271
  Return 16 bit diagnostic register
954
2272
 
955
- Returns
956
- -------
957
- register : int
2273
+ Returns
2274
+ -------
2275
+ register : int
2276
+ """
2277
+ payload = DiagnosticsSDU(
2278
+ code=DiagnosticsCode.RETURN_DIAGNOSTIC_REGISTER,
2279
+ subfunction_data=b"\x00\x00",
2280
+ return_subfunction_bytes=2,
2281
+ )
2282
+
2283
+ output = cast(DiagnosticsSDU.Response, self.query(payload))
2284
+
2285
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
2286
+
2287
+ async def adiagnostics_return_diagnostic_register(self) -> int:
2288
+ """
2289
+ Asynchronously return 16 bit diagnostic register
2290
+
2291
+ Returns
2292
+ -------
2293
+ register : int
2294
+ """
2295
+ payload = DiagnosticsSDU(
2296
+ code=DiagnosticsCode.RETURN_DIAGNOSTIC_REGISTER,
2297
+ subfunction_data=b"\x00\x00",
2298
+ return_subfunction_bytes=2,
2299
+ )
2300
+
2301
+ output = cast(DiagnosticsSDU.Response, await self.aquery(payload))
2302
+
2303
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
2304
+
2305
+ def diagnostics_change_ascii_input_delimiter(self, char: bytes | str) -> None:
2306
+ """
2307
+ Change the ASCII input delimiter to specified value
2308
+
2309
+ Parameters
2310
+ ----------
2311
+ char : bytes or str
2312
+ Single character
958
2313
  """
959
- returned_data = self._diagnostics(
960
- DiagnosticsCode.RETURN_DIAGNOSTIC_REGISTER, b"\x00\x00", 2
2314
+ if len(char) != 1:
2315
+ raise ValueError(f"Invalid char length : {len(char)}")
2316
+ if isinstance(char, str):
2317
+ char = char.encode("ASCII")
2318
+
2319
+ subfunction_data = struct.pack("cB", char, 0)
2320
+ payload = DiagnosticsSDU(
2321
+ code=DiagnosticsCode.CHANGE_ASCII_INPUT_DELIMITER,
2322
+ subfunction_data=subfunction_data,
2323
+ return_subfunction_bytes=2,
961
2324
  )
962
- return int(struct.unpack("H", returned_data)[0])
963
2325
 
964
- def diagnostics_change_ascii_input_delimiter(self, char: bytes) -> None:
2326
+ self.query(payload)
2327
+
2328
+ async def adiagnostics_change_ascii_input_delimiter(
2329
+ self, char: bytes | str
2330
+ ) -> None:
965
2331
  """
966
- Change the ASCII input delimiter to specified value
2332
+ Asynchronously change the ASCII input delimiter to specified value
967
2333
 
968
2334
  Parameters
969
2335
  ----------
970
2336
  char : bytes or str
971
2337
  Single character
972
2338
  """
973
- assert len(char) == 1, f"Invalid char length : {len(char)}"
2339
+ if len(char) != 1:
2340
+ raise ValueError(f"Invalid char length : {len(char)}")
974
2341
  if isinstance(char, str):
975
2342
  char = char.encode("ASCII")
976
2343
 
977
2344
  subfunction_data = struct.pack("cB", char, 0)
978
- self._diagnostics(
979
- DiagnosticsCode.CHANGE_ASCII_INPUT_DELIMITER, subfunction_data, 2
2345
+ payload = DiagnosticsSDU(
2346
+ code=DiagnosticsCode.CHANGE_ASCII_INPUT_DELIMITER,
2347
+ subfunction_data=subfunction_data,
2348
+ return_subfunction_bytes=2,
980
2349
  )
981
2350
 
2351
+ await self.aquery(payload)
2352
+
982
2353
  def diagnostics_force_listen_only_mode(self) -> None:
983
2354
  """
984
2355
  Forces the addressed remote device to its Listen Only Mode for MODBUS communications.
@@ -986,130 +2357,327 @@ class Modbus(Protocol):
986
2357
  communicating without interruption from the addressed remote device. No response is
987
2358
  returned.
988
2359
  When the remote device enters its Listen Only Mode, all active communication controls are
989
- turned off. The Ready watchdog timer is allowed to expire, locking the controls off. While the
990
- device is in this mode, any MODBUS messages addressed to it or broadcast are monitored,
991
- but no actions will be taken and no responses will be sent.
2360
+ turned off. The Ready watchdog timer is allowed to expire, locking the controls off.
2361
+ While the device is in this mode, any MODBUS messages addressed to it or broadcast
2362
+ are monitored, but no actions will be taken and no responses will be sent.
992
2363
  The only function that will be processed after the mode is entered will be the Restart
993
2364
  Communications Option function (function code 8, sub-function 1).
994
2365
  """
995
- self._diagnostics(
996
- DiagnosticsCode.FORCE_LISTEN_ONLY_MODE, b"\x00\x00", 0, check_response=False
2366
+ payload = DiagnosticsSDU(
2367
+ code=DiagnosticsCode.FORCE_LISTEN_ONLY_MODE,
2368
+ subfunction_data=b"\x00\x00",
2369
+ return_subfunction_bytes=0,
2370
+ check_response=False,
2371
+ )
2372
+
2373
+ self.query(payload)
2374
+
2375
+ async def adiagnostics_force_listen_only_mode(self) -> None:
2376
+ """
2377
+ Asynchronously force the addressed remote device to its Listen Only Mode
2378
+ for MODBUS communications.
2379
+ """
2380
+ payload = DiagnosticsSDU(
2381
+ code=DiagnosticsCode.FORCE_LISTEN_ONLY_MODE,
2382
+ subfunction_data=b"\x00\x00",
2383
+ return_subfunction_bytes=0,
2384
+ check_response=False,
997
2385
  )
998
2386
 
2387
+ await self.aquery(payload)
2388
+
999
2389
  def diagnostics_clear_counters_and_diagnostic_register(self) -> None:
1000
2390
  """
1001
2391
  Clear all counters and the diagnostic register
1002
2392
  """
1003
- self._diagnostics(
1004
- DiagnosticsCode.CLEAR_COUNTERS_AND_DIAGNOSTIC_REGISTER, b"\x00\x00", 0
2393
+ payload = DiagnosticsSDU(
2394
+ code=DiagnosticsCode.CLEAR_COUNTERS_AND_DIAGNOSTIC_REGISTER,
2395
+ subfunction_data=b"\x00\x00",
2396
+ return_subfunction_bytes=0,
2397
+ )
2398
+
2399
+ self.query(payload)
2400
+
2401
+ async def adiagnostics_clear_counters_and_diagnostic_register(self) -> None:
2402
+ """
2403
+ Asynchronously clear all counters and the diagnostic register
2404
+ """
2405
+ payload = DiagnosticsSDU(
2406
+ code=DiagnosticsCode.CLEAR_COUNTERS_AND_DIAGNOSTIC_REGISTER,
2407
+ subfunction_data=b"\x00\x00",
2408
+ return_subfunction_bytes=0,
1005
2409
  )
1006
2410
 
2411
+ await self.aquery(payload)
2412
+
1007
2413
  def diagnostics_return_bus_message_count(self) -> int:
1008
2414
  """
1009
- Return the number of messages that the remote device has detection on the communications system since its last restat, clear counters operation, or power-up
2415
+ Return the number of messages that the remote device has detection on the communications
2416
+ system since its last restat, clear counters operation, or power-up
1010
2417
 
1011
2418
  Returns
1012
2419
  -------
1013
2420
  count : int
1014
2421
  """
1015
- response = self._diagnostics(
1016
- DiagnosticsCode.RETURN_BUS_MESSAGE_COUNT, b"\x00\x00", 2
2422
+ payload = DiagnosticsSDU(
2423
+ code=DiagnosticsCode.RETURN_BUS_MESSAGE_COUNT,
2424
+ subfunction_data=b"\x00\x00",
2425
+ return_subfunction_bytes=2,
2426
+ )
2427
+
2428
+ output = cast(DiagnosticsSDU.Response, self.query(payload))
2429
+
2430
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
2431
+
2432
+ async def adiagnostics_return_bus_message_count(self) -> int:
2433
+ """
2434
+ Asynchronously return the number of messages that the remote device has detection on the
2435
+ communications system since its last restat, clear counters operation, or power-up
2436
+ """
2437
+ payload = DiagnosticsSDU(
2438
+ code=DiagnosticsCode.RETURN_BUS_MESSAGE_COUNT,
2439
+ subfunction_data=b"\x00\x00",
2440
+ return_subfunction_bytes=2,
1017
2441
  )
1018
- count = int(struct.unpack("H", response)[0])
1019
- return count
2442
+
2443
+ output = cast(DiagnosticsSDU.Response, await self.aquery(payload))
2444
+
2445
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
1020
2446
 
1021
2447
  def diagnostics_return_bus_communication_error_count(self) -> int:
1022
2448
  """
1023
- Return the number of messages that the remote device has detection on the communications system since its last restart, clear counters operation, or power-up
2449
+ Return the number of messages that the remote device has detection on the communications
2450
+ system since its last restart, clear counters operation, or power-up
1024
2451
 
1025
2452
  Returns
1026
2453
  -------
1027
2454
  count : int
1028
2455
  """
1029
- response = self._diagnostics(
1030
- DiagnosticsCode.RETURN_BUS_COMMUNICATION_ERROR_COUNT, b"\x00\x00", 2
2456
+ payload = DiagnosticsSDU(
2457
+ code=DiagnosticsCode.RETURN_BUS_COMMUNICATION_ERROR_COUNT,
2458
+ subfunction_data=b"\x00\x00",
2459
+ return_subfunction_bytes=2,
2460
+ )
2461
+
2462
+ output = cast(DiagnosticsSDU.Response, self.query(payload))
2463
+
2464
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
2465
+
2466
+ async def adiagnostics_return_bus_communication_error_count(self) -> int:
2467
+ """
2468
+ Asynchronously return the number of messages that the remote device has detection on the
2469
+ communications system since its last restart, clear counters operation, or power-up
2470
+ """
2471
+ payload = DiagnosticsSDU(
2472
+ code=DiagnosticsCode.RETURN_BUS_COMMUNICATION_ERROR_COUNT,
2473
+ subfunction_data=b"\x00\x00",
2474
+ return_subfunction_bytes=2,
1031
2475
  )
1032
- count = int(struct.unpack("H", response)[0])
1033
- return count
2476
+
2477
+ output = cast(DiagnosticsSDU.Response, await self.aquery(payload))
2478
+
2479
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
1034
2480
 
1035
2481
  def diagnostics_return_bus_exception_error_count(self) -> int:
1036
2482
  """
1037
- Return the number of Modbus exceptions responses returned by the remote device since its last restart, clear counters operation, or power-up
2483
+ Return the number of Modbus exceptions responses returned by the remote device since
2484
+ its last restart, clear counters operation, or power-up
1038
2485
 
1039
2486
  Returns
1040
2487
  -------
1041
2488
  count : int
1042
2489
  """
1043
- response = self._diagnostics(
1044
- DiagnosticsCode.RETURN_BUS_EXCEPTION_ERROR_COUNT, b"\x00\x00", 2
2490
+ payload = DiagnosticsSDU(
2491
+ code=DiagnosticsCode.RETURN_BUS_EXCEPTION_ERROR_COUNT,
2492
+ subfunction_data=b"\x00\x00",
2493
+ return_subfunction_bytes=2,
2494
+ )
2495
+
2496
+ output = cast(DiagnosticsSDU.Response, self.query(payload))
2497
+
2498
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
2499
+
2500
+ async def adiagnostics_return_bus_exception_error_count(self) -> int:
2501
+ """
2502
+ Asynchronously return the number of Modbus exceptions responses returned by the remote
2503
+ device since its last restart, clear counters operation, or power-up
2504
+ """
2505
+ payload = DiagnosticsSDU(
2506
+ code=DiagnosticsCode.RETURN_BUS_EXCEPTION_ERROR_COUNT,
2507
+ subfunction_data=b"\x00\x00",
2508
+ return_subfunction_bytes=2,
1045
2509
  )
1046
- count = int(struct.unpack("H", response)[0])
1047
- return count
2510
+
2511
+ output = cast(DiagnosticsSDU.Response, await self.aquery(payload))
2512
+
2513
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
1048
2514
 
1049
2515
  def diagnostics_return_server_no_response_count(self) -> int:
1050
2516
  """
1051
- Return the number of messages addressed to the remote device for which it has returned no response since its last restart, clear counters operation, or power-up
2517
+ Return the number of messages addressed to the remote device for which it has returned
2518
+ no response since its last restart, clear counters operation, or power-up
1052
2519
 
1053
2520
  Returns
1054
2521
  -------
1055
2522
  count : int
1056
2523
  """
1057
- response = self._diagnostics(
1058
- DiagnosticsCode.RETURN_SERVER_NO_RESPONSE_COUNT, b"\x00\x00", 2
2524
+ payload = DiagnosticsSDU(
2525
+ code=DiagnosticsCode.RETURN_SERVER_NO_RESPONSE_COUNT,
2526
+ subfunction_data=b"\x00\x00",
2527
+ return_subfunction_bytes=2,
2528
+ )
2529
+
2530
+ output = cast(DiagnosticsSDU.Response, self.query(payload))
2531
+
2532
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
2533
+
2534
+ async def adiagnostics_return_server_no_response_count(self) -> int:
2535
+ """
2536
+ Asynchronously return the number of messages addressed to the remote device for which it
2537
+ has returned no response since its last restart, clear counters operation, or power-up
2538
+ """
2539
+ payload = DiagnosticsSDU(
2540
+ code=DiagnosticsCode.RETURN_SERVER_NO_RESPONSE_COUNT,
2541
+ subfunction_data=b"\x00\x00",
2542
+ return_subfunction_bytes=2,
1059
2543
  )
1060
- count = int(struct.unpack("H", response)[0])
1061
- return count
2544
+
2545
+ output = cast(DiagnosticsSDU.Response, await self.aquery(payload))
2546
+
2547
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
1062
2548
 
1063
2549
  def diagnostics_return_server_nak_count(self) -> int:
1064
2550
  """
1065
- Return the number of messages addressed to the remote device for which it returned a negative acnowledge (NAK) exception response since its last restart, clear counters operation, or power-up
2551
+ Return the number of messages addressed to the remote device for which it returned
2552
+ a negative acnowledge (NAK) exception response since its last restart, clear counters
2553
+ operation, or power-up
1066
2554
 
1067
2555
  Returns
1068
2556
  -------
1069
2557
  count : int
1070
2558
  """
1071
- response = self._diagnostics(
1072
- DiagnosticsCode.RETURN_SERVER_NAK_COUNT, b"\x00\x00", 2
2559
+ payload = DiagnosticsSDU(
2560
+ code=DiagnosticsCode.RETURN_SERVER_NAK_COUNT,
2561
+ subfunction_data=b"\x00\x00",
2562
+ return_subfunction_bytes=2,
2563
+ )
2564
+
2565
+ output = cast(DiagnosticsSDU.Response, self.query(payload))
2566
+
2567
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
2568
+
2569
+ async def adiagnostics_return_server_nak_count(self) -> int:
2570
+ """
2571
+ Asynchronously return the number of messages addressed to the remote device for which it
2572
+ returned a negative acnowledge (NAK) exception response since its last restart,
2573
+ clear counters operation, or power-up
2574
+ """
2575
+ payload = DiagnosticsSDU(
2576
+ code=DiagnosticsCode.RETURN_SERVER_NAK_COUNT,
2577
+ subfunction_data=b"\x00\x00",
2578
+ return_subfunction_bytes=2,
1073
2579
  )
1074
- count = int(struct.unpack("H", response)[0])
1075
- return count
2580
+
2581
+ output = cast(DiagnosticsSDU.Response, await self.aquery(payload))
2582
+
2583
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
1076
2584
 
1077
2585
  def diagnostics_return_server_busy_count(self) -> int:
1078
2586
  """
1079
- Return the number of messages addressed to the remote device for which it returned a server device busy exception response since its last restart, clear counters operation, or power-up
2587
+ Return the number of messages addressed to the remote device for which it returned a
2588
+ server device busy exception response since its last restart, clear counters operation,
2589
+ or power-up
1080
2590
 
1081
2591
  Returns
1082
2592
  -------
1083
2593
  count : int
1084
2594
  """
1085
- response = self._diagnostics(
1086
- DiagnosticsCode.RETURN_SERVER_BUSY_COUNT, b"\x00\x00", 2
2595
+ payload = DiagnosticsSDU(
2596
+ code=DiagnosticsCode.RETURN_SERVER_BUSY_COUNT,
2597
+ subfunction_data=b"\x00\x00",
2598
+ return_subfunction_bytes=2,
2599
+ )
2600
+
2601
+ output = cast(DiagnosticsSDU.Response, self.query(payload))
2602
+
2603
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
2604
+
2605
+ async def adiagnostics_return_server_busy_count(self) -> int:
2606
+ """
2607
+ Asynchronously return the number of messages addressed to the remote device for which it
2608
+ returned a server device busy exception response since its last restart, clear counters
2609
+ operation, or power-up
2610
+ """
2611
+ payload = DiagnosticsSDU(
2612
+ code=DiagnosticsCode.RETURN_SERVER_BUSY_COUNT,
2613
+ subfunction_data=b"\x00\x00",
2614
+ return_subfunction_bytes=2,
1087
2615
  )
1088
- count = int(struct.unpack("H", response)[0])
1089
- return count
2616
+
2617
+ output = cast(DiagnosticsSDU.Response, await self.aquery(payload))
2618
+
2619
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
1090
2620
 
1091
2621
  def diagnostics_return_bus_character_overrun_count(self) -> int:
1092
2622
  """
1093
- Return the number of messages addressed to the remote device that it could not handle due to a character overrun condition since its last restart, clear counters operation, or power-up
2623
+ Return the number of messages addressed to the remote device that it could not handle
2624
+ due to a character overrun condition since its last restart, clear counters operation,
2625
+ or power-up
1094
2626
 
1095
2627
  Returns
1096
2628
  -------
1097
2629
  count : int
1098
2630
  """
1099
- response = self._diagnostics(
1100
- DiagnosticsCode.RETURN_BUS_CHARACTER_OVERRUN_COUNT, b"\x00\x00", 2
2631
+ payload = DiagnosticsSDU(
2632
+ code=DiagnosticsCode.RETURN_BUS_CHARACTER_OVERRUN_COUNT,
2633
+ subfunction_data=b"\x00\x00",
2634
+ return_subfunction_bytes=2,
2635
+ )
2636
+
2637
+ output = cast(DiagnosticsSDU.Response, self.query(payload))
2638
+
2639
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
2640
+
2641
+ async def adiagnostics_return_bus_character_overrun_count(self) -> int:
2642
+ """
2643
+ Asynchronously return the number of messages addressed to the remote device that it could
2644
+ not handle due to a character overrun condition since its last restart, clear counters
2645
+ operation, or power-up
2646
+ """
2647
+ payload = DiagnosticsSDU(
2648
+ code=DiagnosticsCode.RETURN_BUS_CHARACTER_OVERRUN_COUNT,
2649
+ subfunction_data=b"\x00\x00",
2650
+ return_subfunction_bytes=2,
1101
2651
  )
1102
- count = int(struct.unpack("H", response)[0])
1103
- return count
2652
+
2653
+ output = cast(DiagnosticsSDU.Response, await self.aquery(payload))
2654
+
2655
+ return int(struct.unpack(ENDIAN + "H", output.data)[0])
1104
2656
 
1105
2657
  def diagnostics_clear_overrun_counter_and_flag(self) -> None:
1106
2658
  """
1107
2659
  Clear the overrun error counter and reset the error flag
1108
2660
  """
1109
- self._diagnostics(
1110
- DiagnosticsCode.CLEAR_OVERRUN_COUNTER_AND_FLAG, b"\x00\x00", 0
2661
+ payload = DiagnosticsSDU(
2662
+ code=DiagnosticsCode.CLEAR_OVERRUN_COUNTER_AND_FLAG,
2663
+ subfunction_data=b"\x00\x00",
2664
+ return_subfunction_bytes=0,
2665
+ )
2666
+
2667
+ self.query(payload)
2668
+
2669
+ async def adiagnostics_clear_overrun_counter_and_flag(self) -> None:
2670
+ """
2671
+ Asynchronously clear the overrun error counter and reset the error flag
2672
+ """
2673
+ payload = DiagnosticsSDU(
2674
+ code=DiagnosticsCode.CLEAR_OVERRUN_COUNTER_AND_FLAG,
2675
+ subfunction_data=b"\x00\x00",
2676
+ return_subfunction_bytes=0,
1111
2677
  )
1112
2678
 
2679
+ await self.aquery(payload)
2680
+
1113
2681
  # Get Comm Event Counter - 0x0B
1114
2682
  def get_comm_event_counter(self) -> tuple[int, int]:
1115
2683
  """
@@ -1120,20 +2688,28 @@ class Modbus(Protocol):
1120
2688
  status : int
1121
2689
  event_count : int
1122
2690
  """
1123
- EXCEPTIONS = {
1124
- 1: "Function code not supported",
1125
- 4: "Couldn't get comm event counter",
1126
- }
1127
- query = struct.pack(ENDIAN + "B", FunctionCode.GET_COMM_EVENT_COUNTER.value)
1128
- response = self._parse_pdu(self._adapter.query(self._make_pdu(query)))
1129
- self._raise_if_error(response, EXCEPTIONS)
1130
- _, status, event_count = struct.unpack(ENDIAN + "BHH", response)
1131
- return status, event_count
2691
+ payload = GetCommEventCounterSDU()
2692
+
2693
+ output = cast(GetCommEventCounterSDU.Response, self.query(payload))
2694
+
2695
+ return output.status, output.event_count
2696
+
2697
+ async def aget_comm_event_counter(self) -> tuple[int, int]:
2698
+ """
2699
+ Asynchronously retrieve status word and event count from the remote device's
2700
+ communication event counter
2701
+ """
2702
+ payload = GetCommEventCounterSDU()
2703
+
2704
+ output = cast(GetCommEventCounterSDU.Response, await self.aquery(payload))
2705
+
2706
+ return output.status, output.event_count
1132
2707
 
1133
2708
  # Get Comm Event Log - 0x0C
1134
2709
  def get_comm_event_log(self) -> tuple[int, int, int, bytes]:
1135
2710
  """
1136
- Retrieve status word, event count, message count and a field of event bytes from the remote device
2711
+ Retrieve status word, event count, message count and a field of event bytes from
2712
+ the remote device
1137
2713
 
1138
2714
  Status word and event count are identical to those returned by get_comm_event_counter()
1139
2715
 
@@ -1142,26 +2718,29 @@ class Modbus(Protocol):
1142
2718
  status : int
1143
2719
  event_count : int
1144
2720
  message_count : int
1145
- Number of messages processed since its last restart, clear counters operation, or power-up
2721
+ Number of messages processed since its last restart, clear counters operation,
2722
+ or power-up
1146
2723
  Identical to diagnostics_return_bus_message_count()
1147
2724
  events : bytes
1148
- 0-64 bytes, each corresponding to the status of one Modbus send or receive operation for
1149
- the remote device. Byte 0 is the most recent event
2725
+ 0-64 bytes, each corresponding to the status of one Modbus send or receive
2726
+ operation for the remote device. Byte 0 is the most recent event
1150
2727
  """
2728
+ payload = GetCommEventLogSDU()
1151
2729
 
1152
- EXCEPTIONS = {
1153
- 1: "Function code not supported",
1154
- 4: "Couldn't get comm event log",
1155
- }
1156
- query = struct.pack(ENDIAN + "B", FunctionCode.GET_COMM_EVENT_LOG.value)
1157
- response = self._parse_pdu(self._adapter.query(self._make_pdu(query)))
1158
- self._raise_if_error(response, EXCEPTIONS)
1159
- _, byte_count, status, event_count, message_count = struct.unpack(
1160
- "BBHHH", response
1161
- )
1162
- events = struct.unpack(ENDIAN + f"8x{byte_count - 6}B", response)[0]
2730
+ output = cast(GetCommEventLogSDU.Response, self.query(payload))
2731
+
2732
+ return output.status, output.event_count, output.message_count, output.events
1163
2733
 
1164
- return status, event_count, message_count, events
2734
+ async def aget_comm_event_log(self) -> tuple[int, int, int, bytes]:
2735
+ """
2736
+ Asynchronously retrieve status word, event count, message count and a field of event bytes
2737
+ from the remote device
2738
+ """
2739
+ payload = GetCommEventLogSDU()
2740
+
2741
+ output = cast(GetCommEventLogSDU.Response, await self.aquery(payload))
2742
+
2743
+ return output.status, output.event_count, output.message_count, output.events
1165
2744
 
1166
2745
  # Write Multiple Coils - 0x0F
1167
2746
  def write_multiple_coils(self, start_address: int, values: list[bool]) -> None:
@@ -1174,43 +2753,25 @@ class Modbus(Protocol):
1174
2753
  value : list
1175
2754
  Bool values
1176
2755
  """
1177
- # TODO : Check behavior against page 29 of the modbus spec (endianness of values)
1178
- EXCEPTIONS = {
1179
- 1: "Function code not supported",
1180
- 2: "Invalid start and/or end addresses",
1181
- 3: "Invalid number of outputs and/or byte count",
1182
- 4: "Couldn't write outputs",
1183
- }
2756
+ payload = WriteMultipleCoilsSDU(
2757
+ start_address=start_address,
2758
+ values=values,
2759
+ )
1184
2760
 
1185
- number_of_coils = len(values)
1186
- assert (
1187
- 1 <= number_of_coils <= MAX_DISCRETE_INPUTS
1188
- ), f"Invalid number of coils : {number_of_coils}"
1189
- assert (
1190
- 1 <= start_address <= MAX_ADDRESS - number_of_coils + 1
1191
- ), f"Invalid start address : {start_address}"
1192
- byte_count = ceil(number_of_coils / 8)
2761
+ self.query(payload)
1193
2762
 
1194
- query = struct.pack(
1195
- ENDIAN + f"BHHB{byte_count}s",
1196
- FunctionCode.WRITE_MULTIPLE_COILS.value,
1197
- self._dm_to_pdu_address(start_address),
1198
- number_of_coils,
1199
- byte_count,
1200
- bool_list_to_bytes(values),
1201
- )
1202
- response = self._parse_pdu(
1203
- self._adapter.query(
1204
- self._make_pdu(query), stop_conditions=Length(self._length(5))
1205
- )
2763
+ async def awrite_multiple_coils(
2764
+ self, start_address: int, values: list[bool]
2765
+ ) -> None:
2766
+ """
2767
+ Asynchronously write multiple coil values
2768
+ """
2769
+ payload = WriteMultipleCoilsSDU(
2770
+ start_address=start_address,
2771
+ values=values,
1206
2772
  )
1207
- self._raise_if_error(response, EXCEPTIONS)
1208
2773
 
1209
- _, _, coils_written = struct.unpack(ENDIAN + "BHH", response)
1210
- if coils_written != number_of_coils:
1211
- raise RuntimeError(
1212
- f"Number of coils written ({coils_written}) doesn't match expected value : {number_of_coils}"
1213
- )
2774
+ await self.aquery(payload)
1214
2775
 
1215
2776
  # Write Multiple Registers - 0x10
1216
2777
  def write_multiple_registers(self, start_address: int, values: list[int]) -> None:
@@ -1224,52 +2785,33 @@ class Modbus(Protocol):
1224
2785
  List of integers
1225
2786
 
1226
2787
  """
1227
- EXCEPTIONS = {
1228
- 1: "Function code not supported",
1229
- 2: "Invalid start and/or end addresses",
1230
- 3: "Invalid number of outputs and/or byte count",
1231
- 4: "Couldn't write outputs",
1232
- }
1233
- byte_count = 2 * len(values)
1234
-
1235
- # Specs says 123, but it would exceed 255 bytes over TCP. So 121 is safer
1236
- MAX_NUMBER_OF_REGISTERS = (AVAILABLE_PDU_SIZE - 6) // 2
2788
+ payload = WriteMultipleRegistersSDU(
2789
+ start_address=start_address,
2790
+ values=values,
2791
+ )
1237
2792
 
1238
- assert len(values) > 0, "Empty register list"
1239
- assert (
1240
- len(values) <= MAX_NUMBER_OF_REGISTERS
1241
- ), f"Cannot set more than {MAX_NUMBER_OF_REGISTERS} registers at a time"
1242
- assert (
1243
- MIN_ADDRESS <= start_address <= MAX_ADDRESS - len(values) + 1
1244
- ), f"Invalid address : {start_address}"
2793
+ self.query(payload)
1245
2794
 
1246
- query = struct.pack(
1247
- ENDIAN + f"BHHB{byte_count // 2}H",
1248
- FunctionCode.WRITE_MULTIPLE_REGISTERS.value,
1249
- self._dm_to_pdu_address(start_address),
1250
- byte_count // 2,
1251
- byte_count,
1252
- *values,
1253
- )
1254
- response = self._parse_pdu(
1255
- self._adapter.query(
1256
- self._make_pdu(query), stop_conditions=Length(self._length(5))
1257
- )
2795
+ async def awrite_multiple_registers(
2796
+ self, start_address: int, values: list[int]
2797
+ ) -> None:
2798
+ """
2799
+ Asynchronously write multiple registers
2800
+ """
2801
+ payload = WriteMultipleRegistersSDU(
2802
+ start_address=start_address,
2803
+ values=values,
1258
2804
  )
1259
- self._raise_if_error(response, EXCEPTIONS)
1260
2805
 
1261
- _, _, coils_written = struct.unpack(ENDIAN + "BHH", response)
1262
- if coils_written != byte_count // 2:
1263
- raise RuntimeError(
1264
- f"Number of coils written ({coils_written}) doesn't match expected value : {byte_count // 2}"
1265
- )
2806
+ await self.aquery(payload)
1266
2807
 
1267
2808
  # Report Server ID - 0x11
1268
2809
  def report_server_id(
1269
2810
  self, server_id_length: int, additional_data_length: int
1270
2811
  ) -> tuple[bytes, bool, bytes]:
1271
2812
  """
1272
- Read description of the type, current status and other information specific to a remote device
2813
+ Read description of the type, current status and other information specific to
2814
+ a remote device
1273
2815
 
1274
2816
  Parameters
1275
2817
  ----------
@@ -1283,21 +2825,38 @@ class Modbus(Protocol):
1283
2825
  run_indicator_status : bool
1284
2826
  additional_data : bytes
1285
2827
  """
1286
- EXCEPTIONS = {1: "Function code not supported", 4: "Couldn't report slave ID"}
1287
- query = struct.pack(ENDIAN + "B", FunctionCode.REPORT_SERVER_ID.value)
1288
- response = self._parse_pdu(self._adapter.query(self._make_pdu(query)))
1289
- self._raise_if_error(response, EXCEPTIONS)
1290
- server_id, run_indicator_status, additional_data = struct.unpack(
1291
- ENDIAN + f"{server_id_length}sB{additional_data_length}s", response
2828
+ payload = ReportServerIdSDU(
2829
+ server_id_length=server_id_length,
2830
+ additional_data_length=additional_data_length,
2831
+ )
2832
+
2833
+ output = cast(ReportServerIdSDU.Response, self.query(payload))
2834
+
2835
+ return output.server_id, output.run_indicator_status, output.additional_data
2836
+
2837
+ async def areport_server_id(
2838
+ self, server_id_length: int, additional_data_length: int
2839
+ ) -> tuple[bytes, bool, bytes]:
2840
+ """
2841
+ Asynchronously read description of the type, current status and other information specific
2842
+ to a remote device
2843
+ """
2844
+ payload = ReportServerIdSDU(
2845
+ server_id_length=server_id_length,
2846
+ additional_data_length=additional_data_length,
1292
2847
  )
1293
- return server_id, run_indicator_status == 0xFF, additional_data
2848
+
2849
+ output = cast(ReportServerIdSDU.Response, await self.aquery(payload))
2850
+
2851
+ return output.server_id, output.run_indicator_status, output.additional_data
1294
2852
 
1295
2853
  # Read File Record - 0x14
1296
2854
  def read_file_record(self, records: list[tuple[int, int, int]]) -> list[bytes]:
1297
2855
  """
1298
2856
  Perform a single or multiple file record read
1299
2857
 
1300
- Total response length cannot exceed 253 bytes, meaning the number of records and their length is limited.
2858
+ Total response length cannot exceed 253 bytes, meaning the number of records
2859
+ and their length is limited.
1301
2860
 
1302
2861
 
1303
2862
  Query equation : 2 + 7*N <= 253
@@ -1313,116 +2872,62 @@ class Modbus(Protocol):
1313
2872
  records_data : list
1314
2873
  List of bytes
1315
2874
  """
1316
- SIZE_LIMIT = 253
1317
- REFERENCE_TYPE = 6
1318
- EXCEPTIONS = {
1319
- 1: "Function code not supported",
1320
- 2: "Invalid parameters",
1321
- 3: "Invalid byte count",
1322
- 4: "Couldn't read records",
1323
- }
2875
+ payload = ReadFileRecordSDU(records=records)
1324
2876
 
1325
- assert isinstance(records, list)
2877
+ output = cast(ReadFileRecordSDU.Response, self.query(payload))
1326
2878
 
1327
- query_size = 2 + 7 * len(records)
1328
- response_size = 2 + len(records) + sum(2 * r[2] for r in records)
1329
- if query_size > SIZE_LIMIT:
1330
- raise ValueError(f"Number of records is too high : {len(records)}")
1331
- if response_size > SIZE_LIMIT:
1332
- raise ValueError(
1333
- f"Sum of records lenghts is too high : {sum([r[2] for r in records])}"
1334
- )
2879
+ return output.records_data
1335
2880
 
1336
- sub_req_buffer = b""
1337
- for file_number, record_number, record_length in records:
1338
- sub_req_buffer += struct.pack(
1339
- ENDIAN + "BHHH",
1340
- REFERENCE_TYPE,
1341
- file_number,
1342
- record_number,
1343
- record_length,
1344
- )
2881
+ async def aread_file_record(
2882
+ self, records: list[tuple[int, int, int]]
2883
+ ) -> list[bytes]:
2884
+ """
2885
+ Asynchronously perform a single or multiple file record read
2886
+ """
2887
+ payload = ReadFileRecordSDU(records=records)
1345
2888
 
1346
- byte_count = len(sub_req_buffer)
2889
+ output = cast(ReadFileRecordSDU.Response, await self.aquery(payload))
1347
2890
 
1348
- query = struct.pack(
1349
- ENDIAN + f"BB{byte_count}s",
1350
- FunctionCode.READ_FILE_RECORD,
1351
- byte_count,
1352
- sub_req_buffer,
1353
- )
1354
- response = self._parse_pdu(self._adapter.query(self._make_pdu(query)))
1355
- self._raise_if_error(response, EXCEPTIONS)
1356
- # Parse the response
1357
- # start at position 2
1358
- records_data = []
1359
- i = 2
1360
- while True:
1361
- length = response[i]
1362
- i += 1
1363
- # Ignore teh reference type
1364
- # Read the record data
1365
- records_data.append(response[i : i + length])
1366
- i += length
1367
-
1368
- return records_data
2891
+ return output.records_data
1369
2892
 
1370
2893
  # Write File Record - 0x15
1371
2894
  def write_file_record(self, records: list[tuple[int, int, bytes]]) -> None:
1372
2895
  """
1373
2896
  Perform a single or multiple file record write
1374
2897
 
1375
- Total query and response length cannot exceed 253 bytes, meaning the number of records and their length is limited.
2898
+ Total query and response length cannot exceed 253 bytes, meaning the number of
2899
+ records and their length is limited.
1376
2900
 
1377
2901
  Query equation : 2 + 7*N + sum(Li*2) <= 253
1378
2902
  Response equation : identical
1379
2903
 
1380
- File number can be between 0x0001 and 0xFFFF but lots of legacy equipment will not support file number above 0x000A (10)
2904
+ File number can be between 0x0001 and 0xFFFF but lots of legacy equipment will
2905
+ not support file number above 0x000A (10)
1381
2906
 
1382
2907
  Parameters
1383
2908
  ----------
1384
2909
  records : list
1385
2910
  List of tuples : (file_number, record_number, data)
1386
2911
  """
1387
- REFERENCE_TYPE = 6
1388
- EXCEPTIONS = {
1389
- 1: "Function code not supported",
1390
- 2: "Invalid parameters",
1391
- 3: "Invalid byte count",
1392
- 4: "Couldn't write records",
1393
- }
1394
-
1395
- if isinstance(records, tuple):
1396
- records = [records]
1397
- elif not isinstance(records, list):
1398
- raise TypeError(f"Invalid records type : {records}")
2912
+ payload = WriteFileRecordSDU(records=records)
1399
2913
 
1400
- sub_req_buffer = b""
2914
+ self.query(payload)
1401
2915
 
1402
- for file_number, record_number, data in records:
1403
- sub_req_buffer += struct.pack(
1404
- ENDIAN + f"BHHH{len(data)}s",
1405
- REFERENCE_TYPE,
1406
- file_number,
1407
- record_number,
1408
- len(data) // 2,
1409
- data,
1410
- )
2916
+ async def awrite_file_record(self, records: list[tuple[int, int, bytes]]) -> None:
2917
+ """
2918
+ Asynchronously perform a single or multiple file record write
2919
+ """
2920
+ payload = WriteFileRecordSDU(records=records)
1411
2921
 
1412
- query = struct.pack(
1413
- ENDIAN + f"BB{len(sub_req_buffer)}s",
1414
- FunctionCode.WRITE_FILE_RECORD,
1415
- len(sub_req_buffer),
1416
- sub_req_buffer,
1417
- )
1418
- response = self._parse_pdu(self._adapter.query(self._make_pdu(query)))
1419
- self._raise_if_error(response, EXCEPTIONS)
1420
- assert response == query
2922
+ await self.aquery(payload)
1421
2923
 
1422
2924
  # Mask Write Register - 0x16
1423
- def mask_write_register(self, address: int, and_mask: int, or_mask: int) -> None:
2925
+ async def amask_write_register(
2926
+ self, address: int, and_mask: int, or_mask: int
2927
+ ) -> None:
1424
2928
  """
1425
- This function is used to modify the contents of a holding register using a combination of AND and OR masks applied to the current contents of the register.
2929
+ This function is used to modify the contents of a holding register using a
2930
+ combination of AND and OR masks applied to the current contents of the register.
1426
2931
 
1427
2932
  The algorithm is :
1428
2933
 
@@ -1437,30 +2942,26 @@ class Modbus(Protocol):
1437
2942
  or_mask : int
1438
2943
  0x0000 to 0xFFFF
1439
2944
  """
1440
- EXCEPTIONS = {
1441
- 1: "Function code not supported",
1442
- 2: "Invalid register address",
1443
- 3: "Invalid AND/OR mask",
1444
- 4: "Couldn't write register",
1445
- }
1446
- assert MIN_ADDRESS <= address <= MAX_ADDRESS, f"Invalid address : {address}"
2945
+ payload = MaskWriteRegisterSDU(
2946
+ address=address,
2947
+ and_mask=and_mask,
2948
+ or_mask=or_mask,
2949
+ )
1447
2950
 
1448
- query = struct.pack(
1449
- ENDIAN + "BHHH",
1450
- FunctionCode.MASK_WRITE_REGISTER.value,
1451
- self._dm_to_pdu_address(address),
1452
- and_mask,
1453
- or_mask,
1454
- )
1455
- response = self._parse_pdu(
1456
- self._adapter.query(
1457
- self._make_pdu(query), stop_conditions=Length(self._length(len(query)))
1458
- )
2951
+ await self.aquery(payload)
2952
+
2953
+ def mask_write_register(self, address: int, and_mask: int, or_mask: int) -> None:
2954
+ """
2955
+ This function is used to modify the contents of a holding register using a
2956
+ combination of AND and OR masks applied to the current contents of the register.
2957
+ """
2958
+ payload = MaskWriteRegisterSDU(
2959
+ address=address,
2960
+ and_mask=and_mask,
2961
+ or_mask=or_mask,
1459
2962
  )
1460
- self._raise_if_error(response, EXCEPTIONS)
1461
- assert (
1462
- response == query
1463
- ), f"Response ({response!r}) should match query ({query!r})"
2963
+
2964
+ self.query(payload)
1464
2965
 
1465
2966
  # Read/Write Multiple Registers - 0x17
1466
2967
  def read_write_multiple_registers(
@@ -1468,8 +2969,8 @@ class Modbus(Protocol):
1468
2969
  read_starting_address: int,
1469
2970
  number_of_read_registers: int,
1470
2971
  write_starting_address: int,
1471
- write_values: list[bytes],
1472
- ) -> list[bytes]:
2972
+ write_values: list[int],
2973
+ ) -> list[int]:
1473
2974
  """
1474
2975
  Do a write, then a read operation, each on a specific set of registers.
1475
2976
 
@@ -1485,54 +2986,39 @@ class Modbus(Protocol):
1485
2986
  -------
1486
2987
  read_values : list
1487
2988
  """
1488
- EXCEPTIONS = {
1489
- 1: "Function code not supported",
1490
- 2: "Invalid read/write start/end address",
1491
- 3: "Invalid quantity of read/write and/or byte count",
1492
- 4: "Couldn't read and/or write registers",
1493
- }
2989
+ payload = ReadWriteMultipleRegistersSDU(
2990
+ read_starting_address=read_starting_address,
2991
+ number_of_read_registers=number_of_read_registers,
2992
+ write_starting_address=write_starting_address,
2993
+ write_values=write_values,
2994
+ )
1494
2995
 
1495
- MAX_NUMBER_READ_REGISTERS = (AVAILABLE_PDU_SIZE - 2) // 2
1496
- MAX_NUMBER_WRITE_REGISTERS = (AVAILABLE_PDU_SIZE - 10) // 2
1497
-
1498
- assert (
1499
- 1 <= number_of_read_registers <= MAX_NUMBER_READ_REGISTERS
1500
- ), f"Invalid number of read registers : {number_of_read_registers}"
1501
- assert (
1502
- MIN_ADDRESS
1503
- <= read_starting_address
1504
- <= MAX_ADDRESS - number_of_read_registers + 1
1505
- ), f"Invalid read start address : {read_starting_address}"
1506
-
1507
- assert (
1508
- 1 <= len(write_values) <= MAX_NUMBER_WRITE_REGISTERS
1509
- ), f"Invalid number of write registers : {len(write_values)}"
1510
- assert (
1511
- MIN_ADDRESS <= write_starting_address <= MAX_ADDRESS - len(write_values) + 1
1512
- ), f"Invalid write start address (writing {len(write_values)} registers) : {write_starting_address}"
1513
-
1514
- query = struct.pack(
1515
- ENDIAN + f"BHHHHB{len(write_values)}H",
1516
- FunctionCode.READ_WRITE_MULTIPLE_REGISTERS.value,
1517
- read_starting_address,
1518
- number_of_read_registers,
1519
- write_starting_address,
1520
- len(write_values),
1521
- len(write_values) * 2,
1522
- *write_values,
1523
- )
1524
- response = self._parse_pdu(
1525
- self._adapter.query(
1526
- self._make_pdu(query),
1527
- stop_conditions=Length(self._length(2 + number_of_read_registers * 2)),
1528
- )
2996
+ output = cast(ReadWriteMultipleRegistersSDU.Response, self.query(payload))
2997
+
2998
+ return output.read_values
2999
+
3000
+ async def aread_write_multiple_registers(
3001
+ self,
3002
+ read_starting_address: int,
3003
+ number_of_read_registers: int,
3004
+ write_starting_address: int,
3005
+ write_values: list[int],
3006
+ ) -> list[int]:
3007
+ """
3008
+ Asynchronously do a write, then a read operation, each on a specific set of registers.
3009
+ """
3010
+ payload = ReadWriteMultipleRegistersSDU(
3011
+ read_starting_address=read_starting_address,
3012
+ number_of_read_registers=number_of_read_registers,
3013
+ write_starting_address=write_starting_address,
3014
+ write_values=write_values,
3015
+ )
3016
+
3017
+ output = cast(
3018
+ ReadWriteMultipleRegistersSDU.Response, await self.aquery(payload)
1529
3019
  )
1530
- self._raise_if_error(response, EXCEPTIONS)
1531
- # Parse response
1532
- output = struct.unpack(ENDIAN + f"BB{number_of_read_registers}H", response)
1533
- # _, _, read_values
1534
- read_values = list(output[2:])
1535
- return read_values
3020
+
3021
+ return output.read_values
1536
3022
 
1537
3023
  # Read FIFO Queue - 0x18
1538
3024
  def read_fifo_queue(self, fifo_address: int) -> list[int]:
@@ -1547,23 +3033,21 @@ class Modbus(Protocol):
1547
3033
  -------
1548
3034
  registers : list
1549
3035
  """
1550
- EXCEPTIONS = {
1551
- 1: "Function code not supported",
1552
- 2: "Invalid FIFO address",
1553
- 3: "Invalid FIFO count (>31)",
1554
- 4: "Couldn't read FIFO queue",
1555
- }
1556
- query = struct.pack(ENDIAN + "BH", FunctionCode.READ_FIFO_QUEUE, fifo_address)
1557
- response = self._parse_pdu(self._adapter.query(self._make_pdu(query)))
1558
- self._raise_if_error(response, EXCEPTIONS)
1559
- byte_count = int(struct.unpack(ENDIAN + "xH", response)[0])
1560
- register_count = byte_count // 2 - 1
3036
+ payload = ReadFifoQueueSDU(fifo_address=fifo_address)
3037
+
3038
+ output = cast(ReadFifoQueueSDU.Response, self.query(payload))
3039
+
3040
+ return output.values
1561
3041
 
1562
- values = cast(
1563
- list[int], struct.unpack(ENDIAN + f"{register_count}H", response[5:])[0]
1564
- ) # Ignore the FIFO count
3042
+ async def aread_fifo_queue(self, fifo_address: int) -> list[int]:
3043
+ """
3044
+ Asynchronously read the contents of a First-In-First-Out (FIFO) queue of registers
3045
+ """
3046
+ payload = ReadFifoQueueSDU(fifo_address=fifo_address)
1565
3047
 
1566
- return values
3048
+ output = cast(ReadFifoQueueSDU.Response, await self.aquery(payload))
3049
+
3050
+ return output.values
1567
3051
 
1568
3052
  # Encapsulate Interface Transport - 0x2B
1569
3053
  def encapsulated_interface_transport(
@@ -1573,7 +3057,8 @@ class Modbus(Protocol):
1573
3057
  extra_exceptions: dict[int, str] | None = None,
1574
3058
  ) -> bytes:
1575
3059
  """
1576
- The MODBUS Encapsulated Interface (MEI) Transport is a mechanism for tunneling service requests and method invocations
3060
+ The MODBUS Encapsulated Interface (MEI) Transport is a mechanism for tunneling
3061
+ service requests and method invocations
1577
3062
 
1578
3063
  Parameters
1579
3064
  ----------
@@ -1584,18 +3069,33 @@ class Modbus(Protocol):
1584
3069
  -------
1585
3070
  returned_mei_data : bytes
1586
3071
  """
1587
- EXCEPTIONS = {
1588
- 1: "Function code not supported",
1589
- }
1590
- if extra_exceptions is not None:
1591
- EXCEPTIONS.update(extra_exceptions)
1592
-
1593
- query = struct.pack(
1594
- ENDIAN + f"BB{len(mei_data)}s",
1595
- FunctionCode.ENCAPSULATED_INTERFACE_TRANSPORT,
1596
- mei_type,
1597
- mei_data,
1598
- )
1599
- response = self._parse_pdu(self._adapter.query(self._make_pdu(query)))
1600
- self._raise_if_error(response, EXCEPTIONS)
1601
- return response[2:]
3072
+ payload = EncapsulatedInterfaceTransportSDU(
3073
+ mei_type=mei_type,
3074
+ mei_data=mei_data,
3075
+ extra_exceptions=extra_exceptions,
3076
+ )
3077
+
3078
+ output = cast(EncapsulatedInterfaceTransportSDU.Response, self.query(payload))
3079
+
3080
+ return output.data
3081
+
3082
+ async def aencapsulated_interface_transport(
3083
+ self,
3084
+ mei_type: int,
3085
+ mei_data: bytes,
3086
+ extra_exceptions: dict[int, str] | None = None,
3087
+ ) -> bytes:
3088
+ """
3089
+ Asynchronously use the MODBUS Encapsulated Interface (MEI) Transport
3090
+ """
3091
+ payload = EncapsulatedInterfaceTransportSDU(
3092
+ mei_type=mei_type,
3093
+ mei_data=mei_data,
3094
+ extra_exceptions=extra_exceptions,
3095
+ )
3096
+
3097
+ output = cast(
3098
+ EncapsulatedInterfaceTransportSDU.Response, await self.aquery(payload)
3099
+ )
3100
+
3101
+ return output.data