mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-09 09:19:51 +03:00
first commit
This commit is contained in:
13
src/LICENSE.md
Normal file
13
src/LICENSE.md
Normal file
@@ -0,0 +1,13 @@
|
||||
#### Copyright 2018 [Paul Derbsyhire](mailto:dev@derbyshire.nl). All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met :
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and / or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation are those of the
|
||||
authors and should not be interpreted as representing official policies, either expressed
|
||||
or implied, of [Paul Derbyshire](mailto:dev@derbyshire.nl).
|
||||
578
src/boiler.ino
Normal file
578
src/boiler.ino
Normal file
@@ -0,0 +1,578 @@
|
||||
/*
|
||||
* Boiler Project
|
||||
* Paul Derbyshire - May 2018 - https://github.com/proddy/EMS-ESP-Boiler
|
||||
*
|
||||
* Acknowledgments too https://github.com/susisstrolch/EMS-ESP12 and https://github.com/jeelabs/esp-link
|
||||
*/
|
||||
|
||||
// local libraries
|
||||
#include "ems.h"
|
||||
#include "emsuart.h"
|
||||
|
||||
// private libraries
|
||||
#include <ESPHelper.h>
|
||||
|
||||
// public libraries
|
||||
#include <ArduinoJson.h>
|
||||
#include <Ticker.h> // https://github.com/sstaub/Ticker
|
||||
|
||||
// private function prototypes
|
||||
void heartbeat();
|
||||
void systemCheck();
|
||||
void publishValues();
|
||||
void _showerColdShotStart();
|
||||
void _showerColdShotStop();
|
||||
|
||||
// hostname is also used as the MQTT topic identifier (home/<hostname>)
|
||||
#define HOSTNAME "boiler"
|
||||
|
||||
// Project commands for telnet
|
||||
// Note: ?, *, $, ! and & are reserved
|
||||
#define PROJECT_CMDS \
|
||||
"s=show statistics\n\r" \
|
||||
"* q=toggle Verbose telegram logging\n\r" \
|
||||
"* m=publish stats to MQTT\n\r" \
|
||||
"* p=toggle Poll response\n\r" \
|
||||
"* T=toggle Thermostat suport on/off\n\r" \
|
||||
"* S=toggle Shower Timer on/off\n\r" \
|
||||
"* r [n] to request for data from EMS " \
|
||||
"(33=UBAParameterWW, 18=UBAMonitorFast, 19=UBAMonitorSlow, " \
|
||||
"34=UBAMonitorWWMessage, 91=RC20StatusMessage, 6=RC20Time)\n\r" \
|
||||
"* t [n] set thermostat temperature to n\n\r" \
|
||||
"* w [n] set boiler warm water temperature to n (min 30)\n\r" \
|
||||
"* a [n] activate boiler warm water on (n=1) or off (n=0)"
|
||||
|
||||
// GPIOs
|
||||
#define LED_RX D1 // (GPIO5 on nodemcu)
|
||||
#define LED_TX D2 // (GPIO4 on nodemcu)
|
||||
#define LED_ERR D3 // (GPIO0 on nodemcu)
|
||||
|
||||
// app specific - do not change
|
||||
#define MQTT_BOILER MQTT_BASE HOSTNAME "/"
|
||||
#define TOPIC_START MQTT_BOILER MQTT_TOPIC_START
|
||||
|
||||
#define TOPIC_THERMOSTAT MQTT_BOILER "thermostat"
|
||||
#define TOPIC_SHOWERTIME MQTT_BOILER "showertime"
|
||||
#define TOPIC_THERMOSTAT_TEMP MQTT_BOILER "thermostat_temp"
|
||||
|
||||
// all on
|
||||
#define BOILER_THERMOSTAT_ENABLED 1
|
||||
#define BOILER_SHOWER_ENABLED 1
|
||||
#define BOILER_SHOWER_TIMER 0
|
||||
|
||||
// shower settings
|
||||
const unsigned long SHOWER_PAUSE_TIME = 15000; // 15 seconds, max time if water is switched off & on during a shower
|
||||
const unsigned long SHOWER_MIN_DURATION = 120000; // 2 minutes, before recognizing its a shower
|
||||
const unsigned long SHOWER_MAX_DURATION = 420000; // 7 minutes, before trigger a shot of cold water
|
||||
const unsigned long SHOWER_OFF_DURATION = 3000; // 3 seconds long for cold water
|
||||
const uint8_t SHOWER_BURNPOWER_MIN = 80;
|
||||
|
||||
// for debugging...
|
||||
//const unsigned long SHOWER_MIN_DURATION = 10000; // 10 seconds
|
||||
//const unsigned long SHOWER_MAX_DURATION = 15000; // 15 seconds
|
||||
|
||||
typedef struct {
|
||||
bool wifi_connected;
|
||||
bool boiler_online;
|
||||
bool shower_enabled; // true if we want to report back on shower times
|
||||
bool shower_timer; // true if we want the cold water reminder
|
||||
} _Boiler_Status;
|
||||
|
||||
typedef struct {
|
||||
bool showerOn;
|
||||
unsigned long timerStart; // ms
|
||||
unsigned long timerPause; // ms
|
||||
unsigned long duration; // ms
|
||||
bool isColdShot; // true if we've just sent a jolt of cold water
|
||||
} _Boiler_Shower;
|
||||
|
||||
// ESPHelper
|
||||
netInfo homeNet = {.mqttHost = MQTT_IP,
|
||||
.mqttUser = MQTT_USER,
|
||||
.mqttPass = MQTT_PASS,
|
||||
.mqttPort = 1883,
|
||||
.ssid = WIFI_SSID,
|
||||
.pass = WIFI_PASSWORD
|
||||
};
|
||||
|
||||
ESPHelper myESP(&homeNet);
|
||||
|
||||
// store for overall system status
|
||||
_Boiler_Status Boiler_Status;
|
||||
_Boiler_Shower Boiler_Shower;
|
||||
|
||||
// Debugger to telnet
|
||||
#define myDebug(x, ...) myESP.printf(x, ##__VA_ARGS__);
|
||||
|
||||
// Timers
|
||||
Ticker updateHATimer(publishValues, 300000); // every 5 mins (300000) post HA values
|
||||
Ticker hearbeatTimer(heartbeat, 500); // changing onboard heartbeat led every 500ms
|
||||
Ticker systemCheckTimer(systemCheck, 10000); // every 10 seconds check if Boiler is online
|
||||
Ticker showerResetTimer(_showerColdShotStop, SHOWER_OFF_DURATION, 1); // timer for how long we turn off the hot water
|
||||
const unsigned long POLL_TIMEOUT_ERR = 10000; // if no signal from boiler for last 10 seconds, assume its offline
|
||||
bool heartbeat_state = false;
|
||||
|
||||
const unsigned long TX_HOLD_LED_TIME = 2000; // how long to hold the Tx LED because its so quick
|
||||
|
||||
unsigned long timestamp; // for internal timings, via millis()
|
||||
static int connectionStatus = NO_CONNECTION;
|
||||
bool startMQTTsent = false;
|
||||
|
||||
// Show command - display stats on an 's' command
|
||||
void showInfo() {
|
||||
char s[10]; // for formatting floats using the _float_to_char() function
|
||||
|
||||
// General stats from EMS bus
|
||||
myDebug("EMS Bus stats:\n");
|
||||
myDebug(" Poll is %s, Shower is %s, Shower timer is %s, RxPgks=%d, TxPkgs=%d, #CrcErrors=%d",
|
||||
((EMS_Sys_Status.emsPollEnabled) ? "enabled" : "disabled"),
|
||||
((Boiler_Status.shower_enabled) ? "enabled" : "disabled"),
|
||||
((Boiler_Status.shower_timer) ? "enabled" : "disabled"),
|
||||
EMS_Sys_Status.emsRxPgks,
|
||||
EMS_Sys_Status.emsTxPkgs,
|
||||
EMS_Sys_Status.emxCrcErr);
|
||||
|
||||
myDebug(", RxStatus=");
|
||||
switch (EMS_Sys_Status.emsRxStatus) {
|
||||
case EMS_RX_IDLE:
|
||||
myDebug("idle");
|
||||
break;
|
||||
case EMS_RX_ACTIVE:
|
||||
myDebug("active");
|
||||
break;
|
||||
}
|
||||
|
||||
myDebug(", TxStatus=");
|
||||
switch (EMS_Sys_Status.emsTxStatus) {
|
||||
case EMS_TX_IDLE:
|
||||
myDebug("idle");
|
||||
break;
|
||||
case EMS_TX_PENDING:
|
||||
myDebug("pending");
|
||||
break;
|
||||
case EMS_TX_ACTIVE:
|
||||
myDebug("active");
|
||||
break;
|
||||
}
|
||||
|
||||
myDebug(", TxAction=");
|
||||
switch (EMS_TxTelegram.action) {
|
||||
case EMS_TX_READ:
|
||||
myDebug("read");
|
||||
break;
|
||||
case EMS_TX_WRITE:
|
||||
myDebug("write");
|
||||
break;
|
||||
case EMS_TX_VALIDATE:
|
||||
myDebug("validate");
|
||||
break;
|
||||
case EMS_TX_NONE:
|
||||
myDebug("none");
|
||||
break;
|
||||
}
|
||||
|
||||
myDebug("\nBoiler stats:\n");
|
||||
// UBAMonitorWWMessage & UBAParameterWW
|
||||
myDebug(" Warm Water activated: %s\n", (EMS_Boiler.wWActivated ? "yes" : "no"));
|
||||
myDebug(" Warm Water selected temperature: %d C\n", EMS_Boiler.wWSelTemp);
|
||||
myDebug(" Warm Water circulation pump available: %s\n", (EMS_Boiler.wWCircPump ? "yes" : "no"));
|
||||
myDebug(" Warm Water desired temperature: %d C\n", EMS_Boiler.wWDesiredTemp);
|
||||
myDebug(" Warm Water current temperature: %s C\n", _float_to_char(s, EMS_Boiler.wWCurTmp));
|
||||
myDebug(" Warm Water # starts: %d times\n", EMS_Boiler.wWStarts);
|
||||
myDebug(" Warm Water active time: %d days %d hours %d minutes\n",
|
||||
EMS_Boiler.wWWorkM / 1440,
|
||||
(EMS_Boiler.wWWorkM % 1440) / 60,
|
||||
EMS_Boiler.wWWorkM % 60);
|
||||
myDebug(" Warm Water 3-way valve: %s\n", EMS_Boiler.wWHeat ? "on" : "off");
|
||||
|
||||
// UBAMonitorFast
|
||||
myDebug(" Selected flow temperature: %d C\n", EMS_Boiler.selFlowTemp);
|
||||
myDebug(" Current flow temperature: %s C\n", _float_to_char(s, EMS_Boiler.curFlowTemp));
|
||||
myDebug(" Return temperature: %s C\n", _float_to_char(s, EMS_Boiler.retTemp));
|
||||
|
||||
myDebug(" Gas: %s\n", EMS_Boiler.burnGas ? "on" : "off"); // 0 -gas on
|
||||
myDebug(" Circulating pump: %s\n", EMS_Boiler.heatPmp ? "on" : "off"); // 5 - boiler circuit pump on
|
||||
myDebug(" Fan: %s\n", EMS_Boiler.fanWork ? "on" : "off"); // 2
|
||||
myDebug(" Ignition: %s\n", EMS_Boiler.ignWork ? "on" : "off"); // 3
|
||||
myDebug(" Circulation pump: %s\n", EMS_Boiler.wWCirc ? "on" : "off"); // 7
|
||||
myDebug(" Burner max power: %d %%\n", EMS_Boiler.selBurnPow);
|
||||
myDebug(" Burner current power: %d %%\n", EMS_Boiler.curBurnPow);
|
||||
myDebug(" Flame current: %s uA\n", _float_to_char(s, EMS_Boiler.flameCurr));
|
||||
myDebug(" System pressure: %s bar\n", _float_to_char(s, EMS_Boiler.sysPress));
|
||||
|
||||
// UBAMonitorSlow
|
||||
myDebug(" Outside temperature: %s C\n", _float_to_char(s, EMS_Boiler.extTemp));
|
||||
myDebug(" Boiler temperature: %s C\n", _float_to_char(s, EMS_Boiler.boilTemp));
|
||||
myDebug(" Pump modulation: %d %%\n", EMS_Boiler.pumpMod);
|
||||
myDebug(" # burner restarts: %d\n", EMS_Boiler.burnStarts);
|
||||
myDebug(" Total burner operating time: %d days %d hours %d minutes\n",
|
||||
EMS_Boiler.burnWorkMin / 1440,
|
||||
(EMS_Boiler.burnWorkMin % 1440) / 60,
|
||||
EMS_Boiler.burnWorkMin % 60);
|
||||
myDebug(" Total heat operating time: %d days %d hours %d minutes\n",
|
||||
EMS_Boiler.heatWorkMin / 1440,
|
||||
(EMS_Boiler.heatWorkMin % 1440) / 60,
|
||||
EMS_Boiler.heatWorkMin % 60);
|
||||
|
||||
// Thermostat stats
|
||||
if (EMS_Sys_Status.emsThermostatEnabled) {
|
||||
myDebug("Thermostat stats:\n Thermostat time is %02d:%02d:%02d %d/%d/%d\n",
|
||||
EMS_Thermostat.hour,
|
||||
EMS_Thermostat.minute,
|
||||
EMS_Thermostat.second,
|
||||
EMS_Thermostat.day,
|
||||
EMS_Thermostat.month,
|
||||
EMS_Thermostat.year + 2000);
|
||||
|
||||
myDebug(" Setpoint room temperature is %s C and current room temperature is %s C\n",
|
||||
_float_to_char(s, EMS_Thermostat.setpoint_roomTemp),
|
||||
_float_to_char(s, EMS_Thermostat.curr_roomTemp));
|
||||
}
|
||||
|
||||
if (Boiler_Status.shower_enabled) {
|
||||
// show the Shower Info
|
||||
myDebug("Shower stats:\n Shower is %s\n", (Boiler_Shower.showerOn ? "on" : "off"));
|
||||
char s[70];
|
||||
uint8_t sec = (uint8_t)((Boiler_Shower.duration / 1000) % 60);
|
||||
uint8_t min = (uint8_t)((Boiler_Shower.duration / (1000 * 60)) % 60);
|
||||
sprintf(s, " Last shower duration was %d minutes and %d %s\n", min, sec, (sec == 1) ? "second" : "seconds");
|
||||
myDebug(s);
|
||||
}
|
||||
|
||||
myDebug("\n");
|
||||
}
|
||||
|
||||
// send values to HA via MQTT
|
||||
void publishValues() {
|
||||
// only send values if we actually have them
|
||||
if (((int)EMS_Thermostat.curr_roomTemp == (int)0) || ((int)EMS_Thermostat.setpoint_roomTemp == (int)0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
myDebug("Publishing data to MQTT topics\n");
|
||||
|
||||
// build a JSON with the current temp and selected temp from the Thermostat
|
||||
StaticJsonBuffer<200> jsonBuffer;
|
||||
JsonObject & root = jsonBuffer.createObject();
|
||||
root["currtemp"] = (String)EMS_Thermostat.curr_roomTemp;
|
||||
root["seltemp"] = (String)EMS_Thermostat.setpoint_roomTemp;
|
||||
|
||||
char data[100];
|
||||
root.printTo(data, root.measureLength() + 1);
|
||||
myESP.publish(TOPIC_THERMOSTAT, data);
|
||||
}
|
||||
|
||||
|
||||
// extra commands options for telnet debug window
|
||||
void myDebugCallback() {
|
||||
char * cmd = myESP.consoleGetLastCommand();
|
||||
bool b;
|
||||
|
||||
switch (cmd[0]) {
|
||||
case 's':
|
||||
showInfo();
|
||||
break;
|
||||
case 'p':
|
||||
b = !ems_getPoll();
|
||||
ems_setPoll(b);
|
||||
break;
|
||||
case 'm':
|
||||
publishValues();
|
||||
break;
|
||||
case 'r': // read command for Boiler or Thermostat
|
||||
ems_doReadCommand((uint8_t)strtol(&cmd[2], 0, 16));
|
||||
break;
|
||||
case 't': // set thermostat temp
|
||||
ems_setThermostatTemp(strtof(&cmd[2], 0));
|
||||
break;
|
||||
case 'w': // set warm water temp
|
||||
ems_setWarmWaterTemp((uint8_t)strtol(&cmd[2], 0, 10));
|
||||
break;
|
||||
case 'q': // quiet
|
||||
b = !ems_getLogVerbose();
|
||||
ems_setLogVerbose(b);
|
||||
enableHeartbeat(b);
|
||||
break;
|
||||
case 'a': // set ww activate on or off
|
||||
if ((cmd[2] - '0') == 1) {
|
||||
ems_setWarmWaterActivated(true);
|
||||
} else if ((cmd[2] - '0') == 0) {
|
||||
ems_setWarmWaterActivated(false);
|
||||
}
|
||||
break;
|
||||
case 'T': // toggle Thermostat
|
||||
b = !ems_getThermostatEnabled();
|
||||
ems_setThermostatEnabled(b);
|
||||
break;
|
||||
case 'S': // toggle Shower timer support
|
||||
Boiler_Status.shower_enabled = !Boiler_Status.shower_enabled;
|
||||
myDebug("Shower timer is %s\n", Boiler_Status.shower_enabled ? "enabled" : "disabled");
|
||||
break;
|
||||
default:
|
||||
myDebug("Unknown command '%c'. Use ? for help.\n", cmd[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// MQTT Callback to handle incoming/outgoing changes
|
||||
void MQTTcallback(char * topic, byte * payload, uint8_t length) {
|
||||
// check if start is received, if so return boottime - defined in ESPHelper.h
|
||||
if (strcmp(topic, TOPIC_START) == 0) {
|
||||
payload[length] = '\0'; // add null terminator
|
||||
myDebug("MQTT topic boottime: %s\n", payload);
|
||||
myESP.setBoottime((char *)payload);
|
||||
return;
|
||||
}
|
||||
|
||||
// thermostat_temp
|
||||
if (strcmp(topic, TOPIC_THERMOSTAT_TEMP) == 0) {
|
||||
float f = strtof((char *)payload, 0);
|
||||
char s[10];
|
||||
myDebug("MQTT topic: thermostat_temp value %s\n", _float_to_char(s, f));
|
||||
ems_setThermostatTemp(f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// WifiCallback, called when a WiFi connect has successfully been established
|
||||
void WIFIcallback() {
|
||||
Boiler_Status.wifi_connected = true;
|
||||
|
||||
// turn off the LEDs since we've finished the boot loading
|
||||
digitalWrite(LED_RX, LOW);
|
||||
digitalWrite(LED_TX, LOW);
|
||||
digitalWrite(LED_ERR, LOW);
|
||||
|
||||
// when finally we're all set up, we can fire up the uart (this will enable the interrupts)
|
||||
emsuart_init();
|
||||
}
|
||||
|
||||
|
||||
void _initBoiler() {
|
||||
// default settings
|
||||
ems_setThermostatEnabled(BOILER_THERMOSTAT_ENABLED);
|
||||
Boiler_Status.shower_enabled = BOILER_SHOWER_ENABLED;
|
||||
Boiler_Status.shower_timer = BOILER_SHOWER_TIMER;
|
||||
|
||||
|
||||
// init boiler
|
||||
Boiler_Status.wifi_connected = false;
|
||||
Boiler_Status.boiler_online = true; // assume we have a connection, it will be checked in the loop() anyway
|
||||
|
||||
// init shower
|
||||
Boiler_Shower.timerStart = 0;
|
||||
Boiler_Shower.timerPause = 0;
|
||||
Boiler_Shower.duration = 0;
|
||||
Boiler_Shower.isColdShot = false;
|
||||
}
|
||||
|
||||
//
|
||||
// SETUP
|
||||
// Note: we don't init the UART here as we should wait until everything is loaded first. It's done in loop()
|
||||
//
|
||||
void setup() {
|
||||
// set pin for LEDs - start up with all lit up while we sort stuff out
|
||||
pinMode(LED_RX, OUTPUT);
|
||||
pinMode(LED_TX, OUTPUT);
|
||||
pinMode(LED_ERR, OUTPUT);
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
digitalWrite(LED_RX, HIGH);
|
||||
digitalWrite(LED_TX, HIGH);
|
||||
digitalWrite(LED_ERR, HIGH);
|
||||
|
||||
// Timers
|
||||
updateHATimer.start();
|
||||
systemCheckTimer.start();
|
||||
|
||||
// set up Wifi, MQTT, Telnet
|
||||
myESP.setWifiCallback(WIFIcallback);
|
||||
myESP.setMQTTCallback(MQTTcallback);
|
||||
myESP.addSubscription(TOPIC_START);
|
||||
myESP.addSubscription(TOPIC_THERMOSTAT);
|
||||
myESP.addSubscription(TOPIC_THERMOSTAT_TEMP);
|
||||
myESP.addSubscription(TOPIC_SHOWERTIME);
|
||||
myESP.consoleSetHelpProjectsCmds(PROJECT_CMDS);
|
||||
myESP.consoleSetCallBackProjectCmds(myDebugCallback);
|
||||
myESP.begin(HOSTNAME);
|
||||
|
||||
// init ems stats
|
||||
ems_init();
|
||||
|
||||
// init Boiler specific params
|
||||
_initBoiler();
|
||||
|
||||
// heartbeat, only if setting is enabled
|
||||
enableHeartbeat(ems_getLogVerbose());
|
||||
}
|
||||
|
||||
// flash ERR LEDs
|
||||
// Using a faster way to write to pins as digitalWrite does a lot of overhead like pin checking & disabling interrupts
|
||||
void showLEDs() {
|
||||
// update Ticker
|
||||
hearbeatTimer.update();
|
||||
|
||||
// hearbeat timer, using internal LED on board
|
||||
if (hearbeatTimer.counter() == 20)
|
||||
hearbeatTimer.interval(200);
|
||||
if (hearbeatTimer.counter() == 80)
|
||||
hearbeatTimer.interval(1000);
|
||||
|
||||
if (ems_getLogVerbose()) {
|
||||
// ERR LED
|
||||
if (!Boiler_Status.boiler_online) {
|
||||
WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + 4, (1 << LED_ERR)); // turn on
|
||||
EMS_Sys_Status.emsRxStatus = EMS_RX_IDLE;
|
||||
EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE;
|
||||
} else {
|
||||
WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + 8, (1 << LED_ERR)); // turn off
|
||||
}
|
||||
|
||||
// Rx LED
|
||||
WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + ((EMS_Sys_Status.emsRxStatus == EMS_RX_IDLE) ? 8 : 4), (1 << LED_RX));
|
||||
|
||||
// Tx LED
|
||||
// because sends are quick, if we did a recent send show the LED for a short while
|
||||
uint64_t t = (timestamp - EMS_Sys_Status.emsLastTx);
|
||||
WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + ((t < TX_HOLD_LED_TIME) ? 4 : 8), (1 << LED_TX));
|
||||
}
|
||||
}
|
||||
|
||||
// heartbeat callback to light up the LED, called via Ticker
|
||||
void heartbeat() {
|
||||
digitalWrite(LED_BUILTIN, heartbeat_state);
|
||||
heartbeat_state = !heartbeat_state;
|
||||
}
|
||||
|
||||
// enables or disables the heartbeat LED
|
||||
void enableHeartbeat(bool on) {
|
||||
heartbeat_state = (on) ? LOW : HIGH;
|
||||
heartbeat();
|
||||
if (on)
|
||||
hearbeatTimer.resume();
|
||||
else
|
||||
hearbeatTimer.pause();
|
||||
}
|
||||
|
||||
// do a healthcheck every now and then to see if we connections
|
||||
void systemCheck() {
|
||||
Boiler_Status.boiler_online = ((timestamp - EMS_Sys_Status.emsLastPoll) < POLL_TIMEOUT_ERR);
|
||||
if (!Boiler_Status.boiler_online) {
|
||||
myDebug("Error! Boiler unreachable. Please check connection. Retrying in 10 seconds.\n");
|
||||
}
|
||||
}
|
||||
|
||||
// turn off hot water to send a shot of cold
|
||||
void _showerColdShotStart() {
|
||||
myDebug("Shower: exceeded max shower time, doing a shot of cold...\n");
|
||||
ems_setWarmWaterActivated(false);
|
||||
Boiler_Shower.isColdShot = true;
|
||||
}
|
||||
|
||||
// turn back on the hot water for the shower
|
||||
void _showerColdShotStop() {
|
||||
if (Boiler_Shower.isColdShot) {
|
||||
myDebug("Shower: turning back hot shower water.\n");
|
||||
ems_setWarmWaterActivated(true);
|
||||
Boiler_Shower.isColdShot = false;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Main loop
|
||||
//
|
||||
void loop() {
|
||||
// my myESP to maintain the wifi, mqtt and debugging
|
||||
yield();
|
||||
connectionStatus = myESP.loop();
|
||||
timestamp = millis();
|
||||
|
||||
// Timers
|
||||
updateHATimer.update();
|
||||
systemCheckTimer.update();
|
||||
|
||||
// update the Rx Tx and ERR LEDs
|
||||
showLEDs();
|
||||
|
||||
// do not continue unless we have a wifi connection
|
||||
if (connectionStatus < WIFI_ONLY) {
|
||||
myDebug("Waiting to connect to wifi...\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// if first time connected to MQTT, send welcome start message
|
||||
// which will send all the state values from HA back to the clock via MQTT and return the boottime
|
||||
if ((!startMQTTsent) && (connectionStatus == FULL_CONNECTION)) {
|
||||
myESP.sendStart();
|
||||
startMQTTsent = true;
|
||||
}
|
||||
|
||||
// if we received new data and flagged for pushing, do it
|
||||
if (EMS_Sys_Status.emsRefreshed) {
|
||||
EMS_Sys_Status.emsRefreshed = false;
|
||||
publishValues();
|
||||
}
|
||||
|
||||
/*
|
||||
* Shower Logic
|
||||
*/
|
||||
if (Boiler_Status.shower_enabled) {
|
||||
showerResetTimer.update(); // update Ticker
|
||||
|
||||
// if already in cold mode, ignore all this logic until we're out of the cold blast
|
||||
if (!Boiler_Shower.isColdShot) {
|
||||
// these values come from UBAMonitorFast - type 0x18) which is broadcasted every second so we're pretty accurate
|
||||
Boiler_Shower.showerOn =
|
||||
((EMS_Boiler.selBurnPow >= SHOWER_BURNPOWER_MIN) && (EMS_Boiler.selFlowTemp == 0) && EMS_Boiler.burnGas);
|
||||
|
||||
// is the shower on?
|
||||
if (Boiler_Shower.showerOn) {
|
||||
// if heater was off, start the timer
|
||||
if (Boiler_Shower.timerStart == 0) {
|
||||
Boiler_Shower.timerStart = timestamp;
|
||||
Boiler_Shower.timerPause = 0; // remove any last pauses
|
||||
Boiler_Shower.isColdShot = false;
|
||||
Boiler_Shower.duration = 0;
|
||||
myDebug("Shower: starting timer...\n");
|
||||
} else {
|
||||
// check if the shower has been on too long
|
||||
if ((((timestamp - Boiler_Shower.timerStart) > SHOWER_MAX_DURATION) && !Boiler_Shower.isColdShot)
|
||||
&& Boiler_Status.shower_timer) {
|
||||
_showerColdShotStart();
|
||||
showerResetTimer.start(); // start the timer for n seconds which will reset the water back to hot
|
||||
}
|
||||
}
|
||||
} else { // shower is off
|
||||
// if it just turned off, record the time as it could be a pause
|
||||
if ((Boiler_Shower.timerStart != 0) && (Boiler_Shower.timerPause == 0)) {
|
||||
Boiler_Shower.timerPause = timestamp;
|
||||
myDebug("Shower: water has just turned off...\n");
|
||||
} else {
|
||||
// if shower has been off for longer than the wait time
|
||||
if ((Boiler_Shower.timerPause != 0) && ((timestamp - Boiler_Shower.timerPause) > SHOWER_PAUSE_TIME)) {
|
||||
// its over the wait period, so assume that the shower has finished and calculate the total time and publish
|
||||
Boiler_Shower.duration = (Boiler_Shower.timerPause - Boiler_Shower.timerStart);
|
||||
if (Boiler_Shower.duration > SHOWER_MIN_DURATION) {
|
||||
char s[50];
|
||||
sprintf(s,
|
||||
"%d minutes and %d seconds",
|
||||
(uint8_t)((Boiler_Shower.duration / (1000 * 60)) % 60),
|
||||
(uint8_t)((Boiler_Shower.duration / 1000) % 60));
|
||||
myDebug("Shower: finished, duration was %s\n", s);
|
||||
myESP.publish(TOPIC_SHOWERTIME, s); // publish to HA
|
||||
}
|
||||
|
||||
// reset
|
||||
myDebug("Shower: resetting timers.\n");
|
||||
Boiler_Shower.timerStart = 0;
|
||||
Boiler_Shower.timerPause = 0;
|
||||
_showerColdShotStop(); // turn heat back on in case its off
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// yield to prevent watchdog from timing out
|
||||
// if using delay() this is not needed, but confuses the Ticker library
|
||||
yield();
|
||||
}
|
||||
727
src/ems.cpp
Normal file
727
src/ems.cpp
Normal file
@@ -0,0 +1,727 @@
|
||||
|
||||
/*
|
||||
* ems.cpp
|
||||
* handles all the EMS messages
|
||||
* Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler
|
||||
*/
|
||||
|
||||
#include "ems.h"
|
||||
#include "emsuart.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ESPHelper.h>
|
||||
#include <TimeLib.h>
|
||||
|
||||
_EMS_Sys_Status EMS_Sys_Status; // EMS Status
|
||||
_EMS_TxTelegram EMS_TxTelegram; // Empty buffer for sending telegrams
|
||||
|
||||
// call back for handling Types
|
||||
#define MAX_TYPECALLBACK 11
|
||||
const _EMS_Types EMS_Types[MAX_TYPECALLBACK] =
|
||||
{ {EMS_ID_BOILER, EMS_TYPE_UBAMonitorFast, "UBAMonitorFast", 36, _process_UBAMonitorFast},
|
||||
{EMS_ID_BOILER, EMS_TYPE_UBAMonitorSlow, "UBAMonitorSlow", 28, _process_UBAMonitorSlow},
|
||||
{EMS_ID_BOILER, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", 10, _process_UBAMonitorWWMessage},
|
||||
{EMS_ID_BOILER, EMS_TYPE_UBAParameterWW, "UBAParameterWW", 10, _process_UBAParameterWW},
|
||||
{EMS_ID_BOILER, EMS_TYPE_UBATotalUptimeMessage, "UBATotalUptimeMessage", 30, NULL},
|
||||
{EMS_ID_BOILER, EMS_TYPE_UBAMaintenanceSettingsMessage, "UBAMaintenanceSettingsMessage", 30, NULL},
|
||||
{EMS_ID_BOILER, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", 30, NULL},
|
||||
{EMS_ID_BOILER, EMS_TYPE_UBAMaintenanceStatusMessage, "UBAMaintenanceStatusMessage", 30, NULL},
|
||||
|
||||
{EMS_ID_THERMOSTAT, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", 3, _process_RC20StatusMessage},
|
||||
{EMS_ID_THERMOSTAT, EMS_TYPE_RC20Time, "RC20Time", 20, _process_RC20Time},
|
||||
{EMS_ID_THERMOSTAT, EMS_TYPE_RC20Temperature, "RC20Temperature", 10, _process_RC20Temperature}
|
||||
};
|
||||
|
||||
|
||||
// reserve space for the data we collect from the Boiler and Thermostat
|
||||
_EMS_Boiler EMS_Boiler;
|
||||
_EMS_Thermostat EMS_Thermostat;
|
||||
|
||||
// CRC lookup table with poly 12 for faster checking
|
||||
const uint8_t ems_crc_table[] =
|
||||
{ 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, 0x24,
|
||||
0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E, 0x40, 0x42, 0x44, 0x46, 0x48, 0x4A,
|
||||
0x4C, 0x4E, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5A, 0x5C, 0x5E, 0x60, 0x62, 0x64, 0x66, 0x68, 0x6A, 0x6C, 0x6E, 0x70,
|
||||
0x72, 0x74, 0x76, 0x78, 0x7A, 0x7C, 0x7E, 0x80, 0x82, 0x84, 0x86, 0x88, 0x8A, 0x8C, 0x8E, 0x90, 0x92, 0x94, 0x96,
|
||||
0x98, 0x9A, 0x9C, 0x9E, 0xA0, 0xA2, 0xA4, 0xA6, 0xA8, 0xAA, 0xAC, 0xAE, 0xB0, 0xB2, 0xB4, 0xB6, 0xB8, 0xBA, 0xBC,
|
||||
0xBE, 0xC0, 0xC2, 0xC4, 0xC6, 0xC8, 0xCA, 0xCC, 0xCE, 0xD0, 0xD2, 0xD4, 0xD6, 0xD8, 0xDA, 0xDC, 0xDE, 0xE0, 0xE2,
|
||||
0xE4, 0xE6, 0xE8, 0xEA, 0xEC, 0xEE, 0xF0, 0xF2, 0xF4, 0xF6, 0xF8, 0xFA, 0xFC, 0xFE, 0x19, 0x1B, 0x1D, 0x1F, 0x11,
|
||||
0x13, 0x15, 0x17, 0x09, 0x0B, 0x0D, 0x0F, 0x01, 0x03, 0x05, 0x07, 0x39, 0x3B, 0x3D, 0x3F, 0x31, 0x33, 0x35, 0x37,
|
||||
0x29, 0x2B, 0x2D, 0x2F, 0x21, 0x23, 0x25, 0x27, 0x59, 0x5B, 0x5D, 0x5F, 0x51, 0x53, 0x55, 0x57, 0x49, 0x4B, 0x4D,
|
||||
0x4F, 0x41, 0x43, 0x45, 0x47, 0x79, 0x7B, 0x7D, 0x7F, 0x71, 0x73, 0x75, 0x77, 0x69, 0x6B, 0x6D, 0x6F, 0x61, 0x63,
|
||||
0x65, 0x67, 0x99, 0x9B, 0x9D, 0x9F, 0x91, 0x93, 0x95, 0x97, 0x89, 0x8B, 0x8D, 0x8F, 0x81, 0x83, 0x85, 0x87, 0xB9,
|
||||
0xBB, 0xBD, 0xBF, 0xB1, 0xB3, 0xB5, 0xB7, 0xA9, 0xAB, 0xAD, 0xAF, 0xA1, 0xA3, 0xA5, 0xA7, 0xD9, 0xDB, 0xDD, 0xDF,
|
||||
0xD1, 0xD3, 0xD5, 0xD7, 0xC9, 0xCB, 0xCD, 0xCF, 0xC1, 0xC3, 0xC5, 0xC7, 0xF9, 0xFB, 0xFD, 0xFF, 0xF1, 0xF3, 0xF5,
|
||||
0xF7, 0xE9, 0xEB, 0xED, 0xEF, 0xE1, 0xE3, 0xE5, 0xE7
|
||||
};
|
||||
|
||||
extern ESPHelper myESP;
|
||||
|
||||
#define myDebug(x, ...) myESP.printf(x, ##__VA_ARGS__);
|
||||
|
||||
// constants timers
|
||||
const uint64_t RX_READ_TIMEOUT = 5000; // in ms. 5 seconds timeout for read replies
|
||||
const uint8_t RX_READ_TIMEOUT_COUNT = 4; // 4 retries before timeout
|
||||
|
||||
uint8_t emsLastRxCount = 0;
|
||||
|
||||
// init stats and counters and buffers
|
||||
void ems_init() {
|
||||
// overall status
|
||||
EMS_Sys_Status.emsRxPgks = 0;
|
||||
EMS_Sys_Status.emsTxPkgs = 0;
|
||||
EMS_Sys_Status.emxCrcErr = 0;
|
||||
EMS_Sys_Status.emsRxStatus = EMS_RX_IDLE;
|
||||
EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE;
|
||||
EMS_Sys_Status.emsLastPoll = 0;
|
||||
EMS_Sys_Status.emsLastRx = 0;
|
||||
EMS_Sys_Status.emsLastTx = 0;
|
||||
EMS_Sys_Status.emsRefreshed = false;
|
||||
|
||||
EMS_Sys_Status.emsPollEnabled = false; // start up with Poll disabled
|
||||
EMS_Sys_Status.emsThermostatEnabled = true; // there is a RCxx thermostat active
|
||||
EMS_Sys_Status.emsLogVerbose = false; // Verbose logging is off
|
||||
|
||||
EMS_Thermostat.hour = 0;
|
||||
EMS_Thermostat.minute = 0;
|
||||
EMS_Thermostat.second = 0;
|
||||
EMS_Thermostat.day = 0;
|
||||
EMS_Thermostat.month = 0;
|
||||
EMS_Thermostat.year = 0;
|
||||
|
||||
EMS_Boiler.wWActivated = false; // Warm Water activated
|
||||
EMS_Boiler.wWSelTemp = 0; // Warm Water selected temperature
|
||||
EMS_Boiler.wWCircPump = false; // Warm Water circulation pump Available
|
||||
EMS_Boiler.wWDesiredTemp = 0; // Warm Water desired temperature
|
||||
|
||||
// UBAMonitorFast
|
||||
EMS_Boiler.selFlowTemp = 0; // Selected flow temperature
|
||||
EMS_Boiler.curFlowTemp = -1; // Current flow temperature
|
||||
EMS_Boiler.retTemp = -1; // Return temperature
|
||||
EMS_Boiler.burnGas = false; // Gas on/off
|
||||
EMS_Boiler.fanWork = false; // Fan on/off
|
||||
EMS_Boiler.ignWork = false; // Ignition on/off
|
||||
EMS_Boiler.heatPmp = false; // Circulating pump on/off
|
||||
EMS_Boiler.wWHeat = false; // 3-way valve on WW
|
||||
EMS_Boiler.wWCirc = false; // Circulation on/off
|
||||
EMS_Boiler.selBurnPow = 0; // Burner max power
|
||||
EMS_Boiler.curBurnPow = 0; // Burner current power
|
||||
EMS_Boiler.flameCurr = -1; // Flame current in micro amps
|
||||
EMS_Boiler.sysPress = -1; // System pressure
|
||||
|
||||
// UBAMonitorSlow
|
||||
EMS_Boiler.extTemp = -1; // Outside temperature
|
||||
EMS_Boiler.boilTemp = -1; // Boiler temperature
|
||||
EMS_Boiler.pumpMod = 0; // Pump modulation
|
||||
EMS_Boiler.burnStarts = 0; // # burner restarts
|
||||
EMS_Boiler.burnWorkMin = 0; // Total burner operating time
|
||||
EMS_Boiler.heatWorkMin = 0; // Total heat operating time
|
||||
|
||||
// UBAMonitorWWMessage
|
||||
EMS_Boiler.wWCurTmp = -1; // Warm Water current temperature:
|
||||
EMS_Boiler.wWStarts = 0; // Warm Water # starts
|
||||
EMS_Boiler.wWWorkM = 0; // Warm Water # minutes
|
||||
EMS_Boiler.wWOneTime = false; // Warm Water one time function on/off
|
||||
|
||||
// init the Tx package
|
||||
_initTxBuffer();
|
||||
}
|
||||
|
||||
// init Tx Buffer
|
||||
void _initTxBuffer() {
|
||||
EMS_TxTelegram.length = 0;
|
||||
EMS_TxTelegram.type = 0;
|
||||
EMS_TxTelegram.dest = 0;
|
||||
EMS_TxTelegram.offset = 0;
|
||||
EMS_TxTelegram.length = 0;
|
||||
EMS_TxTelegram.type_validate = 0;
|
||||
EMS_TxTelegram.action = EMS_TX_NONE;
|
||||
EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE;
|
||||
emsLastRxCount = 0;
|
||||
}
|
||||
|
||||
// Getters and Setters for parameters
|
||||
void ems_setPoll(bool b) {
|
||||
EMS_Sys_Status.emsPollEnabled = b;
|
||||
myDebug("EMS Bus Poll is %s\n", EMS_Sys_Status.emsPollEnabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
bool ems_getPoll() {
|
||||
return EMS_Sys_Status.emsPollEnabled;
|
||||
}
|
||||
|
||||
bool ems_getThermostatEnabled() {
|
||||
return EMS_Sys_Status.emsThermostatEnabled;
|
||||
}
|
||||
|
||||
void ems_setThermostatEnabled(bool b) {
|
||||
EMS_Sys_Status.emsThermostatEnabled = b;
|
||||
myDebug("Thermostat is %s\n", EMS_Sys_Status.emsThermostatEnabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
bool ems_getLogVerbose() {
|
||||
return EMS_Sys_Status.emsLogVerbose;
|
||||
}
|
||||
|
||||
void ems_setLogVerbose(bool b) {
|
||||
EMS_Sys_Status.emsLogVerbose = b;
|
||||
myDebug("Verbose logging is %s.\n", EMS_Sys_Status.emsLogVerbose ? "on" : "off");
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate CRC checksum using lookup table
|
||||
* len is length of data in bytes (including the CRC byte at end)
|
||||
*/
|
||||
uint8_t _crcCalculator(uint8_t * data, uint8_t len) {
|
||||
uint8_t crc = 0;
|
||||
|
||||
// read data and stop before the CRC
|
||||
for (uint8_t i = 0; i < len - 1; i++) {
|
||||
crc = ems_crc_table[crc];
|
||||
crc ^= data[i];
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
// debug print a telegram to telnet console
|
||||
// len is length in bytes including the CRC
|
||||
void _debugPrintTelegram(const char * prefix, uint8_t * data, uint8_t len, const char * color) {
|
||||
if (!EMS_Sys_Status.emsLogVerbose)
|
||||
return;
|
||||
|
||||
bool crcok = (data[len - 1] == _crcCalculator(data, len));
|
||||
if (crcok) {
|
||||
myDebug(color)
|
||||
} else {
|
||||
myDebug(COLOR_RED);
|
||||
}
|
||||
|
||||
time_t currentTime = now();
|
||||
myDebug("[%02d:%02d:%02d] %s len=%02d, data: ", hour(currentTime), minute(currentTime), second(currentTime), prefix, len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
myDebug("%02x ", data[i]);
|
||||
}
|
||||
myDebug("(%s) %s\n", crcok ? "OK" : "BAD", COLOR_RESET);
|
||||
}
|
||||
|
||||
// send the contents of the Tx buffer
|
||||
void _ems_sendTelegram() {
|
||||
// only send when Tx is not busy
|
||||
_debugPrintTelegram(((EMS_TxTelegram.action == EMS_TX_WRITE) ? "Sending write telegram:" : "Sending read telegram:"),
|
||||
EMS_TxTelegram.data,
|
||||
EMS_TxTelegram.length,
|
||||
COLOR_CYAN);
|
||||
|
||||
EMS_Sys_Status.emsTxStatus = EMS_TX_ACTIVE;
|
||||
emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length);
|
||||
EMS_Sys_Status.emsTxPkgs++;
|
||||
EMS_Sys_Status.emsLastTx = millis();
|
||||
|
||||
// if it was a write command, check if we need to do a new read to validate the results
|
||||
if (EMS_TxTelegram.action == EMS_TX_WRITE) {
|
||||
// straight after the write check to see if we have to followup with a read to verify the write worked
|
||||
// we do this by re-submitting the same telegram but this time as a write command (type,offset,length are still valid)
|
||||
if (EMS_TxTelegram.type_validate != 0) {
|
||||
EMS_TxTelegram.dest = EMS_TxTelegram.dest | 0x80; // add the 7th bit make sure its a read
|
||||
_buildTxTelegram(1); // get a single value back, which is one byte
|
||||
}
|
||||
EMS_TxTelegram.action = EMS_TX_VALIDATE; // will start the validate next time a telegram is received
|
||||
} else {
|
||||
EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE; // nothing to send
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* parse the telegram message
|
||||
* length is only data bytes, excluding the BRK
|
||||
* Read commands are asynchronous as they're handled by the interrupt
|
||||
* When we receive a Poll Request we need to send quickly
|
||||
*/
|
||||
void ems_parseTelegram(uint8_t * telegram, uint8_t length) {
|
||||
// if we're waiting on a reponse from a read and it hasn't come, try again
|
||||
if ((EMS_Sys_Status.emsTxStatus != EMS_TX_PENDING)
|
||||
&& ((EMS_TxTelegram.action == EMS_TX_READ) || (EMS_TxTelegram.action == EMS_TX_VALIDATE))
|
||||
&& ((millis() - EMS_Sys_Status.emsLastTx) > RX_READ_TIMEOUT)) {
|
||||
if (emsLastRxCount++ >= RX_READ_TIMEOUT_COUNT) {
|
||||
// give up
|
||||
myDebug("Error! no send acknowledgement. Giving up.\n");
|
||||
_initTxBuffer();
|
||||
} else {
|
||||
myDebug("Didn't receive acknowledgement so resending (attempt #%d/%d)...\n",
|
||||
emsLastRxCount,
|
||||
RX_READ_TIMEOUT_COUNT);
|
||||
EMS_Sys_Status.emsTxStatus = EMS_TX_PENDING; // set to pending will trigger sending the same package again
|
||||
}
|
||||
}
|
||||
|
||||
// check if we just received one byte
|
||||
// it could be a Poll request from the boiler which is 0x8B (0x0B | 0x80 to set 7th bit)
|
||||
// or a return code like 0x01 or 0x04 from the last Write command
|
||||
if (length == 1) {
|
||||
uint8_t value = telegram[0]; // 1st byte
|
||||
|
||||
// check first for Poll
|
||||
if (value == (EMS_ID_ME | 0x80)) {
|
||||
// set the timestamp of the last poll, we use this to see if we have a connection to the boiler
|
||||
EMS_Sys_Status.emsLastPoll = millis();
|
||||
|
||||
// do we have something to send? if so send it
|
||||
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) {
|
||||
_ems_sendTelegram();
|
||||
} else {
|
||||
// nothing to send so just send a poll acknowledgement back
|
||||
if (EMS_Sys_Status.emsPollEnabled) {
|
||||
emsaurt_tx_poll();
|
||||
}
|
||||
}
|
||||
// check if we're waiting on a response after a recent Write, which is a return code (0x01 for success or 0x04 for error)
|
||||
} else if (EMS_TxTelegram.action == EMS_TX_WRITE) {
|
||||
// TODO: need to tidy this piece up!
|
||||
// a response from UBA after a write should be within a specific time period <100ms
|
||||
/*
|
||||
if (value == 0x01) {
|
||||
emsaurt_tx_poll(); // send a poll acknowledgement
|
||||
myDebug("Receiver of last write acknowledged OK, ready to validate...\n");
|
||||
EMS_TxTelegram.action = EMS_TX_VALIDATE;
|
||||
} else if (value == 0x04) {
|
||||
EMS_TxTelegram.action = EMS_TX_NONE;
|
||||
myDebug("Return value from write FAILED.\n");
|
||||
emsaurt_tx_poll(); // send a poll acknowledgement
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
return; // all done here, quit. if we haven't processes anything its a poll but not for us
|
||||
}
|
||||
|
||||
// ignore anything that doesn't resemble a proper telegram package
|
||||
// minimal is 5 bytes, excluding CRC at the end
|
||||
if ((length < 5)) {
|
||||
_debugPrintTelegram("Noisy data:", telegram, length, COLOR_MAGENTA);
|
||||
return;
|
||||
}
|
||||
|
||||
// Assume at this point we have something that vaguely resembles a telegram
|
||||
// see if we got a telegram as [src] [dest] [type] [offset] [data] [crc]
|
||||
// so is at least 6 bytes long and the CRC checks out (which is last byte)
|
||||
uint8_t crc = _crcCalculator(telegram, length);
|
||||
if (telegram[length - 1] != crc) {
|
||||
EMS_Sys_Status.emxCrcErr++;
|
||||
_debugPrintTelegram("Corrupt telegram:", telegram, length, COLOR_RED);
|
||||
} else {
|
||||
// go and do the magic
|
||||
_processType(telegram, length);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* decipher the telegram packet
|
||||
* length is only data bytes, excluding the BRK
|
||||
*/
|
||||
void _processType(uint8_t * telegram, uint8_t length) {
|
||||
// extract the 4-byte header information
|
||||
uint8_t src = telegram[0] & 0x7F; // remove 8th bit as we deal with both reads and writes
|
||||
|
||||
// if its an echo of ourselves from the master, ignore
|
||||
if (src == EMS_ID_ME) {
|
||||
_debugPrintTelegram("Telegram echo:", telegram, length, COLOR_BLUE);
|
||||
}
|
||||
|
||||
// header
|
||||
uint8_t dest = telegram[1];
|
||||
uint8_t type = telegram[2];
|
||||
uint8_t * data = telegram + 4; // data block starts at position 5
|
||||
|
||||
// for building a debug message
|
||||
char s[100];
|
||||
char color_s[20];
|
||||
char src_s[20];
|
||||
char dest_s[20];
|
||||
|
||||
// scan through known types
|
||||
int i = 0;
|
||||
bool typeFound = false;
|
||||
while (i < MAX_TYPECALLBACK) {
|
||||
if ((EMS_Types[i].src == src) && (EMS_Types[i].type == type)) {
|
||||
// we have a match
|
||||
typeFound = true;
|
||||
// call callback to fetch the values from the telegram
|
||||
// ignoring the return value for now
|
||||
if ((EMS_Types[i].processType_cb) != (void *)NULL) {
|
||||
(void)EMS_Types[i].processType_cb(data, length);
|
||||
}
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (src == EMS_ID_BOILER) {
|
||||
strcpy(src_s, "Boiler");
|
||||
} else if (src == EMS_ID_THERMOSTAT) {
|
||||
strcpy(src_s, "Thermostat");
|
||||
}
|
||||
|
||||
// was it sent specifically to us?
|
||||
if (dest == EMS_ID_ME) {
|
||||
strcpy(dest_s, "telegram for us");
|
||||
strcpy(color_s, COLOR_YELLOW);
|
||||
|
||||
// did we actually ask for it from an earlier read/write request?
|
||||
// note when we issue a read command the responder (dest) has to return a telegram back immediately
|
||||
if ((EMS_TxTelegram.action == EMS_TX_READ) && (EMS_TxTelegram.type == type) && typeFound) {
|
||||
// yes we were expecting this one one
|
||||
EMS_Sys_Status.emsRxPgks++; // increment rx counter
|
||||
EMS_Sys_Status.emsLastRx = millis();
|
||||
EMS_TxTelegram.action = EMS_TX_NONE;
|
||||
emsLastRxCount = 0; // reset retry count
|
||||
}
|
||||
// send Acknowledgement back to free the bus
|
||||
emsaurt_tx_poll();
|
||||
} else if (dest == EMS_ID_NONE) {
|
||||
// it's probably just a broadcast
|
||||
strcpy(dest_s, "broadcast");
|
||||
strcpy(color_s, COLOR_GREEN);
|
||||
} else {
|
||||
// for someone else
|
||||
strcpy(dest_s, "(not for us)");
|
||||
strcpy(color_s, COLOR_MAGENTA);
|
||||
}
|
||||
|
||||
// debug print
|
||||
sprintf(s, "%s %s, type 0x%02x", src_s, dest_s, type);
|
||||
_debugPrintTelegram(s, telegram, length, color_s);
|
||||
if (typeFound) {
|
||||
myDebug("--> %s(0x%02x) received.\n", EMS_Types[i].typeString, type);
|
||||
}
|
||||
|
||||
// check to see if we're waiting on a specific value, either from a recent read or by chance a broadcast
|
||||
// and then do the comparison
|
||||
if ((EMS_TxTelegram.action == EMS_TX_VALIDATE) && (EMS_TxTelegram.type == type)) {
|
||||
uint8_t offset;
|
||||
|
||||
// we're waiting for a read on our last write to compare the values
|
||||
// if we have a special telegram package we sent to validate use that for comparison, otherwise assume its a simple 1-byte read
|
||||
if (EMS_TxTelegram.type_validate == EMS_ID_NONE) {
|
||||
offset = EMS_TxTelegram.offset; // special, use the offset from the last send
|
||||
} else {
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
// get the data at the position we wrote too
|
||||
// do compare, when validating we always return a single value
|
||||
if (EMS_TxTelegram.checkValue == data[offset]) {
|
||||
myDebug("Last write operation successful (value=%d, offset=%d)\n", EMS_TxTelegram.checkValue, offset);
|
||||
EMS_Sys_Status.emsRefreshed = true; // flag this so values are sent back to HA via MQTT
|
||||
EMS_TxTelegram.action = EMS_TX_NONE; // no more sends
|
||||
} else {
|
||||
myDebug("Last write operation failed. (value=%d, got=%d, offset=%d)\n",
|
||||
EMS_TxTelegram.checkValue,
|
||||
data[offset],
|
||||
offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* UBAParameterWW - type 0x33 - warm water parameters
|
||||
*/
|
||||
bool _process_UBAParameterWW(uint8_t * data, uint8_t length) {
|
||||
EMS_Boiler.wWSelTemp = data[2];
|
||||
EMS_Boiler.wWActivated = (data[1] == 0xFF);
|
||||
EMS_Boiler.wWCircPump = (data[6] == 0xFF);
|
||||
EMS_Boiler.wWDesiredTemp = data[8];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* UBAMonitorWWMessage - type 0x34 - warm water monitor. 19 bytes long
|
||||
*/
|
||||
bool _process_UBAMonitorWWMessage(uint8_t * data, uint8_t length) {
|
||||
EMS_Boiler.wWCurTmp = _toFloat(1, data);
|
||||
EMS_Boiler.wWStarts = _toLong(13, data);
|
||||
EMS_Boiler.wWWorkM = _toLong(10, data);
|
||||
EMS_Boiler.wWOneTime = bitRead(data[5], 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* UBAMonitorFast - type 0x18 - central heating monitor part 1 (25 bytes long)
|
||||
*/
|
||||
bool _process_UBAMonitorFast(uint8_t * data, uint8_t length) {
|
||||
EMS_Boiler.selFlowTemp = data[0];
|
||||
EMS_Boiler.curFlowTemp = _toFloat(1, data);
|
||||
EMS_Boiler.retTemp = _toFloat(13, data);
|
||||
|
||||
uint8_t v = data[7];
|
||||
EMS_Boiler.burnGas = bitRead(v, 0);
|
||||
EMS_Boiler.fanWork = bitRead(v, 2);
|
||||
EMS_Boiler.ignWork = bitRead(v, 3);
|
||||
EMS_Boiler.heatPmp = bitRead(v, 5);
|
||||
EMS_Boiler.wWHeat = bitRead(v, 6);
|
||||
EMS_Boiler.wWCirc = bitRead(v, 7);
|
||||
|
||||
EMS_Boiler.selBurnPow = data[3];
|
||||
EMS_Boiler.curBurnPow = data[4];
|
||||
|
||||
EMS_Boiler.flameCurr = _toFloat(15, data);
|
||||
|
||||
if (data[17] == 0xFF) { // missing value for system pressure
|
||||
EMS_Boiler.sysPress = 0;
|
||||
} else {
|
||||
EMS_Boiler.sysPress = (((float)data[17]) / (float)10);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* UBAMonitorSlow - type 0x19 - central heating monitor part 2 (27 bytes long)
|
||||
*/
|
||||
bool _process_UBAMonitorSlow(uint8_t * data, uint8_t length) {
|
||||
EMS_Boiler.extTemp = _toFloat(0, data); // 0x8000 if not available
|
||||
EMS_Boiler.boilTemp = _toFloat(2, data); // 0x8000 if not available
|
||||
EMS_Boiler.pumpMod = data[13];
|
||||
EMS_Boiler.burnStarts = _toLong(10, data);
|
||||
EMS_Boiler.burnWorkMin = _toLong(13, data);
|
||||
EMS_Boiler.heatWorkMin = _toLong(19, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* RC20StatusMessage - type 0x91 - data from the RC20 thermostat (0x17) - 15 bytes long
|
||||
*/
|
||||
bool _process_RC20StatusMessage(uint8_t * data, uint8_t length) {
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)data[1]) / (float)2;
|
||||
EMS_Thermostat.curr_roomTemp = _toFloat(2, data);
|
||||
|
||||
// set the updated flag to trigger a send back to HA
|
||||
EMS_Sys_Status.emsRefreshed = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* RC20Temperature - type 0xa8 - set temp value from the RC20 thermostat (0x17)
|
||||
* Special case as we only want to store the value after a write command
|
||||
*/
|
||||
bool _process_RC20Temperature(uint8_t * data, uint8_t length) {
|
||||
// only interested in the single byte response we send to validate a temp change
|
||||
if (length == 6) {
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)data[0]) / (float)2;
|
||||
|
||||
EMS_Sys_Status.emsRefreshed = true; // set the updated flag to trigger a send back to HA
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* process_RC20Time - type 0x06 - date and time from the RC20 thermostat (0x17) - 14 bytes long
|
||||
*/
|
||||
bool _process_RC20Time(uint8_t * data, uint8_t length) {
|
||||
EMS_Thermostat.hour = data[2];
|
||||
EMS_Thermostat.minute = data[4];
|
||||
EMS_Thermostat.second = data[5];
|
||||
EMS_Thermostat.day = data[3];
|
||||
EMS_Thermostat.month = data[1];
|
||||
EMS_Thermostat.year = data[0];
|
||||
|
||||
// now we have the time, set our ESP code to it - wil be replaced with NTP
|
||||
setTime(EMS_Thermostat.hour,
|
||||
EMS_Thermostat.minute,
|
||||
EMS_Thermostat.second,
|
||||
EMS_Thermostat.day,
|
||||
EMS_Thermostat.month,
|
||||
EMS_Thermostat.year + 2000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Build the telegram, which includes a single byte followed by the CRC at the end
|
||||
*/
|
||||
void _buildTxTelegram(uint8_t data_value) {
|
||||
// header
|
||||
EMS_TxTelegram.data[0] = EMS_ID_ME; // src
|
||||
EMS_TxTelegram.data[1] = EMS_TxTelegram.dest; // dest
|
||||
EMS_TxTelegram.data[2] = EMS_TxTelegram.type; // type
|
||||
EMS_TxTelegram.data[3] = EMS_TxTelegram.offset; //offset
|
||||
|
||||
// data
|
||||
EMS_TxTelegram.data[4] = data_value; // value, can be size
|
||||
|
||||
// crc
|
||||
EMS_TxTelegram.data[5] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length);
|
||||
|
||||
EMS_Sys_Status.emsTxStatus = EMS_TX_PENDING; // armed and ready to send
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Send a command to Tx to Read from another device
|
||||
* Read commands when sent must to responded too by the destination (target) immediately
|
||||
* usually within a 10ms window
|
||||
*/
|
||||
void ems_doReadCommand(uint8_t type) {
|
||||
if (type == EMS_TYPE_NONE)
|
||||
return; // not a valid type, quit
|
||||
|
||||
// check if there is already something in the queue
|
||||
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) { // send is already pending
|
||||
myDebug("Cannot write - already a telegram pending send.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t dest, size;
|
||||
|
||||
// scan through known types
|
||||
bool typeFound = false;
|
||||
int i = 0;
|
||||
while (i < MAX_TYPECALLBACK) {
|
||||
if (EMS_Types[i].type == type) {
|
||||
typeFound = true; // we have a match
|
||||
// call callback to fetch the values from the telegram
|
||||
dest = EMS_Types[i].src;
|
||||
size = EMS_Types[i].size;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// for adhoc calls use default values
|
||||
if (!typeFound) {
|
||||
dest = EMS_ID_BOILER; // default is boiler
|
||||
size = 1;
|
||||
myDebug("Requesting type (0x%02x) from dest 0x%02x for %d bytes\n", type, dest, size);
|
||||
} else {
|
||||
myDebug("Requesting type %s(0x%02x) from dest 0x%02x for %d bytes\n", EMS_Types[i].typeString, type, dest, size);
|
||||
}
|
||||
|
||||
EMS_TxTelegram.action = EMS_TX_READ; // read command
|
||||
EMS_TxTelegram.dest = dest | 0x80; // set 7th bit to indicate a read
|
||||
EMS_TxTelegram.offset = 0; // 0 for all data
|
||||
EMS_TxTelegram.length = 6; // is always 6 bytes long (including CRC at end)
|
||||
EMS_TxTelegram.type = type;
|
||||
|
||||
_buildTxTelegram(size); // we send the # bytes we want back
|
||||
}
|
||||
|
||||
/*
|
||||
* Set the temperature of the thermostat
|
||||
*/
|
||||
void ems_setThermostatTemp(float temperature) {
|
||||
// check if there is already something in the queue
|
||||
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) { // send is already pending
|
||||
myDebug("Cannot write - already a telegram pending send.\n");
|
||||
return;
|
||||
}
|
||||
char s[10];
|
||||
myDebug("Setting thermostat temperature to %s C\n", _float_to_char(s, temperature));
|
||||
|
||||
EMS_TxTelegram.action = EMS_TX_WRITE; // write command
|
||||
EMS_TxTelegram.dest = EMS_ID_THERMOSTAT;
|
||||
EMS_TxTelegram.type = EMS_TYPE_RC20Temperature;
|
||||
EMS_TxTelegram.offset = 0x1C; // manual setpoint temperature
|
||||
EMS_TxTelegram.length = 6; // includes CRC
|
||||
EMS_TxTelegram.checkValue = (uint8_t)((float)temperature * (float)2); // value to compare against. must be a single int
|
||||
|
||||
// post call is RC20StatusMessage to fetch temps and send to HA
|
||||
EMS_TxTelegram.type_validate = EMS_TYPE_RC20Temperature;
|
||||
|
||||
_buildTxTelegram(EMS_TxTelegram.checkValue);
|
||||
}
|
||||
|
||||
/*
|
||||
* Set the warm water temperature
|
||||
*/
|
||||
void ems_setWarmWaterTemp(uint8_t temperature) {
|
||||
// check if there is already something in the queue
|
||||
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) { // send is already pending
|
||||
myDebug("Cannot write - already a telegram pending send.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
myDebug("Setting boiler warm water temperature to %d C\n", temperature);
|
||||
|
||||
EMS_TxTelegram.action = EMS_TX_WRITE; // write command
|
||||
EMS_TxTelegram.dest = EMS_ID_BOILER;
|
||||
EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW;
|
||||
EMS_TxTelegram.offset = 0x02; // Temperature
|
||||
EMS_TxTelegram.length = 6; // includes CRC
|
||||
EMS_TxTelegram.checkValue = temperature; // value to compare against. must be a single int
|
||||
|
||||
EMS_TxTelegram.type_validate = EMS_ID_NONE; // this means don't send and we'll pick up the data from the next broadcast
|
||||
|
||||
_buildTxTelegram(temperature);
|
||||
}
|
||||
|
||||
/*
|
||||
* Activate / De-activate the Warm Water
|
||||
* true = on, false = off
|
||||
*/
|
||||
void ems_setWarmWaterActivated(bool activated) {
|
||||
// check if there is already something in the queue
|
||||
if (EMS_Sys_Status.emsTxStatus == EMS_TX_PENDING) { // send is already pending
|
||||
myDebug("Cannot write - already a telegram pending send.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
myDebug("Setting boiler warm water to %s\n", activated ? "on" : "off");
|
||||
|
||||
EMS_TxTelegram.action = EMS_TX_WRITE; // write command
|
||||
EMS_TxTelegram.dest = EMS_ID_BOILER;
|
||||
EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW;
|
||||
EMS_TxTelegram.offset = 0x01; // WW activation
|
||||
EMS_TxTelegram.length = 6; // includes CRC
|
||||
EMS_TxTelegram.type_validate = EMS_ID_NONE; // this means don't send and we'll pick up the data from the next broadcast
|
||||
|
||||
if (activated) {
|
||||
EMS_TxTelegram.checkValue = 0xFF; // the EMS value for on
|
||||
_buildTxTelegram(0xFF);
|
||||
} else {
|
||||
EMS_TxTelegram.checkValue = 0x00; // the EMS value for off
|
||||
_buildTxTelegram(0x00);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Helper functions for formatting and converting floats
|
||||
*/
|
||||
|
||||
// function to turn a telegram int (2 bytes) to a float
|
||||
float _toFloat(uint8_t i, uint8_t * data) {
|
||||
if ((data[i] == 0x80) && (data[i + 1] == 0)) // 0x8000 is used when sensor is missing
|
||||
return (float)-1;
|
||||
|
||||
return ((float)(((data[i] << 8) + data[i + 1]))) / 10;
|
||||
}
|
||||
|
||||
// function to turn a telegram long (3 bytes) to a long int
|
||||
uint16_t _toLong(uint8_t i, uint8_t * data) {
|
||||
return (((data[i]) << 16) + ((data[i + 1]) << 8) + (data[i + 2]));
|
||||
}
|
||||
|
||||
// convert float to char
|
||||
char * _float_to_char(char * a, float f, uint8_t precision) {
|
||||
long p[] = {0, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
|
||||
|
||||
char * ret = a;
|
||||
// check for 0x8000 (sensor missing)
|
||||
if (f == -1) {
|
||||
strcpy(ret, "<n/a>");
|
||||
} else {
|
||||
long whole = (long)f;
|
||||
itoa(whole, a, 10);
|
||||
while (*a != '\0')
|
||||
a++;
|
||||
*a++ = '.';
|
||||
long decimal = abs((long)((f - whole) * p[precision]));
|
||||
itoa(decimal, a, 10);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
191
src/ems.h
Normal file
191
src/ems.h
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Header file for EMS.cpp
|
||||
*/
|
||||
|
||||
#ifndef __EMS_H
|
||||
#define __EMS_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// EMS IDs
|
||||
#define EMS_ID_THERMOSTAT 0x17 // x17=RC20, x10=RC30 (Moduline 300)
|
||||
|
||||
#define EMS_ID_NONE 0x00 // Fixed - used as a dest in broadcast messages
|
||||
#define EMS_ID_BOILER 0x08 // Fixed - also known as MC10.
|
||||
#define EMS_ID_ME 0x0B // Fixed - our device, hardcoded as "Service Key"
|
||||
|
||||
// EMS Telegram Types
|
||||
#define EMS_TYPE_NONE 0x00 // none
|
||||
#define EMS_TYPE_UBAMonitorFast 0x18 // is an automatic monitor broadcast
|
||||
#define EMS_TYPE_UBAMonitorSlow 0x19 // is an automatic monitor broadcast
|
||||
#define EMS_TYPE_UBAMonitorWWMessage 0x34 // is an automatic monitor broadcast
|
||||
#define EMS_TYPE_UBAParameterWW 0x33 // is an automatic monitor broadcast
|
||||
#define EMS_TYPE_UBATotalUptimeMessage 0x14
|
||||
#define EMS_TYPE_UBAMaintenanceSettingsMessage 0x15
|
||||
#define EMS_TYPE_UBAParametersMessage 0x16
|
||||
#define EMS_TYPE_UBAMaintenanceStatusMessage 0x1c
|
||||
|
||||
// EMS Telegram types from Thermostat
|
||||
// types 1A and 35 and used for errors from Thermostat
|
||||
#define EMS_TYPE_RC20StatusMessage 0x91
|
||||
#define EMS_TYPE_RC20Time 0x06 // is an automatic monitor broadcast
|
||||
#define EMS_TYPE_RC20Temperature 0xA8
|
||||
#define EMS_TYPE_RCOutdoorTempMessage 0xa3 // we can ignore
|
||||
|
||||
#define EMS_TX_MAXBUFFERSIZE 128 // max size of the buffer. packets are 32 bits
|
||||
|
||||
/* EMS UART transfer status */
|
||||
typedef enum {
|
||||
EMS_RX_IDLE,
|
||||
EMS_RX_ACTIVE // Rx package is being sent
|
||||
} _EMS_RX_STATUS;
|
||||
|
||||
typedef enum {
|
||||
EMS_TX_IDLE,
|
||||
EMS_TX_PENDING, // got Tx package to send, waiting for next Poll to send
|
||||
EMS_TX_ACTIVE // Tx package being sent, no break sent
|
||||
} _EMS_TX_STATUS;
|
||||
|
||||
typedef enum {
|
||||
EMS_TX_NONE,
|
||||
EMS_TX_READ, // doing a read request
|
||||
EMS_TX_WRITE, // doing a write request
|
||||
EMS_TX_VALIDATE // do a validate after a write
|
||||
} _EMS_TX_ACTION;
|
||||
|
||||
// status/counters since last power on
|
||||
typedef struct {
|
||||
_EMS_RX_STATUS emsRxStatus;
|
||||
_EMS_TX_STATUS emsTxStatus;
|
||||
uint16_t emsRxPgks; // received
|
||||
uint16_t emsTxPkgs; // sent
|
||||
uint16_t emxCrcErr; // CRC errors
|
||||
bool emsPollEnabled; // flag enable the response to poll messages
|
||||
bool emsThermostatEnabled; // if there is a RCxx thermostat active
|
||||
bool emsLogVerbose; // Verbose logging
|
||||
unsigned long emsLastPoll; // in ms, last time we received a poll
|
||||
unsigned long emsLastRx; // timings
|
||||
unsigned long emsLastTx; // timings
|
||||
bool emsRefreshed; // fresh data, needs to be pushed out to MQTT
|
||||
} _EMS_Sys_Status;
|
||||
|
||||
// The Tx send package
|
||||
typedef struct {
|
||||
_EMS_TX_ACTION action; // read or write
|
||||
uint8_t dest;
|
||||
uint8_t type;
|
||||
uint8_t offset;
|
||||
uint8_t length;
|
||||
uint8_t checkValue; // value to validate against
|
||||
uint8_t type_validate; // type to call after a successful Write command
|
||||
uint8_t data[EMS_TX_MAXBUFFERSIZE];
|
||||
} _EMS_TxTelegram;
|
||||
|
||||
/*
|
||||
* Telegram package defintions
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
// UBAParameterWW
|
||||
bool wWActivated; // Warm Water activated
|
||||
uint8_t wWSelTemp; // Warm Water selected temperature
|
||||
bool wWCircPump; // Warm Water circulation pump Available
|
||||
uint8_t wWDesiredTemp; // Warm Water desired temperature
|
||||
|
||||
// UBAMonitorFast
|
||||
uint8_t selFlowTemp; // Selected flow temperature
|
||||
float curFlowTemp; // Current flow temperature
|
||||
float retTemp; // Return temperature
|
||||
bool burnGas; // Gas on/off
|
||||
bool fanWork; // Fan on/off
|
||||
bool ignWork; // Ignition on/off
|
||||
bool heatPmp; // Circulating pump on/off
|
||||
bool wWHeat; // 3-way valve on WW
|
||||
bool wWCirc; // Circulation on/off
|
||||
uint8_t selBurnPow; // Burner max power
|
||||
uint8_t curBurnPow; // Burner current power
|
||||
float flameCurr; // Flame current in micro amps
|
||||
float sysPress; // System pressure
|
||||
|
||||
// UBAMonitorSlow
|
||||
float extTemp; // Outside temperature
|
||||
float boilTemp; // Boiler temperature
|
||||
uint8_t pumpMod; // Pump modulation
|
||||
uint16_t burnStarts; // # burner restarts
|
||||
uint16_t burnWorkMin; // Total burner operating time
|
||||
uint16_t heatWorkMin; // Total heat operating time
|
||||
|
||||
// UBAMonitorWWMessage
|
||||
float wWCurTmp; // Warm Water current temperature:
|
||||
uint32_t wWStarts; // Warm Water # starts
|
||||
uint32_t wWWorkM; // Warm Water # minutes
|
||||
bool wWOneTime; // Warm Water one time function on/off
|
||||
} _EMS_Boiler;
|
||||
|
||||
// RC20 data
|
||||
typedef struct {
|
||||
float setpoint_roomTemp;
|
||||
float curr_roomTemp;
|
||||
uint8_t hour;
|
||||
uint8_t minute;
|
||||
uint8_t second;
|
||||
uint8_t day;
|
||||
uint8_t month;
|
||||
uint8_t year;
|
||||
} _EMS_Thermostat;
|
||||
|
||||
// call back function signature
|
||||
typedef bool (*EMS_processType_cb)(uint8_t * data, uint8_t length);
|
||||
|
||||
// Definition for each type, including the relative callback function
|
||||
typedef struct {
|
||||
uint8_t src;
|
||||
uint8_t type;
|
||||
const char typeString[50];
|
||||
uint8_t size; // size of telegram, excluding the 4-byte header and crc
|
||||
EMS_processType_cb processType_cb;
|
||||
} _EMS_Types;
|
||||
|
||||
// function definitions
|
||||
extern void ems_parseTelegram(uint8_t * telegram, uint8_t len);
|
||||
void ems_init();
|
||||
void ems_doReadCommand(uint8_t type);
|
||||
void ems_setThermostatTemp(float temp);
|
||||
void ems_setWarmWaterTemp(uint8_t temperature);
|
||||
void ems_setWarmWaterActivated(bool activated);
|
||||
|
||||
void ems_setPoll(bool b);
|
||||
bool ems_getPoll();
|
||||
bool ems_getThermostatEnabled();
|
||||
void ems_setThermostatEnabled(bool b);
|
||||
bool ems_getLogVerbose();
|
||||
void ems_setLogVerbose(bool b);
|
||||
|
||||
// private functions
|
||||
uint8_t _crcCalculator(uint8_t * data, uint8_t len);
|
||||
void _processType(uint8_t * telegram, uint8_t length);
|
||||
void _initTxBuffer();
|
||||
void _buildTxTelegram(uint8_t data_value);
|
||||
void _debugPrintPackage(const char * prefix, uint8_t * data, uint8_t len, const char * color);
|
||||
|
||||
// callbacks per type
|
||||
bool _process_UBAMonitorFast(uint8_t * data, uint8_t length);
|
||||
bool _process_UBAMonitorSlow(uint8_t * data, uint8_t length);
|
||||
bool _process_UBAMonitorWWMessage(uint8_t * data, uint8_t length);
|
||||
bool _process_UBAParameterWW(uint8_t * data, uint8_t length);
|
||||
bool _process_RC20StatusMessage(uint8_t * data, uint8_t length);
|
||||
bool _process_RC20Time(uint8_t * data, uint8_t length);
|
||||
bool _process_RC20Temperature(uint8_t * data, uint8_t length);
|
||||
|
||||
// helper functions
|
||||
float _toFloat(uint8_t i, uint8_t * data);
|
||||
uint16_t _toLong(uint8_t i, uint8_t * data);
|
||||
char * _float_to_char(char * a, float f, uint8_t precision = 1);
|
||||
|
||||
// global so can referenced in other classes
|
||||
extern _EMS_Sys_Status EMS_Sys_Status;
|
||||
extern _EMS_TxTelegram EMS_TxTelegram;
|
||||
extern _EMS_Boiler EMS_Boiler;
|
||||
extern _EMS_Thermostat EMS_Thermostat;
|
||||
|
||||
#endif
|
||||
182
src/emsuart.cpp
Normal file
182
src/emsuart.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* emsuart.cpp
|
||||
* The low level UART code for ESP8266
|
||||
* Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler
|
||||
*/
|
||||
|
||||
#include "emsuart.h"
|
||||
#include "ems.h"
|
||||
#include "ets_sys.h"
|
||||
#include "osapi.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <user_interface.h>
|
||||
|
||||
_EMSRxBuf * pEMSRxBuf;
|
||||
_EMSRxBuf * paEMSRxBuf[EMS_MAXBUFFERS];
|
||||
uint8_t emsRxBufIdx = 0;
|
||||
|
||||
// queues
|
||||
os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen];
|
||||
|
||||
//
|
||||
// Main interrupt handler
|
||||
// Important: do not use ICACHE_FLASH_ATTR !
|
||||
//
|
||||
static void emsuart_rx_intr_handler(void * para) {
|
||||
static uint16_t length;
|
||||
static uint8_t uart_buffer[EMS_MAXBUFFERSIZE];
|
||||
|
||||
// is a new buffer? if so init the thing
|
||||
if (EMS_Sys_Status.emsRxStatus == EMS_RX_IDLE) {
|
||||
EMS_Sys_Status.emsRxStatus = EMS_RX_ACTIVE; // status set to active
|
||||
length = 0;
|
||||
}
|
||||
|
||||
// fill IRQ buffer, by emptying FIFO
|
||||
if (U0IS & ((1 << UIFF) | (1 << UITO) | (1 << UIBD))) {
|
||||
// get data from Rx
|
||||
while ((USS(EMSUART_UART) >> USRXC) & 0xFF) {
|
||||
uart_buffer[length++] = USF(EMSUART_UART);
|
||||
}
|
||||
|
||||
// clear Rx FIFO full and Rx FIFO timeout interrupts
|
||||
U0IC = (1 << UIFF);
|
||||
U0IC = (1 << UITO);
|
||||
}
|
||||
|
||||
// BREAK detection = End of EMS data block
|
||||
|
||||
if (USIS(EMSUART_UART) & ((1 << UIBD))) {
|
||||
// disable all interrupts and clear them
|
||||
ETS_UART_INTR_DISABLE();
|
||||
|
||||
U0IC = (1 << UIBD); // INT clear the BREAK detect interrupt
|
||||
|
||||
// copy data into transfer buffer
|
||||
pEMSRxBuf->writePtr = length;
|
||||
|
||||
os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, length);
|
||||
|
||||
// set the status flag stating BRK has been received and we can start a new package
|
||||
EMS_Sys_Status.emsRxStatus = EMS_RX_IDLE;
|
||||
|
||||
// call emsuart_recvTask() at next opportunity
|
||||
system_os_post(EMSUART_recvTaskPrio, 0, 0);
|
||||
|
||||
// re-enable UART interrupts
|
||||
ETS_UART_INTR_ENABLE();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* system task triggered on BRK interrupt
|
||||
* Read commands are all asynchronous
|
||||
*/
|
||||
static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) {
|
||||
// get next free EMS Receive buffer
|
||||
_EMSRxBuf * pCurrent = pEMSRxBuf;
|
||||
pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS];
|
||||
|
||||
// transmit EMS buffer, excluding the BRK
|
||||
if (pCurrent->writePtr > 1) {
|
||||
ems_parseTelegram((uint8_t *)pCurrent->buffer, (pCurrent->writePtr) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* init UART0
|
||||
* This is low level ESP8266 code to manually configure the UART driver
|
||||
*/
|
||||
void ICACHE_FLASH_ATTR emsuart_init() {
|
||||
ETS_UART_INTR_DISABLE();
|
||||
ETS_UART_INTR_ATTACH(NULL, NULL);
|
||||
|
||||
// allocate and preset EMS Receive buffers
|
||||
for (int i = 0; i < EMS_MAXBUFFERS; i++) {
|
||||
_EMSRxBuf * p = (_EMSRxBuf *)malloc(sizeof(_EMSRxBuf));
|
||||
paEMSRxBuf[i] = p;
|
||||
}
|
||||
pEMSRxBuf = paEMSRxBuf[0]; // preset EMS Rx Buffer
|
||||
|
||||
// pin settings
|
||||
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U);
|
||||
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0RXD);
|
||||
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0RXD_U);
|
||||
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD);
|
||||
|
||||
// set 9600, 8 bits, no parity check, 1 stop bit
|
||||
USD(EMSUART_UART) = (ESP8266_CLOCK / EMSUART_BAUD);
|
||||
USC0(EMSUART_UART) = EMSUART_CONFIG; // 8N1
|
||||
|
||||
// flush everything left over in buffer, this clears both rx and tx FIFOs
|
||||
uint32_t tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
|
||||
USC0(EMSUART_UART) |= (tmp); // set bits
|
||||
USC0(EMSUART_UART) &= ~(tmp); // clear bits
|
||||
|
||||
// conf 1 params
|
||||
// UCTOE = RX TimeOut enable (1)
|
||||
// UCTOT = RX TimeOut Threshold (7bit) = want this when no more data after 2 characters. (default was 2)
|
||||
// UCFFT = RX FIFO Full Threshold (7 bit) = want this to be 31 for 32 bytes of buffer. (default was 127).
|
||||
USC1(EMSUART_UART) = 0; // reset config first
|
||||
USC1(EMSUART_UART) = (31 << UCFFT) | (0x02 << UCTOT) | (1 << UCTOE); // enable interupts
|
||||
|
||||
// set interrupts for triggers
|
||||
USIC(EMSUART_UART) = 0xffff; // clear all interupts
|
||||
USIE(EMSUART_UART) = 0; // disable all interrupts
|
||||
|
||||
// enable rx break, fifo full and timeout.
|
||||
// not frame error UIFR or overflow UIOF. Frame errors are too frequent
|
||||
// and overflow never happens because our buffer is only max 32 bytes
|
||||
USIE(EMSUART_UART) = (1 << UIBD) | (1 << UIFF) | (1 << UITO);
|
||||
|
||||
// set up interrupt callbacks for Rx and Tx
|
||||
system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen);
|
||||
//system_os_task(emsuart_sendTask, sendTaskPrio, sendTaskQueue, sendTaskQueueLen);
|
||||
|
||||
// disable esp debug which will go to Tx and mess up the line
|
||||
// system_set_os_print(0); // https://github.com/espruino/Espruino/issues/655
|
||||
|
||||
ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, NULL);
|
||||
ETS_UART_INTR_ENABLE();
|
||||
|
||||
// when all ready swap RX and TX to use GPIO13 (D7) and GPIO15 (D8) respectively
|
||||
system_uart_swap();
|
||||
}
|
||||
|
||||
/*
|
||||
* Send a BRK signal
|
||||
* Which is a 11-bit set of zero's (11 cycles)
|
||||
*/
|
||||
void ICACHE_FLASH_ATTR emsuart_tx_brk() {
|
||||
// must make sure Tx FIFO is empty
|
||||
while (((USS(EMSUART_UART) >> USTXC) & 0xff) != 0)
|
||||
;
|
||||
|
||||
uint32_t tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
|
||||
USC0(EMSUART_UART) |= (tmp); // set bits
|
||||
USC0(EMSUART_UART) &= ~(tmp); // clear bits
|
||||
|
||||
// To create a 11-bit <BRK> we set TXD_BRK bit so the break signal will automatically be sent when the tx fifo is empty
|
||||
USC0(EMSUART_UART) |= (1 << UCBRK); // set bit
|
||||
delayMicroseconds(EMS_TX_BRK_WAIT);
|
||||
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear bit
|
||||
}
|
||||
|
||||
/*
|
||||
* Send to tx, ending with a BRK
|
||||
*/
|
||||
void ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) {
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
USF(EMSUART_UART) = buf[i];
|
||||
}
|
||||
emsuart_tx_brk();
|
||||
}
|
||||
|
||||
/*
|
||||
* Send the Poll (our ID) to Tx as a single byte and ending with a <BRK>
|
||||
*/
|
||||
void ICACHE_FLASH_ATTR emsaurt_tx_poll() {
|
||||
USF(EMSUART_UART) = EMS_ID_ME;
|
||||
emsuart_tx_brk();
|
||||
}
|
||||
36
src/emsuart.h
Normal file
36
src/emsuart.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* emsuart.h
|
||||
* Header file for emsuart.cpp
|
||||
* Paul Derbyshire - https://github.com/proddy/EMS-ESP-Boiler
|
||||
*/
|
||||
#ifndef __EMSUART_H
|
||||
#define __EMSUART_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#define EMSUART_UART 0 // UART 0 - there is only one on the esp8266
|
||||
#define EMSUART_CONFIG 0x1c // 8N1 (8 bits, no stop bits, 1 parity)
|
||||
#define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit
|
||||
|
||||
#define EMS_MAXBUFFERS 4 // 4 buffers for circular filling to avoid collisions
|
||||
#define EMS_MAXBUFFERSIZE 128 // max size of the buffer. packets are 32 bits
|
||||
|
||||
// for how long we drop the Tx signal to create a 11-bit Break of zeros
|
||||
// At 9600 baud, 11 bits will be 1144 microseconds
|
||||
// the BRK from UBA is roughly 1.039ms, so accounting for hardware lag use 2078 (for half-duplex) - 8 (lag)
|
||||
#define EMS_TX_BRK_WAIT 2070
|
||||
|
||||
#define EMSUART_recvTaskPrio 1
|
||||
#define EMSUART_recvTaskQueueLen 64
|
||||
|
||||
typedef struct {
|
||||
int16_t writePtr;
|
||||
uint8_t buffer[EMS_MAXBUFFERSIZE];
|
||||
} _EMSRxBuf;
|
||||
|
||||
void ICACHE_FLASH_ATTR emsuart_init();
|
||||
void ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len);
|
||||
void ICACHE_FLASH_ATTR emsaurt_tx_poll();
|
||||
void ICACHE_FLASH_ATTR emsuart_tx_brk();
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user