// https://wokwi.com/projects/384972541717315585
// is
// https://github.com/br3ttb/Arduino-PID-Library/blob/master/examples/PID_RelayOutput/PID_RelayOutput.ino
// adapted to https://forum.arduino.cc/t/time-spent-in-a-function/1202863/17?u=davex
/********************************************************
PID RelayOutput Example
Same as basic example, except that this time, the output
is going to a digital pin which (we presume) is controlling
a relay. the pid is designed to Output an analog value,
but the relay can only be On/Off.
to connect them together we use "time proportioning
control" it's essentially a really slow version of PWM.
first we decide on a window size (5000mS say.) we then
set the pid to adjust its output between 0 and that window
size. lastly, we add some logic that translates the PID
output into "Relay On Time" with the remainder of the
window being "Relay Off Time"
********************************************************/
#include <PID_v1.h> // https://github.com/br3ttb/Arduino-PID-Library
#define PIN_INPUT A0 // setpoint Pot or temperature
#define RELAY_PIN 8 // active low relay pin
#define RELAY_OFF HIGH
#define RELAY_ON LOW
const byte pidNotManualPin = 12;
int pidNotManualFlag = false;
int powerFlag = RELAY_OFF; //
float Tamb = 20; //
static float temp = Tamb ; // °C
//Define Variables we'll be connecting to
double Setpoint = 37.0, Input, Output;
//unsigned long WindowSize = 5 * 60000UL; // 5 minutes
//unsigned long WindowSize = 1 * 60000UL; // 1 minutes
unsigned long WindowSize = 1 * 10 * 1000UL; //
unsigned long windowStartTime;
//Specify the links and initial tuning parameters
double Kp = 0 * WindowSize / 10.0, Ki = 0.05 * WindowSize / 10.0, Kd = 0 ; //+-10deg prop zone
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
void setup()
{
pinMode(pidNotManualPin, INPUT_PULLUP);
temp = (analogRead(PIN_INPUT) + 0.5) * 100 / 1024;
windowStartTime = millis();
//initialize the variables we're linked to
Setpoint = 100;
//tell the PID to range between 0 and the full window size
myPID.SetOutputLimits(0, WindowSize);
//tell the PID to sample at 1/20 the adjustment rate:
myPID.SetSampleTime(WindowSize / 100);
//turn the PID on
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, powerFlag);
myPID.SetMode(AUTOMATIC);
Serial.begin(115200);
}
void loop()
{
//Input = analogRead(PIN_INPUT) / 5.12;
Input = temp ;//* 9 / 5 + 32;
//Input = Setpoint+(temp < Setpoint - 1) - (temp > Setpoint +1 );
Setpoint = (analogRead(PIN_INPUT)) * 100.0 / 1023;
pidNotManualFlag = digitalRead(pidNotManualPin) == LOW;
if (pidNotManualFlag) {
myPID.Compute();
} else {
Output = Setpoint * WindowSize / 100;
}
/************************************************
turn the output pin on/off based on pid output
************************************************/
if (millis() - windowStartTime > WindowSize)
{ //time to shift the Relay Window
windowStartTime += WindowSize;
powerFlag = RELAY_OFF;
//digitalWrite(RELAY_PIN,RELAY_OFF);
}
if (millis() - windowStartTime <= Output ) {
//digitalWrite(RELAY_PIN, HIGH);
// Serial.print('.');
powerFlag = RELAY_ON;
}
else {
powerFlag = RELAY_OFF;
//digitalWrite(RELAY_PIN, LOW);
// powerFlag = true;
}
if (millis() - windowStartTime > Output) {
powerFlag = RELAY_OFF;
}
// update pin to match powerFlag as needed
if (digitalRead(RELAY_PIN) != powerFlag) {
Serial.println(powerFlag == RELAY_ON ? "on" : "off");
digitalWrite(RELAY_PIN, powerFlag);
}
simHotTub();
report();
}
void report(void) {
const unsigned long interval = 500;
static unsigned long last = 0;
static float lastTemp = 0;
if (millis() - last < interval) return;
Serial.print("Input:");
Serial.print(Input);
Serial.print(" Setpoint:");
if (pidNotManualFlag) {
Serial.print(Setpoint);
Serial.print(" Error:");
Serial.print(Setpoint - Input);
} else {
Serial.print(" Watts:");
Serial.print(Setpoint * 5500 / 100);
}
Serial.print(" OutputMs:");
Serial.print(Output);
Serial.print(" Power:");
Serial.print(powerFlag == RELAY_ON);
Serial.print(" Temp:");
Serial.print( temp, 6);
Serial.print(" Rate(degC/hr):");
Serial.print((temp - lastTemp) * 60 * 60 * 1000 / (interval), 5);
Serial.println("");
lastTemp = temp;
last += interval;
}
float simHotTub() {
// periodically simulate the heat transfer into and out of
// a hottub
const float mass = 2000; // kg of water
const float heater = 5500; // W of heater
const float Cp = 4186, A = 3, h = 0.5;
static bool initFlag = true;
const unsigned long interval = 500;
static unsigned long last = 0;
if (millis() - last < interval) return;
last += interval;
if (initFlag) {
initFlag = false;
Serial.print("simHeaterInit ");
Serial.print("tau(s):");
Serial.print(mass * Cp / (A * h));
Serial.print(", dx/dt:");
Serial.print(heater / Cp / (mass), 4);
Serial.print(" T_inf:");
// Q = hA(T1-T0)
// Q/(hA)+T0
float Tinf = heater / (h * A) + Tamb;
Serial.print(Tinf);
Serial.print(" Psetpoint:");
float PSetpoint = h * A * (Setpoint - Tamb);
Serial.print(PSetpoint);
Serial.println();
}
// heater
if (powerFlag == RELAY_ON) {
temp += heater * interval / 1000.0 / (mass * Cp);
}
// convection
temp -= (temp - 20) * A * h / (mass * Cp);
return temp;
}