43
43
border-left-color : # 21f32f ;
44
44
}
45
45
46
- .sidebar .logo-container {
46
+ .sidebar .logo-container {
47
47
text-align : center;
48
48
margin-bottom : 20px ;
49
49
}
76
76
}
77
77
78
78
.chart-controls-container {
79
- margin : 0 20px 0 20px ;
80
- }
79
+ margin : 10 px 20px 0 20px ;
80
+ }
81
81
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
+ }
85
92
93
+ .pid-controls input {
94
+ display : inline-block;
95
+ margin-bottom : 10px ;
96
+ }
86
97
87
98
.chart-container {
88
99
display : inline-block;
129
140
border : none;
130
141
border-radius : 4px ;
131
142
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
+ }
133
205
</ style >
134
206
</ head >
135
207
< body >
@@ -184,8 +256,21 @@ <h1>Fan Curves</h1>
184
256
}
185
257
186
258
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 ] ) ;
188
260
}
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
+ } ) ;
189
274
}
190
275
} ) ;
191
276
@@ -197,6 +282,26 @@ <h1>Fan Curves</h1>
197
282
fan_data [ 'FAN_2' ] [ 'sensor' ] = $ ( '#sensor_id-FAN_2' ) . val ( ) ;
198
283
fan_data [ 'FAN_3' ] [ 'sensor' ] = $ ( '#sensor_id-FAN_3' ) . val ( ) ;
199
284
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
+
200
305
fan_data [ 'FAN_0' ] [ 'temp_th' ] = $ ( '#temp_th-FAN_0' ) . val ( ) ;
201
306
fan_data [ 'FAN_1' ] [ 'temp_th' ] = $ ( '#temp_th-FAN_1' ) . val ( ) ;
202
307
fan_data [ 'FAN_2' ] [ 'temp_th' ] = $ ( '#temp_th-FAN_2' ) . val ( ) ;
@@ -239,9 +344,32 @@ <h1>Fan Curves</h1>
239
344
} ) ;
240
345
} ) ;
241
346
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' ;
244
356
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 } ` ) ;
245
373
const chartContainer = $ ( '<div>' ) . addClass ( 'chart-container' ) . attr ( 'id' , `chart-${ key } ` ) ;
246
374
var fanLabelOverride = ( key == "FAN_0" ) ? "FAN_PUMP" : key ;
247
375
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>
257
385
selected : active_sensor == t_sensor
258
386
} ) ) ;
259
387
}
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 ) ;
262
390
263
391
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 } ` ) ;
265
393
rpm_select . append ( $ ( '<option>' , { value : - 1 , text : 'No alarm' , selected : active_duty_th == - 1 } ) ) ;
266
394
for ( _r = 100 ; _r <= 300 ; _r += 50 ) {
267
395
rpm_select . append ( $ ( '<option>' , { value : _r , text : '< ' + _r + ' RPM' , selected : active_duty_th == _r } ) ) ;
268
396
}
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 ) ;
271
400
272
401
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 } ` ) ;
274
403
temp_select . append ( $ ( '<option>' , { value : 999 , text : 'No alarm' , selected : active_temp_th == - 1 } ) ) ;
275
404
276
405
for ( _t = 30 ; _t <= 60 ; _t += 10 ) {
@@ -281,31 +410,49 @@ <h1>Fan Curves</h1>
281
410
}
282
411
temp_select . append ( $ ( '<option>' , { value : _t , text : '>= ' + _tt + '°' + units , selected : active_temp_th == _t } ) ) ;
283
412
}
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 ) ;
286
416
287
417
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 } ` ) ;
289
419
halt_select . append ( $ ( '<option>' , { value : 0 , text : 'No halt' , selected : halt_on == 0 } ) ) ;
290
420
halt_select . append ( $ ( '<option>' , { value : 1 , text : 'Halt on fan speed alarm' , selected : halt_on == 1 } ) ) ;
291
421
halt_select . append ( $ ( '<option>' , { value : 2 , text : 'Halt on temperature alarm' , selected : halt_on == 2 } ) ) ;
292
422
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 ) ;
295
426
296
427
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 } ` ) ;
298
429
for ( _s = 1 ; _s <= 100 ; _s += 1 ) {
299
430
stepupdown_select . append ( $ ( '<option>' , { value : _s , text : _s + ' seconds' , selected : active_sud_duration == _s } ) ) ;
300
431
}
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 ) ;
303
435
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 ) ;
305
455
306
- $ ( '#charts' ) . append ( chartContainer ) ;
307
- $ ( '#charts' ) . append ( chartControlsContainer ) ;
308
-
309
456
for ( let i = 0 ; i <= 10 ; i ++ ) {
310
457
// Vertical lines
311
458
var newLine = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'line' ) ;
@@ -471,8 +618,17 @@ <h1>Fan Curves</h1>
471
618
. addClass ( 'dot-info' )
472
619
. html ( Math . floor ( data [ index ] . temp ) + '°C at ' + Math . floor ( data [ index ] . fan / 255 * 100 ) + '% fan' )
473
620
. hide ( )
474
- . appendTo ( dot )
621
+ . appendTo ( dot ) ;
475
622
} ) ;
623
+
624
+ if ( mode === 'pid' ) {
625
+ chartContainer . hide ( ) ;
626
+ pidControlsContainer . css ( 'display' , 'inline-block' ) ;
627
+ } else {
628
+ chartContainer . show ( ) ;
629
+ pidControlsContainer . hide ( ) ;
630
+ }
631
+
476
632
}
477
633
478
634
function calculateContainment ( data , index , svg , scaleX , scaleY ) {
0 commit comments