This commit is contained in:
Vincent van der Wal
2025-12-15 20:52:25 +01:00
parent b60e89f74d
commit a4dd7160aa
6 changed files with 181 additions and 14 deletions
+2
View File
@@ -1,4 +1,6 @@
{ {
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [ "recommendations": [
"platformio.platformio-ide" "platformio.platformio-ide"
], ],
+48 -1
View File
@@ -8,6 +8,7 @@
#include <ElegantOTA.h> #include <ElegantOTA.h>
#include "secrets.h" #include "secrets.h"
#include "pid.h"
AsyncWebServer server(80); AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); AsyncWebSocket ws("/ws");
@@ -18,7 +19,7 @@ String jsonString;
// Timer variables // Timer variables
unsigned long lastTime = 0; unsigned long lastTime = 0;
unsigned long timerDelay = 250; unsigned long timerDelay = 500;
int relay = 21; int relay = 21;
int thermoDO = 19; int thermoDO = 19;
@@ -37,9 +38,12 @@ unsigned long lastSwitch = 0;
unsigned long autoSwitchDelay = 20000; // 20s unsigned long autoSwitchDelay = 20000; // 20s
float pwmSwitchDelayOn = 4000; // 4s float pwmSwitchDelayOn = 4000; // 4s
float pwmSwitchDelayOff = 7000; // 7s float pwmSwitchDelayOff = 7000; // 7s
float power = 0;
String mode = "off"; 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 // Get Sensor Readings and return JSON object
String getSensorReadings() String getSensorReadings()
{ {
@@ -60,6 +64,9 @@ String getSensorReadings()
readings["derived_overshoot"] = derivedOverShoot; readings["derived_overshoot"] = derivedOverShoot;
readings["derived_undershoot"] = derivedUnderShoot; readings["derived_undershoot"] = derivedUnderShoot;
readings["mode"] = mode; readings["mode"] = mode;
readings["pid"] = power;
readings["pwm_on"] = pwmSwitchDelayOn;
readings["pwm_off"] = pwmSwitchDelayOff;
serializeJson(readings, jsonString); serializeJson(readings, jsonString);
return jsonString; return jsonString;
@@ -133,6 +140,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
{ {
String target = message.substring(15, 18); String target = message.substring(15, 18);
targetTemp = target.toFloat(); targetTemp = target.toFloat();
pid.setSetpoint(targetTemp);
Serial.printf("\nTarget temp set to "); Serial.printf("\nTarget temp set to ");
Serial.print(targetTemp); Serial.print(targetTemp);
Serial.printf("°C"); Serial.printf("°C");
@@ -147,6 +155,10 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
{ {
mode = "pwm"; mode = "pwm";
} }
else if (message.indexOf("pid") > 0)
{
mode = "pid";
}
else else
{ {
mode = "off"; 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() void setup()
@@ -282,6 +325,10 @@ void setup()
server.serveStatic("/", LittleFS, "/"); server.serveStatic("/", LittleFS, "/");
// Create PID controller instance
pid.setSetpoint(targetTemp);
pid.setDt(4);
ElegantOTA.begin(&server); ElegantOTA.begin(&server);
// Start server // Start server
+60
View File
@@ -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;
}
+25
View File
@@ -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
+36 -12
View File
@@ -16,25 +16,28 @@
const now = new Date(); const now = new Date();
let mode = 'off'; let mode = $state('off');
let temp = 300; let temp = $state(300);
let relais = 0; let relais = $state(0);
let targetTemp = 300; let targetTemp = $state(300);
let setOverShoot = 20; let setOverShoot = 20;
let setUnderShoot = 20; let setUnderShoot = 20;
let targetTempOverShoot = targetTemp; let targetTempOverShoot = $state(targetTemp);
let targetTempUnderShoot = targetTemp; let targetTempUnderShoot = $state(targetTemp);
let calculatedPidOutput = $state(0);
let lastSwitch = new Date(); let lastSwitch = new Date();
let lastSwitchDuration = 0; let lastSwitchDuration = 0;
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
let pauseGraphUpdate = false; let pauseGraphUpdate = $state(false);
let spoofInterval = 250; let spoofInterval = 250;
let speedFactor = 250 / spoofInterval; let speedFactor = 250 / spoofInterval;
const switchDelay = (30 / speedFactor) * 1000; const switchDelay = (30 / speedFactor) * 1000;
let pwmOn = $state(4);
let pwmOnDelay = (4 / speedFactor) * 1000; let pwmOnDelay = (4 / speedFactor) * 1000;
let pwmOff = $state(7);
let pwmOffDelay = (7 / speedFactor) * 1000; let pwmOffDelay = (7 / speedFactor) * 1000;
let maxValues = 4 * 2 * 60 * 60; // 120 minutes (4 values/sec) let maxValues = 4 * 2 * 60 * 60; // 120 minutes (4 values/sec)
@@ -352,6 +355,12 @@
targetTempOverShoot = myObj[key]; targetTempOverShoot = myObj[key];
} else if (key === 'derived_undershoot') { } else if (key === 'derived_undershoot') {
targetTempUnderShoot = myObj[key]; 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') { } else if (key === 'mode') {
mode = myObj[key]; mode = myObj[key];
modeSpan.innerText = mode; modeSpan.innerText = mode;
@@ -402,6 +411,7 @@
const target = e.target as HTMLTextAreaElement; const target = e.target as HTMLTextAreaElement;
let value = Number(target?.value); let value = Number(target?.value);
console.log('Changed pwm on to ' + value); console.log('Changed pwm on to ' + value);
pwmOn = value;
if (!import.meta.env.DEV) { if (!import.meta.env.DEV) {
ws.send('setPWMOn: ' + pad(value, 3)); ws.send('setPWMOn: ' + pad(value, 3));
} else { } else {
@@ -413,6 +423,7 @@
const target = e.target as HTMLTextAreaElement; const target = e.target as HTMLTextAreaElement;
let value = Number(target?.value); let value = Number(target?.value);
console.log('Changed pwm off to ' + value); console.log('Changed pwm off to ' + value);
pwmOff = value;
if (!import.meta.env.DEV) { if (!import.meta.env.DEV) {
ws.send('setPWMOff: ' + pad(value, 3)); ws.send('setPWMOff: ' + pad(value, 3));
} else { } else {
@@ -486,7 +497,7 @@
<div class="graph"> <div class="graph">
<div class="plot-wrapper w-[800px]"> <div class="plot-wrapper w-[800px]">
<div id="plot"></div> <div id="plot"></div>
<canvas bind:this={canvas} id="acquisitions"></canvas> <canvas bind:this={canvas} id="crusty"></canvas>
</div> </div>
</div> </div>
<div class="card-grid settings"> <div class="card-grid settings">
@@ -498,7 +509,7 @@
type="number" type="number"
min="0" min="0"
max="600" max="600"
placeholder="300" value={targetTemp}
id="target_temperature" id="target_temperature"
onchange={changeTargetTemp} onchange={changeTargetTemp}
/> />
@@ -513,11 +524,12 @@
<select class="rounded" name="modes" id="mode_selector" onchange={changeMode}> <select class="rounded" name="modes" id="mode_selector" onchange={changeMode}>
<option value="auto_switch">Auto Switch</option> <option value="auto_switch">Auto Switch</option>
<option value="pwm">PWM</option> <option value="pwm">PWM</option>
<option value="pid">PID</option>
<option value="off" selected>Manual</option> <option value="off" selected>Manual</option>
</select> </select>
</p> </p>
{#if mode === 'pwm'} {#if mode === 'pwm'}
<div class="gap-15 mt-2 flex items-center"> <div class="mt-2 flex items-center gap-15">
<div>on</div> <div>on</div>
<div>off</div> <div>off</div>
</div> </div>
@@ -528,7 +540,7 @@
type="number" type="number"
min="0" min="0"
max="120" max="120"
placeholder="4" bind:value={pwmOn}
id="pwm_on" id="pwm_on"
onchange={changePwmOn} onchange={changePwmOn}
/> />
@@ -538,12 +550,24 @@
type="number" type="number"
min="0" min="0"
max="120" max="120"
placeholder="7" bind:value={pwmOff}
id="pwm_off" id="pwm_off"
onchange={changePwmOff} onchange={changePwmOff}
/> />
<label for="pwm_off"> s </label> <label for="pwm_off"> s </label>
</div> </div>
<div class="mt-3 flex items-center gap-1">
{((pwmOn / (pwmOn + pwmOff)) * 100).toFixed(0)}%, {(
(pwmOn / (pwmOn + pwmOff)) *
1200
).toFixed(0)}W
</div>
{/if}
{#if mode === 'pid'}
<div class="mt-3 flex items-center gap-1">
PID: {calculatedPidOutput.toFixed(0)}% {(calculatedPidOutput * 12).toFixed(0)}W
</div>
<div>ON:{pwmOn.toFixed(2)}s OFF:{pwmOff.toFixed(2)}s</div>
{/if} {/if}
</div> </div>
<div class="card flex cursor-pointer gap-2"> <div class="card flex cursor-pointer gap-2">
+9
View File
@@ -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/"
}
}