first commit

This commit is contained in:
proddy
2018-05-14 23:16:06 +02:00
parent 58ca5c176e
commit 4ab7bb6835
23 changed files with 7838 additions and 0 deletions

39
.clang-format Normal file
View File

@@ -0,0 +1,39 @@
Language: Cpp
BasedOnStyle: LLVM
UseTab: Never
IndentWidth: 4
ColumnLimit: 120
TabWidth: 4
#BreakBeforeBraces: Custom
BraceWrapping:
AfterControlStatement: false
AfterFunction: false
AfterClass: true
AfterEnum: true
BeforeElse: false
ReflowComments: false
AlignAfterOpenBracket: Align # If true, horizontally aligns arguments after an open bracket.
AlignConsecutiveAssignments: true # This will align the assignment operators of consecutive lines
AlignConsecutiveDeclarations: true # This will align the declaration names of consecutive lines
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
#AlwaysBreakAfterReturnType: TopLevel
AlwaysBreakTemplateDeclarations: true # If true, always break after the template<...> of a template declaration
BinPackArguments: false
BinPackParameters: false
BreakBeforeBinaryOperators: NonAssignment
BreakConstructorInitializersBeforeComma: true # Always break constructor initializers before commas and align the commas with the colon.
ExperimentalAutoDetectBinPacking: false
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 4
PenaltyBreakBeforeFirstCallParameter: 200
PenaltyExcessCharacter: 10
PointerAlignment: Middle
SpaceAfterCStyleCast: false
SpaceBeforeAssignmentOperators: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
.pioenvs
.piolibdeps
.clang_complete
.gcc-flags.json
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/*.db
.vscode/.browse.c_cpp.db*
platformio.ini
lib/readme.txt
.vscode/settings.json
.vscode/extensions.json
.vscode/BROWSE.VC.DB-wal
.vscode/BROWSE.VC.DB-shm
lib/readme.txt

65
.travis.yml Normal file
View File

@@ -0,0 +1,65 @@
# Continuous Integration (CI) is the practice, in software
# engineering, of merging all developer working copies with a shared mainline
# several times a day < http://docs.platformio.org/page/ci/index.html >
#
# Documentation:
#
# * Travis CI Embedded Builds with PlatformIO
# < https://docs.travis-ci.com/user/integration/platformio/ >
#
# * PlatformIO integration with Travis CI
# < http://docs.platformio.org/page/ci/travis.html >
#
# * User Guide for `platformio ci` command
# < http://docs.platformio.org/page/userguide/cmd_ci.html >
#
#
# Please choice one of the following templates (proposed below) and uncomment
# it (remove "# " before each line) or use own configuration according to the
# Travis CI documentation (see above).
#
#
# Template #1: General project. Test it using existing `platformio.ini`.
#
# language: python
# python:
# - "2.7"
#
# sudo: false
# cache:
# directories:
# - "~/.platformio"
#
# install:
# - pip install -U platformio
#
# script:
# - platformio run
#
# Template #2: The project is intended to by used as a library with examples
#
# language: python
# python:
# - "2.7"
#
# sudo: false
# cache:
# directories:
# - "~/.platformio"
#
# env:
# - PLATFORMIO_CI_SRC=path/to/test/file.c
# - PLATFORMIO_CI_SRC=examples/file.ino
# - PLATFORMIO_CI_SRC=path/to/test/directory
#
# install:
# - pip install -U platformio
#
# script:
# - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N

348
README.md Normal file
View File

@@ -0,0 +1,348 @@
# EMS-ESP-Boiler
Boiler is an implementation of the EMS bridge (Energy Management System) for Buderus/Bosch/Nefit boilers specifically using an ESP8266. With the code and circuit you'll be able to read values and write commands to the Boilr and any connected devices like a Thermostat. And the data is collected and sent via MQTT to Home Assistant or another server.
[![version](https://img.shields.io/badge/version-1.0-brightgreen.svg)](CHANGELOG.md)
[![branch](https://img.shields.io/badge/branch-dev-orange.svg)](https://github.org/xoseperez/espurna/tree/dev/)
[![license](https://img.shields.io/github/license/xoseperez/espurna.svg)](LICENSE)
- [EMS-ESP-Boiler](#ems-esp-boiler)
- [Introduction](#introduction)
- [Acknowledgments](#acknowledgments)
- [ESP8266 device Compatibility](#esp8266-device-compatibility)
- [Getting Started](#getting-started)
- [Debugging](#debugging)
- [Building the Circuit](#building-the-circuit)
- [Known Issues](#known-issues)
- [To Do](#to-do)
- [How the EMS works](#how-the-ems-works)
- [1. EMS Polling](#1-ems-polling)
- [2. EMS Broadcasting](#2-ems-broadcasting)
- [3. EMS Sending](#3-ems-sending)
- [The Code](#the-code)
- [Customizing](#customizing)
- [MQTT](#mqtt)
- [Home Assistant Configuration](#home-assistant-configuration)
- [Building the Firmware](#building-the-firmware)
- [Using pre-built firmware's](#using-pre-built-firmwares)
- [Using PlatformIO](#using-platformio)
- [Using ESPurna](#using-espurna)
- [Your comments and feeback](#your-comments-and-feeback)
## Introduction
I wanted to build my own Thermostat (similar to an Nefit Easy) and control it via Home Assistant and MQTT messages. The other driver for this project is that I wanted to know how long my two teenage daughters where taking showers and build in a timer that sends a short warning shot of cold water after a specific time *\<evil laugh\>*.
## Acknowledgments
First, a big thanks and appreciation to the following people and their projects for giving me inspiration and code snippets:
**bbqkees** - Kees built an Arduino version to read from the EMS for Domoticz, including a circuit which you can purchase. Check out https://github.com/bbqkees/Nefit-Buderus-EMS-bus-Arduino-Domoticz
**susisstrolch** - A working version of the EMS bridge and circuit for the ESP8266. https://github.com/susisstrolch/EMS-ESP12
**EMS Wiki** - A reference for decoding the EMS telegrams (but I found not always 100% accurate). https://emswiki.thefischer.net/doku.php?id=wiki:ems:telegramme
## ESP8266 device Compatibility
I've tested the code and circuit with a Wemos D1 Mini, Wemos D1 Mini Pro, Nodemcu0.9 and Nodemcu2 development boards.
## Getting Started
1. Build the circuit (or purchase a ready built one from Kees via his [GitHub](https://github.com/bbqkees/Nefit-Buderus-EMS-bus-Arduino-Domoticz) page or the [Domoticz forum](http://www.domoticz.com/forum/viewtopic.php?f=22&t=22079&start=20))
2. Connect the EMS to the circuit and the RX/TX to the ESP8266 on pins D7 and D8. The EMS connection can either be the 12-15V AC direct from the EMS (split from the Thermostat if you have one) or from the Service Jack at the front. Again Kees has a nice explanation [here](https://github.com/bbqkees/Nefit-Buderus-EMS-bus-Arduino-Domoticz/tree/master/Documentation).
3. Optionally connect the three LEDs to show RX and TX traffic and Error codes to pins D1, D2, D3 respectively. I use 220 Ohm pull-down resistors. The pins are configurable in ``boiler.ino``. See the explanation below in the **code** section.
3. Build and upload the firmware to an ESP8266 device. Make sure you set the MQTT and WiFi credentials. If you're not using MQTT leave the MQTT_IP blank. The firmware supports OTA too and the default hostname is 'boiler' or 'boiler.' depending on the mdns resolve.
4. Power the ESP from an external 5V supply, either via USB or direct into the 5v vin pin.
5. Power the EMS circuit using the 3v3 out from the ESP8266. It will also work with 5v.
6. When it has booted, telnet (port 23) to the IP of the ESP8266. If everything is working you should see the messages appear in the window as shown below. I use Telnet client that comes with Linux distro on Windows 10 but you can also use [putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html).
## Debugging
Use the telnet client to inform you of all activity and errors if they happen. Note, if you're unable to to connect start the ESP with serial mode and look for connection errors in the serial out window.
The telnet will show for example:
![Telnet](doc/telnet/telnet_example.JPG)
If you hit 'q' and Enter, it will toggle verbose logging and you will see more details:
![Telnet](doc/telnet/telnet_verbose.JPG)
To see the current values of the Boiler type 's' and hit Enter:
![Telnet](doc/telnet/telnet_stats.JPG)
You can issue commands directly to the bus using 'r' and some other examples I programmed such as:
* **r** to send a read command to a device to fetch values
* **t** set the thermostat temperature to the given value. This is also what Home Assistance uses via MQTT
* **w** to adjust the temperature of the warm water from the boiler
* **a** to turn the warm water on and off
* **p** to toggle the Polling response on/off. It's not necessary to have Polling enabled, but its the proper way
* **T** to toggle thermostat reading on/off
* **S** to toggle the Shower Timer functionality on/off
*Disclaimer: be careful when sending values to the boiler. If in doubt you can always reset the boiler to its original factory settings by following the instructions in the user guide. On my Nefit Trendline HRC30 its pressing the Home and Menu buttons at the same time, selecting factory settings from the scroll menu and pressing the button Reset. ***Yes, I learned the hard way!***
## Building the Circuit
The EMS circuit is really all credit to the hard work many people have done before me, noticeably Juergen with his ESP8266 [version](https://github.com/susisstrolch/EMS-ESP8266_12-PCB/tree/newmaster/Schematics/EMS-ESP8266-12).
I've included a prototype boards you can build yourself on a breadboard. One for only Reading values from the Boiler and a second with the inclusion of the Write logic to send commands.
We need the RX/TX of the ESP8266 for flashing, so the code in ``emsuart.cpp`` switches the UART pins to use RX1 and TX1 (GPIO13/D7 and GPIO15/D8 respectively). This also prevents any bogus stack data being sent to EMS bus when the ESP8266 decides to crash.
The breadboard layout was done using [DIY Layout Creator](https://github.com/bancika/diy-layout-creator) and sources files are included. It looks like:
Read Only | Both Read and Write
--- | ---
![Read only](doc/schematics/readonly.JPG) | ![Read and Write)](doc/schematics/readwrite.JPG)
The schematic from Juergen which this is based off is:
![Schematic](doc/schematics/circuit.png)
**Notes:**\
*Optionally I've also added 2 polyfuses between the EMS and the Inductors which are not shown in the layout or schematics above.*
Here's an example circuit using an NodeMCU2 with the additional LEDs and buck converter. The inputs from the EMS are not shown but there are at J60 and J58 at the bottom left.
![|Breadboard](doc/schematics/breadboard.JPG)
## Known Issues
* Sometimes the first write command is not sent, probably due to a collision somewhere in the uart code. The retries in the code fix that but it is annoying nevertheless.
* Sometimes you get duplicate telegrams being processed. Again not an issue, but annoying. This is a bug somewhere in the code.
## To Do
Here's still things to do on my todo list:
* Make an ESPurna version. ESPurna takes care of the wifi, mqtt, web, telnet and does a better job that my ESPHelper code.
* Complete the ESP32 version. It's surprisingly a lot easier doing the UART code on an ESP32. The first beta version is working.
* Find a better way to control the 3-way valve to switch the warm water off quickly rather than adjusting the temperature.
* Find a stable way of powering the ESP8266 from the EMS 12V using a buck step-down converter. This does work reasonably ok on a breakboard but there is noise.
## How the EMS works
Packages are sent on the EMS "bus" from the Boiler and any other compatible connected device. The protocol is 9600 baud, 8N1 (8 bytes, no parity, 1 stop bit). Each package is terminated with a Break signal depicted as <BRK> which is a 11 bit long low signal (zeros).
A package can be a single byte (see Polling below) or an actual data telegram. A telegram is always in the format:
``[src] [dest] [type] [offset] [data] [crc] <BRK>``
**IDs**
Each device has a unique ID.
The Boiler (MC10) has an ID of 0x08 and is referred to as the Bus Master.
My thermostat, which is a Moduline 300 uses the RC20 format and has an ID 0x17. If you're using an RC30 or RC35 type thermostat use 0x10 and make adjustments in the code as appropriate. Kees did a nice write-up on his github page [here](https://github.com/bbqkees/Nefit-Buderus-EMS-bus-Arduino-Domoticz/blob/master/README.md).
Our device has a special ID of 0x0B which is a reserved ID for a Service tool. Nice and handy.
### 1. EMS Polling
The bus master (boiler) sends out a poll request every second by sending out a sequential list of all possible IDs as a single byte followed by a break signal. The ID has its 7th bit set so it's `[dest|0x80] <BRK>`.
Any connected device can respond to a Polling call with an acknowledging by sending back a single byte with its own ID. For example in our case we would listen for a `[0x8B] <BRK>` (us) and then send back `[0x0B] <BRK>` to say we're alive and ready.
Polling is also the key to start transmitting any packages queued for sending.
## 2. EMS Broadcasting
When a device is broadcasting to everyone there is no specific destination needed so the [dest] is always 0x00.
The Boiler (ID 0x08) will send out these broadcast telegrams regularly:
Type | Description | Data length | Frequency
--- | --- | --- | --- |
0x34 | UBAMonitorWWMessage | 19 bytes | 10 seconds
0x18 | UBAMonitorFast | 25 bytes | 10 seconds
0x19 | UBAMonitorSlow | 22 bytes | every minute
0x1c | UBAWartungsmelding | 27 bytes | every minute
0x2a | status, specific to boiler type | - | 10 seconds
And a thermostat (id 0x17 for a RC20) these:
Type | Description
--- | --- |
0x06 | time on thermostat Y,M,H,D,M,S,wd
0xA8 | setting low, manual, clock, overrule clock setting, manual setpoint temperature
0xA3 | thermostat temperatures
0x91 | set point room temperature x 2, room temperature x 10
Refer to the code in ``ems.cpp`` for further explanation on how to parse these messages or the EMS Wiki (link above) for a detailed explanation.
### 3. EMS Sending
Telegram packets can only be sent after the Boiler sends a poll to the sending device. The response can be a read command to request data or a send command to return data. At the end of the transmission a poll response is sent from the client (e.g. ``<ID> <BRK>``) to say we're all done and free up the bus for other clients.
When doing a request to read data the ``[src]`` is our device (0x0B) and the ``[dest]`` has it's 7-bit set. For example to request data from the Thermostat use ``[dest] = 0x97`` as RC20 has an ID 0x17.
When doing a write request, the 7th bit is masked in the ``[dest]``. After a write request the destination device will send either a single byte 0x01 for success or 0x04 for fail.
## The Code
This code here is really for reference only, I don't expect anyone to use as is since it's highly tailored to my environment and my needs. Most of the code is self explanatory with comments here and there, however if you wish to make some changes start with the ``defines`` and ``const`` sections at the top of ``boiler.ino``.
`emsuart.cpp` handles the low level UART read and write logic. You shouldn't need to touch this. All receive commands from the EMS bus are handled asynchronously using a circular buffer via an interrupt. A seperate call processes the buffer and extracts the telegrams. Since we don't send many Write commands this is done sequentially.
`ems.cpp` is the logic to read the EMS packets (telegrams) and process them. If you have another thermostat type this is where you will configure it. The logic is roughly:
`boiler.ino` is the Arduino code for the ESP8266 that kicks it all off. This is where we have specific logic such as the code to monitor and alert on the Shower timer.
### Customizing
Most of the changes will be done in ``boiler.ino``.
* To add new handlers for data types, create a callback function and add to the EMS_Types at the top of the file ``ems.cpp`` and the defines in ``ems.h``
* To change your thermostat type set ``EMS_ID_THERMOSTAT`` in ``ems.h``. The default is 0x17 for an RC20.
* The DEFINES ``BOILER_THERMOSTAT_ENABLED``, ``BOILER_SHOWER_ENABLED`` and ``BOILER_SHOWER_TIMER`` enabled the Thermostat logic, the shower logic and the shower timer alert logic respectively. 1 is on and 0 is off.
### MQTT
When the ESP8266 boots it will send a start signal via MQTT to the broker. I use this to mark the boot time for the device and send out a notification. This is useful to monitor when the ESP8266 crashes and reboots.
The temperatures of the thermostat are sent as a JSON object using
``home/boiler/thermostat`` and payload for example of ``{"currtemp":"22.30","seltemp":"20.00"}``
This can be also be configured in the ``TOPIC_*`` defines in ``boiler.ino``.
## Home Assistant Configuration
Assuming you've setup up MQTT correctly, here is my HA configuration:
**automations.yaml**
- id: thermostat_temp
alias: 'Adjust Thermostat Temperature'
trigger:
platform: state
entity_id: input_number.thermostat_temp
action:
service: mqtt.publish
data_template:
topic: 'home/boiler/thermostat_temp'
payload: >
{{ states.input_number.thermostat_temp.state }}
- id: boiler_shower
alias: Alert shower time
trigger:
platform: mqtt
topic: home/boiler/showertime
action:
- service: notify.pushbullet
data_template:
title: 'Shower duration was {{trigger.payload}} at {states.sensor.time.state}}'
message: 'Boiler'
- service: notify.boiler_notify
data_template:
title: "Shower finished!"
message: 'Shower duration was {{trigger.payload}} at {{states.sensor.time.state}}'
**input_number.yaml**
thermostat_temp:
name: Set thermostat temperature
icon: mdi:temperature-celsius
min: 10
max: 25
step: 0.5
unit_of_measurement: "°C"
mode: slider
**groups.yaml**
thermostat:
name: Thermostat
view: no
entities:
- sensor.boiler_thermostat_current_temperature
- sensor.boiler_thermostat_set_temperature
- input_number.thermostat_temp
And looks like:
![Home Assistant panel)](doc/ha/ha.JPG)
# Building the Firmware
### Using pre-built firmware's
In the `/firmware` folder, if there are pre-built versions you can upload using esptool (https://github.com/espressif/esptool) bootloader. Follow these instructions for Windows:
1. Check if you have python 2.7 installed. If not download it from https://www.python.org/downloads/ and make sure you add Python to the windows PATH so it'll recognize .py files
2. Install the ESPTool (https://github.com/espressif/esptool) by running `pip install esptool` from a command prompt.
3. Connect the ESP via USB, figure out the COM port
4. do `esptool.py -p <com> write_flash 0x00000 <firmware>` where firmware is the .bin file and com is the com port, e.g. COM3
### Using PlatformIO
There are two ways to compile and build the firmware.
The first method is a standalone version which uses a modified version of [ESPHelper](https://github.com/ItKindaWorks/ESPHelper) for the WiFi, OTA and MQTT handling. I've added some code to add a Telnet server which is useful for debugging since you can't use the serial port because UART is configured to use different pins.
To compile, using PlatformIO and modify the `platformio.ini` adding these build flags:
`WIFI_SSID, WIFI_PASSWORD, MQTT_IP, MQTT_USER, MQTT_PASS`
Here's an example `platformio.ini` file:
```
[platformio]
[common]
framework = arduino
lib_deps =
Time
PubSubClient
ArduinoJson
Ticker
[env:nodemcuv2]
board = nodemcuv2
platform = espressif8266
framework = arduino
lib_deps = ${common.lib_deps}
upload_speed = 921600
build_flags = '-DWIFI_SSID="<my_ssid>"' '-DWIFI_PASSWORD="<my_password>"' '-DMQTT_IP="<broker_ip>"' '-DMQTT_USER="<broker_username>"' '-DMQTT_PASS="<broker_password>"'
; comment out next line if using USB and not OTA
upload_port = "boiler."
```
### Using ESPurna
*Note: This is still work in progress. The ESPurna code for the HTML config is still to be added*
[ESPurna](https://github.com/xoseperez/espurna/wiki) is lovely framework that handles most of the tedious tasks of building IoT devices so you can focus on the functionality you need.
If you're using Windows follow these steps. We'll be using the free Visual Studio Code and PlatformIO. Similar steps also work on Linux distributions.
- First download Git: https://git-scm.com/download/win (install using the default settings)
- Visual Studio Code (VSC): https://code.visualstudio.com/docs/?dv=win
- Install node.js and npm (LTS version): https://nodejs.org/en/download
restart your PC just in case and start Visual Studio Code. It should detect Git. Now go and search for and install these visual studio code extensions:
- PlatformIO IDE
- GitLens
and hit **reload** to activate them all.
Next download espurna by cloning the git repository from https://github.com/xoseperez/espurna.git, start VSC and open the folder ``espurna\code``
- open a terminal window (ctrl-`)
- Install the node modules: ``npm install --only=dev``
- Build the web interface: ``node node_modules/gulp/bin/gulp.js``
If you run into issues refer to proper ESPurnas setup instructions [here](https://github.com/xoseperez/espurna/wiki/Build-and-update-from-Visual-Studio-Code-using-PlatformIO).
### Your comments and feeback
Any comments or suggestions are very welcome. You can contact me at **dev** at **derbyshire** dot **nl** or via an *Issue* in GitHub

BIN
doc/ha/ha.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
doc/schematics/circuit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

2673
doc/schematics/ems_full.diy Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
doc/schematics/readonly.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
doc/telnet/telnet_stats.JPG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

12
extra_script.py Normal file
View File

@@ -0,0 +1,12 @@
Import("env")
#my_flags = env.ParseFlags(env['BUILD_FLAGS'])
#defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")}
# print defines
#env.Replace(PROGNAME="firmware_%s" % defines.get("VERSION"))
# print env.Dump()
env.Replace(PROGNAME="firmware_%s" % env['BOARD'])

915
lib/ESPHelper/ESPHelper.cpp Normal file
View File

@@ -0,0 +1,915 @@
/*
Based off :
1) ESPHelper.cpp - Copyright (c) 2017 ItKindaWorks Inc All right reserved. github.com/ItKindaWorks
2) https://github.com/JoaoLopesF/ESP8266-RemoteDebug-Telnet
*/
#include "ESPHelper.h"
WiFiServer telnetServer(TELNET_PORT);
//initializer with single netInfo network
ESPHelper::ESPHelper(netInfo * startingNet) {
//disconnect from and previous wifi networks
WiFi.softAPdisconnect();
WiFi.disconnect();
//setup current network information
_currentNet = *startingNet;
//validate various bits of network/MQTT info
//network pass
if (_currentNet.pass[0] == '\0') {
_passSet = false;
} else {
_passSet = true;
}
//ssid
if (_currentNet.ssid[0] == '\0') {
_ssidSet = false;
} else {
_ssidSet = true;
}
//mqtt host
if (_currentNet.mqttHost[0] == '\0') {
_mqttSet = false;
} else {
_mqttSet = true;
}
//mqtt port
if (_currentNet.mqttPort == 0) {
_currentNet.mqttPort = 1883;
}
//mqtt username
if (_currentNet.mqttUser[0] == '\0') {
_mqttUserSet = false;
} else {
_mqttUserSet = true;
}
//mqtt password
if (_currentNet.mqttPass[0] == '\0') {
_mqttPassSet = false;
} else {
_mqttPassSet = true;
}
//disable hopping on single network
_hoppingAllowed = false;
//disable ota by default
_useOTA = false;
}
//start the wifi & mqtt systems and attempt connection (currently blocking)
//true on: parameter check validated
//false on: parameter check failed
bool ESPHelper::begin(const char * hostname) {
#ifdef USE_SERIAL1
Serial1.begin(115200);
Serial1.setDebugOutput(true);
#endif
#ifdef USE_SERIAL
Serial.begin(115200);
Serial.setDebugOutput(true);
#endif
// set hostname first
strcpy(_hostname, hostname);
OTA_enable();
setBoottime("<unknown>");
if (_ssidSet) {
strcpy(_clientName, hostname);
/*
// Generate client name based on MAC address and last 8 bits of microsecond counter
_clientName += "esp-";
uint8_t mac[6];
WiFi.macAddress(mac);
_clientName += macToStr(mac);
*/
// set hostname
// can ping by <hostname> or on Windows10 it's <hostname>.
#if defined(ESP8266)
WiFi.hostname(_hostname);
#elif defined(ESP32)
WiFi.setHostname(_hostname);
#endif
//set the wifi mode to station and begin the wifi (connect using either ssid or ssid/pass)
WiFi.mode(WIFI_STA);
if (_passSet) {
WiFi.begin(_currentNet.ssid, _currentNet.pass);
} else {
WiFi.begin(_currentNet.ssid);
}
//as long as an mqtt ip has been set create an instance of PubSub for client
if (_mqttSet) {
//make mqtt client use either the secure or non-secure wifi client depending on the setting
if (_useSecureClient) {
client = PubSubClient(_currentNet.mqttHost,
_currentNet.mqttPort,
wifiClientSecure);
} else {
client = PubSubClient(_currentNet.mqttHost,
_currentNet.mqttPort,
wifiClient);
}
//set the mqtt message callback if needed
if (_mqttCallbackSet) {
client.setCallback(_mqttCallback);
}
}
//define a dummy instance of mqtt so that it is instantiated if no mqtt ip is set
else {
//make mqtt client use either the secure or non-secure wifi client depending on the setting
//(this shouldnt be needed if making a dummy connection since the idea would be that there wont be mqtt in this case)
if (_useSecureClient) {
client = PubSubClient("192.168.1.255",
_currentNet.mqttPort,
wifiClientSecure);
} else {
client = PubSubClient("192.168.1.255",
_currentNet.mqttPort,
wifiClient);
}
}
//ota event handlers
ArduinoOTA.onStart([]() { /* ota start code */ });
ArduinoOTA.onEnd([]() {
//on ota end we disconnect from wifi cleanly before restarting.
WiFi.softAPdisconnect();
WiFi.disconnect();
uint8_t timeout = 0;
//max timeout of 2seconds before just dropping out and restarting
while (WiFi.status() != WL_DISCONNECTED && timeout < 200) {
timeout++;
}
});
ArduinoOTA.onProgress([](unsigned int progress,
unsigned int total) { /* ota progress code */ });
ArduinoOTA.onError([](ota_error_t error) { /* ota error code */ });
//initially attempt to connect to wifi when we begin (but only block for 2 seconds before timing out)
uint8_t timeout = 0; //counter for begin connection attempts
while (((!client.connected() && _mqttSet) || WiFi.status() != WL_CONNECTED)
&& timeout < 200) { //max 2 sec before timeout
reconnect();
timeout++;
}
//attempt to start ota if needed
OTA_begin();
// Initialize the telnet server
telnetServer.begin();
telnetServer.setNoDelay(true);
// init command buffer for console commands
memset(_command, 0, sizeof(_command));
consoleShowHelp(); // show this at bootup
//mark the system as started and return
_hasBegun = true;
return true;
}
//if no ssid was set even then dont try to begin and return false
return false;
}
//end the instance of ESPHelper (shutdown wifi, ota, mqtt)
void ESPHelper::end() {
// Stop telnet Client & Server
if (telnetClient && telnetClient.connected()) {
telnetClient.stop();
}
// Stop server
telnetServer.stop();
OTA_disable();
WiFi.softAPdisconnect();
WiFi.disconnect();
uint8_t timeout = 0;
while (WiFi.status() != WL_DISCONNECTED && timeout < 200) {
timeout++;
}
#ifdef USE_SERIAL
Serial.flush();
#endif
#ifdef USE_SERIAL1
Serial1.flush();
#endif
}
//main loop - should be called as often as possible - handles wifi/mqtt connection and mqtt handler
//true on: network/server connected
//false on: network or server disconnected
int ESPHelper::loop() {
if (_ssidSet) {
//check for good connections and attempt a reconnect if needed
if (((_mqttSet && !client.connected()) || setConnectionStatus() < WIFI_ONLY)
&& _connectionStatus != BROADCAST) {
reconnect();
}
//run the wifi loop as long as the connection status is at a minimum of BROADCAST
if (_connectionStatus >= BROADCAST) {
//run the MQTT loop if we have a full connection
if (_connectionStatus == FULL_CONNECTION) {
client.loop();
}
//check for whether we want to use OTA and whether the system is running
if (_useOTA && _OTArunning) {
ArduinoOTA.handle();
}
//if we want to use OTA but its not running yet, start it up.
else if (_useOTA && !_OTArunning) {
OTA_begin();
ArduinoOTA.handle();
}
// do the telnet stuff
consoleHandle();
return _connectionStatus;
}
}
//return -1 for no connection because of bad network info
return -1;
}
//subscribe to a specific topic (does not add to topic list)
//true on: subscription success
//false on: subscription failed (either from PubSub lib or network is disconnected)
bool ESPHelper::subscribe(const char * topic, uint8_t qos) {
if (_connectionStatus == FULL_CONNECTION) {
//set the return value to the output of subscribe
bool returnVal = client.subscribe(topic, qos);
//loop mqtt client
client.loop();
return returnVal;
}
//if not fully connected return false
else {
return false;
}
}
//add a topic to the list of subscriptions and attempt to subscribe to the topic on the spot
//true on: subscription added to list (does not guarantee that the topic was subscribed to, only that it was added to the list)
//false on: subscription not added to list
bool ESPHelper::addSubscription(const char * topic) {
//default return value is false
bool subscribed = false;
//loop through finding the next available slot for a subscription and add it
for (uint8_t i = 0; i < MAX_SUBSCRIPTIONS; i++) {
if (_subscriptions[i].isUsed == false) {
_subscriptions[i].topic = topic;
_subscriptions[i].isUsed = true;
subscribed = true;
break;
}
}
//if added to the list, subscribe to the topic
if (subscribed) {
subscribe(topic, _qos);
}
return subscribed;
}
//loops through list of subscriptions and attempts to subscribe to all topics
void ESPHelper::resubscribe() {
for (uint8_t i = 0; i < MAX_SUBSCRIPTIONS; i++) {
if (_subscriptions[i].isUsed) {
subscribe(_subscriptions[i].topic, _qos);
yield();
}
}
}
//manually unsubscribes from a topic (This is basically just a wrapper for the pubsubclient function)
bool ESPHelper::unsubscribe(const char * topic) {
return client.unsubscribe(topic);
}
//publish to a specified topic
void ESPHelper::publish(const char * topic, const char * payload) {
if (_mqttSet) {
publish(topic, payload, false);
}
}
//publish to a specified topic with a given retain level
void ESPHelper::publish(const char * topic, const char * payload, bool retain) {
client.publish(topic, payload, retain);
}
//set the callback function for MQTT
void ESPHelper::setMQTTCallback(MQTT_CALLBACK_SIGNATURE) {
_mqttCallback = callback;
//only set the callback if using mqtt AND the system has already been started. Otherwise just save it for later
if (_hasBegun && _mqttSet) {
client.setCallback(_mqttCallback);
}
_mqttCallbackSet = true;
}
//sets a custom function to run when connection to wifi is established
void ESPHelper::setWifiCallback(void (*callback)()) {
_wifiCallback = callback;
_wifiCallbackSet = true;
}
//attempts to connect to wifi & mqtt server if not connected
void ESPHelper::reconnect() {
static uint8_t tryCount = 0;
if (_connectionStatus != BROADCAST
&& setConnectionStatus() != FULL_CONNECTION) {
logger(LOG_CONSOLE, "Attempting WiFi Connection...");
//attempt to connect to the wifi if connection is lost
if (WiFi.status() != WL_CONNECTED) {
_connectionStatus = NO_CONNECTION;
//increment try count each time it cannot connect (this is used to determine when to hop to a new network)
tryCount++;
if (tryCount == 20) {
//change networks (if possible) when we have tried to connect 20 times and failed
changeNetwork();
tryCount = 0;
return;
}
}
// make sure we are connected to WIFI before attempting to reconnect to MQTT
//----note---- maybe want to reset tryCount whenever we succeed at getting wifi connection?
if (WiFi.status() == WL_CONNECTED) {
//if the wifi previously wasnt connected but now is, run the callback
if (_connectionStatus < WIFI_ONLY && _wifiCallbackSet) {
_wifiCallback();
}
logger(LOG_CONSOLE, "---WiFi Connected!---");
_connectionStatus = WIFI_ONLY;
//attempt to connect to mqtt when we finally get connected to WiFi
if (_mqttSet) {
static uint8_t timeout =
0; //allow a max of 5 mqtt connection attempts before timing out
if (!client.connected() && timeout < 5) {
logger(LOG_CONSOLE, "Attempting MQTT connection...");
uint8_t connected = 0;
//connect to mqtt with user/pass
if (_mqttUserSet) {
connected = client.connect(_clientName,
_currentNet.mqttUser,
_currentNet.mqttPass);
}
//connect to mqtt without credentials
else {
connected = client.connect(_clientName);
}
//if connected, subscribe to the topic(s) we want to be notified about
if (connected) {
logger(LOG_CONSOLE, " -- Connected");
//if using https, verify the fingerprint of the server before setting full connection (return on fail)
// removing this as not supported with ESP32, see https://github.com/espressif/arduino-esp32/issues/278
/*
if (wifiClientSecure.verify(_fingerprint,
_currentNet.mqttHost)) {
logger(LOG_CONSOLE,
"Certificate Matches - SUCESS\n");
} else {
logger(LOG_CONSOLE,
"Certificate Doesn't Match - FAIL\n");
return;
}
}
*/
_connectionStatus = FULL_CONNECTION;
resubscribe();
timeout = 0;
} else {
logger(LOG_CONSOLE, " -- Failed\n");
}
timeout++;
}
//if we still cant connect to mqtt after 10 attempts increment the try count
if (timeout >= 5 && !client.connected()) {
timeout = 0;
tryCount++;
if (tryCount == 20) {
changeNetwork();
tryCount = 0;
return;
}
}
}
}
//reset the reconnect metro
//reconnectMetro.reset();
}
}
uint8_t ESPHelper::setConnectionStatus() {
//assume no connection
uint8_t returnVal = NO_CONNECTION;
//make sure were not in broadcast mode
if (_connectionStatus != BROADCAST) {
//if connected to wifi set the mode to wifi only and run the callback if needed
if (WiFi.status() == WL_CONNECTED) {
if (_connectionStatus < WIFI_ONLY
&& _wifiCallbackSet) { //if the wifi previously wasn't connected but now is, run the callback
_wifiCallback();
}
returnVal = WIFI_ONLY;
//if mqtt is connected as well then set the status to full connection
if (client.connected()) {
returnVal = FULL_CONNECTION;
}
}
}
else {
returnVal = BROADCAST;
}
//set the connection status and return
_connectionStatus = returnVal;
return returnVal;
}
//changes the current network settings to the next listed network if network hopping is allowed
void ESPHelper::changeNetwork() {
//only attempt to change networks if hopping is allowed
if (_hoppingAllowed) {
//change the index/reset to 0 if we've hit the last network setting
_currentIndex++;
if (_currentIndex >= _netCount) {
_currentIndex = 0;
}
//set the current netlist to the new network
_currentNet = *_netList[_currentIndex];
//verify various bits of network info
//network password
if (_currentNet.pass[0] == '\0') {
_passSet = false;
} else {
_passSet = true;
}
//ssid
if (_currentNet.ssid[0] == '\0') {
_ssidSet = false;
} else {
_ssidSet = true;
}
//mqtt host
if (_currentNet.mqttHost[0] == '\0') {
_mqttSet = false;
} else {
_mqttSet = true;
}
//mqtt username
if (_currentNet.mqttUser[0] == '\0') {
_mqttUserSet = false;
} else {
_mqttUserSet = true;
}
//mqtt password
if (_currentNet.mqttPass[0] == '\0') {
_mqttPassSet = false;
} else {
_mqttPassSet = true;
}
printf("Trying next network: %s\n", _currentNet.ssid);
//update the network connection
updateNetwork();
}
}
void ESPHelper::updateNetwork() {
logger(LOG_CONSOLE, "\tDisconnecting from WiFi");
WiFi.disconnect();
logger(LOG_CONSOLE, "\tAttempting to begin on new network...");
//set the wifi mode
WiFi.mode(WIFI_STA);
//connect to the network
if (_passSet && _ssidSet) {
WiFi.begin(_currentNet.ssid, _currentNet.pass);
} else if (_ssidSet) {
WiFi.begin(_currentNet.ssid);
} else {
WiFi.begin("NO_SSID_SET");
}
logger(LOG_CONSOLE, "\tSetting new MQTT server");
//setup the mqtt broker info
if (_mqttSet) {
client.setServer(_currentNet.mqttHost, _currentNet.mqttPort);
} else {
client.setServer("192.168.1.3", 1883);
}
logger(LOG_CONSOLE, "\tDone - Ready for next reconnect attempt");
}
//enable use of OTA updates
void ESPHelper::OTA_enable() {
_useOTA = true;
ArduinoOTA.setHostname(_hostname);
OTA_begin();
}
//begin the OTA subsystem but with a check for connectivity and enabled use of OTA
void ESPHelper::OTA_begin() {
if (_connectionStatus >= BROADCAST && _useOTA) {
ArduinoOTA.begin();
_OTArunning = true;
}
}
//disable use of OTA updates
void ESPHelper::OTA_disable() {
_useOTA = false;
_OTArunning = false;
}
// Is CR or LF ?
bool ESPHelper::isCRLF(char character) {
return (character == '\r' || character == '\n');
}
// handler for Telnet
void ESPHelper::consoleHandle() {
// look for Client connect trial
if (telnetServer.hasClient()) {
if (telnetClient && telnetClient.connected()) {
// Verify if the IP is same than actual connection
WiFiClient newClient;
newClient = telnetServer.available();
String ip = newClient.remoteIP().toString();
if (ip == telnetClient.remoteIP().toString()) {
// Reconnect
telnetClient.stop();
telnetClient = newClient;
} else {
// Desconnect (not allow more than one connection)
newClient.stop();
return;
}
} else {
// New TCP client
telnetClient = telnetServer.available();
}
if (!telnetClient) { // No client yet ???
return;
}
// Set client
telnetClient.setNoDelay(true); // faster
telnetClient.flush(); // clear input buffer, to prevent strange characters
_lastTimeCommand = millis(); // To mark time for inactivity
// Show the initial message
consoleShowHelp();
// Empty buffer in
while (telnetClient.available()) {
telnetClient.read();
}
}
// Is client connected ? (to reduce overhead in active)
_telnetConnected = (telnetClient && telnetClient.connected());
// Get command over telnet
if (_telnetConnected) {
char last = ' '; // To avoid process two times the "\r\n"
while (telnetClient.available()) { // get data from Client
// Get character
char character = telnetClient.read();
// Newline (CR or LF) - once one time if (\r\n)
if (isCRLF(character) == true) {
if (isCRLF(last) == false) {
// Process the command
if (strlen(_command) > 0) {
consoleProcessCommand();
}
}
// reset for next command
memset(_command, 0, sizeof(_command));
} else if (isPrintable(character)) {
// Concat char to end of buffer
uint16_t len = strlen(_command);
_command[len] = character;
_command[len + 1] = '\0';
}
// Last char
last = character;
}
// Inactivity - close connection if not received commands from user in telnet to reduce overheads
if ((millis() - _lastTimeCommand) > MAX_TIME_INACTIVE) {
telnetClient.println("* Closing session due to inactivity");
telnetClient.flush();
telnetClient.stop();
_telnetConnected = false;
}
}
}
// Set help for commands over telnet setted by sketch
void ESPHelper::consoleSetHelpProjectsCmds(String help) {
_helpProjectCmds = help;
}
// Set callback of sketch function to process project messages
void ESPHelper::consoleSetCallBackProjectCmds(void (*callback)()) {
_consoleCallbackProjectCmds = callback;
}
// Set bootime received as a string from HA
void ESPHelper::setBoottime(const char * boottime) {
strcpy(_boottime, boottime);
}
// overrides the write call to print to the telnet connection
size_t ESPHelper::write(uint8_t character) {
if (!_verboseMessages)
return 0;
//static uint32_t elapsed = 0;
// If start of a new line, initiate a new string buffer with time counter as a prefix
if (_newLine) {
unsigned long upt = millis();
sprintf(bufferPrint,
"(%s%02d:%02d:%02d%s) ",
COLOR_CYAN,
(uint8_t)((upt / (1000 * 60 * 60)) % 24),
(uint8_t)((upt / (1000 * 60)) % 60),
(uint8_t)((upt / 1000) % 60),
COLOR_RESET);
_newLine = false;
}
// Print ?
bool doPrint = false;
// New line ?
if (character == '\n') {
_newLine = true;
doPrint = true;
} else if (strlen(bufferPrint) == BUFFER_PRINT - 1) { // Limit of buffer
doPrint = true;
}
// Add character to telnet buffer
uint16_t len = strlen(bufferPrint);
bufferPrint[len] = character;
if (_newLine) {
// add additional \r for windows
bufferPrint[++len] = '\r';
}
// terminate string
bufferPrint[++len] = '\0';
// Send the characters buffered by print.h
if (doPrint) {
if (_telnetConnected) {
telnetClient.print(bufferPrint);
}
// echo to Serial if enabled
#ifdef USE_SERIAL
Serial.print(bufferPrint);
#endif
#ifdef USE_SERIAL1
Serial1.print(bufferPrint);
#endif
// Empty the buffer
bufferPrint[0] = '\0';
}
return len + 1;
}
// Show help of commands
void ESPHelper::consoleShowHelp() {
// Show the initial message
String help = "";
help.concat("*\n\r* Remote Debug for ESP8266/ESP32\n\r");
help.concat("* Device hostname: ");
#if defined(ESP8266)
help.concat(WiFi.hostname());
#else
help.concat(WiFi.getHostname());
#endif
help.concat(", IP: ");
help.concat(WiFi.localIP().toString());
help.concat(", MAC address: ");
help.concat(WiFi.macAddress());
help.concat("\n\r* Connected to WiFi AP: ");
help.concat(WiFi.SSID());
#if defined(ESP32)
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
sprintf(s,
"\n* ESP32 chip with %d CPU cores, WiFi%s%s, silicon revision %d\n",
chip_info.cores,
(chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "",
chip_info.revision);
help.concat(s);
#endif
help.concat("\n\r* Boot time: ");
help.concat(_boottime);
help.concat("\n\r* Free Heap RAM: ");
help.concat(ESP.getFreeHeap());
help.concat(" bytes\n\r");
help.concat("*\n\r* Commands:\n\r* ?=help, *=quit, $=memory, !=reboot, "
"&=toggle verbose messages");
if (_helpProjectCmds != "" && (_consoleCallbackProjectCmds)) {
help.concat("\n\r* ");
help.concat(_helpProjectCmds);
}
help.concat("\n\r");
telnetClient.print(help);
#ifdef USE_SERIAL
Serial.print(help);
#endif
#ifdef USE_SERIAL1
Serial1.print(help);
#endif
}
// reset / restart
void ESPHelper::resetESP() {
telnetClient.println("* Reboot ESP...");
telnetClient.flush();
telnetClient.stop();
// end();
// Reset
ESP.restart();
// ESP.reset(); // for ESP8266 only
}
// Get last command received
char * ESPHelper::consoleGetLastCommand() {
return _command;
}
// Process user command over telnet
void ESPHelper::consoleProcessCommand() {
// Set time of last command received
_lastTimeCommand = millis();
uint8_t cmd = _command[0];
if (!_verboseMessages) {
telnetClient.println(
"Warning, verbose messaging is off. Use v to toggle.");
}
// Process the command
if (cmd == '?') {
consoleShowHelp(); // Show help
} else if (cmd == '*') { // quit
telnetClient.println("* Closing telnet connection...");
telnetClient.stop();
} else if (cmd == '$') {
telnetClient.print("* Free Heap RAM (bytes): ");
telnetClient.println(ESP.getFreeHeap());
} else if (cmd == '!') {
resetESP();
} else if (cmd == '&') {
_verboseMessages = !_verboseMessages; // toggle
telnetClient.printf("Verbose messaging is %s\n",
_verboseMessages ? "on" : "off");
} else {
// custom Project commands
if (_consoleCallbackProjectCmds) {
_consoleCallbackProjectCmds();
}
}
}
// Logger
// LOG_CONSOLE sends to the Telnet session
// LOG_HA sends to Telnet session plus a MQTT for Home Assistant
// LOG_NONE turns off all logging
void ESPHelper::logger(log_level_t level, const char * message) {
// do we log to the telnet window?
if ((level == LOG_CONSOLE) && (telnetClient && telnetClient.connected())) {
telnetClient.println(message);
telnetClient.flush();
} else if (level == LOG_HA) {
char s[100];
sprintf(s,
"%s: %s\n",
_hostname,
message); // add new line, for the debug telnet printer
publish(MQTT_NOTIFICATION, s, false);
}
// print to Serial if set in platform.io (requires recompile)
#ifdef USE_SERIAL
Serial.println(message);
#endif
#ifdef USE_SERIAL1
Serial.println(message);
#endif
}
// send specific command to HA via MQTT
// format is: home/<hostname>/command/<cmd>
void ESPHelper::sendHACommand(const char * cmd) {
logger(LOG_CONSOLE, "Sending command to HA...");
char s[100];
sprintf(s, "%s%s/%s", MQTT_BASE, _hostname, MQTT_TOPIC_COMMAND);
publish(s, cmd, false);
}
// send specific start command to HA via MQTT, which returns the boottime
// format is: home/<hostname>/start
void ESPHelper::sendStart() {
logger(LOG_CONSOLE, "Sending Start command to HA...");
char s[100];
sprintf(s, "%s%s/%s", MQTT_BASE, _hostname, MQTT_TOPIC_START);
// send initial payload of "start" to kick things off
publish(s, MQTT_TOPIC_START, false);
}

226
lib/ESPHelper/ESPHelper.h Normal file
View File

@@ -0,0 +1,226 @@
/*
ESPHelper.h
Copyright (c) 2017 ItKindaWorks Inc All right reserved.
github.com/ItKindaWorks
This file is part of ESPHelper
ESPHelper is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ESPHelper is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ESPHelper. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __ESP_HELPER_H
#define __ESP_HELPER_H
#include <ArduinoOTA.h>
#include <Print.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h> //https://github.com/esp8266/Arduino
#include <ESP8266mDNS.h>
#elif defined(ESP32)
#include "esp_system.h"
#include <ESPmDNS.h>
#include <WiFi.h>
#else
#error Only for ESP8266 or ESP32
#endif
// MQTT stuff
#define DEFAULT_QOS 1 //at least once - devices are guarantee to get a message.
#define MQTT_BASE "home/"
#define MQTT_NOTIFICATION MQTT_BASE "notification"
#define MQTT_TOPIC_COMMAND "command"
#define MQTT_TOPIC_START "start"
#define MAX_SUBSCRIPTIONS 25 // max # of subscriptions
#define MAX_TIME_INACTIVE 600000 // Max time for inactivity (ms) - 10 mins
#define TELNET_PORT 23 // telnet port
#define BUFFER_PRINT 500 // length of telnet buffer (default was 150)
#define COMMAND_LENGTH 20 // length of a command
// ANSI Colors
#define COLOR_RESET "\x1B[0m"
#define COLOR_BLACK "\x1B[0;30m"
#define COLOR_RED "\x1B[0;31m"
#define COLOR_GREEN "\x1B[0;32m"
#define COLOR_YELLOW "\x1B[0;33m"
#define COLOR_BLUE "\x1B[0;34m"
#define COLOR_MAGENTA "\x1B[0;35m"
#define COLOR_CYAN "\x1B[0;36m"
#define COLOR_WHITE "\x1B[0;37m"
// Logger
typedef enum { LOG_NONE, LOG_CONSOLE, LOG_HA } log_level_t;
enum connStatus { NO_CONNECTION, BROADCAST, WIFI_ONLY, FULL_CONNECTION };
struct netInfo {
const char * mqttHost;
const char * mqttUser;
const char * mqttPass;
uint16_t mqttPort;
const char * ssid;
const char * pass;
};
typedef struct netInfo netInfo;
struct subscription {
bool isUsed = false;
const char * topic;
};
typedef struct subscription subscription;
// class ESPHelper {
class ESPHelper : public Print {
public:
void consoleSetHelpProjectsCmds(String help);
void consoleSetCallBackProjectCmds(void (*callback)());
char * consoleGetLastCommand();
void resetESP();
void logger(log_level_t level, const char * message);
virtual size_t write(uint8_t);
ESPHelper(netInfo * startingNet);
bool begin(const char * hostname);
void end();
void useSecureClient(const char * fingerprint);
int loop();
bool subscribe(const char * topic, uint8_t qos);
bool addSubscription(const char * topic);
bool removeSubscription(const char * topic);
bool unsubscribe(const char * topic);
bool addHASubscription(const char * topic);
void publish(const char * topic, const char * payload);
void publish(const char * topic, const char * payload, bool retain);
bool setCallback(MQTT_CALLBACK_SIGNATURE);
void setMQTTCallback(MQTT_CALLBACK_SIGNATURE);
void setWifiCallback(void (*callback)());
void sendHACommand(const char * s);
void sendStart();
void reconnect();
void updateNetwork();
const char * getSSID();
void setSSID(const char * ssid);
const char * getPASS();
void setPASS(const char * pass);
const char * getMQTTIP();
void setMQTTIP(const char * mqttIP);
void setMQTTIP(const char * mqttIP, const char * mqttUser, const char * mqttPass);
uint8_t getMQTTQOS();
void setMQTTQOS(uint8_t qos);
String getIP();
IPAddress getIPAddress();
uint8_t getStatus();
void setNetInfo(netInfo newNetwork);
void setNetInfo(netInfo * newNetwork);
netInfo * getNetInfo();
void setHopping(bool canHop);
void listSubscriptions();
void OTA_enable();
void OTA_disable();
void OTA_begin();
void setBoottime(const char * boottime);
void consoleHandle();
private:
netInfo _currentNet;
PubSubClient client;
WiFiClient wifiClient;
WiFiClientSecure wifiClientSecure;
const char * _fingerprint;
bool _useSecureClient = false;
char _clientName[40];
void (*_wifiCallback)();
bool _wifiCallbackSet = false;
#ifdef ESP8266
std::function<void(char *, uint8_t *, uint8_t)> _mqttCallback;
#endif
#ifdef ESP32
void (*_mqttCallback)(char *, uint8_t *, uint8_t);
#endif
bool _mqttCallbackSet = false;
uint8_t _connectionStatus = NO_CONNECTION;
uint8_t _netCount = 0;
uint8_t _currentIndex = 0;
bool _ssidSet = false;
bool _passSet = false;
bool _mqttSet = false;
bool _mqttUserSet = false;
bool _mqttPassSet = false;
bool _useOTA = false;
bool _OTArunning = false;
bool _hoppingAllowed = false;
bool _hasBegun = false;
netInfo ** _netList;
bool _verboseMessages = true;
subscription _subscriptions[MAX_SUBSCRIPTIONS];
char _hostname[64];
uint8_t _qos = DEFAULT_QOS;
IPAddress _apIP = IPAddress(192, 168, 1, 254);
void changeNetwork();
String macToStr(const uint8_t * mac);
bool checkParams();
void resubscribe();
uint8_t setConnectionStatus();
char _boottime[20];
// console/telnet specific
WiFiClient telnetClient;
bool _telnetConnected = false; // Client is connected ?
bool _newLine = true; // New line write ?
char _command[COMMAND_LENGTH]; // Command received, includes options seperated by a space
uint32_t _lastTimeCommand = millis(); // Last time command received
String _helpProjectCmds = ""; // Help of commands setted by project
void (*_consoleCallbackProjectCmds)(); // Callable for projects commands
void consoleShowHelp();
void consoleProcessCommand();
bool isCRLF(char character);
char bufferPrint[BUFFER_PRINT];
};
#endif

13
src/LICENSE.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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