diff --git a/frontend/__main__.py b/frontend/__main__.py index 9f6ea09..ab3e1c1 100644 --- a/frontend/__main__.py +++ b/frontend/__main__.py @@ -10,7 +10,7 @@ from python_toolkit.serial_reader import SerialReader from python_toolkit.serial_mock import SerialMock from python_toolkit.gui.connect import ConnectFrame from .config import DEFAULT_CALIB_WEIGHT, DEFAULT_CALIB, DISPLAY_TYPES, MOV_AVG_DEFAULTS -from .views import * +from .views import CombinedView class WeightApp(tk.Tk): def __init__(self, weight_reader: SerialReader): @@ -19,7 +19,7 @@ class WeightApp(tk.Tk): self.record_start = None self.record_window = [] self.weight_reader = weight_reader - self.view = None + self.view = None self.toolbar = tk.Frame(self, padx=10) self.toolbar.pack(side=tk.LEFT) @@ -50,10 +50,18 @@ class WeightApp(tk.Tk): self.view_type = tk.Frame(self.toolbar, pady=20) self.view_type.pack() self.view_type_label = ttk.Label(self.view_type, text="Visual:") - self.view_type_label.pack(side=tk.LEFT) - self.view_type_select = ttk.Combobox(self.view_type, values=[t.value for t in DISPLAY_TYPES]) - self.view_type_select.set(DISPLAY_TYPES.NUMBER.value) - self.view_type_select.pack(side=tk.LEFT) + self.view_type_label.pack() + + # Checkboxes for multiple view selection + self.view_vars = {} + for display_type in DISPLAY_TYPES: + var = tk.BooleanVar() + if display_type == DISPLAY_TYPES.NUMBER: + var.set(True) # Default to NUMBER view + checkbox = ttk.Checkbutton(self.view_type, text=display_type.name, variable=var) + checkbox.pack() + self.view_vars[display_type] = var + self.view_type_update = ttk.Button(self.view_type, text="Refresh", command=self.update_view) self.view_type_update.pack() @@ -90,6 +98,7 @@ class WeightApp(tk.Tk): self.view_type.pack_forget() self.recording_frame.pack_forget() self.view.pack_forget() + self.connection_settings.pack() @@ -111,32 +120,30 @@ class WeightApp(tk.Tk): self.weight_reader.calib_factor = float(self.calib_weight.get()) / float(self.calib_measurements.get()) def update_view(self): - selected_view = self.view_type_select.get() - - if self.view is not None: - self.view.pack_forget() + # Get selected display types using logical AND operator + selected_types = DISPLAY_TYPES(0) # Start with empty flags + for display_type, var in self.view_vars.items(): + if var.get(): # If checkbox is checked + selected_types |= display_type # Combine using bitwise OR - - if selected_view == DISPLAY_TYPES.NUMBER.value: - self.view = NumberView(self, - tare_command=self.weight_reader.tare, - calibrate_command=self.calibrate, - padx=50) - self.view.pack(side=tk.RIGHT) - elif selected_view == DISPLAY_TYPES.CIRCLE.value: - self.view = CircleView(self, - tare_command=self.weight_reader.tare, - calibrate_command=self.calibrate, - padx=50) - self.view.pack(side=tk.RIGHT) + # Remove existing views + if self.view is not None: + self.view.update_views(selected_types) else: - raise Exception(f"View {selected_view} not found.") + self.view = CombinedView(self, + tare_command=self.weight_reader.tare, + calibrate_command=self.calibrate) + + self.update_weight_display() + def update_weight_display(self): weight = self.weight_reader.value if self.recording: self.record_window.append((time() - self.record_start, weight)) + self.view.refresh(weight) + self.after(20, self.update_weight_display) def calibrate(self): diff --git a/frontend/config.py b/frontend/config.py index 7e9e0ad..38fcb42 100644 --- a/frontend/config.py +++ b/frontend/config.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import Flag # DEFAULT_CALIB = 307333.83 DEFAULT_CALIB = -105030.71880199667 @@ -11,6 +11,6 @@ MOV_AVG_DEFAULTS = { "ignore_samples": 2 } -class DISPLAY_TYPES(Enum): - NUMBER = 'number' - CIRCLE = 'circle' \ No newline at end of file +class DISPLAY_TYPES(Flag): + NUMBER = 1 + CIRCLE = 2 \ No newline at end of file diff --git a/frontend/views/__init__.py b/frontend/views/__init__.py index b5728b9..f505c74 100644 --- a/frontend/views/__init__.py +++ b/frontend/views/__init__.py @@ -1,2 +1,3 @@ from .number import NumberView -from .circle import CircleView \ No newline at end of file +from .circle import CircleView +from .combined_view import CombinedView \ No newline at end of file diff --git a/frontend/views/base.py b/frontend/views/base.py index c1dd5e1..8b605de 100644 --- a/frontend/views/base.py +++ b/frontend/views/base.py @@ -1,32 +1,18 @@ -from tkinter import ttk, Frame, Canvas -from PIL import Image, ImageDraw -from tkinter import PhotoImage -import io +from PIL import Image +class View: -class View(Frame): - - def __init__(self, *args, tare_command=None, calibrate_command=None, **kwargs): - super().__init__(*args, **kwargs) - - self.actions = Frame(self) - self.actions.pack() - self.calibrate_button = ttk.Button(self.actions, text="Calibrate", command=calibrate_command) - self.calibrate_button.pack() - self.tare_button = ttk.Button(self.actions, text="Tare", command=tare_command) - self.tare_button.pack() - - self.size = (168, 144) - self.center = (168 // 2, 144 // 2) + def __init__(self, parent, size, center, **kwargs): + self.size = size + self.center = center self.bkg_im = self._init_im() - self._init_ui() - self.canvas = Canvas(self, width=168, height=144, background='white', - highlightthickness=1, highlightbackground="black") - self.canvas.pack() + self.ui = None + self.init_ui(parent) + + self.update_weight(0.0) - self.refresh(0.0) - def _init_ui(self): + def init_ui(self, parent): pass def _init_im(self): @@ -35,17 +21,3 @@ class View(Frame): def update_weight(self, weight: float) -> None: raise NotImplementedError() - - def refresh(self, weight: float): - # draw weight on bkg_im - im = self.update_weight(weight) - - # Convert PIL image to bytes - buffer = io.BytesIO() - im.save(buffer, format='PNG') - buffer.seek(0) - - # Load into PhotoImage and display on canvas - self.photo = PhotoImage(data=buffer.getvalue()) - self.canvas.delete("all") - self.canvas.create_image(0, 0, anchor="nw", image=self.photo) diff --git a/frontend/views/circle.py b/frontend/views/circle.py index 7117b93..5d70d83 100644 --- a/frontend/views/circle.py +++ b/frontend/views/circle.py @@ -6,12 +6,12 @@ from .base import View class CircleView(View): - def _init_ui(self): - self.target_frame = tk.Frame(self.actions) - self.target_frame.pack() - self.target_label = ttk.Label(self.target_frame, text="Target (g)") + def init_ui(self, parent): + self.ui = tk.Frame(parent) + self.ui.pack() + self.target_label = ttk.Label(self.ui, text="Target (g)") self.target_label.pack(side=tk.LEFT) - self.target = ttk.Entry(self.target_frame) + self.target = ttk.Entry(self.ui) self.target.insert(0, 100.0) self.target.pack(side=tk.LEFT) diff --git a/frontend/views/combined_view.py b/frontend/views/combined_view.py new file mode 100644 index 0000000..24184fb --- /dev/null +++ b/frontend/views/combined_view.py @@ -0,0 +1,69 @@ +import io +import tkinter as tk +from tkinter import Frame, Canvas, ttk, PhotoImage + +from PIL import Image, ImageChops + +from ..config import DISPLAY_TYPES +from . import NumberView, CircleView + +class CombinedView(tk.Frame): + def __init__(self, parent, + tare_command=None, calibrate_command=None, + **kwargs): + super().__init__(parent, **kwargs) + self.views = [] + self.tare_command = tare_command + self.calibrate_command = calibrate_command + + self.actions = Frame(self) + self.actions.pack() + self.calibrate_button = ttk.Button(self.actions, text="Calibrate", command=calibrate_command) + self.calibrate_button.pack() + self.tare_button = ttk.Button(self.actions, text="Tare", command=tare_command) + self.tare_button.pack() + + self.im_size = (168, 144) + self.center = (168 // 2, 144 // 2) + + self.canvas = Canvas(self, width=168, height=144, background='white', + highlightthickness=1, highlightbackground="black") + self.canvas.pack() + + + def update_views(self, selected_types: DISPLAY_TYPES): + for v in self.views: + if v.ui is not None: + v.ui.destroy() + self.views.clear() + + if selected_types & DISPLAY_TYPES.NUMBER: + number_view = NumberView(self.actions, self.im_size, self.center) + self.views.append(number_view) + + if selected_types & DISPLAY_TYPES.CIRCLE: + circle_view = CircleView(self.actions, self.im_size, self.center) + self.views.append(circle_view) + + + def refresh(self, weight: float): + ims = [] + for view in self.views: + im = view.update_weight(weight) + ims.append(im) + + self.canvas.delete("all") + # Combine images by logical_and + if ims: + combined_im = ims[0] + for im in ims[1:]: + combined_im = ImageChops.logical_and(combined_im, im) + + # Convert PIL image to bytes + buffer = io.BytesIO() + combined_im.save(buffer, format='PNG') + buffer.seek(0) + + # Load into PhotoImage and display on canvas + self.photo = PhotoImage(data=buffer.getvalue()) + self.canvas.create_image(0, 0, anchor="nw", image=self.photo) \ No newline at end of file diff --git a/frontend/views/number.py b/frontend/views/number.py index b97476c..e7562ca 100644 --- a/frontend/views/number.py +++ b/frontend/views/number.py @@ -1,4 +1,4 @@ -from PIL import Image, ImageDraw +from PIL import ImageDraw from .base import View