From d8d131e1c14036ec5b55c394ab8c63e5c1544284 Mon Sep 17 00:00:00 2001 From: DAProgs Date: Sun, 11 Oct 2026 20:37:14 -0400 Subject: [PATCH] 2610.1 - First commit --- README.md | 124 +++++++++- items/01.cfg | 21 ++ items/02.cfg | 21 ++ items/03.cfg | 21 ++ items/04.cfg | 21 ++ items/05.cfg | 21 ++ items/06.cfg | 21 ++ items/07.cfg | 21 ++ items/08.cfg | 21 ++ items/09.cfg | 21 ++ items/10.cfg | 21 ++ items/11.cfg | 21 ++ items/12.cfg | 21 ++ items/13.cfg | 21 ++ items/14.cfg | 21 ++ items/15.cfg | 21 ++ items/16.cfg | 21 ++ items/17.cfg | 21 ++ items/18.cfg | 21 ++ items/19.cfg | 21 ++ items/20.cfg | 21 ++ items/21.cfg | 21 ++ items/22.cfg | 21 ++ items/23.cfg | 21 ++ items/24.cfg | 21 ++ items/25.cfg | 21 ++ items/26.cfg | 21 ++ items/27.cfg | 21 ++ items/28.cfg | 21 ++ items/29.cfg | 21 ++ items/30.cfg | 21 ++ items/31.cfg | 21 ++ items/32.cfg | 21 ++ items/33.cfg | 21 ++ items/34.cfg | 21 ++ items/35.cfg | 21 ++ items/36.cfg | 21 ++ items/37.cfg | 21 ++ items/38.cfg | 21 ++ items/39.cfg | 21 ++ items/40.cfg | 21 ++ items/41.cfg | 21 ++ items/42.cfg | 21 ++ items/43.cfg | 21 ++ items/44.cfg | 21 ++ lbms/__init__.py | 0 lbms/cls_com_ports.py | 123 ++++++++++ lbms/frm_guns.py | 53 +++++ lbms/frm_history.py | 131 +++++++++++ lbms/globals.py | 36 +++ lbms/gun_button.py | 137 +++++++++++ lbms/gun_object.py | 98 ++++++++ lbms/main_window.py | 422 ++++++++++++++++++++++++++++++++++ lbms/resources.py | 13 ++ lbms/resources/000.png | Bin 0 -> 4624 bytes lbms/resources/010.png | Bin 0 -> 4797 bytes lbms/resources/020.png | Bin 0 -> 4821 bytes lbms/resources/030.png | Bin 0 -> 4746 bytes lbms/resources/040.png | Bin 0 -> 4598 bytes lbms/resources/050.png | Bin 0 -> 4645 bytes lbms/resources/060.png | Bin 0 -> 4622 bytes lbms/resources/070.png | Bin 0 -> 4818 bytes lbms/resources/080.png | Bin 0 -> 4665 bytes lbms/resources/090.png | Bin 0 -> 4810 bytes lbms/resources/100.png | Bin 0 -> 4569 bytes lbms/resources/Custom98.png | Bin 0 -> 7577 bytes lbms/resources/Custom98_2.png | Bin 0 -> 7852 bytes main.py | 13 ++ requirements.txt | 1 + 69 files changed, 2073 insertions(+), 2 deletions(-) create mode 100644 items/01.cfg create mode 100644 items/02.cfg create mode 100644 items/03.cfg create mode 100644 items/04.cfg create mode 100644 items/05.cfg create mode 100644 items/06.cfg create mode 100644 items/07.cfg create mode 100644 items/08.cfg create mode 100644 items/09.cfg create mode 100644 items/10.cfg create mode 100644 items/11.cfg create mode 100644 items/12.cfg create mode 100644 items/13.cfg create mode 100644 items/14.cfg create mode 100644 items/15.cfg create mode 100644 items/16.cfg create mode 100644 items/17.cfg create mode 100644 items/18.cfg create mode 100644 items/19.cfg create mode 100644 items/20.cfg create mode 100644 items/21.cfg create mode 100644 items/22.cfg create mode 100644 items/23.cfg create mode 100644 items/24.cfg create mode 100644 items/25.cfg create mode 100644 items/26.cfg create mode 100644 items/27.cfg create mode 100644 items/28.cfg create mode 100644 items/29.cfg create mode 100644 items/30.cfg create mode 100644 items/31.cfg create mode 100644 items/32.cfg create mode 100644 items/33.cfg create mode 100644 items/34.cfg create mode 100644 items/35.cfg create mode 100644 items/36.cfg create mode 100644 items/37.cfg create mode 100644 items/38.cfg create mode 100644 items/39.cfg create mode 100644 items/40.cfg create mode 100644 items/41.cfg create mode 100644 items/42.cfg create mode 100644 items/43.cfg create mode 100644 items/44.cfg create mode 100644 lbms/__init__.py create mode 100644 lbms/cls_com_ports.py create mode 100644 lbms/frm_guns.py create mode 100644 lbms/frm_history.py create mode 100644 lbms/globals.py create mode 100644 lbms/gun_button.py create mode 100644 lbms/gun_object.py create mode 100644 lbms/main_window.py create mode 100644 lbms/resources.py create mode 100644 lbms/resources/000.png create mode 100644 lbms/resources/010.png create mode 100644 lbms/resources/020.png create mode 100644 lbms/resources/030.png create mode 100644 lbms/resources/040.png create mode 100644 lbms/resources/050.png create mode 100644 lbms/resources/060.png create mode 100644 lbms/resources/070.png create mode 100644 lbms/resources/080.png create mode 100644 lbms/resources/090.png create mode 100644 lbms/resources/100.png create mode 100644 lbms/resources/Custom98.png create mode 100644 lbms/resources/Custom98_2.png create mode 100644 main.py create mode 100644 requirements.txt 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 0000000000000000000000000000000000000000..4c53605dfa4c02bc2c396cff348ba2089a956a6e GIT binary patch literal 4624 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 literal 0 HcmV?d00001 diff --git a/lbms/resources/010.png b/lbms/resources/010.png new file mode 100644 index 0000000000000000000000000000000000000000..78f5dbb5ebdb0864b7e199909abc594bf41fd696 GIT binary patch literal 4797 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 literal 0 HcmV?d00001 diff --git a/lbms/resources/030.png b/lbms/resources/030.png new file mode 100644 index 0000000000000000000000000000000000000000..9ad934602cf5690c434973aee323eb8dff98b265 GIT binary patch literal 4746 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 Y0~s7!LVYz&WdHyG07*qoM6N<$f^Dq{v;Y7A literal 0 HcmV?d00001 diff --git a/lbms/resources/040.png b/lbms/resources/040.png new file mode 100644 index 0000000000000000000000000000000000000000..4a549e2176060e08bfdedf96f4fd72980ea506ae GIT binary patch literal 4598 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 literal 0 HcmV?d00001 diff --git a/lbms/resources/050.png b/lbms/resources/050.png new file mode 100644 index 0000000000000000000000000000000000000000..36f6993a7da5922766f8355b97ce015ed9ad77d8 GIT binary patch literal 4645 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 literal 0 HcmV?d00001 diff --git a/lbms/resources/080.png b/lbms/resources/080.png new file mode 100644 index 0000000000000000000000000000000000000000..d2b041bce0bfb95a08a4ff471638e0057e5e6f15 GIT binary patch literal 4665 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 literal 0 HcmV?d00001 diff --git a/lbms/resources/Custom98_2.png b/lbms/resources/Custom98_2.png new file mode 100644 index 0000000000000000000000000000000000000000..032bf5f666b5a7334a58ca623d3e710bce329592 GIT binary patch literal 7852 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

E^ssKnfui?TdpEAD0Bn7=u!3(XopW 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 KMNUMnLSTZ{3_+9t literal 0 HcmV?d00001 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