From d451968b715c9a4436f25f7aa79c6dd11d4aae9e Mon Sep 17 00:00:00 2001 From: feczkob Date: Wed, 17 Dec 2025 21:29:32 +0100 Subject: [PATCH 1/4] add TODO, set boundaries for button --- constants.py | 4 +-- main.py | 17 +++++---- schema.drawio | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 schema.drawio diff --git a/constants.py b/constants.py index 20b459a..ced1390 100644 --- a/constants.py +++ b/constants.py @@ -19,10 +19,10 @@ BUTTONS_PIN = machine.Pin(27, machine.Pin.IN) BUTTONS_PIN_ADC = machine.ADC(BUTTONS_PIN) -TARGET_PHASE_1 = 78.0 +TARGET_PHASE_1 = 26.0 TARGET_PHASE_2 = 81.0 TARGET_PHASE_3 = 83.0 -TARGET_PHASE_4 = 90.0 +TARGET_PHASE_4 = 95.0 TARGET_TEMPERATURE = [TARGET_PHASE_1, TARGET_PHASE_2, TARGET_PHASE_3, TARGET_PHASE_4] display = lcd_4bit_mode.LCD16x2(RS,ENABLE,BACK_LIGHT,D4,D5,D6,D7) diff --git a/main.py b/main.py index 7f4e630..c96fb93 100644 --- a/main.py +++ b/main.py @@ -46,6 +46,7 @@ def measure_temperature(): def control_heaters(): global heaters_in_use, measured_temp, TARGET_TEMPERATURE + # TODO: PID controller if measured_temp < TARGET_TEMPERATURE[phase - 1]: heaters_in_use = [True, True, True] HEATER_1.value(heaters_in_use[0] & heaters_enabled[0]) @@ -113,7 +114,9 @@ def draw_to_lcd(target_temp, measured_temp, phase, heaters_enabled, heaters_in_u def handle_buttons(): global TARGET_TEMPERATURE, buttons_adc, phase, mix, heaters_enabled buttons_adc = BUTTONS_PIN_ADC.read_u16() - if 200 <= buttons_adc < 6000: + #print("Buttons ADC value:", buttons_adc) + + if 200 <= buttons_adc < 5000: # RIGHT if(sum(heaters_enabled) == 0): heaters_enabled = [True, True, True] @@ -121,19 +124,19 @@ def handle_buttons(): return heaters_enabled[sum(heaters_enabled) - 1] = False sleep(0.3) # Simple debounce - elif 6000 <= buttons_adc < 14000: + elif 5000 <= buttons_adc < 11000: # UP TARGET_TEMPERATURE[phase - 1] += .1 - print("Increased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) - elif 14000 <= buttons_adc < 20000: + #print("Increased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) + elif 11000 <= buttons_adc < 17000: # DOWN TARGET_TEMPERATURE[phase - 1] -= .1 - print("Decreased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) - elif 20000 <= buttons_adc < 32000: + #print("Decreased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) + elif 17000 <= buttons_adc < 26000: # LEFT mix = not mix sleep(0.3) # Simple debounce - elif 32000 <= buttons_adc < 40000: + elif 26000 <= buttons_adc < 38000: # SELECT phase = (phase % 4) + 1 sleep(0.3) # Simple debounce diff --git a/schema.drawio b/schema.drawio new file mode 100644 index 0000000..315c0ec --- /dev/null +++ b/schema.drawio @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From e7b4fea4b0c208b6bed9cbd9583819945c764e29 Mon Sep 17 00:00:00 2001 From: feczkob Date: Tue, 30 Dec 2025 18:05:12 +0100 Subject: [PATCH 2/4] refactor to reflect proper responsibilities --- control.py | 35 ++++++++++ lcd.py | 111 ++++++++++++++++++++++++++++++++ main.py | 162 +++++------------------------------------------ program_state.py | 48 ++++++++++++++ 4 files changed, 209 insertions(+), 147 deletions(-) create mode 100644 control.py create mode 100644 lcd.py create mode 100644 program_state.py diff --git a/control.py b/control.py new file mode 100644 index 0000000..ad2cb1e --- /dev/null +++ b/control.py @@ -0,0 +1,35 @@ + +from constants import DS_SENSOR, HEATER_1, HEATER_2, HEATER_3, MIXER_PIN +from program_state import ProgramState + +roms = DS_SENSOR.scan() + +class Control: + def __init__(self): + pass + + def measure_temperature(self, state: ProgramState): + DS_SENSOR.convert_temp() + measured = DS_SENSOR.read_temp(roms[0]) + + #print("Measured temperature:", measured) + state.set_measured_temp(measured) + + def control_heaters(self, state: ProgramState): + # TODO: PID controller + #print("Target temp:", state.get_target_temp_for_phase(), "Measured temp:", state.measured_temp) + + if state.should_activate_heaters(): + state.switch_heaters_on() + else: + state.switch_heaters_off() + + HEATER_1.value(state.calculate_heater_state(0)) + HEATER_2.value(state.calculate_heater_state(1)) + HEATER_3.value(state.calculate_heater_state(2)) + + def control_mixer(self, state: ProgramState): + if state.mix: + MIXER_PIN.on() + else: + MIXER_PIN.off() \ No newline at end of file diff --git a/lcd.py b/lcd.py new file mode 100644 index 0000000..1d8f1e8 --- /dev/null +++ b/lcd.py @@ -0,0 +1,111 @@ + +from utime import sleep +from constants import BUTTONS_PIN_ADC, CHAR_HEATER, CHAR_HEATER_ENABLED_OFF, CHAR_HEATER_ENABLED_ON, CHAR_MIX, CHAR_MIX_OFF, CHAR_MIX_ON, CHAR_TARGET, CHAR_THERMOMETER +from program_state import ProgramState + + +class LCD: + def __init__(self, display, state: ProgramState): + self.display = display + self.backlight_on() + + # Create custom characters + self.display.CreateChar(slot=0, bitmap=CHAR_HEATER_ENABLED_OFF) + self.display.CreateChar(slot=1, bitmap=CHAR_TARGET) + self.display.CreateChar(slot=2, bitmap=CHAR_THERMOMETER) + self.display.CreateChar(slot=3, bitmap=CHAR_HEATER_ENABLED_ON) + self.display.CreateChar(slot=4, bitmap=CHAR_HEATER) + self.display.CreateChar(slot=5, bitmap=CHAR_MIX) + self.display.CreateChar(slot=6, bitmap=CHAR_MIX_ON) + self.display.CreateChar(slot=7, bitmap=CHAR_MIX_OFF) + + self.target_temp: float = state.get_target_temp_for_phase() + self.measured_temp: float = state.measured_temp + self.phase = state.phase + self.mix = state.mix + self.heaters_enabled = state.heaters_enabled.copy() + self.heaters_in_use = state.heaters_in_use.copy() + + def backlight_on(self): + self.display.BackLightOn() + + def backlight_off(self): + self.display.BackLightOff() + + def handle_buttons(self, state: ProgramState): + buttons_adc = BUTTONS_PIN_ADC.read_u16() + #print("Buttons ADC value:", buttons_adc) + + if 200 <= buttons_adc < 5000: + # RIGHT + if(sum(state.heaters_enabled) == 0): + state.heaters_enabled = [True, True, True] + sleep(0.3) # Simple debounce + return + state.heaters_enabled[sum(state.heaters_enabled) - 1] = False + sleep(0.3) # Simple debounce + elif 5000 <= buttons_adc < 11000: + # UP + state.change_target_temp_for_phase_by(0.1) + #print("Increased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) + elif 11000 <= buttons_adc < 17000: + # DOWN + state.change_target_temp_for_phase_by(-0.1) + #print("Decreased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) + elif 17000 <= buttons_adc < 26000: + # LEFT + state.switch_mix() + sleep(0.3) # Simple debounce + elif 26000 <= buttons_adc < 38000: + # SELECT + state.switch_phase() + sleep(0.3) # Simple debounce + + def update_display(self, state: ProgramState): + if (round(self.target_temp, 1) == round(state.get_target_temp_for_phase(), 1) and + round(self.measured_temp, 1) == round(state.measured_temp, 1) and + self.phase == state.phase and + self.mix == state.mix and + self.heaters_enabled == state.heaters_enabled and + self.heaters_in_use == state.heaters_in_use): + return # No changes, skip updating the display + + self.target_temp = state.get_target_temp_for_phase() + self.measured_temp = state.measured_temp + self.phase = state.phase + self.mix = state.mix + self.heaters_in_use = state.heaters_in_use.copy() + self.heaters_enabled = state.heaters_enabled.copy() + + self.display.ClearScreenCursorHome() + + # First row + # Temperatures + self.display.WriteLine(f" {round(self.target_temp, 1)}C {round(self.measured_temp, 1)}C", 1) + # Target temperature + self.display.DrawCustomChar(line_number=1, col=1, slot=1) + # Measured temperature + self.display.DrawCustomChar(line_number=1, col=10, slot=2) + + # Second row + # Phase + self.display.WriteLine(f"F{self.phase}", 2) + # Mixer icon + self.display.DrawCustomChar(line_number=2, col=8, slot=5) + # Mixer on/off + if(self.mix): + self.display.DrawCustomChar(line_number=2, col=9, slot=6) + else: + self.display.DrawCustomChar(line_number=2, col=9, slot=7) + # Heater icon + self.display.DrawCustomChar(line_number=2, col=13, slot=4) + # Heaters on/off + if(self.heaters_enabled[0]): + if (self.heaters_in_use[0]): self.display.DrawCustomChar(line_number=2, col=14, slot=3) + else: self.display.DrawCustomChar(line_number=2, col=14, slot=0) + if(self.heaters_enabled[1]): + if (self.heaters_in_use[1]): self.display.DrawCustomChar(line_number=2, col=15, slot=3) + else: self.display.DrawCustomChar(line_number=2, col=15, slot=0) + if(self.heaters_enabled[2]): + if (self.heaters_in_use[2]): self.display.DrawCustomChar(line_number=2, col=16, slot=3) + else: self.display.DrawCustomChar(line_number=2, col=16, slot=0) \ No newline at end of file diff --git a/main.py b/main.py index c96fb93..09a304c 100644 --- a/main.py +++ b/main.py @@ -1,162 +1,30 @@ +from control import Control +from lcd import LCD +from program_state import ProgramState from utime import sleep -from constants import * +from constants import display, HEATER_1, HEATER_2, HEATER_3 - -# Program state -measured_temp: float = 0.0 -phase = 1 -mix = False -heaters_in_use = [False, False, False] -heaters_enabled = [True, True, True] +# Initial setup HEATER_1.off() HEATER_2.off() HEATER_3.off() -# Sensors -buttons_adc = 0 -roms = DS_SENSOR.scan() +# Program state +state = ProgramState() # Screen state -screen_target_temp = 0.0 -screen_measured_temp = measured_temp -screen_phase = phase -screen_mix = mix -screen_heaters_enabled = heaters_enabled.copy() -screen_heaters_in_use = heaters_in_use.copy() - -# Initialize LCD -display.BackLightOn() -display.CreateChar(slot=0, bitmap=CHAR_HEATER_ENABLED_OFF) -display.CreateChar(slot=1, bitmap=CHAR_TARGET) -display.CreateChar(slot=2, bitmap=CHAR_THERMOMETER) -display.CreateChar(slot=3, bitmap=CHAR_HEATER_ENABLED_ON) -display.CreateChar(slot=4, bitmap=CHAR_HEATER) -display.CreateChar(slot=5, bitmap=CHAR_MIX) -display.CreateChar(slot=6, bitmap=CHAR_MIX_ON) -display.CreateChar(slot=7, bitmap=CHAR_MIX_OFF) - -def measure_temperature(): - # TODO set precision? - global measured_temp - DS_SENSOR.convert_temp() - measured = DS_SENSOR.read_temp(roms[0]) - - #print("Measured temperature:", measured) - measured_temp = measured - -def control_heaters(): - global heaters_in_use, measured_temp, TARGET_TEMPERATURE - # TODO: PID controller - if measured_temp < TARGET_TEMPERATURE[phase - 1]: - heaters_in_use = [True, True, True] - HEATER_1.value(heaters_in_use[0] & heaters_enabled[0]) - HEATER_2.value(heaters_in_use[1] & heaters_enabled[1]) - HEATER_3.value(heaters_in_use[2] & heaters_enabled[2]) - else: - heaters_in_use = [False, False, False] - HEATER_1.off() - HEATER_2.off() - HEATER_3.off() - -def control_mixer(): - global mix - if mix: - MIXER_PIN.on() - else: - MIXER_PIN.off() - -def draw_to_lcd(target_temp, measured_temp, phase, heaters_enabled, heaters_in_use): - global screen_target_temp, screen_measured_temp, screen_phase, screen_mix, screen_heaters_in_use, screen_heaters_enabled - - if (round(screen_target_temp, 1) == round(target_temp, 1) and - round(screen_measured_temp, 1) == round(measured_temp, 1) and - screen_phase == phase and - screen_mix == mix and - screen_heaters_enabled == heaters_enabled and - screen_heaters_in_use == heaters_in_use): - return # No changes, skip updating the display - - screen_target_temp = target_temp - screen_measured_temp = measured_temp - screen_phase = phase - screen_mix = mix - screen_heaters_in_use = heaters_in_use.copy() - screen_heaters_enabled = heaters_enabled.copy() - - display.ClearScreenCursorHome() - display.WriteLine(f" {round(screen_target_temp, 1)}C {round(screen_measured_temp, 1)}C", 1) - # Target temperature - display.DrawCustomChar(line_number=1, col=1, slot=1) - # Measured temperature - display.DrawCustomChar(line_number=1, col=10, slot=2) - - display.WriteLine(f"F{screen_phase}", 2) - # Mixer icon - display.DrawCustomChar(line_number=2, col=8, slot=5) - # Mixer on/off - if(screen_mix): - display.DrawCustomChar(line_number=2, col=9, slot=6) - else: - display.DrawCustomChar(line_number=2, col=9, slot=7) - # Heater icon - display.DrawCustomChar(line_number=2, col=13, slot=4) - # Heaters on/off - if(heaters_enabled[0]): - if (heaters_in_use[0]): display.DrawCustomChar(line_number=2, col=14, slot=3) - else: display.DrawCustomChar(line_number=2, col=14, slot=0) - if(heaters_enabled[1]): - if (heaters_in_use[1]): display.DrawCustomChar(line_number=2, col=15, slot=3) - else: display.DrawCustomChar(line_number=2, col=15, slot=0) - if(heaters_enabled[2]): - if (heaters_in_use[2]): display.DrawCustomChar(line_number=2, col=16, slot=3) - else: display.DrawCustomChar(line_number=2, col=16, slot=0) - -def handle_buttons(): - global TARGET_TEMPERATURE, buttons_adc, phase, mix, heaters_enabled - buttons_adc = BUTTONS_PIN_ADC.read_u16() - #print("Buttons ADC value:", buttons_adc) - - if 200 <= buttons_adc < 5000: - # RIGHT - if(sum(heaters_enabled) == 0): - heaters_enabled = [True, True, True] - sleep(0.3) # Simple debounce - return - heaters_enabled[sum(heaters_enabled) - 1] = False - sleep(0.3) # Simple debounce - elif 5000 <= buttons_adc < 11000: - # UP - TARGET_TEMPERATURE[phase - 1] += .1 - #print("Increased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) - elif 11000 <= buttons_adc < 17000: - # DOWN - TARGET_TEMPERATURE[phase - 1] -= .1 - #print("Decreased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) - elif 17000 <= buttons_adc < 26000: - # LEFT - mix = not mix - sleep(0.3) # Simple debounce - elif 26000 <= buttons_adc < 38000: - # SELECT - phase = (phase % 4) + 1 - sleep(0.3) # Simple debounce +lcd = LCD(display, state) -def array_and(b1, b2): - return [a and b for a, b in zip(b1, b2)] +# Control +control = Control() while True: try: - measure_temperature() - handle_buttons() - control_heaters() - control_mixer() - draw_to_lcd( - TARGET_TEMPERATURE[phase - 1], - measured_temp, - phase, - heaters_enabled, - heaters_in_use, - ) + control.measure_temperature(state) + lcd.handle_buttons(state) + control.control_heaters(state) + control.control_mixer(state) + lcd.update_display(state) sleep(0.1) except KeyboardInterrupt: break \ No newline at end of file diff --git a/program_state.py b/program_state.py new file mode 100644 index 0000000..496d75b --- /dev/null +++ b/program_state.py @@ -0,0 +1,48 @@ + +from constants import TARGET_TEMPERATURE + + +class ProgramState: + def __init__(self): + self.target_temp: list[float] = TARGET_TEMPERATURE + self.measured_temp: float = 0.0 + self.phase = 1 + self.mix = False + self.heaters_in_use = [False, False, False] + self.heaters_enabled = [True, True, True] + + def get_target_temp_for_phase(self) -> float: + if 1 <= self.phase <= len(self.target_temp): + return self.target_temp[self.phase - 1] + else: + raise ValueError("Phase out of range") + + def change_target_temp_for_phase_by(self, temperature: float): + if 1 <= self.phase <= len(self.target_temp): + self.target_temp[self.phase - 1] += temperature + else: + raise ValueError("Phase out of range") + + def switch_mix(self): + self.mix = not self.mix + + def switch_phase(self): + self.phase = (self.phase % 4) + 1 + + def set_measured_temp(self, temperature: float): + self.measured_temp = temperature + + def switch_heaters_on(self): + self.heaters_in_use = [True, True, True] + + def switch_heaters_off(self): + self.heaters_in_use = [False, False, False] + + def calculate_heater_state(self, index: int) -> bool: + if 0 <= index < len(self.heaters_in_use): + return self.heaters_in_use[index] and self.heaters_enabled[index] + else: + raise IndexError("Heater index out of range") + + def should_activate_heaters(self) -> bool: + return self.measured_temp < self.get_target_temp_for_phase() \ No newline at end of file From 01c2bb4bc96dcb51e8141ca7d90a22e8cc1fa87c Mon Sep 17 00:00:00 2001 From: feczkob Date: Tue, 30 Dec 2025 20:08:00 +0100 Subject: [PATCH 3/4] add PID controller --- constants.py | 5 ++++- control.py | 25 ++++++++++++----------- lcd.py | 16 ++++++++------- main.py | 10 +++++---- pid.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ program_state.py | 9 ++------ 6 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 pid.py diff --git a/constants.py b/constants.py index ced1390..00d4e8d 100644 --- a/constants.py +++ b/constants.py @@ -15,16 +15,19 @@ HEATER_3 = machine.Pin(9,machine.Pin.OUT) TEMPERATURE_PIN = machine.Pin(10, machine.Pin.IN) DS_SENSOR = ds18x20.DS18X20(onewire.OneWire(TEMPERATURE_PIN)) +roms = DS_SENSOR.scan() MIXER_PIN = machine.Pin(11, machine.Pin.OUT) BUTTONS_PIN = machine.Pin(27, machine.Pin.IN) BUTTONS_PIN_ADC = machine.ADC(BUTTONS_PIN) -TARGET_PHASE_1 = 26.0 +TARGET_PHASE_1 = 78.0 TARGET_PHASE_2 = 81.0 TARGET_PHASE_3 = 83.0 TARGET_PHASE_4 = 95.0 TARGET_TEMPERATURE = [TARGET_PHASE_1, TARGET_PHASE_2, TARGET_PHASE_3, TARGET_PHASE_4] +CYCLE_TIME_SECONDS = 0.1 + display = lcd_4bit_mode.LCD16x2(RS,ENABLE,BACK_LIGHT,D4,D5,D6,D7) diff --git a/control.py b/control.py index ad2cb1e..4a53291 100644 --- a/control.py +++ b/control.py @@ -1,12 +1,10 @@ - -from constants import DS_SENSOR, HEATER_1, HEATER_2, HEATER_3, MIXER_PIN +from constants import DS_SENSOR, HEATER_1, HEATER_2, HEATER_3, MIXER_PIN, roms +from pid import PidController from program_state import ProgramState -roms = DS_SENSOR.scan() - class Control: - def __init__(self): - pass + def __init__(self, pid: PidController): + self.pid = pid def measure_temperature(self, state: ProgramState): DS_SENSOR.convert_temp() @@ -16,14 +14,17 @@ def measure_temperature(self, state: ProgramState): state.set_measured_temp(measured) def control_heaters(self, state: ProgramState): - # TODO: PID controller #print("Target temp:", state.get_target_temp_for_phase(), "Measured temp:", state.measured_temp) - if state.should_activate_heaters(): - state.switch_heaters_on() - else: - state.switch_heaters_off() - + # Calculate PID output + pid_output = self.pid.calculate_pid_output(state) + + # Determine which heaters should be active + heaters_in_use = self.pid.determine_heater_states(pid_output) + + state.switch_heaters(heaters_in_use) + + # Apply heater states to hardware HEATER_1.value(state.calculate_heater_state(0)) HEATER_2.value(state.calculate_heater_state(1)) HEATER_3.value(state.calculate_heater_state(2)) diff --git a/lcd.py b/lcd.py index 1d8f1e8..1cab45c 100644 --- a/lcd.py +++ b/lcd.py @@ -1,4 +1,3 @@ - from utime import sleep from constants import BUTTONS_PIN_ADC, CHAR_HEATER, CHAR_HEATER_ENABLED_OFF, CHAR_HEATER_ENABLED_ON, CHAR_MIX, CHAR_MIX_OFF, CHAR_MIX_ON, CHAR_TARGET, CHAR_THERMOMETER from program_state import ProgramState @@ -37,13 +36,16 @@ def handle_buttons(self, state: ProgramState): #print("Buttons ADC value:", buttons_adc) if 200 <= buttons_adc < 5000: + pass # RIGHT - if(sum(state.heaters_enabled) == 0): - state.heaters_enabled = [True, True, True] - sleep(0.3) # Simple debounce - return - state.heaters_enabled[sum(state.heaters_enabled) - 1] = False - sleep(0.3) # Simple debounce + # PID controls it + + # if(sum(state.heaters_enabled) == 0): + # state.heaters_enabled = [True, True, True] + # sleep(0.3) # Simple debounce + # return + # state.heaters_enabled[sum(state.heaters_enabled) - 1] = False + # sleep(0.3) # Simple debounce elif 5000 <= buttons_adc < 11000: # UP state.change_target_temp_for_phase_by(0.1) diff --git a/main.py b/main.py index 09a304c..2066fd4 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,9 @@ from control import Control from lcd import LCD +from pid import PidController from program_state import ProgramState from utime import sleep -from constants import display, HEATER_1, HEATER_2, HEATER_3 +from constants import CYCLE_TIME_SECONDS, display, HEATER_1, HEATER_2, HEATER_3 # Initial setup HEATER_1.off() @@ -16,8 +17,9 @@ lcd = LCD(display, state) # Control -control = Control() - +pid = PidController() +control = Control(pid) + while True: try: control.measure_temperature(state) @@ -25,6 +27,6 @@ control.control_heaters(state) control.control_mixer(state) lcd.update_display(state) - sleep(0.1) + sleep(CYCLE_TIME_SECONDS) except KeyboardInterrupt: break \ No newline at end of file diff --git a/pid.py b/pid.py new file mode 100644 index 0000000..7661f90 --- /dev/null +++ b/pid.py @@ -0,0 +1,53 @@ +from constants import CYCLE_TIME_SECONDS +from program_state import ProgramState + +class PidController: + def __init__(self, Kp: float = 1.0, Ki: float = 0.1, Kd: float = 0.02): + # PID coefficients (to be tuned) + # https://www.ni.com/en/shop/labview/pid-theory-explained.html + self.Kp = Kp # Proportional gain + self.Ki = Ki # Integral gain + self.Kd = Kd # Derivative gain + + self.pid_last_error = 0.0 + + self.integral = 0.0 + + def calculate_pid_output(self, state: ProgramState) -> float: + """Calculate PID output based on target and measured temperatures""" + # Calculate error + error = state.get_target_temp_for_phase() - state.measured_temp + + # Calculate PID output + proportional = self.Kp * error + self.integral += self.Ki * error * CYCLE_TIME_SECONDS + derivative = self.Kd * (error - self.pid_last_error) / CYCLE_TIME_SECONDS + + output = proportional + self.integral + derivative + + # Update PID state + self.pid_last_error = error + + # Return PID output + return output + + def determine_heater_states(self, pid_output: float) -> list[bool]: + """Determine which heaters should be active based on PID output""" + # Determine heater states based on PID output + # Higher positive output means more heating needed + + #print("PID output:", pid_output) + + # Define thresholds for heater activation + if pid_output > 2.0: + # All heaters on for maximum heating + return [True, True, True] + elif pid_output > 1.0: + # Two heaters on for medium heating + return [True, True, False] + elif pid_output > 0.5: + # One heater on for gentle heating + return [True, False, False] + else: + # No heaters on (cooling or maintaining) + return [False, False, False] \ No newline at end of file diff --git a/program_state.py b/program_state.py index 496d75b..8c23830 100644 --- a/program_state.py +++ b/program_state.py @@ -1,7 +1,5 @@ - from constants import TARGET_TEMPERATURE - class ProgramState: def __init__(self): self.target_temp: list[float] = TARGET_TEMPERATURE @@ -32,11 +30,8 @@ def switch_phase(self): def set_measured_temp(self, temperature: float): self.measured_temp = temperature - def switch_heaters_on(self): - self.heaters_in_use = [True, True, True] - - def switch_heaters_off(self): - self.heaters_in_use = [False, False, False] + def switch_heaters(self, states: list[bool]): + self.heaters_in_use = states def calculate_heater_state(self, index: int) -> bool: if 0 <= index < len(self.heaters_in_use): From 365d8d5d22f61d25e93bdd90cc7da19da1021a97 Mon Sep 17 00:00:00 2001 From: feczkob Date: Sat, 17 Jan 2026 15:34:53 +0100 Subject: [PATCH 4/4] fine tune parameters, more work needed --- lcd.py | 10 +++++----- pid.py | 16 +++++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lcd.py b/lcd.py index 1cab45c..974ba78 100644 --- a/lcd.py +++ b/lcd.py @@ -35,7 +35,7 @@ def handle_buttons(self, state: ProgramState): buttons_adc = BUTTONS_PIN_ADC.read_u16() #print("Buttons ADC value:", buttons_adc) - if 200 <= buttons_adc < 5000: + if 200 <= buttons_adc < 6000: pass # RIGHT # PID controls it @@ -46,19 +46,19 @@ def handle_buttons(self, state: ProgramState): # return # state.heaters_enabled[sum(state.heaters_enabled) - 1] = False # sleep(0.3) # Simple debounce - elif 5000 <= buttons_adc < 11000: + elif 6000 <= buttons_adc < 14000: # UP state.change_target_temp_for_phase_by(0.1) #print("Increased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) - elif 11000 <= buttons_adc < 17000: + elif 14000 <= buttons_adc < 20000: # DOWN state.change_target_temp_for_phase_by(-0.1) #print("Decreased target temperature to:", round(TARGET_TEMPERATURE[phase - 1], 1)) - elif 17000 <= buttons_adc < 26000: + elif 20000 <= buttons_adc < 32000: # LEFT state.switch_mix() sleep(0.3) # Simple debounce - elif 26000 <= buttons_adc < 38000: + elif 32000 <= buttons_adc < 40000: # SELECT state.switch_phase() sleep(0.3) # Simple debounce diff --git a/pid.py b/pid.py index 7661f90..8409a55 100644 --- a/pid.py +++ b/pid.py @@ -2,7 +2,7 @@ from program_state import ProgramState class PidController: - def __init__(self, Kp: float = 1.0, Ki: float = 0.1, Kd: float = 0.02): + def __init__(self, Kp: float = 0.6, Ki: float = 0.001, Kd: float = 0.002): # PID coefficients (to be tuned) # https://www.ni.com/en/shop/labview/pid-theory-explained.html self.Kp = Kp # Proportional gain @@ -12,6 +12,9 @@ def __init__(self, Kp: float = 1.0, Ki: float = 0.1, Kd: float = 0.02): self.pid_last_error = 0.0 self.integral = 0.0 + + # the system is slow, we need to avoid integral windup + self.delta_T = 0.2 def calculate_pid_output(self, state: ProgramState) -> float: """Calculate PID output based on target and measured temperatures""" @@ -20,7 +23,10 @@ def calculate_pid_output(self, state: ProgramState) -> float: # Calculate PID output proportional = self.Kp * error - self.integral += self.Ki * error * CYCLE_TIME_SECONDS + if (abs(error) < self.delta_T): + self.integral += self.Ki * error * CYCLE_TIME_SECONDS + else: + self.integral = 0.0 # Reset integral if error is too large derivative = self.Kd * (error - self.pid_last_error) / CYCLE_TIME_SECONDS output = proportional + self.integral + derivative @@ -39,13 +45,13 @@ def determine_heater_states(self, pid_output: float) -> list[bool]: #print("PID output:", pid_output) # Define thresholds for heater activation - if pid_output > 2.0: + if pid_output > 0.2: # All heaters on for maximum heating return [True, True, True] - elif pid_output > 1.0: + elif pid_output > 0.05: # Two heaters on for medium heating return [True, True, False] - elif pid_output > 0.5: + elif pid_output > -0.05: # One heater on for gentle heating return [True, False, False] else: