Skip to content

Commit 8b3af2a

Browse files
authored
WEB simulator, first version. (#1226)
1 parent 7883a72 commit 8b3af2a

25 files changed

+1643
-77
lines changed

pymodbus/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
__logging.getLogger(__name__).addHandler(__null())
2020

2121

22-
def pymodbus_apply_logging_config():
22+
def pymodbus_apply_logging_config(level=__logging.WARNING):
2323
"""Apply basic logging configuration used by default by Pymodbus maintainers.
2424
2525
Please call this function to format logging appropriately when opening issues.
2626
"""
2727
__logging.basicConfig(
2828
format="%(asctime)s %(levelname)-5s %(module)s:%(lineno)s %(message)s",
2929
datefmt="%H:%M:%S",
30-
level=__logging.WARNING,
30+
level=level,
3131
)

pymodbus/datastore/simulator.py

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
CELL_TYPE_FLOAT32 = "F"
1919
CELL_TYPE_STRING = "S"
2020
CELL_TYPE_NEXT = "n"
21-
CELL_TYPE_ILLEGAL = "X"
21+
CELL_TYPE_INVALID = "X"
2222

2323
WORD_SIZE = 16
2424

@@ -30,7 +30,9 @@ class Cell:
3030
type: int = CELL_TYPE_NONE
3131
access: bool = False
3232
value: int = 0
33-
action: Callable = None
33+
action: int = 0
34+
count_read: int = 0
35+
count_write: int = 0
3436

3537

3638
@dataclasses.dataclass
@@ -42,6 +44,7 @@ class Label: # pylint: disable=too-many-instance-attributes
4244

4345
action: str = "action"
4446
addr: str = "addr"
47+
any: str = "any"
4548
co_size: str = "co size"
4649
defaults: str = "defaults"
4750
di_size: str = "di size"
@@ -52,6 +55,7 @@ class Label: # pylint: disable=too-many-instance-attributes
5255
method: str = "method"
5356
next: str = "next"
5457
random: str = "random"
58+
register: str = "register"
5559
repeat: str = "repeat"
5660
reset: str = "reset"
5761
setup: str = "setup"
@@ -231,7 +235,7 @@ def handle_type_string(self, registers, reg_count, start, stop, value, action):
231235
registers[inx].value = int.from_bytes(
232236
bytes(value[i * 2 : (i + 1) * 2], "UTF-8"), "big"
233237
)
234-
registers[inx].type = CELL_TYPE_STRING
238+
registers[inx].type = CELL_TYPE_STRING if not i else CELL_TYPE_NEXT
235239
registers[start].action = action
236240

237241
def handle_setup_section(self, config, actions):
@@ -300,7 +304,7 @@ def handle_invalid_address(self, registers, reg_count, config):
300304
if registers[i].type != CELL_TYPE_NONE:
301305
txt = f'ERROR Configuration invalid in section "invalid" register {i} already defined'
302306
raise RuntimeError(txt)
303-
registers[i].type = CELL_TYPE_ILLEGAL
307+
registers[i].type = CELL_TYPE_INVALID
304308

305309
def handle_write_allowed(self, registers, reg_count, config):
306310
"""Handle write allowed"""
@@ -312,6 +316,9 @@ def handle_write_allowed(self, registers, reg_count, config):
312316
raise RuntimeError(
313317
f'Error section "{Label.write}" addr {entry} out of range'
314318
)
319+
if registers[i].type in (CELL_TYPE_NONE, CELL_TYPE_INVALID):
320+
txt = f'ERROR Configuration invalid in section "write" register {i} not defined'
321+
raise RuntimeError(txt)
315322
registers[i].access = True
316323

317324
def handle_types(self, registers, actions, reg_count, config):
@@ -351,25 +358,20 @@ def handle_repeat(self, registers, reg_count, config):
351358
)
352359
registers[inx] = dataclasses.replace(registers[copy_inx])
353360

354-
def setup(self, config, actions, custom_actions) -> None:
361+
def setup(self, config, actions) -> None:
355362
"""Load layout from dict with json structure.
356363
357364
:meta private:
358365
"""
359-
actions[""] = None
360-
actions[None] = None
361-
if custom_actions:
362-
actions.update(custom_actions)
363-
364366
registers, offset, typ_exc = self.handle_setup_section(config, actions)
365367
reg_count = len(registers)
366368
self.handle_invalid_address(registers, reg_count, config)
367-
self.handle_write_allowed(registers, reg_count, config)
368369
self.handle_types(registers, actions, reg_count, config)
370+
self.handle_write_allowed(registers, reg_count, config)
369371
self.handle_repeat(registers, reg_count, config)
370372
for i in range(reg_count):
371373
if registers[i].type == CELL_TYPE_NONE:
372-
registers[i].type = CELL_TYPE_ILLEGAL
374+
registers[i].type = CELL_TYPE_INVALID
373375

374376
return (registers, offset, typ_exc, reg_count)
375377

@@ -468,16 +470,42 @@ class ModbusSimulatorContext:
468470
# --------------------------------------------
469471
start_time = int(datetime.now().timestamp())
470472

471-
def __init__(self, config: Dict[str, any], actions: Dict[str, Callable]) -> None:
473+
def __init__(
474+
self, config: Dict[str, any], custom_actions: Dict[str, Callable]
475+
) -> None:
472476
"""Initialize."""
473-
builtin_actions = {
477+
self.action_names = {
474478
Label.increment: self.action_increment,
479+
Label.register: self.action_register,
475480
Label.random: self.action_random,
476481
Label.reset: self.action_reset,
477482
Label.timestamp: self.action_timestamp,
478483
Label.uptime: self.action_uptime,
479484
}
480-
res = Setup().setup(config, builtin_actions, actions)
485+
if custom_actions:
486+
self.action_names.update(custom_actions)
487+
j = len(self.action_names) + 1
488+
self.action_inx_to_name = ["None"] * j
489+
self.action_methods = [None] * j
490+
j = 1
491+
for key, method in self.action_names.items():
492+
self.action_inx_to_name[j] = key
493+
self.action_methods[j] = method
494+
self.action_names[key] = j
495+
j += 1
496+
self.action_names[None] = 0
497+
self.type_names = {
498+
Label.type_none: CELL_TYPE_NONE,
499+
Label.type_bits: CELL_TYPE_BIT,
500+
Label.type_uint16: CELL_TYPE_UINT16,
501+
Label.type_uint32: CELL_TYPE_UINT32,
502+
Label.type_float32: CELL_TYPE_FLOAT32,
503+
Label.type_string: CELL_TYPE_STRING,
504+
Label.next: CELL_TYPE_NEXT,
505+
Label.invalid: CELL_TYPE_INVALID,
506+
Label.any: None,
507+
}
508+
res = Setup().setup(config, self.action_names)
481509
self.registers = res[0]
482510
self.offset = res[1]
483511
self.type_exception = res[2]
@@ -506,7 +534,7 @@ def validate(self, func_code, address, count=1):
506534
fx_write = func_code in self._write_func_code
507535
for i in range(real_address, real_address + count):
508536
reg = self.registers[i]
509-
if reg.type == CELL_TYPE_ILLEGAL:
537+
if reg.type == CELL_TYPE_INVALID:
510538
return False
511539
if fx_write and not reg.access:
512540
return False
@@ -525,7 +553,8 @@ def getValues(self, func_code, address, count=1): # pylint: disable=invalid-nam
525553
for i in range(real_address, real_address + count):
526554
reg = self.registers[i]
527555
if reg.action:
528-
reg.action(self.registers, i, reg)
556+
self.action_methods[reg.action](self.registers, i, reg)
557+
self.registers[i].count_read += 1
529558
result.append(reg.value)
530559
else:
531560
# bit access
@@ -535,7 +564,8 @@ def getValues(self, func_code, address, count=1): # pylint: disable=invalid-nam
535564
for i in range(real_address, real_address + reg_count):
536565
reg = self.registers[i]
537566
if reg.action:
538-
reg.action(i, reg)
567+
self.action_methods[reg.action](i, reg)
568+
self.registers[i].count_read += 1
539569
while count and bit_index < 16:
540570
result.append(bool(reg.value & (2**bit_index)))
541571
count -= 1
@@ -552,6 +582,7 @@ def setValues(self, func_code, address, values): # pylint: disable=invalid-name
552582
real_address = self.offset[func_code] + address
553583
for value in values:
554584
self.registers[real_address].value = value
585+
self.registers[real_address].count_write += 1
555586
real_address += 1
556587
return
557588

@@ -564,6 +595,7 @@ def setValues(self, func_code, address, values): # pylint: disable=invalid-name
564595
self.registers[real_address].value |= bit_mask
565596
else:
566597
self.registers[real_address].value &= ~bit_mask
598+
self.registers[real_address].count_write += 1
567599
bit_index += 1
568600
if bit_index == 16:
569601
bit_index = 0
@@ -574,6 +606,25 @@ def setValues(self, func_code, address, values): # pylint: disable=invalid-name
574606
# Internal action methods
575607
# --------------------------------------------
576608

609+
@classmethod
610+
def action_register(cls, registers, inx, cell):
611+
"""Update with register number.
612+
613+
:meta private:
614+
"""
615+
if cell.type == CELL_TYPE_BIT:
616+
registers[inx].value = inx
617+
elif cell.type == CELL_TYPE_FLOAT32:
618+
regs = cls.build_registers_from_value(float(inx), False)
619+
registers[inx].value = regs[0]
620+
registers[inx + 1].value = regs[1]
621+
elif cell.type == CELL_TYPE_UINT16:
622+
registers[inx].value = inx
623+
elif cell.type == CELL_TYPE_UINT32:
624+
regs = cls.build_registers_from_value(inx, True)
625+
registers[inx].value = regs[0]
626+
registers[inx + 1].value = regs[1]
627+
577628
@classmethod
578629
def action_random(cls, registers, inx, cell):
579630
"""Update with random value.
@@ -583,13 +634,15 @@ def action_random(cls, registers, inx, cell):
583634
if cell.type == CELL_TYPE_BIT:
584635
registers[inx].value = random.randint(0, 65536)
585636
elif cell.type == CELL_TYPE_FLOAT32:
586-
regs = cls.build_registers_from_value(random.uniform(0.0, 100.0), False)
637+
regs = cls.build_registers_from_value(random.uniform(0.0, 65000.0), False)
587638
registers[inx].value = regs[0]
588639
registers[inx + 1].value = regs[1]
589640
elif cell.type == CELL_TYPE_UINT16:
590641
registers[inx].value = random.randint(0, 65536)
591642
elif cell.type == CELL_TYPE_UINT32:
592-
regs = cls.build_registers_from_value(random.uniform(0.0, 100.0), True)
643+
regs = cls.build_registers_from_value(
644+
int(random.uniform(0.0, 65000.0)), True
645+
)
593646
registers[inx].value = regs[0]
594647
registers[inx + 1].value = regs[1]
595648

pymodbus/server/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import external classes, to make them easier to use:
44
"""
55
from pymodbus.server.async_io import (
6+
ModbusSerialServer,
7+
ModbusTcpServer,
8+
ModbusTlsServer,
9+
ModbusUdpServer,
610
ServerAsyncStop,
711
ServerStop,
812
StartAsyncSerialServer,
@@ -14,18 +18,24 @@
1418
StartTlsServer,
1519
StartUdpServer,
1620
)
21+
from pymodbus.server.simulator.http_server import ModbusSimulatorServer
1722

1823

1924
# ---------------------------------------------------------------------------#
2025
# Exported symbols
2126
# ---------------------------------------------------------------------------#
2227
__all__ = [
28+
"ModbusSerialServer",
29+
"ModbusSimulatorServer",
30+
"ModbusTcpServer",
31+
"ModbusTlsServer",
32+
"ModbusUdpServer",
2333
"ServerAsyncStop",
2434
"ServerStop",
35+
"StartAsyncSerialServer",
2536
"StartAsyncTcpServer",
2637
"StartAsyncTlsServer",
2738
"StartAsyncUdpServer",
28-
"StartAsyncSerialServer",
2939
"StartSerialServer",
3040
"StartTcpServer",
3141
"StartTlsServer",

0 commit comments

Comments
 (0)