diff --git a/README.md b/README.md index c8f5b85..71fdabe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,123 @@ -# lbms_py +# LBMS -Python version of LBMS \ No newline at end of file +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`. diff --git a/items/01.cfg b/items/01.cfg new file mode 100644 index 0000000..6210603 --- /dev/null +++ b/items/01.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "01", + "gun_mac": "0013A20040A04224", + "last_seen": "2026-10-10T02:11:37.429272", + "last_count": "2026-10-10T02:07:38.122837", + "count": 0, + "fired": 1695, + "fired_overall": 7045, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:13.124190", + "change_history": [] +} \ No newline at end of file diff --git a/items/02.cfg b/items/02.cfg new file mode 100644 index 0000000..c662dac --- /dev/null +++ b/items/02.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "02", + "gun_mac": "0013A20040A0422D", + "last_seen": "2026-10-10T01:24:48.081258", + "last_count": "2026-10-10T01:18:48.377878", + "count": 0, + "fired": 430, + "fired_overall": 2440, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:33.862428", + "change_history": [] +} \ No newline at end of file diff --git a/items/03.cfg b/items/03.cfg new file mode 100644 index 0000000..86db347 --- /dev/null +++ b/items/03.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "03", + "gun_mac": "0013A200408BD898", + "last_seen": "2026-10-10T02:11:37.504905", + "last_count": "2026-10-10T02:07:45.166963", + "count": 0, + "fired": 836, + "fired_overall": 6581, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:13.199515", + "change_history": [] +} \ No newline at end of file diff --git a/items/04.cfg b/items/04.cfg new file mode 100644 index 0000000..23c1131 --- /dev/null +++ b/items/04.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "04", + "gun_mac": "0013A20040A46DD3", + "last_seen": "2026-10-10T02:11:35.544306", + "last_count": "2026-10-10T02:08:03.029250", + "count": 0, + "fired": 844, + "fired_overall": 6834, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:09.786844", + "change_history": [] +} \ No newline at end of file diff --git a/items/05.cfg b/items/05.cfg new file mode 100644 index 0000000..73db79f --- /dev/null +++ b/items/05.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "05", + "gun_mac": "0013A20040A043AE", + "last_seen": "2026-10-10T01:24:48.308025", + "last_count": "2026-10-10T01:17:18.664658", + "count": 0, + "fired": 721, + "fired_overall": 5853, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:34.316378", + "change_history": [] +} \ No newline at end of file diff --git a/items/06.cfg b/items/06.cfg new file mode 100644 index 0000000..9f1e860 --- /dev/null +++ b/items/06.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "06", + "gun_mac": "0013A20040A042AB", + "last_seen": "2026-10-10T01:24:48.383507", + "last_count": "2026-10-10T01:18:42.971792", + "count": 0, + "fired": 525, + "fired_overall": 5830, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:35.312367", + "change_history": [] +} \ No newline at end of file diff --git a/items/07.cfg b/items/07.cfg new file mode 100644 index 0000000..5884132 --- /dev/null +++ b/items/07.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "07", + "gun_mac": "0013A20040A041EF", + "last_seen": "2026-10-10T01:24:48.458915", + "last_count": "2026-10-10T01:17:13.050309", + "count": 0, + "fired": 347, + "fired_overall": 4607, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:27.637862", + "change_history": [] +} \ No newline at end of file diff --git a/items/08.cfg b/items/08.cfg new file mode 100644 index 0000000..ae45ef0 --- /dev/null +++ b/items/08.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "08", + "gun_mac": "0013A20040A041D4", + "last_seen": "2026-10-10T02:11:35.619775", + "last_count": "2026-10-10T02:07:15.804436", + "count": 0, + "fired": 853, + "fired_overall": 5408, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:10.697408", + "change_history": [] +} \ No newline at end of file diff --git a/items/09.cfg b/items/09.cfg new file mode 100644 index 0000000..78518fc --- /dev/null +++ b/items/09.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "09", + "gun_mac": "0013A20040A0430E", + "last_seen": "2026-10-10T01:24:48.612918", + "last_count": "2026-10-10T01:17:07.735412", + "count": 0, + "fired": 359, + "fired_overall": 6038, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:20.560983", + "change_history": [] +} \ No newline at end of file diff --git a/items/10.cfg b/items/10.cfg new file mode 100644 index 0000000..fd22aa8 --- /dev/null +++ b/items/10.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "10", + "gun_mac": "0013A20040A043CD", + "last_seen": "2026-10-10T02:11:35.695555", + "last_count": "2026-10-10T02:07:51.983535", + "count": 0, + "fired": 556, + "fired_overall": 4975, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:12.670014", + "change_history": [] +} \ No newline at end of file diff --git a/items/11.cfg b/items/11.cfg new file mode 100644 index 0000000..dcc23f1 --- /dev/null +++ b/items/11.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "11", + "gun_mac": "0013A20040A042D0", + "last_seen": "2026-10-10T01:24:48.688663", + "last_count": "2026-10-10T01:18:03.918810", + "count": 0, + "fired": 210, + "fired_overall": 4226, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:34.013925", + "change_history": [] +} \ No newline at end of file diff --git a/items/12.cfg b/items/12.cfg new file mode 100644 index 0000000..e662ab2 --- /dev/null +++ b/items/12.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "12", + "gun_mac": "0013A20040A46E00", + "last_seen": "2026-10-10T01:50:13.583687", + "last_count": "2026-10-10T01:48:11.343588", + "count": 0, + "fired": 573, + "fired_overall": 5392, + "battery": 30, + "battery_overall": 260, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:48:12.221256", + "change_history": [] +} \ No newline at end of file diff --git a/items/13.cfg b/items/13.cfg new file mode 100644 index 0000000..49f14a2 --- /dev/null +++ b/items/13.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "13", + "gun_mac": "0013A20040A043B1", + "last_seen": "2026-10-10T02:11:35.770825", + "last_count": "2026-10-10T02:07:18.145657", + "count": 0, + "fired": 290, + "fired_overall": 4271, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:09.942895", + "change_history": [] +} \ No newline at end of file diff --git a/items/14.cfg b/items/14.cfg new file mode 100644 index 0000000..271703e --- /dev/null +++ b/items/14.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "14", + "gun_mac": "0013A200409F0D5A", + "last_seen": "2026-10-10T02:11:35.846134", + "last_count": "2026-10-10T02:07:16.031206", + "count": 0, + "fired": 770, + "fired_overall": 4549, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:12.060924", + "change_history": [] +} \ No newline at end of file diff --git a/items/15.cfg b/items/15.cfg new file mode 100644 index 0000000..ade01e0 --- /dev/null +++ b/items/15.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "15", + "gun_mac": "0013A20040A04430", + "last_seen": "2026-10-10T01:24:48.915106", + "last_count": "2026-10-10T01:17:30.427381", + "count": 0, + "fired": 282, + "fired_overall": 4598, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:23.679576", + "change_history": [] +} \ No newline at end of file diff --git a/items/16.cfg b/items/16.cfg new file mode 100644 index 0000000..a9f45a2 --- /dev/null +++ b/items/16.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "16", + "gun_mac": "0013A20040A04394", + "last_seen": "2026-10-10T02:11:35.921441", + "last_count": "2026-10-10T02:10:19.816075", + "count": 0, + "fired": 616, + "fired_overall": 4109, + "battery": 20, + "battery_overall": 270, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:02:14.077275", + "change_history": [] +} \ No newline at end of file diff --git a/items/17.cfg b/items/17.cfg new file mode 100644 index 0000000..fb1ed69 --- /dev/null +++ b/items/17.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "17", + "gun_mac": "0013A20040A043E1", + "last_seen": "2026-10-10T01:24:49.066294", + "last_count": "2026-10-10T01:21:26.790991", + "count": 0, + "fired": 264, + "fired_overall": 4235, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:34.089464", + "change_history": [] +} \ No newline at end of file diff --git a/items/18.cfg b/items/18.cfg new file mode 100644 index 0000000..12e1863 --- /dev/null +++ b/items/18.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "18", + "gun_mac": "0013A20040A0439B", + "last_seen": "2026-10-10T02:11:35.921448", + "last_count": "2026-10-10T02:08:14.524493", + "count": 0, + "fired": 499, + "fired_overall": 6064, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:08.195438", + "change_history": [] +} \ No newline at end of file diff --git a/items/19.cfg b/items/19.cfg new file mode 100644 index 0000000..77f22e6 --- /dev/null +++ b/items/19.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "19", + "gun_mac": "0013A200409B772B", + "last_seen": "2026-10-10T01:14:54.025144", + "last_count": "2026-10-10T01:07:01.337267", + "count": 0, + "fired": 532, + "fired_overall": 2584, + "battery": 10, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:32.652887", + "change_history": [] +} \ No newline at end of file diff --git a/items/20.cfg b/items/20.cfg new file mode 100644 index 0000000..9e0850f --- /dev/null +++ b/items/20.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "20", + "gun_mac": "0013A2004093E3CB", + "last_seen": "2026-10-10T01:24:49.141706", + "last_count": "2026-10-10T01:17:08.339802", + "count": 0, + "fired": 241, + "fired_overall": 4487, + "battery": 20, + "battery_overall": 180, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:26.348596", + "change_history": [] +} \ No newline at end of file diff --git a/items/21.cfg b/items/21.cfg new file mode 100644 index 0000000..97913b4 --- /dev/null +++ b/items/21.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "21", + "gun_mac": "0013A200409B7B9A", + "last_seen": "2026-10-10T02:11:35.996654", + "last_count": "2026-10-10T02:07:04.968008", + "count": 0, + "fired": 526, + "fired_overall": 4916, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:13.274962", + "change_history": [] +} \ No newline at end of file diff --git a/items/22.cfg b/items/22.cfg new file mode 100644 index 0000000..9706658 --- /dev/null +++ b/items/22.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "22", + "gun_mac": "0013A2004093E3D5", + "last_seen": "2026-10-10T01:24:50.657532", + "last_count": "2026-10-10T01:17:02.805687", + "count": 0, + "fired": 572, + "fired_overall": 5470, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:12.221616", + "change_history": [] +} \ No newline at end of file diff --git a/items/23.cfg b/items/23.cfg new file mode 100644 index 0000000..a72ae72 --- /dev/null +++ b/items/23.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "23", + "gun_mac": "0013A20040A779AA", + "last_seen": "2026-10-10T01:24:49.292685", + "last_count": "2026-10-10T01:17:08.495889", + "count": 0, + "fired": 257, + "fired_overall": 3734, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:20.334579", + "change_history": [] +} \ No newline at end of file diff --git a/items/24.cfg b/items/24.cfg new file mode 100644 index 0000000..870580f --- /dev/null +++ b/items/24.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "24", + "gun_mac": "0013A200409C9FCA", + "last_seen": "2026-10-10T02:11:36.072618", + "last_count": "2026-10-10T02:08:39.753987", + "count": 0, + "fired": 280, + "fired_overall": 4696, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:08.875426", + "change_history": [] +} \ No newline at end of file diff --git a/items/25.cfg b/items/25.cfg new file mode 100644 index 0000000..f68d507 --- /dev/null +++ b/items/25.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "25", + "gun_mac": "0013A20040AF81C8", + "last_seen": "2026-10-10T02:11:36.148214", + "last_count": "2026-10-10T02:07:18.599152", + "count": 0, + "fired": 829, + "fired_overall": 7668, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:12.972356", + "change_history": [] +} \ No newline at end of file diff --git a/items/26.cfg b/items/26.cfg new file mode 100644 index 0000000..01d3b00 --- /dev/null +++ b/items/26.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "26", + "gun_mac": "0013A20040B1C483", + "last_seen": "2026-10-10T01:24:49.447607", + "last_count": "2026-10-10T01:17:14.341937", + "count": 0, + "fired": 116, + "fired_overall": 5745, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:13:55.741755", + "change_history": [] +} \ No newline at end of file diff --git a/items/27.cfg b/items/27.cfg new file mode 100644 index 0000000..4e246a6 --- /dev/null +++ b/items/27.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "27", + "gun_mac": "0013A20040B1C440", + "last_seen": "2026-10-10T01:24:49.523003", + "last_count": "2026-10-10T01:17:25.489164", + "count": 0, + "fired": 62, + "fired_overall": 3021, + "battery": 60, + "battery_overall": 200, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:10.861491", + "change_history": [] +} \ No newline at end of file diff --git a/items/28.cfg b/items/28.cfg new file mode 100644 index 0000000..3cb3eec --- /dev/null +++ b/items/28.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "28", + "gun_mac": "0013A200409C9FD0", + "last_seen": "2026-10-10T01:24:49.598351", + "last_count": "2026-10-10T01:17:25.564648", + "count": 0, + "fired": 0, + "fired_overall": 5900, + "battery": 10, + "battery_overall": 250, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:10:45.392448", + "change_history": [] +} \ No newline at end of file diff --git a/items/29.cfg b/items/29.cfg new file mode 100644 index 0000000..5025db7 --- /dev/null +++ b/items/29.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "29", + "gun_mac": "0013A200409C9FF8", + "last_seen": "2026-10-10T01:24:49.674085", + "last_count": "2026-10-10T01:17:59.286361", + "count": 0, + "fired": 299, + "fired_overall": 2880, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:22.848329", + "change_history": [] +} \ No newline at end of file diff --git a/items/30.cfg b/items/30.cfg new file mode 100644 index 0000000..6895bb3 --- /dev/null +++ b/items/30.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "30", + "gun_mac": "0013A200409E4383", + "last_seen": "2026-10-10T02:11:36.223777", + "last_count": "2026-10-10T02:07:32.303098", + "count": 0, + "fired": 496, + "fired_overall": 4741, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:11.909830", + "change_history": [] +} \ No newline at end of file diff --git a/items/31.cfg b/items/31.cfg new file mode 100644 index 0000000..abed8fe --- /dev/null +++ b/items/31.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "31", + "gun_mac": "0013A200409CA02A", + "last_seen": "2026-10-10T01:24:44.201071", + "last_count": "2026-10-10T01:17:08.798249", + "count": 0, + "fired": 248, + "fired_overall": 4476, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:27.259328", + "change_history": [] +} \ No newline at end of file diff --git a/items/32.cfg b/items/32.cfg new file mode 100644 index 0000000..60f6140 --- /dev/null +++ b/items/32.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "32", + "gun_mac": "0013A200409C9FEC", + "last_seen": "2026-10-10T02:11:36.299079", + "last_count": "2026-10-10T02:07:23.376882", + "count": 0, + "fired": 301, + "fired_overall": 5341, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:06.078871", + "change_history": [] +} \ No newline at end of file diff --git a/items/33.cfg b/items/33.cfg new file mode 100644 index 0000000..f559a6b --- /dev/null +++ b/items/33.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "33", + "gun_mac": "0013A200409E438B", + "last_seen": "2026-10-10T01:24:49.900542", + "last_count": "2026-10-10T01:17:25.796961", + "count": 0, + "fired": 382, + "fired_overall": 4508, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:29.314237", + "change_history": [] +} \ No newline at end of file diff --git a/items/34.cfg b/items/34.cfg new file mode 100644 index 0000000..b6b07e6 --- /dev/null +++ b/items/34.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "34", + "gun_mac": "0013A20040E5C3D1", + "last_seen": "2026-10-10T01:24:49.900551", + "last_count": "2026-10-10T01:17:20.179630", + "count": 0, + "fired": 485, + "fired_overall": 4282, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:27.183851", + "change_history": [] +} \ No newline at end of file diff --git a/items/35.cfg b/items/35.cfg new file mode 100644 index 0000000..302af1f --- /dev/null +++ b/items/35.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "35", + "gun_mac": "0013A20040F303F6", + "last_seen": "2026-10-10T02:11:36.374513", + "last_count": "2026-10-10T02:07:11.725154", + "count": 0, + "fired": 727, + "fired_overall": 4737, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:11.376279", + "change_history": [] +} \ No newline at end of file diff --git a/items/36.cfg b/items/36.cfg new file mode 100644 index 0000000..303bbbb --- /dev/null +++ b/items/36.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "36", + "gun_mac": "0013A200409E4352", + "last_seen": "2026-10-10T02:11:36.449817", + "last_count": "2026-10-10T02:07:18.825317", + "count": 0, + "fired": 45, + "fired_overall": 5064, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:07:10.772974", + "change_history": [] +} \ No newline at end of file diff --git a/items/37.cfg b/items/37.cfg new file mode 100644 index 0000000..5a2a0bd --- /dev/null +++ b/items/37.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "37", + "gun_mac": "0013A200409CA071", + "last_seen": "2026-10-10T01:24:49.976117", + "last_count": "2026-10-10T01:17:48.424176", + "count": 0, + "fired": 296, + "fired_overall": 4205, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:29.003219", + "change_history": [] +} \ No newline at end of file diff --git a/items/38.cfg b/items/38.cfg new file mode 100644 index 0000000..33adf09 --- /dev/null +++ b/items/38.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "38", + "gun_mac": "0013A20040A779BB", + "last_seen": "2026-10-10T01:24:50.052565", + "last_count": "2026-10-10T01:17:20.405936", + "count": 0, + "fired": 319, + "fired_overall": 4267, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:19.954686", + "change_history": [] +} \ No newline at end of file diff --git a/items/39.cfg b/items/39.cfg new file mode 100644 index 0000000..7215402 --- /dev/null +++ b/items/39.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "39", + "gun_mac": "0013A20040B17596", + "last_seen": "2026-10-10T01:24:50.129654", + "last_count": "2026-10-10T01:17:14.872113", + "count": 0, + "fired": 325, + "fired_overall": 3820, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:35.010575", + "change_history": [] +} \ No newline at end of file diff --git a/items/40.cfg b/items/40.cfg new file mode 100644 index 0000000..9fe7329 --- /dev/null +++ b/items/40.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "40", + "gun_mac": "0013A20040F303E1", + "last_seen": "2026-10-10T01:24:50.205004", + "last_count": "2026-10-10T01:17:09.331530", + "count": 0, + "fired": 231, + "fired_overall": 3938, + "battery": 30, + "battery_overall": 230, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:33.559915", + "change_history": [] +} \ No newline at end of file diff --git a/items/41.cfg b/items/41.cfg new file mode 100644 index 0000000..a94abc9 --- /dev/null +++ b/items/41.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "41", + "gun_mac": "0013A20040F149C4", + "last_seen": "2026-10-10T01:24:50.280508", + "last_count": "2026-10-10T01:17:09.407504", + "count": 0, + "fired": 398, + "fired_overall": 4324, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:32.955749", + "change_history": [] +} \ No newline at end of file diff --git a/items/42.cfg b/items/42.cfg new file mode 100644 index 0000000..d2d2f89 --- /dev/null +++ b/items/42.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "42", + "gun_mac": "0013A20040E5C66C", + "last_seen": "2026-10-10T01:24:50.355784", + "last_count": "2026-10-10T01:17:09.482906", + "count": 0, + "fired": 479, + "fired_overall": 5298, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:30.679673", + "change_history": [] +} \ No newline at end of file diff --git a/items/43.cfg b/items/43.cfg new file mode 100644 index 0000000..9a222f5 --- /dev/null +++ b/items/43.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "43", + "gun_mac": "0013A20040D947EE", + "last_seen": "2026-10-10T02:11:36.525050", + "last_count": "2026-10-10T02:07:18.900675", + "count": 0, + "fired": 761, + "fired_overall": 5670, + "battery": 20, + "battery_overall": 290, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T02:05:40.190825", + "change_history": [] +} \ No newline at end of file diff --git a/items/44.cfg b/items/44.cfg new file mode 100644 index 0000000..72397b6 --- /dev/null +++ b/items/44.cfg @@ -0,0 +1,21 @@ +{ + "gun_id": "44", + "gun_mac": "0013A20040B17EB2", + "last_seen": "2026-10-10T01:24:50.506582", + "last_count": "2026-10-10T01:17:09.633686", + "count": 0, + "fired": 431, + "fired_overall": 3658, + "battery": 20, + "battery_overall": 240, + "battery_last_changed": "0001-01-01T00:00:00", + "settings_battery_type": 0, + "settings_battery_changes": 0, + "settings_battery_life": 660, + "settings_fired_max": 11000, + "history_battery_changes": 0, + "history_battery_usage": 0, + "history_fired": 0, + "fired_last": "2026-10-10T01:14:21.543765", + "change_history": [] +} \ No newline at end of file diff --git a/lbms/__init__.py b/lbms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lbms/cls_com_ports.py b/lbms/cls_com_ports.py new file mode 100644 index 0000000..9035890 --- /dev/null +++ b/lbms/cls_com_ports.py @@ -0,0 +1,123 @@ +import serial +from serial.tools import list_ports + + +class ComPorts: + + com_port_exists = False + my_port_names = [] + no_com_ports_message = "No COM ports found. Please attach a COM-port device." + + def __init__(self): + self.parameter_changed = False + self.port_changed = False + self.port_open = False + self.previous_port = serial.Serial() + self.saved_bit_rate = 9600 + self.saved_handshake = "none" + self.saved_port_name = "" + self.selected_port = serial.Serial() + self.debug_mode = False + self.port_buffer = bytearray() + self.user_interface_data = None + + def _emit_ui(self, action, text, color): + if self.user_interface_data is not None: + self.user_interface_data(action, text, color) + + @property + def read_port_buffer(self): + if len(self.port_buffer) <= 0: + return b"" + data = bytes(self.port_buffer) + del self.port_buffer[:] + return data + + @property + def received_data_length(self): + return len(self.port_buffer) + + @staticmethod + def find_com_ports(): + ComPorts.my_port_names = sorted(p.device for p in list_ports.comports()) + if len(ComPorts.my_port_names) <= 0: + ComPorts.com_port_exists = False + else: + ComPorts.com_port_exists = True + return ComPorts.my_port_names + + def _display_exception(self, module_name, ex): + text = "Exception: {} Module: {}. Method: {}".format(ex, module_name, getattr(ex, "__class__").__name__) + self._emit_ui("DisplayStatus", text, "red") + + def close_com_port(self, port_to_close): + try: + self._emit_ui("SisplayStatus", "", "black") + if port_to_close is not None and port_to_close.is_open: + port_to_close.close() + self._emit_ui("DisplayCurrentSettings", "", "black") + self.port_open = False + except (OSError, serial.SerialException) as ex: + self.parameter_changed = True + self.port_changed = True + self._display_exception("ComPorts.vb", ex) + + def data_received(self): + try: + if self.selected_port.in_waiting > 0: + data = self.selected_port.read(self.selected_port.in_waiting) + self.port_buffer.extend(data) + except Exception as ex: + self._display_exception("ComPorts.vb", ex) + + @staticmethod + def error_received(event_type): + messages = { + "rxover": "Input buffer overflow", + "overrun": "Character buffer overrun", + "rxparity": "Parity error", + "frame": "Framing error", + "txfull": "Output buffer full", + } + message = messages.get(str(event_type).lower()) + if message: + print(message) + + def open_com_port(self): + flag = False + try: + if ComPorts.com_port_exists: + if self.previous_port.is_open: + self.close_com_port(self.previous_port) + if (not self.selected_port.is_open) or self.port_changed: + self.selected_port.open() + if self.selected_port.is_open: + self.selected_port.timeout = 5 + self._emit_ui("DisplayCurrentSettings", "", "black") + self._emit_ui("DisplayStatus", "", "black") + flag = True + self.port_changed = False + except (OSError, serial.SerialException) as ex: + self.parameter_changed = True + self.port_changed = True + self._display_exception("ComPorts.vb", ex) + self.port_open = flag + return flag + + def write_to_com_port(self, data_to_write): + flag = False + try: + if (self.selected_port is None or not self.selected_port.is_open) or self.port_changed: + self.port_open = self.open_com_port() + if self.selected_port.is_open: + self.selected_port.write(data_to_write) + flag = True + except serial.SerialTimeoutException as ex: + self._display_exception("ComPorts.vb", ex) + except (OSError, serial.SerialException) as ex: + self._display_exception("ComPorts.vb", ex) + if isinstance(ex, PermissionError): + self.close_com_port(self.selected_port) + self.parameter_changed = True + self._emit_ui("DisplayCurrentSettings", "", "black") + return flag diff --git a/lbms/frm_guns.py b/lbms/frm_guns.py new file mode 100644 index 0000000..4681bbc --- /dev/null +++ b/lbms/frm_guns.py @@ -0,0 +1,53 @@ +import tkinter as tk +from tkinter import messagebox + +from .gun_button import GunButton + + +class FrmGuns(tk.Toplevel): + + def __init__(self, master, main_window): + super().__init__(master) + self.main_window = main_window + self.title("LBMS") + self.configure(bg="#404040") + self.resizable(False, False) + self.geometry("1010x507") + self.gun_buttons = [] + + self.protocol("WM_DELETE_WINDOW", lambda: None) + self.bind("", self._on_double_click) + self.bind("", lambda e: self.align_icons()) + try: + self.state("zoomed") + except tk.TclError: + self.geometry("{}x{}".format(self.winfo_screenwidth(), self.winfo_screenheight())) + + def add_gun_button(self, gun): + button = GunButton(self, gun) + self.gun_buttons.append(button) + return button + + def align_icons(self): + try: + width = self.winfo_width() + height = self.winfo_height() + if width < 20 or height < 20: + return + item = 0 + for y in range(6): + for x in range(8): + if item < len(self.gun_buttons): + c_control = self.gun_buttons[item] + c_control.place( + x=width / 8 * x + 10, + y=(height - 50) / 6 * y + 10, + width=width / 8 - 20, + height=(height - 20) / 6 - 20) + item += 1 + except Exception: + pass + + def _on_double_click(self, event): + if messagebox.askyesno("Quitter?", "Are you sure you want to exit?"): + self.main_window.root.destroy() diff --git a/lbms/frm_history.py b/lbms/frm_history.py new file mode 100644 index 0000000..7465b93 --- /dev/null +++ b/lbms/frm_history.py @@ -0,0 +1,131 @@ +from datetime import datetime + +import tkinter as tk + +from . import globals as g + + +class FrmHistory(tk.Toplevel): + + FONT = ("Consolas", 12, "bold") + + def __init__(self, master): + super().__init__(master) + self.title("History") + self.my_gun = None + self._build_ui() + self.set_gun_height(549) + + def _build_ui(self): + self.lbl_title = tk.Label(self, text="", anchor="center", font=self.FONT) + self.lbl_title.place(x=13, y=9, width=395, height=23) + + self._lbl_fired = tk.Label(self, text="Fired: ", anchor="e", font=self.FONT) + self._lbl_fired.place(x=13, y=32, width=134, height=23) + self.lbl_fired = tk.Label(self, text="", anchor="w", font=self.FONT) + self.lbl_fired.place(x=155, y=32, width=253, height=23) + + self._lbl_time = tk.Label(self, text="On Time: ", anchor="e", font=self.FONT) + self._lbl_time.place(x=13, y=55, width=134, height=23) + self.lbl_time = tk.Label(self, text="", anchor="w", font=self.FONT) + self.lbl_time.place(x=155, y=55, width=253, height=23) + + self._lbl_hist = tk.Label(self, text="HISTORICAL", anchor="center", bg="black", fg="gray", font=self.FONT) + self._lbl_hist.place(x=1, y=103, width=419, height=23) + + self._lbl_fired_hist = tk.Label(self, text="Fired: ", anchor="e", font=self.FONT) + self._lbl_fired_hist.place(x=13, y=126, width=134, height=23) + self.lbl_fired_hist = tk.Label(self, text="", anchor="w", font=self.FONT) + self.lbl_fired_hist.place(x=155, y=126, width=253, height=23) + + self._lbl_time_hist = tk.Label(self, text="On Time: ", anchor="e", font=self.FONT) + self._lbl_time_hist.place(x=13, y=149, width=134, height=23) + self.lbl_time_hist = tk.Label(self, text="", anchor="w", font=self.FONT) + self.lbl_time_hist.place(x=155, y=149, width=253, height=23) + + self.btn_cancel = tk.Button(self, text="Cancel", font=self.FONT, command=self.destroy) + self.btn_cancel.place(x=333, y=58, width=75, height=42) + + self.btn_change_batteries = tk.Button(self, text="Change Batteries", font=self.FONT, + command=self._btn_change_batteries) + self.btn_change_batteries.place(x=12, y=198, width=396, height=36) + + self.txt_history = tk.Text(self, width=50, height=13, state="disabled", font=self.FONT) + self.txt_history.place(x=12, y=240, width=397, height=258) + + self.txt_set_fired = tk.Entry(self, font=self.FONT) + self.txt_set_fired.place(x=12, y=504, width=100, height=26) + self.btn_set_fired = tk.Button(self, text="SetFired", font=self.FONT, command=self._btn_set_fired) + self.btn_set_fired.place(x=118, y=504, width=113, height=26) + + self.txt_set_time = tk.Entry(self, font=self.FONT) + self.txt_set_time.place(x=12, y=536, width=100, height=26) + self.btn_set_time = tk.Button(self, text="SetTime", font=self.FONT, command=self._btn_set_time) + self.btn_set_time.place(x=118, y=536, width=113, height=26) + + def set_gun_height(self, height): + self.geometry("421x{}".format(height)) + + def set_gun(self, gun): + self.my_gun = gun + admin_widgets = (self.txt_set_fired, self.txt_set_time, self.btn_set_fired, self.btn_set_time) + positions = { + self.txt_set_fired: (12, 504, 100, 26), + self.btn_set_fired: (118, 504, 113, 26), + self.txt_set_time: (12, 536, 100, 26), + self.btn_set_time: (118, 536, 113, 26), + } + if g.admin_mode: + self.set_gun_height(605) + for w in admin_widgets: + x, y, ww, h = positions[w] + w.place(x=x, y=y, width=ww, height=h) + else: + self.set_gun_height(549) + for w in admin_widgets: + w.place_forget() + self.init() + + def init(self): + self.lbl_title.configure(text="{} - {}".format(self.my_gun.gun_id, self.my_gun.gun_mac)) + + self.lbl_fired.configure(text=str(self.my_gun.fired + self.my_gun.fired_overall)) + self.lbl_fired_hist.configure(text=str(self.my_gun.history_fired)) + + self.lbl_time.configure(text=g.minutes_to_string(self.my_gun.battery + self.my_gun.battery_overall)) + self.lbl_time_hist.configure(text=g.minutes_to_string(self.my_gun.history_battery_usage)) + + self.txt_history.configure(state="normal") + self.txt_history.delete("1.0", "end") + for item in self.my_gun.change_history: + self.txt_history.insert("end", item + "\n") + self.txt_history.configure(state="disabled") + + self.focus_set() + + def _btn_change_batteries(self): + now = datetime.now() + self.my_gun.battery_last_changed = now + self.my_gun.change_history.append( + "{},B,{},F,{}".format(now, self.my_gun.battery + self.my_gun.battery_overall, + self.my_gun.fired + self.my_gun.fired_overall)) + self.my_gun.history_battery_changes += 1 + self.my_gun.history_battery_usage = (self.my_gun.battery + self.my_gun.battery_overall + self.my_gun.history_battery_usage) + self.my_gun.history_fired += (self.my_gun.fired + self.my_gun.fired_overall) + + self.my_gun.battery = 0 + self.my_gun.battery_overall = 0 + + self.my_gun.fired = 0 + self.my_gun.fired_overall = 0 + self.my_gun.store_gun_info() + + self.destroy() + + def _btn_set_fired(self): + self.my_gun.fired = int(self.txt_set_fired.get()) + self.my_gun.store_gun_info() + + def _btn_set_time(self): + self.my_gun.battery = int(self.txt_set_time.get()) + self.my_gun.store_gun_info() diff --git a/lbms/globals.py b/lbms/globals.py new file mode 100644 index 0000000..b13b845 --- /dev/null +++ b/lbms/globals.py @@ -0,0 +1,36 @@ +import queue +import re + +gun_list = {} + +g_std_com_port = None + +g_xbee_mode = "" + +admin_mode = False +count_interval = 10 + +message_queue = queue.Queue() +message_dequeue = "" + + +def only_hex_in_string(test): + return re.fullmatch(r"\b[0-9a-fA-F]+\b", test) is not None + + +def minutes_to_string(mins): + r_str = "" + if mins >= 525600: + tmp = int(mins // 525600) + r_str += "{}y ".format(tmp) + mins -= tmp * 525600 + if mins >= 1440: + tmp = int(mins // 1440) + r_str += "{}d ".format(tmp) + mins -= tmp * 1440 + if mins >= 60: + tmp = int(mins // 60) + r_str += "{}h ".format(tmp) + mins -= tmp * 60 + r_str += "{}m".format(mins) + return r_str diff --git a/lbms/gun_button.py b/lbms/gun_button.py new file mode 100644 index 0000000..ad46b4c --- /dev/null +++ b/lbms/gun_button.py @@ -0,0 +1,137 @@ +from datetime import datetime, timedelta + +import tkinter as tk + +from . import globals as g +from .frm_history import FrmHistory +from .resources import load_image + +HISTORY_WINDOW = None + + +def show_history(master, gun): + global HISTORY_WINDOW + if HISTORY_WINDOW is None or not HISTORY_WINDOW.winfo_exists(): + HISTORY_WINDOW = FrmHistory(master) + HISTORY_WINDOW.set_gun(gun) + HISTORY_WINDOW.lift() + HISTORY_WINDOW.focus_set() + + +class GunButton(tk.Frame): + + def __init__(self, master, gun): + super().__init__(master, bd=1, relief="solid", bg="#d9d9d9") + self.gun = gun + self.change_battery = False + self.flash_state = False + + self._gun_img = load_image("Custom98.png") + self._gun_img_fired = load_image("Custom98_2.png") + self._gun_img_w = self._gun_img.width() + + self.pb_battery = tk.Label(self, image=load_image("100.png"), bd=0, bg="#d9d9d9") + self.pb_battery.image = self.pb_battery.cget("image") + self.pb_battery.place(x=3, y=3) + + self.pb_gun = tk.Label(self, image=self._gun_img, bd=0, bg="#d9d9d9") + self.pb_gun.image = self.pb_gun.cget("image") + self.pb_gun.place(x=122, y=3) + + self.lbl_main = tk.Label(self, text="lblMain", anchor="center", justify="center", bg="#d9d9d9", + font=("Consolas", 10, "bold")) + self.lbl_main.place(x=-1, y=0, relwidth=1.0, relheight=1.0) + self.lbl_main.bind("", lambda e: show_history(self, self.gun)) + + self.bind("", self._on_size_changed) + + self.after(250, self._timer_update) + self.after(500, self._timer_battery) + + def _on_size_changed(self, event=None): + self.pb_gun.place(x=self.winfo_width() - self._gun_img_w - 10, y=3) + + def _set_background(self, color): + self.configure(bg=color) + self.lbl_main.configure(bg=color) + self.pb_gun.configure(bg=color) + self.pb_battery.configure(bg=color) + + def _timer_update(self): + try: + now = datetime.now() + self.lbl_main.configure( + text="\n{}\n{}\nshot: {}\ntime: {}".format( + self.gun.gun_id, + self.gun.gun_mac, + self.gun.fired + self.gun.fired_overall, + g.minutes_to_string(self.gun.battery + self.gun.battery_overall))) + + if self.gun.last_seen > now - timedelta(seconds=1): + if self.change_battery: + self._set_background("#ff960a") + else: + self._set_background("#14ff96") + elif self.gun.last_seen > now - timedelta(seconds=30): + secs = (now - self.gun.last_seen).total_seconds() + rg_col = int(secs / 30 * 220) + 20 + self._set_background("#{:02x}{:02x}{:02x}".format(rg_col, 240, rg_col)) + else: + self._set_background("#d9d9d9") + + if self.gun.fired_last > now: + self.pb_gun.configure(image=self._gun_img_fired) + else: + self.pb_gun.configure(image=self._gun_img) + if self.gun.last_seen < now - timedelta(minutes=g.count_interval): + self.pb_gun.configure(image="") + + if self.gun.battery + self.gun.battery_overall >= self.gun.settings_battery_life: + self.change_battery = True + if self.gun.fired + self.gun.fired_overall >= self.gun.settings_fired_max: + self.change_battery = True + + if self.change_battery: + if (self.gun.battery + self.gun.battery_overall < self.gun.settings_battery_life) and \ + (self.gun.fired + self.gun.fired_overall < self.gun.settings_fired_max): + self.change_battery = False + + if not self.change_battery: + t_val = 100 - int((((self.gun.battery + self.gun.battery_overall) / self.gun.settings_battery_life) * 100)) + s_val = 100 - int((((self.gun.fired + self.gun.fired_overall) / self.gun.settings_fired_max) * 100)) + if s_val < t_val: + t_val = s_val + if t_val >= 95: + img = "100.png" + elif t_val >= 85: + img = "090.png" + elif t_val >= 75: + img = "080.png" + elif t_val >= 65: + img = "070.png" + elif t_val >= 55: + img = "060.png" + elif t_val >= 45: + img = "050.png" + elif t_val >= 35: + img = "040.png" + elif t_val >= 20: + img = "030.png" + elif t_val >= 10: + img = "020.png" + else: + img = "010.png" + self.pb_battery.configure(image=load_image(img)) + finally: + self.after(250, self._timer_update) + + def _timer_battery(self): + try: + if self.change_battery: + self.flash_state = not self.flash_state + if self.flash_state: + self.pb_battery.configure(image=load_image("000.png")) + else: + self.pb_battery.configure(image=load_image("010.png")) + finally: + self.after(500, self._timer_battery) diff --git a/lbms/gun_object.py b/lbms/gun_object.py new file mode 100644 index 0000000..4a4ee69 --- /dev/null +++ b/lbms/gun_object.py @@ -0,0 +1,98 @@ +import json +import os +from datetime import datetime, timedelta + + +def _years_ago(dt, years): + try: + return dt.replace(year=dt.year - years) + except ValueError: + return dt.replace(year=dt.year - years, month=2, day=28) + + +class GunObject: + + def __init__(self, gun_id="", gun_mac=""): + self.gun_id = gun_id + self.gun_mac = gun_mac + self.count = 0 + self.fired = 0 + self.fired_overall = 0 + self.battery = 0 + self.battery_overall = 0 + self.settings_battery_type = 0 + self.settings_battery_changes = 0 + self.settings_battery_life = 0 + self.settings_fired_max = 0 + self.history_battery_changes = 0 + self.history_battery_usage = 0 + self.history_fired = 0 + self.change_history = [] + self._init_dates() + + def _init_dates(self): + self.last_seen = _years_ago(datetime.now() - timedelta(minutes=3957), 23) + self.last_count = self.last_seen + self.battery_last_changed = datetime.min + self.fired_last = datetime.min + + def to_dict(self): + return { + "gun_id": self.gun_id, + "gun_mac": self.gun_mac, + "last_seen": self.last_seen.isoformat(), + "last_count": self.last_count.isoformat(), + "count": self.count, + "fired": self.fired, + "fired_overall": self.fired_overall, + "battery": self.battery, + "battery_overall": self.battery_overall, + "battery_last_changed": self.battery_last_changed.isoformat(), + "settings_battery_type": self.settings_battery_type, + "settings_battery_changes": self.settings_battery_changes, + "settings_battery_life": self.settings_battery_life, + "settings_fired_max": self.settings_fired_max, + "history_battery_changes": self.history_battery_changes, + "history_battery_usage": self.history_battery_usage, + "history_fired": self.history_fired, + "fired_last": self.fired_last.isoformat(), + "change_history": list(self.change_history), + } + + @classmethod + def from_dict(cls, data): + gun = cls() + gun.gun_id = data.get("gun_id", "") + gun.gun_mac = data.get("gun_mac", "") + gun.count = data.get("count", 0) + gun.fired = data.get("fired", 0) + gun.fired_overall = data.get("fired_overall", 0) + gun.battery = data.get("battery", 0) + gun.battery_overall = data.get("battery_overall", 0) + gun.settings_battery_type = data.get("settings_battery_type", 0) + gun.settings_battery_changes = data.get("settings_battery_changes", 0) + gun.settings_battery_life = data.get("settings_battery_life", 0) + gun.settings_fired_max = data.get("settings_fired_max", 0) + gun.history_battery_changes = data.get("history_battery_changes", 0) + gun.history_battery_usage = data.get("history_battery_usage", 0) + gun.history_fired = data.get("history_fired", 0) + gun.change_history = list(data.get("change_history", [])) + for name in ("last_seen", "last_count", "battery_last_changed", "fired_last"): + raw = data.get(name) + setattr(gun, name, datetime.fromisoformat(raw) if raw else datetime.min) + return gun + + def store_gun_info(self): + try: + os.makedirs("./items", exist_ok=True) + path = "./items/{}.cfg".format(self.gun_id) + with open(path, "w", encoding="utf-8") as f: + json.dump(self.to_dict(), f, indent=1) + return True + except Exception: + return False + + @classmethod + def load_gun_info(cls, path): + with open(path, "r", encoding="utf-8") as f: + return cls.from_dict(json.load(f)) diff --git a/lbms/main_window.py b/lbms/main_window.py new file mode 100644 index 0000000..c7b0ac8 --- /dev/null +++ b/lbms/main_window.py @@ -0,0 +1,422 @@ +import os +import queue +import sys +import threading +import time +from datetime import datetime, timedelta + +import serial +import serial.tools.list_ports +import tkinter as tk +from tkinter import filedialog, messagebox, ttk + +from . import globals as g +from .frm_guns import FrmGuns +from .gun_object import GunObject + + +def bytes_to_hex(data): + return "".join("{:02X}".format(b) for b in data) + + +class MainWindow: + + def __init__(self, root): + self.root = root + self.root.title("LBMS Console v1") + self.root.geometry("1408x863") + self.font = ("Consolas", 10) + + self.save_data = datetime.now() + timedelta(seconds=60) + self.valid_counter = 0 + self.limit_fire = 11000 + self.limit_battery = 660 + self.gun_objects = {} + self.serial_port = None + self.sim_list = [] + self.time_offset = 0 + self.list_index = 0 + self.simulate_enabled = False + self.show_enabled = False + self._reader_thread = None + self._reader_stop = threading.Event() + + self.button1 = tk.Button(self.root, text="Button1", font=self.font, command=self._button1_click) + self.button1.place(x=0, y=0, width=87, height=27) + self.button2 = tk.Button(self.root, text="Button2", font=self.font, command=self._button2_click) + self.button2.place(x=90, y=0, width=87, height=27) + self.txt_comm_port = ttk.Combobox(self.root, font=self.font, + values=[p.device for p in serial.tools.list_ports.comports()]) + self.txt_comm_port.set("COM4") + self.txt_comm_port.place(x=112, y=3, width=116) + self.label1 = tk.Label(self.root, text="Label1", font=self.font) + self.label1.place(x=247, y=12) + self.label2 = tk.Label(self.root, text="Label2", font=self.font) + self.label2.place(x=12, y=30) + self.btn_simulate = tk.Button(self.root, text="Simulate", font=self.font, command=self._btn_simulate_click) + self.btn_simulate.place(x=405, y=2, width=75, height=23) + self.label3 = tk.Label(self.root, text="Label3", font=self.font) + self.label3.place(x=505, y=3) + self.label4 = tk.Label(self.root, text="Label4", font=self.font) + self.label4.place(x=505, y=18) + + self.text_box2 = tk.Text(self.root, bg="black", fg="#00c000", font=self.font) + self.text_box2.place(x=14, y=48, width=477, height=784) + self.text_box3 = tk.Text(self.root, bg="black", fg="#00c000", font=("Courier", 10, "bold")) + self.text_box3.place(x=497, y=48, width=899, height=784) + + self.root.bind("", self._on_resize) + self.root.protocol("WM_DELETE_WINDOW", self._on_closing) + + self._timer_loop(75, self._timer1_tick) + self._timer_loop(5, self._tmr_simulate_tick) + self._timer_loop(1000, self._tmr_show_tick) + + self._on_load() + + def _timer_loop(self, ms, func): + def loop(): + try: + func() + finally: + self.root.after(ms, loop) + loop() + + def _on_resize(self, event): + if event.widget is not self.root: + return + width = event.width + height = event.height + self.text_box2.place(x=14, y=48, width=width // 2 - 25, height=height - 48) + self.text_box3.place(x=width // 2 + 11, y=48, width=width - width // 2 - 23, height=height - 48) + + def write_error(self, str_msg): + with open("err.log", "a", encoding="utf-8") as f: + f.write(str_msg + "\n") + + def _fatal(self, str_msg): + self.write_error(str_msg) + os._exit(1) + + def _on_load(self): + arg_com = "" + for arg in sys.argv[1:]: + if "=" not in arg: + continue + key, _, value = arg.partition("=") + key = key.lower() + if key == "admin": + if value.lower() == "true": + g.admin_mode = True + elif key == "com": + arg_com = value.strip() + elif key == "fired": + self.limit_fire = int(value.strip()) + elif key == "battery": + self.limit_battery = int(value.strip()) + + self.save_data = datetime.now() + timedelta(seconds=60) + self.serial_port = serial.Serial(baudrate=115200, bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE) + g.message_queue = queue.Queue() + g.message_dequeue = "" + self._create_id_list() + self.gun_objects = {} + self.frm_guns = FrmGuns(self.root, self) + self.frm_guns.lift() + + items_dir = "./items" + if os.path.isdir(items_dir): + for file_name in sorted(os.listdir(items_dir)): + if file_name.endswith(".cfg"): + t_go = GunObject.load_gun_info(os.path.join(items_dir, file_name)) + self.gun_objects[t_go.gun_mac] = t_go + else: + for mac, gun_id in g.gun_list.items(): + self.gun_objects[mac] = GunObject(gun_id, mac) + + for item in self.gun_objects.values(): + item.settings_battery_life = self.limit_battery + item.settings_fired_max = self.limit_fire + self.frm_guns.add_gun_button(item) + self.frm_guns.align_icons() + self.show_enabled = True + + if arg_com != "": + self._auto_start(arg_com) + self.root.iconify() + + def _create_id_list(self): + g.gun_list = { + "0013A20040A04224": "01", + "0013A20040A0422D": "02", + "0013A200408BD898": "03", + "0013A20040A46DD3": "04", + "0013A20040A043AE": "05", + "0013A20040A042AB": "06", + "0013A20040A041EF": "07", + "0013A20040A041D4": "08", + "0013A20040A0430E": "09", + "0013A20040A043CD": "10", + "0013A20040A042D0": "11", + "0013A20040A46E00": "12", + "0013A20040A043B1": "13", + "0013A200409F0D5A": "14", + "0013A20040A04430": "15", + "0013A20040A04394": "16", + "0013A20040A043E1": "17", + "0013A20040A0439B": "18", + "0013A200409B772B": "19", + "0013A2004093E3CB": "20", + "0013A200409B7B9A": "21", + "0013A2004093E3D5": "22", + "0013A20040A779AA": "23", + "0013A200409C9FCA": "24", + "0013A20040AF81C8": "25", + "0013A20040B1C483": "26", + "0013A20040B1C440": "27", + "0013A200409C9FD0": "28", + "0013A200409C9FF8": "29", + "0013A200409E4383": "30", + "0013A200409CA02A": "31", + "0013A200409C9FEC": "32", + "0013A200409E438B": "33", + "0013A20040E5C3D1": "34", + "0013A20040F303F6": "35", + "0013A200409E4352": "36", + "0013A200409CA071": "37", + "0013A20040A779BB": "38", + "0013A20040B17596": "39", + "0013A20040F303E1": "40", + "0013A20040F149C4": "41", + "0013A20040E5C66C": "42", + "0013A20040D947EE": "43", + "0013A20040B17EB2": "44", + } + + def get_gun_id(self, gun_id): + return g.gun_list.get(gun_id, gun_id) + + def _auto_start(self, com_port): + try: + self.serial_port.port = com_port + self.serial_port.open() + self._start_reader() + except Exception as ex: + messagebox.showerror("ERROR!!!", str(ex)) + + def _button1_click(self): + if self.serial_port is not None and self.serial_port.is_open: + self._close_serial() + else: + self.serial_port.port = self.txt_comm_port.get() + self.serial_port.open() + self._start_reader() + + def _close_serial(self): + self._reader_stop.set() + if self.serial_port.is_open: + self.serial_port.close() + + def _start_reader(self): + self._reader_stop.clear() + if self._reader_thread is not None and self._reader_thread.is_alive(): + return + self._reader_thread = threading.Thread(target=self._serial_reader, daemon=True) + self._reader_thread.start() + + def _serial_reader(self): + while not self._reader_stop.is_set(): + try: + if not self.serial_port.is_open: + break + bytes_to_read = self.serial_port.in_waiting + if bytes_to_read > 0: + num_array = self.serial_port.read(bytes_to_read) + g.message_queue.put(bytes_to_hex(num_array)) + except Exception as ex: + self.write_error("read: " + str(ex)) + break + time.sleep(0.005) + + def _timer1_tick(self): + now = datetime.now() + try: + if self.save_data < now: + self.save_data = now + timedelta(seconds=61) + self.save_guns() + if self.serial_port is not None and self.serial_port.is_open: + self.label1.configure(text="Open") + else: + self.label1.configure(text="Closed") + self.label2.configure(text=str(g.message_queue.qsize())) + except Exception as ex: + self._fatal("1" + str(ex)) + return + tmp_str = "" + try: + q_amount = g.message_queue.qsize() + if q_amount > 6: + count_t = int(q_amount * 0.5) + for _ in range(count_t): + tmp_str += g.message_queue.get() + elif 1 <= q_amount <= 6: + tmp_str = g.message_queue.get() + g.message_dequeue += tmp_str + except Exception as ex: + self._fatal("2" + str(ex)) + return + try: + self._dequeue_message() + for _ in range(11): + if len(g.message_dequeue) > 200: + self._dequeue_message() + except Exception as ex: + self._fatal("3" + str(ex)) + return + try: + if len(g.message_dequeue) > 16: + c_count = 0 + for x in range(6, len(g.message_dequeue) - 2, 2): + c_count += int(g.message_dequeue[x:x + 2], 16) + stats = ("Lines Read : {}\n" + "Valid Packets: {}\n" + "Queue Lenght: {}\n" + "Decode Lenght: {}\n" + "Packet Lenght: {}\n" + "\n" + "\n" + "{}").format(self.list_index, + self.valid_counter, + g.message_queue.qsize(), + len(g.message_dequeue), + len(tmp_str), + g.message_dequeue) + self.text_box2.delete("1.0", "end") + self.text_box2.insert("1.0", stats) + except Exception as ex: + self._fatal("4" + str(ex)) + + def _dequeue_message(self): + valid_message = False + try: + if len(g.message_dequeue) >= 92: + tmp_start = g.message_dequeue.find("7E002A80") + if tmp_start >= 0: + if len(g.message_dequeue) >= tmp_start + 80: + x_addr = g.message_dequeue[tmp_start + 8:tmp_start + 24] + g.message_dequeue = g.message_dequeue[tmp_start + 92:] + self.valid_counter += 1 + self._add_seen(x_addr) + valid_message = True + if len(g.message_dequeue) >= 80: + tmp_start = g.message_dequeue.find("7E002480") + if tmp_start >= 0: + if len(g.message_dequeue) >= tmp_start + 80: + x_addr = g.message_dequeue[tmp_start + 8:tmp_start + 24] + x_fire = g.message_dequeue[tmp_start + 8 + 16 + 34:tmp_start + 8 + 16 + 34 + 4] + g.message_dequeue = g.message_dequeue[tmp_start + 8 + 16 + 34 + 4 + 18:] + self.valid_counter += 1 + tmpfire1 = int(x_fire[0:2], 16) + tmpfire2 = int(x_fire[2:4], 16) + tmp_fire = (tmpfire1 * 256) + tmpfire2 + self._add_count(x_addr, tmp_fire) + self._add_seen(x_addr) + valid_message = True + if len(g.message_dequeue) >= 60: + tmp_start = g.message_dequeue.find("7E002480") + if tmp_start >= 0: + if len(g.message_dequeue) >= tmp_start + 80: + x_addr = g.message_dequeue[tmp_start + 8:tmp_start + 24] + g.message_dequeue = g.message_dequeue[tmp_start + 8 + 16 + 36:] + self.valid_counter += 1 + self._add_seen(x_addr) + valid_message = True + if not valid_message and len(g.message_dequeue) >= 200: + g.message_dequeue = g.message_dequeue[len(g.message_dequeue) // 2:] + except Exception: + pass + + def _add_seen(self, mac): + gun = self.gun_objects.get(mac) + if gun is None: + return + now = datetime.now() + if gun.last_count < now - timedelta(minutes=g.count_interval): + gun.battery += g.count_interval + gun.last_count = now + gun.last_seen = now + + def _add_count(self, mac, count): + gun = self.gun_objects.get(mac) + if gun is None: + return + if count < gun.fired: + gun.fired_overall += gun.fired + gun.battery_overall += gun.battery + gun.battery = 0 + elif count > gun.fired: + gun.fired_last = datetime.now() + timedelta(seconds=3) + gun.fired = count + + def _btn_simulate_click(self): + if self.simulate_enabled: + self.simulate_enabled = False + self.label4.configure(text=str(datetime.now())) + self.btn_simulate.configure(text="Simulate") + return + self.sim_list = [] + g.message_dequeue = "" + the_file = filedialog.askopenfilename() + if not the_file: + return + self.list_index = 0 + with open(the_file, "r", encoding="utf-8", errors="replace") as file_reader: + for line in file_reader: + self.sim_list.append(line.rstrip("\r\n")) + + first_ts = datetime.fromisoformat(self.sim_list[0].split(",")[0]) + self.time_offset = (first_ts - datetime.now()).total_seconds() + + self.label3.configure(text=str(datetime.now())) + self.label4.configure(text="") + self.btn_simulate.configure(text="Stop") + self.simulate_enabled = True + + def _tmr_simulate_tick(self): + if not self.simulate_enabled: + return + if self.list_index >= len(self.sim_list) - 1: + self.simulate_enabled = False + self.btn_simulate.configure(text="Simulate.") + self.label4.configure(text=str(datetime.now())) + return + parts = self.sim_list[self.list_index].split(",") + if datetime.now() + timedelta(seconds=self.time_offset) > datetime.fromisoformat(parts[0]): + g.message_queue.put(parts[1]) + self.list_index += 1 + + def _tmr_show_tick(self): + if not self.show_enabled: + return + parts = [] + for item in self.gun_objects.values(): + parts.append("{} {:04d}-{:04d} ({:04d})".format(item.gun_id, + item.fired, + item.fired_overall, + item.fired + item.fired_overall)) + parts.append(" T: {:03d} ({})\n".format(item.battery + item.battery_overall, item.last_seen)) + self.text_box3.delete("1.0", "end") + self.text_box3.insert("1.0", "".join(parts)) + + def _button2_click(self): + self.save_guns() + messagebox.showinfo("LBMS", "done") + + def save_guns(self): + for item in self.gun_objects.values(): + item.store_gun_info() + + def _on_closing(self): + self.save_guns() + self.root.destroy() diff --git a/lbms/resources.py b/lbms/resources.py new file mode 100644 index 0000000..e90753e --- /dev/null +++ b/lbms/resources.py @@ -0,0 +1,13 @@ +from pathlib import Path + +import tkinter as tk + +RESOURCE_DIR = Path(__file__).resolve().parent / "resources" + +_cache = {} + + +def load_image(name): + if name not in _cache: + _cache[name] = tk.PhotoImage(file=str(RESOURCE_DIR / name)) + return _cache[name] diff --git a/lbms/resources/000.png b/lbms/resources/000.png new file mode 100644 index 0000000..4c53605 Binary files /dev/null and b/lbms/resources/000.png differ diff --git a/lbms/resources/010.png b/lbms/resources/010.png new file mode 100644 index 0000000..78f5dbb Binary files /dev/null and b/lbms/resources/010.png differ diff --git a/lbms/resources/020.png b/lbms/resources/020.png new file mode 100644 index 0000000..6214f64 Binary files /dev/null and b/lbms/resources/020.png differ diff --git a/lbms/resources/030.png b/lbms/resources/030.png new file mode 100644 index 0000000..9ad9346 Binary files /dev/null and b/lbms/resources/030.png differ diff --git a/lbms/resources/040.png b/lbms/resources/040.png new file mode 100644 index 0000000..4a549e2 Binary files /dev/null and b/lbms/resources/040.png differ diff --git a/lbms/resources/050.png b/lbms/resources/050.png new file mode 100644 index 0000000..36f6993 Binary files /dev/null and b/lbms/resources/050.png differ diff --git a/lbms/resources/060.png b/lbms/resources/060.png new file mode 100644 index 0000000..6d15ee4 Binary files /dev/null and b/lbms/resources/060.png differ diff --git a/lbms/resources/070.png b/lbms/resources/070.png new file mode 100644 index 0000000..000c736 Binary files /dev/null and b/lbms/resources/070.png differ diff --git a/lbms/resources/080.png b/lbms/resources/080.png new file mode 100644 index 0000000..d2b041b Binary files /dev/null and b/lbms/resources/080.png differ diff --git a/lbms/resources/090.png b/lbms/resources/090.png new file mode 100644 index 0000000..df0f513 Binary files /dev/null and b/lbms/resources/090.png differ diff --git a/lbms/resources/100.png b/lbms/resources/100.png new file mode 100644 index 0000000..5b8daf6 Binary files /dev/null and b/lbms/resources/100.png differ diff --git a/lbms/resources/Custom98.png b/lbms/resources/Custom98.png new file mode 100644 index 0000000..e5e728a Binary files /dev/null and b/lbms/resources/Custom98.png differ diff --git a/lbms/resources/Custom98_2.png b/lbms/resources/Custom98_2.png new file mode 100644 index 0000000..032bf5f Binary files /dev/null and b/lbms/resources/Custom98_2.png differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..fbb64ed --- /dev/null +++ b/main.py @@ -0,0 +1,13 @@ +import tkinter as tk + +from lbms.main_window import MainWindow + + +def main(): + root = tk.Tk() + MainWindow(root) + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ad05ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyserial>=3.5