Skip to content

Commit e992f5b

Browse files
committed
Added PID functionality
1 parent 4d53619 commit e992f5b

File tree

8 files changed

+277
-84
lines changed

8 files changed

+277
-84
lines changed

data/fans.html

Lines changed: 185 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
border-left-color: #21f32f;
4444
}
4545

46-
.sidebar .logo-container {
46+
.sidebar .logo-container {
4747
text-align: center;
4848
margin-bottom: 20px;
4949
}
@@ -76,13 +76,24 @@
7676
}
7777

7878
.chart-controls-container {
79-
margin: 0 20px 0 20px ;
80-
}
79+
margin: 10px 20px 0 20px ;
80+
}
8181

82-
.chart-controls-container select {
83-
margin: 0 5px 10px 0;
84-
}
82+
.chart-controls-container select {
83+
margin: 0 5px 10px 0;
84+
}
85+
86+
.pid-controls {
87+
margin: 10px 20px 0 20px ;
88+
padding: 10px 10px 0px 10px ;
89+
border: 1px dotted #000;
90+
display: none;
91+
}
8592

93+
.pid-controls input {
94+
display: inline-block;
95+
margin-bottom: 10px ;
96+
}
8697

8798
.chart-container {
8899
display: inline-block;
@@ -129,7 +140,68 @@
129140
border: none;
130141
border-radius: 4px;
131142
cursor: pointer;
132-
}
143+
}
144+
145+
146+
.switch {
147+
position: relative;
148+
display: inline-block;
149+
width: 60px;
150+
height: 34px;
151+
}
152+
153+
.switch input {
154+
opacity: 0;
155+
width: 0;
156+
height: 0;
157+
}
158+
159+
.slider {
160+
position: absolute;
161+
cursor: pointer;
162+
top: 0;
163+
left: 0;
164+
right: 0;
165+
bottom: 0;
166+
background-color: #ccc;
167+
-webkit-transition: .4s;
168+
transition: .4s;
169+
}
170+
171+
.slider:before {
172+
position: absolute;
173+
content: "";
174+
height: 26px;
175+
width: 26px;
176+
left: 4px;
177+
bottom: 4px;
178+
background-color: white;
179+
-webkit-transition: .4s;
180+
transition: .4s;
181+
}
182+
183+
input:checked + .slider {
184+
background-color: #2196F3;
185+
}
186+
187+
input:focus + .slider {
188+
box-shadow: 0 0 1px #2196F3;
189+
}
190+
191+
input:checked + .slider:before {
192+
-webkit-transform: translateX(26px);
193+
-ms-transform: translateX(26px);
194+
transform: translateX(26px);
195+
}
196+
197+
/* Rounded sliders */
198+
.slider.round {
199+
border-radius: 34px;
200+
}
201+
202+
.slider.round:before {
203+
border-radius: 50%;
204+
}
133205
</style>
134206
</head>
135207
<body>
@@ -184,8 +256,21 @@ <h1>Fan Curves</h1>
184256
}
185257

186258
for (const key in fan_data) {
187-
createChart(key, fan_data[key]['curves'], fan_data[key]['sensor'], fan_data[key]['temp_th'], fan_data[key]['duty_th'], fan_data[key]['sud_dur'], fan_data[key]['halt_on'], units);
259+
createChart(key, fan_data[key]);
188260
}
261+
262+
$('.mode-toggle').change(function() {
263+
var fanId = $(this).data('fan');
264+
if (this.checked) {
265+
$(`#chart-${fanId}`).hide();
266+
$(`#pid-controls-${fanId}`).css("display", "inline-block");
267+
fan_data[fanId]['mode'] = 'pid';
268+
} else {
269+
$(`#chart-${fanId}`).show();
270+
$(`#pid-controls-${fanId}`).hide();
271+
fan_data[fanId]['mode'] = 'curve';
272+
}
273+
});
189274
}
190275
});
191276

@@ -197,6 +282,26 @@ <h1>Fan Curves</h1>
197282
fan_data['FAN_2']['sensor'] = $('#sensor_id-FAN_2').val();
198283
fan_data['FAN_3']['sensor'] = $('#sensor_id-FAN_3').val();
199284

285+
fan_data['FAN_0']['pid_target'] = $('#pid_target-FAN_0').val();
286+
fan_data['FAN_1']['pid_target'] = $('#pid_target-FAN_1').val();
287+
fan_data['FAN_2']['pid_target'] = $('#pid_target-FAN_2').val();
288+
fan_data['FAN_3']['pid_target'] = $('#pid_target-FAN_3').val();
289+
290+
fan_data['FAN_0']['pid_p'] = $('#pid_p-FAN_0').val();
291+
fan_data['FAN_1']['pid_p'] = $('#pid_p-FAN_1').val();
292+
fan_data['FAN_2']['pid_p'] = $('#pid_p-FAN_2').val();
293+
fan_data['FAN_3']['pid_p'] = $('#pid_p-FAN_3').val();
294+
295+
fan_data['FAN_0']['pid_i'] = $('#pid_i-FAN_0').val();
296+
fan_data['FAN_1']['pid_i'] = $('#pid_i-FAN_1').val();
297+
fan_data['FAN_2']['pid_i'] = $('#pid_i-FAN_2').val();
298+
fan_data['FAN_3']['pid_i'] = $('#pid_i-FAN_3').val();
299+
300+
fan_data['FAN_0']['pid_d'] = $('#pid_d-FAN_0').val();
301+
fan_data['FAN_1']['pid_d'] = $('#pid_d-FAN_1').val();
302+
fan_data['FAN_2']['pid_d'] = $('#pid_d-FAN_2').val();
303+
fan_data['FAN_3']['pid_d'] = $('#pid_d-FAN_3').val();
304+
200305
fan_data['FAN_0']['temp_th'] = $('#temp_th-FAN_0').val();
201306
fan_data['FAN_1']['temp_th'] = $('#temp_th-FAN_1').val();
202307
fan_data['FAN_2']['temp_th'] = $('#temp_th-FAN_2').val();
@@ -239,9 +344,32 @@ <h1>Fan Curves</h1>
239344
});
240345
});
241346

242-
function createChart(key, data, active_sensor, active_temp_th, active_duty_th, active_sud_duration, halt_on, units) {
243-
const chartControlsContainer = $('<div>').addClass('chart-controls-container').attr('id', `chart-controls-${key}`);
347+
function createChart(key, fanData) {
348+
const data = fanData['curves'];
349+
const active_sensor = fanData['sensor'];
350+
const active_temp_th = fanData['temp_th'];
351+
const active_duty_th = fanData['duty_th'];
352+
const active_sud_duration = fanData['sud_dur'];
353+
const halt_on = fanData['halt_on'];
354+
const units = fanData['units'];
355+
const mode = fanData['mode'] || 'curve';
244356

357+
const fanContainer = $('<div>').addClass('fan-container').attr('id', `fan-container-${key}`);
358+
$('#charts').append(fanContainer);
359+
360+
const modeToggleContainer = $(`
361+
<div>
362+
<span>Fan Curve</span>
363+
<label class="switch">
364+
<input type="checkbox" class="mode-toggle" data-fan="${key}" ${mode === 'pid' ? 'checked' : ''}>
365+
<span class="slider round"></span>
366+
</label>
367+
<span>PID Control</span>
368+
</div>
369+
`);
370+
fanContainer.append(modeToggleContainer);
371+
372+
const controlsContainer = $('<div>').addClass('chart-controls-container').attr('id', `chart-controls-${key}`);
245373
const chartContainer = $('<div>').addClass('chart-container').attr('id', `chart-${key}`);
246374
var fanLabelOverride = (key == "FAN_0") ? "FAN_PUMP" : key;
247375
chartContainer.append(`<h3 style="position: absolute; opacity: 30%; margin-inline: auto; width: fit-content; left:0; right:0; top: 50%; transform: translateY(-50%); user-select: none;">${fanLabelOverride}</h3>`);
@@ -257,20 +385,21 @@ <h1>Fan Curves</h1>
257385
selected: active_sensor == t_sensor
258386
}));
259387
}
260-
temp_src_select_label.appendTo(chartControlsContainer);
261-
temp_src_select.appendTo(chartControlsContainer);
388+
temp_src_select_label.appendTo(controlsContainer);
389+
temp_src_select.appendTo(controlsContainer);
262390

263391
const rpm_select_label = $(`<label for="duty_th-${key}">Fan RPM alarm:</label>`);
264-
const rpm_select = $(`<select style="bottom: 10px; right: 100px;"></select><br>`).attr('id', `duty_th-${key}`);
392+
const rpm_select = $(`<select style="bottom: 10px; right: 100px;"></select>`).attr('id', `duty_th-${key}`);
265393
rpm_select.append($('<option>', { value: -1, text : 'No alarm', selected: active_duty_th == -1 }));
266394
for (_r = 100; _r <= 300; _r+=50) {
267395
rpm_select.append($('<option>', { value: _r, text : '< ' + _r + ' RPM', selected: active_duty_th == _r }));
268396
}
269-
rpm_select_label.appendTo(chartControlsContainer);
270-
rpm_select.appendTo(chartControlsContainer);
397+
rpm_select_label.appendTo(controlsContainer);
398+
rpm_select.appendTo(controlsContainer);
399+
$(`<br>`).appendTo(controlsContainer);
271400

272401
const temp_select_label = $(`<label for="temp_th-${key}">Temperature alarm:</label>`);
273-
const temp_select = $(`<select style="bottom: 10px; right: 100px;"></select><br>`).attr('id', `temp_th-${key}`);
402+
const temp_select = $(`<select style="bottom: 10px; right: 100px;"></select>`).attr('id', `temp_th-${key}`);
274403
temp_select.append($('<option>', { value: 999, text : 'No alarm', selected: active_temp_th == -1 }));
275404

276405
for (_t = 30; _t <= 60; _t+=10) {
@@ -281,31 +410,49 @@ <h1>Fan Curves</h1>
281410
}
282411
temp_select.append($('<option>', { value: _t, text : '>= ' + _tt + '°' + units, selected: active_temp_th == _t }));
283412
}
284-
temp_select_label.appendTo(chartControlsContainer);
285-
temp_select.appendTo(chartControlsContainer);
413+
temp_select_label.appendTo(controlsContainer);
414+
temp_select.appendTo(controlsContainer);
415+
$(`<br>`).appendTo(controlsContainer);
286416

287417
const halt_select_label = $(`<label for="halt_on-${key}">Halt PC on:</label>`);
288-
const halt_select = $(`<select style="bottom: 10px; right: 100px;"></select><br>`).attr('id', `halt-${key}`);
418+
const halt_select = $(`<select style="bottom: 10px; right: 100px;"></select>`).attr('id', `halt-${key}`);
289419
halt_select.append($('<option>', { value: 0, text : 'No halt', selected: halt_on == 0 }));
290420
halt_select.append($('<option>', { value: 1, text : 'Halt on fan speed alarm', selected: halt_on == 1 }));
291421
halt_select.append($('<option>', { value: 2, text : 'Halt on temperature alarm', selected: halt_on == 2 }));
292422
halt_select.append($('<option>', { value: 3, text : 'Halt on both', selected: halt_on == 3 }));
293-
halt_select_label.appendTo(chartControlsContainer);
294-
halt_select.appendTo(chartControlsContainer);
423+
halt_select_label.appendTo(controlsContainer);
424+
halt_select.appendTo(controlsContainer);
425+
$(`<br>`).appendTo(controlsContainer);
295426

296427
const stepupdown_select_label = $(`<label for="step-${key}">Step up/down duration:</label>`);
297-
const stepupdown_select = $(`<select style="bottom: 10px; right: 100px;"></select><br>`).attr('id', `step-${key}`);
428+
const stepupdown_select = $(`<select style="bottom: 10px; right: 100px;"></select>`).attr('id', `step-${key}`);
298429
for (_s = 1; _s <= 100; _s+=1) {
299430
stepupdown_select.append($('<option>', { value: _s, text : _s + ' seconds', selected: active_sud_duration == _s }));
300431
}
301-
stepupdown_select_label.appendTo(chartControlsContainer);
302-
stepupdown_select.appendTo(chartControlsContainer);
432+
stepupdown_select_label.appendTo(controlsContainer);
433+
stepupdown_select.appendTo(controlsContainer);
434+
$(`<br>`).appendTo(controlsContainer);
303435

304-
$('<hr>').appendTo(chartControlsContainer);
436+
$('<hr>').appendTo(controlsContainer);
437+
438+
fanContainer.append(chartContainer);
439+
440+
const pidControlsContainer = $(`
441+
<div class="pid-controls" id="pid-controls-${key}">
442+
<label for="pid_target-${key}">Target Temperature (°${units}):</label>
443+
<input type="number" id="pid_target-${key}" value="${fanData.pid_target || 40}"><br>
444+
<label for="pid_p-${key}" title="Proportional (Kp): How strongly the fan reacts to the current temperature difference. Higher values mean a stronger reaction. Range: 0.1 - 10.0">Reaction Speed:</label>
445+
<input type="number" id="pid_p-${key}" step="0.1" value="${fanData.pid_p || 1.0}" min="0.1" max="10.0"><br>
446+
<label for="pid_i-${key}" title="Integral (Ki): Corrects for small, steady-state temperature errors over time. Helps eliminate temperature drift. Range: 0.0 - 1.0">Correction Strength:</label>
447+
<input type="number" id="pid_i-${key}" step="0.1" value="${fanData.pid_i || 0.1}" min="0.0" max="1.0"><br>
448+
<label for="pid_d-${key}" title="Derivative (Kd): Predicts future temperature changes and dampens the fan's reaction to prevent overshooting the target. Range: 0.0 - 5.0">Stability:</label>
449+
<input type="number" id="pid_d-${key}" step="0.1" value="${fanData.pid_d || 0.5}" min="0.0" max="5.0"><br>
450+
</div>
451+
`);
452+
fanContainer.append(pidControlsContainer);
453+
454+
fanContainer.append(controlsContainer);
305455

306-
$('#charts').append(chartContainer);
307-
$('#charts').append(chartControlsContainer);
308-
309456
for (let i = 0; i <= 10; i++) {
310457
// Vertical lines
311458
var newLine = document.createElementNS('http://www.w3.org/2000/svg','line');
@@ -471,8 +618,17 @@ <h1>Fan Curves</h1>
471618
.addClass('dot-info')
472619
.html(Math.floor(data[index].temp) + '&degC at ' + Math.floor(data[index].fan/255*100) + '% fan')
473620
.hide()
474-
.appendTo(dot)
621+
.appendTo(dot);
475622
});
623+
624+
if (mode === 'pid') {
625+
chartContainer.hide();
626+
pidControlsContainer.css('display', 'inline-block');
627+
} else {
628+
chartContainer.show();
629+
pidControlsContainer.hide();
630+
}
631+
476632
}
477633

478634
function calculateContainment(data, index, svg, scaleX, scaleY) {

lib/README

Lines changed: 0 additions & 46 deletions
This file was deleted.

mock-ui/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ func main() {
8383

8484
http.HandleFunc("/get-curves", func(w http.ResponseWriter, r *http.Request) {
8585
noCacheHeaders(w)
86-
fmt.Fprintf(w, `{"FAN_0": {"units": "C", "curves": [{"temp": 30, "fan": 20}, {"temp": 33, "fan": 40}, {"temp": 36, "fan": 60}, {"temp": 39, "fan": 80}, {"temp": 42, "fan": 100}], "sensor": "TEMP_1"}, "FAN_1": {"units": "C", "curves": [], "sensor": "TEMP_1"}, "FAN_2": {"units": "C", "curves": [], "sensor": "TEMP_1"}, "FAN_3": {"units": "C", "curves": [], "sensor": "TEMP_1"}}`)
86+
fmt.Fprintf(w, `{"FAN_0": {"mode": "pid", "units": "C", "curves": [{"temp": 30, "fan": 20}, {"temp": 33, "fan": 40}, {"temp": 36, "fan": 60}, {"temp": 39, "fan": 80}, {"temp": 42, "fan": 100}], "sensor": "TEMP_1"},
87+
"FAN_1": {"units": "C", "curves": [{"temp": 30, "fan": 20}, {"temp": 33, "fan": 40}, {"temp": 36, "fan": 60}, {"temp": 39, "fan": 80}, {"temp": 42, "fan": 100}], "sensor": "TEMP_1"},
88+
"FAN_2": {"units": "C", "curves": [{"temp": 30, "fan": 20}, {"temp": 33, "fan": 40}, {"temp": 36, "fan": 60}, {"temp": 39, "fan": 80}, {"temp": 42, "fan": 100}], "sensor": "TEMP_1"},
89+
"FAN_3": {"units": "C", "curves": [{"temp": 30, "fan": 20}, {"temp": 33, "fan": 40}, {"temp": 36, "fan": 60}, {"temp": 39, "fan": 80}, {"temp": 42, "fan": 100}], "sensor": "TEMP_1"}}`)
8790
})
8891

8992
http.HandleFunc("/get-rgb", func(w http.ResponseWriter, r *http.Request) {

platformio.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ lib_deps =
2222
adafruit/Adafruit GFX Library@^1.11.10
2323
knolleary/PubSubClient@^2.8
2424
adafruit/Adafruit ADS1X15@^2.4.0
25+
br3ttb/PID@^1.2.1
2526
monitor_speed = 115200
2627
board_build.filesystem = littlefs
2728
build_flags =
2829
-DARDUINO_USB_CDC_ON_BOOT=0
2930
-DARDUINO_USB_MSC_ON_BOOT=0
3031
-DARDUINO_USB_DFU_ON_BOOT=0
31-
3232
extra_scripts = pre:extra_script.py

src/globals.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ std::map<int, FanRpmTarget> m_TargetFanRpm = {
5151
{3, {0, 0, 0, 0, false}}
5252
};
5353

54+
std::map<int, double> m_PidOutputs;
55+
5456
std::map<int, FanPinPair> PIN_FAN_MAP = {
5557
{0, {14, 13}},
5658
{1, {12, 11}},

src/globals.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ extern int a_FanIds[ACTIVE_FANS];
7373
// Fan State
7474
extern unsigned long a_CurrentFanSpeedsRpm[ACTIVE_FANS];
7575
extern std::map<int, FanRpmTarget> m_TargetFanRpm;
76+
extern std::map<int, double> m_PidOutputs;
7677

7778
// Fan ISR Timestamps (MUST be volatile)
7879
extern volatile unsigned long fan0_TS1, fan0_TS2;

0 commit comments

Comments
 (0)