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": [
"platformio.platformio-ide"
],
+48 -1
View File
@@ -8,6 +8,7 @@
#include <ElegantOTA.h>
#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
+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();
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 @@
<div class="graph">
<div class="plot-wrapper w-[800px]">
<div id="plot"></div>
<canvas bind:this={canvas} id="acquisitions"></canvas>
<canvas bind:this={canvas} id="crusty"></canvas>
</div>
</div>
<div class="card-grid settings">
@@ -498,7 +509,7 @@
type="number"
min="0"
max="600"
placeholder="300"
value={targetTemp}
id="target_temperature"
onchange={changeTargetTemp}
/>
@@ -513,11 +524,12 @@
<select class="rounded" name="modes" id="mode_selector" onchange={changeMode}>
<option value="auto_switch">Auto Switch</option>
<option value="pwm">PWM</option>
<option value="pid">PID</option>
<option value="off" selected>Manual</option>
</select>
</p>
{#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>off</div>
</div>
@@ -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}
/>
<label for="pwm_off"> s </label>
</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}
</div>
<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/"
}
}