modsim 0.5.2__tar.gz → 1.0.1__tar.gz
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.
- {modsim-0.5.2 → modsim-1.0.1}/PKG-INFO +23 -12
- {modsim-0.5.2 → modsim-1.0.1}/README.md +17 -4
- modsim-1.0.1/modsim/__init__.py +1 -0
- modsim-1.0.1/modsim/compat.py +131 -0
- modsim-1.0.1/modsim/main.py +252 -0
- {modsim-0.5.2 → modsim-1.0.1}/modsim/utils.py +35 -20
- {modsim-0.5.2 → modsim-1.0.1}/pyproject.toml +9 -5
- modsim-0.5.2/modsim/__init__.py +0 -1
- modsim-0.5.2/modsim/main.py +0 -234
- {modsim-0.5.2 → modsim-1.0.1}/LICENSE +0 -0
- {modsim-0.5.2 → modsim-1.0.1}/modsim/__main__.py +0 -0
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: modsim
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: A Simple Modbus TCP Device Simulator used for modpoll tool
|
|
5
|
-
Home-page: https://github.com/
|
|
5
|
+
Home-page: https://github.com/top-maker/modsim
|
|
6
6
|
Author: Ying Shaodong
|
|
7
7
|
Author-email: helloysd@gmail.com
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Project-URL:
|
|
17
|
-
Project-URL: Repository, https://github.com/gavinying/modsim
|
|
13
|
+
Requires-Dist: pymodbus (>=3.16.1,<3.17.0)
|
|
14
|
+
Project-URL: Documentation, https://top-maker.github.io/modsim/
|
|
15
|
+
Project-URL: Repository, https://github.com/top-maker/modsim
|
|
18
16
|
Description-Content-Type: text/markdown
|
|
19
17
|
|
|
20
18
|
# ModSim - A Simple Modbus TCP Device Simulator
|
|
21
19
|
|
|
22
|
-
*modsim* is a
|
|
23
|
-
|
|
20
|
+
*modsim* is a Modbus device simulator that can run locally or in a container. A public simulator is available at `modsim.topmaker.net:502` for quick testing.
|
|
21
|
+
Its default datastore contains all four Modbus tables:
|
|
24
22
|
- Coil Type
|
|
25
23
|
- Discrete input Type
|
|
26
24
|
- Input register Type
|
|
@@ -89,8 +87,21 @@ To verify it, you can test by [modpoll](https://github.com/gavinying/modpoll) to
|
|
|
89
87
|
modpoll \
|
|
90
88
|
--tcp localhost \
|
|
91
89
|
--tcp-port 5020 \
|
|
92
|
-
--config https://raw.githubusercontent.com/gavinying/modpoll/
|
|
90
|
+
--config https://raw.githubusercontent.com/gavinying/modpoll/main/examples/modsim.csv
|
|
93
91
|
```
|
|
94
92
|
|
|
95
|
-
|
|
93
|
+
To expose the standard Modbus TCP port on the host, use `docker run -p 502:5020 topmaker/modsim:latest`. Your system may require permission to bind port 502.
|
|
94
|
+
|
|
95
|
+
## Local server behavior
|
|
96
|
+
|
|
97
|
+
Run the simulator without Docker with `python -m modsim --comm tcp --port 5020`. The default server exposes the four Modbus tables with the sample data shown above. Coil and holding-register writes are supported; discrete inputs and input registers are read-only. Requests outside an initialized address range return a Modbus exception. The server handles SIGINT and SIGTERM by stopping its listener so the port can be reused after shutdown.
|
|
98
|
+
|
|
99
|
+
The default TCP simulator uses one sequential datastore shared by all Unit IDs.
|
|
100
|
+
The CLI currently supports only `--store sequential`; `--slaves` and `--context`
|
|
101
|
+
are not implemented and are rejected. TCP and UDP use the socket framer, TLS uses
|
|
102
|
+
the TLS framer, and serial accepts RTU or ASCII framing. Serial requires a suitable local device and is not covered by the local protocol regression suite. TLS uses `examples/certificates/pymodbus.crt` and `pymodbus.key` relative to the working directory. A local test covers a self-signed server certificate and a verified client connection; the server does not authenticate client certificates.
|
|
103
|
+
|
|
104
|
+
UDP mode (`--comm udp`) listens on the loopback address and has local read and
|
|
105
|
+
shutdown coverage. TCP and UDP report a startup error if the requested port is
|
|
106
|
+
already in use.
|
|
96
107
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ModSim - A Simple Modbus TCP Device Simulator
|
|
2
2
|
|
|
3
|
-
*modsim* is a
|
|
4
|
-
|
|
3
|
+
*modsim* is a Modbus device simulator that can run locally or in a container. A public simulator is available at `modsim.topmaker.net:502` for quick testing.
|
|
4
|
+
Its default datastore contains all four Modbus tables:
|
|
5
5
|
- Coil Type
|
|
6
6
|
- Discrete input Type
|
|
7
7
|
- Input register Type
|
|
@@ -70,7 +70,20 @@ To verify it, you can test by [modpoll](https://github.com/gavinying/modpoll) to
|
|
|
70
70
|
modpoll \
|
|
71
71
|
--tcp localhost \
|
|
72
72
|
--tcp-port 5020 \
|
|
73
|
-
--config https://raw.githubusercontent.com/gavinying/modpoll/
|
|
73
|
+
--config https://raw.githubusercontent.com/gavinying/modpoll/main/examples/modsim.csv
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
To expose the standard Modbus TCP port on the host, use `docker run -p 502:5020 topmaker/modsim:latest`. Your system may require permission to bind port 502.
|
|
77
|
+
|
|
78
|
+
## Local server behavior
|
|
79
|
+
|
|
80
|
+
Run the simulator without Docker with `python -m modsim --comm tcp --port 5020`. The default server exposes the four Modbus tables with the sample data shown above. Coil and holding-register writes are supported; discrete inputs and input registers are read-only. Requests outside an initialized address range return a Modbus exception. The server handles SIGINT and SIGTERM by stopping its listener so the port can be reused after shutdown.
|
|
81
|
+
|
|
82
|
+
The default TCP simulator uses one sequential datastore shared by all Unit IDs.
|
|
83
|
+
The CLI currently supports only `--store sequential`; `--slaves` and `--context`
|
|
84
|
+
are not implemented and are rejected. TCP and UDP use the socket framer, TLS uses
|
|
85
|
+
the TLS framer, and serial accepts RTU or ASCII framing. Serial requires a suitable local device and is not covered by the local protocol regression suite. TLS uses `examples/certificates/pymodbus.crt` and `pymodbus.key` relative to the working directory. A local test covers a self-signed server certificate and a verified client connection; the server does not authenticate client certificates.
|
|
86
|
+
|
|
87
|
+
UDP mode (`--comm udp`) listens on the loopback address and has local read and
|
|
88
|
+
shutdown coverage. TCP and UDP report a startup error if the requested port is
|
|
89
|
+
already in use.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.1"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Compatibility layer for PyModbus API changes.
|
|
2
|
+
|
|
3
|
+
This module provides backward-compatible wrappers for APIs that changed
|
|
4
|
+
between PyModbus 3.6.9 and 3.16.1.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import struct
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Endian:
|
|
12
|
+
"""Compatibility class for endianness constants removed in PyModbus 3.16.1."""
|
|
13
|
+
|
|
14
|
+
BIG = "big"
|
|
15
|
+
LITTLE = "little"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BinaryPayloadBuilder:
|
|
19
|
+
"""Compatibility wrapper for PyModbus BinaryPayloadBuilder.
|
|
20
|
+
|
|
21
|
+
This class was removed in PyModbus 3.9.0+ but is needed for backward
|
|
22
|
+
compatibility with existing datastore initialization code.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, byteorder: str = "big", wordorder: str = "big") -> None:
|
|
26
|
+
"""Initialize the builder.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
byteorder: Either "big" or "little" for byte order
|
|
30
|
+
wordorder: Either "big" or "little" for word order (register order)
|
|
31
|
+
"""
|
|
32
|
+
self.byteorder = byteorder
|
|
33
|
+
self.wordorder = wordorder
|
|
34
|
+
self._bits: List[bool] = []
|
|
35
|
+
self._registers: List[int] = []
|
|
36
|
+
|
|
37
|
+
def add_bits(self, bits: List[bool]) -> "BinaryPayloadBuilder":
|
|
38
|
+
"""Add a list of bits to the payload.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
bits: List of boolean values
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Self for chaining
|
|
45
|
+
"""
|
|
46
|
+
self._bits.extend(bits)
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def add_16bit_uint(self, value: int) -> "BinaryPayloadBuilder":
|
|
50
|
+
"""Add a 16-bit unsigned integer."""
|
|
51
|
+
self._registers.append(value & 0xFFFF)
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def add_16bit_int(self, value: int) -> "BinaryPayloadBuilder":
|
|
55
|
+
"""Add a 16-bit signed integer."""
|
|
56
|
+
# Convert negative to unsigned representation (two's complement)
|
|
57
|
+
if value < 0:
|
|
58
|
+
value = (1 << 16) + value
|
|
59
|
+
self._registers.append(value & 0xFFFF)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def add_32bit_uint(self, value: int) -> "BinaryPayloadBuilder":
|
|
63
|
+
"""Add a 32-bit unsigned integer (uses 2 registers)."""
|
|
64
|
+
if self.wordorder == "big":
|
|
65
|
+
self._registers.append((value >> 16) & 0xFFFF)
|
|
66
|
+
self._registers.append(value & 0xFFFF)
|
|
67
|
+
else:
|
|
68
|
+
self._registers.append(value & 0xFFFF)
|
|
69
|
+
self._registers.append((value >> 16) & 0xFFFF)
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def add_32bit_int(self, value: int) -> "BinaryPayloadBuilder":
|
|
73
|
+
"""Add a 32-bit signed integer (uses 2 registers)."""
|
|
74
|
+
# Convert to unsigned if negative
|
|
75
|
+
if value < 0:
|
|
76
|
+
value = (1 << 32) + value
|
|
77
|
+
return self.add_32bit_uint(value)
|
|
78
|
+
|
|
79
|
+
def add_32bit_float(self, value: float) -> "BinaryPayloadBuilder":
|
|
80
|
+
"""Add a 32-bit float (uses 2 registers)."""
|
|
81
|
+
fmt = ">f" # big-endian float
|
|
82
|
+
packed = struct.pack(fmt, value)
|
|
83
|
+
value_int = struct.unpack(">I", packed)[0]
|
|
84
|
+
return self.add_32bit_uint(value_int)
|
|
85
|
+
|
|
86
|
+
def add_64bit_uint(self, value: int) -> "BinaryPayloadBuilder":
|
|
87
|
+
"""Add a 64-bit unsigned integer (uses 4 registers)."""
|
|
88
|
+
if self.wordorder == "big":
|
|
89
|
+
self._registers.append((value >> 48) & 0xFFFF)
|
|
90
|
+
self._registers.append((value >> 32) & 0xFFFF)
|
|
91
|
+
self._registers.append((value >> 16) & 0xFFFF)
|
|
92
|
+
self._registers.append(value & 0xFFFF)
|
|
93
|
+
else:
|
|
94
|
+
self._registers.append(value & 0xFFFF)
|
|
95
|
+
self._registers.append((value >> 16) & 0xFFFF)
|
|
96
|
+
self._registers.append((value >> 32) & 0xFFFF)
|
|
97
|
+
self._registers.append((value >> 48) & 0xFFFF)
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def add_64bit_int(self, value: int) -> "BinaryPayloadBuilder":
|
|
101
|
+
"""Add a 64-bit signed integer (uses 4 registers)."""
|
|
102
|
+
# Convert to unsigned if negative
|
|
103
|
+
if value < 0:
|
|
104
|
+
value = (1 << 64) + value
|
|
105
|
+
return self.add_64bit_uint(value)
|
|
106
|
+
|
|
107
|
+
def add_64bit_float(self, value: float) -> "BinaryPayloadBuilder":
|
|
108
|
+
"""Add a 64-bit float (uses 4 registers)."""
|
|
109
|
+
packed = struct.pack(">d", value)
|
|
110
|
+
value_int = struct.unpack(">Q", packed)[0]
|
|
111
|
+
return self.add_64bit_uint(value_int)
|
|
112
|
+
|
|
113
|
+
def add_string(self, value: str) -> "BinaryPayloadBuilder":
|
|
114
|
+
"""Add a string (encoded as UTF-8)."""
|
|
115
|
+
encoded = value.encode("utf-8")
|
|
116
|
+
# Pad to even number of bytes
|
|
117
|
+
if len(encoded) % 2:
|
|
118
|
+
encoded += b"\x00"
|
|
119
|
+
# Convert bytes to registers (2 bytes per register)
|
|
120
|
+
for i in range(0, len(encoded), 2):
|
|
121
|
+
reg_value = (encoded[i] << 8) | encoded[i + 1]
|
|
122
|
+
self._registers.append(reg_value)
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def to_coils(self) -> List[bool]:
|
|
126
|
+
"""Return the bits as a list of booleans."""
|
|
127
|
+
return self._bits
|
|
128
|
+
|
|
129
|
+
def to_registers(self) -> List[int]:
|
|
130
|
+
"""Return the registers as a list of integers."""
|
|
131
|
+
return self._registers
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Modbus simulator server and process lifecycle."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import ssl
|
|
6
|
+
import asyncio
|
|
7
|
+
import signal
|
|
8
|
+
import logging
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pymodbus.datastore import (
|
|
13
|
+
ModbusDeviceContext,
|
|
14
|
+
ModbusSequentialDataBlock,
|
|
15
|
+
ModbusServerContext,
|
|
16
|
+
)
|
|
17
|
+
from pymodbus.pdu.device import ModbusDeviceIdentification
|
|
18
|
+
from pymodbus.server import (
|
|
19
|
+
StartAsyncSerialServer,
|
|
20
|
+
ModbusTcpServer,
|
|
21
|
+
ModbusTlsServer,
|
|
22
|
+
ModbusUdpServer,
|
|
23
|
+
ServerAsyncStop,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from . import __version__
|
|
27
|
+
from .compat import BinaryPayloadBuilder, Endian
|
|
28
|
+
from .utils import get_commandline, get_utc_time, get_logger
|
|
29
|
+
|
|
30
|
+
_logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def setup_server(args: Any) -> Any:
|
|
34
|
+
"""Run server setup."""
|
|
35
|
+
# The datastores only respond to the addresses that are initialized
|
|
36
|
+
# If you initialize a DataBlock to addresses of 0x00 to 0xFF, a request to
|
|
37
|
+
# 0x100 will respond with an invalid address exception.
|
|
38
|
+
# This is because many devices exhibit this kind of behavior (but not all)
|
|
39
|
+
if not args.context:
|
|
40
|
+
_logger.info("Create datastore")
|
|
41
|
+
# build your payload
|
|
42
|
+
co_builder = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.BIG)
|
|
43
|
+
# address=0, bytes=3
|
|
44
|
+
for _ in range(3):
|
|
45
|
+
# The bits within each byte are big-endian
|
|
46
|
+
# The coils status (from the first position) = [1,0,1,0,0,0,0,1]
|
|
47
|
+
co_builder.add_bits([True, False, True, False, False, False, False, True])
|
|
48
|
+
|
|
49
|
+
hr_builder = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.BIG)
|
|
50
|
+
# address=0, bytes=8
|
|
51
|
+
hr_builder.add_16bit_uint(1)
|
|
52
|
+
hr_builder.add_16bit_uint(2)
|
|
53
|
+
hr_builder.add_16bit_uint(3)
|
|
54
|
+
hr_builder.add_16bit_uint(4)
|
|
55
|
+
# address=8, bytes=8
|
|
56
|
+
hr_builder.add_16bit_int(-1)
|
|
57
|
+
hr_builder.add_16bit_int(-2)
|
|
58
|
+
hr_builder.add_16bit_int(-3)
|
|
59
|
+
hr_builder.add_16bit_int(-4)
|
|
60
|
+
# address=16, bytes=8
|
|
61
|
+
hr_builder.add_32bit_uint(12345678)
|
|
62
|
+
hr_builder.add_32bit_uint(12345678)
|
|
63
|
+
# address=24, bytes=8
|
|
64
|
+
hr_builder.add_32bit_int(-12345678)
|
|
65
|
+
hr_builder.add_32bit_int(-12345678)
|
|
66
|
+
# address=32, bytes=8
|
|
67
|
+
hr_builder.add_32bit_float(12.34)
|
|
68
|
+
hr_builder.add_32bit_float(-12.34)
|
|
69
|
+
# address=40, bytes=8
|
|
70
|
+
hr_builder.add_64bit_uint(12345678900)
|
|
71
|
+
# address=48, bytes=8
|
|
72
|
+
hr_builder.add_64bit_int(-12345678900)
|
|
73
|
+
# address=56, bytes=16
|
|
74
|
+
hr_builder.add_64bit_float(123.45)
|
|
75
|
+
hr_builder.add_64bit_float(-123.45)
|
|
76
|
+
# address=72, bytes=?
|
|
77
|
+
hr_builder.add_string("this is a string")
|
|
78
|
+
|
|
79
|
+
# use that payload in the data store
|
|
80
|
+
co_values = co_builder.to_coils()
|
|
81
|
+
hr_values = hr_builder.to_registers()
|
|
82
|
+
# Note: In PyModbus 3.16.1+, ModbusSequentialDataBlock uses address - 1 internally
|
|
83
|
+
# to convert to SimData. To maintain address ranges, we use address + 1 here.
|
|
84
|
+
# This maps Modbus addresses the same way as zero_mode=True did in older versions.
|
|
85
|
+
device_context = ModbusDeviceContext(
|
|
86
|
+
co=ModbusSequentialDataBlock(1, co_values),
|
|
87
|
+
di=ModbusSequentialDataBlock(10001, co_values),
|
|
88
|
+
ir=ModbusSequentialDataBlock(30001, hr_values),
|
|
89
|
+
hr=ModbusSequentialDataBlock(40001, hr_values),
|
|
90
|
+
)
|
|
91
|
+
args.context = ModbusServerContext(devices=device_context, single=True)
|
|
92
|
+
|
|
93
|
+
# initialize the server information
|
|
94
|
+
# If you don't set this or any fields, they are defaulted to empty strings.
|
|
95
|
+
args.identity = ModbusDeviceIdentification(
|
|
96
|
+
info_name={
|
|
97
|
+
"VendorName": "Pymodbus",
|
|
98
|
+
"ProductCode": "PM",
|
|
99
|
+
"VendorUrl": "https://github.com/riptideio/pymodbus/",
|
|
100
|
+
"ProductName": "Pymodbus Server",
|
|
101
|
+
"ModelName": "Pymodbus Server",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
return args
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def run_async_server(args: Any) -> Any:
|
|
108
|
+
"""Run server."""
|
|
109
|
+
txt = f"Start ASYNC server, listening on {args.port} - {args.comm}"
|
|
110
|
+
_logger.info(txt)
|
|
111
|
+
server: Any
|
|
112
|
+
if args.comm == "tcp":
|
|
113
|
+
address: tuple[str, int] = ("", args.port)
|
|
114
|
+
server = ModbusTcpServer(
|
|
115
|
+
context=args.context,
|
|
116
|
+
identity=args.identity,
|
|
117
|
+
address=address,
|
|
118
|
+
framer=args.framer,
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
if not await server.listen():
|
|
122
|
+
raise OSError(
|
|
123
|
+
"Could not start Modbus TCP listener on port %s" % args.port
|
|
124
|
+
)
|
|
125
|
+
await server.serving
|
|
126
|
+
finally:
|
|
127
|
+
await server.shutdown()
|
|
128
|
+
return server
|
|
129
|
+
elif args.comm == "udp":
|
|
130
|
+
address = ("127.0.0.1", args.port)
|
|
131
|
+
server = ModbusUdpServer(
|
|
132
|
+
context=args.context,
|
|
133
|
+
identity=args.identity,
|
|
134
|
+
address=address,
|
|
135
|
+
framer=args.framer,
|
|
136
|
+
)
|
|
137
|
+
try:
|
|
138
|
+
if not await server.listen():
|
|
139
|
+
raise OSError(
|
|
140
|
+
"Could not start Modbus UDP listener on port %s" % args.port
|
|
141
|
+
)
|
|
142
|
+
await server.serving
|
|
143
|
+
finally:
|
|
144
|
+
await server.shutdown()
|
|
145
|
+
return server
|
|
146
|
+
elif args.comm == "serial":
|
|
147
|
+
await StartAsyncSerialServer(
|
|
148
|
+
context=args.context,
|
|
149
|
+
identity=args.identity,
|
|
150
|
+
port=args.port,
|
|
151
|
+
framer=args.framer,
|
|
152
|
+
)
|
|
153
|
+
return None
|
|
154
|
+
elif args.comm == "tls":
|
|
155
|
+
address = ("", args.port)
|
|
156
|
+
cwd = os.getcwd().split("/")[-1]
|
|
157
|
+
if cwd == "examples":
|
|
158
|
+
path = "."
|
|
159
|
+
elif cwd == "test":
|
|
160
|
+
path = "../examples"
|
|
161
|
+
else:
|
|
162
|
+
path = "examples"
|
|
163
|
+
|
|
164
|
+
# Create SSL context from cert and key files
|
|
165
|
+
sslctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
166
|
+
sslctx.load_cert_chain(
|
|
167
|
+
certfile=f"{path}/certificates/pymodbus.crt",
|
|
168
|
+
keyfile=f"{path}/certificates/pymodbus.key",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
server = ModbusTlsServer(
|
|
172
|
+
context=args.context,
|
|
173
|
+
identity=args.identity,
|
|
174
|
+
address=address,
|
|
175
|
+
framer=args.framer,
|
|
176
|
+
sslctx=sslctx,
|
|
177
|
+
)
|
|
178
|
+
try:
|
|
179
|
+
if not await server.listen():
|
|
180
|
+
raise OSError(
|
|
181
|
+
"Could not start Modbus TLS listener on port %s" % args.port
|
|
182
|
+
)
|
|
183
|
+
await server.serving
|
|
184
|
+
finally:
|
|
185
|
+
await server.shutdown()
|
|
186
|
+
return server
|
|
187
|
+
raise ValueError(f"Unknown communication type: {args.comm}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def app() -> None:
|
|
191
|
+
args = get_commandline()
|
|
192
|
+
global _logger
|
|
193
|
+
_logger = get_logger(args.log)
|
|
194
|
+
_logger.info(
|
|
195
|
+
f"Starting modsim v{__version__} at {get_utc_time().strftime('%Y-%m-%d %H:%M:%S')} UTC"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
run_args = setup_server(args)
|
|
199
|
+
asyncio.run(_run_server_with_signals(run_args), debug=True)
|
|
200
|
+
|
|
201
|
+
_logger.info("exiting.")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def _run_server_with_signals(args: Any) -> None:
|
|
205
|
+
"""Run the server and close its listener when the process is signaled."""
|
|
206
|
+
loop = asyncio.get_running_loop()
|
|
207
|
+
server_task = asyncio.create_task(run_async_server(args))
|
|
208
|
+
stop_requested = False
|
|
209
|
+
|
|
210
|
+
async def stop_server() -> None:
|
|
211
|
+
for _ in range(100):
|
|
212
|
+
if server_task.done():
|
|
213
|
+
return
|
|
214
|
+
try:
|
|
215
|
+
await ServerAsyncStop()
|
|
216
|
+
return
|
|
217
|
+
except (AttributeError, RuntimeError):
|
|
218
|
+
await asyncio.sleep(0.01)
|
|
219
|
+
|
|
220
|
+
def request_stop() -> None:
|
|
221
|
+
nonlocal stop_requested
|
|
222
|
+
if not stop_requested:
|
|
223
|
+
stop_requested = True
|
|
224
|
+
if args.comm in ("tcp", "udp", "tls"):
|
|
225
|
+
server_task.cancel()
|
|
226
|
+
else:
|
|
227
|
+
loop.create_task(stop_server())
|
|
228
|
+
|
|
229
|
+
installed_signals = []
|
|
230
|
+
for signum in (signal.SIGINT, signal.SIGTERM):
|
|
231
|
+
try:
|
|
232
|
+
loop.add_signal_handler(signum, request_stop)
|
|
233
|
+
installed_signals.append(signum)
|
|
234
|
+
except (NotImplementedError, RuntimeError):
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
await server_task
|
|
239
|
+
except asyncio.CancelledError:
|
|
240
|
+
if not stop_requested:
|
|
241
|
+
raise
|
|
242
|
+
finally:
|
|
243
|
+
for signum in installed_signals:
|
|
244
|
+
loop.remove_signal_handler(signum)
|
|
245
|
+
if not server_task.done():
|
|
246
|
+
server_task.cancel()
|
|
247
|
+
with suppress(asyncio.CancelledError):
|
|
248
|
+
await server_task
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
app()
|
|
@@ -3,19 +3,13 @@
|
|
|
3
3
|
import argparse
|
|
4
4
|
import logging.config
|
|
5
5
|
from datetime import datetime, timezone
|
|
6
|
-
from typing import Any,
|
|
7
|
-
from pymodbus.
|
|
8
|
-
|
|
9
|
-
ModbusBinaryFramer,
|
|
10
|
-
ModbusRtuFramer,
|
|
11
|
-
ModbusSocketFramer,
|
|
12
|
-
ModbusTlsFramer,
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
from pymodbus.framer import (
|
|
8
|
+
FramerType,
|
|
13
9
|
)
|
|
14
10
|
|
|
15
11
|
from . import __version__
|
|
16
12
|
|
|
17
|
-
# global
|
|
18
|
-
args: Optional[argparse.Namespace] = None
|
|
19
13
|
config: Dict[str, Any] = {}
|
|
20
14
|
|
|
21
15
|
|
|
@@ -35,7 +29,7 @@ def get_parser() -> argparse.ArgumentParser:
|
|
|
35
29
|
)
|
|
36
30
|
parser.add_argument(
|
|
37
31
|
"--framer",
|
|
38
|
-
choices=["ascii", "
|
|
32
|
+
choices=["ascii", "rtu", "socket", "tls"],
|
|
39
33
|
help="set framer, default depends on --comm",
|
|
40
34
|
type=str,
|
|
41
35
|
)
|
|
@@ -74,7 +68,25 @@ def get_parser() -> argparse.ArgumentParser:
|
|
|
74
68
|
|
|
75
69
|
|
|
76
70
|
def get_commandline() -> argparse.Namespace:
|
|
77
|
-
|
|
71
|
+
parser = get_parser()
|
|
72
|
+
args = parser.parse_args()
|
|
73
|
+
|
|
74
|
+
if args.store != "sequential":
|
|
75
|
+
parser.error("only --store sequential is supported")
|
|
76
|
+
if args.slaves != 0:
|
|
77
|
+
parser.error(
|
|
78
|
+
"--slaves is not supported; the default datastore is shared by all unit IDs"
|
|
79
|
+
)
|
|
80
|
+
if args.context is not None:
|
|
81
|
+
parser.error("--context is not supported from the command line")
|
|
82
|
+
allowed_framers = {
|
|
83
|
+
"tcp": {"socket"},
|
|
84
|
+
"udp": {"socket"},
|
|
85
|
+
"serial": {"rtu", "ascii"},
|
|
86
|
+
"tls": {"tls"},
|
|
87
|
+
}
|
|
88
|
+
if args.framer and args.framer not in allowed_framers[args.comm]:
|
|
89
|
+
parser.error("framer %s is not supported for %s" % (args.framer, args.comm))
|
|
78
90
|
|
|
79
91
|
# set defaults
|
|
80
92
|
comm_defaults: Dict[str, list] = {
|
|
@@ -84,26 +96,29 @@ def get_commandline() -> argparse.Namespace:
|
|
|
84
96
|
"tls": ["tls", 5020],
|
|
85
97
|
}
|
|
86
98
|
framers: Dict[str, Any] = {
|
|
87
|
-
"ascii":
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"tls": ModbusTlsFramer,
|
|
99
|
+
"ascii": FramerType.ASCII,
|
|
100
|
+
"rtu": FramerType.RTU,
|
|
101
|
+
"socket": FramerType.SOCKET,
|
|
102
|
+
"tls": FramerType.TLS,
|
|
92
103
|
}
|
|
93
104
|
args.framer = framers[args.framer or comm_defaults[args.comm][0]]
|
|
94
105
|
args.port = args.port or comm_defaults[args.comm][1]
|
|
95
106
|
if args.comm != "serial" and args.port:
|
|
96
|
-
|
|
107
|
+
try:
|
|
108
|
+
args.port = int(args.port)
|
|
109
|
+
except ValueError:
|
|
110
|
+
parser.error("port must be an integer between 1 and 65535")
|
|
111
|
+
if not 1 <= args.port <= 65535:
|
|
112
|
+
parser.error("port must be an integer between 1 and 65535")
|
|
97
113
|
return args
|
|
98
114
|
|
|
99
115
|
|
|
100
|
-
def get_logger() -> logging.Logger:
|
|
116
|
+
def get_logger(level: str = "info") -> logging.Logger:
|
|
101
117
|
if config and config.get("logging"):
|
|
102
118
|
logging.config.dictConfig(config["logging"])
|
|
103
119
|
else:
|
|
104
|
-
print("no config file found, load default settings.")
|
|
105
120
|
logging.basicConfig(
|
|
106
|
-
level=
|
|
121
|
+
level=level.upper(),
|
|
107
122
|
format="%(asctime)s | %(levelname).1s | %(processName)s | %(name)s | %(message)s",
|
|
108
123
|
)
|
|
109
124
|
logger = logging.getLogger(__name__)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "modsim"
|
|
3
|
-
version = "
|
|
3
|
+
version = "1.0.1"
|
|
4
4
|
description = "A Simple Modbus TCP Device Simulator used for modpoll tool"
|
|
5
5
|
authors = ["Ying Shaodong <helloysd@gmail.com>"]
|
|
6
|
-
repository = "https://github.com/
|
|
7
|
-
documentation = "https://
|
|
6
|
+
repository = "https://github.com/top-maker/modsim"
|
|
7
|
+
documentation = "https://top-maker.github.io/modsim/"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
packages = [{ include = "modsim" }]
|
|
10
10
|
|
|
11
11
|
[tool.poetry.dependencies]
|
|
12
|
-
python = ">=3.
|
|
13
|
-
pymodbus = "~3.
|
|
12
|
+
python = ">=3.10,<4.0"
|
|
13
|
+
pymodbus = "~3.16.1"
|
|
14
14
|
|
|
15
15
|
[tool.poetry.group.dev.dependencies]
|
|
16
16
|
pre-commit = "^3.5.0"
|
|
@@ -23,6 +23,9 @@ tox = "^4.16.0"
|
|
|
23
23
|
mkdocs = "^1.6.0"
|
|
24
24
|
mkdocs-material = "^9.5.28"
|
|
25
25
|
mkdocstrings = {extras = ["python"], version = "^0.25.1"}
|
|
26
|
+
mkdocs-autorefs = ">=1.2,<1.4"
|
|
27
|
+
markdown = ">=3.7,<3.8"
|
|
28
|
+
pymdown-extensions = ">=10.11,<11"
|
|
26
29
|
|
|
27
30
|
[build-system]
|
|
28
31
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -40,6 +43,7 @@ show_error_codes = true
|
|
|
40
43
|
|
|
41
44
|
[tool.pytest.ini_options]
|
|
42
45
|
testpaths = ["tests"]
|
|
46
|
+
markers = ["integration: local protocol integration test"]
|
|
43
47
|
|
|
44
48
|
[tool.poetry.scripts]
|
|
45
49
|
modsim = "modsim.main:app"
|
modsim-0.5.2/modsim/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.2"
|
modsim-0.5.2/modsim/main.py
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Pymodbus asynchronous Server Example.
|
|
3
|
-
An example of a multi threaded asynchronous server.
|
|
4
|
-
usage: server_async.py [-h] [--comm {tcp,udp,serial,tls}]
|
|
5
|
-
[--framer {ascii,binary,rtu,socket,tls}]
|
|
6
|
-
[--log {critical,error,warning,info,debug}]
|
|
7
|
-
[--port PORT] [--store {sequential,sparse,factory,none}]
|
|
8
|
-
[--slaves SLAVES]
|
|
9
|
-
Command line options for examples
|
|
10
|
-
options:
|
|
11
|
-
-h, --help show this help message and exit
|
|
12
|
-
--comm {tcp,udp,serial,tls}
|
|
13
|
-
"serial", "tcp", "udp" or "tls"
|
|
14
|
-
--framer {ascii,binary,rtu,socket,tls}
|
|
15
|
-
"ascii", "binary", "rtu", "socket" or "tls"
|
|
16
|
-
--log {critical,error,warning,info,debug}
|
|
17
|
-
"critical", "error", "warning", "info" or "debug"
|
|
18
|
-
--port PORT the port to use
|
|
19
|
-
--store {sequential,sparse,factory,none}
|
|
20
|
-
"sequential", "sparse", "factory" or "none"
|
|
21
|
-
--slaves SLAVES number of slaves to respond to
|
|
22
|
-
The corresponding client can be started as:
|
|
23
|
-
python3 client_sync.py
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
import os
|
|
27
|
-
import asyncio
|
|
28
|
-
from typing import Any
|
|
29
|
-
|
|
30
|
-
from pymodbus.constants import Endian
|
|
31
|
-
from pymodbus.datastore import (
|
|
32
|
-
ModbusSequentialDataBlock,
|
|
33
|
-
ModbusServerContext,
|
|
34
|
-
ModbusSlaveContext,
|
|
35
|
-
)
|
|
36
|
-
from pymodbus.device import ModbusDeviceIdentification
|
|
37
|
-
from pymodbus.payload import BinaryPayloadBuilder
|
|
38
|
-
from pymodbus.server import (
|
|
39
|
-
StartAsyncSerialServer,
|
|
40
|
-
StartAsyncTcpServer,
|
|
41
|
-
StartAsyncTlsServer,
|
|
42
|
-
StartAsyncUdpServer,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
from . import __version__
|
|
46
|
-
from .utils import get_commandline, get_utc_time, get_logger
|
|
47
|
-
|
|
48
|
-
_logger = get_logger()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def setup_server(args: Any) -> Any:
|
|
52
|
-
"""Run server setup."""
|
|
53
|
-
# The datastores only respond to the addresses that are initialized
|
|
54
|
-
# If you initialize a DataBlock to addresses of 0x00 to 0xFF, a request to
|
|
55
|
-
# 0x100 will respond with an invalid address exception.
|
|
56
|
-
# This is because many devices exhibit this kind of behavior (but not all)
|
|
57
|
-
if not args.context:
|
|
58
|
-
_logger.info("Create datastore")
|
|
59
|
-
# ----------------------------------------------------------------------- #
|
|
60
|
-
# build your payload
|
|
61
|
-
# ----------------------------------------------------------------------- #
|
|
62
|
-
co_builder = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.BIG)
|
|
63
|
-
# address=0, bytes=3
|
|
64
|
-
for i in range(3):
|
|
65
|
-
# The bits within each byte are big-endian
|
|
66
|
-
# The coils status (from the first position) = [1,0,1,0,0,0,0,1]
|
|
67
|
-
co_builder.add_bits([True, False, False, False, False, True, False, True])
|
|
68
|
-
|
|
69
|
-
hr_builder = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.BIG)
|
|
70
|
-
# address=0, bytes=8
|
|
71
|
-
hr_builder.add_16bit_uint(1)
|
|
72
|
-
hr_builder.add_16bit_uint(2)
|
|
73
|
-
hr_builder.add_16bit_uint(3)
|
|
74
|
-
hr_builder.add_16bit_uint(4)
|
|
75
|
-
# address=8, bytes=8
|
|
76
|
-
hr_builder.add_16bit_int(-1)
|
|
77
|
-
hr_builder.add_16bit_int(-2)
|
|
78
|
-
hr_builder.add_16bit_int(-3)
|
|
79
|
-
hr_builder.add_16bit_int(-4)
|
|
80
|
-
# address=16, bytes=8
|
|
81
|
-
hr_builder.add_32bit_uint(12345678)
|
|
82
|
-
hr_builder.add_32bit_uint(12345678)
|
|
83
|
-
# address=24, bytes=8
|
|
84
|
-
hr_builder.add_32bit_int(-12345678)
|
|
85
|
-
hr_builder.add_32bit_int(-12345678)
|
|
86
|
-
# address=32, bytes=8
|
|
87
|
-
hr_builder.add_32bit_float(12.34)
|
|
88
|
-
hr_builder.add_32bit_float(-12.34)
|
|
89
|
-
# address=40, bytes=8
|
|
90
|
-
hr_builder.add_64bit_uint(12345678900)
|
|
91
|
-
# address=48, bytes=8
|
|
92
|
-
hr_builder.add_64bit_int(-12345678900)
|
|
93
|
-
# address=56, bytes=16
|
|
94
|
-
hr_builder.add_64bit_float(123.45)
|
|
95
|
-
hr_builder.add_64bit_float(-123.45)
|
|
96
|
-
# address=72, bytes=?
|
|
97
|
-
hr_builder.add_string("this is a string")
|
|
98
|
-
|
|
99
|
-
# ----------------------------------------------------------------------- #
|
|
100
|
-
# use that payload in the data store
|
|
101
|
-
# ----------------------------------------------------------------------- #
|
|
102
|
-
# Here we use the same reference block for each underlying store.
|
|
103
|
-
# ----------------------------------------------------------------------- #
|
|
104
|
-
co_values = co_builder.to_coils()
|
|
105
|
-
hr_values = hr_builder.to_registers()
|
|
106
|
-
store = ModbusSlaveContext(
|
|
107
|
-
co=ModbusSequentialDataBlock(0, co_values),
|
|
108
|
-
di=ModbusSequentialDataBlock(10000, co_values),
|
|
109
|
-
ir=ModbusSequentialDataBlock(30000, hr_values),
|
|
110
|
-
hr=ModbusSequentialDataBlock(40000, hr_values),
|
|
111
|
-
zero_mode=True,
|
|
112
|
-
)
|
|
113
|
-
args.context = ModbusServerContext(slaves=store, single=True)
|
|
114
|
-
|
|
115
|
-
# ----------------------------------------------------------------------- #
|
|
116
|
-
# initialize the server information
|
|
117
|
-
# ----------------------------------------------------------------------- #
|
|
118
|
-
# If you don't set this or any fields, they are defaulted to empty strings.
|
|
119
|
-
# ----------------------------------------------------------------------- #
|
|
120
|
-
args.identity = ModbusDeviceIdentification(
|
|
121
|
-
info_name={
|
|
122
|
-
"VendorName": "Pymodbus",
|
|
123
|
-
"ProductCode": "PM",
|
|
124
|
-
"VendorUrl": "https://github.com/riptideio/pymodbus/",
|
|
125
|
-
"ProductName": "Pymodbus Server",
|
|
126
|
-
"ModelName": "Pymodbus Server",
|
|
127
|
-
}
|
|
128
|
-
)
|
|
129
|
-
return args
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
async def run_async_server(args: Any) -> Any:
|
|
133
|
-
"""Run server."""
|
|
134
|
-
txt = f"Start ASYNC server, listening on {args.port} - {args.comm}"
|
|
135
|
-
_logger.info(txt)
|
|
136
|
-
if args.comm == "tcp":
|
|
137
|
-
address = ("", args.port) if args.port else None
|
|
138
|
-
server = await StartAsyncTcpServer(
|
|
139
|
-
context=args.context, # Data storage
|
|
140
|
-
identity=args.identity, # server identify
|
|
141
|
-
# TBD host=
|
|
142
|
-
# TBD port=
|
|
143
|
-
address=address, # listen address
|
|
144
|
-
# custom_functions=[], # allow custom handling
|
|
145
|
-
framer=args.framer, # The framer strategy to use
|
|
146
|
-
# ignore_missing_slaves=True, # ignore request to a missing slave
|
|
147
|
-
# broadcast_enable=False, # treat slave_id 0 as broadcast address,
|
|
148
|
-
# timeout=1, # waiting time for request to complete
|
|
149
|
-
# TBD strict=True, # use strict timing, t1.5 for Modbus RTU
|
|
150
|
-
)
|
|
151
|
-
elif args.comm == "udp":
|
|
152
|
-
address = ("127.0.0.1", args.port) if args.port else None
|
|
153
|
-
server = await StartAsyncUdpServer(
|
|
154
|
-
context=args.context, # Data storage
|
|
155
|
-
identity=args.identity, # server identify
|
|
156
|
-
address=address, # listen address
|
|
157
|
-
# custom_functions=[], # allow custom handling
|
|
158
|
-
framer=args.framer, # The framer strategy to use
|
|
159
|
-
# handler=None, # handler for each session
|
|
160
|
-
# ignore_missing_slaves=True, # ignore request to a missing slave
|
|
161
|
-
# broadcast_enable=False, # treat unit_id 0 as broadcast address,
|
|
162
|
-
# timeout=1, # waiting time for request to complete
|
|
163
|
-
# TBD strict=True, # use strict timing, t1.5 for Modbus RTU
|
|
164
|
-
# defer_start=False, # Only define server do not activate
|
|
165
|
-
)
|
|
166
|
-
elif args.comm == "serial":
|
|
167
|
-
# socat -d -d PTY,link=/tmp/ptyp0,raw,echo=0,ispeed=9600
|
|
168
|
-
# PTY,link=/tmp/ttyp0,raw,echo=0,ospeed=9600
|
|
169
|
-
server = await StartAsyncSerialServer(
|
|
170
|
-
context=args.context, # Data storage
|
|
171
|
-
identity=args.identity, # server identify
|
|
172
|
-
# timeout=1, # waiting time for request to complete
|
|
173
|
-
port=args.port, # serial port
|
|
174
|
-
# custom_functions=[], # allow custom handling
|
|
175
|
-
framer=args.framer, # The framer strategy to use
|
|
176
|
-
# handler=None, # handler for each session
|
|
177
|
-
# stopbits=1, # The number of stop bits to use
|
|
178
|
-
# bytesize=8, # The bytesize of the serial messages
|
|
179
|
-
# parity="N", # Which kind of parity to use
|
|
180
|
-
# baudrate=9600, # The baud rate to use for the serial device
|
|
181
|
-
# handle_local_echo=False, # Handle local echo of the USB-to-RS485 adaptor
|
|
182
|
-
# ignore_missing_slaves=True, # ignore request to a missing slave
|
|
183
|
-
# broadcast_enable=False, # treat unit_id 0 as broadcast address,
|
|
184
|
-
# strict=True, # use strict timing, t1.5 for Modbus RTU
|
|
185
|
-
# defer_start=False, # Only define server do not activate
|
|
186
|
-
)
|
|
187
|
-
elif args.comm == "tls":
|
|
188
|
-
address = ("", args.port) if args.port else None
|
|
189
|
-
cwd = os.getcwd().split("/")[-1]
|
|
190
|
-
if cwd == "examples":
|
|
191
|
-
path = "."
|
|
192
|
-
elif cwd == "test":
|
|
193
|
-
path = "../examples"
|
|
194
|
-
else:
|
|
195
|
-
path = "examples"
|
|
196
|
-
server = await StartAsyncTlsServer(
|
|
197
|
-
context=args.context, # Data storage
|
|
198
|
-
host="localhost", # define tcp address where to connect to.
|
|
199
|
-
# port=port, # on which port
|
|
200
|
-
identity=args.identity, # server identify
|
|
201
|
-
# custom_functions=[], # allow custom handling
|
|
202
|
-
address=address, # listen address
|
|
203
|
-
framer=args.framer, # The framer strategy to use
|
|
204
|
-
# handler=None, # handler for each session
|
|
205
|
-
allow_reuse_address=True, # allow the reuse of an address
|
|
206
|
-
# The cert file path for TLS (used if sslctx is None)
|
|
207
|
-
certfile=f"{path}/certificates/pymodbus.crt",
|
|
208
|
-
# sslctx=sslctx, # The SSLContext to use for TLS (default None and auto create)
|
|
209
|
-
# The key file path for TLS (used if sslctx is None)
|
|
210
|
-
keyfile=f"{path}/certificates/pymodbus.key",
|
|
211
|
-
# password="none", # The password for for decrypting the private key file
|
|
212
|
-
# reqclicert=False, # Force the sever request client"s certificate
|
|
213
|
-
# ignore_missing_slaves=True, # ignore request to a missing slave
|
|
214
|
-
# broadcast_enable=False, # treat unit_id 0 as broadcast address,
|
|
215
|
-
# timeout=1, # waiting time for request to complete
|
|
216
|
-
# TBD strict=True, # use strict timing, t1.5 for Modbus RTU
|
|
217
|
-
defer_start=False, # Only define server do not activate
|
|
218
|
-
)
|
|
219
|
-
return server
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def app() -> None:
|
|
223
|
-
_logger.info(
|
|
224
|
-
f"Starting modsim v{__version__} at {get_utc_time().strftime('%Y-%m-%d %H:%M:%S')} UTC"
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
run_args = setup_server(get_commandline())
|
|
228
|
-
asyncio.run(run_async_server(run_args), debug=True)
|
|
229
|
-
|
|
230
|
-
_logger.info("exiting.")
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if __name__ == "__main__":
|
|
234
|
-
app()
|
|
File without changes
|
|
File without changes
|