first commit
39
.clang-format
Normal 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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
[](CHANGELOG.md)
|
||||
[](https://github.org/xoseperez/espurna/tree/dev/)
|
||||
[](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:
|
||||
|
||||

|
||||
|
||||
If you hit 'q' and Enter, it will toggle verbose logging and you will see more details:
|
||||
|
||||

|
||||
|
||||
To see the current values of the Boiler type 's' and hit Enter:
|
||||
|
||||

|
||||
|
||||
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
|
||||
--- | ---
|
||||
 | 
|
||||
|
||||
The schematic from Juergen which this is based off is:
|
||||
|
||||

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

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
|
||||
# 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
|
After Width: | Height: | Size: 26 KiB |
BIN
doc/schematics/breadboard.JPG
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
doc/schematics/circuit.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
2673
doc/schematics/ems_full.diy
Normal file
1818
doc/schematics/ems_readonly.diy
Normal file
BIN
doc/schematics/readonly.JPG
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
doc/schematics/readwrite.JPG
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
doc/telnet/telnet_example.JPG
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
doc/telnet/telnet_stats.JPG
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
doc/telnet/telnet_verbose.JPG
Normal file
|
After Width: | Height: | Size: 214 KiB |
12
extra_script.py
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||