# LBMS Laser gun monitoring system: listens on a serial port for XBee packets from 44 tracked guns, decodes fire counts and "seen" beacons, estimates on-time (battery use), and displays a live grid of gun tiles. Includes a history dialog (per-gun stats, battery-change log) and a simulate mode that replays captured log files. This repository contains: - `LBMS/` — the original VB.NET WinForms project (reference, unchanged) - `lbms/`, `main.py` — the **Python port** (documented below) ## Python port ### Requirements - Python 3.8+ - `tkinter` (standard library; on Debian/Ubuntu: `sudo apt install python3-tk`) - [`pyserial`](https://pypi.org/project/pyserial/) ```sh pip install -r requirements.txt ``` ### Running ```sh python3 main.py [options] ``` Options (all optional, `key=value` pairs): | Option | Default | Description | |-----------|---------|----------------------------------------------------------| | `com` | — | Serial port to open automatically on start (e.g. `/dev/ttyUSB0`). The main window is minimized when this is used. | | `admin` | `false` | `admin=true` unlocks the SetFired/SetTime controls in the history dialog. | | `fired` | `11000` | Fire-count threshold; reaching it flags the gun for a battery change. | | `battery` | `660` | On-time (minutes) threshold; reaching it flags the gun for a battery change. | If no port is given, enter one in the text box (e.g. `COM4` / `/dev/ttyUSB0`) and click **Button1** to open/close it. Status shows in `Label1` (Open/Closed). ### Interface - **LBMS Console** (main window) — port controls, live decode stats (lines read, valid packets, queue/decode lengths, raw hex), and a scrolling per-gun summary. - **LBMS** (gun grid) — one tile per gun, 8x6 grid. Tile color: - green — seen in the last second - orange — seen in the last second but flagged for battery change - fading green — seen within the last 30 seconds - default grey — no recent contact - gun icon appears for ~3 s after a fire event; battery icon shows estimated remaining time/shots; both flash when a battery change is due. - Click a tile to open its **History** dialog: current + historical fired/on-time totals, change log, and **Change Batteries** (rolls current totals into history and zeroes them). Admin mode adds SetFired/SetTime. - Double-click the grid window to exit the app (with confirmation). - **Simulate** — pick a capture log (`timestamp,hex` per line, e.g. the files inside `LBMS/220619.zip`), and packets are replayed through the normal decode pipeline at their original timing. ### Serial protocol The port runs 115200, 8N1. Data is uppercased to hex and fed into a queue; the 75 ms main timer drains it into a decode buffer and scans for: | Marker | Length (hex chars) | Layout | Action | |-----------|--------------------|---------------------------------------------------------------|-------------------------------| | `7E002480`| 80 | head(8) + MAC(16) + payload(34) + fire count(4) + tail(18) | add fire count, mark seen | | `7E002A80`| 92 | head(8) + MAC(16) + payload(36) + tail(32) | mark seen | Fire count is big-endian (`hi*256 + lo`). When a count ever decreases (gun rebooted), the previous count is added to the gun's overall total and battery time is rolled over. Every time a known gun is seen and the last count is older than `CountInterval` (10 min), that interval is added to its on-time — which is how battery life is estimated. Junk data: if no packet is found and the buffer exceeds 200 chars, the first half is discarded. ### Persistence State is saved to `./items/.cfg` (one JSON file per gun) on close and every ~61 s. On start, if `./items/` exists it is loaded; otherwise all 44 guns are created fresh from the MAC list in `lbms/main_window.py` (`_create_id_list`). ### Project layout ``` main.py # entry point requirements.txt lbms/ ├── globals.py # shared state: gun list, message queue, helpers ├── gun_object.py # GunObject model + JSON save/load ├── cls_com_ports.py # ComPorts serial wrapper (pyserial) ├── resources.py # PNG image loader (lbms/resources/) ├── gun_button.py # one gun tile (text, colors, battery/gun icons) ├── frm_guns.py # gun grid window ├── frm_history.py # per-gun history dialog └── main_window.py # console window: timers, serial reader, packet decoder ``` ### Threading model - A daemon **reader thread** polls the serial port and enqueues hex strings into `lbms.globals.message_queue`. - All decoding and UI updates happen on the tkinter thread via `after()` timers (75 ms main tick, 5 ms simulate, 1000 ms summary, 250/500 ms per tile) — the same architecture as the original WinForms event/timer design. - Unrecoverable decode errors are appended to `err.log` and the app exits (matching the VB `End` behavior). ## Differences from the VB.NET original - GUI: WinForms → tkinter; serial: `System.IO.Ports` → pyserial. - `./items/*.cfg` files are plain **JSON** instead of base64-wrapped `BinaryFormatter` blobs, so .NET-era `.cfg` files are not readable by the Python port (state starts fresh on first run). - The VB `Form1.vb` referenced a `Button2` missing from its designer file; the port implements it as the **Button2** "save now" button next to Button1. - The simulate log format `YYYY-MM-DD HH:MM:SS.mmm,` is parsed with `datetime.fromisoformat`.