2610.1 - First commit
No files matched your search
@@ -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
|
||||
@@ -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("<Double-Button-1>", self._on_double_click)
|
||||
self.bind("<Configure>", 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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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("<Button-1>", lambda e: show_history(self, self.gun))
|
||||
|
||||
self.bind("<Configure>", 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)
|
||||
@@ -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))
|
||||
@@ -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("<Configure>", 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()
|
||||
@@ -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]
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |