modbus-connector 0.8.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.
- modbus_connector-0.8.1/LICENSE +21 -0
- modbus_connector-0.8.1/PKG-INFO +492 -0
- modbus_connector-0.8.1/README.md +456 -0
- modbus_connector-0.8.1/pyproject.toml +60 -0
- modbus_connector-0.8.1/setup.cfg +4 -0
- modbus_connector-0.8.1/src/modbus_connector/__init__.py +1 -0
- modbus_connector-0.8.1/src/modbus_connector/__main__.py +6 -0
- modbus_connector-0.8.1/src/modbus_connector/app.py +29 -0
- modbus_connector-0.8.1/src/modbus_connector/backend.py +323 -0
- modbus_connector-0.8.1/src/modbus_connector/connection_panel.py +422 -0
- modbus_connector-0.8.1/src/modbus_connector/csv_dialogs.py +193 -0
- modbus_connector-0.8.1/src/modbus_connector/datalogger.py +83 -0
- modbus_connector-0.8.1/src/modbus_connector/datalogger_dialog.py +189 -0
- modbus_connector-0.8.1/src/modbus_connector/graph_window.py +509 -0
- modbus_connector-0.8.1/src/modbus_connector/log_panel.py +94 -0
- modbus_connector-0.8.1/src/modbus_connector/main_window.py +213 -0
- modbus_connector-0.8.1/src/modbus_connector/models.py +452 -0
- modbus_connector-0.8.1/src/modbus_connector/registers_panel.py +1423 -0
- modbus_connector-0.8.1/src/modbus_connector/scanner_panel.py +308 -0
- modbus_connector-0.8.1/src/modbus_connector/session_widget.py +213 -0
- modbus_connector-0.8.1/src/modbus_connector/settings_store.py +27 -0
- modbus_connector-0.8.1/src/modbus_connector/theme.py +90 -0
- modbus_connector-0.8.1/src/modbus_connector/timeseries.py +38 -0
- modbus_connector-0.8.1/src/modbus_connector/worker.py +274 -0
- modbus_connector-0.8.1/src/modbus_connector.egg-info/PKG-INFO +492 -0
- modbus_connector-0.8.1/src/modbus_connector.egg-info/SOURCES.txt +38 -0
- modbus_connector-0.8.1/src/modbus_connector.egg-info/dependency_links.txt +1 -0
- modbus_connector-0.8.1/src/modbus_connector.egg-info/entry_points.txt +2 -0
- modbus_connector-0.8.1/src/modbus_connector.egg-info/requires.txt +12 -0
- modbus_connector-0.8.1/src/modbus_connector.egg-info/top_level.txt +1 -0
- modbus_connector-0.8.1/tests/test_backend.py +363 -0
- modbus_connector-0.8.1/tests/test_datalogger.py +124 -0
- modbus_connector-0.8.1/tests/test_graph_window.py +345 -0
- modbus_connector-0.8.1/tests/test_main_window_tabs.py +79 -0
- modbus_connector-0.8.1/tests/test_models.py +419 -0
- modbus_connector-0.8.1/tests/test_registers_panel.py +1018 -0
- modbus_connector-0.8.1/tests/test_scanner_panel.py +79 -0
- modbus_connector-0.8.1/tests/test_session_widget.py +129 -0
- modbus_connector-0.8.1/tests/test_theme.py +114 -0
- modbus_connector-0.8.1/tests/test_timeseries.py +40 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cramen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modbus-connector
|
|
3
|
+
Version: 0.8.1
|
|
4
|
+
Summary: PySide6 GUI for debugging Modbus buses and developing Modbus devices
|
|
5
|
+
Author: cramen
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cramen/modbus_connector
|
|
8
|
+
Project-URL: Repository, https://github.com/cramen/modbus_connector
|
|
9
|
+
Project-URL: Issues, https://github.com/cramen/modbus_connector/issues
|
|
10
|
+
Keywords: modbus,modbus-tcp,modbus-rtu,rs485,plc,scada,gui
|
|
11
|
+
Classifier: Environment :: MacOS X
|
|
12
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
13
|
+
Classifier: Environment :: X11 Applications
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: System :: Hardware
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pymodbus[serial]==3.6.9
|
|
26
|
+
Requires-Dist: PySide6
|
|
27
|
+
Requires-Dist: pyqtgraph
|
|
28
|
+
Requires-Dist: pyqtdarktheme
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
Provides-Extra: build
|
|
34
|
+
Requires-Dist: pyinstaller>=6; extra == "build"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# modbus_connector
|
|
38
|
+
|
|
39
|
+
<img src="assets/icon.png" width="96" align="right" alt="Modbus Connector icon">
|
|
40
|
+
|
|
41
|
+
[Russian version: README_ru.md](README_ru.md)
|
|
42
|
+
|
|
43
|
+
**[Download ready-made builds for macOS / Windows / Linux → Releases](https://github.com/cramen/modbus_connector/releases)**
|
|
44
|
+
|
|
45
|
+
**[Watch the video presentation (1 min, Russian voice-over) → docs/presentation.mp4](docs/presentation.mp4)**
|
|
46
|
+
|
|
47
|
+
A PySide6 GUI application for debugging Modbus buses and developing Modbus
|
|
48
|
+
devices. Works with Modbus TCP and Modbus RTU via synchronous pymodbus clients;
|
|
49
|
+
all Modbus logic runs in a separate thread (QThread), so the GUI never freezes.
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- Multiple simultaneous connections in tabs: each tab is an independent
|
|
54
|
+
session with its own connection, register table, log and scanner. Settings
|
|
55
|
+
for all tabs persist between launches in
|
|
56
|
+
`~/.modbus_connector/settings.json` (old single-session settings files keep
|
|
57
|
+
working), and can also be saved to / loaded from an arbitrary JSON file via
|
|
58
|
+
the File menu.
|
|
59
|
+
- Connection types: TCP (host, port, timeout), RTU (serial port, baudrate,
|
|
60
|
+
parity, etc.; RTU by default) and **RTU over TCP / RTU over UDP** for
|
|
61
|
+
RS-485↔Ethernet converters — all configured in the GUI.
|
|
62
|
+
- Register table: rows with a name, area type (coils, discrete inputs,
|
|
63
|
+
holding/input registers), address and count; read and write values.
|
|
64
|
+
Enter in the "New value" column sends the write command, Ctrl+R (Cmd+R on
|
|
65
|
+
macOS) reads the current row; the whole table is keyboard-friendly.
|
|
66
|
+
- Rich value display: per-row formats (dec/hex/s16/u32/s32/f32/u64/s64/f64/
|
|
67
|
+
ascii) with byte order variants (ABCD/CDAB/BADC/DCBA), scaling with offset
|
|
68
|
+
and engineering units; a value that changed between reads flashes for a
|
|
69
|
+
couple of seconds.
|
|
70
|
+
- Per-row Unit ID (rows can address different devices on the same bus) and
|
|
71
|
+
per-row polling interval (slow and fast registers in one table).
|
|
72
|
+
- Live graphs ("Graph…" button, separate window): a trend sparkline per table
|
|
73
|
+
row and a full plot window with multiple series, sliding-window/follow or
|
|
74
|
+
manual zoom, and draggable markers with per-series min/max/avg.
|
|
75
|
+
- Background logging of polled values to a CSV or JSON Lines file ("Log to
|
|
76
|
+
file" button), with a configurable set of fields.
|
|
77
|
+
- System/Light/Dark theme (pyqtdarktheme) from the View menu — graphs,
|
|
78
|
+
sparklines, status colors and highlights all follow the theme.
|
|
79
|
+
- Filter box and one-click "Sort by address" for large tables.
|
|
80
|
+
- Advanced protocol functions: Mask Write Register (0x16), Read/Write Multiple
|
|
81
|
+
Registers (0x17), Read Device Identification (0x2B) and serial-line
|
|
82
|
+
Diagnostics (0x08) — via dedicated dialogs.
|
|
83
|
+
- Link visibility: transaction statistics in the status bar (count, errors
|
|
84
|
+
with percentage, top error kind, average response time), human-readable
|
|
85
|
+
Modbus exception names and a live connection indicator (green = alive,
|
|
86
|
+
orange "(idle)" = link idle or degraded).
|
|
87
|
+
- Address scanner ("Scanner…" button, separate window): iterates unit ids in
|
|
88
|
+
a given range with configurable probes and shows devices that answered at
|
|
89
|
+
least one probe; double-click a found unit to select it for the connection.
|
|
90
|
+
A second sweep scans the register address space of a known unit and lists
|
|
91
|
+
the addresses that respond.
|
|
92
|
+
- Log panel at the bottom of the window, toggled with the "Log" button:
|
|
93
|
+
human-readable requests/responses, optional raw bus traffic in hex
|
|
94
|
+
("Raw" checkbox) and export of the whole log to a file ("Save…").
|
|
95
|
+
|
|
96
|
+
## Screenshots
|
|
97
|
+
|
|
98
|
+
Main window (light and dark themes — the View menu switches them) — two
|
|
99
|
+
connection tabs, register table with read values and per-row trend
|
|
100
|
+
sparklines, log:
|
|
101
|
+
|
|
102
|
+

|
|
103
|
+
|
|
104
|
+

|
|
105
|
+
|
|
106
|
+
Live graph — multiple series in a follow window, zoom and draggable markers
|
|
107
|
+
with per-series min/max/avg (the "Graph…" button in the connection panel);
|
|
108
|
+
light and dark themes:
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
114
|
+
Per-row display settings — Scale/Offset/Unit and a byte-order override per
|
|
115
|
+
register row (the "Display…" button above the table):
|
|
116
|
+
|
|
117
|
+

|
|
118
|
+
|
|
119
|
+
CSV export — choose which columns to write and their order (the "CSV" button
|
|
120
|
+
above the table):
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
CSV import — map file columns to register fields before loading the table:
|
|
125
|
+
|
|
126
|
+

|
|
127
|
+
|
|
128
|
+
Logging to a file — write polled values to CSV or JSON Lines: file, format,
|
|
129
|
+
field selection and a per-row checklist (the "⚙" button above the table):
|
|
130
|
+
|
|
131
|
+

|
|
132
|
+
|
|
133
|
+
Address scanner — unit sweep with probes and the register address scan:
|
|
134
|
+
|
|
135
|
+

|
|
136
|
+
|
|
137
|
+
## Requirements
|
|
138
|
+
|
|
139
|
+
- Python 3.11+
|
|
140
|
+
- `PySide6`, `pymodbus[serial]==3.6.9`
|
|
141
|
+
|
|
142
|
+
## Installation
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python -m venv .venv
|
|
146
|
+
source .venv/bin/activate
|
|
147
|
+
pip install -e .[dev]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Run
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
modbus-connector
|
|
154
|
+
# or
|
|
155
|
+
python -m modbus_connector
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Usage
|
|
159
|
+
|
|
160
|
+
### Connecting
|
|
161
|
+
|
|
162
|
+
1. Choose the connection type: **TCP** (host, port), **RTU** (serial port,
|
|
163
|
+
baudrate, data bits, parity, stop bits — RTU is the default; use "Refresh"
|
|
164
|
+
to rescan serial ports) or **RTU over TCP** / **RTU over UDP** (host, port —
|
|
165
|
+
RTU frames inside a network socket, for RS-485↔Ethernet converters such as
|
|
166
|
+
USR or Elfin).
|
|
167
|
+
2. Set the **Unit ID** of the target device (used for all register operations)
|
|
168
|
+
and the response **Timeout**.
|
|
169
|
+
3. Press **Connect**. Input fields are locked while connected; press
|
|
170
|
+
**Disconnect** to change settings.
|
|
171
|
+
|
|
172
|
+
The status label next to the button is live: green means the link is up,
|
|
173
|
+
orange with an "(idle)" suffix means the connection is configured but the last
|
|
174
|
+
transaction timed out — pymodbus reconnects transparently on the next request,
|
|
175
|
+
so this is informational, not an error. The status bar at the bottom of the
|
|
176
|
+
window shows transaction counters: total, errors with a percentage and the
|
|
177
|
+
most frequent error kind (the full breakdown by error type is in the label's
|
|
178
|
+
tooltip), plus the average response time of successful operations. Modbus
|
|
179
|
+
exception responses are reported by name (e.g. "Illegal Data Address (0x02)")
|
|
180
|
+
in the log.
|
|
181
|
+
|
|
182
|
+
### Working with tabs
|
|
183
|
+
|
|
184
|
+
The main window holds connections in tabs. The **+** button in the tab bar
|
|
185
|
+
corner opens another independent session — its own connection, register table,
|
|
186
|
+
log and scanner window. The tab title follows the connection (e.g. `tcp
|
|
187
|
+
192.168.1.10:502`); the last remaining tab cannot be closed. The status bar
|
|
188
|
+
statistics follow the active tab. All tabs are saved to the settings on exit
|
|
189
|
+
and restored on the next launch — including each table's column widths. The
|
|
190
|
+
**View** menu switches the theme (System/Light/Dark); the choice is app-wide
|
|
191
|
+
and is saved with the settings.
|
|
192
|
+
|
|
193
|
+
### Adding registers
|
|
194
|
+
|
|
195
|
+
Press **Add register** and fill in the row: an arbitrary **Name**, the area
|
|
196
|
+
**Type** (coils, discrete inputs, holding registers, input registers),
|
|
197
|
+
**Address** (decimal or hex, e.g. `0x10`) and **Count** (how many values to
|
|
198
|
+
read starting at the address). The optional **Unit ID** column overrides the
|
|
199
|
+
connection-wide unit for this row (empty = use the connection unit) — handy
|
|
200
|
+
for polling several devices on one RS-485 bus. All columns are plain cells —
|
|
201
|
+
the table is fully navigable with the keyboard. The ✕ button deletes a row.
|
|
202
|
+
|
|
203
|
+
### Reading values
|
|
204
|
+
|
|
205
|
+
- **Ctrl+R** (**Cmd+R** on macOS) — reads the row that has the keyboard focus;
|
|
206
|
+
**Ctrl+Shift+R** reads every row (the "Read all" button).
|
|
207
|
+
- Quick actions on the focused row (also in the table's right-click menu):
|
|
208
|
+
**Ctrl+C** copies the value, **Ctrl+0**/**Ctrl+1** write 0/1,
|
|
209
|
+
**Ctrl+=** (or numpad **Ctrl++**)/**Ctrl+-** step the last read value,
|
|
210
|
+
**Ctrl+T** toggles it
|
|
211
|
+
(coils flip the bit; a register goes 0↔1). Writes use raw values, so
|
|
212
|
+
scaled/hex displays stay safe; input/discrete areas report "read-only".
|
|
213
|
+
- **Read all** — reads every row once.
|
|
214
|
+
- **Start polling and record** — a split button: the main action reads all
|
|
215
|
+
rows repeatedly with the interval set in the "Interval" field
|
|
216
|
+
(milliseconds) and records value history; press **Stop polling** to stop.
|
|
217
|
+
The dropdown offers **Start polling** (without recording) and **Start
|
|
218
|
+
polling and record** — picking one while polling runs switches the
|
|
219
|
+
recording mode on the fly, and the last choice becomes the main action.
|
|
220
|
+
The optional **Poll, ms** column overrides the interval per row (empty =
|
|
221
|
+
global interval; finer values are effectively clamped to the global tick).
|
|
222
|
+
|
|
223
|
+
Read values appear in the **Value** column; every request and response is also
|
|
224
|
+
shown in the log panel (toggled with the "Log" button). Bus-reading controls
|
|
225
|
+
(Read all, polling, logging, the 0x16/0x17 dialogs and the scanner's Start
|
|
226
|
+
buttons) are enabled only while a connection is up — dropping the connection
|
|
227
|
+
stops polling and logging.
|
|
228
|
+
|
|
229
|
+
### Display formats, scaling and units
|
|
230
|
+
|
|
231
|
+
For register rows the **Format** column chooses how the Value column renders:
|
|
232
|
+
`dec` (default), `hex` (`0xNNNN`), `s16` (signed 16-bit), `u32`/`s32`/`f32`
|
|
233
|
+
(pairs of registers as one 32-bit value), `u64`/`s64`/`f64` (groups of four
|
|
234
|
+
registers) and `ascii` (two characters per register, e.g. device names and
|
|
235
|
+
serial numbers; the string ends at the first NUL byte). Coils and discrete
|
|
236
|
+
inputs always show 0/1.
|
|
237
|
+
|
|
238
|
+
Multi-register values are big-endian by default (the first register is the
|
|
239
|
+
high word); the **Order** combo above the table sets the byte layout for all
|
|
240
|
+
rows (`ABCD` default, `CDAB` word-swapped, `BADC` byte-swapped words, `DCBA`
|
|
241
|
+
full reverse), and a per-row override is available in the **Display…** dialog.
|
|
242
|
+
A leftover register that does not fill a whole 32/64-bit group is shown as-is.
|
|
243
|
+
|
|
244
|
+
The **Scale**, **Offset**, **Unit** and per-row **Order** settings live in the
|
|
245
|
+
**Display…** dialog above the table (one row per register row). The raw
|
|
246
|
+
registers are first decoded according to Format and Order, then each decoded
|
|
247
|
+
number is displayed as `x * scale + offset` with the unit appended
|
|
248
|
+
(e.g. `23.5 °C`). Scaling is skipped for the `hex` and `ascii` formats.
|
|
249
|
+
Table columns can be resized by dragging the header separators.
|
|
250
|
+
|
|
251
|
+
A value that changed since the previous read flashes green for ~2 seconds.
|
|
252
|
+
|
|
253
|
+
Use the **Filter…** box above the table to show only rows whose name, type,
|
|
254
|
+
address or unit id contains the text, and **Sort by address** to reorder the
|
|
255
|
+
table by address.
|
|
256
|
+
|
|
257
|
+
### Writing values
|
|
258
|
+
|
|
259
|
+
Type the value(s) into the **New value** column and press **Enter** — the
|
|
260
|
+
write command is sent immediately. Values are always raw: display scaling
|
|
261
|
+
(Scale/Offset) is never applied to them.
|
|
262
|
+
|
|
263
|
+
- registers: decimal or hex numbers (`4321`, `0x10E1`); for Count > 1 enter
|
|
264
|
+
several values separated by commas or spaces (`1, 2, 0xFF`) — a single value
|
|
265
|
+
uses function "write single register", several values use "write multiple
|
|
266
|
+
registers";
|
|
267
|
+
- coils: `0`/`1`, `true`/`false`, `on`/`off` (case-insensitive).
|
|
268
|
+
|
|
269
|
+
After a successful write the row is re-read automatically, so the **Value**
|
|
270
|
+
column reflects the applied change. Parse errors and Modbus errors are
|
|
271
|
+
reported in the log panel.
|
|
272
|
+
|
|
273
|
+
Note: only coils and holding registers are writable — discrete inputs and
|
|
274
|
+
input registers are read-only by the protocol.
|
|
275
|
+
|
|
276
|
+
### Advanced protocol functions
|
|
277
|
+
|
|
278
|
+
- **Mask write (0x16)…** (button above the table) — Mask Write Register:
|
|
279
|
+
AND/OR masks applied to one holding register, setting or clearing individual
|
|
280
|
+
bits without touching the others. Table rows covering the address are
|
|
281
|
+
re-read after a successful write.
|
|
282
|
+
- **Read/Write (0x17)…** — Read/Write Multiple Registers: writes values and
|
|
283
|
+
reads back a range in one atomic transaction (no race window); the returned
|
|
284
|
+
values go to the log.
|
|
285
|
+
- **Device ID…** (connection panel, enabled while connected) — Read Device
|
|
286
|
+
Identification (0x2B/0x0E): vendor name, product code, revision and other
|
|
287
|
+
objects reported by the device.
|
|
288
|
+
- **Diagnostics…** (connection panel, enabled while connected) — serial-line
|
|
289
|
+
diagnostics (0x08): loopback echo check and bus/slave message counters with
|
|
290
|
+
Refresh and Clear counters. This is a serial-line function, but some TCP
|
|
291
|
+
devices answer it too.
|
|
292
|
+
|
|
293
|
+
### Scanning for devices
|
|
294
|
+
|
|
295
|
+
Press **Scanner…** to open the scanner window. Set the unit id range
|
|
296
|
+
(default 1–247) and the probe list (register type + address + count to try on
|
|
297
|
+
each address; sensible defaults are prefilled). **Start scan** begins the
|
|
298
|
+
sweep, **Stop** aborts it; units that answered at least one probe appear in
|
|
299
|
+
the results list. **Double-click a found unit** to copy it into the connection
|
|
300
|
+
panel's Unit ID field. Scanning pauses polling in the main window.
|
|
301
|
+
|
|
302
|
+
The **Registers scan** section below works the other way around: for a known
|
|
303
|
+
unit it reads a range of addresses of a chosen register type one by one and
|
|
304
|
+
lists every address that answered as `0xNNNN (dec)` — a quick way to map the
|
|
305
|
+
register space of an unfamiliar device.
|
|
306
|
+
|
|
307
|
+
The scanner's range, probe list and address-scan parameters persist in the
|
|
308
|
+
settings along with everything else.
|
|
309
|
+
|
|
310
|
+
### Graphs
|
|
311
|
+
|
|
312
|
+
Every register row captures its value history while polling runs in the
|
|
313
|
+
poll-and-record mode (scaled engineering value; hex/ascii rows are skipped)
|
|
314
|
+
and shows it as a small trend sparkline in the **Trend** column. The buffer
|
|
315
|
+
is bounded to ~10k samples per row; when recording is off, sparklines and
|
|
316
|
+
graph curves freeze on the last recorded data. **Graph…** (connection panel)
|
|
317
|
+
opens the full plot window:
|
|
318
|
+
|
|
319
|
+
- the **Series** checklist on the left picks which table rows are plotted
|
|
320
|
+
(new rows join checked by default);
|
|
321
|
+
- **X scale: Follow** slides a window of the given width along the latest
|
|
322
|
+
data, **Full** fits everything, **Manual** freezes the view — zooming or
|
|
323
|
+
panning (wheel at cursor, left-drag, or the **Zoom rect** toggle) switches
|
|
324
|
+
the mode to Manual so the change is visible; **Reset view** returns to
|
|
325
|
+
Follow;
|
|
326
|
+
- **Markers** shows two draggable vertical lines (green A, red B) and a stats
|
|
327
|
+
table with per-series min/max/avg between them plus Δt, updated live;
|
|
328
|
+
- hovering the plot shows a crosshair: a dashed vertical line at the cursor's
|
|
329
|
+
time and a top-right readout with every series' value at that moment
|
|
330
|
+
(nearest recorded sample, marked with a dot on each curve);
|
|
331
|
+
- **Clear** empties the recorded history and restarts the relative time
|
|
332
|
+
axis (markers are re-placed once new data arrives); right-clicking a Trend
|
|
333
|
+
cell in the table offers the same "Clear history";
|
|
334
|
+
- **Start polling and record** duplicates the table's poll control: starts
|
|
335
|
+
polling with recording (or just enables recording if polling already runs),
|
|
336
|
+
turns into **Stop polling** while recording is active.
|
|
337
|
+
|
|
338
|
+
Closing the graph window only hides it; the data stays.
|
|
339
|
+
|
|
340
|
+
### Logging values to a file
|
|
341
|
+
|
|
342
|
+
The **Log to file** button above the table writes every read value to a file
|
|
343
|
+
while it is on; starting it also starts polling if it wasn't running (with
|
|
344
|
+
history recording if the split button's mode is "and record"). The **⚙**
|
|
345
|
+
button next to it opens the settings: the file (a timestamped name in the
|
|
346
|
+
home directory is suggested), the format, which optional fields —
|
|
347
|
+
timestamp (wall clock, ISO 8601 with milliseconds), row name, register
|
|
348
|
+
address and register type — accompany the value, and which table rows get
|
|
349
|
+
logged at all (the "Rows to log" checklist; new rows join logged by default,
|
|
350
|
+
and the per-row choice persists with the session). Values are
|
|
351
|
+
machine-friendly: decoded numbers with scale/offset but without the unit,
|
|
352
|
+
multi-value rows joined with ";", coils/discrete inputs as 0/1, hex/ascii
|
|
353
|
+
rows as displayed.
|
|
354
|
+
|
|
355
|
+
Formats: **CSV** (one row per read, a header row in new files) and **JSON
|
|
356
|
+
Lines** — one JSON object per line, which streams and appends cleanly.
|
|
357
|
+
Appending to an existing file is the default; the settings (not the on/off
|
|
358
|
+
state) persist with the session. Stopping logging leaves polling running.
|
|
359
|
+
|
|
360
|
+
### Log panel
|
|
361
|
+
The log panel at the bottom of the main window (toggled with the **Log**
|
|
362
|
+
button) shows every request and response with timestamps. The **Raw**
|
|
363
|
+
checkbox additionally displays raw bus frames in hex (`→ tx …` / `← rx …`) —
|
|
364
|
+
off by default to keep the log readable. **Save…** exports the entire log
|
|
365
|
+
(including raw frames hidden by the checkbox) to a text file; **Clear**
|
|
366
|
+
empties it.
|
|
367
|
+
|
|
368
|
+
### CSV import/export
|
|
369
|
+
|
|
370
|
+
The **CSV** drop-down above the table exchanges the register table with
|
|
371
|
+
spreadsheet tools:
|
|
372
|
+
|
|
373
|
+
- **Import table…** loads a CSV file and *replaces* the whole table. A
|
|
374
|
+
mapping dialog appears first: every file column can be matched to a
|
|
375
|
+
register field (name, kind, address, count, unit_id, poll_ms, format,
|
|
376
|
+
scale, offset, unit, order) or skipped; matches are guessed from column
|
|
377
|
+
names ("type" counts as kind) and the essential fields name/kind/address
|
|
378
|
+
must be mapped. Errors are reported in the log, an invalid file leaves the
|
|
379
|
+
table untouched.
|
|
380
|
+
- **Export…** opens a column chooser first — check which columns to write
|
|
381
|
+
and reorder them (arrows/Space/Ctrl+Up/Ctrl+Down) — then writes the CSV:
|
|
382
|
+
the chosen columns plus an optional `value` column with the currently
|
|
383
|
+
displayed (formatted/scaled) text — readable as a report and re-importable:
|
|
384
|
+
the `value` column is simply skipped by default in the mapping dialog, so
|
|
385
|
+
the round trip "export → edit in Excel → import" works out of the box.
|
|
386
|
+
|
|
387
|
+
Only `name`, `kind` and `address` are required on import — unmapped optional
|
|
388
|
+
fields fall back to defaults. Files are written UTF-8 with BOM so Excel opens
|
|
389
|
+
them cleanly.
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
## Building a standalone executable
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
./build.sh # macOS / Linux
|
|
396
|
+
build.bat # Windows (cmd, also works by double-click)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The script installs PyInstaller (the `build` extra) and builds a standalone
|
|
400
|
+
application into `dist/`: on macOS — `ModbusConnector.app` plus a
|
|
401
|
+
`ModbusConnector.dmg` disk image; on Windows/Linux — a `ModbusConnector/`
|
|
402
|
+
folder with the executable inside (`ModbusConnector.exe` on Windows; copy the
|
|
403
|
+
whole folder to another machine). The artifact does not require Python on the
|
|
404
|
+
target machine.
|
|
405
|
+
|
|
406
|
+
macOS notes:
|
|
407
|
+
|
|
408
|
+
- Run: double-click `ModbusConnector.app` or `open dist/ModbusConnector.app`.
|
|
409
|
+
Never run files from the intermediate `build/` directory (the script removes
|
|
410
|
+
it after building).
|
|
411
|
+
- To move the app to another machine use the ready-made
|
|
412
|
+
`dist/ModbusConnector.dmg` (do not rename or repack the `.app` into a `.pkg`
|
|
413
|
+
yourself — such a file is not a valid installer).
|
|
414
|
+
- The app is ad-hoc signed: on another Mac Gatekeeper will warn about an
|
|
415
|
+
unidentified developer on first launch — open via right-click → "Open", or
|
|
416
|
+
remove the quarantine: `xattr -dr com.apple.quarantine ModbusConnector.app`.
|
|
417
|
+
|
|
418
|
+
Linux notes:
|
|
419
|
+
|
|
420
|
+
- RTU connections to serial ports (`/dev/ttyUSB*`, `/dev/ttyACM*`, etc.) require
|
|
421
|
+
membership in the port's group, usually `dialout` (sometimes `uucp`). If you
|
|
422
|
+
see `Errno 13` / "Permission denied" on connect, check the port:
|
|
423
|
+
```bash
|
|
424
|
+
ls -l /dev/ttyUSB0
|
|
425
|
+
```
|
|
426
|
+
Add your user to the group and re-login (or run `newgrp dialout`):
|
|
427
|
+
```bash
|
|
428
|
+
sudo usermod -aG dialout $USER
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Project layout
|
|
432
|
+
|
|
433
|
+
```
|
|
434
|
+
src/modbus_connector/
|
|
435
|
+
models.py # Qt-free data types and helpers: TcpParams/RtuParams/
|
|
436
|
+
# RtuOverTcpParams/RtuOverUdpParams, RegisterRow, ScanProbe,
|
|
437
|
+
# DisplayFormat, ByteOrder, describe_connection(),
|
|
438
|
+
# parse_values()/format_values(),
|
|
439
|
+
# format_register_values()/format_scaled_values(),
|
|
440
|
+
# EXCEPTION_CODES/describe_exception(), Stats/StatsSnapshot
|
|
441
|
+
backend.py # ModbusBackend — synchronous pymodbus wrapper (no Qt):
|
|
442
|
+
# read/write, mask write (0x16), read/write registers (0x17),
|
|
443
|
+
# device identification (0x2B), diagnostics (0x08),
|
|
444
|
+
# unit scan, register address scan, raw traffic hook
|
|
445
|
+
worker.py # ModbusWorker (QObject) — signals/slots over backend, QThread;
|
|
446
|
+
# timing statistics, liveness checks, traffic forwarding
|
|
447
|
+
connection_panel.py # connection panel (TCP/RTU/RTU over TCP/RTU over UDP,
|
|
448
|
+
# state/set_state) with a live status indicator
|
|
449
|
+
# (gray/green/orange); Device ID…/Diagnostics… dialogs
|
|
450
|
+
registers_panel.py # register table: per-row unit/poll/format/order/scaling,
|
|
451
|
+
# change highlighting, filter/sort, Enter = write,
|
|
452
|
+
# Mask write…/Read/Write… dialogs
|
|
453
|
+
scanner_panel.py # unit scanner + register address scan (separate window)
|
|
454
|
+
log_panel.py # log panel (hideable): Raw hex traffic toggle, Save…
|
|
455
|
+
settings_store.py # settings persistence in ~/.modbus_connector/settings.json
|
|
456
|
+
session_widget.py # SessionWidget — one Modbus session (panels, scanner
|
|
457
|
+
# window, worker thread) as a self-contained widget
|
|
458
|
+
main_window.py # main window: sessions in tabs, File menu,
|
|
459
|
+
# status bar following the active tab
|
|
460
|
+
app.py # QApplication creation and startup
|
|
461
|
+
__main__.py # python -m modbus_connector
|
|
462
|
+
tests/
|
|
463
|
+
conftest.py # modbus_server fixture: test Modbus TCP server on 127.0.0.1
|
|
464
|
+
test_models.py # value parsing/formatting, exceptions, Stats
|
|
465
|
+
test_backend.py # ModbusBackend against the test server
|
|
466
|
+
test_registers_panel.py # offscreen Qt tests for the register table
|
|
467
|
+
test_session_widget.py # session state round-trip and shutdown
|
|
468
|
+
test_main_window_tabs.py # tab lifecycle and settings round-trip
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Development
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
pytest # tests
|
|
475
|
+
ruff check . # lint
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
The backend tests start a real Modbus TCP server (pymodbus) on 127.0.0.1
|
|
479
|
+
with a free port and exercise read/write/scan through `ModbusBackend`.
|
|
480
|
+
|
|
481
|
+
## CI
|
|
482
|
+
|
|
483
|
+
GitHub Actions (`.github/workflows/build.yml`) runs tests and builds artifacts
|
|
484
|
+
for all three OSes on macOS/Windows/Linux runners on every push to `main`:
|
|
485
|
+
`modbus-connector-macos` (DMG), `modbus-connector-windows` and
|
|
486
|
+
`modbus-connector-linux` (zip with the executable). Download them from the
|
|
487
|
+
workflow run page (Actions → a run → Artifacts; kept for 90 days); a build can
|
|
488
|
+
also be started manually via "Run workflow".
|
|
489
|
+
|
|
490
|
+
Pushing a `v*` tag (e.g. `git tag v0.1.0 && git push origin v0.1.0`)
|
|
491
|
+
automatically attaches the same files to a GitHub Release — a permanent
|
|
492
|
+
download page (Releases in the repository).
|