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.
- syndesi/__init__.py +22 -2
- syndesi/adapters/adapter.py +332 -489
- syndesi/adapters/adapter_worker.py +820 -0
- syndesi/adapters/auto.py +58 -25
- syndesi/adapters/descriptors.py +38 -0
- syndesi/adapters/ip.py +203 -71
- syndesi/adapters/serialport.py +154 -25
- syndesi/adapters/stop_conditions.py +354 -0
- syndesi/adapters/timeout.py +58 -21
- syndesi/adapters/visa.py +236 -11
- syndesi/cli/console.py +51 -16
- syndesi/cli/shell.py +95 -47
- syndesi/cli/terminal_tools.py +8 -8
- syndesi/component.py +315 -0
- syndesi/protocols/delimited.py +92 -107
- syndesi/protocols/modbus.py +2368 -868
- syndesi/protocols/protocol.py +186 -33
- syndesi/protocols/raw.py +45 -62
- syndesi/protocols/scpi.py +65 -102
- syndesi/remote/remote.py +188 -0
- syndesi/scripts/syndesi.py +12 -2
- syndesi/tools/errors.py +49 -31
- syndesi/tools/log_settings.py +21 -8
- syndesi/tools/{log.py → logmanager.py} +24 -13
- syndesi/tools/types.py +9 -7
- syndesi/version.py +5 -1
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/METADATA +1 -1
- syndesi-0.5.0.dist-info/RECORD +41 -0
- syndesi/adapters/backend/__init__.py +0 -0
- syndesi/adapters/backend/adapter_backend.py +0 -438
- syndesi/adapters/backend/adapter_manager.py +0 -48
- syndesi/adapters/backend/adapter_session.py +0 -346
- syndesi/adapters/backend/backend.py +0 -438
- syndesi/adapters/backend/backend_status.py +0 -0
- syndesi/adapters/backend/backend_tools.py +0 -66
- syndesi/adapters/backend/descriptors.py +0 -153
- syndesi/adapters/backend/ip_backend.py +0 -149
- syndesi/adapters/backend/serialport_backend.py +0 -241
- syndesi/adapters/backend/stop_condition_backend.py +0 -219
- syndesi/adapters/backend/timed_queue.py +0 -39
- syndesi/adapters/backend/timeout.py +0 -252
- syndesi/adapters/backend/visa_backend.py +0 -197
- syndesi/adapters/ip_server.py +0 -102
- syndesi/adapters/stop_condition.py +0 -90
- syndesi/cli/backend_console.py +0 -96
- syndesi/cli/backend_status.py +0 -274
- syndesi/cli/backend_wrapper.py +0 -61
- syndesi/scripts/syndesi_backend.py +0 -37
- syndesi/tools/backend_api.py +0 -175
- syndesi/tools/backend_logger.py +0 -64
- syndesi/tools/exceptions.py +0 -16
- syndesi/tools/internal.py +0 -0
- syndesi-0.4.2.dist-info/RECORD +0 -60
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/WHEEL +0 -0
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/entry_points.txt +0 -0
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/top_level.txt +0 -0
syndesi/protocols/modbus.py
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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 .
|
|
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
|
|
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(
|
|
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,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
raise
|
|
232
|
-
|
|
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
|
-
|
|
349
|
+
class ModbusSDU:
|
|
350
|
+
"""
|
|
351
|
+
Modbus Service Data Unit
|
|
238
352
|
|
|
239
|
-
|
|
240
|
-
|
|
353
|
+
Subclasses of this contain either request or response fields to a modbus
|
|
354
|
+
frame
|
|
355
|
+
"""
|
|
241
356
|
|
|
242
|
-
def
|
|
243
|
-
"""
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
365
|
+
def exceptions(self) -> dict[int, str]:
|
|
366
|
+
"""Return a dictionary of exceptions based on their integer code"""
|
|
367
|
+
return {}
|
|
250
368
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
374
|
+
class ModbusRequestSDU(ModbusSDU):
|
|
375
|
+
"""
|
|
376
|
+
Base class for modbus request SDUs (Service Data Unit)
|
|
377
|
+
"""
|
|
263
378
|
|
|
264
|
-
|
|
265
|
-
|
|
379
|
+
@abstractmethod
|
|
380
|
+
def function_code(self) -> FunctionCode:
|
|
381
|
+
"""Return the function code of this modbus request"""
|
|
266
382
|
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
|
309
|
-
|
|
399
|
+
output += len(_ASCII_HEADER) + len(_ASCII_TRAILER)
|
|
310
400
|
return output
|
|
311
401
|
|
|
312
|
-
def
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
412
|
+
class SerialLineOnlySDU(ModbusRequestSDU):
|
|
413
|
+
"""
|
|
414
|
+
Marker class for serial line only Modbus function codes
|
|
415
|
+
"""
|
|
352
416
|
|
|
353
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
421
|
+
# Read Coils - 0x01
|
|
422
|
+
@dataclass
|
|
423
|
+
class ReadCoilsSDU(ModbusRequestSDU):
|
|
424
|
+
"""Read coils request."""
|
|
363
425
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
start_address : int
|
|
367
|
-
number_of_coils : int
|
|
426
|
+
start_address: int
|
|
427
|
+
number_of_coils: int
|
|
368
428
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
429
|
+
@dataclass
|
|
430
|
+
class Response(ModbusSDU):
|
|
431
|
+
"""Response data."""
|
|
432
|
+
|
|
433
|
+
coils: list[bool]
|
|
373
434
|
|
|
374
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
450
|
+
def make_sdu(self) -> bytes:
|
|
451
|
+
data = struct.pack(
|
|
389
452
|
ENDIAN + "BHH",
|
|
390
453
|
FunctionCode.READ_COILS.value,
|
|
391
|
-
self.
|
|
392
|
-
number_of_coils,
|
|
454
|
+
_dm_to_pdu_address(self.start_address),
|
|
455
|
+
self.number_of_coils,
|
|
393
456
|
)
|
|
394
457
|
|
|
395
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
468
|
+
# Read discrete inputs - 0x02
|
|
469
|
+
@dataclass
|
|
470
|
+
class ReadDiscreteInputs(ModbusRequestSDU):
|
|
471
|
+
"""Read discrete inputs request."""
|
|
419
472
|
|
|
420
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
435
|
-
----------
|
|
436
|
-
start_address : int
|
|
437
|
-
number_of_inputs : int
|
|
480
|
+
inputs: list[bool]
|
|
438
481
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
inputs : list
|
|
442
|
-
List of booleans
|
|
443
|
-
"""
|
|
482
|
+
def function_code(self) -> FunctionCode:
|
|
483
|
+
return FunctionCode.READ_DISCRETE_INPUTS
|
|
444
484
|
|
|
445
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
497
|
+
def make_sdu(self) -> bytes:
|
|
498
|
+
sdu = struct.pack(
|
|
460
499
|
ENDIAN + "BHH",
|
|
461
|
-
|
|
462
|
-
self.
|
|
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
|
-
|
|
480
|
-
|
|
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
|
-
|
|
487
|
-
----------
|
|
488
|
-
start_address : int
|
|
489
|
-
number_of_registers : int
|
|
490
|
-
1 to 125
|
|
516
|
+
return self.Response(inputs)
|
|
491
517
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
506
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
581
|
+
address: int
|
|
582
|
+
status: bool
|
|
606
583
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
) ->
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
638
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
2035
|
+
output = cast(ReadInputRegistersSDU.Response, self.query(payload))
|
|
725
2036
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
registers
|
|
743
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
2073
|
+
status : bool
|
|
756
2074
|
"""
|
|
757
|
-
|
|
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
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
4: "Couldn't write register",
|
|
799
|
-
}
|
|
2103
|
+
payload = WriteSingleRegisterSDU(
|
|
2104
|
+
address=address,
|
|
2105
|
+
value=value,
|
|
2106
|
+
)
|
|
800
2107
|
|
|
801
|
-
|
|
2108
|
+
self.query(payload)
|
|
802
2109
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
815
|
-
|
|
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:
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
871
|
-
|
|
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
|
-
|
|
2184
|
+
success : bool
|
|
878
2185
|
"""
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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.
|
|
2193
|
+
output = cast(DiagnosticsSDU.Response, self.query(payload))
|
|
893
2194
|
|
|
894
|
-
|
|
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
|
|
2197
|
+
async def adiagnostics_return_query_data(self, data: int = 0x1234) -> bool:
|
|
909
2198
|
"""
|
|
910
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
948
|
-
DiagnosticsCode.RESTART_COMMUNICATIONS_OPTION,
|
|
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
|
-
|
|
960
|
-
|
|
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
|
-
|
|
2326
|
+
self.query(payload)
|
|
2327
|
+
|
|
2328
|
+
async def adiagnostics_change_ascii_input_delimiter(
|
|
2329
|
+
self, char: bytes | str
|
|
2330
|
+
) -> None:
|
|
965
2331
|
"""
|
|
966
|
-
|
|
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
|
-
|
|
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
|
-
|
|
979
|
-
DiagnosticsCode.CHANGE_ASCII_INPUT_DELIMITER,
|
|
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.
|
|
990
|
-
device is in this mode, any MODBUS messages addressed to it or broadcast
|
|
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
|
-
|
|
996
|
-
DiagnosticsCode.FORCE_LISTEN_ONLY_MODE,
|
|
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
|
-
|
|
1004
|
-
DiagnosticsCode.CLEAR_COUNTERS_AND_DIAGNOSTIC_REGISTER,
|
|
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
|
|
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
|
-
|
|
1016
|
-
DiagnosticsCode.RETURN_BUS_MESSAGE_COUNT,
|
|
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
|
-
|
|
1019
|
-
|
|
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
|
|
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
|
-
|
|
1030
|
-
DiagnosticsCode.RETURN_BUS_COMMUNICATION_ERROR_COUNT,
|
|
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
|
-
|
|
1033
|
-
|
|
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
|
|
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
|
-
|
|
1044
|
-
DiagnosticsCode.RETURN_BUS_EXCEPTION_ERROR_COUNT,
|
|
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
|
-
|
|
1047
|
-
|
|
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
|
|
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
|
-
|
|
1058
|
-
DiagnosticsCode.RETURN_SERVER_NO_RESPONSE_COUNT,
|
|
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
|
-
|
|
1061
|
-
|
|
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
|
|
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
|
-
|
|
1072
|
-
DiagnosticsCode.RETURN_SERVER_NAK_COUNT,
|
|
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
|
-
|
|
1075
|
-
|
|
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
|
|
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
|
-
|
|
1086
|
-
DiagnosticsCode.RETURN_SERVER_BUSY_COUNT,
|
|
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
|
-
|
|
1089
|
-
|
|
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
|
|
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
|
-
|
|
1100
|
-
DiagnosticsCode.RETURN_BUS_CHARACTER_OVERRUN_COUNT,
|
|
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
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
1110
|
-
DiagnosticsCode.CLEAR_OVERRUN_COUNTER_AND_FLAG,
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2877
|
+
output = cast(ReadFileRecordSDU.Response, self.query(payload))
|
|
1326
2878
|
|
|
1327
|
-
|
|
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
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
-
|
|
2889
|
+
output = cast(ReadFileRecordSDU.Response, await self.aquery(payload))
|
|
1347
2890
|
|
|
1348
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2914
|
+
self.query(payload)
|
|
1401
2915
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
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[
|
|
1472
|
-
) -> list[
|
|
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
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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
|
-
|
|
1531
|
-
|
|
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
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|