diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6ffd6a5..080e70d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,10 @@ { + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format "recommendations": [ "platformio.platformio-ide" ], "unwantedRecommendations": [ "ms-vscode.cpptools-extension-pack" ] -} \ No newline at end of file +} diff --git a/src/main.cpp b/src/main.cpp index c62ef3a..28c3434 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include #include "secrets.h" +#include "pid.h" AsyncWebServer server(80); AsyncWebSocket ws("/ws"); @@ -18,7 +19,7 @@ String jsonString; // Timer variables unsigned long lastTime = 0; -unsigned long timerDelay = 250; +unsigned long timerDelay = 500; int relay = 21; int thermoDO = 19; @@ -37,9 +38,12 @@ unsigned long lastSwitch = 0; unsigned long autoSwitchDelay = 20000; // 20s float pwmSwitchDelayOn = 4000; // 4s float pwmSwitchDelayOff = 7000; // 7s +float power = 0; String mode = "off"; +PIDController pid(1.0, 0.5, 1.0, 100.0, 0.0, 50.0); // Kp, Ki, Kd, max, min, integ_max + // Get Sensor Readings and return JSON object String getSensorReadings() { @@ -60,6 +64,9 @@ String getSensorReadings() readings["derived_overshoot"] = derivedOverShoot; readings["derived_undershoot"] = derivedUnderShoot; readings["mode"] = mode; + readings["pid"] = power; + readings["pwm_on"] = pwmSwitchDelayOn; + readings["pwm_off"] = pwmSwitchDelayOff; serializeJson(readings, jsonString); return jsonString; @@ -133,6 +140,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) { String target = message.substring(15, 18); targetTemp = target.toFloat(); + pid.setSetpoint(targetTemp); Serial.printf("\nTarget temp set to "); Serial.print(targetTemp); Serial.printf("°C"); @@ -147,6 +155,10 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) { mode = "pwm"; } + else if (message.indexOf("pid") > 0) + { + mode = "pid"; + } else { mode = "off"; @@ -265,6 +277,37 @@ void regulateRelais() } } } + if (mode == "pid") + { + // output = Kp×error + Ki×∫error×dt + Kd×(Δerror/Δt)`[^2][^5] + power = pid.compute(temperature); + pwmSwitchDelayOff = (pwmSwitchDelayOn / (power / 100)) - pwmSwitchDelayOn; + + if (pwmSwitchDelayOff > 0) + { + if ((millis() - lastSwitch) > pwmSwitchDelayOn) + { + if (digitalRead(relay) == HIGH) + { + digitalWrite(relay, LOW); + lastSwitch = millis(); + } + } + } + + if (pwmSwitchDelayOff > 0) + { + + if ((millis() - lastSwitch) > pwmSwitchDelayOff) + { + if (digitalRead(relay) == LOW) + { + digitalWrite(relay, HIGH); + lastSwitch = millis(); + } + } + } + } } void setup() @@ -282,6 +325,10 @@ void setup() server.serveStatic("/", LittleFS, "/"); + // Create PID controller instance + pid.setSetpoint(targetTemp); + pid.setDt(4); + ElegantOTA.begin(&server); // Start server diff --git a/src/pid.cpp b/src/pid.cpp new file mode 100644 index 0000000..f2eb8cd --- /dev/null +++ b/src/pid.cpp @@ -0,0 +1,60 @@ +#include "pid.h" + +double PIDController::clamp(double value, double min_val, double max_val) const +{ + if (value < min_val) + return min_val; + if (value > max_val) + return max_val; + return value; +} + +PIDController::PIDController(double Kp, double Ki, double Kd, + double max_out, double min_out, double integ_max) + : kp(Kp), ki(Ki), kd(Kd), setpoint(0.0), integral(0.0), + prev_error(0.0), max_output(max_out), min_output(min_out), + integral_max(integ_max), dt(0.1), first_run(true) {} + +void PIDController::setSetpoint(double sp) +{ + setpoint = sp; +} + +void PIDController::setDt(double sample_time) +{ + dt = sample_time; +} + +double PIDController::compute(double current_temp) +{ + double error = setpoint - current_temp; + + // Proportional + double p_term = kp * error; + + // Integral + anti-windup + integral += ki * error * dt; + integral = clamp(integral, -integral_max, integral_max); + + // Derivative + double d_term = 0.0; + if (!first_run) + { + d_term = kd * (error - prev_error) / dt; + } + first_run = false; + + // Output + double output = p_term + integral + d_term; + output = clamp(output, min_output, max_output); + + prev_error = error; + return output; +} + +void PIDController::reset() +{ + integral = 0.0; + prev_error = 0.0; + first_run = true; +} \ No newline at end of file diff --git a/src/pid.h b/src/pid.h new file mode 100644 index 0000000..3dfb307 --- /dev/null +++ b/src/pid.h @@ -0,0 +1,25 @@ +#ifndef PID_CONTROLLER_H +#define PID_CONTROLLER_H + +class PIDController +{ +private: + double kp, ki, kd, setpoint, integral, prev_error; + double max_output, min_output, integral_max, dt; + bool first_run; + + // Clamp function (C++11 compatible) + double clamp(double value, double min_val, double max_val) const; + +public: + PIDController(double Kp, double Ki, double Kd, + double max_out = 100.0, double min_out = 0.0, + double integ_max = 50.0); + + void setSetpoint(double sp); + void setDt(double sample_time); + double compute(double current_temp); + void reset(); +}; + +#endif \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e84cc07..70af113 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -16,25 +16,28 @@ const now = new Date(); - let mode = 'off'; - let temp = 300; - let relais = 0; - let targetTemp = 300; + let mode = $state('off'); + let temp = $state(300); + let relais = $state(0); + let targetTemp = $state(300); let setOverShoot = 20; let setUnderShoot = 20; - let targetTempOverShoot = targetTemp; - let targetTempUnderShoot = targetTemp; + let targetTempOverShoot = $state(targetTemp); + let targetTempUnderShoot = $state(targetTemp); + let calculatedPidOutput = $state(0); let lastSwitch = new Date(); let lastSwitchDuration = 0; let canvas: HTMLCanvasElement; - let pauseGraphUpdate = false; + let pauseGraphUpdate = $state(false); let spoofInterval = 250; let speedFactor = 250 / spoofInterval; const switchDelay = (30 / speedFactor) * 1000; + let pwmOn = $state(4); let pwmOnDelay = (4 / speedFactor) * 1000; + let pwmOff = $state(7); let pwmOffDelay = (7 / speedFactor) * 1000; let maxValues = 4 * 2 * 60 * 60; // 120 minutes (4 values/sec) @@ -352,6 +355,12 @@ targetTempOverShoot = myObj[key]; } else if (key === 'derived_undershoot') { targetTempUnderShoot = myObj[key]; + } else if (key === 'pid') { + calculatedPidOutput = myObj[key]; + } else if (key === 'pwm_on') { + pwmOn = Number(myObj[key]) / 1000; + } else if (key === 'pwm_off') { + pwmOff = Number(myObj[key]) / 1000; } else if (key === 'mode') { mode = myObj[key]; modeSpan.innerText = mode; @@ -402,6 +411,7 @@ const target = e.target as HTMLTextAreaElement; let value = Number(target?.value); console.log('Changed pwm on to ' + value); + pwmOn = value; if (!import.meta.env.DEV) { ws.send('setPWMOn: ' + pad(value, 3)); } else { @@ -413,6 +423,7 @@ const target = e.target as HTMLTextAreaElement; let value = Number(target?.value); console.log('Changed pwm off to ' + value); + pwmOff = value; if (!import.meta.env.DEV) { ws.send('setPWMOff: ' + pad(value, 3)); } else { @@ -486,7 +497,7 @@
- +
@@ -498,7 +509,7 @@ type="number" min="0" max="600" - placeholder="300" + value={targetTemp} id="target_temperature" onchange={changeTargetTemp} /> @@ -513,11 +524,12 @@

{#if mode === 'pwm'} -
+
on
off
@@ -528,7 +540,7 @@ type="number" min="0" max="120" - placeholder="4" + bind:value={pwmOn} id="pwm_on" onchange={changePwmOn} /> @@ -538,12 +550,24 @@ type="number" min="0" max="120" - placeholder="7" + bind:value={pwmOff} id="pwm_off" onchange={changePwmOff} />
+
+ {((pwmOn / (pwmOn + pwmOff)) * 100).toFixed(0)}%, {( + (pwmOn / (pwmOn + pwmOff)) * + 1200 + ).toFixed(0)}W +
+ {/if} + {#if mode === 'pid'} +
+ PID: {calculatedPidOutput.toFixed(0)}% {(calculatedPidOutput * 12).toFixed(0)}W +
+
ON:{pwmOn.toFixed(2)}s OFF:{pwmOff.toFixed(2)}s
{/if}
diff --git a/src/tmp.json b/src/tmp.json new file mode 100644 index 0000000..0e1c27e --- /dev/null +++ b/src/tmp.json @@ -0,0 +1,9 @@ +{ + "m.homeserver": { + "base_url": "tjet.nl" + }, + "org.matrix.msc2965.authentication": { + "issuer": "https://account.tjet.nl/", + "account": "https://account.tjet.nl/account/" + } +} \ No newline at end of file