From ecc1ab1337aa281982622b640fdedb6d1c98bf57 Mon Sep 17 00:00:00 2001 From: Jurn Wubben Date: Mon, 25 Aug 2025 15:23:04 +0200 Subject: [PATCH 1/2] created basic tkinter gui --- app.py | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 2 +- netbrite.py | 28 ++++++++-- 3 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 app.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..2fe4874 --- /dev/null +++ b/app.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +from typing import Any, cast +import tkinter as tk +from tkinter import ttk, messagebox, simpledialog +from typing import final +from netbrite import ( + NetBrite, + NetbriteConnectionException, + NetbriteTransferException, + Zone, + Message, +) + +WIDTH = 300 +DEFAULT_ZONE = Zone(0, 0, 150, 7) +DEFAULT_ZONE_NAME = "default" + + +@final +class SignControlGUI(tk.Tk): + def __init__(self): + super().__init__() + self.title("SignControl") + self.geometry(f"{WIDTH}x350") + self.resizable(False, True) + + self.devices: list[NetBrite] = [] + + self._build_ui() + + def _build_ui(self): + frm_devices = ttk.LabelFrame(self, text="Devices") + frm_devices.pack(fill="both", expand=True, padx=5, pady=5) + + self.lst_devices = tk.Listbox(frm_devices, height=6) + self.lst_devices.pack(fill="both", expand=True, padx=5, pady=5) + + btn_frm = ttk.Frame(frm_devices) + btn_frm.pack(fill="both", padx=5, pady=5) + ttk.Button(btn_frm, text="Add", command=self._add_device).pack(side="left") + ttk.Button(btn_frm, text="Remove", command=self._remove_device).pack( + side="left" + ) + + frm_msg = ttk.LabelFrame(self, text="Message") + frm_msg.pack(fill="both", expand=False, padx=5, pady=5) + self.txt = tk.Text(frm_msg, height=1, wrap="word") + self.txt.pack(fill="both", expand=False, padx=5, pady=5) + + hint = ( + "Templates: {scrollon} {scrolloff} {blinkon} {blinkoff} " + "{left} {center} {right} {erase} {serialnum} {bell} " + "{red} {green} {yellow}" + ) + ttk.Label( + self, + text=hint, + font=("Segoe UI", 8), + foreground="gray", + wraplength=WIDTH - 20, + justify="center", + ).pack( + padx=5, + pady=(0, 5), + ) + + btm_btn_frm = ttk.Frame(self) + btm_btn_frm.pack(fill="y", padx=5, pady=5) + ttk.Button(btm_btn_frm, text="Send", command=self._send_message).pack( + side="left" + ) + ttk.Button(btm_btn_frm, text="Reset", command=self._reset_selected_zone).pack( + side="left" + ) + ttk.Button(btm_btn_frm, text="Quit", command=self.quit).pack(side="right") + + _ = self.bind("", lambda e: self._send_message()) + + def _get_selection(self) -> tuple[int, NetBrite] | None: + sel = cast( + tuple[int], + self.lst_devices.curselection(), # pyright: ignore[reportUnknownMemberType] + ) + + if not sel: + _ = messagebox.showwarning("No device", "Please select a device first.") + return None + + index = sel[0] + + return (index, self.devices[index]) + + def _add_device(self): + ip = simpledialog.askstring("Add device", "IP address:") + if not ip: + return + self._add_device_ip(ip.strip()) + + def _add_device_ip(self, ip: str): + try: + device = NetBrite(ip) + + length = len(self.devices) + self._reset_zone(device, length) + + self.devices.append(device) + self.lst_devices.insert("end", f"{ip}") + self.lst_devices.selection_set(length) + except NetbriteConnectionException as exc: + _ = messagebox.showerror("Connection error", str(exc)) + + def _remove_device(self): + sel = self._get_selection() + if sel is None: + return + + idx = int(sel[0]) + _ = self.devices.pop(idx) + self.lst_devices.delete(idx) + + def _send_message(self): + sel = self._get_selection() + if sel is None: + return + + print(sel) + + _, dev = sel + raw_text = self.txt.get("1.0", "end-1c").strip() + if not raw_text: + return + + msg = Message("{erase}" + raw_text) + try: + dev.message(msg, DEFAULT_ZONE_NAME) + self.txt.delete("1.0", "end") + except NetbriteTransferException as exc: + _ = messagebox.showerror("Send failed", str(exc)) + + def _reset_selected_zone(self): + sel = self._get_selection() + if sel is None: + return + + index, dev = sel + self._reset_zone(dev, index) + + def _reset_zone(self, device: NetBrite, index: int): + zone = DEFAULT_ZONE + message = Message("{erase}Nr. " + str(index)) + + device.zones({DEFAULT_ZONE_NAME: zone}) + device.message(message, DEFAULT_ZONE_NAME) + + +if __name__ == "__main__": + SignControlGUI().mainloop() diff --git a/flake.nix b/flake.nix index 53bf9e0..5739b33 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,7 @@ nativeBuildInputs = [ pkgs.entr pkgs.fastapi-cli - (pkgs.python3.withPackages (x: [x.crc x.fastapi])) + (pkgs.python3.withPackages (x: [x.crc x.fastapi x.sqlmodel x.sqlalchemy x.tkinter])) ]; }; }; diff --git a/netbrite.py b/netbrite.py index ba97efb..1a39dbb 100644 --- a/netbrite.py +++ b/netbrite.py @@ -10,6 +10,14 @@ import re DEFAULT_PORT = 700 +class NetbriteConnectionException(Exception): + pass + + +class NetbriteTransferException(Exception): + pass + + class Colors(Enum): RED = 0x01 GREEN = 0x02 @@ -101,7 +109,7 @@ class Message: (rb"\{right\}", b"\x10\x28"), (rb"\{pause\}", b"\x10\x05"), (rb"\{erase\}", b"\x10\x03"), - (rb"\{serial\}", b"\x10\x09"), + (rb"\{serialnum\}", b"\x10\x09"), (rb"\{bell\}", b"\x10\x05"), (rb"\{red\}", b"\x10\x0c" + pack("B", Colors.RED.value)), (rb"\{green\}", b"\x10\x0c" + pack("B", Colors.GREEN.value)), @@ -183,16 +191,26 @@ class NetBrite: try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.settimeout(5000) + self.sock.settimeout(2) self.connect() except OSError as e: - raise ConnectionError(f"Error while opening network socket. {e}") + raise NetbriteConnectionException( + f"Error while opening network socket. {e}" + ) def connect(self): - self.sock.connect((self.address, self.port)) + try: + self.sock.connect((self.address, self.port)) + except OSError as e: + raise NetbriteConnectionException( + f"Error while opening network socket. {e}" + ) def tx(self, pkt: bytes): - _ = self.sock.send(pkt) + try: + _ = self.sock.send(pkt) + except OSError as e: + raise NetbriteTransferException(f"Error while opening network socket. {e}") def message(self, msg: Message, zoneName: str): z = self.zones_list.get(zoneName) From 3010def921df5862de1d73705bab62fc6395db80 Mon Sep 17 00:00:00 2001 From: Jurn Wubben Date: Mon, 25 Aug 2025 15:51:01 +0200 Subject: [PATCH 2/2] completed gui --- app.py | 73 ++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/app.py b/app.py index 2fe4874..8f99a3b 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -from typing import Any, cast import tkinter as tk from tkinter import ttk, messagebox, simpledialog from typing import final @@ -33,8 +32,10 @@ class SignControlGUI(tk.Tk): frm_devices = ttk.LabelFrame(self, text="Devices") frm_devices.pack(fill="both", expand=True, padx=5, pady=5) - self.lst_devices = tk.Listbox(frm_devices, height=6) - self.lst_devices.pack(fill="both", expand=True, padx=5, pady=5) + self.radios_frame = ttk.Frame(frm_devices) + self.radios_frame.pack(fill="both", expand=True, padx=5, pady=5) + + self.selected_index = tk.IntVar(value=-1) btn_frm = ttk.Frame(frm_devices) btn_frm.pack(fill="both", padx=5, pady=5) @@ -78,18 +79,11 @@ class SignControlGUI(tk.Tk): _ = self.bind("", lambda e: self._send_message()) def _get_selection(self) -> tuple[int, NetBrite] | None: - sel = cast( - tuple[int], - self.lst_devices.curselection(), # pyright: ignore[reportUnknownMemberType] - ) - - if not sel: + idx = self.selected_index.get() + if idx == -1: _ = messagebox.showwarning("No device", "Please select a device first.") return None - - index = sel[0] - - return (index, self.devices[index]) + return idx, self.devices[idx] def _add_device(self): ip = simpledialog.askstring("Add device", "IP address:") @@ -100,13 +94,21 @@ class SignControlGUI(tk.Tk): def _add_device_ip(self, ip: str): try: device = NetBrite(ip) - length = len(self.devices) - self._reset_zone(device, length) + self._reset_zone(device) self.devices.append(device) - self.lst_devices.insert("end", f"{ip}") - self.lst_devices.selection_set(length) + + radio = ttk.Radiobutton( + self.radios_frame, + text=ip, + value=length, + variable=self.selected_index, + ) + radio.pack(anchor="w") + + self.selected_index.set(length) + except NetbriteConnectionException as exc: _ = messagebox.showerror("Connection error", str(exc)) @@ -114,18 +116,26 @@ class SignControlGUI(tk.Tk): sel = self._get_selection() if sel is None: return + idx, _ = sel - idx = int(sel[0]) _ = self.devices.pop(idx) - self.lst_devices.delete(idx) + + radio = self.radios_frame.winfo_children()[idx] + radio.destroy() + + for new_idx, widget in enumerate(self.radios_frame.winfo_children()): + if isinstance(widget, ttk.Radiobutton): + _ = widget.configure(value=new_idx) + + self.selected_index.set( + -1 if not self.devices else min(idx, len(self.devices) - 1) + ) def _send_message(self): sel = self._get_selection() if sel is None: return - print(sel) - _, dev = sel raw_text = self.txt.get("1.0", "end-1c").strip() if not raw_text: @@ -136,22 +146,29 @@ class SignControlGUI(tk.Tk): dev.message(msg, DEFAULT_ZONE_NAME) self.txt.delete("1.0", "end") except NetbriteTransferException as exc: - _ = messagebox.showerror("Send failed", str(exc)) + _ = messagebox.showerror( + "Send failed, try removing and adding the device.", str(exc) + ) def _reset_selected_zone(self): sel = self._get_selection() if sel is None: return - index, dev = sel - self._reset_zone(dev, index) + _, dev = sel + self._reset_zone(dev) - def _reset_zone(self, device: NetBrite, index: int): + def _reset_zone(self, device: NetBrite): zone = DEFAULT_ZONE - message = Message("{erase}Nr. " + str(index)) + message = Message("hello") - device.zones({DEFAULT_ZONE_NAME: zone}) - device.message(message, DEFAULT_ZONE_NAME) + try: + device.zones({DEFAULT_ZONE_NAME: zone}) + device.message(message, DEFAULT_ZONE_NAME) + except NetbriteTransferException as exc: + _ = messagebox.showerror( + "Send failed, try removing and adding the device.", str(exc) + ) if __name__ == "__main__":