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 sys
import time
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
import RPi.GPIO as GPIO
# 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_pins import * # Import pin assignments
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 set_updating_enabled as tab_analog_plot__set_updating_enabled
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
adc = MCP3208()
@ -38,10 +44,33 @@ root.configure(bg="black")
notebook = ttk.Notebook(root)
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
create_control_tab(notebook, adc, shift_reg, pwm1, pwm2)
create_adc_plot_tab(notebook, adc)
create_digital_plot_tab(notebook)
create_exit_tab(notebook, root, pwm1, pwm2)
# Run GUI
root.mainloop()

View File

@ -4,7 +4,14 @@ from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
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):
frame = ttk.Frame(notebook)
@ -12,41 +19,34 @@ def create_adc_plot_tab(notebook, adc):
figure = Figure(figsize=(8, 5), dpi=100)
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_xlim(-MAX_HISTORY, 0) # Keep time axis fixed
canvas = FigureCanvasTkAgg(figure, master=frame)
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
adc_channels = list(range(8))
data = {ch: [] for ch in adc_channels}
time_data = []
# Initialize lines for fast updates
lines = {ch: ax.plot(time_data, data[ch], label=f"ADC {ch+1}")[0] for ch in adc_channels}
def update_plot():
current_time = time.time()
if len(time_data) > 50:
if not updating_enabled:
frame.after(NOT_ACTIVE_CHECK_INTERVAL, update_plot)
return
# Shift existing data left
for ch in adc_channels:
data[ch].pop(0)
time_data.pop(0)
data[ch].append(round(adc.read(ch) * 12 / 4095, 2))
time_data.append(current_time)
# Update only the y-data for efficiency
for ch in adc_channels:
voltage = round(adc.read(ch) * 12 / 4095, 2)
data[ch].append(voltage)
lines[ch].set_ydata(data[ch])
ax.clear()
ax.set_title("ADC Readings Over Time")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Voltage (V)")
ax.set_ylim(0, 12)
for ch in adc_channels:
ax.plot(time_data, data[ch], label=f"ADC {ch+1}")
ax.legend(loc="upper right")
canvas.draw()
canvas.draw_idle() # Efficient redraw
frame.after(ADC_PLOT_UPDATE_INTERVAL, 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__), "..")))
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):
frame = ttk.Frame(notebook)
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)]
adc_values = [tk.StringVar(value="0.00V") for _ in range(8)]
pwm_values = [tk.IntVar(value=0), tk.IntVar(value=0)]
output_buttons = {} # Store button references to change colors
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()):
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():
if not updating_enabled:
frame.after(NOT_ACTIVE_CHECK_INTERVAL, update_adc)
return
for i, adc_channel in enumerate(ADC_CHANNELS.values()):
value = adc.read(adc_channel)
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):
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):
duty_cycle = int(float(value))
if channel == 0:
pwm1.ChangeDutyCycle(duty_cycle)
else:
pwm2.ChangeDutyCycle(duty_cycle)
if index in output_buttons:
for btn in output_buttons[index]:
btn.configure(bg="green" if new_state else "red", activebackground="green" if new_state else "red")
# UI Layout
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.pack(expand=True, fill="both")
control_frame = ttk.Frame(frame, padding=40)
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):
ttk.Label(control_frame, text=f"ADC {i+1}:", font=("Arial", 14)).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, text=f"IN {i+1}:", font=("Arial", 14)).grid(row=i, column=2, sticky="e")
ttk.Label(control_frame, textvariable=digital_input_states[i], width=6, font=("Arial", 14)).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")
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", 18)).grid(row=i, column=1, sticky="w")
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", 18)).grid(row=i, column=3, sticky="w")
for i in range(2):
ttk.Label(control_frame, text=f"PWM{i+1}:", font=("Arial", 14)).grid(row=i, column=5, sticky="e")
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`
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))
btn.grid(row=i, column=4, sticky="w", padx=20)
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_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 os
import threading
import tkinter as tk
from tkinter import ttk
import RPi.GPIO as GPIO
@ -10,47 +11,53 @@ import time
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
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):
frame = ttk.Frame(notebook)
notebook.add(frame, text="Digital Inputs")
figure = Figure(figsize=(8, 5), dpi=100)
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_xlim(-MAX_HISTORY, 0) # Keep time axis fixed
canvas = FigureCanvasTkAgg(figure, master=frame)
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
input_channels = list(range(8))
data = {ch: [] for ch in input_channels}
time_data = []
# Initialize lines for fast updates
lines = {ch: ax.step(time_data, data[ch], where="post")[0] for ch in input_channels}
def update_plot():
current_time = time.time()
if len(time_data) > 50:
if not updating_enabled:
frame.after(NOT_ACTIVE_CHECK_INTERVAL, update_plot)
return
# Shift existing data left
for ch in input_channels:
data[ch].pop(0)
time_data.pop(0)
data[ch].append(GPIO.input(GPIO_DIGITAL_INPUTS[ch]))
time_data.append(current_time)
# Update only the y-data for efficiency
for ch in input_channels:
state = GPIO.input(GPIO_DIGITAL_INPUTS[ch])
data[ch].append(state)
lines[ch].set_ydata(data[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)
for ch in input_channels:
ax.step(time_data, data[ch], label=f"IN {ch+1}", where="post")
ax.legend(loc="upper right")
canvas.draw()
frame.after(500, update_plot)
canvas.draw_idle() # More efficient than draw()
frame.after(DIGITAL_PLOT_UPDATE_INTERVAL, 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)