This commit is contained in:
2025-04-25 23:04:40 +02:00
parent c4bf2206de
commit 9a956ac116
6 changed files with 237 additions and 1 deletions

0
frontend/__init__.py Normal file
View File

120
frontend/app.py Normal file
View File

@@ -0,0 +1,120 @@
import tkinter as tk
from tkinter import ttk
import threading
from .serial_reader import WeightReader
from .config import DEFAULT_CALIB, DISPLAY_TYPES
class WeightApp(tk.Tk):
def __init__(self, weight_reader):
super().__init__()
self.weight_reader: WeightReader = weight_reader
self.toolbar = tk.Frame(self, padx=10)
self.toolbar.pack(side=tk.LEFT)
#### Connection Settings ####
self.connection_settings = tk.Frame(self.toolbar, pady=10)
self.scan_ports_button = ttk.Button(self.connection_settings, text="Scan Devices", command=self.update_devices)
self.scan_ports_button.pack()
self.port_label = ttk.Label(self.connection_settings, text="Port:")
self.port_label.pack(side=tk.LEFT)
self.port = ttk.Combobox(self.connection_settings, values=[])
self.update_devices()
self.port.pack(side=tk.LEFT)
self.connection_settings.pack()
self.connect_button = ttk.Button(self.toolbar, text="Connect", command=self.connect)
self.connect_button.pack()
#### Weight Reader Settings ####
self.reader_settings = tk.Frame(self.toolbar, pady=30)
self.reader_settings.pack()
self.calib_factor = tk.Frame(self.reader_settings)
self.calib_factor.pack()
self.calib_label = ttk.Label(self.calib_factor, text="Calibration Factor:")
self.calib_label.pack()
self.calib_weight = ttk.Combobox(self.calib_factor, values=[100], width=5)
self.calib_weight.set(100)
self.calib_weight.pack(side=tk.LEFT)
self.calib_measurements = ttk.Entry(self.calib_factor)
self.calib_measurements.insert(0, DEFAULT_CALIB)
self.calib_measurements.pack(side=tk.LEFT)
self.update_calib_button = ttk.Button(self.reader_settings, text="Update Calibration", command=self.update_calib)
self.update_calib_button.pack()
self.display_type = tk.Frame(self.toolbar, pady=20)
self.display_type.pack()
self.display_type_label = ttk.Label(self.display_type, text="Visual:")
self.display_type_label.pack(side=tk.LEFT)
self.display_type_select = ttk.Combobox(self.display_type, values=[t.value for t in DISPLAY_TYPES])
self.display_type_select.set(DISPLAY_TYPES.NUMBER.value)
self.display_type_select.pack(side=tk.LEFT)
#### Display ####
self.main_frame = tk.Frame(self, width=144, height=168, padx=50)
self.main_frame.pack(side=tk.RIGHT)
#### Actions ####
self.actions = tk.Frame(self.main_frame)
self.actions.pack()
self.tare_button = ttk.Button(self.actions, text="Tare", command=self.weight_reader.tare)
self.tare_button.pack()
self.canvas = tk.Canvas(self.main_frame, width=144, height=168, background='white')
self.canvas.pack()
self.label = self.canvas.create_text(50, 68, text="0.0 g", font=("Arial", 18), fill='black', justify='left')
self.update_weight_display()
self.focus_force()
def connect(self):
if self.weight_reader.serial is None:
port = self.port.get()
self.weight_reader.connect(port)
self.connect_button.config(text="Disconnect")
self.connection_settings.pack_forget()
else:
self.weight_reader.disconnect()
self.connect_button.config(text="Connect")
self.connect_button.pack_forget()
self.reader_settings.pack_forget()
self.actions.pack_forget()
self.connection_settings.pack()
self.connect_button.pack()
self.reader_settings.pack()
self.actions.pack()
def update_devices(self):
self.weight_reader.scan_devices()
self.port.config(values=self.weight_reader.ports)
if len(self.weight_reader.ports) > 0:
self.port.set(self.weight_reader.ports[0])
def update_calib(self):
self.weight_reader.calib_factor = self.calib_weight.get() / float(self.calib_measurements.get())
def update_weight_display(self):
weight = self.weight_reader.value
self.canvas.itemconfig(self.label, text=f"{weight:.1f} g")
self.after(100, self.update_weight_display)
def main():
weight_reader = WeightReader()
threading.Thread(target=weight_reader.read_weights, daemon=True).start()
app = WeightApp(weight_reader)
app.protocol("WM_DELETE_WINDOW", lambda: on_closing(app, weight_reader))
app.mainloop()
def on_closing(app, weight_reader):
weight_reader.stop()
app.destroy()
if __name__ == "__main__":
main()

14
frontend/config.py Normal file
View File

@@ -0,0 +1,14 @@
from enum import Enum
DEFAULT_CALIB = 307333.83
DEFAULT_CALIB_WEIGHT = 100.
MOV_AVG_DEFAULTS = {
"window_size": 10,
"decimals": 1,
"reset_threshold": 0.5,
"ignore_samples": 2
}
class DISPLAY_TYPES(Enum):
NUMBER = 'number'

90
frontend/serial_reader.py Normal file
View File

@@ -0,0 +1,90 @@
import time
from statistics import mean
from serial import Serial
from serial.tools import list_ports
from .config import DEFAULT_CALIB, DEFAULT_CALIB_WEIGHT, MOV_AVG_DEFAULTS
class WeightReader:
@property
def value(self):
return (self.current_raw_weight - self._tare) * self.calib_factor
@property
def calib_factor(self):
return self._calib_factor
@calib_factor.setter
def calib_factor(self, value):
self.calib_factor = value
self._raw_reset_threshold = self.reset_threshold / value
def __init__(self):
self.running = True
self.ports = [d.device for d in list_ports.grep('usbmodem')]
self.serial = None
self._calib_factor = DEFAULT_CALIB_WEIGHT / DEFAULT_CALIB
self.window_size = MOV_AVG_DEFAULTS['window_size']
self.reset_threshold = MOV_AVG_DEFAULTS['reset_threshold']
self._raw_reset_threshold = MOV_AVG_DEFAULTS['reset_threshold'] / self._calib_factor
self.ignore_samples = MOV_AVG_DEFAULTS['ignore_samples']
self._tare = 0.0
self.window = []
self.current_raw_weight = 0
self.ignored_samples = 0
def scan_devices(self):
self.ports = [d.device for d in list_ports.grep('usbmodem')]
def connect(self, port, baudrate=115200):
self.serial = Serial(port, baudrate)
def disconnect(self):
self.serial.close()
self.serial = None
def read_weights(self):
while self.running:
if self.serial is not None:
try:
line = self.serial.readline().decode('utf-8')
raw_weight = int(line.split(',')[1])
self.filter(raw_weight)
except:
pass
else:
time.sleep(1)
def stop(self):
self.running = False
def filter(self, raw_weight):
if len(self.window) < self.window_size:
self.window.append(raw_weight)
self.current_raw_weight = mean(self.window)
else:
out_of_threshold = abs(self.current_raw_weight - raw_weight) > self._raw_reset_threshold
if out_of_threshold and\
self.ignored_samples < self.ignore_samples:
self.ignored_samples += 1
elif out_of_threshold:
self.ignored_samples = 0
self.window = [raw_weight]
self.current_raw_weight = raw_weight
else:
self.ignored_samples = 0
self.window.append(raw_weight)
self.current_raw_weight = mean(self.window)
def tare(self):
self._tare = self.current_raw_weight