mbtoolcli 0.3.1__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.
- mbtoolcli-0.3.1.dist-info/METADATA +473 -0
- mbtoolcli-0.3.1.dist-info/RECORD +16 -0
- mbtoolcli-0.3.1.dist-info/WHEEL +5 -0
- mbtoolcli-0.3.1.dist-info/entry_points.txt +4 -0
- mbtoolcli-0.3.1.dist-info/licenses/LICENSE +21 -0
- mbtoolcli-0.3.1.dist-info/top_level.txt +1 -0
- modbus_slave_sim/__init__.py +42 -0
- modbus_slave_sim/cli.py +903 -0
- modbus_slave_sim/colors.py +57 -0
- modbus_slave_sim/datatypes.py +386 -0
- modbus_slave_sim/gui_server.py +1049 -0
- modbus_slave_sim/master.py +647 -0
- modbus_slave_sim/registers.py +288 -0
- modbus_slave_sim/rtu_server.py +158 -0
- modbus_slave_sim/simulation.py +318 -0
- modbus_slave_sim/tcp_server.py +178 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modbus register data structures and management.
|
|
3
|
+
|
|
4
|
+
This module defines the register types used in Modbus protocol:
|
|
5
|
+
- Coils (1-bit, read/write)
|
|
6
|
+
- Discrete Inputs (1-bit, read-only)
|
|
7
|
+
- Input Registers (16-bit, read-only)
|
|
8
|
+
- Holding Registers (16-bit, read/write)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from .colors import c, BOLD, DIM, RED, GREEN, GREEN_BOLD, RED_BOLD, CYAN, CYAN_BOLD, MAGENTA, WHITE_BOLD
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseRegister:
|
|
18
|
+
"""Base class for all Modbus register types."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, address: int, value: Any = 0, description: str = ""):
|
|
21
|
+
"""
|
|
22
|
+
Initialize a register.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
address: Register address (0-65535)
|
|
26
|
+
value: Initial value
|
|
27
|
+
description: Human-readable description
|
|
28
|
+
"""
|
|
29
|
+
self.address = address
|
|
30
|
+
self.value = value
|
|
31
|
+
self.description = description
|
|
32
|
+
self._simulation_enabled = False
|
|
33
|
+
self._simulation_config: Dict[str, Any] = {}
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def simulation_enabled(self) -> bool:
|
|
37
|
+
"""Check if simulation is enabled for this register."""
|
|
38
|
+
return self._simulation_enabled
|
|
39
|
+
|
|
40
|
+
@simulation_enabled.setter
|
|
41
|
+
def simulation_enabled(self, enabled: bool):
|
|
42
|
+
"""Enable or disable simulation for this register."""
|
|
43
|
+
self._simulation_enabled = enabled
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def simulation_config(self) -> Dict[str, Any]:
|
|
47
|
+
"""Get simulation configuration."""
|
|
48
|
+
return self._simulation_config
|
|
49
|
+
|
|
50
|
+
@simulation_config.setter
|
|
51
|
+
def simulation_config(self, config: Dict[str, Any]):
|
|
52
|
+
"""Set simulation configuration."""
|
|
53
|
+
self._simulation_config = config
|
|
54
|
+
|
|
55
|
+
def __repr__(self):
|
|
56
|
+
return f"{self.__class__.__name__}(address={self.address}, value={self.value})"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CoilRegister(BaseRegister):
|
|
60
|
+
"""
|
|
61
|
+
Coil register (1-bit, read/write).
|
|
62
|
+
|
|
63
|
+
Address range: 0-65535
|
|
64
|
+
Function codes: 1 (Read), 5 (Write Single), 15 (Write Multiple)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, address: int, value: bool = False, description: str = ""):
|
|
68
|
+
super().__init__(address, value, description)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def value(self) -> bool:
|
|
72
|
+
return self._value
|
|
73
|
+
|
|
74
|
+
@value.setter
|
|
75
|
+
def value(self, val: bool):
|
|
76
|
+
self._value = bool(val)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DiscreteInputRegister(BaseRegister):
|
|
80
|
+
"""
|
|
81
|
+
Discrete Input register (1-bit, read-only).
|
|
82
|
+
|
|
83
|
+
Address range: 0-65535
|
|
84
|
+
Function codes: 2 (Read)
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, address: int, value: bool = False, description: str = ""):
|
|
88
|
+
super().__init__(address, value, description)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def value(self) -> bool:
|
|
92
|
+
return self._value
|
|
93
|
+
|
|
94
|
+
@value.setter
|
|
95
|
+
def value(self, val: bool):
|
|
96
|
+
self._value = bool(val)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class InputRegister(BaseRegister):
|
|
100
|
+
"""
|
|
101
|
+
Input Register (16-bit, read-only).
|
|
102
|
+
|
|
103
|
+
Address range: 0-65535
|
|
104
|
+
Function codes: 4 (Read)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, address: int, value: int = 0, description: str = ""):
|
|
108
|
+
super().__init__(address, value, description)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def value(self) -> int:
|
|
112
|
+
return self._value
|
|
113
|
+
|
|
114
|
+
@value.setter
|
|
115
|
+
def value(self, val: int):
|
|
116
|
+
self._value = int(val) & 0xFFFF
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class HoldingRegister(BaseRegister):
|
|
120
|
+
"""
|
|
121
|
+
Holding Register (16-bit, read/write).
|
|
122
|
+
|
|
123
|
+
Address range: 0-65535
|
|
124
|
+
Function codes: 3 (Read), 6 (Write Single), 16 (Write Multiple)
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, address: int, value: int = 0, description: str = ""):
|
|
128
|
+
super().__init__(address, value, description)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def value(self) -> int:
|
|
132
|
+
return self._value
|
|
133
|
+
|
|
134
|
+
@value.setter
|
|
135
|
+
def value(self, val: int):
|
|
136
|
+
self._value = int(val) & 0xFFFF
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ModbusRegisterMap:
|
|
140
|
+
"""
|
|
141
|
+
Container for all Modbus registers.
|
|
142
|
+
|
|
143
|
+
Manages coils, discrete inputs, input registers, and holding registers.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(self):
|
|
147
|
+
self.coils: Dict[int, CoilRegister] = {}
|
|
148
|
+
self.discrete_inputs: Dict[int, DiscreteInputRegister] = {}
|
|
149
|
+
self.input_registers: Dict[int, InputRegister] = {}
|
|
150
|
+
self.holding_registers: Dict[int, HoldingRegister] = {}
|
|
151
|
+
|
|
152
|
+
def add_coil(self, address: int, value: bool = False, description: str = ""):
|
|
153
|
+
"""Add a coil register."""
|
|
154
|
+
self.coils[address] = CoilRegister(address, value, description)
|
|
155
|
+
|
|
156
|
+
def add_discrete_input(self, address: int, value: bool = False, description: str = ""):
|
|
157
|
+
"""Add a discrete input register."""
|
|
158
|
+
self.discrete_inputs[address] = DiscreteInputRegister(address, value, description)
|
|
159
|
+
|
|
160
|
+
def add_input_register(self, address: int, value: int = 0, description: str = ""):
|
|
161
|
+
"""Add an input register."""
|
|
162
|
+
self.input_registers[address] = InputRegister(address, value, description)
|
|
163
|
+
|
|
164
|
+
def add_holding_register(self, address: int, value: int = 0, description: str = ""):
|
|
165
|
+
"""Add a holding register."""
|
|
166
|
+
self.holding_registers[address] = HoldingRegister(address, value, description)
|
|
167
|
+
|
|
168
|
+
def get_coil(self, address: int) -> Optional[CoilRegister]:
|
|
169
|
+
"""Get a coil by address."""
|
|
170
|
+
return self.coils.get(address)
|
|
171
|
+
|
|
172
|
+
def get_discrete_input(self, address: int) -> Optional[DiscreteInputRegister]:
|
|
173
|
+
"""Get a discrete input by address."""
|
|
174
|
+
return self.discrete_inputs.get(address)
|
|
175
|
+
|
|
176
|
+
def get_input_register(self, address: int) -> Optional[InputRegister]:
|
|
177
|
+
"""Get an input register by address."""
|
|
178
|
+
return self.input_registers.get(address)
|
|
179
|
+
|
|
180
|
+
def get_holding_register(self, address: int) -> Optional[HoldingRegister]:
|
|
181
|
+
"""Get a holding register by address."""
|
|
182
|
+
return self.holding_registers.get(address)
|
|
183
|
+
|
|
184
|
+
def get_all_registers(self) -> List[BaseRegister]:
|
|
185
|
+
"""Get all registers as a flat list."""
|
|
186
|
+
registers = []
|
|
187
|
+
registers.extend(self.coils.values())
|
|
188
|
+
registers.extend(self.discrete_inputs.values())
|
|
189
|
+
registers.extend(self.input_registers.values())
|
|
190
|
+
registers.extend(self.holding_registers.values())
|
|
191
|
+
return registers
|
|
192
|
+
|
|
193
|
+
def get_simulated_registers(self) -> List[BaseRegister]:
|
|
194
|
+
"""Get all registers with simulation enabled."""
|
|
195
|
+
return [r for r in self.get_all_registers() if r.simulation_enabled]
|
|
196
|
+
|
|
197
|
+
def load_from_dict(self, data: Dict[str, list]):
|
|
198
|
+
"""Load register map from a dictionary (parsed from JSON).
|
|
199
|
+
|
|
200
|
+
Expected format:
|
|
201
|
+
{
|
|
202
|
+
"coils": [{"address": 0, "value": false, "description": "..."}, ...],
|
|
203
|
+
"discrete_inputs": [...],
|
|
204
|
+
"input_registers": [{"address": 0, "value": 0, "description": "..."}, ...],
|
|
205
|
+
"holding_registers": [...]
|
|
206
|
+
}
|
|
207
|
+
"""
|
|
208
|
+
for entry in data.get("coils", []):
|
|
209
|
+
self.add_coil(entry["address"], entry.get("value", False), entry.get("description", ""))
|
|
210
|
+
for entry in data.get("discrete_inputs", []):
|
|
211
|
+
self.add_discrete_input(entry["address"], entry.get("value", False), entry.get("description", ""))
|
|
212
|
+
for entry in data.get("input_registers", []):
|
|
213
|
+
self.add_input_register(entry["address"], entry.get("value", 0), entry.get("description", ""))
|
|
214
|
+
for entry in data.get("holding_registers", []):
|
|
215
|
+
self.add_holding_register(entry["address"], entry.get("value", 0), entry.get("description", ""))
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def load_from_json(cls, filepath: str) -> "ModbusRegisterMap":
|
|
219
|
+
"""Load register map from a JSON file.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
filepath: Path to JSON file
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
New ModbusRegisterMap instance with loaded data
|
|
226
|
+
"""
|
|
227
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
228
|
+
data = json.load(f)
|
|
229
|
+
register_map = cls()
|
|
230
|
+
register_map.load_from_dict(data)
|
|
231
|
+
return register_map
|
|
232
|
+
|
|
233
|
+
def create_default_map(self):
|
|
234
|
+
"""Create a default register map with sample data."""
|
|
235
|
+
# Coils (0-7)
|
|
236
|
+
for i in range(8):
|
|
237
|
+
self.add_coil(i, value=(i % 2 == 0), description=f"Coil {i}")
|
|
238
|
+
|
|
239
|
+
# Discrete Inputs (0-7)
|
|
240
|
+
for i in range(8):
|
|
241
|
+
self.add_discrete_input(i, value=(i % 2 == 1), description=f"Discrete Input {i}")
|
|
242
|
+
|
|
243
|
+
# Input Registers (0-9) - Read-only simulated values
|
|
244
|
+
self.add_input_register(0, 1234, "Temperature")
|
|
245
|
+
self.add_input_register(1, 5678, "Pressure")
|
|
246
|
+
self.add_input_register(2, 9012, "Flow Rate")
|
|
247
|
+
self.add_input_register(3, 3456, "Level")
|
|
248
|
+
self.add_input_register(4, 7890, "Speed")
|
|
249
|
+
self.add_input_register(5, 1111, "Counter A")
|
|
250
|
+
self.add_input_register(6, 2222, "Counter B")
|
|
251
|
+
self.add_input_register(7, 3333, "Status Word")
|
|
252
|
+
self.add_input_register(8, 4444, "Error Code")
|
|
253
|
+
self.add_input_register(9, 5555, "System Time")
|
|
254
|
+
|
|
255
|
+
# Holding Registers (0-9) - Read/Write configuration values
|
|
256
|
+
self.add_holding_register(0, 100, "Setpoint 1")
|
|
257
|
+
self.add_holding_register(1, 200, "Setpoint 2")
|
|
258
|
+
self.add_holding_register(2, 300, "Gain")
|
|
259
|
+
self.add_holding_register(3, 400, "Integral Time")
|
|
260
|
+
self.add_holding_register(4, 500, "Derivative Time")
|
|
261
|
+
self.add_holding_register(5, 600, "Output Limit High")
|
|
262
|
+
self.add_holding_register(6, 700, "Output Limit Low")
|
|
263
|
+
self.add_holding_register(7, 800, "Alarm Setpoint 1")
|
|
264
|
+
self.add_holding_register(8, 900, "Alarm Setpoint 2")
|
|
265
|
+
self.add_holding_register(9, 1000, "Control Mode")
|
|
266
|
+
|
|
267
|
+
def print_status(self):
|
|
268
|
+
"""Print the current status of all registers."""
|
|
269
|
+
print(f"\n{c('=== Register Status ===', CYAN_BOLD)}")
|
|
270
|
+
|
|
271
|
+
print(f"\n{c('Coils (Read/Write):', WHITE_BOLD)}")
|
|
272
|
+
for addr, coil in sorted(self.coils.items()):
|
|
273
|
+
state = c("ON ", GREEN_BOLD) if coil.value else c("OFF", RED_BOLD)
|
|
274
|
+
print(f" [{c(str(addr).rjust(5), MAGENTA)}] = {state} - {c(coil.description, DIM)}")
|
|
275
|
+
|
|
276
|
+
print(f"\n{c('Discrete Inputs (Read-Only):', WHITE_BOLD)}")
|
|
277
|
+
for addr, di in sorted(self.discrete_inputs.items()):
|
|
278
|
+
state = c("ON ", GREEN_BOLD) if di.value else c("OFF", RED_BOLD)
|
|
279
|
+
print(f" [{c(str(addr).rjust(5), MAGENTA)}] = {state} - {c(di.description, DIM)}")
|
|
280
|
+
|
|
281
|
+
print(f"\n{c('Input Registers (Read-Only):', WHITE_BOLD)}")
|
|
282
|
+
for addr, ir in sorted(self.input_registers.items()):
|
|
283
|
+
print(f" [{c(str(addr).rjust(5), MAGENTA)}] = {c(str(ir.value).rjust(5), CYAN_BOLD)} - {c(ir.description, DIM)}")
|
|
284
|
+
|
|
285
|
+
print(f"\n{c('Holding Registers (Read/Write):', WHITE_BOLD)}")
|
|
286
|
+
for addr, hr in sorted(self.holding_registers.items()):
|
|
287
|
+
print(f" [{c(str(addr).rjust(5), MAGENTA)}] = {c(str(hr.value).rjust(5), CYAN_BOLD)} - {c(hr.description, DIM)}")
|
|
288
|
+
print(f"{c('========================', CYAN_BOLD)}\n")
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modbus RTU/ASCII Server implementation.
|
|
3
|
+
|
|
4
|
+
This module provides a Modbus serial slave server that handles
|
|
5
|
+
serial communication using the Modbus RTU or ASCII protocol.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pymodbus.datastore import (
|
|
12
|
+
ModbusSequentialDataBlock,
|
|
13
|
+
ModbusSlaveContext,
|
|
14
|
+
ModbusServerContext,
|
|
15
|
+
)
|
|
16
|
+
from pymodbus.device import ModbusDeviceIdentification
|
|
17
|
+
from pymodbus.server import StartSerialServer
|
|
18
|
+
from pymodbus.framer import Framer
|
|
19
|
+
|
|
20
|
+
from .registers import ModbusRegisterMap
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ModbusSerialServer:
|
|
26
|
+
"""Modbus Serial Slave Server (RTU or ASCII)."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
register_map: ModbusRegisterMap,
|
|
31
|
+
port: str = "/dev/ttyUSB0",
|
|
32
|
+
baudrate: int = 9600,
|
|
33
|
+
parity: str = "N",
|
|
34
|
+
stopbits: int = 1,
|
|
35
|
+
bytesize: int = 8,
|
|
36
|
+
timeout: float = 1.0,
|
|
37
|
+
slave_id: int = 1,
|
|
38
|
+
framer: str = "rtu",
|
|
39
|
+
inter_char_delay: Optional[float] = None,
|
|
40
|
+
):
|
|
41
|
+
self.register_map = register_map
|
|
42
|
+
self.port = port
|
|
43
|
+
self.baudrate = baudrate
|
|
44
|
+
self.parity = parity
|
|
45
|
+
self.stopbits = stopbits
|
|
46
|
+
self.bytesize = bytesize
|
|
47
|
+
self.timeout = timeout
|
|
48
|
+
self.slave_id = slave_id
|
|
49
|
+
self.framer = framer
|
|
50
|
+
self.inter_char_delay = inter_char_delay
|
|
51
|
+
self._server = None
|
|
52
|
+
self._slave_context = None
|
|
53
|
+
|
|
54
|
+
def _create_data_blocks(self):
|
|
55
|
+
"""Create Modbus data blocks from register map."""
|
|
56
|
+
max_coil_addr = max(self.register_map.coils.keys()) + 1 if self.register_map.coils else 10
|
|
57
|
+
coil_values = [False] * (max_coil_addr + 1)
|
|
58
|
+
for addr, coil in self.register_map.coils.items():
|
|
59
|
+
coil_values[addr] = coil.value
|
|
60
|
+
coil_block = ModbusSequentialDataBlock(1, coil_values)
|
|
61
|
+
|
|
62
|
+
max_di_addr = max(self.register_map.discrete_inputs.keys()) + 1 if self.register_map.discrete_inputs else 10
|
|
63
|
+
di_values = [False] * (max_di_addr + 1)
|
|
64
|
+
for addr, di in self.register_map.discrete_inputs.items():
|
|
65
|
+
di_values[addr] = di.value
|
|
66
|
+
di_block = ModbusSequentialDataBlock(1, di_values)
|
|
67
|
+
|
|
68
|
+
max_hr_addr = max(self.register_map.holding_registers.keys()) + 1 if self.register_map.holding_registers else 10
|
|
69
|
+
hr_values = [0] * (max_hr_addr + 1)
|
|
70
|
+
for addr, hr in self.register_map.holding_registers.items():
|
|
71
|
+
hr_values[addr] = hr.value
|
|
72
|
+
hr_block = ModbusSequentialDataBlock(1, hr_values)
|
|
73
|
+
|
|
74
|
+
max_ir_addr = max(self.register_map.input_registers.keys()) + 1 if self.register_map.input_registers else 10
|
|
75
|
+
ir_values = [0] * (max_ir_addr + 1)
|
|
76
|
+
for addr, ir in self.register_map.input_registers.items():
|
|
77
|
+
ir_values[addr] = ir.value
|
|
78
|
+
ir_block = ModbusSequentialDataBlock(1, ir_values)
|
|
79
|
+
|
|
80
|
+
return coil_block, di_block, hr_block, ir_block
|
|
81
|
+
|
|
82
|
+
def _create_device_identification(self) -> ModbusDeviceIdentification:
|
|
83
|
+
identification = ModbusDeviceIdentification()
|
|
84
|
+
identification.VendorName = "Modbus Slave Simulator"
|
|
85
|
+
identification.ProductCode = "MBS-001"
|
|
86
|
+
identification.VendorUrl = "https://github.com/yourusername/modbus-slave-sim"
|
|
87
|
+
identification.ProductName = "Modbus Slave Sim"
|
|
88
|
+
identification.ModelName = "Virtual Slave Device"
|
|
89
|
+
identification.MajorMinorRevision = "1.0.0"
|
|
90
|
+
return identification
|
|
91
|
+
|
|
92
|
+
def start(self):
|
|
93
|
+
"""Start the Modbus serial server."""
|
|
94
|
+
coil_block, di_block, hr_block, ir_block = self._create_data_blocks()
|
|
95
|
+
|
|
96
|
+
self._slave_context = ModbusSlaveContext(
|
|
97
|
+
co=coil_block, di=di_block, hr=hr_block, ir=ir_block,
|
|
98
|
+
)
|
|
99
|
+
server_context = ModbusServerContext(
|
|
100
|
+
slaves=self._slave_context, single=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
identification = self._create_device_identification()
|
|
104
|
+
|
|
105
|
+
framer_map = {"rtu": Framer.RTU, "ascii": Framer.ASCII}
|
|
106
|
+
pymodbus_framer = framer_map.get(self.framer, Framer.RTU)
|
|
107
|
+
protocol = "ASCII" if self.framer == "ascii" else "RTU"
|
|
108
|
+
logger.info(f"Starting Modbus {protocol} server on {self.port}")
|
|
109
|
+
logger.info(f"Baudrate: {self.baudrate}, Parity: {self.parity}, Stopbits: {self.stopbits}")
|
|
110
|
+
logger.info(f"Slave ID: {self.slave_id}")
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
kwargs = dict(
|
|
114
|
+
context=server_context,
|
|
115
|
+
identity=identification,
|
|
116
|
+
port=self.port,
|
|
117
|
+
framer=pymodbus_framer,
|
|
118
|
+
baudrate=self.baudrate,
|
|
119
|
+
parity=self.parity,
|
|
120
|
+
stopbits=self.stopbits,
|
|
121
|
+
bytesize=self.bytesize,
|
|
122
|
+
timeout=self.timeout,
|
|
123
|
+
)
|
|
124
|
+
if self.inter_char_delay is not None:
|
|
125
|
+
kwargs["inter_char_delay"] = self.inter_char_delay
|
|
126
|
+
self._server = StartSerialServer(**kwargs)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to start {protocol} server: {e}")
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
def stop(self):
|
|
132
|
+
if self._server:
|
|
133
|
+
self._server.shutdown()
|
|
134
|
+
self._server = None
|
|
135
|
+
logger.info("Modbus serial server stopped")
|
|
136
|
+
|
|
137
|
+
def set_register(self, fc, address, value):
|
|
138
|
+
if self._slave_context:
|
|
139
|
+
self._slave_context.setValues(fc, address, [value])
|
|
140
|
+
|
|
141
|
+
def set_registers(self, fc, address, values):
|
|
142
|
+
if self._slave_context:
|
|
143
|
+
self._slave_context.setValues(fc, address, values)
|
|
144
|
+
|
|
145
|
+
def update_data_blocks(self):
|
|
146
|
+
if not self._slave_context:
|
|
147
|
+
return
|
|
148
|
+
for addr, coil in self.register_map.coils.items():
|
|
149
|
+
self._slave_context.setValues(0x01, addr, [coil.value])
|
|
150
|
+
for addr, di in self.register_map.discrete_inputs.items():
|
|
151
|
+
self._slave_context.setValues(0x02, addr, [di.value])
|
|
152
|
+
for addr, hr in self.register_map.holding_registers.items():
|
|
153
|
+
self._slave_context.setValues(0x03, addr, [hr.value])
|
|
154
|
+
for addr, ir in self.register_map.input_registers.items():
|
|
155
|
+
self._slave_context.setValues(0x04, addr, [ir.value])
|
|
156
|
+
|
|
157
|
+
def is_running(self) -> bool:
|
|
158
|
+
return self._server is not None
|