mirror of
https://github.com/vincentvdwal/crust-craft.git
synced 2026-08-18 17:26:39 +02:00
add back in Kd
This commit is contained in:
+60
-1
@@ -352,6 +352,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.oven {
|
.oven {
|
||||||
|
pointer-events: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
@@ -420,7 +421,49 @@ p {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
height: 300px;
|
height: 340px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.3rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-btn {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 7px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.25rem 0.55rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
color 0.15s,
|
||||||
|
border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-btn:hover {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--mode-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-btn.active {
|
||||||
|
background: var(--mode-color);
|
||||||
|
border-color: var(--mode-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-section canvas {
|
.chart-section canvas {
|
||||||
@@ -548,6 +591,22 @@ p {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Kp / Ki / Kd grid – three equal columns, inputs wide enough for 0.0001 */
|
||||||
|
.k-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.6rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k-input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding-left: 0.45rem;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Action buttons ────────────────────────────────────── */
|
/* ── Action buttons ────────────────────────────────────── */
|
||||||
.action-btn {
|
.action-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
+25
-6
@@ -17,10 +17,11 @@ const MAX_HEAT_RATE = 14; // °C/s at peak of S-curve
|
|||||||
const NOISE_AMP = 0.35; // °C peak random noise
|
const NOISE_AMP = 0.35; // °C peak random noise
|
||||||
|
|
||||||
// PI-mode parameters – keep in sync with the firmware (main.cpp)
|
// PI-mode parameters – keep in sync with the firmware (main.cpp)
|
||||||
const PAUSE_TEMP = 275; // low keep-warm hold
|
const PAUSE_TEMP = 250; // low keep-warm hold
|
||||||
const BAKE_BOOST = 40; // °C added on top of target while baking
|
const BAKE_BOOST = 40; // °C added on top of target while baking
|
||||||
const BAKE_WAIT_MS = 60 * 1000; // 1 min warm-up before baking
|
const BAKE_WAIT_MS = 60 * 1000; // 1 min warm-up before baking
|
||||||
const BAKE_DURATION_MS = 3 * 60 * 1000; // 3 min bake, then back to preheat
|
const BAKE_DURATION_MS = 3 * 60 * 1000; // 3 min bake, then back to preheat
|
||||||
|
const D_FILTER = 0.8; // derivative low-pass smoothing (matches firmware)
|
||||||
|
|
||||||
/** S-curve heating factor: parabolic 4x(1-x), peaks at x = 0.5. */
|
/** S-curve heating factor: parabolic 4x(1-x), peaks at x = 0.5. */
|
||||||
function heatFactor(temp: number, target: number): number {
|
function heatFactor(temp: number, target: number): number {
|
||||||
@@ -63,20 +64,23 @@ export class SpoofSocket {
|
|||||||
mode: 'preheat',
|
mode: 'preheat',
|
||||||
temperature: ROOM_TEMP,
|
temperature: ROOM_TEMP,
|
||||||
relais: 0,
|
relais: 0,
|
||||||
target_temp: 460,
|
target_temp: 350,
|
||||||
pause_temp: PAUSE_TEMP,
|
pause_temp: PAUSE_TEMP,
|
||||||
bake_boost: BAKE_BOOST,
|
bake_boost: BAKE_BOOST,
|
||||||
pwm_on: 2000,
|
pwm_on: 2000,
|
||||||
pwm_off: 4000,
|
pwm_off: 4000,
|
||||||
pid: 0,
|
pid: 0,
|
||||||
kp: 0.6,
|
kp: 0.55,
|
||||||
ki: 0.03,
|
ki: 0.005,
|
||||||
kd: 0.0,
|
kd: 0.0,
|
||||||
bake_phase: '',
|
bake_phase: '',
|
||||||
bake_remaining: 0
|
bake_remaining: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
private integral = 0;
|
private integral = 0;
|
||||||
|
private dFilt = 0;
|
||||||
|
private prevTemp = ROOM_TEMP;
|
||||||
|
private firstRun = true;
|
||||||
private lastSwitch = Date.now();
|
private lastSwitch = Date.now();
|
||||||
private bakeStart = 0;
|
private bakeStart = 0;
|
||||||
private ticker: ReturnType<typeof setInterval> | null = null;
|
private ticker: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -136,7 +140,11 @@ export class SpoofSocket {
|
|||||||
}
|
}
|
||||||
// Reset integral arriving from a non-PID mode (stale value) or when the
|
// Reset integral arriving from a non-PID mode (stale value) or when the
|
||||||
// new target is below the current temp (mirrors firmware applyMode).
|
// new target is below the current temp (mirrors firmware applyMode).
|
||||||
if (this.isPidMode(m) && (!wasPid || this.activeSetpoint() < s.temperature)) this.integral = 0;
|
if (this.isPidMode(m) && (!wasPid || this.activeSetpoint() < s.temperature)) {
|
||||||
|
this.integral = 0;
|
||||||
|
this.dFilt = 0;
|
||||||
|
this.firstRun = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handle(cmd: string, value?: number | string): void {
|
private handle(cmd: string, value?: number | string): void {
|
||||||
@@ -213,7 +221,18 @@ export class SpoofSocket {
|
|||||||
// integral clamped to ±70 to match the firmware (anti-overshoot)
|
// integral clamped to ±70 to match the firmware (anti-overshoot)
|
||||||
this.integral = Math.max(-70, Math.min(70, this.integral + s.ki * error * dt));
|
this.integral = Math.max(-70, Math.min(70, this.integral + s.ki * error * dt));
|
||||||
}
|
}
|
||||||
output = Math.max(0, Math.min(100, pTerm + this.integral));
|
|
||||||
|
// Filtered derivative on measurement (mirrors PIDController::compute)
|
||||||
|
let dTerm = 0;
|
||||||
|
if (!this.firstRun) {
|
||||||
|
const deriv = (-s.kd * (s.temperature - this.prevTemp)) / dt;
|
||||||
|
this.dFilt = D_FILTER * this.dFilt + (1 - D_FILTER) * deriv;
|
||||||
|
dTerm = this.dFilt;
|
||||||
|
}
|
||||||
|
this.firstRun = false;
|
||||||
|
this.prevTemp = s.temperature;
|
||||||
|
|
||||||
|
output = Math.max(0, Math.min(100, pTerm + this.integral + dTerm));
|
||||||
s.pid = output;
|
s.pid = output;
|
||||||
|
|
||||||
if (output >= 100) {
|
if (output >= 100) {
|
||||||
|
|||||||
+4
-4
@@ -35,9 +35,9 @@ float pwmSwitchDelayOff = 4000; // 4s
|
|||||||
|
|
||||||
float power = 100;
|
float power = 100;
|
||||||
|
|
||||||
float kp = 0.6;
|
float kp = 0.55;
|
||||||
float ki = 0.03;
|
float ki = 0.005;
|
||||||
float kd = 0.0;
|
float kd = 0.5;
|
||||||
|
|
||||||
// Heating modes. PREHEAT, PAUSE and BAKING are all PI-regulated and only
|
// Heating modes. PREHEAT, PAUSE and BAKING are all PI-regulated and only
|
||||||
// differ in the setpoint they drive towards. The oven always boots in PREHEAT
|
// differ in the setpoint they drive towards. The oven always boots in PREHEAT
|
||||||
@@ -46,7 +46,7 @@ enum class Mode { MANUAL, PWM, PREHEAT, PAUSE, BAKING };
|
|||||||
Mode mode = Mode::PREHEAT;
|
Mode mode = Mode::PREHEAT;
|
||||||
|
|
||||||
// Setpoints / timing for the PI-based modes (pauseTemp / bakeBoost are UI-adjustable)
|
// Setpoints / timing for the PI-based modes (pauseTemp / bakeBoost are UI-adjustable)
|
||||||
float pauseTemp = 275.0f; // low "keep-warm" hold
|
float pauseTemp = 250.0f; // low "keep-warm" hold
|
||||||
float bakeBoost = 40.0f; // °C added on top of target
|
float bakeBoost = 40.0f; // °C added on top of target
|
||||||
constexpr unsigned long BAKE_WAIT_MS = 60UL * 1000; // 1 min warm-up before baking
|
constexpr unsigned long BAKE_WAIT_MS = 60UL * 1000; // 1 min warm-up before baking
|
||||||
constexpr unsigned long BAKE_DURATION_MS = 3UL * 60 * 1000; // 3 min bake, then -> PREHEAT
|
constexpr unsigned long BAKE_DURATION_MS = 3UL * 60 * 1000; // 3 min bake, then -> PREHEAT
|
||||||
|
|||||||
+22
-6
@@ -1,5 +1,9 @@
|
|||||||
#include "pid.h"
|
#include "pid.h"
|
||||||
|
|
||||||
|
// Low-pass smoothing for the derivative term (0 = none, →1 = heavy). The
|
||||||
|
// thermocouple reads in 0.25 °C steps, so the raw derivative is very noisy.
|
||||||
|
static constexpr double D_FILTER = 0.8;
|
||||||
|
|
||||||
double PIDController::clamp(double value, double min_val, double max_val) const
|
double PIDController::clamp(double value, double min_val, double max_val) const
|
||||||
{
|
{
|
||||||
if (value < min_val)
|
if (value < min_val)
|
||||||
@@ -12,8 +16,8 @@ double PIDController::clamp(double value, double min_val, double max_val) const
|
|||||||
PIDController::PIDController(double Kp, double Ki, double Kd,
|
PIDController::PIDController(double Kp, double Ki, double Kd,
|
||||||
double max_out, double min_out, double integ_max)
|
double max_out, double min_out, double integ_max)
|
||||||
: kp(Kp), ki(Ki), kd(Kd), setpoint(0.0), integral(0.0),
|
: kp(Kp), ki(Ki), kd(Kd), setpoint(0.0), integral(0.0),
|
||||||
prev_error(0.0), max_output(max_out), min_output(min_out),
|
prev_measurement(0.0), d_filtered(0.0), max_output(max_out),
|
||||||
integral_max(integ_max), dt(0.1), first_run(true) {}
|
min_output(min_out), integral_max(integ_max), dt(0.1), first_run(true) {}
|
||||||
|
|
||||||
void PIDController::setSetpoint(double sp)
|
void PIDController::setSetpoint(double sp)
|
||||||
{
|
{
|
||||||
@@ -63,16 +67,28 @@ double PIDController::compute(double current_temp)
|
|||||||
integral = clamp(integral, -integral_max, integral_max);
|
integral = clamp(integral, -integral_max, integral_max);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output
|
// Derivative on the *measurement* (not the error) so a setpoint change
|
||||||
output = clamp(p_term + integral, min_output, max_output);
|
// doesn't cause a derivative kick, low-pass filtered to tame sensor noise.
|
||||||
|
// d(error)/dt = -d(temp)/dt for a fixed setpoint, hence the leading minus.
|
||||||
|
double d_term = 0.0;
|
||||||
|
if (!first_run && dt > 0.0)
|
||||||
|
{
|
||||||
|
double deriv = -kd * (current_temp - prev_measurement) / dt;
|
||||||
|
d_filtered = D_FILTER * d_filtered + (1.0 - D_FILTER) * deriv;
|
||||||
|
d_term = d_filtered;
|
||||||
|
}
|
||||||
|
first_run = false;
|
||||||
|
prev_measurement = current_temp;
|
||||||
|
|
||||||
prev_error = error;
|
// Output
|
||||||
|
output = clamp(p_term + integral + d_term, min_output, max_output);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PIDController::reset()
|
void PIDController::reset()
|
||||||
{
|
{
|
||||||
integral = 0.0;
|
integral = 0.0;
|
||||||
prev_error = 0.0;
|
prev_measurement = 0.0;
|
||||||
|
d_filtered = 0.0;
|
||||||
first_run = true;
|
first_run = true;
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
class PIDController
|
class PIDController
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
double kp, ki, kd, setpoint, integral, prev_error;
|
double kp, ki, kd, setpoint, integral, prev_measurement, d_filtered;
|
||||||
double max_output, min_output, integral_max, dt;
|
double max_output, min_output, integral_max, dt;
|
||||||
bool first_run;
|
bool first_run;
|
||||||
|
|
||||||
|
|||||||
+150
-37
@@ -27,9 +27,19 @@
|
|||||||
Tooltip
|
Tooltip
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rolling window for the chart (~5 min at 2 pts/sec)
|
// Chart view ranges (minutes); 'all' = the whole stored history.
|
||||||
const CHART_WINDOW = 600;
|
const POINTS_PER_MIN = 120; // 2 readings/sec × 60
|
||||||
// Full buffer kept for CSV export (24h at 2 pts/sec)
|
const MAX_RENDER_POINTS = 1500; // points actually drawn; longer ranges get downsampled
|
||||||
|
const RANGES = [
|
||||||
|
{ label: '5m', value: 5 },
|
||||||
|
{ label: '10m', value: 10 },
|
||||||
|
{ label: '20m', value: 20 },
|
||||||
|
{ label: '30m', value: 30 },
|
||||||
|
{ label: '60m', value: 60 },
|
||||||
|
{ label: 'All', value: 'all' }
|
||||||
|
] as const;
|
||||||
|
type RangeValue = (typeof RANGES)[number]['value'];
|
||||||
|
// Full buffer kept for CSV export + as the chart source (24h at 2 pts/sec)
|
||||||
const MAX_STORED = 24 * 2 * 60 * 60;
|
const MAX_STORED = 24 * 2 * 60 * 60;
|
||||||
|
|
||||||
let gateway = `ws://${page.url.host}/ws`;
|
let gateway = `ws://${page.url.host}/ws`;
|
||||||
@@ -38,21 +48,23 @@
|
|||||||
let mode = $state('preheat');
|
let mode = $state('preheat');
|
||||||
let temp = $state(0);
|
let temp = $state(0);
|
||||||
let relais = $state(0);
|
let relais = $state(0);
|
||||||
let targetTemp = $state(460);
|
let targetTemp = $state(350);
|
||||||
let calculatedPidOutput = $state(0);
|
let calculatedPidOutput = $state(0);
|
||||||
let bakeRemaining = $state(0);
|
let bakeRemaining = $state(0);
|
||||||
let bakePhase = $state('');
|
let bakePhase = $state('');
|
||||||
let pauseTemp = $state(275);
|
let pauseTemp = $state(250);
|
||||||
let bakeBoost = $state(40);
|
let bakeBoost = $state(40);
|
||||||
|
|
||||||
let pwmOn = $state(2);
|
let pwmOn = $state(2);
|
||||||
let pwmOff = $state(4);
|
let pwmOff = $state(4);
|
||||||
|
|
||||||
let kp = $state(0.6);
|
let kp = $state(0.55);
|
||||||
let ki = $state(0.03);
|
let ki = $state(0.005);
|
||||||
|
let kd = $state(0.0);
|
||||||
|
|
||||||
let pauseGraphUpdate = $state(false);
|
let pauseGraphUpdate = $state(false);
|
||||||
let darkMode = $state(true);
|
let darkMode = $state(true);
|
||||||
|
let chartRange = $state<RangeValue>(5);
|
||||||
|
|
||||||
// ── Modes ──────────────────────────────────────────────
|
// ── Modes ──────────────────────────────────────────────
|
||||||
// Preheat / Pause / Baking are all PI-regulated on the firmware; they only
|
// Preheat / Pause / Baking are all PI-regulated on the firmware; they only
|
||||||
@@ -182,10 +194,11 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Full history – only used for CSV export
|
// Full history – source for both the chart and the CSV export
|
||||||
let temperatureData: Array<[number, number]> = [];
|
let temperatureData: Array<[number, number]> = [];
|
||||||
let relaisData: Array<[number, number]> = [];
|
let relaisData: Array<[number, number]> = [];
|
||||||
let powerData: Array<[number, number]> = [];
|
let powerData: Array<[number, number]> = [];
|
||||||
|
let targetData: Array<[number, number]> = [];
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
let downloadCSVButton: HTMLAnchorElement;
|
let downloadCSVButton: HTMLAnchorElement;
|
||||||
@@ -198,6 +211,84 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pushCapped = (arr: Array<[number, number]>, t: number, v: number) => {
|
||||||
|
if (arr.length >= MAX_STORED) arr.shift();
|
||||||
|
arr.push([t, v]);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Pt = { x: number; y: number };
|
||||||
|
|
||||||
|
// Build a chart series from the raw history slice src[start..], downsampling
|
||||||
|
// to at most `maxPoints` by keeping each bucket's min AND max so peaks (e.g.
|
||||||
|
// overshoot spikes) are never hidden. Reads the source directly so only the
|
||||||
|
// (small) output is allocated, even for the "All" range.
|
||||||
|
const buildSeries = (src: Array<[number, number]>, start: number, maxPoints: number): Pt[] => {
|
||||||
|
const count = src.length - start;
|
||||||
|
if (count <= 0) return [];
|
||||||
|
if (count <= maxPoints) {
|
||||||
|
const out: Pt[] = new Array(count);
|
||||||
|
for (let i = 0; i < count; i++) out[i] = { x: src[start + i][0], y: src[start + i][1] };
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const buckets = Math.max(1, Math.floor(maxPoints / 2));
|
||||||
|
const size = count / buckets;
|
||||||
|
const out: Pt[] = [];
|
||||||
|
for (let b = 0; b < buckets; b++) {
|
||||||
|
const s = start + Math.floor(b * size);
|
||||||
|
const e = start + Math.min(count, Math.floor((b + 1) * size));
|
||||||
|
let mnI = s;
|
||||||
|
let mxI = s;
|
||||||
|
for (let i = s + 1; i < e; i++) {
|
||||||
|
if (src[i][1] < src[mnI][1]) mnI = i;
|
||||||
|
if (src[i][1] > src[mxI][1]) mxI = i;
|
||||||
|
}
|
||||||
|
const a = Math.min(mnI, mxI); // emit in time order
|
||||||
|
const z = Math.max(mnI, mxI);
|
||||||
|
out.push({ x: src[a][0], y: src[a][1] });
|
||||||
|
if (z !== a) out.push({ x: src[z][0], y: src[z][1] });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rebuild the chart datasets from the stored history for the selected range,
|
||||||
|
// then zoom the y-axis to the visible temperature so small oscillations are
|
||||||
|
// readable.
|
||||||
|
const refreshChart = () => {
|
||||||
|
if (!chart) return;
|
||||||
|
const total = temperatureData.length;
|
||||||
|
const count = chartRange === 'all' ? total : Math.min(total, chartRange * POINTS_PER_MIN);
|
||||||
|
const start = total - count;
|
||||||
|
|
||||||
|
chart.data.datasets[0].data = buildSeries(temperatureData, start, MAX_RENDER_POINTS);
|
||||||
|
chart.data.datasets[1].data = buildSeries(targetData, start, MAX_RENDER_POINTS);
|
||||||
|
chart.data.datasets[2].data = buildSeries(relaisData, start, MAX_RENDER_POINTS);
|
||||||
|
|
||||||
|
// Zoom y tight around the temperature actually on screen (+ a little pad).
|
||||||
|
let lo = Infinity;
|
||||||
|
let hi = -Infinity;
|
||||||
|
for (const p of chart.data.datasets[0].data as Pt[]) {
|
||||||
|
if (p.y < lo) lo = p.y;
|
||||||
|
if (p.y > hi) hi = p.y;
|
||||||
|
}
|
||||||
|
const yScale = chart.options.scales!.y as { min?: number; max?: number };
|
||||||
|
if (lo === Infinity) {
|
||||||
|
yScale.min = 0;
|
||||||
|
yScale.max = undefined;
|
||||||
|
} else {
|
||||||
|
const span = Math.max(hi - lo, 8); // never zoom tighter than ~8°C
|
||||||
|
const pad = span * 0.15;
|
||||||
|
yScale.min = Math.max(0, Math.floor(lo - pad));
|
||||||
|
yScale.max = Math.ceil(hi + pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.update('none');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setRange = (v: RangeValue) => {
|
||||||
|
chartRange = v;
|
||||||
|
refreshChart();
|
||||||
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
chart = new Chart(canvas, {
|
chart = new Chart(canvas, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -216,13 +307,19 @@
|
|||||||
type: 'time',
|
type: 'time',
|
||||||
time: {
|
time: {
|
||||||
tooltipFormat: 'HH:mm:ss',
|
tooltipFormat: 'HH:mm:ss',
|
||||||
displayFormats: { minute: 'HH:mm', hour: 'HH:mm' }
|
// 24-hour formats for every unit so no am/pm leaks in
|
||||||
|
displayFormats: {
|
||||||
|
millisecond: 'HH:mm:ss',
|
||||||
|
second: 'HH:mm:ss',
|
||||||
|
minute: 'HH:mm',
|
||||||
|
hour: 'HH:mm'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
ticks: { color: '#7a7f96' },
|
ticks: { color: '#7a7f96' },
|
||||||
grid: { color: 'rgba(255,255,255,0.05)' }
|
grid: { color: 'rgba(255,255,255,0.05)' }
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
min: 0,
|
// min/max are set dynamically in refreshChart() to zoom on the data
|
||||||
ticks: { color: '#7a7f96' },
|
ticks: { color: '#7a7f96' },
|
||||||
grid: { color: 'rgba(255,255,255,0.05)' }
|
grid: { color: 'rgba(255,255,255,0.05)' }
|
||||||
},
|
},
|
||||||
@@ -323,6 +420,7 @@
|
|||||||
calculatedPidOutput = data.pid;
|
calculatedPidOutput = data.pid;
|
||||||
kp = data.kp;
|
kp = data.kp;
|
||||||
ki = data.ki;
|
ki = data.ki;
|
||||||
|
kd = data.kd ?? 0;
|
||||||
bakeRemaining = data.bake_remaining ?? 0;
|
bakeRemaining = data.bake_remaining ?? 0;
|
||||||
bakePhase = data.bake_phase ?? '';
|
bakePhase = data.bake_phase ?? '';
|
||||||
pauseTemp = data.pause_temp ?? pauseTemp;
|
pauseTemp = data.pause_temp ?? pauseTemp;
|
||||||
@@ -330,30 +428,20 @@
|
|||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (temperatureData.length >= MAX_STORED) temperatureData.shift();
|
// Effective setpoint the oven is actually driving towards (boost / pause).
|
||||||
temperatureData.push([now, temp]);
|
const effTarget =
|
||||||
if (relaisData.length >= MAX_STORED) relaisData.shift();
|
data.mode === 'baking'
|
||||||
relaisData.push([now, relais]);
|
? data.target_temp + (data.bake_boost ?? bakeBoost)
|
||||||
if (powerData.length >= MAX_STORED) powerData.shift();
|
: data.mode === 'pause'
|
||||||
powerData.push([now, calculatedPidOutput]);
|
? (data.pause_temp ?? pauseTemp)
|
||||||
|
: data.target_temp;
|
||||||
|
|
||||||
if (!pauseGraphUpdate && chart) {
|
pushCapped(temperatureData, now, temp);
|
||||||
const tempSeries = chart.data.datasets[0].data as { x: number; y: number }[];
|
pushCapped(relaisData, now, data.relais);
|
||||||
const targetSeries = chart.data.datasets[1].data as { x: number; y: number }[];
|
pushCapped(powerData, now, calculatedPidOutput);
|
||||||
const relaySeries = chart.data.datasets[2].data as { x: number; y: number }[];
|
pushCapped(targetData, now, effTarget);
|
||||||
|
|
||||||
tempSeries.push({ x: now, y: temp });
|
if (!pauseGraphUpdate) refreshChart();
|
||||||
targetSeries.push({ x: now, y: targetTemp });
|
|
||||||
relaySeries.push({ x: now, y: data.relais });
|
|
||||||
|
|
||||||
if (tempSeries.length > CHART_WINDOW) {
|
|
||||||
tempSeries.shift();
|
|
||||||
targetSeries.shift();
|
|
||||||
relaySeries.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
chart.update('none');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchRelais = () => send('switchRelais');
|
const switchRelais = () => send('switchRelais');
|
||||||
@@ -510,7 +598,19 @@
|
|||||||
|
|
||||||
<!-- Chart -->
|
<!-- Chart -->
|
||||||
<div class="chart-section">
|
<div class="chart-section">
|
||||||
<canvas bind:this={canvas} id="crusty"></canvas>
|
<div class="chart-toolbar">
|
||||||
|
{#each RANGES as r (r.value)}
|
||||||
|
<button
|
||||||
|
class="range-btn {chartRange === r.value ? 'active' : ''}"
|
||||||
|
onclick={() => setRange(r.value)}
|
||||||
|
>
|
||||||
|
{r.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="chart-canvas-wrap">
|
||||||
|
<canvas bind:this={canvas} id="crusty"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controls -->
|
<!-- Controls -->
|
||||||
@@ -631,11 +731,11 @@
|
|||||||
<p class="subinfo mt-3">
|
<p class="subinfo mt-3">
|
||||||
ON {pwmOn.toFixed(2)} s / OFF {pwmOff ? pwmOff.toFixed(2) : 0} s
|
ON {pwmOn.toFixed(2)} s / OFF {pwmOff ? pwmOff.toFixed(2) : 0} s
|
||||||
</p>
|
</p>
|
||||||
<div class="subcontrol-grid mt-3">
|
<div class="k-grid mt-3">
|
||||||
<div class="subcontrol">
|
<div class="subcontrol">
|
||||||
<label class="sublabel" for="kp">Kp</label>
|
<label class="sublabel" for="kp">Kp</label>
|
||||||
<input
|
<input
|
||||||
class="num-input"
|
class="num-input k-input"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="50"
|
max="50"
|
||||||
@@ -648,16 +748,29 @@
|
|||||||
<div class="subcontrol">
|
<div class="subcontrol">
|
||||||
<label class="sublabel" for="ki">Ki</label>
|
<label class="sublabel" for="ki">Ki</label>
|
||||||
<input
|
<input
|
||||||
class="num-input"
|
class="num-input k-input"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="50"
|
max="50"
|
||||||
step="0.1"
|
step="0.0001"
|
||||||
value={ki}
|
value={ki}
|
||||||
id="ki"
|
id="ki"
|
||||||
onchange={(e) => changeKValue(e, 'i')}
|
onchange={(e) => changeKValue(e, 'i')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="subcontrol">
|
||||||
|
<label class="sublabel" for="kd">Kd</label>
|
||||||
|
<input
|
||||||
|
class="num-input k-input"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="50"
|
||||||
|
step="0.0001"
|
||||||
|
value={kd}
|
||||||
|
id="kd"
|
||||||
|
onchange={(e) => changeKValue(e, 'd')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user