Files
2022-09-07 18:28:11 +10:00

498 lines
13 KiB
Arduino

/*
ESP8266 (D1 mini)+ SHT30 > MQTT
*/
// --- LIBRARIES IMPORT ---
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include "ESP8266TimerInterrupt.h"
#include <Arduino.h>
#include <DailyStruggleButton.h>
// Project Libraries
#include <EEPROM.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <max6675.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <PID_v1.h>
#include "src/SevenSegmentTM1637/SevenSegmentTM1637.h"
#include "src/SevenSegmentTM1637/SevenSegmentExtended.h"
// --------------------
#define WIFI_SSID "OpenWRT"
#define WIFI_PASSWORD "thisispass"
#define MQTT_SERVER "192.168.0.50"
#define MQTT_CLIENT_NAME "espresso"
#define MQTT_TOPIC "espresso/status"
#define K_TYPE_CLK D5
#define K_TYPE_CS D6
#define K_TYPE_S0 D7
#define DS_MILK_PIN D2 // DS probe SDA line
#define DISPLAY_CLK D3 //Set the CLK pin connection to the display
#define DISPLAY_DIO D4 //Set the DIO pin connection to the display
#define DISPLAY_BUT_TOG D1 // Button to toggle display output
#define RELAY_PIN D0 // Control for SSR relay output
#define TIMER_INTERVAL_MS 250
#define MQTT_PUBLISH_RATE 2000
#define BUTTON_POLL_RATE 50
#define DEBUG false // debug to serial port
#define TIMER_DEBUG false // debug interrupt to serial port
// --- INIT ---
// Milk Probe
OneWire oneWire(DS_MILK_PIN);
DallasTemperature ds(&oneWire);
// Boiler Probe
MAX6675 ktc(K_TYPE_CLK, K_TYPE_CS, K_TYPE_S0);
// Display
SevenSegmentExtended display(DISPLAY_CLK, DISPLAY_DIO);
DailyStruggleButton modeButton;
int modeSelect = 1; // Modes determine what is displayed on the screen.
// Milk setup
float tempMilk = 0;
// Desired temperature setpoint
double tempDesired = 93;
double tempActual = 0;
double maxBoilerTemp = 110; // Safety value, will always turn off the relay if this is exceeded (note: steam goes higher than this, but that's okay)
double Kp = 2.2; // PID setup (note: these values are overwritten from EEPROM, initialize only)
double Ki = 0.3;
double Kd = 0.5;
double Input, Output;
PID myPID(&Input, &Output, &tempDesired, Kp, Ki, Kd, P_ON_M, DIRECT);
double PWMOutput;
unsigned long windowStartTime;
int WindowSize = 5000;
ESP8266WebServer server(80);
char jsonresult[512];
boolean useWifi = true; // Enable Wifi, toggles false if not available.
boolean useMQTT = true; // Enable MQTT, toggles false if not available.
boolean useServer = true; // Initialise webserver for JSON control.
WiFiClient net;
PubSubClient mqtt(MQTT_SERVER, 1883, net);
// All timers reference the value of now
unsigned long now = millis(); //This variable is used to keep track of time
unsigned long previousScreenDraw = now;
unsigned long previousMqttStatsMillis = now;
unsigned long previousButtonCheck = now;
unsigned long shotTimer = now;
// PWM Tracking
double ontime = 0;
int rolling_on = 0;
// --------------------
// Timer interrupt to check temperature and confirm failsafe
volatile uint32_t lastMillis = 0;
// Init ESP8266 timer 1
ESP8266Timer ITimer;
void ICACHE_RAM_ATTR TimerHandler(void) {
#if (TIMER_DEBUG > 0)
if (lastMillis != 0)
Serial.println("Delta ms = " + String(millis() - lastMillis));
lastMillis = millis();
#endif
tempActual = ktc.readCelsius();
// Safety to turn off if max temp is exceeded or the watchdog (now from loop) hasn't been poked in 10 seconds
if (tempActual >= maxBoilerTemp || tempActual < 3 || (millis() - now) >= 10000 || isnan(tempActual))
{
digitalWrite(RELAY_PIN, LOW);
if (TIMER_DEBUG) Serial.println("Aborting...");
}
}
double round2(double value) {
// Round down to 2 decimal places
return (int)(value * 100 + 0.5) / 100.0;
}
char *genJSON() {
DynamicJsonDocument doc(256);
doc["Uptime"] = now / 1000;
doc["Setpoint"] = round2(tempDesired);
doc["ActualTemp"] = round2(tempActual);
doc["Kp"] = round2(Kp);
doc["Ki"] = round2(Ki);
doc["Kd"] = round2(Kd);
doc["RelayOnTime"] = round2(ontime); // send rolling window for on time of PWM from 0 to 1.
serializeJson(doc, jsonresult);
return jsonresult;
}
void handleJSON() {
server.send(200, "application/json", String(genJSON()));
}
void handleSetvals() {
String message;
String setpoint_val = server.arg("setpoint");
if (setpoint_val != NULL)
{
double setpoint_tmp = setpoint_val.toFloat();
if (setpoint_tmp <= 110.0 && setpoint_tmp > 0.1)
{
tempDesired = setpoint_tmp;
message += "Setpoint: " + setpoint_val;
message += "\n";
}
else
{
message += "Setpoint: " + String(setpoint_val) + " is invalid\n";
}
}
String kp_val = server.arg("kp");
if (kp_val != NULL)
{
Kp = kp_val.toFloat();
message += "Kp: " + kp_val;
}
String ki_val = server.arg("ki");
if (ki_val != NULL)
{
Ki = ki_val.toFloat();
message += "Ki: " + ki_val;
}
String kd_val = server.arg("kd");
if (kd_val != NULL)
{
Kd = kd_val.toFloat();
message += "Kd: " + kd_val;
}
myPID.SetTunings(Kp, Ki, Kd);
server.send(200, "text/plain", message);
}
void handleSave() {
writeConfigValuesToEEPROM();
server.send(200, "text/plain", "Wrote config to EEPROM.");
}
void controlRelay() {
// Provide the PID loop with the current temperature
Input = tempActual;
// Safety to turn off if max temp is exceeded
if (Input >= maxBoilerTemp || isnan(Input) )
{
digitalWrite(RELAY_PIN, LOW);
return;
}
myPID.Compute();
// Starts a new PWM cycle every WindowSize milliseconds
if ((now - windowStartTime) > WindowSize)
{
ontime = (double)(rolling_on) * 10 / WindowSize; // rolling_on will be maximum of WindowSize / 10 ms, so this finds a value between 0 and 1.
rolling_on = 0;
windowStartTime += WindowSize;
}
// Calculate the number of milliseconds that have passed in the current PWM cycle.
// If that is less than the Output value, the relay is turned ON
// If that is greater than (or equal to) the Output value, the relay is turned OFF.
PWMOutput = Output * (WindowSize / 100.00);
if ((PWMOutput > 100) && (PWMOutput > (now - windowStartTime)))
{
rolling_on++;
digitalWrite(RELAY_PIN, HIGH);
}
else
{
digitalWrite(RELAY_PIN, LOW);
}
}
void writeConfigValuesToEEPROM() {
// Store config values to eeprom
EEPROM.begin(sizeof(double) * 4);
int EEaddress = 0;
EEPROM.put(EEaddress, tempDesired);
EEaddress += sizeof(tempDesired);
EEPROM.put(EEaddress, Kp);
EEaddress += sizeof(Kp);
EEPROM.put(EEaddress, Ki);
EEaddress += sizeof(Ki);
EEPROM.put(EEaddress, Kd);
EEPROM.commit();
EEPROM.end();
}
void buttonPush(byte buttonEvent) {
switch (buttonEvent){
case onPress:
if (DEBUG) Serial.println("Button Pressed!");
break;
case onRelease:
switch (modeSelect) {
case 1: // Switch to shotClock
modeSelect = 2;
shotTimer = millis();
break;
case 2: // Switch to milk temperature
modeSelect = 3;
break;
case 3: // Return to boiler temperature
modeSelect = 1;
break;
}
break;
case onLongPress:
shotTimer = millis();
break;
}
}
void displayUpdate() {
display.clear();
switch (modeSelect) {
case 1: // Brew Temperature Display
int val;
if(isnan(tempActual)){
display.print("ERR");
}
else {
if(tempActual >= 100) {
display.printNumber((int)(round(tempActual)));
display.setColonOn(false);
} else {
display.printDualCounter((int)(round(tempDesired)), (int)(round(tempActual)));
display.setColonOn(true);
}
}
break;
case 2: // Timer Display
// Update later with switch/case for other temps
display.printDualCounter((int)(round(tempActual)), (int)(round(((millis()-shotTimer)/1000)%99)));
display.setColonOn(true);
break;
case 3: // Milk Temperature Display
display.setColonOn(false);
display.printNumber((int)(round(tempMilk)));
break;
}
}
bool initWIFI() {
bool enableWifi = true;
// Connect to WiFi
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long wifiConnectTimeStart = millis();
while ( WiFi.status() != WL_CONNECTED ) {
if (DEBUG) Serial.println("Connecting Wifi");
delay(500);
if ((millis() - wifiConnectTimeStart) > 7500)
{
enableWifi = false;
display.clear();
display.print("ErWF");
delay(500);
break;
}
}
#if (DEBUG > 0)
if (enableWifi) {
Serial.println("");
Serial.print("Connected to ");
Serial.println(WIFI_SSID);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
else {
Serial.print("Wifi failed to connect...");
}
#endif
return enableWifi;
}
bool initMQTT() {
bool enableMQTT = true;
// Start MQTT client
unsigned long mqttConnectTimeStart = millis();
if (DEBUG) Serial.println("Trying to connect to MQTT broker...");
while (!mqtt.connected()) {
if (DEBUG) Serial.println("connecting MQTT");
if (!mqtt.connect(MQTT_CLIENT_NAME)) delay(500);
if (millis() - mqttConnectTimeStart > 3000)
{
enableMQTT = false;
display.clear();
display.print("ErMQ");
delay(500);
break;
}
}
#if (DEBUG > 0)
if (enableMQTT) {
Serial.println("Connected to MQTT broker, enabling mqtt!");
}
else {
Serial.println("Did not connect to MQTT broker disabling mqtt!");
}
#endif
return enableMQTT;
}
void initServer() {
// Start server
server.on("/json", handleJSON);
server.on("/set", HTTP_POST, handleSetvals);
server.on("/save", HTTP_POST, handleSave);
server.begin();
#if (DEBUG > 0)
Serial.println("HTTP server started...");
#endif
}
void initPID() {
// PID Setup.
windowStartTime = now;
myPID.SetOutputLimits(0, 100); // PID output 0 - 100
myPID.SetSampleTime(50); // PID Samples every 50ms
myPID.SetMode(AUTOMATIC); // Enable control loop
// Initialize the target setpoint and PID parameters
EEPROM.begin(sizeof(double) * 4);
// Read configuration values from eeprom
int EEaddress = 0;
EEPROM.get(EEaddress, tempDesired);
EEaddress += sizeof(tempDesired);
EEPROM.get(EEaddress, Kp);
EEaddress += sizeof(Kp);
EEPROM.get(EEaddress, Ki);
EEaddress += sizeof(Ki);
EEPROM.get(EEaddress, Kd);
EEPROM.end();
// Set PID values from EEPROM
myPID.SetTunings(Kp, Ki, Kd);
}
void setup() {
if (DEBUG) Serial.begin(9600);
// Disable the relay ASAP while we get our shit together.
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW);
tempActual = ktc.readCelsius(); // first read
// Display setup
display.begin(); // initializes the display
display.setBacklight(100); // set the brightness to 100 %
display.printNumber(8888); // Flash 8888 on the display
// Setup libraries
if (useWifi) useWifi = initWIFI();
if (useWifi) { // check again, lazy return code...
if (useMQTT) useMQTT = initMQTT();
initServer();
} else {
if (DEBUG) Serial.println("No Wifi, no point in trying MQTT or setting up server");
useMQTT = false;
useServer = false;
}
initPID(); // Setup PID loop values from EEPROM and start calc.
// Setup button and check button on interrupt
modeButton.set(DISPLAY_BUT_TOG, buttonPush);
modeButton.enableLongPress(500);
// Setup milk probe
ds.begin();
ds.requestTemperatures();
ds.setWaitForConversion(false); // The default is stupidly blocking (>600 ms)... this disables that.
// Create interrupt to check temperature of boiler probe and also track button state. This guarantees checking the temperature and triggering the watchdog if something goes wrong with main loop or PID.
ITimer.attachInterruptInterval(TIMER_INTERVAL_MS * 1000, TimerHandler);
delay(250); // Let everything settle.
}
void loop() {
now = millis();
// Temperature updated in interrupt
controlRelay();
// Check for JSON
if (useServer) server.handleClient();
if (useMQTT)
{
// Publish status via MQTT
if (now - previousMqttStatsMillis >= MQTT_PUBLISH_RATE)
{
mqtt.publish(MQTT_TOPIC, genJSON());
previousMqttStatsMillis = now;
#if (DEBUG > 0)
String tempc = "";
Serial.print("Milk C = ");
tempc = String(tempMilk,1);
Serial.print(tempc);
Serial.print(", Boiler C = ");
Serial.println(tempActual);
#endif
}
}
if (now - previousScreenDraw >= 500) {
tempMilk = ds.getTempCByIndex(0); // get temperature from last draw
ds.requestTemperatures(); // request next one
displayUpdate();
previousScreenDraw = now;
}
modeButton.poll();
delay(10);
}