From bb0436b139f906a09a955cba3886e01e0a7be1f8 Mon Sep 17 00:00:00 2001 From: Lidya Langana Date: Wed, 19 Aug 2026 11:23:36 -0700 Subject: [PATCH 1/8] Add mock LED backend integration --- src/devices/led_mock.py | 24 +++++++++++++++ src/devices/library.py | 1 + src/titrator.py | 4 +++ src/ui_state/main_menu.py | 3 ++ src/ui_state/set_menu/set_led.py | 52 ++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 src/devices/led_mock.py create mode 100644 src/ui_state/set_menu/set_led.py diff --git a/src/devices/led_mock.py b/src/devices/led_mock.py new file mode 100644 index 0000000..c25f393 --- /dev/null +++ b/src/devices/led_mock.py @@ -0,0 +1,24 @@ +""" +Mock LED device for the Tank Controller. +""" + + +class LED: + """Mock LED that tracks an ON/OFF state.""" + + def __init__(self): + """Initialize the LED as off.""" + self.is_on = False + + def on(self): + """Turn the LED on.""" + self.is_on = True + + def off(self): + """Turn the LED off.""" + self.is_on = False + + def toggle(self): + """Toggle the LED between on and off.""" + self.is_on = not self.is_on + \ No newline at end of file diff --git a/src/devices/library.py b/src/devices/library.py index 4b2e2da..2a7c41f 100644 --- a/src/devices/library.py +++ b/src/devices/library.py @@ -21,6 +21,7 @@ from src.devices.max31865_mock import MAX31865 from src.devices.serial_mock import Serial from src.devices.spi_mock import SPI + from src.devices.led_mock import LED else: import adafruit_ads1x15.ads1115 as ADS import board diff --git a/src/titrator.py b/src/titrator.py index cc7f1a1..b67cf77 100644 --- a/src/titrator.py +++ b/src/titrator.py @@ -14,6 +14,7 @@ SyringePump, TemperatureControl, TemperatureProbe, + LED, ) from src.devices.ph_control import PHControl from src.devices.ph_probe_mock import PHProbe @@ -64,6 +65,9 @@ def __init__(self): # Initialize LCD self.lcd = LiquidCrystal() + # Initialize LED + self.led = LED() + # Initialize Keypad self.keypad = Keypad() diff --git a/src/ui_state/main_menu.py b/src/ui_state/main_menu.py index 8204608..91959df 100644 --- a/src/ui_state/main_menu.py +++ b/src/ui_state/main_menu.py @@ -9,6 +9,7 @@ from src.ui_state.set_menu.set_chill_or_heat import SetChillOrHeat from src.ui_state.set_menu.set_google_mins import SetGoogleSheetInterval from src.ui_state.set_menu.set_kd import SetKD +from src.ui_state.set_menu.set_led import SetLED from src.ui_state.set_menu.set_ki import SetKI from src.ui_state.set_menu.set_kp import SetKP from src.ui_state.set_menu.set_ph_calibration import PHCalibration @@ -77,6 +78,7 @@ def __init__(self, titrator): "Set chill/heat", "Set Google mins", "Set KD", + "Set LED", "Set KI", "Set KP", "Set pH target", @@ -114,6 +116,7 @@ def __init__(self, titrator): SetChillOrHeat, # Set chill/heat SetGoogleSheetInterval, # Set Google mins SetKD, # Set KD + SetLED, # Set LED SetKI, # Set KI SetKP, # Set KP SetPHTarget, # Set pH target diff --git a/src/ui_state/set_menu/set_led.py b/src/ui_state/set_menu/set_led.py new file mode 100644 index 0000000..efe9276 --- /dev/null +++ b/src/ui_state/set_menu/set_led.py @@ -0,0 +1,52 @@ +""" +The file to hold the Set LED class +""" + +from src.devices.library import Keypad +from src.ui_state.ui_state import UIState + + +class SetLED(UIState): + """ + This is a class for the SetLED state of the Tank Controller + """ + + def __init__(self, titrator, previous_state=None): + super().__init__(titrator) + self.previous_state = previous_state + + def loop(self): + """ + The main loop for the SetLED state + """ + self.titrator.lcd.print("LED 1:on; 9:off", line=1) + + if self.titrator.led.is_on: + self.titrator.lcd.print("Currently on", line=2) + else: + self.titrator.lcd.print("Currently off", line=2) + + def handle_key(self, key): + """ + Handle key presses to control the LED. + """ + if key == Keypad.KEY_1: + self.titrator.led.on() + self.titrator.lcd.print("LED on", line=2) + self.return_to_main_menu(ms_delay=3000) + + if key == Keypad.KEY_9: + self.titrator.led.off() + self.titrator.lcd.print("LED off", line=2) + self.return_to_main_menu(ms_delay=3000) + + if key == Keypad.KEY_A: + if self.titrator.led.is_on: + self.titrator.lcd.print("LED on", line=2) + else: + self.titrator.lcd.print("LED off", line=2) + self.return_to_main_menu(ms_delay=3000) + + if key == Keypad.KEY_D: + self._set_next_state(self.previous_state, True) + \ No newline at end of file From 304155dc14eec25912e6564c5d1cd7c08bc3bbf2 Mon Sep 17 00:00:00 2001 From: Lidya Langana Date: Wed, 19 Aug 2026 11:29:33 -0700 Subject: [PATCH 2/8] Added tests for the mock LED, and SetLED --- tests/devices/led_mock_test.py | 14 ++++++++++++ tests/ui_state/set_menu/set_led_test.py | 29 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/devices/led_mock_test.py create mode 100644 tests/ui_state/set_menu/set_led_test.py diff --git a/tests/devices/led_mock_test.py b/tests/devices/led_mock_test.py new file mode 100644 index 0000000..a2366fc --- /dev/null +++ b/tests/devices/led_mock_test.py @@ -0,0 +1,14 @@ +from src.devices.library import LED + +led = LED() + +print(led.is_on) # False + +led.on() +print(led.is_on) # True + +led.off() +print(led.is_on) # False + +led.toggle() +print(led.is_on) # True diff --git a/tests/ui_state/set_menu/set_led_test.py b/tests/ui_state/set_menu/set_led_test.py new file mode 100644 index 0000000..0daaa02 --- /dev/null +++ b/tests/ui_state/set_menu/set_led_test.py @@ -0,0 +1,29 @@ +""" +The file to test the SetLED class +""" + +from src.devices.library import Keypad +from src.titrator import Titrator +from src.ui_state.set_menu.set_led import SetLED + + +def test_led_on(): + """Test turning the LED on.""" + titrator = Titrator() + state = SetLED(titrator) + + state.handle_key(Keypad.KEY_1) + + assert titrator.led.is_on is True + + +def test_led_off(): + """Test turning the LED off.""" + titrator = Titrator() + state = SetLED(titrator) + + titrator.led.on() + state.handle_key(Keypad.KEY_9) + + assert titrator.led.is_on is False + \ No newline at end of file From 1f4e439410e69ab396e84f5a05e7b453a6aca87f Mon Sep 17 00:00:00 2001 From: Lidya Langana Date: Wed, 19 Aug 2026 11:31:25 -0700 Subject: [PATCH 3/8] Add mock LED GUI display --- src/gui.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/src/gui.py b/src/gui.py index 1409117..a8a16da 100644 --- a/src/gui.py +++ b/src/gui.py @@ -34,15 +34,31 @@ def __init__(self, titrator): # Initialize the GUI Frame self.root = tk.Tk() - self.root.geometry("280x200") + self.root.state("zoomed") self.root.title("Tank Controller") self.root.configure(background="black") + + # Split window into left and right sections self.root.columnconfigure(0, weight=1) + self.root.columnconfigure(1, weight=1) self.root.rowconfigure(0, weight=1) + # --------------------------------------------------------- + # LEFT SIDE - LCD AND KEYPAD + # --------------------------------------------------------- + + left_frame = tk.Frame(self.root, bg="lightgray") + left_frame.grid(row=0, column=0, sticky=STICKY) + + left_frame.columnconfigure(0, weight=1) + left_frame.rowconfigure(0, weight=1) + left_frame.rowconfigure(1, weight=1) + # Initialize the Labels - label_frame = tk.Frame(self.root) + label_frame = tk.Frame(left_frame) label_frame.config(bg=BG) + + label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(0, weight=1) label_frame.rowconfigure(1, weight=1) label_frame.rowconfigure(2, weight=1) @@ -95,7 +111,7 @@ def __init__(self, titrator): label_frame.grid(row=0, column=0, sticky=STICKY) # Initialize the Buttons - buttonframe = tk.Frame(self.root) + buttonframe = tk.Frame(left_frame) buttonframe.columnconfigure(0, weight=1) buttonframe.columnconfigure(1, weight=1) buttonframe.columnconfigure(2, weight=1) @@ -215,7 +231,50 @@ def __init__(self, titrator): buttonframe.grid(row=1, column=0, sticky=STICKY) - self.thread = threading.Thread(target=self.update_lcd, daemon=True) + # --------------------------------------------------------- + # RIGHT SIDE - BOARD LED + # --------------------------------------------------------- + + right_frame = tk.Frame(self.root, bg="darkgray") + right_frame.grid(row=0, column=1, sticky=STICKY) + + right_frame.columnconfigure(0, weight=1) + right_frame.columnconfigure(1, weight=1) + right_frame.rowconfigure(0, weight=1) + + self.led_canvas = tk.Canvas( + right_frame, + width=80, + height=80, + bg="darkgray", + highlightthickness=0, + ) + self.led_canvas.grid(row=0, column=0, padx=30, sticky=tk.E) + + self.led_circle = self.led_canvas.create_oval( + 5, + 5, + 50, + 50, + fill="red", + outline="black", + width=3, + ) + + self.led_label = tk.Label( + right_frame, + text="Board LED", + font=("Arial",12), + bg="darkgray", + fg="black", + ) + self.led_label.grid(row=0, column=1, padx=20, sticky=tk.W) + + # Start GUI update thread + self.thread = threading.Thread( + target=self.update_gui, + daemon=True, + ) self.thread.start() self.root.mainloop() @@ -226,12 +285,13 @@ def button_press(self, key): """ self.titrator.keypad.set_key(key) - def update_lcd(self): + def update_gui(self): """ - The function to update the GUI LCD + The function to update the GUI LCD and LED """ while True: time.sleep(0.001) + self.line_1.config( text=self.titrator.lcd.get_line(1), anchor=self.titrator.lcd.get_style(1), @@ -248,3 +308,14 @@ def update_lcd(self): text=self.titrator.lcd.get_line(4), anchor=self.titrator.lcd.get_style(4), ) + + if self.titrator.led.is_on: + self.led_canvas.itemconfig( + self.led_circle, + fill="yellow", + ) + else: + self.led_canvas.itemconfig( + self.led_circle, + fill="black", + ) From f2004d8e5d00d96d50f924439d13ee927d3003ee Mon Sep 17 00:00:00 2001 From: Lidya Langana Date: Fri, 21 Aug 2026 12:28:45 -0700 Subject: [PATCH 4/8] change the screen --- src/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui.py b/src/gui.py index a8a16da..6067cbc 100644 --- a/src/gui.py +++ b/src/gui.py @@ -34,7 +34,7 @@ def __init__(self, titrator): # Initialize the GUI Frame self.root = tk.Tk() - self.root.state("zoomed") + self.root.geometry("560x200") self.root.title("Tank Controller") self.root.configure(background="black") From e09fdea0b233a848232f2dc940f71e26fa647670 Mon Sep 17 00:00:00 2001 From: Lidya Langana Date: Fri, 21 Aug 2026 13:27:40 -0700 Subject: [PATCH 5/8] Add pytest unit tests for mock LED --- tests/devices/led_mock_test.py | 54 ++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/tests/devices/led_mock_test.py b/tests/devices/led_mock_test.py index a2366fc..485c764 100644 --- a/tests/devices/led_mock_test.py +++ b/tests/devices/led_mock_test.py @@ -1,14 +1,50 @@ -from src.devices.library import LED +""" +The file to test the Mock LED class. +""" -led = LED() +from src.devices.led_mock import LED -print(led.is_on) # False -led.on() -print(led.is_on) # True +def test_led_starts_off(): + """ + LED should be off when initialized. + """ + led = LED() -led.off() -print(led.is_on) # False + assert led.is_on is False -led.toggle() -print(led.is_on) # True + +def test_led_turns_on(): + """ + LED should turn on when on() is called. + """ + led = LED() + + led.on() + + assert led.is_on is True + + +def test_led_turns_off(): + """ + LED should turn off when off() is called. + """ + led = LED() + led.on() + + led.off() + + assert led.is_on is False + + +def test_led_toggle(): + """ + LED should switch between on and off when toggle() is called. + """ + led = LED() + + led.toggle() + assert led.is_on is True + + led.toggle() + assert led.is_on is False From 9dd8105282b463886a3621a078a13bd0ac7e0a29 Mon Sep 17 00:00:00 2001 From: Lidya Langana Date: Fri, 28 Aug 2026 09:16:10 -0700 Subject: [PATCH 6/8] Move Board LED indicator to top-right of GUI --- src/gui.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/gui.py b/src/gui.py index 6067cbc..11b4cb6 100644 --- a/src/gui.py +++ b/src/gui.py @@ -249,13 +249,13 @@ def __init__(self, titrator): bg="darkgray", highlightthickness=0, ) - self.led_canvas.grid(row=0, column=0, padx=30, sticky=tk.E) + self.led_canvas.grid(row=0, column=0, padx=5, pady=10, sticky=tk.NE) self.led_circle = self.led_canvas.create_oval( - 5, - 5, - 50, - 50, + 4, + 4, + 30, + 30, fill="red", outline="black", width=3, @@ -268,7 +268,7 @@ def __init__(self, titrator): bg="darkgray", fg="black", ) - self.led_label.grid(row=0, column=1, padx=20, sticky=tk.W) + self.led_label.grid(row=0, column=1, padx=2, pady=10, sticky=tk.NW) # Start GUI update thread self.thread = threading.Thread( From 637f72a33f1136cf7ec025ec421fb75e1f2cf600 Mon Sep 17 00:00:00 2001 From: Lidya Langana Date: Fri, 28 Aug 2026 09:18:07 -0700 Subject: [PATCH 7/8] fix formatting --- src/devices/led_mock.py | 1 - src/ui_state/set_menu/set_led.py | 1 - tests/ui_state/set_menu/set_led_test.py | 1 - 3 files changed, 3 deletions(-) diff --git a/src/devices/led_mock.py b/src/devices/led_mock.py index c25f393..96c30af 100644 --- a/src/devices/led_mock.py +++ b/src/devices/led_mock.py @@ -21,4 +21,3 @@ def off(self): def toggle(self): """Toggle the LED between on and off.""" self.is_on = not self.is_on - \ No newline at end of file diff --git a/src/ui_state/set_menu/set_led.py b/src/ui_state/set_menu/set_led.py index efe9276..e00dccb 100644 --- a/src/ui_state/set_menu/set_led.py +++ b/src/ui_state/set_menu/set_led.py @@ -49,4 +49,3 @@ def handle_key(self, key): if key == Keypad.KEY_D: self._set_next_state(self.previous_state, True) - \ No newline at end of file diff --git a/tests/ui_state/set_menu/set_led_test.py b/tests/ui_state/set_menu/set_led_test.py index 0daaa02..1f5d65c 100644 --- a/tests/ui_state/set_menu/set_led_test.py +++ b/tests/ui_state/set_menu/set_led_test.py @@ -26,4 +26,3 @@ def test_led_off(): state.handle_key(Keypad.KEY_9) assert titrator.led.is_on is False - \ No newline at end of file From 73cd06d49421f5d6d4b4b512c12853dc987b8809 Mon Sep 17 00:00:00 2001 From: Lidya Langana Date: Fri, 28 Aug 2026 09:26:05 -0700 Subject: [PATCH 8/8] fix formatting by running format.bat/format.sh --- src/devices/library.py | 2 +- src/gui.py | 2 +- src/titrator.py | 2 +- src/ui_state/main_menu.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/devices/library.py b/src/devices/library.py index 2a7c41f..0602636 100644 --- a/src/devices/library.py +++ b/src/devices/library.py @@ -17,11 +17,11 @@ from src.devices.digital_mock import DigitalInOut from src.devices.heater_mock import Heater from src.devices.keypad_mock import Keypad + from src.devices.led_mock import LED from src.devices.liquid_crystal_mock import LiquidCrystal from src.devices.max31865_mock import MAX31865 from src.devices.serial_mock import Serial from src.devices.spi_mock import SPI - from src.devices.led_mock import LED else: import adafruit_ads1x15.ads1115 as ADS import board diff --git a/src/gui.py b/src/gui.py index 11b4cb6..bbe4656 100644 --- a/src/gui.py +++ b/src/gui.py @@ -264,7 +264,7 @@ def __init__(self, titrator): self.led_label = tk.Label( right_frame, text="Board LED", - font=("Arial",12), + font=("Arial", 12), bg="darkgray", fg="black", ) diff --git a/src/titrator.py b/src/titrator.py index b67cf77..77d8630 100644 --- a/src/titrator.py +++ b/src/titrator.py @@ -7,6 +7,7 @@ from src.devices.date_time import DateTime from src.devices.eeprom import EEPROM from src.devices.library import ( + LED, Heater, Keypad, LiquidCrystal, @@ -14,7 +15,6 @@ SyringePump, TemperatureControl, TemperatureProbe, - LED, ) from src.devices.ph_control import PHControl from src.devices.ph_probe_mock import PHProbe diff --git a/src/ui_state/main_menu.py b/src/ui_state/main_menu.py index 91959df..bac8601 100644 --- a/src/ui_state/main_menu.py +++ b/src/ui_state/main_menu.py @@ -9,9 +9,9 @@ from src.ui_state.set_menu.set_chill_or_heat import SetChillOrHeat from src.ui_state.set_menu.set_google_mins import SetGoogleSheetInterval from src.ui_state.set_menu.set_kd import SetKD -from src.ui_state.set_menu.set_led import SetLED from src.ui_state.set_menu.set_ki import SetKI from src.ui_state.set_menu.set_kp import SetKP +from src.ui_state.set_menu.set_led import SetLED from src.ui_state.set_menu.set_ph_calibration import PHCalibration from src.ui_state.set_menu.set_ph_calibration_clear import ResetPHCalibration from src.ui_state.set_menu.set_ph_sine_wave import SetPHSineWave