For PID control I wrote my own function nodes to do that. I've included the code for the cascade PID below. Why did I write my own PID functions? Partly because I wanted to output all the internal calculations to help with my tuning. In addition to the custom functionality like output limiting and the cascade PID control for the mash control, and features to reduce integrator windup. I've also written PID algorithms before for other projects/pretty familiar with them so this was one of the less difficult parts of the project in ways.
I'm not doing any data visualization on my dashboard, I look at that afterwards using grafana for now, likely will switch to influxdb 2.0 when that is finalized.
The code below requires certain flow variables to be initialized...
// brew2 PID algorithm - integrators
// Justin Angevaare
// May 2018 - Nov. 2019
// Calculate outer integrator max
// Output to flow context
if(flow.get('outer_i')>0.0){
flow.set('outer_integrator_max', (flow.get('outer_max')-flow.get('outer_min'))/flow.get('outer_i'))}else {
flow.set('outer_integrator_max', 0.0)}
// Get current output max
// Output to flow context
if(global.get('rims_limiter')){
flow.set('inner_max', flow.get('inner_limit'))}else {
flow.set('inner_max', 100)}
// Calculate inner integrator max
// Output to flow context
if(flow.get('inner_i')>0.0){
flow.set('inner_integrator_max', flow.get('inner_max')/flow.get('inner_i'))}else {
flow.set('inner_integrator_max', 0.0)}
return msg;
and the rest of this assumes that this function will be triggered every 2 seconds when running.
// brew2 Cascade PID algorithm
// Justin Angevaare
// Nov. 2019
// Manually specify update interval
msg.interval = 2000;
//
// Begin Outer Control Loop
//
// Output from Outer Control will be the target RIMS delta T
// Calculate outer error
msg.outer_target = flow.get('mash_target')
msg.outer_error = msg.outer_target - global.get('temp-MLT');
// Update outer integrator
// For purposes of integrator, bound error by [-1, 1]
msg.outer_integrator = flow.get('outer_integrator');
msg.outer_integrator += (msg.interval/1000) * Math.max(Math.min(msg.outer_error, 1), -1);
// Bound total outer integrator by absolute maximum
msg.outer_integrator = Math.max(Math.min(msg.outer_integrator, flow.get('outer_integrator_max')), -flow.get('outer_integrator_max'));
// Output updated integrator to flow context
flow.set('outer_integrator', msg.outer_integrator);
// Calculate proportional action
msg.outer_output = msg.outer_error * flow.get('outer_p');
// Calculate intergral action
msg.outer_output += msg.outer_integrator * flow.get('outer_i');
// Calculate derivative action
msg.outer_derivative = (msg.outer_error - flow.get('last_outer_error'))/(msg.interval/1000);
msg.outer_output += msg.outer_derivative * flow.get('outer_d');
// Bound outer_output by [outer_min, outer_max]
msg.outer_output = Math.max(Math.min(msg.outer_output, flow.get('outer_max')), flow.get('outer_min'));
// Set inner_target
msg.inner_target = Math.min(msg.outer_target + msg.outer_output, 90)
// Output updated last_outer_error to flow context
msg.last_outer_error = msg.outer_error;
flow.set('last_outer_error', msg.last_outer_error);
//
// Begin Inner Control Loop
// Output from inner control loop will be RIMS element duty %
// Calculate inner error
msg.inner_error = msg.inner_target - global.get('temp-RIMS');
// Update inner integrator
// For purposes of integrator, bound error by [-1, 1]
msg.inner_integrator = flow.get('inner_integrator');
msg.inner_integrator += (msg.interval/1000) * Math.max(Math.min(msg.inner_error, 1), -1);
// Bound total inner integrator by absolute maximum
msg.inner_integrator = Math.max(Math.min(msg.inner_integrator, flow.get('inner_integrator_max')), -flow.get('inner_integrator_max'));
// Output updated integrator to flow context
flow.set('inner_integrator', msg.inner_integrator);
// Calculate proportional action
msg.inner_output = msg.inner_error * flow.get('inner_p');
// Calculate intergral action
msg.inner_output += msg.inner_integrator * flow.get('inner_i');
// Calculate derivative action
msg.inner_derivative = (msg.inner_error - flow.get('last_inner_error'))/(msg.interval/1000);
msg.inner_output += msg.inner_derivative * flow.get('inner_d');
// Bound inner_output by [0.0, max_output]
msg.inner_output = Math.max(Math.min(msg.inner_output, flow.get('inner_max')), 0.0);
// Output updated last_inner_error to flow context
msg.last_inner_error = msg.inner_error;
flow.set('last_inner_error', msg.last_inner_error);
// Set payload to element output
msg.payload = msg.inner_output;
return msg;