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,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulation engine for dynamic register value generation.
|
|
3
|
+
|
|
4
|
+
This module provides different simulation types for generating dynamic
|
|
5
|
+
values in Modbus registers:
|
|
6
|
+
- Sine wave
|
|
7
|
+
- Linear increment/decrement
|
|
8
|
+
- Random values
|
|
9
|
+
- Constant value
|
|
10
|
+
- Counter
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
import random
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from .registers import BaseRegister, ModbusRegisterMap
|
|
21
|
+
from .colors import c, RED_BOLD
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SimulationType(Enum):
|
|
25
|
+
"""Available simulation types."""
|
|
26
|
+
CONSTANT = "constant"
|
|
27
|
+
SINE = "sine"
|
|
28
|
+
LINEAR = "linear"
|
|
29
|
+
RANDOM = "random"
|
|
30
|
+
COUNTER = "counter"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SimulationConfig:
|
|
34
|
+
"""Configuration for a single register simulation."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
sim_type: SimulationType,
|
|
39
|
+
min_value: float = 0,
|
|
40
|
+
max_value: float = 10000,
|
|
41
|
+
amplitude: float = 1000,
|
|
42
|
+
frequency: float = 0.1,
|
|
43
|
+
offset: float = 5000,
|
|
44
|
+
step: float = 1,
|
|
45
|
+
is_signed: bool = False,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initialize simulation configuration.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
sim_type: Type of simulation
|
|
52
|
+
min_value: Minimum value (for random, linear wrapping)
|
|
53
|
+
max_value: Maximum value (for random, linear wrapping)
|
|
54
|
+
amplitude: Amplitude for sine wave
|
|
55
|
+
frequency: Frequency in Hz for sine wave
|
|
56
|
+
offset: Offset (center) for sine wave
|
|
57
|
+
step: Step size for linear and counter
|
|
58
|
+
is_signed: Whether values should be treated as signed
|
|
59
|
+
"""
|
|
60
|
+
self.sim_type = sim_type
|
|
61
|
+
self.min_value = min_value
|
|
62
|
+
self.max_value = max_value
|
|
63
|
+
self.amplitude = amplitude
|
|
64
|
+
self.frequency = frequency
|
|
65
|
+
self.offset = offset
|
|
66
|
+
self.step = step
|
|
67
|
+
self.is_signed = is_signed
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
70
|
+
"""Convert to dictionary."""
|
|
71
|
+
return {
|
|
72
|
+
"sim_type": self.sim_type.value,
|
|
73
|
+
"min_value": self.min_value,
|
|
74
|
+
"max_value": self.max_value,
|
|
75
|
+
"amplitude": self.amplitude,
|
|
76
|
+
"frequency": self.frequency,
|
|
77
|
+
"offset": self.offset,
|
|
78
|
+
"step": self.step,
|
|
79
|
+
"is_signed": self.is_signed,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SimulationEngine:
|
|
84
|
+
"""
|
|
85
|
+
Engine for simulating dynamic values in Modbus registers.
|
|
86
|
+
|
|
87
|
+
Runs in a background thread and updates register values based on
|
|
88
|
+
their simulation configuration.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, register_map: ModbusRegisterMap):
|
|
92
|
+
"""
|
|
93
|
+
Initialize the simulation engine.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
register_map: The register map to simulate values for
|
|
97
|
+
"""
|
|
98
|
+
self.register_map = register_map
|
|
99
|
+
self._running = False
|
|
100
|
+
self._thread: Optional[threading.Thread] = None
|
|
101
|
+
self._interval = 0.1 # Update interval in seconds (100ms)
|
|
102
|
+
self._start_time = time.time()
|
|
103
|
+
self._counters: Dict[int, float] = {}
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def interval(self) -> float:
|
|
107
|
+
"""Get the update interval."""
|
|
108
|
+
return self._interval
|
|
109
|
+
|
|
110
|
+
@interval.setter
|
|
111
|
+
def interval(self, seconds: float):
|
|
112
|
+
"""Set the update interval."""
|
|
113
|
+
self._interval = max(0.01, seconds) # Minimum 10ms
|
|
114
|
+
|
|
115
|
+
def configure_simulation(
|
|
116
|
+
self,
|
|
117
|
+
address: int,
|
|
118
|
+
reg_type: str,
|
|
119
|
+
config: SimulationConfig,
|
|
120
|
+
):
|
|
121
|
+
"""
|
|
122
|
+
Configure simulation for a specific register.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
address: Register address
|
|
126
|
+
reg_type: Register type ('coils', 'discrete_inputs', 'input_registers', 'holding_registers')
|
|
127
|
+
config: Simulation configuration
|
|
128
|
+
"""
|
|
129
|
+
register = self._get_register(address, reg_type)
|
|
130
|
+
if register:
|
|
131
|
+
register.simulation_enabled = True
|
|
132
|
+
register.simulation_config = config.to_dict()
|
|
133
|
+
self._counters[address] = 0.0
|
|
134
|
+
|
|
135
|
+
def remove_simulation(self, address: int, reg_type: str):
|
|
136
|
+
"""Remove simulation from a register."""
|
|
137
|
+
register = self._get_register(address, reg_type)
|
|
138
|
+
if register:
|
|
139
|
+
register.simulation_enabled = False
|
|
140
|
+
register.simulation_config = {}
|
|
141
|
+
self._counters.pop(address, None)
|
|
142
|
+
|
|
143
|
+
def _get_register(self, address: int, reg_type: str) -> Optional[BaseRegister]:
|
|
144
|
+
"""Get register by type and address."""
|
|
145
|
+
reg_map = {
|
|
146
|
+
"coils": self.register_map.coils,
|
|
147
|
+
"discrete_inputs": self.register_map.discrete_inputs,
|
|
148
|
+
"input_registers": self.register_map.input_registers,
|
|
149
|
+
"holding_registers": self.register_map.holding_registers,
|
|
150
|
+
}
|
|
151
|
+
return reg_map.get(reg_type, {}).get(address)
|
|
152
|
+
|
|
153
|
+
def _calculate_value(
|
|
154
|
+
self,
|
|
155
|
+
config: SimulationConfig,
|
|
156
|
+
address: int,
|
|
157
|
+
current_time: float,
|
|
158
|
+
) -> Any:
|
|
159
|
+
"""
|
|
160
|
+
Calculate the next value based on simulation type.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
config: Simulation configuration
|
|
164
|
+
address: Register address
|
|
165
|
+
current_time: Current timestamp
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Calculated value
|
|
169
|
+
"""
|
|
170
|
+
elapsed = current_time - self._start_time
|
|
171
|
+
|
|
172
|
+
if config.sim_type == SimulationType.SINE:
|
|
173
|
+
# Sine wave: offset + amplitude * sin(2π * frequency * time)
|
|
174
|
+
value = config.offset + config.amplitude * math.sin(
|
|
175
|
+
2 * math.pi * config.frequency * elapsed
|
|
176
|
+
)
|
|
177
|
+
return int(value) & 0xFFFF
|
|
178
|
+
|
|
179
|
+
elif config.sim_type == SimulationType.LINEAR:
|
|
180
|
+
# Linear increment with wrapping
|
|
181
|
+
counter = self._counters.get(address, 0.0)
|
|
182
|
+
counter += config.step
|
|
183
|
+
if counter > config.max_value:
|
|
184
|
+
counter = config.min_value
|
|
185
|
+
self._counters[address] = counter
|
|
186
|
+
return int(counter) & 0xFFFF
|
|
187
|
+
|
|
188
|
+
elif config.sim_type == SimulationType.RANDOM:
|
|
189
|
+
# Random value within range
|
|
190
|
+
value = random.randint(int(config.min_value), int(config.max_value))
|
|
191
|
+
return value & 0xFFFF
|
|
192
|
+
|
|
193
|
+
elif config.sim_type == SimulationType.COUNTER:
|
|
194
|
+
# Monotonically increasing counter
|
|
195
|
+
counter = self._counters.get(address, 0.0)
|
|
196
|
+
counter += config.step
|
|
197
|
+
if counter > 65535:
|
|
198
|
+
counter = config.min_value
|
|
199
|
+
self._counters[address] = counter
|
|
200
|
+
return int(counter) & 0xFFFF
|
|
201
|
+
|
|
202
|
+
elif config.sim_type == SimulationType.CONSTANT:
|
|
203
|
+
# Constant value (no change)
|
|
204
|
+
return int(config.offset) & 0xFFFF
|
|
205
|
+
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
def _update_registers(self):
|
|
209
|
+
"""Update all simulated registers with new values."""
|
|
210
|
+
current_time = time.time()
|
|
211
|
+
|
|
212
|
+
for register in self.register_map.get_simulated_registers():
|
|
213
|
+
try:
|
|
214
|
+
config_dict = register.simulation_config
|
|
215
|
+
if not config_dict:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
config = SimulationConfig(
|
|
219
|
+
sim_type=SimulationType(config_dict.get("sim_type", "constant")),
|
|
220
|
+
min_value=config_dict.get("min_value", 0),
|
|
221
|
+
max_value=config_dict.get("max_value", 10000),
|
|
222
|
+
amplitude=config_dict.get("amplitude", 1000),
|
|
223
|
+
frequency=config_dict.get("frequency", 0.1),
|
|
224
|
+
offset=config_dict.get("offset", 5000),
|
|
225
|
+
step=config_dict.get("step", 1),
|
|
226
|
+
is_signed=config_dict.get("is_signed", False),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
new_value = self._calculate_value(config, register.address, current_time)
|
|
230
|
+
register.value = new_value
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print(c(f"Simulation error for register {register.address}: {e}", RED_BOLD))
|
|
234
|
+
|
|
235
|
+
def _simulation_loop(self):
|
|
236
|
+
"""Main simulation loop running in background thread."""
|
|
237
|
+
while self._running:
|
|
238
|
+
self._update_registers()
|
|
239
|
+
time.sleep(self._interval)
|
|
240
|
+
|
|
241
|
+
def start(self):
|
|
242
|
+
"""Start the simulation engine."""
|
|
243
|
+
if self._running:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
self._running = True
|
|
247
|
+
self._start_time = time.time()
|
|
248
|
+
self._thread = threading.Thread(
|
|
249
|
+
target=self._simulation_loop,
|
|
250
|
+
daemon=True,
|
|
251
|
+
name="SimulationEngine"
|
|
252
|
+
)
|
|
253
|
+
self._thread.start()
|
|
254
|
+
|
|
255
|
+
def stop(self):
|
|
256
|
+
"""Stop the simulation engine."""
|
|
257
|
+
self._running = False
|
|
258
|
+
if self._thread:
|
|
259
|
+
self._thread.join(timeout=2.0)
|
|
260
|
+
self._thread = None
|
|
261
|
+
|
|
262
|
+
def is_running(self) -> bool:
|
|
263
|
+
"""Check if the simulation engine is running."""
|
|
264
|
+
return self._running
|
|
265
|
+
|
|
266
|
+
def setup_default_simulations(self):
|
|
267
|
+
"""
|
|
268
|
+
Set up default simulations for demonstration purposes.
|
|
269
|
+
|
|
270
|
+
Configures:
|
|
271
|
+
- Input Registers 0-4: Sine waves with different frequencies
|
|
272
|
+
- Input Register 5-6: Counter
|
|
273
|
+
- Holding Register 9: Linear increment
|
|
274
|
+
"""
|
|
275
|
+
# Input registers: sine waves
|
|
276
|
+
for i in range(5):
|
|
277
|
+
self.configure_simulation(
|
|
278
|
+
i, "input_registers",
|
|
279
|
+
SimulationConfig(
|
|
280
|
+
sim_type=SimulationType.SINE,
|
|
281
|
+
amplitude=2000 + i * 500,
|
|
282
|
+
frequency=0.05 + i * 0.02,
|
|
283
|
+
offset=5000 + i * 1000,
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Input registers: counters
|
|
288
|
+
for i in range(5, 7):
|
|
289
|
+
self.configure_simulation(
|
|
290
|
+
i, "input_registers",
|
|
291
|
+
SimulationConfig(
|
|
292
|
+
sim_type=SimulationType.COUNTER,
|
|
293
|
+
step=1,
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Holding register 9: linear increment
|
|
298
|
+
self.configure_simulation(
|
|
299
|
+
9, "holding_registers",
|
|
300
|
+
SimulationConfig(
|
|
301
|
+
sim_type=SimulationType.LINEAR,
|
|
302
|
+
min_value=0,
|
|
303
|
+
max_value=1000,
|
|
304
|
+
step=10,
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def get_simulation_status(self) -> List[Dict[str, Any]]:
|
|
309
|
+
"""Get status of all simulated registers."""
|
|
310
|
+
status = []
|
|
311
|
+
for register in self.register_map.get_simulated_registers():
|
|
312
|
+
status.append({
|
|
313
|
+
"address": register.address,
|
|
314
|
+
"value": register.value,
|
|
315
|
+
"description": register.description,
|
|
316
|
+
"config": register.simulation_config,
|
|
317
|
+
})
|
|
318
|
+
return status
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modbus TCP Server implementation.
|
|
3
|
+
|
|
4
|
+
This module provides a Modbus TCP slave server that handles
|
|
5
|
+
incoming TCP connections and processes Modbus requests.
|
|
6
|
+
Supports standard Modbus TCP (default) as well as RTU/ASCII
|
|
7
|
+
over TCP for virtual serial port simulation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from pymodbus.datastore import (
|
|
15
|
+
ModbusSequentialDataBlock,
|
|
16
|
+
ModbusSlaveContext,
|
|
17
|
+
ModbusServerContext,
|
|
18
|
+
)
|
|
19
|
+
from pymodbus.device import ModbusDeviceIdentification
|
|
20
|
+
from pymodbus.server import StartTcpServer
|
|
21
|
+
from pymodbus.framer import Framer
|
|
22
|
+
|
|
23
|
+
from .registers import ModbusRegisterMap
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ModbusTCPServer:
|
|
29
|
+
"""
|
|
30
|
+
Modbus TCP Slave Server.
|
|
31
|
+
|
|
32
|
+
Provides a Modbus TCP server that simulates a slave device with
|
|
33
|
+
configurable registers and optional simulation.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
register_map: ModbusRegisterMap,
|
|
39
|
+
host: str = "0.0.0.0",
|
|
40
|
+
port: int = 502,
|
|
41
|
+
slave_id: int = 1,
|
|
42
|
+
framer: str = "tcp",
|
|
43
|
+
):
|
|
44
|
+
self.register_map = register_map
|
|
45
|
+
self.host = host
|
|
46
|
+
self.port = port
|
|
47
|
+
self.slave_id = slave_id
|
|
48
|
+
self.framer = framer
|
|
49
|
+
self._server = None
|
|
50
|
+
self._slave_context = None
|
|
51
|
+
|
|
52
|
+
def _create_data_blocks(self):
|
|
53
|
+
"""Create Modbus data blocks from register map."""
|
|
54
|
+
max_coil_addr = max(self.register_map.coils.keys()) + 1 if self.register_map.coils else 10
|
|
55
|
+
coil_values = [False] * (max_coil_addr + 1)
|
|
56
|
+
for addr, coil in self.register_map.coils.items():
|
|
57
|
+
coil_values[addr] = coil.value
|
|
58
|
+
coil_block = ModbusSequentialDataBlock(1, coil_values)
|
|
59
|
+
|
|
60
|
+
max_di_addr = max(self.register_map.discrete_inputs.keys()) + 1 if self.register_map.discrete_inputs else 10
|
|
61
|
+
di_values = [False] * (max_di_addr + 1)
|
|
62
|
+
for addr, di in self.register_map.discrete_inputs.items():
|
|
63
|
+
di_values[addr] = di.value
|
|
64
|
+
di_block = ModbusSequentialDataBlock(1, di_values)
|
|
65
|
+
|
|
66
|
+
max_hr_addr = max(self.register_map.holding_registers.keys()) + 1 if self.register_map.holding_registers else 10
|
|
67
|
+
hr_values = [0] * (max_hr_addr + 1)
|
|
68
|
+
for addr, hr in self.register_map.holding_registers.items():
|
|
69
|
+
hr_values[addr] = hr.value
|
|
70
|
+
hr_block = ModbusSequentialDataBlock(1, hr_values)
|
|
71
|
+
|
|
72
|
+
max_ir_addr = max(self.register_map.input_registers.keys()) + 1 if self.register_map.input_registers else 10
|
|
73
|
+
ir_values = [0] * (max_ir_addr + 1)
|
|
74
|
+
for addr, ir in self.register_map.input_registers.items():
|
|
75
|
+
ir_values[addr] = ir.value
|
|
76
|
+
ir_block = ModbusSequentialDataBlock(1, ir_values)
|
|
77
|
+
|
|
78
|
+
return coil_block, di_block, hr_block, ir_block
|
|
79
|
+
|
|
80
|
+
def _create_device_identification(self) -> ModbusDeviceIdentification:
|
|
81
|
+
identification = ModbusDeviceIdentification()
|
|
82
|
+
identification.VendorName = "Modbus Slave Simulator"
|
|
83
|
+
identification.ProductCode = "MBS-001"
|
|
84
|
+
identification.VendorUrl = "https://github.com/yourusername/modbus-slave-sim"
|
|
85
|
+
identification.ProductName = "Modbus Slave Sim"
|
|
86
|
+
identification.ModelName = "Virtual Slave Device"
|
|
87
|
+
identification.MajorMinorRevision = "1.0.0"
|
|
88
|
+
return identification
|
|
89
|
+
|
|
90
|
+
def start(self):
|
|
91
|
+
"""Start the Modbus TCP server in a background thread."""
|
|
92
|
+
coil_block, di_block, hr_block, ir_block = self._create_data_blocks()
|
|
93
|
+
|
|
94
|
+
self._slave_context = ModbusSlaveContext(
|
|
95
|
+
co=coil_block, di=di_block, hr=hr_block, ir=ir_block,
|
|
96
|
+
)
|
|
97
|
+
server_context = ModbusServerContext(
|
|
98
|
+
slaves=self._slave_context, single=True,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
identification = self._create_device_identification()
|
|
102
|
+
|
|
103
|
+
protocol_name = self._get_protocol_name()
|
|
104
|
+
logger.info(f"Starting Modbus {protocol_name} server on {self.host}:{self.port}")
|
|
105
|
+
logger.info(f"Slave ID: {self.slave_id}")
|
|
106
|
+
|
|
107
|
+
framer_map = {
|
|
108
|
+
"tcp": Framer.SOCKET,
|
|
109
|
+
"rtu": Framer.RTU,
|
|
110
|
+
"ascii": Framer.ASCII,
|
|
111
|
+
"rtu-tcp": Framer.RTU,
|
|
112
|
+
"ascii-tcp": Framer.ASCII,
|
|
113
|
+
}
|
|
114
|
+
pymodbus_framer = framer_map.get(self.framer, Framer.RTU)
|
|
115
|
+
|
|
116
|
+
def _run_server():
|
|
117
|
+
try:
|
|
118
|
+
self._server = StartTcpServer(
|
|
119
|
+
context=server_context,
|
|
120
|
+
identity=identification,
|
|
121
|
+
address=(self.host, self.port),
|
|
122
|
+
framer=pymodbus_framer,
|
|
123
|
+
)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.error(f"Failed to start TCP server: {e}")
|
|
126
|
+
raise
|
|
127
|
+
|
|
128
|
+
self._thread = threading.Thread(target=_run_server, daemon=True, name="ModbusTCPServer")
|
|
129
|
+
self._thread.start()
|
|
130
|
+
|
|
131
|
+
def _get_protocol_name(self) -> str:
|
|
132
|
+
names = {"tcp": "TCP", "rtu": "RTU (Serial)", "ascii": "ASCII (Serial)",
|
|
133
|
+
"rtu-tcp": "RTU over TCP", "ascii-tcp": "ASCII over TCP"}
|
|
134
|
+
return names.get(self.framer, self.framer)
|
|
135
|
+
|
|
136
|
+
def stop(self):
|
|
137
|
+
if self._server:
|
|
138
|
+
self._server.shutdown()
|
|
139
|
+
self._server = None
|
|
140
|
+
logger.info("Modbus TCP server stopped")
|
|
141
|
+
|
|
142
|
+
def set_register(self, fc, address, value):
|
|
143
|
+
"""Set a register value via the slave context.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
fc: Function code (3=HR, 4=IR, 1=CO, 2=DI)
|
|
147
|
+
address: Register address
|
|
148
|
+
value: New value
|
|
149
|
+
"""
|
|
150
|
+
if self._slave_context:
|
|
151
|
+
self._slave_context.setValues(fc, address, [value])
|
|
152
|
+
|
|
153
|
+
def set_registers(self, fc, address, values):
|
|
154
|
+
"""Set multiple register values via the slave context.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
fc: Function code (3=HR, 4=IR, 1=CO, 2=DI)
|
|
158
|
+
address: Starting register address
|
|
159
|
+
values: List of new values
|
|
160
|
+
"""
|
|
161
|
+
if self._slave_context:
|
|
162
|
+
self._slave_context.setValues(fc, address, values)
|
|
163
|
+
|
|
164
|
+
def update_data_blocks(self):
|
|
165
|
+
"""Update server data blocks from register map."""
|
|
166
|
+
if not self._slave_context:
|
|
167
|
+
return
|
|
168
|
+
for addr, coil in self.register_map.coils.items():
|
|
169
|
+
self._slave_context.setValues(0x01, addr, [coil.value])
|
|
170
|
+
for addr, di in self.register_map.discrete_inputs.items():
|
|
171
|
+
self._slave_context.setValues(0x02, addr, [di.value])
|
|
172
|
+
for addr, hr in self.register_map.holding_registers.items():
|
|
173
|
+
self._slave_context.setValues(0x03, addr, [hr.value])
|
|
174
|
+
for addr, ir in self.register_map.input_registers.items():
|
|
175
|
+
self._slave_context.setValues(0x04, addr, [ir.value])
|
|
176
|
+
|
|
177
|
+
def is_running(self) -> bool:
|
|
178
|
+
return self._server is not None
|