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
modbus_slave_sim/cli.py
ADDED
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Modbus Tool - A comprehensive Modbus Master/Slave testing tool.
|
|
4
|
+
|
|
5
|
+
A combined Modbus testing tool supporting both Master and Slave modes.
|
|
6
|
+
Similar to modpoll + diagslave, this tool allows you to:
|
|
7
|
+
- Act as a Master to read/write Modbus devices
|
|
8
|
+
- Act as a Slave to simulate Modbus devices
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
mbtool [mode] [options]
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
# Master mode - read 10 holding registers
|
|
15
|
+
mbtool --master -a 1 -c 10 -r 0
|
|
16
|
+
|
|
17
|
+
# Slave mode - start TCP server
|
|
18
|
+
mbtool --slave -p 502
|
|
19
|
+
|
|
20
|
+
# Master mode - write float value
|
|
21
|
+
mbtool --master --write -t float -a 1 -r 0 -v 3.14
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import csv
|
|
26
|
+
import logging
|
|
27
|
+
import signal
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
from pymodbus.framer import Framer # noqa: F401 - used by rtu_server
|
|
33
|
+
|
|
34
|
+
from . import __version__
|
|
35
|
+
from .registers import ModbusRegisterMap
|
|
36
|
+
from .simulation import SimulationEngine, SimulationType
|
|
37
|
+
from .tcp_server import ModbusTCPServer
|
|
38
|
+
from .rtu_server import ModbusSerialServer
|
|
39
|
+
from .master import ModbusMaster
|
|
40
|
+
from .datatypes import DataType, ByteOrder, parse_data_type, parse_byte_order, format_value
|
|
41
|
+
from .colors import (
|
|
42
|
+
c, RST, BOLD, DIM, RED, RED_BOLD, GREEN, GREEN_BOLD,
|
|
43
|
+
YELLOW, YELLOW_BOLD, CYAN, CYAN_BOLD, MAGENTA, MAGENTA_BOLD, WHITE_BOLD,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def setup_logging(verbose: bool = False):
|
|
48
|
+
"""Configure logging based on verbosity level."""
|
|
49
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
50
|
+
logging.basicConfig(
|
|
51
|
+
level=level,
|
|
52
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
53
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def enable_debug_frame():
|
|
58
|
+
"""Enable raw Modbus frame logging via pymodbus debug loggers."""
|
|
59
|
+
for name in ["pymodbus", "pymodbus.client", "pymodbus.server",
|
|
60
|
+
"pymodbus.transport", "pymodbus.framer", "pymodbus.logging"]:
|
|
61
|
+
logging.getLogger(name).setLevel(logging.DEBUG)
|
|
62
|
+
logging.getLogger("pymodbus").addHandler(logging.StreamHandler())
|
|
63
|
+
print(c("Raw frame debug logging enabled", DIM))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_alarm(expr: str) -> dict:
|
|
67
|
+
"""Parse alarm expression like '0>100' or 'addr>threshold'."""
|
|
68
|
+
import re
|
|
69
|
+
m = re.match(r"(\d+)\s*(>|>=|<|<=|==|!=)\s*(-?\d+\.?\d*)", expr)
|
|
70
|
+
if not m:
|
|
71
|
+
print(c(f"Error: Invalid alarm expression '{expr}'. Format: ADDR>VALUE (e.g. 0>100)", RED_BOLD))
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
addr = int(m.group(1))
|
|
74
|
+
op = m.group(2)
|
|
75
|
+
val_str = m.group(3)
|
|
76
|
+
return {"addr": addr, "op": op, "value": float(val_str) if "." in val_str else int(val_str)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_args(args=None):
|
|
80
|
+
"""Parse command line arguments."""
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
description="Modbus Tool - A comprehensive Modbus Master/Slave testing tool",
|
|
83
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
84
|
+
epilog=f"""
|
|
85
|
+
{c('Usage Examples:', BOLD)}
|
|
86
|
+
{c('─'*60, DIM)}
|
|
87
|
+
|
|
88
|
+
{c('SLAVE MODE', CYAN_BOLD)}
|
|
89
|
+
%(prog)s --slave Start TCP server on port 502
|
|
90
|
+
%(prog)s --slave -p 5020 Start TCP on port 5020
|
|
91
|
+
%(prog)s --slave --simulate Enable default simulation
|
|
92
|
+
%(prog)s --slave -m rtu -p COM3 Start RTU server on COM3
|
|
93
|
+
%(prog)s --slave -m ascii -p COM3 Start ASCII server on COM3
|
|
94
|
+
%(prog)s --slave --register-map dev.json Load custom register map
|
|
95
|
+
|
|
96
|
+
{c('MASTER MODE - Read', CYAN_BOLD)}
|
|
97
|
+
%(prog)s --master -a 1 -r 0 -c 10 Read 10 holding registers (FC03)
|
|
98
|
+
%(prog)s --master -a 1 -r 0 -c 5 -t float Read 5 float values (2 regs each)
|
|
99
|
+
%(prog)s --master -a 1 -r 0 -c 10 -t hex Read in hex format
|
|
100
|
+
%(prog)s --master -f 1 -a 1 -r 0 -c 8 Read 8 coils (FC01)
|
|
101
|
+
%(prog)s --master -f 2 -a 1 -r 0 -c 8 Read 8 discrete inputs (FC02)
|
|
102
|
+
%(prog)s --master -f 4 -a 1 -r 0 -c 5 Read 5 input registers (FC04)
|
|
103
|
+
|
|
104
|
+
{c('MASTER MODE - Write', CYAN_BOLD)}
|
|
105
|
+
%(prog)s --master --write -a 1 -r 0 -v 100 Write 100 to register
|
|
106
|
+
%(prog)s --master --write -t float -a 1 -r 0 -v 3.14 Write float value
|
|
107
|
+
%(prog)s --master --write -a 0 -v "100,200,300" Batch write 3 values
|
|
108
|
+
%(prog)s --master --write-coil -r 0 --coil-state 1 Write coil ON (FC05)
|
|
109
|
+
|
|
110
|
+
{c('MASTER MODE - Poll', CYAN_BOLD)}
|
|
111
|
+
%(prog)s --master --poll -a 1 -r 0 -c 5 -i 2 Poll every 2s
|
|
112
|
+
%(prog)s --master --poll -i 1 -n 10 Poll 10 times then stop
|
|
113
|
+
%(prog)s --master --poll --alarm "0>100" Alarm on threshold
|
|
114
|
+
%(prog)s --master --poll --output data.csv Log results to CSV
|
|
115
|
+
|
|
116
|
+
{c('ADVANCED', CYAN_BOLD)}
|
|
117
|
+
%(prog)s --master -t float32 --byte-order CDBA Use word-swap byte order
|
|
118
|
+
%(prog)s --master --debug-frame Show raw Modbus frames
|
|
119
|
+
%(prog)s --scan -H 192.168.1.100 -p 502 Scan slave IDs 1-247
|
|
120
|
+
|
|
121
|
+
{c('Examples with output:', BOLD)}
|
|
122
|
+
{c('─'*60, DIM)}
|
|
123
|
+
|
|
124
|
+
# Read 10 holding registers (simplest read command)
|
|
125
|
+
%(prog)s --master -a 1 -r 0 -c 10
|
|
126
|
+
Output:
|
|
127
|
+
Reading 10 register(s) from address 0...
|
|
128
|
+
Results (10 values):
|
|
129
|
+
[0] = 100
|
|
130
|
+
[1] = 200
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
# Continuous monitor with CSV logging and alarm
|
|
134
|
+
%(prog)s --master --poll -a 1 -r 0 -c 5 -t int16 -i 1 --output log.csv --alarm "0>1000"
|
|
135
|
+
Output:
|
|
136
|
+
Polling register 0, count=5, type=int16
|
|
137
|
+
Interval: 1.0s, Iterations: infinite
|
|
138
|
+
Output: log.csv
|
|
139
|
+
Alarm: register[0] > 1000
|
|
140
|
+
Press Ctrl+C to stop
|
|
141
|
+
|
|
142
|
+
[1] 150 !!! ALARM: register[0] > 1000 (current: 150) !!!
|
|
143
|
+
[2] 45
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
{c('Function Codes:', BOLD)}
|
|
147
|
+
1 Read Coils FC01 Read/write boolean coils
|
|
148
|
+
2 Read Discrete Inputs FC02 Read-only boolean inputs
|
|
149
|
+
3 Read Holding Registers FC03 Read/write 16-bit registers (default)
|
|
150
|
+
4 Read Input Registers FC04 Read-only 16-bit registers
|
|
151
|
+
|
|
152
|
+
{c('Data Types:', BOLD)}
|
|
153
|
+
int16 Signed 16-bit integer 1 register Range: -32768 to 32767
|
|
154
|
+
uint16 Unsigned 16-bit integer 1 register Range: 0 to 65535
|
|
155
|
+
int32 Signed 32-bit integer 2 registers Range: -2^31 to 2^31-1
|
|
156
|
+
uint32 Unsigned 32-bit integer 2 registers Range: 0 to 2^32-1
|
|
157
|
+
float 32-bit IEEE 754 float 2 registers ~7 decimal digits precision
|
|
158
|
+
hex Hexadecimal view 1+ registers e.g. 0xABCD
|
|
159
|
+
|
|
160
|
+
{c('Byte Order Options (for 32-bit types):', BOLD)}
|
|
161
|
+
ABCD Big-Endian (standard) Reg0=High, Reg1=Low
|
|
162
|
+
CDBA Word swap (Siemens) Reg0=Low, Reg1=High
|
|
163
|
+
BADC Byte swap Bytes swapped within each register
|
|
164
|
+
DCBA Little-Endian Full little-endian
|
|
165
|
+
|
|
166
|
+
{c('More info:', DIM)}
|
|
167
|
+
Documentation: https://github.com/yourusername/modbus-tool#readme
|
|
168
|
+
Report issues: https://github.com/yourusername/modbus-tool/issues
|
|
169
|
+
""",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# ── Mode selection ──
|
|
173
|
+
mode_group = parser.add_mutually_exclusive_group()
|
|
174
|
+
mode_group.add_argument(
|
|
175
|
+
"--master",
|
|
176
|
+
action="store_true",
|
|
177
|
+
help="作为主站(客户端)读写从站设备",
|
|
178
|
+
)
|
|
179
|
+
mode_group.add_argument(
|
|
180
|
+
"--slave",
|
|
181
|
+
action="store_true",
|
|
182
|
+
help="作为从站(服务端)模拟一个 Modbus 设备",
|
|
183
|
+
)
|
|
184
|
+
mode_group.add_argument(
|
|
185
|
+
"--scan",
|
|
186
|
+
action="store_true",
|
|
187
|
+
help="扫描模式:探测 1-247 号从站 ID,发现网络中活动的设备",
|
|
188
|
+
)
|
|
189
|
+
mode_group.add_argument(
|
|
190
|
+
"--gui",
|
|
191
|
+
action="store_true",
|
|
192
|
+
help="启动 Web 图形界面(GUI),在浏览器中操作所有功能",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# ── Connection options ──
|
|
196
|
+
conn = parser.add_argument_group("Connection Options (连接选项)")
|
|
197
|
+
conn.add_argument(
|
|
198
|
+
"-m", "--mode",
|
|
199
|
+
metavar="PROTOCOL",
|
|
200
|
+
choices=["tcp", "rtu", "ascii", "rtu-tcp", "ascii-tcp"],
|
|
201
|
+
default="tcp",
|
|
202
|
+
help="通信协议:tcp=Modbus TCP标准协议, rtu=串口RTU(需真实串口), ascii=串口ASCII(需真实串口),\n"
|
|
203
|
+
" rtu-tcp=RTU over TCP(虚拟串口, 无需物理串口),\n"
|
|
204
|
+
" ascii-tcp=ASCII over TCP(虚拟串口, 无需物理串口) (默认: tcp)",
|
|
205
|
+
)
|
|
206
|
+
conn.add_argument(
|
|
207
|
+
"-H", "--host",
|
|
208
|
+
metavar="IP",
|
|
209
|
+
default="127.0.0.1",
|
|
210
|
+
help="TCP 模式下目标主机的 IP 地址 (默认: 127.0.0.1)",
|
|
211
|
+
)
|
|
212
|
+
conn.add_argument(
|
|
213
|
+
"-p", "--port",
|
|
214
|
+
metavar="PORT/PATH",
|
|
215
|
+
type=str,
|
|
216
|
+
default="502",
|
|
217
|
+
help="TCP 端口号(如 502) 或串口设备路径(如 COM3, /dev/ttyUSB0)。\n"
|
|
218
|
+
" TCP 模式填端口号,RTU/ASCII 模式填串口路径 (默认: 502)",
|
|
219
|
+
)
|
|
220
|
+
conn.add_argument(
|
|
221
|
+
"-a", "--slave-id",
|
|
222
|
+
metavar="ID",
|
|
223
|
+
type=str,
|
|
224
|
+
default="1",
|
|
225
|
+
help="从站设备 ID 号(1-247)。单个: 1, 多个逗号分隔: 1,2,3 (默认: 1)",
|
|
226
|
+
)
|
|
227
|
+
conn.add_argument(
|
|
228
|
+
"--timeout",
|
|
229
|
+
metavar="SEC",
|
|
230
|
+
type=float,
|
|
231
|
+
default=3.0,
|
|
232
|
+
help="等待从站响应的超时时长(秒),超时视为通信失败 (默认: 3.0)",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# ── Serial options ──
|
|
236
|
+
serial = parser.add_argument_group("Serial Port Options (串口参数, RTU/ASCII 模式)")
|
|
237
|
+
serial.add_argument(
|
|
238
|
+
"-b", "--baudrate",
|
|
239
|
+
metavar="RATE",
|
|
240
|
+
type=int,
|
|
241
|
+
default=9600,
|
|
242
|
+
help="串口波特率:300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200 (默认: 9600)",
|
|
243
|
+
)
|
|
244
|
+
serial.add_argument(
|
|
245
|
+
"--parity",
|
|
246
|
+
metavar="P",
|
|
247
|
+
choices=["N", "E", "O"],
|
|
248
|
+
default="N",
|
|
249
|
+
help="串口校验位:N=无校验, E=偶校验, O=奇校验 (默认: N)",
|
|
250
|
+
)
|
|
251
|
+
serial.add_argument(
|
|
252
|
+
"--stopbits",
|
|
253
|
+
metavar="BITS",
|
|
254
|
+
type=int,
|
|
255
|
+
choices=[1, 2],
|
|
256
|
+
default=1,
|
|
257
|
+
help="停止位个数:1 或 2 (默认: 1)",
|
|
258
|
+
)
|
|
259
|
+
serial.add_argument(
|
|
260
|
+
"--bytesize",
|
|
261
|
+
metavar="BITS",
|
|
262
|
+
type=int,
|
|
263
|
+
choices=[7, 8],
|
|
264
|
+
default=8,
|
|
265
|
+
help="每字节数据位:7 或 8 (默认: 8)",
|
|
266
|
+
)
|
|
267
|
+
serial.add_argument(
|
|
268
|
+
"--inter-char-delay",
|
|
269
|
+
metavar="SEC",
|
|
270
|
+
type=float,
|
|
271
|
+
default=None,
|
|
272
|
+
help="RTU 模式下字符间额外延迟(秒),用于解决部分设备的通信时序问题",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# ── Master read options ──
|
|
276
|
+
rd = parser.add_argument_group("Master Read Options (主站读取参数)")
|
|
277
|
+
rd.add_argument(
|
|
278
|
+
"-r", "--register",
|
|
279
|
+
metavar="ADDR",
|
|
280
|
+
type=int,
|
|
281
|
+
default=0,
|
|
282
|
+
help="要读取的起始寄存器/线圈地址编号 (默认: 0)",
|
|
283
|
+
)
|
|
284
|
+
rd.add_argument(
|
|
285
|
+
"-c", "--count",
|
|
286
|
+
metavar="N",
|
|
287
|
+
type=int,
|
|
288
|
+
default=1,
|
|
289
|
+
help="连续读取的寄存器/线圈/离散输入的数量 (默认: 1)",
|
|
290
|
+
)
|
|
291
|
+
rd.add_argument(
|
|
292
|
+
"-t", "--data-type",
|
|
293
|
+
metavar="TYPE",
|
|
294
|
+
type=str,
|
|
295
|
+
default="int16",
|
|
296
|
+
help="寄存器数值的解释格式:int16, uint16, int32, uint32, float, hex\n"
|
|
297
|
+
" int16=有符号16位整数, uint16=无符号16位整数,\n"
|
|
298
|
+
" int32=有符号32位(占2寄存器), float=32位浮点数(占2寄存器),\n"
|
|
299
|
+
" hex=十六进制显示 (默认: int16)",
|
|
300
|
+
)
|
|
301
|
+
rd.add_argument(
|
|
302
|
+
"-f", "--function-code",
|
|
303
|
+
metavar="FC",
|
|
304
|
+
type=int,
|
|
305
|
+
choices=[1, 2, 3, 4],
|
|
306
|
+
default=3,
|
|
307
|
+
help="Modbus 功能码:\n"
|
|
308
|
+
" 1=读线圈(FC01, 布尔值, 可读写)\n"
|
|
309
|
+
" 2=读离散输入(FC02, 布尔值, 只读)\n"
|
|
310
|
+
" 3=读保持寄存器(FC03, 16位值, 可读写) ←默认\n"
|
|
311
|
+
" 4=读输入寄存器(FC04, 16位值, 只读)",
|
|
312
|
+
)
|
|
313
|
+
rd.add_argument(
|
|
314
|
+
"--hex",
|
|
315
|
+
action="store_true",
|
|
316
|
+
help="同时以十六进制(0xABCD)格式显示数值",
|
|
317
|
+
)
|
|
318
|
+
rd.add_argument(
|
|
319
|
+
"--byte-order",
|
|
320
|
+
metavar="ORDER",
|
|
321
|
+
type=str,
|
|
322
|
+
default="ABCD",
|
|
323
|
+
help="32位数据(int32/uint32/float)的字节排列顺序:\n"
|
|
324
|
+
" ABCD=标准大端序(寄存器0=高16位, 1=低16位)\n"
|
|
325
|
+
" CDBA=西门子字交换(寄存器0=低16位, 1=高16位)\n"
|
|
326
|
+
" BADC=字节交换\n"
|
|
327
|
+
" DCBA=小端序(完整小端) (默认: ABCD)",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# ── Master write options ──
|
|
331
|
+
wr = parser.add_argument_group("Master Write Options (主站写入参数)")
|
|
332
|
+
wr.add_argument(
|
|
333
|
+
"--write",
|
|
334
|
+
action="store_true",
|
|
335
|
+
help="写入寄存器模式,配合 --value 指定要写入的值。\n"
|
|
336
|
+
" 示例:--write -r 0 --value 100 写入单值\n"
|
|
337
|
+
" 示例:--write -r 0 --value \"100,200,300\" 批量写入",
|
|
338
|
+
)
|
|
339
|
+
wr.add_argument(
|
|
340
|
+
"--write-coil",
|
|
341
|
+
action="store_true",
|
|
342
|
+
help="写入线圈模式(FC05),配合 --coil-state 指定 ON/OFF",
|
|
343
|
+
)
|
|
344
|
+
wr.add_argument(
|
|
345
|
+
"--coil-state",
|
|
346
|
+
metavar="0/1",
|
|
347
|
+
type=int,
|
|
348
|
+
choices=[0, 1],
|
|
349
|
+
default=1,
|
|
350
|
+
help="线圈目标状态:0=断开(OFF), 1=闭合(ON) (默认: 1)",
|
|
351
|
+
)
|
|
352
|
+
wr.add_argument(
|
|
353
|
+
"--value",
|
|
354
|
+
"-v",
|
|
355
|
+
metavar="VAL",
|
|
356
|
+
type=str,
|
|
357
|
+
help="要写入的值:\n"
|
|
358
|
+
" 单个值如 \"100\" 或 \"3.14\"\n"
|
|
359
|
+
" 批量值用逗号分隔如 \"100,200,300\" 会依次写入连续地址",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# ── Poll options ──
|
|
363
|
+
pl = parser.add_argument_group("Polling Options (轮询参数)")
|
|
364
|
+
pl.add_argument(
|
|
365
|
+
"--poll", "--continuous",
|
|
366
|
+
action="store_true",
|
|
367
|
+
help="连续轮询模式:按固定间隔反复读取寄存器值,按 Ctrl+C 停止",
|
|
368
|
+
)
|
|
369
|
+
pl.add_argument(
|
|
370
|
+
"-i", "--interval",
|
|
371
|
+
metavar="SEC",
|
|
372
|
+
type=float,
|
|
373
|
+
default=1.0,
|
|
374
|
+
help="轮询的时间间隔(秒)。设为 0.5 表示每秒读 2 次 (默认: 1.0)",
|
|
375
|
+
)
|
|
376
|
+
pl.add_argument(
|
|
377
|
+
"-n", "--iterations",
|
|
378
|
+
metavar="N",
|
|
379
|
+
type=int,
|
|
380
|
+
default=0,
|
|
381
|
+
help="轮询总次数,达到后自动停止。0 表示无限轮询直到 Ctrl+C (默认: 0)",
|
|
382
|
+
)
|
|
383
|
+
pl.add_argument(
|
|
384
|
+
"--alarm",
|
|
385
|
+
metavar="EXPR",
|
|
386
|
+
type=str,
|
|
387
|
+
default=None,
|
|
388
|
+
help="数值越限告警。格式:寄存器号>阈值(如 0>1000 表示寄存器0超过1000时告警)\n"
|
|
389
|
+
" 支持的运算符: >, <, >=, <=, ==, !=\n"
|
|
390
|
+
" 示例: '0>100' '2<=50' '1==0'",
|
|
391
|
+
)
|
|
392
|
+
pl.add_argument(
|
|
393
|
+
"--output", "-o",
|
|
394
|
+
metavar="FILE",
|
|
395
|
+
type=str,
|
|
396
|
+
default=None,
|
|
397
|
+
help="将轮询或读取结果导出到 CSV 文件,方便后用 Excel 分析",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# ── Slave options ──
|
|
401
|
+
sv = parser.add_argument_group("Slave Server Options (从站服务器参数)")
|
|
402
|
+
sv.add_argument(
|
|
403
|
+
"--register-map",
|
|
404
|
+
metavar="FILE.json",
|
|
405
|
+
type=str,
|
|
406
|
+
default=None,
|
|
407
|
+
help="从 JSON 文件加载自定义寄存器表,代替默认的寄存器映射。\n"
|
|
408
|
+
" 文件格式见 README.md 中的 JSON 示例",
|
|
409
|
+
)
|
|
410
|
+
sv.add_argument(
|
|
411
|
+
"--simulate",
|
|
412
|
+
action="store_true",
|
|
413
|
+
help="启动动态数值模拟(正弦波、计数器、线性斜坡等),让从站数据不断变化",
|
|
414
|
+
)
|
|
415
|
+
sv.add_argument(
|
|
416
|
+
"--sim-interval",
|
|
417
|
+
metavar="SEC",
|
|
418
|
+
type=float,
|
|
419
|
+
default=0.1,
|
|
420
|
+
help="模拟值的更新间隔(秒),越小变化越平滑 (默认: 0.1)",
|
|
421
|
+
)
|
|
422
|
+
sv.add_argument(
|
|
423
|
+
"--status",
|
|
424
|
+
action="store_true",
|
|
425
|
+
help="查看当前所有寄存器的数值和状态,查看后立即退出",
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# ── Display options ──
|
|
429
|
+
disp = parser.add_argument_group("Display / Debug Options (显示与调试)")
|
|
430
|
+
disp.add_argument(
|
|
431
|
+
"--gui-port",
|
|
432
|
+
type=int,
|
|
433
|
+
default=8080,
|
|
434
|
+
help="Web GUI 的端口号 (默认: 8080)",
|
|
435
|
+
)
|
|
436
|
+
disp.add_argument(
|
|
437
|
+
"-V", "--verbose",
|
|
438
|
+
action="store_true",
|
|
439
|
+
help="显示详细的调试日志信息",
|
|
440
|
+
)
|
|
441
|
+
disp.add_argument(
|
|
442
|
+
"--debug-frame",
|
|
443
|
+
action="store_true",
|
|
444
|
+
help="启用 Modbus 协议原始帧日志,显示收发的十六进制报文",
|
|
445
|
+
)
|
|
446
|
+
disp.add_argument(
|
|
447
|
+
"--version",
|
|
448
|
+
action="version",
|
|
449
|
+
version=f"%(prog)s {__version__}",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# RTU port as positional argument (hidden, for backward compatibility)
|
|
453
|
+
parser.add_argument(
|
|
454
|
+
"port_positional",
|
|
455
|
+
nargs="?",
|
|
456
|
+
default=None,
|
|
457
|
+
help=argparse.SUPPRESS,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return parser.parse_args(args)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def resolve_port(args):
|
|
464
|
+
"""Resolve port argument."""
|
|
465
|
+
port = args.port
|
|
466
|
+
is_serial = args.mode in ("rtu", "ascii")
|
|
467
|
+
if args.port_positional is not None:
|
|
468
|
+
try:
|
|
469
|
+
port = int(args.port_positional)
|
|
470
|
+
except ValueError:
|
|
471
|
+
if is_serial:
|
|
472
|
+
port = args.port_positional
|
|
473
|
+
else:
|
|
474
|
+
try:
|
|
475
|
+
port = int(args.port)
|
|
476
|
+
except ValueError:
|
|
477
|
+
if is_serial:
|
|
478
|
+
pass # serial port path
|
|
479
|
+
else:
|
|
480
|
+
port = 502 # fallback for TCP
|
|
481
|
+
return port
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def parse_slave_ids(raw: str) -> list[int]:
|
|
485
|
+
"""Parse slave ID string. Returns list of ints."""
|
|
486
|
+
raw = raw.strip()
|
|
487
|
+
if "," in raw:
|
|
488
|
+
return [int(x.strip()) for x in raw.split(",")]
|
|
489
|
+
return [int(raw)]
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def run_scan_mode(args):
|
|
493
|
+
"""Run the tool in Scan mode."""
|
|
494
|
+
port = resolve_port(args)
|
|
495
|
+
|
|
496
|
+
master = ModbusMaster(
|
|
497
|
+
host=args.host,
|
|
498
|
+
port=int(port) if isinstance(port, (int, str)) and str(port).isdigit() else 502,
|
|
499
|
+
mode=args.mode,
|
|
500
|
+
slave_id=1,
|
|
501
|
+
timeout=args.timeout,
|
|
502
|
+
baudrate=args.baudrate,
|
|
503
|
+
parity=args.parity,
|
|
504
|
+
stopbits=args.stopbits,
|
|
505
|
+
bytesize=args.bytesize,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
addr_str = f"{args.mode.upper()} {args.host}:{port}"
|
|
509
|
+
print(f"\n{c('Scanning', YELLOW)} {c(addr_str, CYAN_BOLD)} {c('for active slave devices...', YELLOW)}")
|
|
510
|
+
|
|
511
|
+
if args.mode == "tcp":
|
|
512
|
+
port_num = int(port)
|
|
513
|
+
print(f" {c('Range:', DIM)} Slave ID 1-247")
|
|
514
|
+
print(f" {c('Target:', DIM)} {args.host}:{port_num}")
|
|
515
|
+
else:
|
|
516
|
+
print(f" {c('Range:', DIM)} Slave ID 1-247")
|
|
517
|
+
print(f" {c('Target:', DIM)} {port}")
|
|
518
|
+
|
|
519
|
+
if not master.connect():
|
|
520
|
+
print(c("Failed to connect!", RED_BOLD))
|
|
521
|
+
sys.exit(1)
|
|
522
|
+
|
|
523
|
+
print()
|
|
524
|
+
found = master.scan_slaves(start=1, end=247, timeout_per=min(args.timeout, 0.5))
|
|
525
|
+
master.disconnect()
|
|
526
|
+
|
|
527
|
+
print()
|
|
528
|
+
if found:
|
|
529
|
+
print(f" {c('Found', GREEN_BOLD)} {c(str(len(found)), CYAN_BOLD)} {c('active slave(s):', GREEN)} {c(', '.join(str(s) for s in found), CYAN_BOLD)}")
|
|
530
|
+
else:
|
|
531
|
+
print(f" {c('No active slaves found.', YELLOW)}")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def run_master_mode(args):
|
|
535
|
+
"""Run the tool in Master mode."""
|
|
536
|
+
logger = logging.getLogger(__name__)
|
|
537
|
+
byte_order = parse_byte_order(args.byte_order)
|
|
538
|
+
|
|
539
|
+
# Handle port argument compatibility
|
|
540
|
+
port = resolve_port(args)
|
|
541
|
+
|
|
542
|
+
# Create master client
|
|
543
|
+
master = ModbusMaster(
|
|
544
|
+
host=args.host,
|
|
545
|
+
port=int(port) if isinstance(port, (int, str)) and str(port).isdigit() else port,
|
|
546
|
+
mode=args.mode,
|
|
547
|
+
slave_id=parse_slave_ids(args.slave_id)[0],
|
|
548
|
+
timeout=args.timeout,
|
|
549
|
+
baudrate=args.baudrate,
|
|
550
|
+
parity=args.parity,
|
|
551
|
+
stopbits=args.stopbits,
|
|
552
|
+
bytesize=args.bytesize,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Connect
|
|
556
|
+
addr_str = f"{args.mode.upper()} {args.host}:{port}"
|
|
557
|
+
print(f"\n{c('Connecting to', YELLOW)} {c(addr_str, CYAN_BOLD)}...")
|
|
558
|
+
if not master.connect():
|
|
559
|
+
print(c("Failed to connect!", RED_BOLD))
|
|
560
|
+
sys.exit(1)
|
|
561
|
+
|
|
562
|
+
print(f"{c('Connected!', GREEN_BOLD)}\n")
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
# Parse data type
|
|
566
|
+
data_type = parse_data_type(args.data_type)
|
|
567
|
+
|
|
568
|
+
if args.write_coil:
|
|
569
|
+
# Write coil mode
|
|
570
|
+
if args.register is None:
|
|
571
|
+
print(c("Error: -r/--register is required for coil write", RED_BOLD))
|
|
572
|
+
sys.exit(1)
|
|
573
|
+
value = bool(args.coil_state)
|
|
574
|
+
print(f"{c('Writing coil', YELLOW)} {c(str(args.register), CYAN_BOLD)} {c('=', YELLOW)} {c(str(value), CYAN_BOLD)}...")
|
|
575
|
+
success = master.write_single_coil(args.register, value)
|
|
576
|
+
if success:
|
|
577
|
+
print(c("Coil write successful!", GREEN_BOLD))
|
|
578
|
+
else:
|
|
579
|
+
print(c("Coil write failed!", RED_BOLD))
|
|
580
|
+
sys.exit(1)
|
|
581
|
+
|
|
582
|
+
elif args.write:
|
|
583
|
+
# Write mode
|
|
584
|
+
if args.value is None:
|
|
585
|
+
print(c("Error: --value is required for write operations", RED_BOLD))
|
|
586
|
+
sys.exit(1)
|
|
587
|
+
|
|
588
|
+
# Support batch write: comma-separated values
|
|
589
|
+
raw_values = [v.strip() for v in args.value.split(",")]
|
|
590
|
+
batch_mode = len(raw_values) > 1
|
|
591
|
+
|
|
592
|
+
parsed_values = []
|
|
593
|
+
for raw_val in raw_values:
|
|
594
|
+
try:
|
|
595
|
+
if data_type in (DataType.FLOAT32,):
|
|
596
|
+
parsed_values.append(float(raw_val))
|
|
597
|
+
else:
|
|
598
|
+
parsed_values.append(int(raw_val))
|
|
599
|
+
except ValueError:
|
|
600
|
+
print(f"{c('Error:', RED_BOLD)} {c(f'Invalid value \'{raw_val}\' for type {args.data_type}', RED)}")
|
|
601
|
+
sys.exit(1)
|
|
602
|
+
|
|
603
|
+
if batch_mode:
|
|
604
|
+
# Write multiple registers in sequence
|
|
605
|
+
print(f"{c('Batch writing', YELLOW)} {c(str(len(parsed_values)), CYAN_BOLD)} {c('value(s) starting at register', YELLOW)} {c(str(args.register), CYAN_BOLD)}...")
|
|
606
|
+
all_ok = True
|
|
607
|
+
for i, val in enumerate(parsed_values):
|
|
608
|
+
regs_per = DATA_TYPE_REGISTERS.get(data_type, 1)
|
|
609
|
+
ok = master.write_converted(args.register + i * regs_per, val, data_type, byte_order=byte_order)
|
|
610
|
+
if ok:
|
|
611
|
+
print(f" [{c(str(i), MAGENTA)}] = {c(str(val), CYAN_BOLD)} {c('OK', GREEN_BOLD)}")
|
|
612
|
+
else:
|
|
613
|
+
print(f" [{c(str(i), MAGENTA)}] = {c(str(val), CYAN_BOLD)} {c('FAILED', RED_BOLD)}")
|
|
614
|
+
all_ok = False
|
|
615
|
+
if all_ok:
|
|
616
|
+
print(c("Batch write successful!", GREEN_BOLD))
|
|
617
|
+
else:
|
|
618
|
+
print(c("Some writes failed!", RED_BOLD))
|
|
619
|
+
sys.exit(1)
|
|
620
|
+
else:
|
|
621
|
+
# Single value write
|
|
622
|
+
value = parsed_values[0]
|
|
623
|
+
print(f"{c('Writing', YELLOW)} {c(str(value), CYAN_BOLD)} {c('to register', YELLOW)} {c(str(args.register), CYAN_BOLD)}...")
|
|
624
|
+
success = master.write_converted(args.register, value, data_type, byte_order=byte_order)
|
|
625
|
+
if success:
|
|
626
|
+
print(c("Write successful!", GREEN_BOLD))
|
|
627
|
+
else:
|
|
628
|
+
print(c("Write failed!", RED_BOLD))
|
|
629
|
+
sys.exit(1)
|
|
630
|
+
|
|
631
|
+
elif args.poll:
|
|
632
|
+
# Polling mode
|
|
633
|
+
print(f"{c('Polling register', YELLOW)} {c(str(args.register), CYAN_BOLD)}, "
|
|
634
|
+
f"{c('count', YELLOW)}={c(str(args.count), CYAN_BOLD)}, "
|
|
635
|
+
f"{c('type', YELLOW)}={c(args.data_type, CYAN_BOLD)}")
|
|
636
|
+
print(f"{c('Interval', YELLOW)}: {c(f'{args.interval}s', CYAN_BOLD)}, "
|
|
637
|
+
f"{c('Iterations', YELLOW)}: {c(str(args.iterations or 'infinite'), CYAN_BOLD)}")
|
|
638
|
+
if byte_order != ByteOrder.ABCD:
|
|
639
|
+
print(f"{c('Byte Order', YELLOW)}: {c(byte_order.value, CYAN_BOLD)}")
|
|
640
|
+
if args.output:
|
|
641
|
+
print(f"{c('Output', YELLOW)}: {c(args.output, CYAN_BOLD)}")
|
|
642
|
+
print(f"{c('Press Ctrl+C to stop', DIM)}\n")
|
|
643
|
+
|
|
644
|
+
# Parse alarm if specified
|
|
645
|
+
alarm = None
|
|
646
|
+
if args.alarm:
|
|
647
|
+
alarm = parse_alarm(args.alarm)
|
|
648
|
+
print(f" {c('Alarm:', YELLOW)} register[{alarm['addr']}] {alarm['op']} {alarm['value']}\n")
|
|
649
|
+
|
|
650
|
+
from .datatypes import DATA_TYPE_REGISTERS
|
|
651
|
+
master.poll(
|
|
652
|
+
address=args.register,
|
|
653
|
+
count=args.count,
|
|
654
|
+
data_type=data_type,
|
|
655
|
+
function_code=args.function_code,
|
|
656
|
+
interval=args.interval,
|
|
657
|
+
iterations=args.iterations,
|
|
658
|
+
hex_format=args.hex,
|
|
659
|
+
byte_order=byte_order,
|
|
660
|
+
alarm=alarm,
|
|
661
|
+
output_file=args.output,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
else:
|
|
665
|
+
# Read mode
|
|
666
|
+
print(f"{c('Reading', YELLOW)} {c(str(args.count), CYAN_BOLD)} {c('register(s) from address', YELLOW)} {c(str(args.register), CYAN_BOLD)}...")
|
|
667
|
+
|
|
668
|
+
value = master.read_and_convert(
|
|
669
|
+
address=args.register,
|
|
670
|
+
count=args.count,
|
|
671
|
+
data_type=data_type,
|
|
672
|
+
function_code=args.function_code,
|
|
673
|
+
byte_order=byte_order,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
if value is not None:
|
|
677
|
+
# Export to CSV if requested
|
|
678
|
+
if args.output:
|
|
679
|
+
with open(args.output, "w", newline="") as f:
|
|
680
|
+
writer = csv.writer(f)
|
|
681
|
+
writer.writerow(["address", "value"])
|
|
682
|
+
if isinstance(value, list):
|
|
683
|
+
for i, v in enumerate(value):
|
|
684
|
+
writer.writerow([args.register + i, v])
|
|
685
|
+
else:
|
|
686
|
+
writer.writerow([args.register, value])
|
|
687
|
+
print(f"\n{c('Results exported to', DIM)} {c(args.output, CYAN_BOLD)}")
|
|
688
|
+
|
|
689
|
+
if isinstance(value, list):
|
|
690
|
+
print(f"\n{c(f'Results ({len(value)} values):', BOLD)}")
|
|
691
|
+
for i, v in enumerate(value):
|
|
692
|
+
formatted = format_value(v, data_type, args.hex)
|
|
693
|
+
print(f" [{c(str(args.register + i), MAGENTA)}] = {c(formatted, CYAN_BOLD)}")
|
|
694
|
+
else:
|
|
695
|
+
formatted = format_value(value, data_type, args.hex)
|
|
696
|
+
print(f"\n{c('Result:', BOLD)} {c(formatted, CYAN_BOLD)}")
|
|
697
|
+
else:
|
|
698
|
+
print(f"\n{c('Read failed!', RED_BOLD)}")
|
|
699
|
+
sys.exit(1)
|
|
700
|
+
|
|
701
|
+
finally:
|
|
702
|
+
master.disconnect()
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def run_slave_mode(args):
|
|
706
|
+
"""Run the tool in Slave mode."""
|
|
707
|
+
logger = logging.getLogger(__name__)
|
|
708
|
+
|
|
709
|
+
# Handle port argument compatibility
|
|
710
|
+
port = resolve_port(args)
|
|
711
|
+
|
|
712
|
+
# Create register map
|
|
713
|
+
if args.register_map:
|
|
714
|
+
try:
|
|
715
|
+
register_map = ModbusRegisterMap.load_from_json(args.register_map)
|
|
716
|
+
print(f"{c('Loaded register map from', DIM)} {c(args.register_map, CYAN_BOLD)}")
|
|
717
|
+
except Exception as e:
|
|
718
|
+
print(f"{c('Error loading register map:', RED_BOLD)} {c(str(e), RED)}")
|
|
719
|
+
sys.exit(1)
|
|
720
|
+
else:
|
|
721
|
+
register_map = ModbusRegisterMap()
|
|
722
|
+
register_map.create_default_map()
|
|
723
|
+
|
|
724
|
+
# Show status and exit
|
|
725
|
+
if args.status:
|
|
726
|
+
register_map.print_status()
|
|
727
|
+
sys.exit(0)
|
|
728
|
+
|
|
729
|
+
# Create and start server
|
|
730
|
+
server = None
|
|
731
|
+
simulation_engine = None
|
|
732
|
+
|
|
733
|
+
try:
|
|
734
|
+
if args.mode == "tcp":
|
|
735
|
+
server = ModbusTCPServer(
|
|
736
|
+
register_map=register_map,
|
|
737
|
+
host=args.host,
|
|
738
|
+
port=int(port),
|
|
739
|
+
slave_id=parse_slave_ids(args.slave_id)[0],
|
|
740
|
+
)
|
|
741
|
+
logger.info(f"Starting Modbus TCP server on {args.host}:{port}")
|
|
742
|
+
elif args.mode == "rtu-tcp":
|
|
743
|
+
server = ModbusTCPServer(
|
|
744
|
+
register_map=register_map,
|
|
745
|
+
host=args.host,
|
|
746
|
+
port=int(port),
|
|
747
|
+
slave_id=parse_slave_ids(args.slave_id)[0],
|
|
748
|
+
framer="rtu-tcp",
|
|
749
|
+
)
|
|
750
|
+
logger.info(f"Starting Modbus RTU over TCP server on {args.host}:{port}")
|
|
751
|
+
elif args.mode == "ascii-tcp":
|
|
752
|
+
server = ModbusTCPServer(
|
|
753
|
+
register_map=register_map,
|
|
754
|
+
host=args.host,
|
|
755
|
+
port=int(port),
|
|
756
|
+
slave_id=parse_slave_ids(args.slave_id)[0],
|
|
757
|
+
framer="ascii-tcp",
|
|
758
|
+
)
|
|
759
|
+
logger.info(f"Starting Modbus ASCII over TCP server on {args.host}:{port}")
|
|
760
|
+
else:
|
|
761
|
+
framer = "rtu" if args.mode == "rtu" else "ascii"
|
|
762
|
+
protocol_name = "ASCII" if args.mode == "ascii" else "RTU"
|
|
763
|
+
server = ModbusSerialServer(
|
|
764
|
+
register_map=register_map,
|
|
765
|
+
port=str(port),
|
|
766
|
+
baudrate=args.baudrate,
|
|
767
|
+
parity=args.parity,
|
|
768
|
+
stopbits=args.stopbits,
|
|
769
|
+
bytesize=args.bytesize,
|
|
770
|
+
timeout=args.timeout,
|
|
771
|
+
slave_id=parse_slave_ids(args.slave_id)[0],
|
|
772
|
+
framer=framer,
|
|
773
|
+
inter_char_delay=args.inter_char_delay,
|
|
774
|
+
)
|
|
775
|
+
logger.info(f"Starting Modbus {protocol_name} server on {port}")
|
|
776
|
+
|
|
777
|
+
# Setup simulation if requested
|
|
778
|
+
if args.simulate:
|
|
779
|
+
simulation_engine = SimulationEngine(register_map)
|
|
780
|
+
simulation_engine.interval = args.sim_interval
|
|
781
|
+
simulation_engine.setup_default_simulations()
|
|
782
|
+
logger.info("Simulation enabled with default patterns")
|
|
783
|
+
|
|
784
|
+
# Start the server
|
|
785
|
+
server.start()
|
|
786
|
+
|
|
787
|
+
# Start simulation engine if enabled
|
|
788
|
+
if simulation_engine:
|
|
789
|
+
simulation_engine.start()
|
|
790
|
+
|
|
791
|
+
# Print status
|
|
792
|
+
sim_label = c("Enabled", GREEN_BOLD) if args.simulate else c("Disabled", DIM)
|
|
793
|
+
|
|
794
|
+
# Determine protocol display name
|
|
795
|
+
proto_display = {
|
|
796
|
+
"tcp": "TCP",
|
|
797
|
+
"rtu": "RTU (Serial)",
|
|
798
|
+
"ascii": "ASCII (Serial)",
|
|
799
|
+
"rtu-tcp": "RTU over TCP",
|
|
800
|
+
"ascii-tcp": "ASCII over TCP",
|
|
801
|
+
}.get(args.mode, args.mode.upper())
|
|
802
|
+
|
|
803
|
+
is_real_serial = args.mode in ("rtu", "ascii")
|
|
804
|
+
is_tcp_mode = args.mode in ("tcp", "rtu-tcp", "ascii-tcp")
|
|
805
|
+
|
|
806
|
+
print(f"\n{c('='*60, DIM)}")
|
|
807
|
+
print(f"{c('Modbus Slave Server Status:', CYAN_BOLD)}")
|
|
808
|
+
print(f" {c('Protocol:', WHITE_BOLD)} {c(proto_display, YELLOW_BOLD)}")
|
|
809
|
+
if is_tcp_mode:
|
|
810
|
+
print(f" {c('Address:', WHITE_BOLD)} {c(f'{args.host}:{port}', YELLOW_BOLD)}")
|
|
811
|
+
else:
|
|
812
|
+
print(f" {c('Port:', WHITE_BOLD)} {c(str(port), YELLOW_BOLD)}")
|
|
813
|
+
print(f" {c('Baudrate:', WHITE_BOLD)} {c(str(args.baudrate), YELLOW_BOLD)}")
|
|
814
|
+
print(f" {c('Parity:', WHITE_BOLD)} {c(args.parity, YELLOW_BOLD)}")
|
|
815
|
+
print(f" {c('Stopbits:', WHITE_BOLD)} {c(str(args.stopbits), YELLOW_BOLD)}")
|
|
816
|
+
print(f" {c('Slave ID:', WHITE_BOLD)} {c(args.slave_id, YELLOW_BOLD)}")
|
|
817
|
+
if args.register_map:
|
|
818
|
+
print(f" {c('Reg Map:', WHITE_BOLD)} {c(args.register_map, YELLOW_BOLD)}")
|
|
819
|
+
print(f" {c('Simulation:', WHITE_BOLD)} {sim_label}")
|
|
820
|
+
print(f"{c('='*60, DIM)}\n")
|
|
821
|
+
|
|
822
|
+
print(f"{c('Server is running.', GREEN_BOLD)} {c('Press Ctrl+C to stop.', DIM)}\n")
|
|
823
|
+
|
|
824
|
+
# Setup signal handlers
|
|
825
|
+
def signal_handler(signum, frame):
|
|
826
|
+
print(f"\n\n{c('Shutting down server...', YELLOW_BOLD)}")
|
|
827
|
+
raise KeyboardInterrupt
|
|
828
|
+
|
|
829
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
830
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
831
|
+
|
|
832
|
+
# Keep running until interrupted
|
|
833
|
+
import math
|
|
834
|
+
_start = time.time()
|
|
835
|
+
while True:
|
|
836
|
+
if simulation_engine and simulation_engine.is_running():
|
|
837
|
+
elapsed = time.time() - _start
|
|
838
|
+
# Holding registers 0-1: Lasheng encoder (sine wave)
|
|
839
|
+
enc_val = int(500000 + 300000 * math.sin(2 * math.pi * 0.05 * elapsed))
|
|
840
|
+
server.set_registers(0x03, 0, [(enc_val >> 16) & 0xFFFF, enc_val & 0xFFFF])
|
|
841
|
+
# Input registers 0-1: LingGuang distance (sine wave)
|
|
842
|
+
dist_val = int(150000000 + 100000000 * math.sin(2 * math.pi * 0.03 * elapsed))
|
|
843
|
+
server.set_registers(0x04, 0, [(dist_val >> 16) & 0xFFFF, dist_val & 0xFFFF])
|
|
844
|
+
# Holding registers 61-63: Qingjiao Roll/Pitch/Yaw
|
|
845
|
+
roll_val = int(3000 * math.sin(2 * math.pi * 0.02 * elapsed))
|
|
846
|
+
pitch_val = int(2000 * math.sin(2 * math.pi * 0.025 * elapsed + 1))
|
|
847
|
+
yaw_val = int(1000 * math.sin(2 * math.pi * 0.015 * elapsed + 2))
|
|
848
|
+
server.set_registers(0x03, 61, [roll_val & 0xFFFF, pitch_val & 0xFFFF, yaw_val & 0xFFFF])
|
|
849
|
+
time.sleep(0.2)
|
|
850
|
+
|
|
851
|
+
except KeyboardInterrupt:
|
|
852
|
+
print(f"\n\n{c('Stopping server...', YELLOW_BOLD)}")
|
|
853
|
+
except Exception as e:
|
|
854
|
+
logger.error(f"Server error: {e}")
|
|
855
|
+
sys.exit(1)
|
|
856
|
+
finally:
|
|
857
|
+
# Cleanup
|
|
858
|
+
if simulation_engine:
|
|
859
|
+
simulation_engine.stop()
|
|
860
|
+
if server:
|
|
861
|
+
server.stop()
|
|
862
|
+
print(c("Server stopped.", DIM))
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def main(args=None):
|
|
866
|
+
"""Main entry point for the Modbus Tool."""
|
|
867
|
+
parsed = parse_args(args)
|
|
868
|
+
setup_logging(parsed.verbose)
|
|
869
|
+
|
|
870
|
+
# Launch Web GUI mode
|
|
871
|
+
if parsed.gui:
|
|
872
|
+
from .gui_server import run_gui
|
|
873
|
+
run_gui(host="127.0.0.1", port=parsed.gui_port)
|
|
874
|
+
return
|
|
875
|
+
|
|
876
|
+
if not (parsed.scan or parsed.master or parsed.slave):
|
|
877
|
+
print(c("Error: 请指定运行模式 --master, --slave, --scan 或 --gui", RED_BOLD))
|
|
878
|
+
print(c("提示:运行 --help 查看帮助", DIM))
|
|
879
|
+
sys.exit(1)
|
|
880
|
+
|
|
881
|
+
# Enable debug frame logging if requested
|
|
882
|
+
if parsed.debug_frame:
|
|
883
|
+
enable_debug_frame()
|
|
884
|
+
|
|
885
|
+
# Print banner
|
|
886
|
+
ver = f"v{__version__}"
|
|
887
|
+
print(f"""
|
|
888
|
+
{c('╔══════════════════════════════════════════════════════════════╗', CYAN_BOLD)}
|
|
889
|
+
{c('║', CYAN_BOLD)} {c(f'Modbus Tool {ver}', GREEN_BOLD)}{c(f'{" "*(25-len(ver))}║', CYAN_BOLD)}
|
|
890
|
+
{c('║', CYAN_BOLD)} {c('Master + Slave Testing Tool Combined', YELLOW_BOLD)}{c(' ║', CYAN_BOLD)}
|
|
891
|
+
{c('╚══════════════════════════════════════════════════════════════╝', CYAN_BOLD)}
|
|
892
|
+
""")
|
|
893
|
+
|
|
894
|
+
if parsed.scan:
|
|
895
|
+
run_scan_mode(parsed)
|
|
896
|
+
elif parsed.master:
|
|
897
|
+
run_master_mode(parsed)
|
|
898
|
+
else:
|
|
899
|
+
run_slave_mode(parsed)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
if __name__ == "__main__":
|
|
903
|
+
main()
|