GUI: Optimize performance, Layout, Add "EXIT" tab

This commit is contained in:
jonny 2025-01-30 17:15:36 +01:00
parent bb84205531
commit 7d83e354fa
5 changed files with 191 additions and 74 deletions

View File

@ -1,7 +1,9 @@
import os import os
import sys import sys
import time
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from tkinter import messagebox
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
# Add the parent directory to the module search path # Add the parent directory to the module search path
@ -10,8 +12,12 @@ from interface_board_libs.adc_mcp3208 import MCP3208
from interface_board_libs.shift_register import ShiftRegister from interface_board_libs.shift_register import ShiftRegister
from interface_board_pins import * # Import pin assignments from interface_board_pins import * # Import pin assignments
from tab_control import create_control_tab from tab_control import create_control_tab
from tab_control import set_updating_enabled as tab_control__set_updating_enabled
from tab_adc_plot import create_adc_plot_tab from tab_adc_plot import create_adc_plot_tab
from tab_adc_plot import set_updating_enabled as tab_analog_plot__set_updating_enabled
from tab_digital_plot import create_digital_plot_tab from tab_digital_plot import create_digital_plot_tab
from tab_digital_plot import set_updating_enabled as tab_digital_plot__set_updating_enabled
from tab_exit import create_exit_tab
# Initialize ADC & Shift Register # Initialize ADC & Shift Register
adc = MCP3208() adc = MCP3208()
@ -38,10 +44,33 @@ root.configure(bg="black")
notebook = ttk.Notebook(root) notebook = ttk.Notebook(root)
notebook.pack(expand=True, fill="both") notebook.pack(expand=True, fill="both")
# Track active tab
def on_tab_change(event):
active_tab = event.widget.tab(event.widget.index("current"), "text")
print (f"INFO: switched to tab {active_tab}")
match active_tab:
case "ADC Plot":
tab_control__set_updating_enabled(False)
tab_analog_plot__set_updating_enabled(True)
tab_digital_plot__set_updating_enabled(False)
case "Digital Inputs":
tab_control__set_updating_enabled(False)
tab_analog_plot__set_updating_enabled(False)
tab_digital_plot__set_updating_enabled(True)
case "Controls":
tab_control__set_updating_enabled(True)
tab_analog_plot__set_updating_enabled(False)
tab_digital_plot__set_updating_enabled(False)
case _:
print(f"unhandled change to tab {active_tab}")
notebook.bind("<<NotebookTabChanged>>", on_tab_change)
# Add tabs # Add tabs
create_control_tab(notebook, adc, shift_reg, pwm1, pwm2) create_control_tab(notebook, adc, shift_reg, pwm1, pwm2)
create_adc_plot_tab(notebook, adc) create_adc_plot_tab(notebook, adc)
create_digital_plot_tab(notebook) create_digital_plot_tab(notebook)
create_exit_tab(notebook, root, pwm1, pwm2)
# Run GUI # Run GUI
root.mainloop() root.mainloop()

View File

@ -4,7 +4,14 @@ from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import time import time
ADC_PLOT_UPDATE_INTERVAL = 100 ADC_PLOT_UPDATE_INTERVAL = 50 # 20 FPS, smooth and efficient
NOT_ACTIVE_CHECK_INTERVAL = 2000
MAX_HISTORY = 100
updating_enabled = False
adc_channels = list(range(8))
data = {ch: [0] * MAX_HISTORY for ch in adc_channels} # Preallocate lists
time_data = list(range(-MAX_HISTORY, 0)) # Simulated time axis
def create_adc_plot_tab(notebook, adc): def create_adc_plot_tab(notebook, adc):
frame = ttk.Frame(notebook) frame = ttk.Frame(notebook)
@ -12,41 +19,34 @@ def create_adc_plot_tab(notebook, adc):
figure = Figure(figsize=(8, 5), dpi=100) figure = Figure(figsize=(8, 5), dpi=100)
ax = figure.add_subplot(1, 1, 1) ax = figure.add_subplot(1, 1, 1)
ax.set_title("ADC Readings Over Time")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Voltage (V)")
ax.set_ylim(0, 12) ax.set_ylim(0, 12)
ax.set_xlim(-MAX_HISTORY, 0) # Keep time axis fixed
canvas = FigureCanvasTkAgg(figure, master=frame) canvas = FigureCanvasTkAgg(figure, master=frame)
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
adc_channels = list(range(8)) # Initialize lines for fast updates
data = {ch: [] for ch in adc_channels} lines = {ch: ax.plot(time_data, data[ch], label=f"ADC {ch+1}")[0] for ch in adc_channels}
time_data = []
def update_plot(): def update_plot():
current_time = time.time() if not updating_enabled:
if len(time_data) > 50: frame.after(NOT_ACTIVE_CHECK_INTERVAL, update_plot)
for ch in adc_channels: return
data[ch].pop(0)
time_data.pop(0)
time_data.append(current_time) # Shift existing data left
for ch in adc_channels: for ch in adc_channels:
voltage = round(adc.read(ch) * 12 / 4095, 2) data[ch].pop(0)
data[ch].append(voltage) data[ch].append(round(adc.read(ch) * 12 / 4095, 2))
ax.clear()
ax.set_title("ADC Readings Over Time")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Voltage (V)")
ax.set_ylim(0, 12)
# Update only the y-data for efficiency
for ch in adc_channels: for ch in adc_channels:
ax.plot(time_data, data[ch], label=f"ADC {ch+1}") lines[ch].set_ydata(data[ch])
ax.legend(loc="upper right") canvas.draw_idle() # Efficient redraw
canvas.draw()
frame.after(ADC_PLOT_UPDATE_INTERVAL, update_plot) frame.after(ADC_PLOT_UPDATE_INTERVAL, update_plot)
update_plot() update_plot()
def set_updating_enabled(is_active):
global updating_enabled
updating_enabled = is_active
print(f"adc_plot tab: set updating_enabled to {updating_enabled}")

View File

@ -8,6 +8,12 @@ import RPi.GPIO as GPIO
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from interface_board_pins import * # Import pin assignments from interface_board_pins import * # Import pin assignments
updating_enabled = False
ADC_VALUES_UPDATE_INTERVAL = 500
DIGITAL_INPUTS_UPDATE_INTERVAL = 200
NOT_ACTIVE_CHECK_INTERVAL = 2000
def create_control_tab(notebook, adc, shift_reg, pwm1, pwm2): def create_control_tab(notebook, adc, shift_reg, pwm1, pwm2):
frame = ttk.Frame(notebook) frame = ttk.Frame(notebook)
notebook.add(frame, text="Controls") notebook.add(frame, text="Controls")
@ -16,47 +22,101 @@ def create_control_tab(notebook, adc, shift_reg, pwm1, pwm2):
digital_output_states = [tk.BooleanVar(value=False) for _ in range(8)] digital_output_states = [tk.BooleanVar(value=False) for _ in range(8)]
adc_values = [tk.StringVar(value="0.00V") for _ in range(8)] adc_values = [tk.StringVar(value="0.00V") for _ in range(8)]
pwm_values = [tk.IntVar(value=0), tk.IntVar(value=0)] pwm_values = [tk.IntVar(value=0), tk.IntVar(value=0)]
output_buttons = {} # Store button references to change colors
def update_inputs(): def update_inputs():
if not updating_enabled:
frame.after(NOT_ACTIVE_CHECK_INTERVAL, update_inputs)
return
for i, pin in enumerate(GPIO_DIGITAL_INPUTS.values()): for i, pin in enumerate(GPIO_DIGITAL_INPUTS.values()):
digital_input_states[i].set("HIGH" if GPIO.input(pin) else "LOW") digital_input_states[i].set("HIGH" if GPIO.input(pin) else "LOW")
frame.after(500, update_inputs) frame.after(DIGITAL_INPUTS_UPDATE_INTERVAL, update_inputs)
def update_adc(): def update_adc():
if not updating_enabled:
frame.after(NOT_ACTIVE_CHECK_INTERVAL, update_adc)
return
for i, adc_channel in enumerate(ADC_CHANNELS.values()): for i, adc_channel in enumerate(ADC_CHANNELS.values()):
value = adc.read(adc_channel) value = adc.read(adc_channel)
adc_values[i].set(f"{round(value * 12 / 4095, 2)}V") adc_values[i].set(f"{round(value * 12 / 4095, 2)}V")
frame.after(1000, update_adc) frame.after(ADC_VALUES_UPDATE_INTERVAL, update_adc)
def toggle_output(index): def toggle_output(index):
shift_reg.set_pin(index, digital_output_states[index].get()) """ Toggle shift register output and update button color. """
new_state = not digital_output_states[index].get()
digital_output_states[index].set(new_state)
shift_reg.set_pin(index, new_state)
def update_pwm(channel, value): if index in output_buttons:
duty_cycle = int(float(value)) for btn in output_buttons[index]:
if channel == 0: btn.configure(bg="green" if new_state else "red", activebackground="green" if new_state else "red")
pwm1.ChangeDutyCycle(duty_cycle)
else:
pwm2.ChangeDutyCycle(duty_cycle)
# UI Layout
style = ttk.Style() style = ttk.Style()
style.configure("TScale", thickness=30) # Increases slider thickness style.configure("TScale", thickness=60)
style.configure("TButton", font=("Arial", 18), padding=5)
control_frame = ttk.Frame(frame, padding=30) control_frame = ttk.Frame(frame, padding=40)
control_frame.pack(expand=True, fill="both") control_frame.grid(row=0, column=0, sticky="nsew")
frame.grid_rowconfigure(0, weight=1)
frame.grid_rowconfigure(1, weight=0)
frame.grid_columnconfigure(0, weight=1)
for i in range(8): for i in range(8):
ttk.Label(control_frame, text=f"ADC {i+1}:", font=("Arial", 14)).grid(row=i, column=0, sticky="e") ttk.Label(control_frame, text=f"ADC {i}:", font=("Arial", 18)).grid(row=i, column=0, sticky="e")
ttk.Label(control_frame, textvariable=adc_values[i], width=10, font=("Arial", 14)).grid(row=i, column=1, sticky="w") ttk.Label(control_frame, textvariable=adc_values[i], width=10, font=("Arial", 18)).grid(row=i, column=1, sticky="w")
ttk.Label(control_frame, text=f"IN {i+1}:", font=("Arial", 14)).grid(row=i, column=2, sticky="e") ttk.Label(control_frame, text=f"IN {i}:", font=("Arial", 18)).grid(row=i, column=2, sticky="e", padx=20)
ttk.Label(control_frame, textvariable=digital_input_states[i], width=6, font=("Arial", 14)).grid(row=i, column=3, sticky="w") ttk.Label(control_frame, textvariable=digital_input_states[i], width=6, font=("Arial", 18)).grid(row=i, column=3, sticky="w")
btn = ttk.Checkbutton(control_frame, text=f"OUT {i+1}", variable=digital_output_states[i], command=lambda i=i: toggle_output(i))
btn.grid(row=i, column=4, sticky="w")
for i in range(2): btn = tk.Button(control_frame, text=f"OUT {i}", font=("Arial", 16), width=10, bg="red", activebackground="red", command=lambda i=i: toggle_output(i))
ttk.Label(control_frame, text=f"PWM{i+1}:", font=("Arial", 14)).grid(row=i, column=5, sticky="e") btn.grid(row=i, column=4, sticky="w", padx=20)
slider = ttk.Scale(control_frame, from_=0, to=100, orient="horizontal", length=400, variable=pwm_values[i], command=lambda val, i=i: update_pwm(i, val), style="TScale")
slider.grid(row=i, column=6, sticky="w", pady=10) # Added spacing with `pady=10` output_buttons.setdefault(i, []).append(btn)
if i == 0:
buzzer_btn = tk.Button(control_frame, text="BUZZER", font=("Arial", 16), width=10, bg="red", activebackground="red", command=lambda: toggle_output(0))
buzzer_btn.grid(row=i, column=5, padx=20)
output_buttons[0].append(buzzer_btn)
if i == 1:
relay2_btn = tk.Button(control_frame, text="RELAY2", font=("Arial", 16), width=10, bg="red", activebackground="red", command=lambda: toggle_output(1))
relay2_btn.grid(row=i, column=5, padx=20)
output_buttons[1].append(relay2_btn)
if i == 2:
relay1_btn = tk.Button(control_frame, text="RELAY1", font=("Arial", 16), width=10, bg="red", activebackground="red", command=lambda: toggle_output(2))
relay1_btn.grid(row=i, column=5, padx=20)
output_buttons[2].append(relay1_btn)
pwm_frame = ttk.Frame(frame, padding=20)
pwm_frame.grid(row=1, column=0, sticky="ew")
pwm_frame.columnconfigure(0, weight=0)
pwm_frame.columnconfigure(1, weight=1)
pwm_frame.columnconfigure(2, weight=0)
pwm1_label = ttk.Label(pwm_frame, text="PWM1:", font=("Arial", 18))
pwm1_label.grid(row=0, column=0, padx=20, sticky="e")
pwm1_slider = ttk.Scale(
pwm_frame, from_=0, to=100, orient="horizontal", length=400,
variable=pwm_values[0], command=lambda val: pwm1.ChangeDutyCycle(int(float(val)))
)
pwm1_slider.grid(row=0, column=1, columnspan=2, sticky="we", padx=10)
pwm2_label = ttk.Label(pwm_frame, text="PWM2:", font=("Arial", 18))
pwm2_label.grid(row=1, column=0, padx=20, sticky="e")
pwm2_slider = ttk.Scale(
pwm_frame, from_=0, to=100, orient="horizontal", length=400,
variable=pwm_values[1], command=lambda val: pwm2.ChangeDutyCycle(int(float(val)))
)
pwm2_slider.grid(row=1, column=1, columnspan=2, sticky="we", padx=10)
update_inputs() update_inputs()
update_adc() update_adc()
def set_updating_enabled(is_active):
global updating_enabled
updating_enabled = is_active
print(f"control tab: set updating_enabled to {updating_enabled}")

View File

@ -1,5 +1,6 @@
import sys import sys
import os import os
import threading
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
@ -10,47 +11,53 @@ import time
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from interface_board_pins import * # Import pin assignments from interface_board_pins import * # Import pin assignments
# Adjusted Constants
DIGITAL_PLOT_UPDATE_INTERVAL = 50 # 50ms (20 FPS) is smooth enough
NOT_ACTIVE_CHECK_INTERVAL = 2000 # Check inactive tabs less frequently
MAX_HISTORY = 100 # Keep last 100 values for scrolling
updating_enabled = False # Track if the tab is active
input_channels = list(range(8))
data = {ch: [0] * MAX_HISTORY for ch in input_channels} # Preallocate data
time_data = list(range(-MAX_HISTORY, 0)) # Simulated time axis
def create_digital_plot_tab(notebook): def create_digital_plot_tab(notebook):
frame = ttk.Frame(notebook) frame = ttk.Frame(notebook)
notebook.add(frame, text="Digital Inputs") notebook.add(frame, text="Digital Inputs")
figure = Figure(figsize=(8, 5), dpi=100) figure = Figure(figsize=(8, 5), dpi=100)
ax = figure.add_subplot(1, 1, 1) ax = figure.add_subplot(1, 1, 1)
ax.set_title("Digital Input States Over Time")
ax.set_xlabel("Time (s)")
ax.set_ylabel("State (0=LOW, 1=HIGH)")
ax.set_ylim(-0.2, 1.2) ax.set_ylim(-0.2, 1.2)
ax.set_xlim(-MAX_HISTORY, 0) # Keep time axis fixed
canvas = FigureCanvasTkAgg(figure, master=frame) canvas = FigureCanvasTkAgg(figure, master=frame)
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
input_channels = list(range(8)) # Initialize lines for fast updates
data = {ch: [] for ch in input_channels} lines = {ch: ax.step(time_data, data[ch], where="post")[0] for ch in input_channels}
time_data = []
def update_plot(): def update_plot():
current_time = time.time() if not updating_enabled:
if len(time_data) > 50: frame.after(NOT_ACTIVE_CHECK_INTERVAL, update_plot)
for ch in input_channels: return
data[ch].pop(0)
time_data.pop(0) # Shift existing data left
time_data.append(current_time)
for ch in input_channels: for ch in input_channels:
state = GPIO.input(GPIO_DIGITAL_INPUTS[ch]) data[ch].pop(0)
data[ch].append(state) data[ch].append(GPIO.input(GPIO_DIGITAL_INPUTS[ch]))
ax.clear()
ax.set_title("Digital Input States Over Time")
ax.set_xlabel("Time (s)")
ax.set_ylabel("State (0=LOW, 1=HIGH)")
ax.set_ylim(-0.2, 1.2)
# Update only the y-data for efficiency
for ch in input_channels: for ch in input_channels:
ax.step(time_data, data[ch], label=f"IN {ch+1}", where="post") lines[ch].set_ydata(data[ch])
ax.legend(loc="upper right") canvas.draw_idle() # More efficient than draw()
canvas.draw() frame.after(DIGITAL_PLOT_UPDATE_INTERVAL, update_plot)
frame.after(500, update_plot)
update_plot() update_plot()
def set_updating_enabled(is_active):
global updating_enabled
updating_enabled = is_active
print(f"digital_plot tab: set updating_enabled to {updating_enabled}")

View File

@ -0,0 +1,21 @@
import tkinter as tk
from tkinter import ttk
def create_exit_tab(notebook, root, pwm1, pwm2):
frame = ttk.Frame(notebook)
notebook.add(frame, text="EXIT")
ttk.Label(frame, text="Exit the GUI", font=("Arial", 20)).pack(pady=20)
def exit_program():
pwm1.stop()
pwm2.stop()
root.quit()
root.destroy()
exit_button = ttk.Button(frame, text="Exit Application", command=exit_program, style="TButton")
exit_button.pack(pady=20)
style = ttk.Style()
style.configure("TButton", font=("Arial", 18), padding=10)