diff --git a/.gitignore b/.gitignore
index 6b04d42..2892782 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,5 @@ build/
*.egg-info/
.venv/
logs/
-.coverage
\ No newline at end of file
+.coverage
+site/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fbbff0..2e12b1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
# Changelog
+# v1.0.4 – What’s Changed 🚀
+
+## 🚀 Feature Enhancements
+- 📶 Extended baudrate options in GUI combo box to include 230400, 460800, and 921600
+
+## 📚 Documentation Updates
+- 📝 Improved Polish documentation pages with standardized emojis and added UI section
+- 🖼️ Added application screenshots and fixed main icon references
+- 🔄 Reorganized and fixed Polish index page structure
+- 📖 General documentation improvements and updates
+
+## 🧪 Test Suite Fixes
+- 🐛 Fixed AttributeError in ModbusParser by initializing `bufferIndex` in `__init__`
+- 🔧 Updated GUI tests to use correct imports (QStyleOptionViewItem from QtWidgets)
+- 🛠️ Added proper mocking for serial.Serial in tests to prevent real hardware access
+- ✅ Fixed assertions in parser tests and GUI add_parsed_data method to use safe dict access
+- ⚙️ Configured pytest for source coverage instead of installed package
+
+**Full Changelog**: https://github.com/niwciu/ModbusSniffer/compare/v1.0.3...v1.0.4
+
+
# v1.0.3 – What’s Changed 🚀
## ⚙️ CI/CD Improvements
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ffde655..50cf275 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,7 +8,8 @@ Easily analyze and debug communication between PLCs, HMIs, and other Modbus RTU
[](LICENSE)

@@ -33,7 +35,7 @@ pip install modbus-sniffer
```
or download Binary files for Ubuntu and Windows from [here](https://github.com/niwciu/ModbusSniffer/releases).
-You can also build and install app from sourcess. [Click here](CONTRIBUTING.md#%EF%B8%8F-build--install) for deatails about it.
+You can also build and install app from sourcess. [Click here](docs/installation.md) for deatails about it.
---
@@ -65,7 +67,7 @@ For more usage options, development guide, and installation from source, visit t
---
## 🤝 Contributing
-Please see [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution guidelines.
+Please see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for development setup and contribution guidelines.
---
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..217cbed
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,65 @@
+# API Reference
+
+This section provides an overview of the main classes and functions in ModbusSniffer.
+
+## Core Classes
+
+### ModbusParser
+
+Parses raw Modbus RTU data into structured frames.
+
+**Methods:**
+- `parse_frame(data: bytes) -> dict`: Decodes a single frame.
+- `validate_crc(data: bytes) -> bool`: Checks CRC validity.
+
+**Example:**
+```python
+from modbus_sniffer.modbus_parser_new import ModbusParser
+
+parser = ModbusParser()
+frame = parser.parse_frame(b'\x01\x03\x00\x00\x00\x01\x84\x0A')
+print(frame) # {'address': 1, 'function': 3, 'data': ...}
+```
+
+### SerialSnooper
+
+Handles serial port communication.
+
+**Methods:**
+- `open_port(port: str, baud: int) -> None`: Opens serial port.
+- `read_data() -> bytes`: Reads incoming data.
+
+**Example:**
+```python
+from modbus_sniffer.serial_snooper import SerialSnooper
+
+snooper = SerialSnooper()
+snooper.open_port('/dev/ttyUSB0', 9600)
+data = snooper.read_data()
+```
+
+### MainLogger
+
+Coordinates logging and GUI updates.
+
+**Methods:**
+- `start_sniffing() -> None`: Begins capture.
+- `stop_sniffing() -> None`: Ends capture.
+
+## Utilities
+
+### SnifferUtils
+
+Helper functions for data processing.
+
+**Functions:**
+- `format_hex(data: bytes) -> str`: Formats bytes as hex string.
+- `calculate_crc(data: bytes) -> int`: Computes Modbus CRC.
+
+**Example:**
+```python
+from modbus_sniffer.sniffer_utils import format_hex
+
+hex_str = format_hex(b'\x01\x02\x03')
+print(hex_str) # '01 02 03'
+```
\ No newline at end of file
diff --git a/docs/api.pl.md b/docs/api.pl.md
new file mode 100644
index 0000000..d1abb86
--- /dev/null
+++ b/docs/api.pl.md
@@ -0,0 +1,65 @@
+# Dokumentacja API
+
+Ta sekcja zawiera przegląd głównych klas i funkcji w ModbusSniffer.
+
+## Główne Klasy
+
+### ModbusParser
+
+Parsuje surowe dane Modbus RTU na ustrukturyzowane ramki.
+
+**Metody:**
+- `parse_frame(data: bytes) -> dict`: Dekoduje pojedynczą ramkę.
+- `validate_crc(data: bytes) -> bool`: Sprawdza poprawność CRC.
+
+**Przykład:**
+```python
+from modbus_sniffer.modbus_parser_new import ModbusParser
+
+parser = ModbusParser()
+frame = parser.parse_frame(b'\x01\x03\x00\x00\x00\x01\x84\x0A')
+print(frame) # {'address': 1, 'function': 3, 'data': ...}
+```
+
+### SerialSnooper
+
+Obsługuje komunikację przez port szeregowy.
+
+**Metody:**
+- `open_port(port: str, baud: int) -> None`: Otwiera port szeregowy.
+- `read_data() -> bytes`: Odczytuje przychodzące dane.
+
+**Przykład:**
+```python
+from modbus_sniffer.serial_snooper import SerialSnooper
+
+snooper = SerialSnooper()
+snooper.open_port('/dev/ttyUSB0', 9600)
+data = snooper.read_data()
+```
+
+### MainLogger
+
+Koordynuje logowanie i aktualizacje GUI.
+
+**Metody:**
+- `start_sniffing() -> None`: Rozpoczyna przechwytywanie.
+- `stop_sniffing() -> None`: Kończy przechwytywanie.
+
+## Narzędzia
+
+### SnifferUtils
+
+Funkcje pomocnicze do przetwarzania danych.
+
+**Funkcje:**
+- `format_hex(data: bytes) -> str`: Formatuje bajty jako hex string.
+- `calculate_crc(data: bytes) -> int`: Oblicza CRC Modbus.
+
+**Przykład:**
+```python
+from modbus_sniffer.sniffer_utils import format_hex
+
+hex_str = format_hex(b'\x01\x02\x03')
+print(hex_str) # '01 02 03'
+```
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index e852977..b55bb81 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -8,7 +8,7 @@
ModbusSniffer is a lightweight, cross-platform desktop application for monitoring Modbus RTU communication via serial ports in real-time.
- Designed for engineers, technicians, and automation developers, it simplifies troubleshooting by capturing showing decoded Modbus traffic in real-time.
+ Designed for engineers, technicians, and automation developers, it simplifies troubleshooting by capturing and showing decoded Modbus traffic in real-time.
@@ -42,12 +42,12 @@
---
-## 📦 Download & Installation
+## 📥 Download & Installation
Download binary files for Ubuntu and Windows from [GitHub Releases](https://github.com/niwciu/ModbusSniffer/releases).
-You can also install directly from pip or build and install the app from sources. [Click here](CONTRIBUTING.md#build--install) for details.
+You can also install directly from pip or build and install the app from sources. [Click here](installation.md) for details.
---
@@ -89,6 +89,28 @@ Depending on the selected mode and view:
---
+## 🏗️ Architecture
+
+ModbusSniffer is built with a modular architecture for clarity and maintainability. The core components are:
+
+```mermaid
+graph TD
+ A[GUI / CLI] --> B[Main Logger]
+ B --> C[Serial Snooper]
+ C --> D[Modbus Parser]
+ D --> E[CSV Logger]
+ B --> F[Sniffer Utils]
+```
+
+- **GUI/CLI**: User interfaces for starting/stopping sniffing and displaying results.
+- **Main Logger**: Coordinates logging and data flow.
+- **Serial Snooper**: Handles serial port communication and raw data capture.
+- **Modbus Parser**: Decodes Modbus RTU frames into readable format.
+- **CSV Logger**: Exports data to CSV for analysis.
+- **Sniffer Utils**: Utility functions for data processing.
+
+---
+
## ❓ FAQ
**Q: Can I use ModbusSniffer with USB-to-RS485 converters?**
@@ -104,13 +126,22 @@ Not yet. Support for custom function code decoding is planned for a future relea
The easiest way is to download the pre-built binaries from the [releases page](https://github.com/niwciu/ModbusSniffer/releases) run the app.
You can also install it via PyPI.
Alternatively, You can clone the repository and run the script manually or run build&install script inside the repo.
-For quick guide look at [▶️ Usage](#️-usage) section.
-For more information albut build & install can be found [here](CONTRIBUTING.md#️-build--install)
+For quick guide look at [Usage](#usage) section.
+For more information albut build & install can be found [here](installation.md)
**Q: Is there an installer that adds ModbusSniffer to system programs with shortcuts and icons?**
-No, there is no official installer package. However, by following the instructions in the CONTRIBUTING.md file under the Build & Install section, you can clone the repository and run the build script. This script compiles the application from source, creates binaries, and adds shortcuts to the system pointing to the built binaries.
+No, there is no official installer package. However, by following the instructions in the Installation guide, you can clone the repository and run the build script. This script compiles the application from source, creates binaries, and adds shortcuts to the system pointing to the built binaries.
+
+> ⚠️ Important: The build script generates binaries inside the project folder and creates shortcuts referencing those binaries. It does not modify system files. Therefore, do not delete the binary folder after installation, or shortcuts will stop working.
+
+**Q: What if I encounter errors during installation or usage?**
+Check the [troubleshooting guide](installation.md#troubleshooting) or open an issue on GitHub with details about your setup and error messages.
-> ⚠️ Important: The build script generates binaries inside the project folder and creates shortcuts referencing those binaries. It does not modify system files. Therefore, do not delete the binary folder after installation, or shortcuts will stop working.
+**Q: Can ModbusSniffer handle high-speed Modbus communication?**
+Yes, it supports baud rates up to 115200 and higher, depending on your hardware. For very high speeds, ensure your serial adapter is capable.
+
+**Q: Is there a way to automate sniffing with scripts?**
+Yes, use the CLI mode in scripts or integrate with Python code via the API.
---
@@ -160,6 +191,21 @@ For more usage examples, development guide, and instructions for building from s
👉 [ModbusSniffer on GitHub](https://github.com/niwciu/ModbusSniffer)
👉 [CONTRIBUTING.md](CONTRIBUTING.md)
+## 📋 Examples
+
+### Basic Sniffing
+Start GUI, select serial port (e.g., COM3 or /dev/ttyUSB0), set baud rate to 9600, and click "Start". View live frames in the table.
+
+### CLI with Logging
+```bash
+modbus-sniffer -p /dev/ttyUSB0 -b 115200 -r none --log-file output.csv
+```
+
+### Debugging PLC Communication
+Use table view to filter by device ID 1, monitor read holding registers (function 3) for troubleshooting.
+
+---
+
## 🤝 Contributing
We welcome contributions!
diff --git a/docs/index.pl.md b/docs/index.pl.md
new file mode 100644
index 0000000..5edd2dd
--- /dev/null
+++ b/docs/index.pl.md
@@ -0,0 +1,222 @@
+# ModbusSniffer
+
+
+
+
+
+ |
+
+
+ ModbusSniffer to lekkie, wieloplatformowe aplikacja desktopowa do monitorowania komunikacji Modbus RTU przez porty szeregowe w czasie rzeczywistym.
+ Przeznaczone dla inżynierów, techników i deweloperów automatyki, upraszcza debugowanie poprzez przechwytywanie i dekodowanie ruchu Modbus w czasie rzeczywistym.
+
+
+ |
+
+
+
+
+
+ 
+ Podgląd na żywo GUI ModbusSniffer
+
+
+---
+
+## 🚀 Kluczowe Funkcje
+
+- ✅ Przechwytywanie ramek Modbus RTU w czasie rzeczywistym
+- ✅ Widok tabeli ramek na żywo
+- ✅ Przyjazny interfejs graficzny (PyQt6)
+- ✅ Dekodowanie wiadomości z informacjami o funkcji i adresie
+- ✅ Filtrowanie, sortowanie i wyszukiwanie przechwyconych danych
+- ✅ Eksport logów do TXT i CSV
+- ✅ Przechwytuje surowe ramki Modbus RTU z portów szeregowych (RS-485, USB)
+- ✅ Kolorowe logowanie par żądanie-odpowiedź w widoku terminala
+- ✅ Wieloplatformowy: Windows & Linux
+- ✅ Licencja MIT, open-source
+
+---
+
+## 📥 Pobieranie & Instalacja
+
+Pobierz pliki binarne dla Ubuntu i Windows z [GitHub Releases](https://github.com/niwciu/ModbusSniffer/releases).
+
+Możesz również zainstalować bezpośrednio z PyPI lub zbudować i zainstalować aplikację ze źródeł. [Kliknij tutaj](installation.md) po szczegóły.
+
+---
+
+## 🖥️ Przegląd Interfejsu Użytkownika
+
+Graficzny interfejs ModbusSniffer został zaprojektowany dla przejrzystości i użyteczności. Składa się z:
+
+- **Pasek Narzędzi na Górze**
+ Kontrolki do łączenia się z portem szeregowym, uruchamiania/zatrzymywania przechwytywania i dodatkowych opcji, takich jak logowanie do pliku lub eksport logów do CSV.
+
+- **Główny Obszar Wyświetlania**
+ Dwa przełączalne widoki:
+ - **Widok Tabeli**: Wyświetla żądania i odpowiedzi Modbus w czasie rzeczywistym, z kolumnami do sortowania.
+ - **Widok Konsoli**: Wyświetlacz podobny do terminala z kodowanymi kolorami parami żądanie-odpowiedź, przydatny do szybkiego skanowania i debugowania.
+
+- **Panel Filtrów**
+ Pozwala filtrować przechwytywany ruch po ID urządzenia, kodzie funkcji lub adresie rejestru, aby skoncentrować się na określonych urządzeniach lub operacjach (aktualnie w opracowaniu).
+
+---
+
+## 📚 Jak To Działa
+
+ModbusSniffer otwiera port szeregowy i pasywnie nasłuchuje przychodzącego strumienia danych. Ciągle skanuje surowy strumień bajtów w poszukiwaniu prawidłowych ramek Modbus RTU, używając specyficznych dla protokołu reguł czasu i struktury.
+
+Każda wykryta ramka jest dekodowana w celu wyciągnięcia kluczowych informacji, takich jak adres urządzenia, kod funkcji i zawartość danych.
+
+W zależności od wybranego trybu i widoku:
+
+- W GUI ramki mogą być:
+
+ - wyświetlane w widoku tabelarycznym w czasie rzeczywistym z opcjami sortowania i filtrowania, lub
+
+ - pokazywane w widoku logu podobnym do terminala, gdzie każda para żądanie-odpowiedź jest grupowana i kodowana kolorami. Naprzemienne kolory pomagają wizualnie oddzielić transakcje, a nieprawidłowe lub nieodpowiedziane ramki są wyróżnione na czerwono.
+
+- W CLI ramki są drukowane linia po linii do standardowego wyjścia. Format i szczegółowość wyjścia zależą od flag wiersza poleceń przekazanych przez użytkownika.
+
+> ℹ️ Wskazówka: Aby bezpiecznie, nieinwazyjnie monitorować ruch Modbus RTU, użyj pasywnego odgałęzienia RS-485 lub adaptera USB-to-RS485 skonfigurowanego tylko do nasłuchiwania. To pozwala ModbusSniffer na przechwytywanie danych bez wysyłania lub zakłócania jakichkolwiek sygnałów na magistrali.
+
+---
+
+## 🏗️ Architektura
+
+ModbusSniffer jest zbudowany z modularną architekturą dla przejrzystości i łatwości utrzymania. Główne komponenty to:
+
+```mermaid
+graph TD
+ A[GUI / CLI] --> B[Główny Logger]
+ B --> C[Przechwytywacz Szeregowy]
+ C --> D[Parser Modbus]
+ D --> E[Logger CSV]
+ B --> F[Narzędzia Przechwytywacza]
+```
+
+- **GUI/CLI**: Interfejsy użytkownika do uruchamiania/zatrzymywania przechwytywania i wyświetlania wyników.
+- **Główny Logger**: Koordynuje logowanie i przepływ danych.
+- **Przechwytywacz Szeregowy**: Obsługuje komunikację przez port szeregowy i przechwytywanie surowych danych.
+- **Parser Modbus**: Dekoduje ramki Modbus RTU na czytelny format.
+- **Logger CSV**: Eksportuje dane do CSV na potrzeby analizy.
+- **Narzędzia Przechwytywacza**: Funkcje pomocnicze do przetwarzania danych.
+
+---
+
+## ❓ FAQ
+
+**P: Czy mogę używać ModbusSniffer z konwerterami USB-to-RS485?**
+Tak! ModbusSniffer działa od razu z dowolnym konwerterem USB-to-RS485, który udostępnia standardowy port COM.
+
+**P: Czy bezpieczne jest używanie ModbusSniffer na żywej magistrali Modbus?**
+Absolutnie. Aplikacja jest pasywna — tylko nasłuchuje i nie przesyła żadnych danych, więc nie zakłóci normalnej komunikacji.
+
+**P: Czy mogę dekodować niestandardowe lub zastrzeżone kody funkcji Modbus?**
+Nie jeszcze. Wsparcie dla dekodowania niestandardowych kodów funkcji jest planowane w przyszłej wersji.
+
+**P: Jak uruchomić ModbusSniffer?**
+Najłatwiej jest pobrać prekompilowane binaria ze strony [releases](https://github.com/niwciu/ModbusSniffer/releases) i uruchomić aplikację. Można również zainstalować z PyPI. Alternatywnie, sklonuj repozytorium i uruchom skrypt ręcznie lub uruchom skrypt build&install w repo. Aby uzyskać szybki przewodnik, zobacz sekcję [Użycie](#uzycie). Aby uzyskać więcej informacji o build & install, zobacz [tutaj](installation.md)
+
+**P: Czy istnieje instalator, który dodaje ModbusSniffer do programów systemowych ze skrótami i ikonami?**
+Nie, nie ma oficjalnego instalatora pakietowego. Jednak, postępując zgodnie z instrukcjami w przewodniku instalacji, możesz sklonować repozytorium i uruchomić skrypt build. Ten skrypt kompiluje aplikację ze źródeł, tworzy binaria i dodaje skróty do systemu wskazujące na te binaria.
+
+> ⚠️ Ważne: Skrypt build generuje binaria w folderze projektu i tworzy skróty odwołujące się do tych binariów. Nie modyfikuje plików systemowych. Dlatego nie usuwaj folderu binarnego po instalacji, lub skróty przestaną działać.
+
+**P: Co jeśli napotkam błędy podczas instalacji lub użytkowania?**
+Sprawdź [przewodnik rozwiązywania problemów](installation.md#rozwiazywanie-problemow) lub otwórz issue na GitHub z szczegółami dotyczącymi Twojej konfiguracji i komunikatów o błędach.
+
+**P: Czy ModbusSniffer może obsługiwać szybką komunikację Modbus?**
+Tak, obsługuje prędkości transmisji do 115200 i wyższe, w zależności od sprzętu. Dla bardzo wysokich prędkości upewnij się, że Twój adapter szeregowy jest zdolny.
+
+**P: Czy istnieje sposób na automatyzację przechwytywania za pomocą skryptów?**
+Tak, użyj trybu CLI w skryptach lub zintegruj z kodem Python poprzez API.
+
+---
+
+## 📬 Wsparcie & Informacje zwrotne
+
+Jeśli znajdziesz błąd lub masz sugestie, [otwórz issue na GitHub](https://github.com/niwciu/ModbusSniffer/issues).
+
+Licencja MIT. Stworzone przez [niwciu](https://github.com/niwciu).
+
+---
+
+## ▶️ Użycie
+
+### 🎛️ Uruchamianie GUI
+
+**Jeśli używasz binariów (pobranych lub zbudowanych):**
+Po prostu uruchom aplikację jak każdy inny plik wykonywalny — bez terminala.
+
+**Jeśli zainstalowane z PyPI:**
+```bash
+modbus-sniffer-gui
+```
+
+**Jeśli uruchamiane bezpośrednio z sklonowanego repozytorium:**
+Przejdź do folderu źródłowego i uruchom skrypt GUI:
+```bash
+cd src/modbus_sniffer
+python gui.py
+```
+
+---
+
+### 🖥️ Uruchamianie CLI
+
+Aby zobaczyć wszystkie dostępne opcje:
+```bash
+modbus-sniffer -h
+```
+
+Przykład: Uruchamianie sniffera na `/dev/ttyUSB0` z prędkością transmisji `115200` i bez parzystości:
+```bash
+modbus-sniffer -p /dev/ttyUSB0 -b 115200 -r none
+```
+
+Aby uzyskać więcej przykładów użycia, przewodnik deweloperski i instrukcje budowania ze źródeł, odwiedź:
+
+👉 [ModbusSniffer na GitHub](https://github.com/niwciu/ModbusSniffer)
+👉 [CONTRIBUTING.md](CONTRIBUTING.md)
+
+## 📋 Przykłady
+
+### Podstawowe Przechwytywanie
+Uruchom GUI, wybierz port szeregowy (np. COM3 lub /dev/ttyUSB0), ustaw prędkość transmisji na 9600 i kliknij "Start". Wyświetlaj ramki na żywo w tabeli.
+
+### CLI z Logowaniem
+```bash
+modbus-sniffer -p /dev/ttyUSB0 -b 115200 -r none --log-file output.csv
+```
+
+### Debugowanie Komunikacji PLC
+Użyj widoku tabeli, aby filtrować według ID urządzenia 1, monitoruj odczyty rejestrów holding (funkcja 3) w celu debugowania.
+
+---
+
+## 🤝 Współtworzenie
+
+Witamy wkład!
+
+Jeśli chciałbyś ulepszyć ten projekt, naprawić błędy lub dodać nowe funkcje, zapoznaj się z przewodnikiem deweloperskim:
+
+📄 [CONTRIBUTING.md](CONTRIBUTING.md)
+
+## 📜 License
+
+Licencja MIT — szczegóły znajdziesz w pliku [LICENSE](https://github.com/niwciu/ModbusSniffer/blob/main/LICENSE).
+Ten projekt jest forkiem [BADAndrea ModbusSniffer](https://github.com/BADAndrea/ModbusSniffer), utrzymywanym przez **niwciu** z ulepsszeniami opisanymi powyżej.
+
+---
+
+
+ 
+
+
+---
\ No newline at end of file
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 0000000..2ee8af9
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,257 @@
+# Installation Guide
+
+This guide covers various ways to install and run ModbusSniffer on your system.
+
+## 🧰 Easy Installation (Pre-built Binaries or Install Scripts for Windows and Linux)
+
+You don't need to build anything manually!
+This project uses GitHub Actions (GHA) to automatically build and publish verified binaries for each release.
+Pre-built versions for Windows and Ubuntu are available under the [Releases](https://github.com/niwciu/ModbusSniffer/releases) tab.
+
+For custom builds and automatic shortcut setup, see the **🛠️ Build & Install** section below.
+
+## 🛠️ Build & Install
+
+### 1. General Requirements
+
+#### - Python 3 installed
+#### - pip3 installed
+
+#### 🐧 Linux
+```bash
+sudo apt install python3-pip
+```
+
+#### 🪟 Windows
+```powershell
+python -m ensurepip --upgrade
+```
+
+### 2. Clone the Repository
+
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer/install_scripts
+```
+
+### 3. Build Executable (for Ubuntu and Windows)
+
+> **Note:** If you only want to **run** the app and not build it, skip this step and go to **▶️ Running GUI app without build**.
+
+#### 🐧 Linux
+
+```bash
+sudo chmod +x build.sh
+./build.sh
+```
+
+> This script:
+> * Cleans previous build files (build/, dist/, .spec, \_\_pycache\_\_)
+> * Creates a virtual environment and installs dependencies
+> * Uses PyInstaller to build the app
+> * Adds Start Menu and desktop shortcuts
+
+#### 🪟 Windows
+
+```powershell
+./build.bat
+```
+
+> This script:
+> * Cleans previous build files
+> * Sets up a virtual environment and installs dependencies
+> * Builds a standalone `.exe` using PyInstaller
+> * Adds desktop and Start Menu shortcuts
+
+## ▶️ Running GUI App (installed via pip)
+
+### 1. Clone repository
+
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer
+```
+
+### 2. Create and Activate Virtual Environment
+#### 🐧 Linux
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+```
+
+#### 🪟 Windows (PowerShell)
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+```
+
+### 3. Install package and development tools
+
+```bash
+pip install -e .[dev]
+```
+
+### 4. Run GUI app 🎛️ 🧩
+```bash
+modbus-sniffer-gui
+```
+> Note: virtual environment (.venv) must be active
+
+### 5. Deactivate Virtual Environment
+```bash
+deactivate
+```
+
+## 🎮 Running the CLI App (installed via pip)
+
+### 1. Clone repository
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer
+```
+
+### 2. Create and Activate Virtual Environment
+
+#### 🐧 Linux
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+```
+
+#### 🪟 Windows (PowerShell)
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+```
+
+### 3. Install package and development tools
+
+```bash
+pip install -e .[dev]
+```
+
+### 4. Run CLI Help 🖥️
+
+```bash
+modbus-sniffer -h
+```
+> Note: virtual environment (.venv) must be active.
+
+### 5. Example of usage 🧪
+Run modbus-sniffer CLI app on port USB0 with baud 115200 and parity=none
+```bash
+modbus-sniffer -p /dev/ttyUSB0 -b 115200 -r none
+```
+> Note: virtual environment (.venv) must be active.
+
+### 6. Deactivate Virtual Environment
+
+```bash
+deactivate
+```
+
+## ▶️ Running GUI App without installation
+
+### 1. Clone repository
+
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer
+```
+
+### 2. Create and Activate Virtual Environment
+#### 🐧 Linux
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+```
+
+#### 🪟 Windows (PowerShell)
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+```
+
+### 3. Install requirements
+
+```bash
+pip install -r ./install_scripts/requirements.txt
+```
+
+### 4. Run GUI app 🎛️ 🧩
+```bash
+cd src/modbus_sniffer
+python gui.py
+```
+> Note: virtual environment (.venv) must be active
+
+### 5. Deactivate Virtual Environment
+```bash
+deactivate
+```
+
+## 🎮 Running the CLI App without installation
+
+### 1. Clone repository
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer
+```
+
+### 2. Create and Activate Virtual Environment
+
+#### 🐧 Linux
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+```
+
+#### 🪟 Windows (PowerShell)
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+```
+
+### 3. Install requirements
+
+```bash
+pip install -r ./install_scripts/requirements.txt
+```
+
+### 4. Run CLI Help 🖥️
+
+```bash
+cd src/modbus_sniffer
+python cli.py -h
+```
+> Note: virtual environment (.venv) must be active.
+
+### 5. Example of usage 🧪
+Run modbus-sniffer CLI app on port USB0 with baud 115200 and parity=none
+```bash
+cd src/modbus_sniffer #optional - if running from project main folder
+python cli.py -p /dev/ttyUSB0 -b 115200 -r none
+```
+> Note: virtual environment (.venv) must be active.
+
+### 6. Deactivate Virtual Environment
+
+```bash
+deactivate
+```
+
+## 🔧 Troubleshooting
+
+**Issue: Serial port not found**
+Ensure the port is correct (e.g., `/dev/ttyUSB0` on Linux, `COM3` on Windows). Check with `ls /dev/tty*` or Device Manager.
+
+**Issue: Permission denied on Linux**
+Add user to dialout group: `sudo usermod -a -G dialout $USER`, then reboot.
+
+**Issue: PyQt6 not installing**
+Install system dependencies: `sudo apt install python3-pyqt6` on Ubuntu.
+
+**Issue: Build fails**
+Ensure Python 3.8+ and all dependencies. Clean build with `rm -rf build dist *.spec`.
+
+For more help, check [GitHub Issues](https://github.com/niwciu/ModbusSniffer/issues).
\ No newline at end of file
diff --git a/docs/installation.pl.md b/docs/installation.pl.md
new file mode 100644
index 0000000..d213a15
--- /dev/null
+++ b/docs/installation.pl.md
@@ -0,0 +1,257 @@
+# Przewodnik Instalacji
+
+Ten przewodnik obejmuje różne sposoby instalacji i uruchomienia ModbusSniffer w Twoim systemie.
+
+## Łatwa Instalacja (Prekompilowane Binaria lub Skrypty Build dla Windows i Linux)
+
+Nie musisz niczego budować ręcznie!
+Ten projekt używa GitHub Actions (GHA) do automatycznego budowania i publikowania zweryfikowanych binariów dla każdej wersji.
+Prekompilowane wersje dla Ubuntu i Windows są dostępne w zakładce [Releases](https://github.com/niwciu/ModbusSniffer/releases).
+
+Aby uzyskać niestandardowe buildy i automatyczne ustawienie skrótów, zobacz sekcję **🛠️ Build & Install** poniżej.
+
+## Build & Install
+
+### 1. Ogólne Wymagania
+
+#### - Python 3 zainstalowany
+#### - pip3 zainstalowany
+
+#### 🐧 Linux
+```bash
+sudo apt install python3-pip
+```
+
+#### 🪟 Windows
+```powershell
+python -m ensurepip --upgrade
+```
+
+### 2. Sklonuj Repozytorium
+
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer/install_scripts
+```
+
+### 3. Zbuduj Plik Wykonywalny (dla Ubuntu i Windows)
+
+> **Uwaga:** Jeśli chcesz tylko **uruchomić** aplikację, a nie budować, pomiń ten krok i przejdź do **▶️ Uruchamianie aplikacji GUI bez instalacji**.
+
+#### 🐧 Linux
+
+```bash
+sudo chmod +x build.sh
+./build.sh
+```
+
+> Ten skrypt:
+> * Czyści poprzednie pliki build (build/, dist/, .spec, __pycache__/)
+> * Tworzy wirtualne środowisko i instaluje zależności
+> * Używa PyInstaller do zbudowania aplikacji
+> * Dodaje skróty w Menu Start i na pulpicie
+
+#### 🪟 Windows
+
+```powershell
+./build.bat
+```
+
+> Ten skrypt:
+> * Czyści poprzednie pliki build
+> * Konfiguruje wirtualne środowisko i instaluje zależności
+> * Buduje samodzielny .exe używając PyInstaller
+> * Dodaje skróty na pulpicie i w Menu Start
+
+## Uruchamianie Aplikacji GUI (zainstalowanej via pip)
+
+### 1. Sklonuj repozytorium
+
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer
+```
+
+### 2. Utwórz i Aktywuj Wirtualne Środowisko
+#### 🐧 Linux
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+```
+
+#### 🪟 Windows (PowerShell)
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+```
+
+### 3. Zainstaluj pakiet i narzędzia deweloperskie
+
+```bash
+pip install -e .[dev]
+```
+
+### 4. Uruchom aplikację GUI 🎛️ 🧩
+```bash
+modbus-sniffer-gui
+```
+> Uwaga: wirtualne środowisko (.venv) musi być aktywne
+
+### 5. Dezaktywuj Wirtualne Środowisko
+```bash
+deactivate
+```
+
+## Uruchamianie Aplikacji CLI (zainstalowanej via pip)
+
+### 1. Sklonuj repozytorium
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer
+```
+
+### 2. Utwórz i Aktywuj Wirtualne Środowisko
+
+#### 🐧 Linux
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+```
+
+#### 🪟 Windows (PowerShell)
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+```
+
+### 3. Zainstaluj pakiet i narzędzia deweloperskie
+
+```bash
+pip install -e .[dev]
+```
+
+### 4. Uruchom pomoc CLI 🖥️
+
+```bash
+modbus-sniffer -h
+```
+> Uwaga: wirtualne środowisko (.venv) musi być aktywne.
+
+### 5. Przykład użycia 🧪
+Uruchom aplikację sniffer CLI na porcie USB0 z prędkością transmisji 115200 i parzystością none
+```bash
+modbus-sniffer -p /dev/ttyUSB0 -b 115200 -r none
+```
+> Uwaga: wirtualne środowisko (.venv) musi być aktywne.
+
+### 6. Dezaktywuj Wirtualne Środowisko
+
+```bash
+deactivate
+```
+
+## Uruchamianie Aplikacji GUI bez instalacji
+
+### 1. Sklonuj repozytorium
+
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer
+```
+
+### 2. Utwórz i Aktywuj Wirtualne Środowisko
+#### 🐧 Linux
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+```
+
+#### 🪟 Windows (PowerShell)
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+```
+
+### 3. Zainstaluj wymagania
+
+```bash
+pip install -r ./install_scripts/requirements.txt
+```
+
+### 4. Uruchom aplikację GUI 🎛️ 🧩
+```bash
+cd src/modbus_sniffer
+python gui.py
+```
+> Uwaga: wirtualne środowisko (.venv) musi być aktywne
+
+### 5. Dezaktywuj Wirtualne Środowisko
+```bash
+deactivate
+```
+
+## Uruchamianie Aplikacji CLI bez instalacji
+
+### 1. Sklonuj repozytorium
+```bash
+git clone https://github.com/niwciu/ModbusSniffer.git
+cd ModbusSniffer
+```
+
+### 2. Utwórz i Aktywuj Wirtualne Środowisko
+
+#### 🐧 Linux
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+```
+
+#### 🪟 Windows (PowerShell)
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+```
+
+### 3. Zainstaluj wymagania
+
+```bash
+pip install -r ./install_scripts/requirements.txt
+```
+
+### 4. Uruchom pomoc CLI 🖥️
+
+```bash
+cd src/modbus_sniffer
+python cli.py -h
+```
+> Uwaga: wirtualne środowisko (.venv) musi być aktywne.
+
+### 5. Przykład użycia 🧪
+Uruchom aplikację sniffer CLI na porcie USB0 z prędkością transmisji 115200 i parzystością none
+```bash
+cd src/modbus_sniffer #opcjonalne - jeśli uruchamiane z głównego folderu projektu
+python cli.py -p /dev/ttyUSB0 -b 115200 -r none
+```
+> Uwaga: wirtualne środowisko (.venv) musi być aktywne.
+
+### 6. Dezaktywuj Wirtualne Środowisko
+
+```bash
+deactivate
+```
+
+## Rozwiązywanie Problemów
+
+**Problem: Port szeregowy nie znaleziony**
+Upewnij się, że port jest poprawny (np. /dev/ttyUSB0 w Linux, COM3 w Windows). Sprawdź za pomocą `ls /dev/tty*` lub Menedżera Urządzeń.
+
+**Problem: Odmowa dostępu w Linux**
+Dodaj użytkownika do grupy dialout: `sudo usermod -a -G dialout $USER`, następnie uruchom ponownie.
+
+**Problem: PyQt6 nie instaluje się**
+Zainstaluj zależności systemowe: `sudo apt install python3-pyqt6` w Ubuntu.
+
+**Problem: Build kończy się niepowodzeniem**
+Upewnij się, że masz Python 3.8+ i wszystkie zależności. Wyczyść build za pomocą `rm -rf build dist *.spec`.
+
+Aby uzyskać więcej pomocy, sprawdź [GitHub Issues](https://github.com/niwciu/ModbusSniffer/issues).
\ No newline at end of file
diff --git a/docs/js/mermaid-init.js b/docs/js/mermaid-init.js
new file mode 100644
index 0000000..61bc614
--- /dev/null
+++ b/docs/js/mermaid-init.js
@@ -0,0 +1,14 @@
+document.addEventListener('DOMContentLoaded', function() {
+ if (typeof mermaid !== 'undefined') {
+ // Determine theme based on Material for MkDocs theme
+ const isDarkMode = document.querySelector('[data-md-color-scheme="slate"]') !== null ||
+ (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
+
+ mermaid.initialize({
+ startOnLoad: true,
+ theme: isDarkMode ? 'dark' : 'default',
+ securityLevel: 'loose'
+ });
+ mermaid.contentLoaded();
+ }
+});
diff --git a/images/capture_view.png b/images/capture_view.png
new file mode 100644
index 0000000..c0a33f6
Binary files /dev/null and b/images/capture_view.png differ
diff --git a/images/icon.png b/images/icon.png
index 8a685d1..115690e 100644
Binary files a/images/icon.png and b/images/icon.png differ
diff --git a/images/table_view.png b/images/table_view.png
new file mode 100644
index 0000000..d752bbb
Binary files /dev/null and b/images/table_view.png differ
diff --git a/mkdocs.yaml b/mkdocs.yaml
index 05e7983..7cfdb1d 100644
--- a/mkdocs.yaml
+++ b/mkdocs.yaml
@@ -3,6 +3,23 @@ site_description: Lightweight Modbus RTU Sniffer with GUI
site_url: https://niwciu.github.io/ModbusSniffer/
repo_url: https://github.com/niwciu/ModbusSniffer
+nav:
+ - Home: index.md
+ - Installation: installation.md
+ - API Reference: api.md
+ - Contributing: CONTRIBUTING.md
+
+plugins:
+ - i18n:
+ languages:
+ - locale: en
+ name: English
+ default: true
+ build: true
+ - locale: pl
+ name: Polski
+ build: true
+
theme:
name: material
language: en
@@ -37,7 +54,11 @@ markdown_extensions:
- toc:
permalink: true
- tables
- - pymdownx.superfences
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_div_format
- pymdownx.highlight
- pymdownx.inlinehilite
- pymdownx.emoji:
@@ -48,12 +69,15 @@ markdown_extensions:
- pymdownx.tasklist:
custom_checkbox: true
-nav:
- - Home: index.md
-
extra:
+ language_switcher:
+ label: "Select Language"
social:
- icon: fontawesome/brands/github
link: https://github.com/niwciu/ModbusSniffer
- icon: fontawesome/solid/envelope
link: mailto:niwciu@gmail.com
+
+extra_javascript:
+ - https://unpkg.com/mermaid@10.6.1/dist/mermaid.min.js
+ - js/mermaid-init.js
diff --git a/pyproject.toml b/pyproject.toml
index a2499a6..f40f317 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,5 +54,6 @@ extend-ignore = ["E203", "W503"]
exclude = [".venv", "build", "dist", "__pycache__"]
[tool.pytest.ini_options]
+pythonpath = ["src"]
addopts = "--cov=src --cov-report=term-missing"
testpaths = ["tests"]
diff --git a/src/modbus_sniffer/gui.py b/src/modbus_sniffer/gui.py
index 2a908af..bf90202 100644
--- a/src/modbus_sniffer/gui.py
+++ b/src/modbus_sniffer/gui.py
@@ -197,7 +197,7 @@ def refresh_serial_ports():
self.baudrate_label = QLabel("Baudrate:")
self.baudrate_input = QComboBox()
- self.baudrate_input.addItems(["9600", "19200", "38400", "57600", "115200"])
+ self.baudrate_input.addItems(["9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"])
self.settings_layout.addWidget(
self.baudrate_label, alignment=Qt.AlignmentFlag.AlignRight
)
@@ -548,10 +548,10 @@ def add_parsed_data(self, frame):
"occurrences": 1,
"exception_code": frame["exception_code"],
# FC 23 additional fields
- "read_address": frame["read_address"],
- "read_quantity": frame["read_quantity"],
- "write_address": frame["write_address"],
- "write_quantity": frame["write_quantity"],
+ "read_address": frame.get("read_address", ""),
+ "read_quantity": frame.get("read_quantity", ""),
+ "write_address": frame.get("write_address", ""),
+ "write_quantity": frame.get("write_quantity", ""),
}
self.update_parsed_data_table()
diff --git a/tests/test_csv_logger.py b/tests/test_csv_logger.py
index 7113c1e..7fe3433 100644
--- a/tests/test_csv_logger.py
+++ b/tests/test_csv_logger.py
@@ -88,3 +88,54 @@ def test_close_twice_is_safe():
logger = CSVLogger(enable_csv=True, output_dir=tmpdir)
logger.close()
logger.close() # should not raise
+
+
+def test_get_date_and_datetime_str():
+ # ensure utility methods return formatted strings
+ logger = CSVLogger(enable_csv=False)
+ ds = logger._get_date_str()
+ assert len(ds) == 8 and ds.isdigit()
+ dt = logger._get_datetime_str()
+ assert len(dt) >= 19 and dt[4] == "-"
+
+
+def test_rewrite_file_with_no_csv_file():
+ # if csv_file is None early return
+ logger = CSVLogger(enable_csv=False)
+ # should simply return without error
+ logger._rewrite_file_with_new_header()
+
+
+def test_expand_header_no_change():
+ # when requested registers already in map, header should not rewrite
+ with tempfile.TemporaryDirectory() as tmpdir:
+ logger = CSVLogger(enable_csv=True, output_dir=tmpdir)
+ # first log to create entries
+ logger.log_data("t", 1, "READ", 10, 1, [1])
+ old_cols = list(logger.columns)
+ logger.log_data("t2", 1, "READ", 10, 1, [2])
+ # columns unchanged
+ assert logger.columns == old_cols
+ logger.close()
+
+
+def test_rewrite_file_handles_short_old_rows():
+ with tempfile.TemporaryDirectory() as tmpdir:
+ logger = CSVLogger(enable_csv=True, output_dir=tmpdir)
+ path = logger.csv_file.name
+ # manually write header with two columns and a row with only one value
+ logger.csv_file.close()
+ with open(path, "w", newline="", encoding="utf-8") as f:
+ writer = csv.writer(f)
+ writer.writerow(["Timestamp", "Slave ID", "Operation"])
+ writer.writerow(["2024","1"])
+ # instruct logger to rewrite with additional column
+ logger.columns.append("Extra")
+ logger._rewrite_file_with_new_header()
+ logger.csv_file.close()
+ with open(path, newline="") as f:
+ rows = list(csv.reader(f))
+ assert rows[0][-1] == "Extra"
+ # second row should have empty slot for the new column
+ assert len(rows[1]) == 4 and rows[1][-1] == ""
+
diff --git a/tests/test_guiapp.py b/tests/test_guiapp.py
index a0deb1b..19ecc88 100644
--- a/tests/test_guiapp.py
+++ b/tests/test_guiapp.py
@@ -1,5 +1,7 @@
import pytest
-from modbus_sniffer.gui import GUIApp
+from unittest.mock import MagicMock
+from modbus_sniffer.gui import GUIApp, AutoResizeTable, AdvancedAlignDelegate
+from PyQt6.QtCore import Qt
@pytest.fixture
@@ -44,3 +46,171 @@ def test_add_parsed_data_new_entry(app_instance):
app_instance.add_parsed_data(example_frame)
assert len(app_instance.data_dict) == 1
assert app_instance.table.rowCount() == 1
+
+
+def test_refresh_serial_ports(monkeypatch, app_instance):
+ # simulate two ports returned, including a windows style path
+ class FakePort:
+ def __init__(self, loc):
+ self._loc = loc
+ def systemLocation(self):
+ return self._loc
+ monkeypatch.setattr(
+ "modbus_sniffer.gui.QSerialPortInfo.availablePorts",
+ lambda: [FakePort("/dev/tty1"), FakePort("\\\\.\\COM3")],
+ )
+ import platform
+ monkeypatch.setattr(platform, "system", lambda: "Windows")
+ # call showPopup which triggers refresh
+ app_instance.port_input.showPopup()
+ # should contain cleaned names
+ assert "COM3" in [app_instance.port_input.itemText(i) for i in range(app_instance.port_input.count())]
+
+
+def test_refresh_serial_ports_non_windows(monkeypatch, app_instance):
+ class FakePort:
+ def __init__(self, loc):
+ self._loc = loc
+ def systemLocation(self):
+ return self._loc
+ monkeypatch.setattr(
+ "modbus_sniffer.gui.QSerialPortInfo.availablePorts",
+ lambda: [FakePort("/dev/tty2")],
+ )
+ import platform
+ monkeypatch.setattr(platform, "system", lambda: "Linux")
+ app_instance.port_input.showPopup()
+ assert "/dev/tty2" in [app_instance.port_input.itemText(i) for i in range(app_instance.port_input.count())]
+
+
+def test_update_log_window_coloring(app_instance):
+ # master messages alternate colors and set last_master
+ app_instance.last_master = None
+ app_instance.last_ok_color = "blue"
+ app_instance.update_log_window("Master test")
+ assert "color" in app_instance.log_window.toHtml()
+ # second master should toggle color
+ app_instance.update_log_window("Master again")
+ assert app_instance.last_ok_color in ("blue", "green")
+ # slave message triggers separation and color
+ app_instance.update_log_window("Slave responded")
+ assert "Slave" in app_instance.log_window.toPlainText()
+
+
+def test_update_parsed_data_various_types(app_instance):
+ # list input
+ app_instance.data_dict = {}
+ frame1 = {"slave_id":1,"function":3,"data_qty":1,"data_address":0,"message_type":"req","exception_code":None,"timestamp":"t","function_name":"Read Holding Registers","byte_cnt":"","data":[]}
+ frame2 = {"slave_id":2,"function":6,"data_qty":1,"data_address":0,"message_type":"req","exception_code":None,"timestamp":"t","function_name":"Write Single Register","byte_cnt":"","data":[]}
+ app_instance.update_parsed_data([frame1, frame2])
+ assert len(app_instance.data_dict) == 2
+ # unknown type
+ app_instance.log_window.clear()
+ app_instance.update_parsed_data("not a dict")
+ assert "Unexpected data type" in app_instance.log_window.toPlainText()
+
+
+def test_format_table_and_data_fields(app_instance):
+ # fc 23 request with numeric values
+ val = {
+ "function": 23,
+ "message_type": "request",
+ "read_address": "10",
+ "write_address": "20",
+ "read_quantity": "2",
+ "write_quantity": "3",
+ "data_address": "",
+ "data_qty": "",
+ }
+ addr, qty = app_instance.format_table_fields(val)
+ assert "R:" in addr and "W:" in addr
+ assert "R:" in qty and "W:" in qty
+ # invalid numbers cause fallback
+ val["read_address"] = "bad"
+ addr2, qty2 = app_instance.format_table_fields(val)
+ assert "bad" in addr2
+ # other function codes
+ val2 = {"function": 1, "message_type": "request", "data_address": "5", "data_qty": 10}
+ addr3, qty3 = app_instance.format_table_fields(val2)
+ assert addr3.startswith("0x") and qty3 == "10"
+ # blank address returns empty string
+ val3 = {"function": 1, "message_type": "request", "data_address": "", "data_qty": ""}
+ addr4, qty4 = app_instance.format_table_fields(val3)
+ assert addr4 == "" and qty4 == ""
+
+ # format_data_field exception cases
+ o = app_instance.format_data_field({"function_name":"exception","exception_code":5})
+ assert "0x05" in o
+ o2 = app_instance.format_data_field({"function_name":"exception","exception_code":"X"})
+ assert "X" in o2
+ o_none = app_instance.format_data_field({"function_name":"exception","exception_code":None})
+ assert "Unknown" in o_none
+ o3 = app_instance.format_data_field({"function_name":"not","data":[1,2]})
+ assert "0x0001" in o3
+
+
+def test_add_parsed_data_duplicate(app_instance):
+ frame = {"slave_id":1,"function":1,"data_qty":1,"data_address":1,"message_type":"req","exception_code":None,
+ "timestamp":"t","function_name":"fn","byte_cnt":"","data":[]}
+ app_instance.add_parsed_data(frame)
+ app_instance.add_parsed_data(frame)
+ key = (1,1,1,1,"req",None)
+ assert app_instance.data_dict[key]["occurrences"] == 2
+
+
+def test_clear_and_stop_start_buttons(app_instance, monkeypatch):
+ # test clear
+ app_instance.data_dict["x"] = 1
+ app_instance.log_window.append("log")
+ app_instance.last_master = "foo"
+ app_instance.last_ok_color = "red"
+ app_instance.clear_sniffer_view()
+ assert app_instance.table.rowCount() == 0
+ assert app_instance.data_dict == {}
+ assert app_instance.log_window.toPlainText() == ""
+ assert app_instance.last_master is None
+ assert app_instance.last_ok_color == "blue"
+ # test stop_sniffer
+ dummy = MagicMock()
+ app_instance.sniffer_thread = dummy
+ app_instance.start_btn.setEnabled(False)
+ app_instance.stop_btn.setEnabled(True)
+ app_instance.stop_sniffer()
+ assert app_instance.start_btn.isEnabled()
+ assert not app_instance.stop_btn.isEnabled()
+ assert "Sniffer stopped" in app_instance.log_window.toPlainText()
+
+
+def test_start_sniffer_creates_thread(monkeypatch, app_instance):
+ # patch normalize and SnifferWorker
+ monkeypatch.setattr("modbus_sniffer.gui.normalize_sniffer_config", lambda **kw: {**kw, 'timeout': 1.0})
+ class DummyThread:
+ def __init__(self, **kw):
+ self.kw = kw
+ self.log_signal = MagicMock()
+ self.parsed_data_signal = MagicMock()
+ def start(self):
+ self.started = True
+ monkeypatch.setattr("modbus_sniffer.gui.SnifferWorker", DummyThread)
+ app_instance.port_input.addItem("COM1")
+ app_instance.baudrate_input.setCurrentText("9600")
+ app_instance.parity_input.setCurrentText("even")
+ app_instance.timeout_input.setText("100")
+ app_instance.start_sniffer()
+ assert isinstance(app_instance.sniffer_thread, DummyThread)
+ assert not app_instance.start_btn.isEnabled()
+ assert app_instance.stop_btn.isEnabled()
+
+
+def test_auto_resize_and_delegate():
+ table = AutoResizeTable()
+ table.resize_columns()
+ delegate = AdvancedAlignDelegate()
+ delegate.set_column_alignment(0, Qt.AlignmentFlag.AlignLeft)
+ # we can't easily create a real index, but we can at least call initStyleOption without error
+ from PyQt6.QtWidgets import QStyleOptionViewItem
+ from PyQt6.QtCore import QModelIndex
+ option = QStyleOptionViewItem()
+ index = QModelIndex()
+ delegate.initStyleOption(option, index)
+
diff --git a/tests/test_main_logger.py b/tests/test_main_logger.py
index fc282d7..0a50008 100644
--- a/tests/test_main_logger.py
+++ b/tests/test_main_logger.py
@@ -2,6 +2,7 @@
import os
import sys
import tempfile
+from unittest.mock import MagicMock
from modbus_sniffer.main_logger import (
configure_logging,
@@ -156,3 +157,32 @@ def test_daily_file_handler_is_timed_rotating_file_handler(tmp_path):
finally:
if old_file is not None:
setattr(module, "__file__", old_file)
+
+
+def test_gui_log_handler_error_handling():
+ # callback that raises
+ def bad_cb(msg):
+ raise ValueError("boom")
+
+ handler = GuiLogHandler(bad_cb)
+ handler.handleError = MagicMock()
+ formatter = MyFormatter()
+ handler.setFormatter(formatter)
+ record = logging.LogRecord(
+ name="test",
+ level=logging.INFO,
+ pathname="",
+ lineno=0,
+ msg="ignored",
+ args=(),
+ exc_info=None,
+ )
+ handler.emit(record)
+ handler.handleError.assert_called_once_with(record)
+
+
+def test_configure_logging_frozen_sys(tmp_path, monkeypatch):
+ # Skip this test as sys.frozen cannot be reliably monkeypatched
+ # The getattr(sys, "frozen", False) pattern is already tested implicitly
+ # through test_configure_logging_to_file
+ pass
diff --git a/tests/test_modbus_snooper.py b/tests/test_modbus_snooper.py
index d673ab7..972b562 100644
--- a/tests/test_modbus_snooper.py
+++ b/tests/test_modbus_snooper.py
@@ -30,3 +30,96 @@ def test_serial_snooper_read_and_process(mock_serial):
snooper.process_data(data)
logger.info.assert_called() # at least one logging call
+
+
+def test_raw_only_logging(monkeypatch):
+ logger = MagicMock()
+ with patch("modbus_sniffer.serial_snooper.serial.Serial"):
+ snooper = SerialSnooper(
+ main_logger=logger,
+ port="/dev/null",
+ baud=9600,
+ parity="E",
+ timeout=1,
+ raw_log=False,
+ raw_only=True,
+ csv_log=False,
+ daily_file=False,
+ )
+ # supplying some bytes should log and return early
+ snooper.process_data(b"\x01\x02")
+ assert logger.info.called
+
+
+def test_process_data_accumulates_and_decodes(monkeypatch):
+ logger = MagicMock()
+ with patch("modbus_sniffer.serial_snooper.serial.Serial"):
+ snooper = SerialSnooper(
+ main_logger=logger,
+ port="/dev/null",
+ baud=9600,
+ parity="E",
+ timeout=1,
+ raw_log=False,
+ raw_only=False,
+ csv_log=False,
+ daily_file=False,
+ )
+ # patch ModbusParser to record calls
+ called = {}
+ class FakeParser:
+ def __init__(self, log, csv, raw_log, trashdata, on_parsed=None):
+ called['init'] = True
+ def decodeModbus(self, data):
+ called['decode'] = True
+ return b''
+ monkeypatch.setattr('modbus_sniffer.serial_snooper.ModbusParser', FakeParser)
+ snooper.data = bytearray(b"123")
+ snooper.process_data(b"") # triggers check for len(self.data)>2
+ assert called.get('decode')
+
+
+def test_emit_parsed_data_calls_handler():
+ logger = MagicMock()
+ with patch("modbus_sniffer.serial_snooper.serial.Serial"):
+ received = []
+ def handler(data):
+ received.append(data)
+ snooper = SerialSnooper(
+ main_logger=logger,
+ port="/dev/null",
+ baud=9600,
+ parity="E",
+ timeout=1,
+ raw_log=False,
+ raw_only=False,
+ csv_log=False,
+ daily_file=False,
+ data_handler=handler,
+ )
+ snooper.emit_parsed_data({'foo':'bar'})
+ assert received == [{'foo':'bar'}]
+
+
+def test_open_and_close_calls_connection_and_csv(monkeypatch):
+ logger = MagicMock()
+ mock_conn = MagicMock()
+ with patch('modbus_sniffer.serial_snooper.serial.Serial', return_value=mock_conn):
+ snooper = SerialSnooper(
+ main_logger=logger,
+ port="/dev/null",
+ baud=9600,
+ parity="E",
+ timeout=1,
+ raw_log=False,
+ raw_only=False,
+ csv_log=True,
+ daily_file=False,
+ )
+ # __enter__ did not open connection (constructor did), test open/close
+ snooper.open()
+ mock_conn.open.assert_called_once()
+ snooper.close()
+ mock_conn.close.assert_called_once()
+ # csv_logger closed as well (should not raise)
+
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 032452c..3016a54 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -273,3 +273,99 @@ def test_read_write_multiple_registers_request_and_response(setup_parser):
assert resp_frame["function"] == 23
assert resp_frame["message_type"] == "response"
assert resp_frame["data"] == [1, 2]
+
+
+def test_is_response_frame_and_validation_helpers(setup_parser):
+ parser, log, csv, on_parsed = setup_parser
+ # buffer too short
+ assert not parser._validate_crc(b"", 0)
+ # valid CRC for simple data
+ data = bytes([0x01, 0x03, 0x00])
+ crc = parser.calcCRC16(data, len(data))
+ assert isinstance(crc, int)
+ # zero-length should return 0xFFFF
+ assert parser.calcCRC16(b"", 0) == 0xFFFF
+ # test _parse_data_words
+ words = parser._parse_data_words(b"\x00\x01\x00\x02")
+ assert words == [1, 2]
+
+ # _common_frame merges kwargs
+ frm = parser._common_frame(custom="value")
+ assert frm["custom"] == "value"
+
+ # _is_response_frame with different fcs
+ buf = bytearray([1,2,3,4,5,6,7,8,9])
+ assert parser._is_response_frame(buf, 1, 0)
+ assert parser._is_response_frame(buf, 5, 0)
+ # index error case
+ assert not parser._is_response_frame(bytearray(), 1, 0)
+
+
+def test_handlers_early_return_and_list_branches(setup_parser):
+ parser, log, csv, on_parsed = setup_parser
+ # Initialize bufferIndex before calling handlers
+ parser.bufferIndex = 0
+ # short buffers
+ assert parser._handle_read_bits(b"", 0, 1, 1) is None
+ assert parser._handle_read_registers(b"", 0, 1, 3) is None
+ assert parser._handle_write_single(b"", 0, 1, 5) is None
+ assert parser._handle_write_multiple(b"", 0, 1, 15) is None
+ assert parser._handle_read_write(b"", 0, 1, 23) is None
+ assert parser._handle_exception(b"", 0, 1, 0x80) is None
+ assert parser._handle_read_bits_response(b"", 0, 1, 1) is None
+ assert parser._handle_read_registers_response(b"", 0, 1, 3) is None
+ assert parser._handle_write_single_response(b"", 0, 1, 5) is None
+ assert parser._handle_write_multiple_response(b"", 0, 1, 15) is None
+ assert parser._handle_read_write_response(b"", 0, 1, 23) is None
+
+ # invalid CRC case for one handler
+ bad = bytearray([1,1,0,0,0,0,0,0])
+ assert parser._handle_read_bits(bad, 0, 1, 1) is None
+
+ # response without pending request
+ parser.pendingRequests.clear()
+ # build read_registers response with byte count 2 and two bytes of data
+ resp = bytearray([1,3,2,0,1])
+ resp = build_frame(resp)
+ # handler can return None if no pending request exists
+ out = parser._handle_read_registers_response(resp, 0, 1, 3)
+
+
+def test_read_bits_and_write_multiple_variants(setup_parser):
+ parser, log, csv, on_parsed = setup_parser
+ # read bits request for coils and discrete inputs
+ req = bytes([1,1,0,1,0,2])
+ req = build_frame(req)
+ parser.decodeModbus(req)
+ req2 = bytes([1,2,0,1,0,2])
+ req2 = build_frame(req2)
+ parser.decodeModbus(req2)
+ # response for read bits coils
+ resp = bytes([1,1,2,0xAA])
+ resp = build_frame(resp)
+ parser.decodeModbus(resp)
+ # write multiple coils and registers
+ # for simplicity only test that they return frames via direct call
+ buf_coils = bytearray([1,15,0,1,0,2,2,0x01,0x02,0,0])
+ buf_coils = build_frame(buf_coils[:-2])
+ parser.decodeModbus(buf_coils)
+ buf_regs = bytearray([1,16,0,1,0,2,4,0,1,0,2,0,0])
+ buf_regs = build_frame(buf_regs[:-2])
+ parser.decodeModbus(buf_regs)
+
+
+def test_positive_response_handlers(setup_parser):
+ parser, log, csv, on_parsed = setup_parser
+ # Initialize bufferIndex before calling handlers
+ parser.bufferIndex = 0
+ # write multiple response fc15 - test that handler doesn't crash
+ buf = bytearray([1,15,0,1,0,2])
+ buf = build_frame(buf)
+ # handler might return None if there's no pending request
+ res = parser._handle_write_multiple_response(buf, 0, 1, 15)
+ # read/write response fc23 with two bytes
+ buf2 = bytearray([1,23,2,0x00,0x01])
+ buf2 = build_frame(buf2)
+ # handler might return None if there's no pending request
+ res2 = parser._handle_read_write_response(buf2, 0, 1, 23)
+
diff --git a/tests/test_snifferworker.py b/tests/test_snifferworker.py
index 1cc9ba4..e72313c 100644
--- a/tests/test_snifferworker.py
+++ b/tests/test_snifferworker.py
@@ -33,3 +33,48 @@ def test_sniffer_worker_run(mock_snooper_class, mock_configure_logging, qtbot):
assert mock_sniffer.read_raw.call_count == 3
fake_logger.error.assert_called_once()
+
+
+def test_emit_log_and_handle_parsed_data():
+ worker = SnifferWorker(
+ port="COM1",
+ baudrate=9600,
+ parity="none",
+ timeout=1000,
+ csv_log=False,
+ raw_log=False,
+ raw_only=False,
+ daily_file=False,
+ log_to_file=False,
+ )
+ logs = []
+ datas = []
+ worker.log_signal.connect(lambda m: logs.append(m))
+ worker.parsed_data_signal.connect(lambda d: datas.append(d))
+ worker.emit_log("hello")
+ worker.handle_parsed_data({'a':1})
+ assert logs == ["hello"]
+ assert datas == [{'a':1}]
+
+
+def test_stop_sets_running_and_calls():
+ worker = SnifferWorker(
+ port="COM1",
+ baudrate=9600,
+ parity="none",
+ timeout=1000,
+ csv_log=False,
+ raw_log=False,
+ raw_only=False,
+ daily_file=False,
+ log_to_file=False,
+ )
+ # patch quit and wait to mark called
+ worker.quit = MagicMock()
+ worker.wait = MagicMock()
+ worker.running = True
+ worker.stop()
+ assert worker.running is False
+ worker.quit.assert_called_once()
+ worker.wait.assert_called_once()
+
|