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.
Files changed (40) hide show
  1. modbus_connector-0.8.1/LICENSE +21 -0
  2. modbus_connector-0.8.1/PKG-INFO +492 -0
  3. modbus_connector-0.8.1/README.md +456 -0
  4. modbus_connector-0.8.1/pyproject.toml +60 -0
  5. modbus_connector-0.8.1/setup.cfg +4 -0
  6. modbus_connector-0.8.1/src/modbus_connector/__init__.py +1 -0
  7. modbus_connector-0.8.1/src/modbus_connector/__main__.py +6 -0
  8. modbus_connector-0.8.1/src/modbus_connector/app.py +29 -0
  9. modbus_connector-0.8.1/src/modbus_connector/backend.py +323 -0
  10. modbus_connector-0.8.1/src/modbus_connector/connection_panel.py +422 -0
  11. modbus_connector-0.8.1/src/modbus_connector/csv_dialogs.py +193 -0
  12. modbus_connector-0.8.1/src/modbus_connector/datalogger.py +83 -0
  13. modbus_connector-0.8.1/src/modbus_connector/datalogger_dialog.py +189 -0
  14. modbus_connector-0.8.1/src/modbus_connector/graph_window.py +509 -0
  15. modbus_connector-0.8.1/src/modbus_connector/log_panel.py +94 -0
  16. modbus_connector-0.8.1/src/modbus_connector/main_window.py +213 -0
  17. modbus_connector-0.8.1/src/modbus_connector/models.py +452 -0
  18. modbus_connector-0.8.1/src/modbus_connector/registers_panel.py +1423 -0
  19. modbus_connector-0.8.1/src/modbus_connector/scanner_panel.py +308 -0
  20. modbus_connector-0.8.1/src/modbus_connector/session_widget.py +213 -0
  21. modbus_connector-0.8.1/src/modbus_connector/settings_store.py +27 -0
  22. modbus_connector-0.8.1/src/modbus_connector/theme.py +90 -0
  23. modbus_connector-0.8.1/src/modbus_connector/timeseries.py +38 -0
  24. modbus_connector-0.8.1/src/modbus_connector/worker.py +274 -0
  25. modbus_connector-0.8.1/src/modbus_connector.egg-info/PKG-INFO +492 -0
  26. modbus_connector-0.8.1/src/modbus_connector.egg-info/SOURCES.txt +38 -0
  27. modbus_connector-0.8.1/src/modbus_connector.egg-info/dependency_links.txt +1 -0
  28. modbus_connector-0.8.1/src/modbus_connector.egg-info/entry_points.txt +2 -0
  29. modbus_connector-0.8.1/src/modbus_connector.egg-info/requires.txt +12 -0
  30. modbus_connector-0.8.1/src/modbus_connector.egg-info/top_level.txt +1 -0
  31. modbus_connector-0.8.1/tests/test_backend.py +363 -0
  32. modbus_connector-0.8.1/tests/test_datalogger.py +124 -0
  33. modbus_connector-0.8.1/tests/test_graph_window.py +345 -0
  34. modbus_connector-0.8.1/tests/test_main_window_tabs.py +79 -0
  35. modbus_connector-0.8.1/tests/test_models.py +419 -0
  36. modbus_connector-0.8.1/tests/test_registers_panel.py +1018 -0
  37. modbus_connector-0.8.1/tests/test_scanner_panel.py +79 -0
  38. modbus_connector-0.8.1/tests/test_session_widget.py +129 -0
  39. modbus_connector-0.8.1/tests/test_theme.py +114 -0
  40. 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
+ ![Main window, light theme](docs/screenshots/main_window.png)
103
+
104
+ ![Main window, dark theme](docs/screenshots/main_window_dark.png)
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
+ ![Live graph, light theme](docs/screenshots/graph_window.png)
111
+
112
+ ![Live graph, dark theme](docs/screenshots/graph_window_dark.png)
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
+ ![Display settings](docs/screenshots/display_dialog.png)
118
+
119
+ CSV export — choose which columns to write and their order (the "CSV" button
120
+ above the table):
121
+
122
+ ![CSV export](docs/screenshots/export_dialog.png)
123
+
124
+ CSV import — map file columns to register fields before loading the table:
125
+
126
+ ![CSV import](docs/screenshots/import_dialog.png)
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
+ ![Logging settings](docs/screenshots/logging_dialog.png)
132
+
133
+ Address scanner — unit sweep with probes and the register address scan:
134
+
135
+ ![Scanner](docs/screenshots/scanner.png)
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).