Compare commits

...

53 Commits

Author SHA1 Message Date
Chris Giacofei
c98aebc716 Check-In iSpindel config for HomeAssistant. 2024-04-29 12:51:17 -04:00
Chris Giacofei
2bd12374fc Delete unused file. 2024-04-29 12:50:28 -04:00
Chris Giacofei
179ea1aeca Not using any MQTT stuff for now. 2024-04-29 12:49:26 -04:00
Chris Giacofei
0e74d66125 Change check for out of bounds setpoints.
Makes more sense this way, and is compatible with more boards.

ESP32 chips don't seem to like min/max with mixed types.
2024-04-29 12:47:49 -04:00
Chris Giacofei
ff8129f400 Just declare the enum. 2024-04-29 12:46:41 -04:00
Chris Giacofei
81133f2221 Unused libries go away. 2024-04-26 08:57:39 -04:00
Chris Giacofei
6730f9f390 Latest boil kettle code. 2024-04-26 08:55:20 -04:00
Chris Giacofei
31b23910f0 Added PID submodule. 2024-04-26 08:48:05 -04:00
Chris Giacofei
b302480610 Remove to add as submodule. 2024-04-26 08:47:39 -04:00
Chris Giacofei
99600c9135 rearrange custom libraries. 2024-04-26 08:41:06 -04:00
1fb657f491 Updated libraries. 2022-01-24 19:40:23 +00:00
216e80e642 Don't start mqtt unless network is connected.
Also comments.
2022-01-24 11:41:52 -05:00
8565c87ce4 Kettle power won't change on compute if mode is manual.
There's no point in checking first.
2022-01-24 11:41:09 -05:00
85c710e3b7 Don't update the menu if nothing actually changed. 2022-01-24 11:40:12 -05:00
045d5a8feb This is simpler and far easier to read. 2022-01-24 11:39:18 -05:00
484db54d89 Encoder interrupt handles auto mode.
Add logic for changing the setpoint temperature
as well as duty cycle.
2022-01-24 11:11:10 -05:00
c5b19e9103 Sketch name needs to match the folder. 2022-01-24 10:57:33 -05:00
a0a25f05e2 Put more conplex datatype definitions in separate file. 2022-01-24 10:57:09 -05:00
e76ca05674 Ignore all the .h files. 2022-01-24 10:35:20 -05:00
fbfe28ab3a Make into a valid sketch. 2022-01-24 10:35:07 -05:00
50bee2daa0 Improved readability... I hope.
Arduino sketch file now only contains #includes and setup/loop
functions. Everything else moved to separate included files.
2022-01-24 10:30:57 -05:00
288c7dfc80 Need to load struct items separately.
Byte array can't be assigned directly.
2022-01-24 08:30:31 -05:00
05280526b0 Need Adafruit RTD control class. 2022-01-24 08:29:14 -05:00
e2b0205709 That's not the right data type. 2022-01-24 08:28:43 -05:00
be781f1275 Use overloaded functions for get/set. 2022-01-21 11:44:28 -05:00
9a535c665b Use simpler constructor. 2022-01-21 11:35:09 -05:00
8b72a16203 Made a much more involved kettle control class.
All the input/output tracking is handled with class members
instead of passing pointers to global variables.
2022-01-21 10:56:41 -05:00
c7857c1b03 Changed pin names because I can. 2022-01-21 10:55:13 -05:00
e12f3e262f Get rid of sprintf. 2022-01-20 14:13:49 -05:00
3a1d2ad3ad Use settings from EEPROM. 2022-01-20 14:13:20 -05:00
f75cbe5f6c Use begin() method for temp controller. 2022-01-20 14:12:00 -05:00
a1eb11b81a Merge branch 'use-eeprom' into temp-control 2022-01-20 10:57:01 -05:00
24b3cdae1c Cleanup 2022-01-20 10:21:55 -05:00
9818e77866 Ignore stuff. 2022-01-20 10:02:55 -05:00
2673fa7fa3 Use a separate sketch to load EEPROM. 2022-01-20 09:53:25 -05:00
fb0afa7e43 Merge remote-tracking branch 'origin/use-eeprom' into no-more-strings 2022-01-20 09:33:19 -05:00
421a0c6580 Change libraries used for mqtt and json parsing.
Not using strings for data any more.
2022-01-20 08:40:32 -05:00
8750bceead Load config to EEPROM 2022-01-19 16:30:26 -05:00
1aef2aa15f LCD_Line will return garbage if not static. 2022-01-19 11:03:21 -05:00
c4fb9fa0b3 Use integer math.
This feels like a good idea. ?
2022-01-19 09:47:59 -05:00
2a70f6e89a Update LCD outputs. 2022-01-19 09:08:42 -05:00
e0d2d0aa52 Fix type mismatches. 2022-01-19 07:42:15 -05:00
8954a94737 Merge branch 'master' into temp-control 2022-01-19 07:14:35 -05:00
bc93148db6 Update ignored files. 2022-01-19 07:09:07 -05:00
18c43187cb Initial junk to get temp control working.
Totally not working yet, just getting it pushed
to the remote
2022-01-18 19:45:05 -05:00
6a0de023c4 Now it's gone.
For reals this tim.
2022-01-17 17:52:34 -05:00
3c00630461 You don't belong here. 2022-01-17 17:51:09 -05:00
d2972b0a40 Crap. 2022-01-17 17:50:02 -05:00
b782cfff86 Add compressor delay definition. 2022-01-17 17:43:13 -05:00
5d876b035b Add dir and ino file for temp controllers.
Included basic working example to setup ethernet
and get an IP address using DHCP.
2022-01-17 17:27:09 -05:00
84981a29dd Update the changelog. 2022-01-15 20:40:07 -05:00
180e3b065f Make slowPWM::compute return state instead of changing pin.
I just like it better this way.
2022-01-15 18:10:46 -05:00
8908eccdb6 Add some docs 2022-01-14 21:10:51 -05:00
24 changed files with 684 additions and 173 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
tags
*.zip
.geany*

3
.gitmodules vendored Normal file
View 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

View File

@ -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.

View File

@ -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/)

View File

@ -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();
}
}
}
*/

View File

@ -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);
}

@ -0,0 +1 @@
Subproject commit 7787498eda8955288e99a18d02581a425203bac5

View 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

View 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
View 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);
}

View File

@ -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

View 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;
}

View 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

Binary file not shown.

BIN
docs/menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

11
docs/pins.txt Normal file
View 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
View File

@ -0,0 +1 @@
*.h

View 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() {
}

View File

@ -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

View 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
View File

@ -0,0 +1 @@
config.h

13
temp_controller/README.md Normal file
View 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.

View 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() {
}