new file: GaggiaPID.ino
new file: PID_AutoTune_v0/Examples/AutoTune_Example/AutoTune_Example.pde new file: PID_AutoTune_v0/PID_AutoTune_v0.cpp new file: PID_AutoTune_v0/PID_AutoTune_v0.h new file: src/SevenSegmentTM1637/SevenSegmentAsciiMap.cpp new file: src/SevenSegmentTM1637/SevenSegmentAsciiMap.h new file: src/SevenSegmentTM1637/SevenSegmentExtended.cpp new file: src/SevenSegmentTM1637/SevenSegmentExtended.h new file: src/SevenSegmentTM1637/SevenSegmentFun.cpp new file: src/SevenSegmentTM1637/SevenSegmentFun.h new file: src/SevenSegmentTM1637/SevenSegmentTM1637.cpp new file: src/SevenSegmentTM1637/SevenSegmentTM1637.h Committer: Isaac <isaac@iosull.com>
This commit is contained in:
+590
@@ -0,0 +1,590 @@
|
||||
/*
|
||||
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 <PID_AutoTune_v0.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 DEEP_SLEEP false // deep sleep then reset or idling (D0 must be connected to RST) https://github.com/nodemcu/nodemcu-devkit-v1.0
|
||||
#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 potentially overwritten from EEPROM
|
||||
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;
|
||||
|
||||
// PID Autotune parameters
|
||||
byte ATuneModeRemember = 2;
|
||||
bool tuning = false;
|
||||
PID_ATune aTune(&Input, &Output);
|
||||
|
||||
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...");
|
||||
}
|
||||
}
|
||||
|
||||
void enableAutoTune(double step, double noise, int lookback) {
|
||||
tuning = true;
|
||||
|
||||
aTune.SetNoiseBand(noise);
|
||||
aTune.SetOutputStep(step);
|
||||
aTune.SetLookbackSec(lookback);
|
||||
AutoTuneHelper(true);
|
||||
}
|
||||
|
||||
void disableAutoTune() {
|
||||
tuning = false;
|
||||
|
||||
// Cancel autotune
|
||||
aTune.Cancel();
|
||||
AutoTuneHelper(false);
|
||||
}
|
||||
|
||||
void AutoTuneHelper(boolean start) {
|
||||
if (start)
|
||||
{
|
||||
ATuneModeRemember = myPID.GetMode();
|
||||
}
|
||||
else
|
||||
{
|
||||
myPID.SetMode(ATuneModeRemember);
|
||||
}
|
||||
}
|
||||
|
||||
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["autotuning"] = tuning;
|
||||
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 handleAutotuneStart() {
|
||||
|
||||
double step = 750;
|
||||
String step_val = server.arg("step");
|
||||
if (step_val != NULL)
|
||||
{
|
||||
step = step_val.toFloat();
|
||||
}
|
||||
|
||||
double noise = 1;
|
||||
String noise_val = server.arg("noise");
|
||||
if (noise_val != NULL)
|
||||
{
|
||||
noise = noise_val.toFloat();
|
||||
}
|
||||
|
||||
unsigned int lookback = 20;
|
||||
String lookback_val = server.arg("lookback");
|
||||
if (lookback_val != NULL)
|
||||
{
|
||||
lookback = lookback_val.toInt();
|
||||
}
|
||||
|
||||
String message = "Autotune started: noise(" + String(noise) + ") " + "step(" + String(step) + ") lookback(" + String(lookback) + ")";
|
||||
enableAutoTune(step, noise, lookback);
|
||||
server.send(200, "text/plain", message);
|
||||
}
|
||||
|
||||
void handleAutotuneStop() {
|
||||
disableAutoTune();
|
||||
server.send(200, "text/plain", "Autotune stopped.");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (tuning)
|
||||
{
|
||||
if (aTune.Runtime())
|
||||
{
|
||||
tuning = false;
|
||||
|
||||
// We're done, set the tuning parameters
|
||||
Kp = aTune.GetKp();
|
||||
Ki = aTune.GetKi();
|
||||
Kd = aTune.GetKd();
|
||||
myPID.SetTunings(Kp, Ki, Kd);
|
||||
AutoTuneHelper(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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))));
|
||||
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.on("/autotunestart", HTTP_POST, handleAutotuneStart);
|
||||
server.on("/autotunestop", HTTP_POST, handleAutotuneStop);
|
||||
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);
|
||||
|
||||
// Set 1 for PID or 0 for PI controller autotuning parameters
|
||||
aTune.SetControlType(1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user