Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c98aebc716 | ||
|
2bd12374fc | ||
|
179ea1aeca | ||
|
0e74d66125 | ||
|
ff8129f400 | ||
|
81133f2221 | ||
|
6730f9f390 | ||
|
31b23910f0 | ||
|
b302480610 | ||
|
99600c9135 | ||
1fb657f491 | |||
216e80e642 | |||
8565c87ce4 | |||
85c710e3b7 | |||
045d5a8feb | |||
484db54d89 | |||
c5b19e9103 | |||
a0a25f05e2 | |||
e76ca05674 | |||
fbfe28ab3a | |||
50bee2daa0 | |||
288c7dfc80 | |||
05280526b0 | |||
e2b0205709 | |||
be781f1275 | |||
9a535c665b | |||
8b72a16203 | |||
c7857c1b03 | |||
e12f3e262f | |||
3a1d2ad3ad | |||
f75cbe5f6c | |||
a1eb11b81a | |||
24b3cdae1c | |||
9818e77866 | |||
2673fa7fa3 | |||
fb0afa7e43 | |||
421a0c6580 | |||
8750bceead | |||
1aef2aa15f | |||
c4fb9fa0b3 | |||
2a70f6e89a | |||
e0d2d0aa52 | |||
8954a94737 | |||
bc93148db6 | |||
18c43187cb | |||
6a0de023c4 | |||
3c00630461 | |||
d2972b0a40 | |||
b782cfff86 | |||
5d876b035b | |||
84981a29dd | |||
180e3b065f | |||
8908eccdb6 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
tags
|
||||
*.zip
|
||||
.geany*
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "boil_kettle/src/Arduino-PID-Library"]
|
||||
path = boil_kettle/src/Arduino-PID-Library
|
||||
url = https://git.giacofei.org/Brewery/Arduino-PID-Library.git
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,8 +1,13 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
## [1.0.0] - 2022-01-14
|
||||
First working release running in the brewery.
|
||||
|
||||
### Features
|
||||
Super simple rotary encoder control.
|
||||
- Rotary button toggles heat on/off.
|
||||
- Turning encoder raises and lowers element power (duty cycle).
|
||||
- rotary speed is used to advance power faster if the knob is turned faster.
|
||||
|
||||
## [0.0.1] - 2021-09-13
|
||||
Initial upload of files.
|
||||
|
@ -3,9 +3,9 @@
|
||||
Simple PWM & (eventually) temperature control for an Arduino based electric brewing system.
|
||||
|
||||
## Libraries needed to make this thing work
|
||||
- [ArduinoJson.h](https://www.arduino.cc/reference/en/libraries/arduinojson/)
|
||||
- ~~[ArduinoJson.h](https://www.arduino.cc/reference/en/libraries/arduinojson/)~~
|
||||
- [MD_REncoder.h](https://www.arduino.cc/reference/en/libraries/md_rencoder/)
|
||||
- [LiquidMenu.h](https://www.arduino.cc/reference/en/libraries/liquidmenu/)
|
||||
- [LiquidCrystal_I2C.h](https://www.arduino.cc/reference/en/libraries/liquidcrystal-i2c/)
|
||||
- [MQTT.h](https://www.arduino.cc/reference/en/libraries/mqtt/)
|
||||
- ~~[MQTT.h](https://www.arduino.cc/reference/en/libraries/mqtt/)~~
|
||||
- [Adafruit_MAX31865.h](https://www.arduino.cc/reference/en/libraries/adafruit-max31865-library/)
|
||||
|
@ -1,103 +1,127 @@
|
||||
|
||||
//Built-in
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
#include <Ethernet.h>
|
||||
|
||||
// Additoinal Libraries
|
||||
#include <ArduinoJson.h>
|
||||
#include <MQTT.h>
|
||||
#include <LiquidCrystal_I2C.h>
|
||||
#include <LiquidMenu.h> // LiquidMenu_config.h needs to be modified to use I2C.
|
||||
#include <LiquidMenu.h> // LiquidMenu_config.h needs to be modified to use I2C.
|
||||
#include <MD_REncoder.h>
|
||||
#include <Adafruit_MAX31865.h>
|
||||
//#include <EEPROM-Storage.h>
|
||||
|
||||
// My Includes
|
||||
#include "config.h"
|
||||
#include "button.h"
|
||||
#include "slowPWM.h"
|
||||
#include "src/button/button.h"
|
||||
#include "src/slowPWM/slowPWM.h"
|
||||
#include "src/thermoControl/thermoControl.h"
|
||||
//#include "src/Arduino-PID-Library/PID_v1.h"
|
||||
|
||||
// Pin definitions
|
||||
#define encoderCLK 2
|
||||
#define encoderDT 3
|
||||
#define encoderBTN 4
|
||||
#define kettlePWM 5
|
||||
double KettleDuty = 0;
|
||||
double KettleSetpoint = 70;
|
||||
double CurrentTemp;
|
||||
bool tuning = false;
|
||||
|
||||
// Global variables.
|
||||
byte KettleDuty = 0;
|
||||
bool KettleOn = false;
|
||||
//EEPROMStorage<double> eepromKp(0, 2.0); // 9 bytes for doubles 8 + 1 for checksum
|
||||
//EEPROMStorage<double> eepromKi(9, 5.0); // Initialize at zero.
|
||||
//EEPROMStorage<double> eepromKd(18, 1.0);
|
||||
|
||||
// User I/O objects.
|
||||
Button Enter;
|
||||
slowPWM boilPWM;
|
||||
MD_REncoder rotary = MD_REncoder(encoderDT, encoderCLK);
|
||||
LiquidCrystal_I2C lcd(0x27,20,4);
|
||||
LiquidCrystal_I2C lcd(0x27, 20, 4);
|
||||
Adafruit_MAX31865 thermoRTD = Adafruit_MAX31865(KettleRTD);
|
||||
thermoControl Controller(&CurrentTemp, &KettleSetpoint, &KettleDuty, AUTOMATIC);
|
||||
|
||||
EthernetClient net;
|
||||
MQTTClient mqtt_client;
|
||||
//PID ControllerPID(&CurrentTemp, &KettleDuty, &KettleSetpoint, eepromKp, eepromKi, eepromKd, P_ON_M, DIRECT);
|
||||
|
||||
unsigned long lastRun = 0;
|
||||
|
||||
// Return a character array to represent the
|
||||
// On/Off state of the kettle.
|
||||
char* KettleState() {
|
||||
if (KettleOn) {
|
||||
return (char*)"On";
|
||||
} else {
|
||||
return (char*)"Off";
|
||||
switch (Controller.Mode()) {
|
||||
case OFF: return (char*)"OFF";
|
||||
case AUTOMATIC: return (char*)"AUT";
|
||||
case MANUAL: return (char*)"MAN";
|
||||
default: return (char*)"OFF";
|
||||
}
|
||||
}
|
||||
|
||||
byte setpoint() {
|
||||
return (byte)KettleSetpoint;
|
||||
}
|
||||
|
||||
byte duty() {
|
||||
return (byte)KettleDuty;
|
||||
}
|
||||
|
||||
byte current() {
|
||||
return (byte)CurrentTemp;
|
||||
}
|
||||
|
||||
// LCD menu setup.
|
||||
LiquidLine KettleState_line(0, 0, "Boil Kettle ", KettleState);
|
||||
LiquidLine kettle_temp_line(0, 1, "Setpoint (F) ", setpoint);
|
||||
LiquidLine kettle_power_line(0, 2, "Power (%) ", duty);
|
||||
LiquidLine kettle_currenttemp_line(0, 3, "Actual Temp (F) ", current);
|
||||
LiquidScreen home_screen(KettleState_line, kettle_temp_line, kettle_power_line,kettle_currenttemp_line);
|
||||
LiquidMenu menu(lcd);
|
||||
|
||||
// Interrupt function to run when encoder is turned.
|
||||
//
|
||||
// Increases/decreases the kettle output to a max
|
||||
// of 100% and minimum of 0%.
|
||||
void doEncoder()
|
||||
{
|
||||
void doEncoder() {
|
||||
uint8_t result = rotary.read();
|
||||
uint8_t inc;
|
||||
uint8_t inc = 1;
|
||||
|
||||
if (result) {
|
||||
uint8_t speed = rotary.speed();
|
||||
speed >= 10 ? inc = 5 : inc = 1;
|
||||
}
|
||||
|
||||
if (result == DIR_CW && KettleDuty < 100) {
|
||||
KettleDuty = (KettleDuty / inc) * inc + inc;
|
||||
} else if (result == DIR_CCW && KettleDuty > 0) {
|
||||
KettleDuty = (KettleDuty / inc) * inc - inc;
|
||||
if (Controller.Mode() == AUTOMATIC) {
|
||||
if (result == DIR_CW) {
|
||||
KettleSetpoint = (KettleSetpoint / inc) * inc + inc;
|
||||
} else if (result == DIR_CCW) {
|
||||
KettleSetpoint = (KettleSetpoint / inc) * inc - inc;
|
||||
}
|
||||
|
||||
if (KettleSetpoint > 212) KettleSetpoint=212;
|
||||
if (KettleSetpoint < 0) KettleSetpoint=0;
|
||||
|
||||
} else if (Controller.Mode() == MANUAL) {
|
||||
if (result == DIR_CW) {
|
||||
KettleDuty = (KettleDuty / inc) * inc + inc;
|
||||
} else if (result == DIR_CCW) {
|
||||
KettleDuty = (KettleDuty / inc) * inc - inc;
|
||||
}
|
||||
|
||||
if (KettleDuty > 100) KettleDuty = 100;
|
||||
if (KettleDuty < 0) KettleDuty = 0;
|
||||
|
||||
}
|
||||
|
||||
//menu.update();
|
||||
}
|
||||
|
||||
// LCD menu setup.
|
||||
LiquidLine KettleState_line(0, 0, "Boil Kettle ", KettleState);
|
||||
LiquidLine kettle_power_line(0, 1, "Kettle Power % ", KettleDuty);
|
||||
LiquidScreen home_screen(KettleState_line, kettle_power_line);
|
||||
LiquidMenu menu(lcd);
|
||||
|
||||
void setup() {
|
||||
|
||||
unsigned long lastRun = millis() - UpdateInterval;
|
||||
Serial.begin(9600);
|
||||
lastRun = millis() - UpdateInterval;
|
||||
Serial.begin(115200);
|
||||
rotary.begin();
|
||||
Ethernet.begin(mac, ip);
|
||||
Serial.println("Setting up...");
|
||||
thermoRTD.begin(MAX31865_3WIRE);
|
||||
|
||||
attachInterrupt(digitalPinToInterrupt(encoderCLK), doEncoder, CHANGE);
|
||||
attachInterrupt(digitalPinToInterrupt(encoderDT), doEncoder, CHANGE);
|
||||
|
||||
|
||||
pinMode(encoderCLK, INPUT_PULLUP);
|
||||
pinMode(encoderDT, INPUT_PULLUP);
|
||||
Enter.begin(encoderBTN);
|
||||
boilPWM.begin(kettlePWM, PeriodPWM);
|
||||
|
||||
// if you get a connection, report back via serial:
|
||||
if (Ethernet.linkStatus() == LinkON) {
|
||||
SetupMQTT(MQTT_BROKER);
|
||||
} else {
|
||||
// if you didn't get a connection to the server:
|
||||
Serial.println("connection failed");
|
||||
}
|
||||
Controller.Mode(OFF);
|
||||
|
||||
lcd.init();
|
||||
lcd.backlight();
|
||||
@ -105,40 +129,126 @@ void setup() {
|
||||
menu.init();
|
||||
menu.add_screen(home_screen);
|
||||
menu.update();
|
||||
|
||||
};
|
||||
|
||||
void UpdateBoilKettle(){
|
||||
static byte last_KettleDuty = 0;
|
||||
|
||||
void run_kettle() {
|
||||
if (Enter.pressed()) {
|
||||
KettleOn = !KettleOn;
|
||||
Serial.println("Enter Pressed");
|
||||
if (Controller.CycleMode() == OFF) {
|
||||
KettleDuty = 0;
|
||||
}
|
||||
menu.update();
|
||||
}
|
||||
|
||||
if (last_KettleDuty != KettleDuty) {
|
||||
last_KettleDuty = KettleDuty;
|
||||
menu.update();
|
||||
}
|
||||
Controller.Compute();
|
||||
|
||||
if (KettleOn) {
|
||||
boilPWM.compute(KettleDuty);
|
||||
} else {
|
||||
boilPWM.compute(0);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
void run_tuning() {
|
||||
if (Enter.pressed()) {
|
||||
stopAutoTune();
|
||||
}
|
||||
int result = ControllerPID.ComputeTune();
|
||||
if (result!=0)
|
||||
{
|
||||
tuning = false;
|
||||
}
|
||||
if(!tuning)
|
||||
{ //we're done, set the tuning parameters
|
||||
ControllerPID.SetTunings(ControllerPID.TunedKp(),ControllerPID.TunedKi(),ControllerPID.TunedKd());
|
||||
ControllerPID.Mode(OFF);
|
||||
}
|
||||
}
|
||||
*/
|
||||
void loop() {
|
||||
UpdateBoilKettle();
|
||||
unsigned long now = millis();
|
||||
static unsigned long lastTime=now-2000;
|
||||
unsigned long timeChange = (now - lastTime);
|
||||
static byte lastoutput = LOW;
|
||||
static double lastKettleDuty = 0;
|
||||
static double lastKettleSetpoint = 0;
|
||||
static byte lastTemperature;
|
||||
|
||||
unsigned long elapsedTime = (millis() - lastRun);
|
||||
|
||||
if (Ethernet.linkStatus() == LinkON && elapsedTime >= UpdateInterval) {
|
||||
mqtt_client.loop();
|
||||
//if (!mqtt_client.connected()) ConnectMQTT();
|
||||
|
||||
SendSensorData();
|
||||
lastRun = millis();
|
||||
if(timeChange >= 2000) {
|
||||
CurrentTemp = 32 + 1.8 * thermoRTD.temperature(RNOMINAL_KETTLE, RREF_KETTLE);
|
||||
lastTime = now;
|
||||
}
|
||||
|
||||
if (!tuning) {
|
||||
run_kettle();
|
||||
} else {
|
||||
//run_tuning();
|
||||
}
|
||||
|
||||
byte output = boilPWM.Compute(KettleDuty);
|
||||
|
||||
if (output != lastoutput) {
|
||||
digitalWrite(kettlePWM, output);
|
||||
lastoutput = output;
|
||||
}
|
||||
|
||||
if (lastKettleDuty != KettleDuty || lastKettleSetpoint != KettleSetpoint || lastTemperature != (byte)CurrentTemp) {
|
||||
menu.update();
|
||||
lastKettleDuty = KettleDuty;
|
||||
lastKettleSetpoint = KettleSetpoint;
|
||||
lastTemperature = (byte)CurrentTemp;
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
void startAutoTune() {
|
||||
if(!tuning) {
|
||||
//Set the output to the desired starting frequency.
|
||||
KettleDuty=50;
|
||||
ControllerPID.SetNoiseBand(1);
|
||||
ControllerPID.SetOutputStep(30);
|
||||
ControllerPID.SetLookbackSec(20);
|
||||
ControllerPID.Mode(AUTOMATIC);
|
||||
tuning=true;
|
||||
}
|
||||
}
|
||||
|
||||
void stopAutoTune() {
|
||||
if(tuning) { //cancel autotune
|
||||
ControllerPID.Cancel();
|
||||
ControllerPID.Mode(OFF);
|
||||
tuning=false;
|
||||
}
|
||||
}
|
||||
|
||||
void SaveTunings() {
|
||||
eepromKp = ControllerPID.GetKp();
|
||||
eepromKi = ControllerPID.GetKi();
|
||||
eepromKd = ControllerPID.GetKd();
|
||||
}
|
||||
|
||||
void SerialSend()
|
||||
{
|
||||
Serial.print("setpoint: ");Serial.print(KettleSetpoint); Serial.print(" ");
|
||||
Serial.print("input: ");Serial.print(CurrentTemp); Serial.print(" ");
|
||||
Serial.print("output: ");Serial.print(KettleDuty); Serial.print(" ");
|
||||
if(tuning){
|
||||
Serial.println("tuning mode");
|
||||
} else {
|
||||
Serial.print("kp: ");Serial.print(ControllerPID.GetKp());Serial.print(" ");
|
||||
Serial.print("ki: ");Serial.print(ControllerPID.GetKi());Serial.print(" ");
|
||||
Serial.print("kd: ");Serial.print(ControllerPID.GetKd());Serial.println();
|
||||
}
|
||||
}
|
||||
|
||||
void SerialReceive()
|
||||
{
|
||||
if(Serial.available()) {
|
||||
String myinput;
|
||||
myinput = Serial.readString();
|
||||
myinput.trim();
|
||||
if(myinput=="start"){
|
||||
startAutoTune();
|
||||
} else if(myinput=="stop") {
|
||||
stopAutoTune();
|
||||
} else if(myinput=="save") {
|
||||
SaveTunings();
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
@ -1,72 +0,0 @@
|
||||
void ConnectMQTT() {
|
||||
static const char *password = MQTT_PASSWORD;
|
||||
static const char *user = MQTT_USER;
|
||||
Serial.println("connecting MQTT...");
|
||||
while (!mqtt_client.connect("brewhouse", user, password)) {
|
||||
Serial.print(".");
|
||||
delay(1000);
|
||||
}
|
||||
|
||||
Serial.println("\nconnected!");
|
||||
mqtt_client.subscribe("brewery/setpoint/bk");
|
||||
}
|
||||
|
||||
void MessageReceived(String &topic, String &payload) {
|
||||
Serial.println("incoming: " + topic + " - " + payload);
|
||||
|
||||
/** JSON Parser Setup */
|
||||
StaticJsonDocument<200> doc;
|
||||
|
||||
// Deserialize the JSON document
|
||||
DeserializationError error = deserializeJson(doc, payload);
|
||||
|
||||
// Test if parsing succeeds.
|
||||
if (error) {
|
||||
Serial.print(F("deserializeJson() failed: "));
|
||||
Serial.println(error.f_str());
|
||||
return;
|
||||
}
|
||||
char buf[30];
|
||||
strcpy(buf,TOPIC_PREFIX);
|
||||
strcat(buf,BOIL_SETPOINT_TOPIC);
|
||||
if (topic == buf) {
|
||||
// Update PWM setpoint.
|
||||
String name = doc["entity"];
|
||||
String setting = doc["setpoint"];
|
||||
|
||||
KettleDuty = setting.toInt();
|
||||
String unit = doc["units"];
|
||||
|
||||
Serial.println("Updating setpoint for " + name + " to " + setting + " " + unit);
|
||||
}
|
||||
}
|
||||
|
||||
void SetupMQTT(const char *broker) {
|
||||
// Note: Local domain names (e.g. "Computer.local" on OSX) are not supported
|
||||
// by Arduino. You need to set the IP address directly.
|
||||
Serial.println("Setup MQTT client.");
|
||||
mqtt_client.begin(broker, net);
|
||||
mqtt_client.onMessage(MessageReceived);
|
||||
|
||||
ConnectMQTT();
|
||||
}
|
||||
|
||||
static void SendSensorData() {
|
||||
Serial.println("Sending data...");
|
||||
|
||||
// NOTE: max message length is 250 bytes.
|
||||
StaticJsonDocument<200> doc;
|
||||
|
||||
doc["entity"] = "boil_kettle";
|
||||
doc["setpoint"] = KettleDuty;
|
||||
doc["units"] = "%";
|
||||
|
||||
String jstr;
|
||||
serializeJson(doc, jstr);
|
||||
|
||||
String topic = TOPIC_PREFIX;
|
||||
topic += "sensor/boil_kettle";
|
||||
|
||||
mqtt_client.publish(topic, jstr);
|
||||
|
||||
}
|
1
boil_kettle/src/Arduino-PID-Library
Submodule
1
boil_kettle/src/Arduino-PID-Library
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 7787498eda8955288e99a18d02581a425203bac5
|
35
boil_kettle/src/datatypes.h
Normal file
35
boil_kettle/src/datatypes.h
Normal file
@ -0,0 +1,35 @@
|
||||
#ifndef DATATYPES_h
|
||||
#define DATATYPES_h
|
||||
|
||||
struct Vessel {
|
||||
char name[16];
|
||||
char setpoint[20];
|
||||
char sensor[20];
|
||||
double Rref;
|
||||
double RNominal;
|
||||
};
|
||||
|
||||
struct Topic {
|
||||
char root[10];
|
||||
Vessel mash;
|
||||
Vessel boil;
|
||||
};
|
||||
|
||||
struct Mqtt {
|
||||
IPAddress broker;
|
||||
char user[10];
|
||||
char password[21];
|
||||
Topic topic;
|
||||
};
|
||||
|
||||
struct ConfigData {
|
||||
Mqtt mqtt;
|
||||
byte mac[6];
|
||||
IPAddress ip;
|
||||
int interval;
|
||||
int period;
|
||||
double threshold;
|
||||
double hysteresis;
|
||||
};
|
||||
|
||||
#endif
|
88
boil_kettle/src/functions.h
Normal file
88
boil_kettle/src/functions.h
Normal file
@ -0,0 +1,88 @@
|
||||
#include "../globals.h"
|
||||
|
||||
// Interrupt function to run when encoder is turned.
|
||||
//
|
||||
// Increases/decreases the kettle output to a max
|
||||
// of 100% and minimum of 0%.
|
||||
void doEncoder()
|
||||
{
|
||||
uint8_t result = rotary.read();
|
||||
int inc;
|
||||
|
||||
if (result) {
|
||||
uint8_t speed = rotary.speed();
|
||||
speed >= 10 ? inc = 5 : inc = 1;
|
||||
if (result == DIR_CCW) inc = inc * -1;
|
||||
|
||||
SettingChanged = true;
|
||||
|
||||
if (KettleController.Mode() == MANUAL) {
|
||||
uint8_t KettleDuty = (uint8_t)KettleController.Power();
|
||||
KettleDuty = max(0, min((KettleDuty / inc) * inc + inc, 100));
|
||||
KettleController.Power((double)KettleDuty);
|
||||
} else if (KettleController.Mode() == AUTOMATIC) {
|
||||
uint8_t KettleTemp = (uint8_t)KettleController.Setpoint();
|
||||
KettleTemp = max(0, min((KettleTemp / inc) * inc + inc, 220));
|
||||
KettleController.Setpoint((double)KettleTemp);
|
||||
}
|
||||
} else {
|
||||
SettingChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Return a character array to represent the
|
||||
// state of the kettle.
|
||||
char* ShowKettleState() {
|
||||
if (KettleController.Mode() == MANUAL) {
|
||||
return (char*)"Kettle: Manual";
|
||||
} else if (KettleController.Mode() == AUTOMATIC) {
|
||||
return (char*)"Kettle: Auto";
|
||||
} else {
|
||||
return (char*)"Kettle: Off";
|
||||
}
|
||||
}
|
||||
|
||||
char* ShowKettleSetting() {
|
||||
static char LCD_Line[21];
|
||||
char setting[4];
|
||||
if (KettleController.Mode() == MANUAL) {
|
||||
strcpy(LCD_Line, (char*)"Kettle Power: ");
|
||||
itoa(KettleController.Power(), setting, 10);
|
||||
strcat(LCD_Line, setting);
|
||||
strcat(LCD_Line, (char*)"%");
|
||||
|
||||
return LCD_Line;
|
||||
} else if (KettleController.Mode() == AUTOMATIC) {
|
||||
strcpy(LCD_Line, (char*)"Kettle Temp: ");
|
||||
itoa(KettleController.Setpoint(), setting, 10);
|
||||
strcat(LCD_Line, setting);
|
||||
strcat(LCD_Line, (char*)"F");
|
||||
return LCD_Line;
|
||||
} else {
|
||||
return (char*)"It's Off stoopid";
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateBoilKettle(){
|
||||
|
||||
if (Enter.pressed()) {
|
||||
Serial.println("Pressed");
|
||||
KettleController.CycleMode();
|
||||
SettingChanged = true;
|
||||
}
|
||||
|
||||
if (SettingChanged) {
|
||||
menu.update();
|
||||
SettingChanged = false;
|
||||
}
|
||||
|
||||
if (KettleController.Mode() != OFF) {
|
||||
// Compute will return false if MANUAL is set so duty will not
|
||||
// be changed here.
|
||||
KettleController.Compute();
|
||||
KettleDuty = KettleController.Power();
|
||||
digitalWrite(O_PWM, boilPWM.compute(KettleDuty));
|
||||
} else {
|
||||
digitalWrite(O_PWM, boilPWM.compute(0));
|
||||
}
|
||||
}
|
79
boil_kettle/src/mqtt.h
Normal file
79
boil_kettle/src/mqtt.h
Normal file
@ -0,0 +1,79 @@
|
||||
#include "../config.h"
|
||||
#include "../globals.h"
|
||||
|
||||
void ConnectMQTT() {
|
||||
ConfigData config;
|
||||
EEPROM.get(ConfAddress, config);
|
||||
|
||||
static const char *user = config.mqtt.user;
|
||||
static const char *password = config.mqtt.password;
|
||||
//config.mqtt.topic.root
|
||||
//config.mqtt.broker
|
||||
|
||||
while (!mqtt_client.connect("brewhouse", user, password)) {
|
||||
Serial.print(".");
|
||||
delay(1000);
|
||||
}
|
||||
|
||||
char topic[30];
|
||||
strcpy(topic,config.mqtt.topic.root);
|
||||
strcat(topic,config.mqtt.topic.boil.setpoint);
|
||||
mqtt_client.subscribe(topic);
|
||||
|
||||
strcpy(topic,config.mqtt.topic.root);
|
||||
strcat(topic,config.mqtt.topic.mash.setpoint);
|
||||
mqtt_client.subscribe(topic);
|
||||
}
|
||||
|
||||
void MessageReceived(char* topic, byte* payload, unsigned int length) {
|
||||
ConfigData config;
|
||||
EEPROM.get(ConfAddress, config);
|
||||
|
||||
char buf[30];
|
||||
strcpy(buf,config.mqtt.topic.root);
|
||||
strcat(buf,config.mqtt.topic.boil.setpoint);
|
||||
|
||||
char msg[length+1];
|
||||
|
||||
for (int i=0;i<length;i++) {
|
||||
msg[i] = (char)payload[i];
|
||||
}
|
||||
msg[length] = 0;
|
||||
|
||||
if (strcmp(topic, buf) == 0) {
|
||||
cJSON *monitor_json = cJSON_Parse(msg);
|
||||
const cJSON *name = NULL;
|
||||
const cJSON *setting = NULL;
|
||||
const cJSON *unit = NULL;
|
||||
|
||||
// Update PWM setpoint.
|
||||
setting = cJSON_GetObjectItemCaseSensitive(monitor_json, "setpoint");
|
||||
KettleDuty = setting->valueint;
|
||||
cJSON_Delete(monitor_json);
|
||||
}
|
||||
}
|
||||
|
||||
static void SendSensorData() {
|
||||
ConfigData config;
|
||||
EEPROM.get(ConfAddress, config);
|
||||
|
||||
char *string = NULL;
|
||||
cJSON *entity = NULL;
|
||||
cJSON *setpoint = NULL;
|
||||
cJSON *units = NULL;
|
||||
|
||||
cJSON *monitor = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(monitor, "entity", config.mqtt.topic.boil.name);
|
||||
cJSON_AddNumberToObject(monitor, "setpoint", KettleDuty);
|
||||
cJSON_AddStringToObject(monitor, "units", "%");
|
||||
char *msg = cJSON_Print(monitor);
|
||||
|
||||
char topic[30];
|
||||
strcpy(topic,config.mqtt.topic.root);
|
||||
strcat(topic,config.mqtt.topic.boil.sensor);
|
||||
|
||||
|
||||
mqtt_client.publish(topic, msg);
|
||||
cJSON_Delete(monitor);
|
||||
|
||||
}
|
@ -17,11 +17,10 @@ class slowPWM {
|
||||
lastSwitchTime = 0;
|
||||
outputState = LOW;
|
||||
pinMode(pin, OUTPUT);
|
||||
Serial.println("Setup PWM");
|
||||
}
|
||||
|
||||
void compute(byte duty) {
|
||||
unsigned long onTime = (duty * period) / 100;
|
||||
byte Compute(double duty) {
|
||||
unsigned long onTime = (duty * period) / 1000;
|
||||
unsigned long offTime = period - onTime;
|
||||
unsigned long currentTime = millis();
|
||||
|
||||
@ -35,7 +34,8 @@ class slowPWM {
|
||||
lastSwitchTime = currentTime;
|
||||
outputState = HIGH;
|
||||
}
|
||||
digitalWrite(outputPin, outputState);
|
||||
|
||||
return outputState;
|
||||
}
|
||||
};
|
||||
#endif
|
82
boil_kettle/src/thermoControl/thermoControl.cpp
Normal file
82
boil_kettle/src/thermoControl/thermoControl.cpp
Normal file
@ -0,0 +1,82 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Adafruit_MAX31865.h>
|
||||
#include "thermoControl.h"
|
||||
|
||||
thermoControl::thermoControl(double* current_temp, double* setpoint, double* power, modes mode) {
|
||||
outMax = 100;
|
||||
outMin = 10;
|
||||
OpMode = mode;
|
||||
SampleInterval = 100;
|
||||
lastTime = millis()-SampleInterval;
|
||||
output_pwm = power;
|
||||
control_temp = setpoint;
|
||||
actual_temp = current_temp;
|
||||
hysteresis = 1.0;
|
||||
max_pwr_threshold = 5.0;
|
||||
Serial.println("Controller Started");
|
||||
}
|
||||
|
||||
bool thermoControl::Compute() {
|
||||
unsigned long now = millis();
|
||||
unsigned long timeChange = (now - lastTime);
|
||||
|
||||
if(timeChange >= SampleInterval && OpMode == AUTOMATIC) {
|
||||
double error = *control_temp - *actual_temp;
|
||||
|
||||
if (error >= max_pwr_threshold) {
|
||||
*output_pwm = outMax;
|
||||
} else if (error > hysteresis) {
|
||||
*output_pwm = 100 * error / max_pwr_threshold;
|
||||
if (*output_pwm > 100) *output_pwm = 100;
|
||||
if (*output_pwm < 0) *output_pwm = 0;
|
||||
} else {
|
||||
*output_pwm = 0;
|
||||
}
|
||||
|
||||
lastTime = now;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void thermoControl::SampleTime(int NewSampleTime) {
|
||||
if (NewSampleTime > 0) {
|
||||
SampleInterval = NewSampleTime;
|
||||
}
|
||||
}
|
||||
|
||||
void thermoControl::PowerLimits(double Max, double Min) {
|
||||
if(Min >= Max) return;
|
||||
outMax = Max;
|
||||
outMin = Min;
|
||||
}
|
||||
|
||||
void thermoControl::Hysteresis(double hys) {
|
||||
hysteresis = hys;
|
||||
}
|
||||
|
||||
void thermoControl::ThreshPWR(double thresh) {
|
||||
max_pwr_threshold = thresh;
|
||||
}
|
||||
|
||||
void thermoControl::Mode(modes newMode) {
|
||||
Serial.println(newMode);
|
||||
OpMode = newMode;
|
||||
}
|
||||
|
||||
modes thermoControl::Mode() {
|
||||
return OpMode;
|
||||
}
|
||||
|
||||
modes thermoControl::CycleMode() {
|
||||
if (OpMode + 1 == OVERFLOW) {
|
||||
OpMode = (modes)(0);
|
||||
} else {
|
||||
OpMode = (modes)(OpMode + 1);
|
||||
}
|
||||
return OpMode;
|
||||
}
|
||||
|
41
boil_kettle/src/thermoControl/thermoControl.h
Normal file
41
boil_kettle/src/thermoControl/thermoControl.h
Normal file
@ -0,0 +1,41 @@
|
||||
#ifndef THERMOCONTROL_h
|
||||
#define THERMOCONTROL_h
|
||||
|
||||
#include <Adafruit_MAX31865.h>
|
||||
|
||||
enum modes : uint8_t {OFF, AUTOMATIC, MANUAL, OVERFLOW};
|
||||
|
||||
class thermoControl {
|
||||
private:
|
||||
double *output_pwm;
|
||||
double *control_temp;
|
||||
double * actual_temp;
|
||||
double current_temp;
|
||||
double hysteresis;
|
||||
double max_pwr_threshold;
|
||||
int outMax;
|
||||
int outMin;
|
||||
int SampleInterval;
|
||||
unsigned long lastTime;
|
||||
modes OpMode;
|
||||
|
||||
public:
|
||||
thermoControl(double*, double*, double*, modes);
|
||||
void nope();
|
||||
bool Compute();
|
||||
double Power();
|
||||
void Power(double);
|
||||
double Setpoint();
|
||||
void Setpoint(double);
|
||||
void SampleTime(int);
|
||||
void PowerLimits(double, double);
|
||||
void Hysteresis(double);
|
||||
void Hysteresis();
|
||||
void ThreshPWR(double);
|
||||
void ThreshPWR();
|
||||
void Mode(modes);
|
||||
modes Mode();
|
||||
modes CycleMode();
|
||||
};
|
||||
|
||||
#endif
|
BIN
docs/Pinout-Mega2560rev3_latest.pdf
Normal file
BIN
docs/Pinout-Mega2560rev3_latest.pdf
Normal file
Binary file not shown.
BIN
docs/menu.png
Normal file
BIN
docs/menu.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
11
docs/pins.txt
Normal file
11
docs/pins.txt
Normal file
@ -0,0 +1,11 @@
|
||||
// Pin Definitions
|
||||
#define encoderCLK 2
|
||||
#define encoderDT 3
|
||||
#define encoderBTN 4
|
||||
#define kettlePWM 5
|
||||
|
||||
#define kettleRTDCS 47
|
||||
#define mashRTDCS 48
|
||||
|
||||
LCD display:
|
||||
SDA & SCL pins are 20 & 21
|
1
eeprom_setup/.gitignore
vendored
Normal file
1
eeprom_setup/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.h
|
49
eeprom_setup/eeprom_setup.ino
Normal file
49
eeprom_setup/eeprom_setup.ino
Normal file
@ -0,0 +1,49 @@
|
||||
#include <EEPROM.h>
|
||||
#include <Ethernet.h>
|
||||
|
||||
#include "config.h" // Symlinked from boil_kettle sketch
|
||||
#include "datatypes.h" //
|
||||
|
||||
void setup() {
|
||||
Vessel BoilKettle;
|
||||
strcpy(BoilKettle.name, "boil_kettle");
|
||||
strcpy(BoilKettle.setpoint, BOIL_SETPOINT_TOPIC);
|
||||
strcpy(BoilKettle.sensor, BOIL_ACTUAL_TOPIC);
|
||||
BoilKettle.Rref = RREF_KETTLE;
|
||||
BoilKettle.RNominal = RNOMINAL_KETTLE;
|
||||
|
||||
// Vessel MashTun {mashname, MASH_SETPOINT_TOPIC, MASH_ACTUAL_TOPIC, RREF_MASH, RNOMINAL_MASH};
|
||||
Vessel MashTun;
|
||||
strcpy(MashTun.name, "mash_tun");
|
||||
strcpy(MashTun.setpoint, MASH_SETPOINT_TOPIC);
|
||||
strcpy(MashTun.sensor, MASH_ACTUAL_TOPIC);
|
||||
MashTun.Rref = RREF_MASH;
|
||||
MashTun.RNominal = RNOMINAL_MASH;
|
||||
|
||||
Topic mqtt_topics;
|
||||
strcpy(mqtt_topics.root,TOPIC_ROOT);
|
||||
mqtt_topics.mash = MashTun;
|
||||
mqtt_topics.boil = BoilKettle;
|
||||
|
||||
Mqtt mqtt_data;
|
||||
mqtt_data.broker = MQTT_BROKER;
|
||||
strcpy(mqtt_data.user, MQTT_USER);
|
||||
strcpy(mqtt_data.password, MQTT_PASSWORD);
|
||||
mqtt_data.topic = mqtt_topics;
|
||||
|
||||
ConfigData conf;
|
||||
conf.mqtt = mqtt_data;
|
||||
memcpy(conf.mac, mac, sizeof(mac[0])*6);
|
||||
conf.ip = ip;
|
||||
conf.interval = UpdateInterval;
|
||||
conf.period = PeriodPWM;
|
||||
conf.threshold = ThreshPWR;
|
||||
conf.hysteresis = Hysteresis;
|
||||
|
||||
EEPROM.put(0, conf);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
}
|
||||
|
@ -1,22 +1,25 @@
|
||||
- alias: Set BK Input Value
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: "brewery/sensor/boil_kettle"
|
||||
action:
|
||||
service: input_select.select_option
|
||||
data:
|
||||
entity_id: input_select.boil_kettle_pwm
|
||||
option: "{{ trigger.payload.setpoint }}"
|
||||
|
||||
# This automation script runs when the thermostat mode selector is changed.
|
||||
# It publishes its value to the same MQTT topic it is also subscribed to.
|
||||
- alias: Set BK PWM
|
||||
trigger:
|
||||
platform: state
|
||||
entity_id: input_select.boil_kettle_pwm
|
||||
action:
|
||||
service: mqtt.publish
|
||||
alias: Send iSpindel
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: sensor.ispindel001_gravity,sensor.ispindel002_gravity
|
||||
condition: []
|
||||
action:
|
||||
- service: rest_command.send_ispindel
|
||||
data:
|
||||
topic: "brewery/setpoint/bk"
|
||||
retain: true
|
||||
payload: "setpoint: {{ states('input_select.thermostat_mode') }}"
|
||||
name: "{{ device_attr(device_id(trigger.entity_id), \"name\") }}"
|
||||
temp: >-
|
||||
{% macro sensor(n,s) -%}sensor.{{ device_attr(device_id(n), "name") |
|
||||
lower }}_{{s}}{%- endmacro %}
|
||||
{{states(sensor(trigger.entity_id,"temperature"))}}
|
||||
temp_unit: C
|
||||
gravity: >-
|
||||
{% macro sensor(n,s) -%}sensor.{{ device_attr(device_id(n), "name") |
|
||||
lower }}_{{s}}{%- endmacro %}
|
||||
{{states(sensor(trigger.entity_id,"gravity"))}}
|
||||
gravity_unit: P
|
||||
battery: >-
|
||||
{% macro sensor(n,s) -%}sensor.{{ device_attr(device_id(n), "name") |
|
||||
lower }}_{{s}}{%- endmacro %}
|
||||
{{states(sensor(trigger.entity_id,"battery_voltage"))}}
|
||||
mode: single
|
||||
|
5
home_assistant/rest_command.yaml
Normal file
5
home_assistant/rest_command.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
send_ispindel:
|
||||
url: !secret BrewfatherEndpoint
|
||||
method: POST
|
||||
content_type: "application/json"
|
||||
payload: '{ "name":"{{ name }}", "temp":{{ temp }}, "temp_unit":"F", "gravity":{{ gravity }}, "gravity_unit": "P", "battery": {{ battery }} }'
|
1
temp_controller/.gitignore
vendored
Normal file
1
temp_controller/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
config.h
|
13
temp_controller/README.md
Normal file
13
temp_controller/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Temperature Control
|
||||
|
||||
Enables basic thermostat control for fermentation, kegerator, etc.
|
||||
|
||||
## Requirements
|
||||
Arduino Nano (clone) with ethernet shield.
|
||||
|
||||
For programing the board:
|
||||
- Board : Arduino Nano
|
||||
- Processor : ATmega328P
|
||||
- Port : /dev/ttyUSB0
|
||||
|
||||
This board/shield combo works with the [EthernetENC](https://github.com/jandrassy/EthernetENC/wiki) library.
|
53
temp_controller/temp_controller.ino
Normal file
53
temp_controller/temp_controller.ino
Normal file
@ -0,0 +1,53 @@
|
||||
/* Built-in */
|
||||
#include <SPI.h>
|
||||
|
||||
/* Extra Libraries */
|
||||
#include <EthernetENC.h>
|
||||
|
||||
/* Local includes */
|
||||
#include "config.h"
|
||||
|
||||
// Set 10 minute compressor delay if not otherwise defined.
|
||||
#ifndef COMP_DELAY
|
||||
#define COMP_DELAY 600000
|
||||
#endif
|
||||
|
||||
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
|
||||
IPAddress ip(192, 168, 1, 177);
|
||||
IPAddress myDns(192, 168, 1, 1);
|
||||
|
||||
// Initialize the Ethernet client library
|
||||
// with the IP address and port of the server
|
||||
// that you want to connect to (port 80 is default for HTTP):
|
||||
EthernetClient client;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(9600);
|
||||
while (!Serial) {
|
||||
; // wait for serial port to connect. Needed for native USB port only
|
||||
}
|
||||
|
||||
// start the Ethernet connection:
|
||||
Serial.println("Initialize Ethernet with DHCP:");
|
||||
if (Ethernet.begin(mac) == 0) {
|
||||
Serial.println("Failed to configure Ethernet using DHCP");
|
||||
// Check for Ethernet hardware present
|
||||
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
|
||||
Serial.println("Ethernet shield was not found. Sorry, can't run without hardware. :(");
|
||||
while (true) {
|
||||
delay(1); // do nothing, no point running without Ethernet hardware
|
||||
}
|
||||
}
|
||||
if (Ethernet.linkStatus() == LinkOFF) {
|
||||
Serial.println("Ethernet cable is not connected.");
|
||||
}
|
||||
// try to congifure using IP address instead of DHCP:
|
||||
Ethernet.begin(mac, ip, myDns);
|
||||
} else {
|
||||
Serial.print(" DHCP assigned IP ");
|
||||
Serial.println(Ethernet.localIP());
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
}
|
Loading…
Reference in New Issue
Block a user