version 1.2.0. See ChangeLog

This commit is contained in:
proddy
2019-01-02 00:14:36 +01:00
parent aaa8505402
commit e14c2c658c
21 changed files with 3057 additions and 2009 deletions

39
.clang-format Normal file
View File

@@ -0,0 +1,39 @@
Language: Cpp
BasedOnStyle: LLVM
UseTab: Never
IndentWidth: 4
ColumnLimit: 140
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

1
.gitignore vendored
View File

@@ -6,4 +6,3 @@
platformio.ini
lib/readme.txt
.travis.yml
.clang-format

View File

@@ -5,11 +5,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.2.0] 2019-01-01
### Fixed
- Incorrect indenting in `climate.yaml` (thanks @mrfixit1)
- Improved support for slower WiFi connections
- Fixed issue with OTA not always giving back a completion response to platformio
- Fixed issue with repeating reads after a raw mode send
- Fixed handling of long integers (thanks @SpaceTeddy)
### Added
- Setting the mode and setpoint temperature on a RC35
- added 'dout' flashmode to platformio.ini so OTA works now when uploading to a Wemos D1 Pro's or any other board with larger flash's
- added un tested supporting RC35 type of thermostats
- Try and discover and set Boiler and Thermostat types automatically
- Fetch UBATotalUptimeMessage from Boiler to get total working minutes
- Added check to see if bus is connected. Shown in stats page
- If no Wifi connection can be made, start up as a WiFi Access Point (AP)
- Report out service codes and water-flow [pull-request](https://github.com/proddy/EMS-ESP-Boiler/pull/20/files). Thanks @Bonusbartus
### Changed
- Build option is called `DEBUG_SUPPORT` (was `USE_SERIAL`)
- Replaced old **ESPHelper** with my own **MyESP** library to handle Wifi, MQTT, MDNS and Telnet handlers. Supports asynchronous TCP and has smaller memory footprint. And moved to libs directory.
- Simplified LED error checking. If enabled (by default), solid means connected and flashing means error. Uses either an external pull-up or the onboard ESP8266 LED.
- Improved Telnet debugging which uses TelnetSpy to keep a buffer of previous output
- Optimized memory usage & heap conflicts, removing nasty things like strcpy, sprintf where possible
- Improved checking for tap water on/off (thanks @Bonusbartus)
### Removed
- Time and TimeLib's. Not used in code.
- Removed build option `MQTT_MAX_PACKAGE_SIZE` as not using the PubSubClient library any more
- Removed all of Espurna's pre-built firmwares and instructions to build. Keeping it simple.
## [1.1.1] 2018-12-23
@@ -21,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed handling of negative flaoting point values (like outdoor temp)
- Fixed handling of negative floating point values (like outdoor temp)
- Fixed handling of auto & manual mode on an RC30
- [Fixed condition where all telegram types were processed, instead of only broadcasts or our own reads](https://github.com/proddy/EMS-ESP-Boiler/issues/15)

View File

@@ -4,7 +4,7 @@ EMS-ESP-Boiler is a project to build a controller circuit running with an ESP826
There are 3 parts to this project, first the design of the circuit, second the code for the ESP8266 microcontroller firmware and lastly an example configuration for Home Assistant to monitor the data and issue direct commands via MQTT.
[![version](https://img.shields.io/badge/version-1.1.0-brightgreen.svg)](CHANGELOG.md)
[![version](https://img.shields.io/badge/version-1.1.2-brightgreen.svg)](CHANGELOG.md)
- [EMS-ESP-Boiler](#ems-esp-boiler)
- [Introduction](#introduction)
@@ -33,6 +33,7 @@ There are 3 parts to this project, first the design of the circuit, second the c
- [Building The Firmware](#building-the-firmware)
- [Using PlatformIO Standalone](#using-platformio-standalone)
- [Building Using Arduino IDE](#building-using-arduino-ide)
- [Troubleshooting](#troubleshooting)
- [Known Issues](#known-issues)
- [Wish List](#wish-list)
- [Your Comments and Feedback](#your-comments-and-feedback)
@@ -208,9 +209,9 @@ The code is built on the Arduino framework and is dependent on these external li
`ems.cpp` is the logic to read the EMS packets (telegrams), validates them and process them based on the type.
`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 and light up the LEDs. LED support is enabled by setting the -DUSE_LED build flag.
`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 and light up the LEDs. LED support is enabled by default and can be switched off at compile time using the -DNO_LED build flag.
`ESPHelper.cpp` is my customized version of [ESPHelper](https://github.com/ItKindaWorks/ESPHelper) with added Telnet support and some other minor tweaking.
`MyESP.cpp` is my custom library to handle WiFi, MQTT, MDNS and Telnet. Uses a modified version of TelnetSpy (https://github.com/yasheena/telnetspy)
### Supported EMS Types
@@ -227,11 +228,20 @@ The code is built on the Arduino framework and is dependent on these external li
| Thermostat | 0x02 | Version | reads Version major/minor |
| Thermostat | 0x91, 0x41, 0x0A | Status Message | read monitor values |
In `boiler.ino` you can make calls to automatically send these read commands. See the function *regularUpdates()*
In `boiler.ino` you can make calls to automatically request these types in the function *regularUpdates()*.
### Supported Thermostats
Modify `EMS_ID_THERMOSTAT` in `myconfig.h` to the thermostat type you want to support.
I am still working on adding more support to known thermostats.
Currently known types and collected versions:
Moduline 300 = Type 77 Version 03.03
Moduline 400 = Type 78 Version 03.03
Buderus RC35 = Type 86 Version 01.15
Nefit Easy = Type 202 Version 02.19
Nefit Trendline HRC30 = Type 123 Version 06.01
BC10 = Type 123 Version 04.05
#### RC20 (Moduline 300)
@@ -245,8 +255,6 @@ Type's 3F, 49, 53, 5D are identical. So are 4B, 55, 5F and mostly zero's. Types
#### RC35
***not implemented yet***!
An RC35 thermostat can support up to 4 heating circuits each controlled with their own Monitor and Working Mode IDs.
Fetching the thermostats setpoint temp us by requesting 0x3E and looking at the 3rd byte in the data telegram (data[2]) and dividing by 2.
@@ -259,8 +267,7 @@ There is limited support for an Nefit Easy TC100/TC200 type thermostat. The curr
### Customizing The Code
- To configure for your thermostat and specific boiler settings, modify `my_config.h`. Here you can
- set the thermostat type. The default ID is 0x17 for an RC30 Moduline 300.
- set flags for enabled/disabling functionality such as `BOILER_THERMOSTAT_ENABLED`, `BOILER_SHOWER_ENABLED` and `BOILER_SHOWER_TIMER`.
- set flags for enabled/disabling functionality such as `BOILER_SHOWER_ENABLED` and `BOILER_SHOWER_TIMER`.
- Set WIFI and MQTT settings, instead of doing this in `platformio.ini`
- To add new handlers for EMS data types, first create a callback function and add to the `EMS_Types` array at the top of the file `ems.cpp` and modify `ems.h`
@@ -327,7 +334,7 @@ PlatformIO is my preferred way. The code uses a modified version [ESPHelper](htt
% cd EMS-ESP-Boiler
% cp platformio.ini-example platformio.ini
```
- edit `platformio.ini` to set `env_default` and the flags `WIFI_SSID WIFI_PASSWORD, MQTT_IP, MQTT_USER, MQTT_PASS`. If you're not using MQTT leave MQTT_IP empty (`MQTT_IP=""`)
- edit `platformio.ini` to set `env_default` and the flags like `WIFI_SSID WIFI_PASSWORD, MQTT_IP, MQTT_USER, MQTT_PASS`. If you're not using MQTT leave MQTT_IP empty (`MQTT_IP=""`)
```c
% platformio run -t upload
```
@@ -339,7 +346,7 @@ Porting to the Arduino IDE can be a little tricky but it is possible.
- Add the ESP8266 boards (from Preferences add Additional Board URL `http://arduino.esp8266.com/stable/package_esp8266com_index.json`)
- Go to Boards Manager and install ESP8266 2.4.x platform
- Select your ESP8266 from Tools->Boards and the correct port with Tools->Port
- From the Library Manager install the needed libraries from platformio.ini such as ArduinoJson 5.13.x, PubSubClient 2.6.x, CRC32 and Time
- From the Library Manager install the needed libraries from platformio.ini
- The Arduino IDE doesn't have a common way to set build flags (ugh!) so you'll need to un-comment these lines in `boiler.ino`:
```c
@@ -353,6 +360,12 @@ Porting to the Arduino IDE can be a little tricky but it is possible.
- Put all the files in a single sketch folder (`ESPHelper.*, boiler.ino, ems.*, emsuart.*`)
- cross your fingers and hit CTRL-R to compile...
## Troubleshooting
If the WiFi, MQTT, MDNS or something else fails to connect, re-build the firmware using the `-DDEBUG_SUPPORT` option, connect the ESP8266 to a USB in your computer and monitor the Serial output. A lot of detailed logging will be printed to help you pinpoint the cause of the error.
The onboard LED will flash if there is no connection with the EMS bus. You can disable LED support by adding -DNO_LED to the build options.
## Known Issues
Some annoying issues that need fixing:
@@ -364,7 +377,6 @@ Some annoying issues that need fixing:
- Measure amount of gas in m3 per day for the hot water vs the central heating, and convert this into cost in Home Assistant
- Support changing temps on an Nefit Easy. To do this you must send XMPP messages directly to the thermostat. See this project: https://github.com/robertklep/nefit-easy-core
- Store custom params like wifi credentials, mqtt, thermostat type on ESP8266 using SPIFFS
- Automatic detection of thermostat type
- Add support for a temperature sensor on the circuit (DS18B20)
## Your Comments and Feedback

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -42,6 +42,12 @@
unit_of_measurement: '°C'
value_template: '{{ value_json.wWSelTemp }}'
- platform: mqtt
state_topic: 'home/boiler/boiler_data'
name: 'Warm Water tapwater flow rate'
unit_of_measurement: 'l/min'
value_template: '{{ value_json.wWCurFlow }}'
- platform: mqtt
state_topic: 'home/boiler/boiler_data'
name: 'Warm Water current temperature'

View File

@@ -14,6 +14,7 @@ views:
- sensor.warm_water_current_temperature
- sensor.warm_water_activated
- sensor.warm_water_3way_valve
- sensor.warm_water_tapwater_flow_rate
- type: divider
- sensor.boiler_temperature
- sensor.return_temperature

View File

@@ -1,12 +1,28 @@
#!/usr/bin/env python
from subprocess import call
import os
Import("env")
def code_check(source, target, env):
print("\n** Starting cppcheck...")
call(["cppcheck", os.getcwd()+"/.", "--force", "--enable=all"])
print("\n** Finished cppcheck...\n")
print("\n** Starting cpplint...")
call(["cpplint", "--extensions=ino,cpp,h", "--filter=-legal/copyright,-build/include,-whitespace",
"--linelength=120", "--recursive", "src"])
print("\n** Finished cpplint...")
#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'])
# built in targets: (buildprog, size, upload, program, buildfs, uploadfs, uploadfsota)
env.AddPreAction("buildprog", code_check)
# env.AddPostAction(.....)
# see http://docs.platformio.org/en/latest/projectconf/advanced_scripting.html#before-pre-and-after-post-actions
# env.Replace(PROGNAME="firmware_%s" % defines.get("VERSION"))
# env.Replace(PROGNAME="firmware_%s" % env['BOARD'])

626
lib/TelnetSpy/TelnetSpy.cpp Normal file
View File

@@ -0,0 +1,626 @@
/*
* TELNET SERVER FOR ESP8266 / ESP32
* Cloning the serial port via Telnet.
*
* Written by Wolfgang Mattis (arduino@yasheena.de).
* Version 1.1 / September 7, 2018.
* MIT license, all text above must be included in any redistribution.
*/
#ifdef ESP8266
extern "C" {
#include "user_interface.h"
}
#endif
#include "TelnetSpy.h"
#ifndef min
#define min(a, b) ((a) < (b) ? (a) : (b))
#endif
#ifndef max
#define max(a, b) ((a) > (b) ? (a) : (b))
#endif
static TelnetSpy * actualObject = NULL;
static void TelnetSpy_putc(char c) {
if (actualObject) {
actualObject->write(c);
}
}
static void TelnetSpy_ignore_putc(char c) {
;
}
TelnetSpy::TelnetSpy() {
port = TELNETSPY_PORT;
telnetServer = NULL;
started = false;
listening = false;
firstMainLoop = true;
usedSer = &Serial;
storeOffline = true;
connected = false;
callbackConnect = NULL;
callbackDisconnect = NULL;
welcomeMsg = strdup(TELNETSPY_WELCOME_MSG);
rejectMsg = strdup(TELNETSPY_REJECT_MSG);
minBlockSize = TELNETSPY_MIN_BLOCK_SIZE;
collectingTime = TELNETSPY_COLLECTING_TIME;
maxBlockSize = TELNETSPY_MAX_BLOCK_SIZE;
pingTime = TELNETSPY_PING_TIME;
pingRef = 0xFFFFFFFF;
waitRef = 0xFFFFFFFF;
telnetBuf = NULL;
bufLen = 0;
uint16_t size = TELNETSPY_BUFFER_LEN;
while (!setBufferSize(size)) {
size = size >> 1;
if (size < minBlockSize) {
setBufferSize(minBlockSize);
break;
}
}
debugOutput = TELNETSPY_CAPTURE_OS_PRINT;
if (debugOutput) {
setDebugOutput(true);
}
}
TelnetSpy::~TelnetSpy() {
end();
}
// added by proddy
void TelnetSpy::disconnectClient() {
if (client.connected()) {
client.flush();
client.stop();
}
if (connected && (callbackDisconnect != NULL)) {
callbackDisconnect();
}
connected = false;
}
void TelnetSpy::setPort(uint16_t portToUse) {
port = portToUse;
if (listening) {
if (client.connected()) {
client.flush();
client.stop();
}
if (connected && (callbackDisconnect != NULL)) {
callbackDisconnect();
}
connected = false;
telnetServer->close();
delete telnetServer;
telnetServer = new WiFiServer(port);
if (started) {
telnetServer->begin();
telnetServer->setNoDelay(bufLen > 0);
}
}
}
void TelnetSpy::setWelcomeMsg(char * msg) {
if (welcomeMsg) {
free(welcomeMsg);
}
welcomeMsg = strdup(msg);
}
void TelnetSpy::setRejectMsg(char * msg) {
if (rejectMsg) {
free(rejectMsg);
}
rejectMsg = strdup(msg);
}
void TelnetSpy::setMinBlockSize(uint16_t minSize) {
minBlockSize = min(max((uint16_t)1, minSize), maxBlockSize);
}
void TelnetSpy::setCollectingTime(uint16_t colTime) {
collectingTime = colTime;
}
void TelnetSpy::setMaxBlockSize(uint16_t maxSize) {
maxBlockSize = max(maxSize, minBlockSize);
}
bool TelnetSpy::setBufferSize(uint16_t newSize) {
if (telnetBuf && (bufLen == newSize)) {
return true;
}
if (newSize == 0) {
bufLen = 0;
if (telnetBuf) {
free(telnetBuf);
telnetBuf = NULL;
}
if (telnetServer) {
telnetServer->setNoDelay(false);
}
return true;
}
newSize = max(newSize, minBlockSize);
uint16_t oldBufLen = bufLen;
bufLen = newSize;
uint16_t tmp;
if (!telnetBuf || (bufUsed == 0)) {
bufRdIdx = 0;
bufWrIdx = 0;
bufUsed = 0;
} else {
if (bufLen < oldBufLen) {
if (bufRdIdx < bufWrIdx) {
if (bufWrIdx > bufLen) {
tmp = min(bufLen, (uint16_t)(bufWrIdx - max(bufLen, bufRdIdx)));
memcpy(telnetBuf, &telnetBuf[bufWrIdx - tmp], tmp);
bufWrIdx = tmp;
if (bufWrIdx > bufRdIdx) {
bufRdIdx = bufWrIdx;
} else {
if (bufRdIdx > bufLen) {
bufRdIdx = 0;
}
}
if (bufRdIdx == bufWrIdx) {
bufUsed = bufLen;
} else {
bufUsed = bufWrIdx - bufRdIdx;
}
}
} else {
if (bufWrIdx > bufLen) {
memcpy(telnetBuf, &telnetBuf[bufWrIdx - bufLen], bufLen);
bufRdIdx = 0;
bufWrIdx = 0;
bufUsed = bufLen;
} else {
tmp = min(bufLen - bufWrIdx, oldBufLen - bufRdIdx);
memcpy(&telnetBuf[bufLen - tmp], &telnetBuf[oldBufLen - tmp], tmp);
bufRdIdx = bufLen - tmp;
bufUsed = bufWrIdx + tmp;
}
}
}
}
char * temp = (char *)realloc(telnetBuf, bufLen);
if (!temp) {
return false;
}
telnetBuf = temp;
if (telnetBuf && (bufLen > oldBufLen) && (bufRdIdx > bufWrIdx)) {
tmp = bufLen - (oldBufLen - bufRdIdx);
memcpy(&telnetBuf[tmp], &telnetBuf[bufRdIdx], oldBufLen - bufRdIdx);
bufRdIdx = tmp;
}
if (telnetServer) {
telnetServer->setNoDelay(true);
}
return true;
}
uint16_t TelnetSpy::getBufferSize() {
if (!telnetBuf) {
return 0;
}
return bufLen;
}
void TelnetSpy::setStoreOffline(bool store) {
storeOffline = store;
}
bool TelnetSpy::getStoreOffline() {
return storeOffline;
}
void TelnetSpy::setPingTime(uint16_t pngTime) {
pingTime = pngTime;
if (pingTime == 0) {
pingRef = 0xFFFFFFFF;
} else {
pingRef = (millis() & 0x7FFFFFF) + pingTime;
}
}
void TelnetSpy::setSerial(HardwareSerial * usedSerial) {
usedSer = usedSerial;
}
size_t TelnetSpy::write(uint8_t data) {
if (telnetBuf) {
if (storeOffline || client.connected()) {
if (bufUsed == bufLen) {
if (client.connected()) {
sendBlock();
}
if (bufUsed == bufLen) {
char c;
while (bufUsed > 0) {
c = pullTelnetBuf();
if (c == '\n') {
addTelnetBuf('\r');
break;
}
}
if (peekTelnetBuf() == '\r') {
pullTelnetBuf();
}
}
}
addTelnetBuf(data);
/*
if (data == '\n') {
addTelnetBuf('\r'); // added by proddy, fix for Windows
}
*/
}
} else {
if (client.connected()) {
client.write(data);
}
}
if (usedSer) {
return usedSer->write(data);
}
return 1;
}
int TelnetSpy::available(void) {
if (usedSer) {
int avail = usedSer->available();
if (avail > 0) {
return avail;
}
}
if (client.connected()) {
return telnetAvailable();
}
return 0;
}
int TelnetSpy::read(void) {
int val;
if (usedSer) {
val = usedSer->read();
if (val != -1) {
return val;
}
}
if (client.connected()) {
if (telnetAvailable()) {
val = client.read();
}
}
return val;
}
int TelnetSpy::peek(void) {
int val;
if (usedSer) {
val = usedSer->peek();
if (val != -1) {
return val;
}
}
if (client.connected()) {
if (telnetAvailable()) {
val = client.peek();
}
}
return val;
}
void TelnetSpy::flush(void) {
if (usedSer) {
usedSer->flush();
}
}
#ifdef ESP8266
void TelnetSpy::begin(unsigned long baud, SerialConfig config, SerialMode mode, uint8_t tx_pin) {
if (usedSer) {
usedSer->begin(baud, config, mode, tx_pin);
}
started = true;
}
#else // ESP32
void TelnetSpy::begin(unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin, bool invert) {
if (usedSer) {
usedSer->begin(baud, config, rxPin, txPin, invert);
}
started = true;
}
#endif
void TelnetSpy::end() {
if (debugOutput) {
setDebugOutput(false);
}
if (usedSer) {
usedSer->end();
}
if (client.connected()) {
client.flush();
client.stop();
}
if (connected && (callbackDisconnect != NULL)) {
callbackDisconnect();
}
connected = false;
telnetServer->close();
delete telnetServer;
telnetServer = NULL;
listening = false;
started = false;
}
#ifdef ESP8266
void TelnetSpy::swap(uint8_t tx_pin) {
if (usedSer) {
usedSer->swap(tx_pin);
}
}
void TelnetSpy::set_tx(uint8_t tx_pin) {
if (usedSer) {
usedSer->set_tx(tx_pin);
}
}
void TelnetSpy::pins(uint8_t tx, uint8_t rx) {
if (usedSer) {
usedSer->pins(tx, rx);
}
}
bool TelnetSpy::isTxEnabled(void) {
if (usedSer) {
return usedSer->isTxEnabled();
}
return true;
}
bool TelnetSpy::isRxEnabled(void) {
if (usedSer) {
return usedSer->isRxEnabled();
}
return true;
}
#endif
int TelnetSpy::availableForWrite(void) {
if (usedSer) {
return min(usedSer->availableForWrite(), bufLen - bufUsed);
}
return bufLen - bufUsed;
}
TelnetSpy::operator bool() const {
if (usedSer) {
return (bool)*usedSer;
}
return true;
}
void TelnetSpy::setDebugOutput(bool en) {
debugOutput = en;
if (debugOutput) {
actualObject = this;
#ifdef ESP8266
os_install_putc1((void *)TelnetSpy_putc); // Set system printing (os_printf) to TelnetSpy
system_set_os_print(true);
#else // ESP32 \
// ToDo: How can be done this for ESP32 ?
#endif
} else {
if (actualObject == this) {
#ifdef ESP8266
system_set_os_print(false);
os_install_putc1((void *)TelnetSpy_ignore_putc); // Ignore system printing
#else // ESP32 \
// ToDo: How can be done this for ESP32 ?
#endif
actualObject = NULL;
}
}
}
uint32_t TelnetSpy::baudRate(void) {
if (usedSer) {
return usedSer->baudRate();
}
return 115200;
}
void TelnetSpy::sendBlock() {
uint16_t len = bufUsed;
if (len > maxBlockSize) {
len = maxBlockSize;
}
len = min(len, (uint16_t)(bufLen - bufRdIdx));
client.write(&telnetBuf[bufRdIdx], len);
bufRdIdx += len;
if (bufRdIdx >= bufLen) {
bufRdIdx = 0;
}
bufUsed -= len;
if (bufUsed == 0) {
bufRdIdx = 0;
bufWrIdx = 0;
}
waitRef = 0xFFFFFFFF;
if (pingRef != 0xFFFFFFFF) {
pingRef = (millis() & 0x7FFFFFF) + pingTime;
if (pingRef > 0x7FFFFFFF) {
pingRef -= 0x80000000;
}
}
}
void TelnetSpy::addTelnetBuf(char c) {
telnetBuf[bufWrIdx] = c;
if (bufUsed == bufLen) {
bufRdIdx++;
if (bufRdIdx >= bufLen) {
bufRdIdx = 0;
}
} else {
bufUsed++;
}
bufWrIdx++;
if (bufWrIdx >= bufLen) {
bufWrIdx = 0;
}
}
char TelnetSpy::pullTelnetBuf() {
if (bufUsed == 0) {
return 0;
}
char c = telnetBuf[bufRdIdx++];
if (bufRdIdx >= bufLen) {
bufRdIdx = 0;
}
bufUsed--;
return c;
}
char TelnetSpy::peekTelnetBuf() {
if (bufUsed == 0) {
return 0;
}
return telnetBuf[bufRdIdx];
}
int TelnetSpy::telnetAvailable() {
int n = client.available();
while (n > 0) {
if (0xff == client.peek()) { // If esc char for telnet NVT protocol data remove that telegram:
client.read(); // Remove esc char
n--;
if (0xff == client.peek()) { // If esc sequence for 0xFF data byte...
return n; // ...return info about available data (just this 0xFF data byte)
}
client.read(); // Skip the rest of the telegram of the telnet NVT protocol data
client.read();
n--;
n--;
} else { // If next char is a normal data byte...
return n; // ...return info about available data
}
}
return 0;
}
bool TelnetSpy::isClientConnected() {
return connected;
}
void TelnetSpy::setCallbackOnConnect(telnetSpyCallback callback) {
callbackConnect = callback;
}
void TelnetSpy::setCallbackOnDisconnect(telnetSpyCallback callback) {
callbackDisconnect = callback;
}
void TelnetSpy::handle() {
if (firstMainLoop) {
firstMainLoop = false;
// Between setup() and loop() the configuration for os_print may be changed so it must be renewed
if (debugOutput && (actualObject == this)) {
setDebugOutput(true);
}
}
if (!started) {
return;
}
if (!listening) {
if (WiFi.status() != WL_CONNECTED) {
return;
}
telnetServer = new WiFiServer(port);
telnetServer->begin();
telnetServer->setNoDelay(bufLen > 0);
listening = true;
if (usedSer) {
usedSer->println("[TELNET] Telnet server started"); // added by Proddy
}
}
if (telnetServer->hasClient()) {
if (client.connected()) {
WiFiClient rejectClient = telnetServer->available();
if (strlen(rejectMsg) > 0) {
rejectClient.write((const uint8_t *)rejectMsg, strlen(rejectMsg));
}
rejectClient.flush();
rejectClient.stop();
} else {
client = telnetServer->available();
if (strlen(welcomeMsg) > 0) {
client.write((const uint8_t *)welcomeMsg, strlen(welcomeMsg));
}
}
}
if (client.connected()) {
if (!connected) {
connected = true;
if (pingTime != 0) {
pingRef = (millis() & 0x7FFFFFF) + pingTime;
}
if (callbackConnect != NULL) {
callbackConnect();
}
}
} else {
if (connected) {
connected = false;
client.flush();
client.stop();
pingRef = 0xFFFFFFFF;
waitRef = 0xFFFFFFFF;
if (callbackDisconnect != NULL) {
callbackDisconnect();
}
}
}
if (client.connected() && (bufUsed > 0)) {
if (bufUsed >= minBlockSize) {
sendBlock();
} else {
unsigned long m = millis() & 0x7FFFFFF;
if (waitRef == 0xFFFFFFFF) {
waitRef = m + collectingTime;
if (waitRef > 0x7FFFFFFF) {
waitRef -= 0x80000000;
}
} else {
if (!((waitRef < 0x20000000) && (m > 0x60000000)) && (m >= waitRef)) {
sendBlock();
}
}
}
}
if (client.connected() && (pingRef != 0xFFFFFFFF)) {
unsigned long m = millis() & 0x7FFFFFF;
if (!((pingRef < 0x20000000) && (m > 0x60000000)) && (m >= pingRef)) {
addTelnetBuf(0);
sendBlock();
}
}
}

278
lib/TelnetSpy/TelnetSpy.h Normal file
View File

@@ -0,0 +1,278 @@
/*
* TELNET SERVER FOR ESP8266 / ESP32
* Cloning the serial port via Telnet.
*
* Written by Wolfgang Mattis (arduino@yasheena.de).
* Version 1.1 / September 7, 2018.
* MIT license, all text above must be included in any redistribution.
*/
/*
* DESCRIPTION
*
* This module allows you "Debugging over the air". So if you already use
* ArduinoOTA this is a helpful extension for wireless development. Use
* "TelnetSpy" instead of "Serial" to send data to the serial port and a copy
* to a telnet connection. There is a circular buffer which allows to store the
* data while the telnet connection is not established. So its possible to
* collect data even when the Wifi and Telnet connections are still not
* established. Its also possible to create a telnet session only if it is
* neccessary: then you will get the already collected data as far as it is
* still stored in the circular buffer. Data send from telnet terminal to
* ESP8266 / ESP32 will be handled as data received by serial port. It is also
* possible to use more than one instance of TelnetSpy, for example to send
* control information on the first instance and data dumps on the second
* instance.
*
* USAGE
*
* Add the following line to your sketch:
* #include <TelnetSpy.h>
* TelnetSpy LOG;
*
* Add the following line to your initialisation block ( void setup() ):
* LOG.begin();
*
* Add the following line at the beginning of your main loop ( void loop() ):
* LOG.handle();
*
* Use the following functions of the TelnetSpy object to modify behavior
*
* Change the port number of this telnet server. If a client is already
* connected it will be disconnected.
* Default: 23
* void setPort(uint16_t portToUse);
*
* Change the message which will be send to the telnet client after a session
* is established.
* Default: "Connection established via TelnetSpy.\n"
* void setWelcomeMsg(char* msg);
*
* Change the message which will be send to the telnet client if another
* session is already established.
* Default: "TelnetSpy: Only one connection possible.\n"
* void setRejectMsg(char* msg);
*
* Change the amount of characters to collect before sending a telnet block.
* Default: 64
* void setMinBlockSize(uint16_t minSize);
*
* Change the time (in ms) to wait before sending a telnet block if its size is
* less than <minSize> (defined by setMinBlockSize).
* Default: 100
* void setCollectingTime(uint16_t colTime);
*
* Change the maximum size of the telnet packets to send.
* Default: 512
* void setMaxBlockSize(uint16_t maxSize);
*
* Change the size of the ring buffer. Set it to 0 to disable buffering.
* Changing size tries to preserve the already collected data. If the new
* buffer size is too small the youngest data will be preserved only. Returns
* false if the requested buffer size cannot be set.
* Default: 3000
* bool setBufferSize(uint16_t newSize);
*
* This function returns the actual size of the ring buffer.
* uint16_t getBufferSize();
*
* Enable / disable storing new data in the ring buffer if no telnet connection
* is established. This function allows you to store important data only. You
* can do this by disabling "storeOffline" for sending less important data.
* Default: true
* void setStoreOffline(bool store);
*
* Get actual state of storing data when offline.
* bool getStoreOffline();
*
* If no data is sent via TelnetSpy the detection of a disconnected client has
* a long timeout. Use setPingTime to define the time (in ms) without traffic
* after which a ping (chr(0)) is sent to the telnet client to detect a
* disconnect earlier. Use 0 as parameter to disable pings.
* Default: 1500
* void setPingTime(uint16_t pngTime);
*
* Set the serial port you want to use with this object (especially for ESP32)
* or NULL if no serial port should be used (telnet only).
* Default: Serial
* void setSerial(HardwareSerial* usedSerial);
*
* This function returns true, if a telnet client is connected.
* bool isClientConnected();
*
* This function installs a callback function which will be called on every
* telnet connect of this object (except rejected connect tries). Use NULL to
* remove the callback.
* Default: NULL
* void setCallbackOnConnect(void (*callback)());
*
* This function installs a callback function which will be called on every
* telnet disconnect of this object (except rejected connect tries). Use NULL
* to remove the callback.
* Default: NULL
* void setCallbackOnDisconnect(void (*callback)());
*
* HINT
*
* Add the following lines to your sketch:
* #define SERIAL TelnetSpy
* //#define SERIAL Serial
*
* Replace "Serial" with "SERIAL" in your sketch. Now you can switch between
* serial only and serial with telnet by changing the comments of the defines
* only.
*
* IMPORTANT
*
* To connect to the telnet server you have to:
* - establish the Wifi connection
* - execute "TelnetSpy.begin(WhatEverYouWant);"
*
* The order is not important.
*
* All you do with "Serial" you can also do with "TelnetSpy", but remember:
* Transfering data also via telnet will need more performance than the serial
* port only. So time critical things may be influenced.
*
* It is not possible to establish more than one telnet connection at the same
* time. But its possible to use more than one instance of TelnetSpy.
*
* If you have problems with low memory you may reduce the value of the define
* TELNETSPY_BUFFER_LEN for a smaller ring buffer on initialisation.
*
* Usage of void setDebugOutput(bool) to enable / disable of capturing of
* os_print calls when you have more than one TelnetSpy instance: That
* TelnetSpy object will handle this functionality where you used
* setDebugOutput at last. On default TelnetSpy has the capturing of OS_print
* calls enabled. So if you have more instances the last created instance will
* handle the capturing.
*/
#ifndef TelnetSpy_h
#define TelnetSpy_h
#define TELNETSPY_BUFFER_LEN 3000
#define TELNETSPY_MIN_BLOCK_SIZE 64
#define TELNETSPY_COLLECTING_TIME 100
#define TELNETSPY_MAX_BLOCK_SIZE 512
#define TELNETSPY_PING_TIME 1500
#define TELNETSPY_PORT 23
#define TELNETSPY_CAPTURE_OS_PRINT true
#define TELNETSPY_WELCOME_MSG "Connection established via TelnetSpy2.\n"
#define TELNETSPY_REJECT_MSG "TelnetSpy: Only one connection possible.\n"
#ifdef ESP8266
#include <ESP8266WiFi.h>
#else // ESP32
#include <WiFi.h>
#endif
#include <WiFiClient.h>
class TelnetSpy : public Stream {
public:
TelnetSpy();
~TelnetSpy();
void handle(void);
void setPort(uint16_t portToUse);
void setWelcomeMsg(char * msg);
void setRejectMsg(char * msg);
void setMinBlockSize(uint16_t minSize);
void setCollectingTime(uint16_t colTime);
void setMaxBlockSize(uint16_t maxSize);
bool setBufferSize(uint16_t newSize);
uint16_t getBufferSize();
void setStoreOffline(bool store);
bool getStoreOffline();
void setPingTime(uint16_t pngTime);
void setSerial(HardwareSerial * usedSerial);
bool isClientConnected();
void disconnectClient(); // added by Proddy
typedef std::function<void()> telnetSpyCallback; // added by Proddy
void setCallbackOnConnect(telnetSpyCallback callback); // changed by proddy
void setCallbackOnDisconnect(telnetSpyCallback callback); // changed by proddy
// Functions offered by HardwareSerial class:
#ifdef ESP8266
void begin(unsigned long baud) {
begin(baud, SERIAL_8N1, SERIAL_FULL, 1);
}
void begin(unsigned long baud, SerialConfig config) {
begin(baud, config, SERIAL_FULL, 1);
}
void begin(unsigned long baud, SerialConfig config, SerialMode mode) {
begin(baud, config, mode, 1);
}
void begin(unsigned long baud, SerialConfig config, SerialMode mode, uint8_t tx_pin);
#else // ESP32
void begin(unsigned long baud, uint32_t config = SERIAL_8N1, int8_t rxPin = -1, int8_t txPin = -1, bool invert = false);
#endif
void end();
#ifdef ESP8266
void swap() {
swap(1);
}
void swap(uint8_t tx_pin);
void set_tx(uint8_t tx_pin);
void pins(uint8_t tx, uint8_t rx);
bool isTxEnabled(void);
bool isRxEnabled(void);
#endif
int available(void) override;
int peek(void) override;
int read(void) override;
int availableForWrite(void);
void flush(void) override;
size_t write(uint8_t) override;
inline size_t write(unsigned long n) {
return write((uint8_t)n);
}
inline size_t write(long n) {
return write((uint8_t)n);
}
inline size_t write(unsigned int n) {
return write((uint8_t)n);
}
inline size_t write(int n) {
return write((uint8_t)n);
}
using Print::write;
operator bool() const;
void setDebugOutput(bool);
uint32_t baudRate(void);
protected:
void sendBlock(void);
void addTelnetBuf(char c);
char pullTelnetBuf();
char peekTelnetBuf();
int telnetAvailable();
WiFiServer * telnetServer;
WiFiClient client;
uint16_t port;
HardwareSerial * usedSer;
bool storeOffline;
bool started;
bool listening;
bool firstMainLoop;
unsigned long waitRef;
unsigned long pingRef;
uint16_t pingTime;
char * welcomeMsg;
char * rejectMsg;
uint16_t minBlockSize;
uint16_t collectingTime;
uint16_t maxBlockSize;
bool debugOutput;
char * telnetBuf;
uint16_t bufLen;
uint16_t bufUsed;
uint16_t bufRdIdx;
uint16_t bufWrIdx;
bool connected;
telnetSpyCallback callbackConnect; // added by proddy
telnetSpyCallback callbackDisconnect; // added by proddy
};
#endif

617
lib/myESP/MyESP.cpp Normal file
View File

@@ -0,0 +1,617 @@
/*
* MyESP - my ESP helper class to handle Wifi, MDNS, MQTT and Telnet
*
* Paul Derbyshire - December 2018
*
* Some ideas from https://github.com/JoaoLopesF/ESP8266-RemoteDebug-Telnet
* Ideas from Espurna https://github.com/xoseperez/espurna
*/
#include "MyESP.h"
// constructor
MyESP::MyESP() {
_app_hostname = strdup("MyESP");
_app_name = strdup("MyESP");
_app_version = strdup("1.0.0");
_boottime = strdup("unknown");
_extern_WIFICallback = NULL;
_extern_WIFICallbackSet = false;
_consoleCallbackProjectCmds = NULL;
_helpProjectCmds = NULL;
_helpProjectCmds_count = 0;
_mqtt_host = NULL;
_mqtt_password = NULL;
_mqtt_username = NULL;
_wifi_password = NULL;
_wifi_ssid = NULL;
_mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN;
_verboseMessages = true;
_command = (char *)malloc(TELNET_MAX_COMMAND_LENGTH); // reserve buffer for Serial/Telnet commands
}
MyESP::~MyESP() {
end();
}
// end
void MyESP::end() {
free(_command);
SerialAndTelnet.end();
jw.disconnect();
}
// general debug to the telnet or serial channels
void MyESP::myDebug(const char * format, ...) {
va_list args;
va_start(args, format);
char test[1];
int len = ets_vsnprintf(test, 1, format, args) + 1;
char * buffer = new char[len];
ets_vsnprintf(buffer, len, format, args);
va_end(args);
SerialAndTelnet.println(buffer);
delete[] buffer;
}
// for flashmemory. Must use PSTR()
void MyESP::myDebug_P(PGM_P format_P, ...) {
char format[strlen_P(format_P) + 1];
memcpy_P(format, format_P, sizeof(format));
va_list args;
va_start(args, format_P);
char test[1];
int len = ets_vsnprintf(test, 1, format, args) + 1;
char * buffer = new char[len];
ets_vsnprintf(buffer, len, format, args);
va_end(args);
SerialAndTelnet.println(buffer);
delete[] buffer;
}
// called when WiFi is connected, and used to start MDNS
void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) {
if ((code == MESSAGE_CONNECTED) || (code == MESSAGE_ACCESSPOINT_CREATED)) {
#if defined(ARDUINO_ARCH_ESP32)
String hostname = String(WiFi.getHostname());
#else
String hostname = WiFi.hostname();
#endif
myDebug_P(PSTR("[WIFI] ----------------------------------------------"));
myDebug_P(PSTR("[WIFI] SSID %s"), WiFi.SSID().c_str());
myDebug_P(PSTR("[WIFI] CH %d"), WiFi.channel());
myDebug_P(PSTR("[WIFI] RSSI %d"), WiFi.RSSI());
myDebug_P(PSTR("[WIFI] IP %s"), WiFi.localIP().toString().c_str());
myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.macAddress().c_str());
myDebug_P(PSTR("[WIFI] GW %s"), WiFi.gatewayIP().toString().c_str());
myDebug_P(PSTR("[WIFI] MASK %s"), WiFi.subnetMask().toString().c_str());
myDebug_P(PSTR("[WIFI] DNS %s"), WiFi.dnsIP().toString().c_str());
myDebug_P(PSTR("[WIFI] HOST %s"), hostname.c_str());
myDebug_P(PSTR("[WIFI] ----------------------------------------------"));
if (WiFi.getMode() & WIFI_AP) {
myDebug_P(PSTR("[WIFI] MODE AP --------------------------------------"));
myDebug_P(PSTR("[WIFI] SSID %s"), jw.getAPSSID().c_str());
myDebug_P(PSTR("[WIFI] IP %s"), WiFi.softAPIP().toString().c_str());
myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.softAPmacAddress().c_str());
myDebug_P(PSTR("[WIFI] ----------------------------------------------"));
}
// start MDNS
if (MDNS.begin((char *)hostname.c_str())) {
myDebug_P(PSTR("[MDNS] OK"));
} else {
myDebug_P(PSTR("[MDNS] FAIL"));
}
// call any final custom settings
if (_extern_WIFICallbackSet) {
myDebug_P(PSTR("[WIFI] calling custom wifi settings function"));
_extern_WIFICallback(); // call callback to set any custom things
}
}
if (code == MESSAGE_CONNECTING) {
myDebug_P(PSTR("[WIFI] Connecting to %s"), parameter);
}
if (code == MESSAGE_CONNECT_FAILED) {
myDebug_P(PSTR("[WIFI] Could not connect to %s"), parameter);
}
if (code == MESSAGE_DISCONNECTED) {
myDebug_P(PSTR("[WIFI] Disconnected"));
}
}
// received MQTT message
void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) {
if (len == 0)
return;
char message[len + 1];
strlcpy(message, (char *)payload, len + 1);
// myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message);
// check for our default ones
if ((strcmp(topic, MQTT_HA) == 0) && (strcmp(message, MQTT_TOPIC_START_PAYLOAD) == 0)) {
myDebug_P(PSTR("[MQTT] HA rebooted - restarting device"));
resetESP();
return;
}
char s[100];
snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, MQTT_TOPIC_START);
if (strcmp(topic, s) == 0) {
myDebug_P(PSTR("[MQTT] boottime: %s"), message);
setBoottime(message);
return;
}
// Send message event to custom service
(_mqtt_callback)(MQTT_MESSAGE_EVENT, topic, message);
}
// MQTT subscribe
void MyESP::mqttSubscribe(const char * topic) {
if (mqttClient.connected() && (strlen(topic) > 0)) {
char s[100];
snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, topic);
unsigned int packetId = mqttClient.subscribe(s, MQTT_QOS);
myDebug_P(PSTR("[MQTT] Subscribing to %s (PID %d)"), s, packetId);
}
}
// MQTT unsubscribe
void MyESP::mqttUnsubscribe(const char * topic) {
if (mqttClient.connected() && (strlen(topic) > 0)) {
char s[100];
snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, topic);
unsigned int packetId = mqttClient.unsubscribe(s);
myDebug_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)"), s, packetId);
}
}
// MQTT Publish
void MyESP::mqttPublish(const char * topic, const char * payload) {
char s[500];
snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, topic);
// myDebug_P(PSTR("[MQTT] Sending pubish to %s with payload %s"), s, payload);
mqttClient.publish(s, MQTT_QOS, false, payload);
}
// MQTT onConnect - when a connect is established automatically subscribe to my HA topics
void MyESP::_mqttOnConnect() {
myDebug_P(PSTR("[MQTT] Connected"));
_mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN;
#ifndef NO_HA
// standard subscribes for HA (Home Assistant)
mqttClient.subscribe(MQTT_HA, MQTT_QOS); // to "ha"
mqttSubscribe(MQTT_TOPIC_START); // to home/<hostname>/start
// send specific start command to HA via MQTT, which returns the boottime
char s[48];
snprintf(s, sizeof(s), "%s%s/%s", MQTT_BASE, _app_hostname, MQTT_TOPIC_START);
mqttClient.publish(s, MQTT_QOS, false, MQTT_TOPIC_START_PAYLOAD);
#endif
// call custom
(_mqtt_callback)(MQTT_CONNECT_EVENT, NULL, NULL);
}
// MQTT setup
void MyESP::_mqtt_setup() {
if (!_mqtt_host) {
myDebug_P(PSTR("[MQTT] disabled"));
}
_mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN;
mqttClient.onConnect([this](bool sessionPresent) { _mqttOnConnect(); });
mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) {
if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {
myDebug_P(PSTR("[MQTT] TCP Disconnected"));
(_mqtt_callback)(MQTT_DISCONNECT_EVENT, NULL, NULL); // call callback with disconnect
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) {
myDebug_P(PSTR("[MQTT] Identifier Rejected"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) {
myDebug_P(PSTR("[MQTT] Server unavailable"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) {
myDebug_P(PSTR("[MQTT] Malformed credentials"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) {
myDebug_P(PSTR("[MQTT] Not authorized"));
}
});
//mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { myDebug_P(PSTR("[MQTT] Subscribe ACK for PID %d"), packetId); });
//mqttClient.onPublish([this](uint16_t packetId) { myDebug_P(PSTR("[MQTT] Publish ACK for PID %d"), packetId); });
mqttClient.onMessage(
[this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
_mqttOnMessage(topic, payload, len);
});
}
// WiFI setup
void MyESP::_wifi_setup() {
jw.setHostname(_app_hostname); // Set WIFI hostname (otherwise it would be ESP-XXXXXX)
jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); });
jw.enableAP(false);
jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
jw.setReconnectTimeout(WIFI_RECONNECT_INTERVAL);
jw.enableAPFallback(true); // AP mode only as fallback, but disabled
jw.enableSTA(true); // Enable STA mode (connecting to a router)
jw.enableScan(false); // Configure it to scan available networks and connect in order of dBm
jw.cleanNetworks(); // Clean existing network configuration
jw.addNetwork(_wifi_ssid, _wifi_password); // Add a network
}
// MDNS setup
void MyESP::_mdns_setup() {
MDNS.addService("telnet", "tcp", TELNETSPY_PORT);
// for OTA discovery
MDNS.addServiceTxt("arduino", "tcp", "app_name", (const char *)_app_name);
MDNS.addServiceTxt("arduino", "tcp", "app_version", (const char *)_app_version);
MDNS.addServiceTxt("arduino", "tcp", "mac", WiFi.macAddress());
{
char buffer[6] = {0};
itoa(ESP.getFlashChipRealSize() / 1024, buffer, 10);
MDNS.addServiceTxt("arduino", "tcp", "mem_size", (const char *)buffer);
}
{
char buffer[6] = {0};
itoa(ESP.getFlashChipSize() / 1024, buffer, 10);
MDNS.addServiceTxt("arduino", "tcp", "sdk_size", (const char *)buffer);
}
{
char buffer[6] = {0};
itoa(ESP.getFreeSketchSpace(), buffer, 10);
MDNS.addServiceTxt("arduino", "tcp", "free_space", (const char *)buffer);
}
}
// OTA Setup
void MyESP::_ota_setup() {
ArduinoOTA.setPort(OTA_PORT);
ArduinoOTA.setHostname(_app_hostname);
ArduinoOTA.onStart([this]() { myDebug_P(PSTR("[OTA] Start")); });
ArduinoOTA.onEnd([this]() { myDebug_P(PSTR("[OTA] Done, restarting...")); });
ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) {
static unsigned int _progOld;
unsigned int _prog = (progress / (total / 100));
if (_prog != _progOld) {
myDebug_P(PSTR("[OTA] Progress: %u%%\r"), _prog);
_progOld = _prog;
}
});
ArduinoOTA.onError([this](ota_error_t error) {
if (error == OTA_AUTH_ERROR)
myDebug_P(PSTR("Auth Failed"));
else if (error == OTA_BEGIN_ERROR)
myDebug_P(PSTR("Begin Failed"));
else if (error == OTA_CONNECT_ERROR)
myDebug_P(PSTR("Connect Failed"));
else if (error == OTA_RECEIVE_ERROR)
myDebug_P(PSTR("Receive Failed"));
else if (error == OTA_END_ERROR)
myDebug_P(PSTR("End Failed"));
});
ArduinoOTA.begin();
}
// sets boottime
void MyESP::setBoottime(char * boottime) {
if (_boottime) {
free(_boottime);
}
_boottime = strdup(boottime);
}
// returns boottime
char * MyESP::getBoottime() {
return _boottime;
}
// Set callback of sketch function to process project messages
void MyESP::consoleSetCallBackProjectCmds(command_t * cmds, uint8_t count, void (*callback)()) {
_helpProjectCmds = cmds; // command list
_helpProjectCmds_count = count; // number of commands
_consoleCallbackProjectCmds = callback; // external function to handle commands
}
void MyESP::_telnetConnected() {
myDebug_P(PSTR("[TELNET] Telnet connection established"));
_consoleShowHelp(); // Show the initial message
}
void MyESP::_telnetDisconnected() {
myDebug_P(PSTR("[TELNET] Telnet connection closed"));
}
// Initialize the telnet server
void MyESP::_telnet_setup() {
SerialAndTelnet.setWelcomeMsg("");
SerialAndTelnet.setCallbackOnConnect([this]() { _telnetConnected(); });
SerialAndTelnet.setCallbackOnDisconnect([this]() { _telnetDisconnected(); });
SerialAndTelnet.begin(115200);
SerialAndTelnet.setDebugOutput(false);
#ifndef DEBUG_SUPPORT
SerialAndTelnet.setSerial(NULL);
#endif
// init command buffer for console commands
memset(_command, 0, TELNET_MAX_COMMAND_LENGTH);
}
// Show help of commands
void MyESP::_consoleShowHelp() {
String help = "\n\r**********************************************\n\r* Remote Telnet Command Center & Log Monitor "
"*\n\r**********************************************\n\r";
help += "* Device hostname: " + WiFi.hostname() + "\tIP: " + WiFi.localIP().toString() + "\tMAC address: " + WiFi.macAddress() + "\n\r";
help += "* Connected to WiFi AP: " + WiFi.SSID() + "\n\r";
help += "* Boot time: ";
help.concat(_boottime);
help += "\n\r* ";
help.concat(_app_name);
help += " Version ";
help.concat(_app_version);
help += "\n\r* Free RAM: ";
help.concat(ESP.getFreeHeap());
help += " bytes\n\r";
#ifdef DEBUG_SUPPORT
help += "* !! in DEBUG_SUPPORT mode !!\n\r";
#endif
help += "*\n\r* Commands:\n\r* ?=this help, CTRL-D=quit, $=show free memory, !=reboot ESP, &=suspend all messages\n\r";
// print custom commands if available
if (_consoleCallbackProjectCmds) {
for (uint8_t i = 0; i < _helpProjectCmds_count; i++) {
help += FPSTR("* ");
help += FPSTR(_helpProjectCmds[i].key);
for (int j = 0; j < (8 - strlen(_helpProjectCmds[i].key)); j++) { // padding
help += FPSTR(" ");
}
help += FPSTR(_helpProjectCmds[i].description);
help += FPSTR("\n\r");
}
}
SerialAndTelnet.println(help.c_str());
}
// reset / restart
void MyESP::resetESP() {
myDebug_P(PSTR("* Reboot ESP..."));
end();
#if defined(ARDUINO_ARCH_ESP32)
ESP.reset(); // for ESP8266 only
#else
ESP.restart();
#endif
}
// Get last command received
char * MyESP::consoleGetLastCommand() {
return _command;
}
// Process user command over telnet
void MyESP::consoleProcessCommand() {
uint8_t cmd = _command[0];
// Process the command
if (cmd == '?') {
_consoleShowHelp(); // Show help
} else if (cmd == '$') {
myDebug("* Free RAM (bytes): %d", ESP.getFreeHeap());
} else if (cmd == '!') {
resetESP();
} else if (cmd == '&') {
_verboseMessages = !_verboseMessages; // toggle
myDebug("Suspend all messages is %s", _verboseMessages ? "disabled" : "enabled");
} else {
// custom Project commands
if (_consoleCallbackProjectCmds) {
_consoleCallbackProjectCmds();
}
}
if (!_verboseMessages) {
myDebug("Warning, all log messages have been supsended. Use & to re-enable.");
}
}
// sends a MQTT notification message to Home Assistant
void MyESP::sendHANotification(const char * message) {
char payload[48];
snprintf(payload, sizeof(payload), "%s : %s", _app_hostname, message);
myDebug_P(PSTR("[MQTT] Sending HA notification %s"), payload);
mqttClient.publish(MQTT_NOTIFICATION, MQTT_QOS, false, payload);
}
// send specific command to HA via MQTT
// format is: home/<hostname>/command with payload <cmd>
void MyESP::sendHACommand(const char * cmd) {
myDebug_P(PSTR("[MQTT] Sending HA command %s"), cmd);
char topic[48];
snprintf(topic, sizeof(topic), "%s%s/%s", MQTT_BASE, _app_hostname, MQTT_TOPIC_COMMAND);
mqttClient.publish(topic, MQTT_QOS, false, cmd);
}
// handler for Telnet
void MyESP::_telnetHandle() {
SerialAndTelnet.handle();
char last = ' '; // To avoid processing double "\r\n"
while (SerialAndTelnet.available()) {
char character = SerialAndTelnet.read(); // Get character
// check for ctrl-D
if ((character == 0xEC) || (character == 0x04)) {
SerialAndTelnet.disconnectClient();
}
// if we reached our buffer limit, send what we have
if (strlen(_command) >= TELNET_MAX_COMMAND_LENGTH) {
consoleProcessCommand(); // Process the command
memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); // reset for next command
}
// Check for newline (CR or LF)
if (_isCRLF(character) == true) {
if (_isCRLF(last) == false) {
if (strlen(_command) > 0) {
consoleProcessCommand(); // Process the command
}
}
memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); // reset for next command
} else if (isPrintable(character)) {
// Concat char to end of buffer
uint16_t len = strlen(_command);
_command[len] = character;
_command[len + 1] = '\0';
}
last = character; // remember last char
}
}
// sets a custom function to run when wifi is started
void MyESP::setWIFICallback(void (*callback)()) {
_extern_WIFICallback = callback;
_extern_WIFICallbackSet = true;
}
// the mqtt callback for after connecting to subscribe and process incoming messages
void MyESP::setMQTTCallback(mqtt_callback_f callback) {
_mqtt_callback = callback;
}
// Is CR or LF ?
bool MyESP::_isCRLF(char character) {
return (character == '\r' || character == '\n');
}
// ensure we have a connection to MQTT broker
void MyESP::_mqttConnect() {
if (!_mqtt_host || mqttClient.connected() || (WiFi.status() != WL_CONNECTED)) {
return;
}
// Check reconnect interval
static unsigned long last = 0;
if (millis() - last < _mqtt_reconnect_delay)
return;
last = millis();
// Increase the reconnect delay
_mqtt_reconnect_delay += MQTT_RECONNECT_DELAY_STEP;
if (_mqtt_reconnect_delay > MQTT_RECONNECT_DELAY_MAX) {
_mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX;
}
mqttClient.setServer(_mqtt_host, MQTT_PORT);
mqttClient.setClientId(_app_hostname);
if (_mqtt_username && _mqtt_password) {
myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s & its password"), _mqtt_username);
mqttClient.setCredentials(_mqtt_username, _mqtt_password);
} else {
myDebug_P(PSTR("[MQTT] Connecting to MQTT..."));
}
// Connect to the MQTT broker
mqttClient.connect();
}
// Setup everything we need
void MyESP::setup(char * app_hostname,
char * app_name,
char * app_version,
char * wifi_ssid,
char * wifi_password,
char * mqtt_host,
char * mqtt_username,
char * mqtt_password) {
// get general params first
_app_hostname = strdup(app_hostname);
_app_name = strdup(app_name);
_app_version = strdup(app_version);
// Check SSID too long or missing
if (!wifi_ssid || *wifi_ssid == 0x00 || strlen(wifi_ssid) > 31) {
_wifi_ssid = NULL;
} else {
_wifi_ssid = strdup(wifi_ssid);
}
// Check PASS too long
if (wifi_password && strlen(wifi_password) > 63) {
_wifi_password = NULL;
} else {
_wifi_password = strdup(wifi_password);
}
// can be empty
if (!mqtt_host || *mqtt_host == 0x00) {
_mqtt_host = NULL;
} else {
_mqtt_host = strdup(mqtt_host);
}
// mqtt username and password can be empty
if (!mqtt_username || *mqtt_username == 0x00) {
_mqtt_username = NULL;
} else {
_mqtt_username = strdup(mqtt_username);
}
// can be empty
if (!mqtt_password || *mqtt_password == 0x00) {
_mqtt_password = NULL;
} else {
_mqtt_password = strdup(mqtt_password);
}
// call setup of the services...
_telnet_setup(); // Telnet setup
_wifi_setup(); // WIFI setup
_mqtt_setup(); // MQTT Setup
_mdns_setup(); // MDNS setup
_ota_setup(); // OTA setup
}
/*
* Loop. This is called as often as possible and it handles wifi, telnet, mqtt etc
*/
void MyESP::loop() {
jw.loop(); // WiFi
_telnetHandle(); // Telnet/Debugger
ArduinoOTA.handle(); // OTA
_mqttConnect(); // MQTT
yield(); // ...and breath
}
MyESP myESP;

169
lib/myESP/MyESP.h Normal file
View File

@@ -0,0 +1,169 @@
/*
* MyEsp.h
*
* Paul Derbyshire - December 2018
*/
#pragma once
#ifndef MyEMS_h
#define MyEMS_h
#include <ArduinoJson.h>
#include <ArduinoOTA.h>
#include <AsyncMqttClient.h> // https://github.com/marvinroger/async-mqtt-client
#include <DNSServer.h>
#include <ESPAsyncTCP.h> // https://github.com/me-no-dev/ESPAsyncTCP
#include <JustWifi.h> // https://github.com/xoseperez/justwifi
#include <TelnetSpy.h> // modified from https://github.com/yasheena/telnetspy
#if defined(ARDUINO_ARCH_ESP32)
#include <ESPmDNS.h>
#else
#include <ESP8266mDNS.h>
#endif
// WIFI
#define WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms
#define WIFI_RECONNECT_INTERVAL 60000 // If could not connect to WIFI, retry after this time in ms
// OTA
#define OTA_PORT 8266 // OTA port
// MQTT
#define MQTT_BASE "home/"
#define MQTT_NOTIFICATION MQTT_BASE "notification"
#define MQTT_TOPIC_COMMAND "command"
#define MQTT_TOPIC_START "start"
#define MQTT_TOPIC_START_PAYLOAD "start"
#define MQTT_HA MQTT_BASE "ha"
#define MQTT_PORT 1883 // MQTT port
#define MQTT_QOS 1
#define MQTT_RECONNECT_DELAY_MIN 5000 // Try to reconnect in 5 seconds upon disconnection
#define MQTT_RECONNECT_DELAY_STEP 5000 // Increase the reconnect delay in 5 seconds after each failed attempt
#define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most
// Internal MQTT events
#define MQTT_CONNECT_EVENT 0
#define MQTT_DISCONNECT_EVENT 1
#define MQTT_MESSAGE_EVENT 2
// Telnet
#define TELNET_MAX_COMMAND_LENGTH 80 // length of a command
#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"
typedef struct {
char key[10];
char description[400];
} command_t;
typedef std::function<void(unsigned int, const char *, const char *)> mqtt_callback_f;
// calculates size of an 2d array at compile time
template <typename T, size_t N>
constexpr size_t ArraySize(T (&)[N]) {
return N;
}
// class definition
class MyESP {
public:
MyESP();
~MyESP();
// wifi
void setWIFICallback(void (*callback)());
void setMQTTCallback(mqtt_callback_f callback);
// ha
void sendHACommand(const char * cmd);
void sendHANotification(const char * message);
// mqtt
void mqttSubscribe(const char * topic);
void mqttUnsubscribe(const char * topic);
void mqttPublish(const char * topic, const char * payload);
// debug & telnet
void myDebug(const char * format, ...);
void myDebug_P(PGM_P format_P, ...);
void consoleSetCallBackProjectCmds(command_t * cmds, uint8_t count, void (*callback)());
char * consoleGetLastCommand();
void consoleProcessCommand();
void end();
void loop();
void setup(char * app_hostname,
char * app_name,
char * app_version,
char * wifi_ssid,
char * wifi_password,
char * mqtt_host,
char * mqtt_username,
char * mqtt_password);
char * getBoottime();
void setBoottime(char * boottime);
void resetESP();
private:
// mqtt
AsyncMqttClient mqttClient;
unsigned long _mqtt_reconnect_delay;
void _mqttOnMessage(char * topic, char * payload, size_t len);
void _mqttConnect();
void _mqtt_setup();
mqtt_callback_f _mqtt_callback;
void _mqttOnConnect();
void _sendStart();
char * _mqtt_host;
char * _mqtt_username;
char * _mqtt_password;
char * _boottime;
// wifi
DNSServer dnsServer; // For Access Point (AP) support
void _wifiCallback(justwifi_messages_t code, char * parameter);
void _wifi_setup();
void (*_extern_WIFICallback)();
bool _extern_WIFICallbackSet;
char * _wifi_ssid;
char * _wifi_password;
// mdns
void _mdns_setup();
// ota
void _ota_setup();
// telnet & debug
TelnetSpy SerialAndTelnet;
void _telnetConnected();
void _telnetDisconnected();
void _telnetHandle();
void _telnet_setup();
char * _command; // the input command from either Serial or Telnet
command_t * _helpProjectCmds; // Help of commands setted by project
uint8_t _helpProjectCmds_count; // # available commands
void _consoleShowHelp();
void (*_consoleCallbackProjectCmds)(); // Callable for projects commands
void _consoleProcessCommand();
bool _isCRLF(char character);
bool _verboseMessages;
// general
char * _app_hostname;
char * _app_name;
char * _app_version;
};
extern MyESP myESP;
#endif

View File

@@ -1,19 +1,20 @@
[platformio]
; change this for your ESP8266 device
env_default = nodemcuv2
; env_default = d1_mini
; env_default = nodemcuv2
env_default = d1_mini
[common]
platform = espressif8266
; optional flags are -DUSE_LED -DSHOWER_TEST -DUSE_SERIAL
build_flags = -g -w -DMQTT_MAX_PACKET_SIZE=400
flash_mode = dout
; optional flags are -DNO_LED -DDEBUG_SUPPORT
build_flags = -g -w
build_flags_custom = '-DWIFI_SSID="my_ssid"' '-DWIFI_PASSWORD="my_password"' '-DMQTT_IP="my_broker_ip"' '-DMQTT_USER="my_broker_username"' '-DMQTT_PASS="my_broker_password"'
lib_deps =
Time
PubSubClient
ArduinoJson
CRC32
CircularBuffer
JustWifi
AsyncMqttClient
ArduinoJson
[env:nodemcuv2]
board = nodemcuv2
@@ -21,12 +22,13 @@ platform = ${common.platform}
framework = arduino
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags} ${common.build_flags_custom}
board_build.flash_mode = ${common.flash_mode}
upload_speed = 921600
monitor_speed = 115200
; comment out next line if using USB and not OTA
upload_port = "boiler."
upload_port = "boiler"
; examples....
;upload_port = "boiler"
;upload_port = "boiler."
;upload_port = "boiler.local"
;upload_port = 10.10.10.6
@@ -36,12 +38,13 @@ platform = ${common.platform}
framework = arduino
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags} ${common.build_flags_custom}
board_build.flash_mode = ${common.flash_mode}
upload_speed = 921600
monitor_speed = 115200
; comment out next line if using USB and not OTA
upload_port = "boiler."
upload_port = "boiler"
; examples....
;upload_port = "boiler"
;upload_port = "boiler."
;upload_port = "boiler.local"
;upload_port = 10.10.10.6

View File

@@ -1,895 +0,0 @@
/*
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, const char * app_name, const char * app_version) {
#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();
strcpy(_app_name, app_name); // app name
strcpy(_app_version, app_version); // app version
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;
}
//sets a custom function to run when telnet is started
void ESPHelper::setInitCallback(void (*callback)()) {
_initCallback = callback;
_initCallbackSet = 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 - SUCCESS\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
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();
_initCallback(); // call callback to set any custom things
// Empty buffer
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 processing double "\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 telnet session due to inactivity");
telnetClient.flush();
telnetClient.stop();
_telnetConnected = false;
}
}
}
// Set callback of sketch function to process project messages
void ESPHelper::consoleSetCallBackProjectCmds(command_t * cmds, uint8_t count, void (*callback)()) {
_helpProjectCmds = cmds; // command list
_helpProjectCmds_count = count; // number of commands
_consoleCallbackProjectCmds = callback; // external function to handle commands
}
// 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() {
String help = "**********************************************\n\r* Remote Telnet Command Center & Log Monitor "
"*\n\r**********************************************\n\r";
help += "* Device hostname: " + WiFi.hostname() + "\tIP: " + WiFi.localIP().toString()
+ "\tMAC address: " + WiFi.macAddress() + "\n\r";
help += "* Connected to WiFi AP: " + WiFi.SSID() + "\n\r";
help += "* Boot time: ";
help.concat(_boottime);
help += "\n\r* ";
help.concat(_app_name);
help += " Version ";
help.concat(_app_version);
help += "\n\r* Free RAM: ";
help.concat(ESP.getFreeHeap());
help += " bytes\n\r";
help += "*\n\r* Commands:\n\r* ?=this help, q=quit telnet, $=show free memory, !=reboot, &=suspend all "
"notifications\n\r";
char s[100];
// print custom commands if available
if (_consoleCallbackProjectCmds) {
for (uint8_t i = 0; i < _helpProjectCmds_count; i++) {
help += FPSTR("* ");
help += FPSTR(_helpProjectCmds[i].key);
for (int j = 0; j < (8 - strlen(_helpProjectCmds[i].key)); j++) { // padding
help += FPSTR(" ");
}
help += FPSTR(_helpProjectCmds[i].description);
help += FPSTR("\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, all messages are supsended. Use & to enable.");
}
// Process the command
if (cmd == '?') {
consoleShowHelp(); // Show help
} else if (cmd == 'q') { // quit
telnetClient.println("* Closing telnet connection...");
telnetClient.stop();
} else if (cmd == '$') {
telnetClient.print("* Free RAM (bytes): ");
telnetClient.println(ESP.getFreeHeap());
} else if (cmd == '!') {
resetESP();
} else if (cmd == '&') {
_verboseMessages = !_verboseMessages; // toggle
telnetClient.printf("Suspend all messages is %s\n\r", _verboseMessages ? "disabled" : "enabled");
} 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);
}

View File

@@ -1,219 +0,0 @@
/*
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/>.
*/
#pragma once
#include <ArduinoOTA.h>
#include <ESP8266WiFi.h> //https://github.com/esp8266/Arduino
#include <ESP8266mDNS.h>
#include <Print.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
#include <pgmspace.h>
// 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 MQTT_HA MQTT_BASE "ha"
#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 };
typedef struct {
const char * mqttHost;
const char * mqttUser;
const char * mqttPass;
uint16_t mqttPort;
const char * ssid;
const char * pass;
} netInfo;
typedef struct {
bool isUsed = false;
const char * topic;
} subscription;
typedef struct {
char key[10];
char description[400];
} command_t;
// class ESPHelper {
class ESPHelper : public Print {
public:
void consoleSetCallBackProjectCmds(command_t * cmds, uint8_t count, 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, const char * app_name, const char * app_version);
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 setInitCallback(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;
void (*_initCallback)();
bool _initCallbackSet = false;
std::function<void(char *, uint8_t *, uint8_t)> _mqttCallback;
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[24];
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[24];
char _app_name[24];
char _app_version[10];
// 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
command_t * _helpProjectCmds; // Help of commands setted by project
uint8_t _helpProjectCmds_count; // # available commands
void (*_consoleCallbackProjectCmds)(); // Callable for projects commands
void consoleShowHelp();
void consoleProcessCommand();
bool isCRLF(char character);
char bufferPrint[BUFFER_PRINT];
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

126
src/ems.h
View File

@@ -1,26 +1,28 @@
/*
* Header file for EMS.cpp
*
* You shouldn't need to change much in this file
*/
#pragma once
#include "emsuart.h"
#include "my_config.h" // include custom configuration settings
#include <Arduino.h>
#include <CircularBuffer.h> // https://github.com/rlogiacco/CircularBuffer
#include <MyESP.h>
// EMS IDs
#define EMS_ID_NONE 0x00 // Fixed - used as a dest in broadcast messages and empty type IDs
#define EMS_ID_BOILER 0x08 // Fixed - also known as MC10.
#define EMS_ID_ME 0x0B // Fixed - our device, hardcoded as "Service Key"
//#define EMS_ID_THERMOSTAT 0xFF // Fixed - to recognize a Thermostat
//#define EMS_ID_BOILER 0x08 // Fixed - also known as MC10.
#define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC
#define EMS_MAX_TELEGRAM_LENGTH 99 // max length of a telegram, including CRC
#define EMS_TX_MAXBUFFERSIZE 128 // max size of the buffer. packets are 32 bits
#define EMS_ID_THERMOSTAT_RC20 0x17 // RC20 (e.g. Moduline 300)
#define EMS_ID_THERMOSTAT_RC30 0x10 // RC30 (e.g. Moduline 400)
#define EMS_ID_THERMOSTAT_EASY 0x18 // TC100 (Nefit Easy)
// max length of a telegram, including CRC, for Rx and Tx.
// This can differs per firmware version and typically 32 is the max
#define EMS_MAX_TELEGRAM_LENGTH 99
// define here the EMS telegram types you need
@@ -43,6 +45,9 @@
#define EMS_OFFSET_UBAParameterWW_wwtemp 2 // WW Temperature
#define EMS_OFFSET_UBAParameterWW_wwactivated 1 // WW Activated
#define EMS_OFFSET_UBAParameterWW_wwComfort 9 // WW is in comfort or eco mode
#define EMS_VALUE_UBAParameterWW_wwComfort_Comfort 0x00 // the value for comfort
#define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco
/*
* Thermostat...
@@ -64,6 +69,12 @@
#define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode
#define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature
// RC35 specific - not implemented yet
#define EMS_TYPE_RC35StatusMessage 0x3E // is an automatic thermostat broadcast giving us temps
#define EMS_TYPE_RC35Set 0x3D // for setting values like temp and mode
#define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode
#define EMS_OFFSET_RC35Set_temp 2 // position of thermostat setpoint temperature
// Easy specific
#define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat
@@ -71,6 +82,7 @@
#define EMS_VALUE_INT_ON 1 // boolean true
#define EMS_VALUE_INT_OFF 0 // boolean false
#define EMS_VALUE_INT_NOTSET 0xFF // for 8-bit ints
#define EMS_VALUE_LONG_NOTSET 0xFFFFFF // for 3-byte longs
#define EMS_VALUE_FLOAT_NOTSET -255 // float unset
/* EMS UART transfer status */
@@ -116,6 +128,7 @@ typedef struct {
bool emsBoilerEnabled; // is the boiler online
_EMS_SYS_LOGGING emsLogging; // logging
bool emsRefreshed; // fresh data, needs to be pushed out to MQTT
bool emsBusConnected; // is there an active bus
} _EMS_Sys_Status;
// The Tx send package
@@ -130,11 +143,13 @@ typedef struct {
uint8_t comparisonValue; // value to compare against during a validate
uint8_t comparisonOffset; // offset of where the byte is we want to compare too later
uint8_t comparisonPostRead; // after a successful write call this to read
bool hasSent; // has been sent, just pending ack
bool forceRefresh; // should we send to MQTT after a successful Tx?
uint8_t data[EMS_TX_MAXBUFFERSIZE];
unsigned long timestamp; // when created
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
} _EMS_TxTelegram;
#define EMS_TX_TELEGRAM_QUEUE_MAX 20 // max size of Tx FIFO queue
// default empty Tx
const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = {
EMS_TX_TELEGRAM_INIT, // action
@@ -147,11 +162,42 @@ const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = {
0, // comparisonValue
0, // comparisonOffset
EMS_ID_NONE, // comparisonPostRead
false, // hasSent
false, // forceRefresh
0, // timestamp
{0x00} // data
};
// Known Buderus non-Thermostat types
typedef enum {
EMS_MODEL_NONE,
EMS_MODEL_ALL, // common for all devices
// service key
EMS_MODEL_SERVICEKEY, // this is us
// main buderus boiler type devices
EMS_MODEL_BK15,
EMS_MODEL_UBA,
EMS_MODEL_BC10,
EMS_MODEL_MM10,
EMS_MODEL_WM10,
// thermostats
EMS_MODEL_ES73,
EMS_MODEL_RC20,
EMS_MODEL_RC30,
EMS_MODEL_RC35,
EMS_MODEL_EASY
} _EMS_MODEL_ID;
typedef struct {
_EMS_MODEL_ID model_id;
uint8_t product_id;
uint8_t type_id;
char model_string[50];
} _Model_Type;
/*
* Telegram package defintions
*/
@@ -160,6 +206,7 @@ typedef struct { // UBAParameterWW
uint8_t wWSelTemp; // Warm Water selected temperature
uint8_t wWCircPump; // Warm Water circulation pump Available
uint8_t wWDesiredTemp; // Warm Water desired temperature
uint8_t wWComfort; // Warm water comfort or ECO mode
// UBAMonitorFast
uint8_t selFlowTemp; // Selected flow temperature
@@ -175,30 +222,56 @@ typedef struct { // UBAParameterWW
uint8_t curBurnPow; // Burner current power
float flameCurr; // Flame current in micro amps
float sysPress; // System pressure
uint8_t serviceCodeChar1; // First Character in status/service code
uint8_t serviceCodeChar2; // Second Character in status/service code
// 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
uint32_t burnStarts; // # burner restarts
uint32_t burnWorkMin; // Total burner operating time
uint32_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
uint8_t wWOneTime; // Warm Water one time function on/off
uint8_t wWCurFlow; // Warm Water current flow in l/min
// UBATotalUptimeMessage
uint32_t UBAuptime; // Total UBA working hours
// calculated values
uint8_t tapwaterActive; // Hot tap water is on/off
uint8_t heatingActive; // Central heating is on/off
// settings
char version[10];
uint8_t type_id;
_EMS_MODEL_ID model_id;
} _EMS_Boiler;
// Definition for thermostat type
typedef struct {
_EMS_MODEL_ID model_id;
bool read_supported;
bool write_supported;
} _Thermostat_Type;
#define EMS_THERMOSTAT_READ_YES true
#define EMS_THERMOSTAT_READ_NO false
#define EMS_THERMOSTAT_WRITE_YES true
#define EMS_THERMOSTAT_WRITE_NO false
// Thermostat data
typedef struct {
uint8_t type; // thermostat type (RC30, Easy etc)
uint8_t type_id; // the type ID of the thermostat
_EMS_MODEL_ID model_id; // which Thermostat type
bool read_supported;
bool write_supported;
char version[10];
float setpoint_roomTemp; // current set temp
float curr_roomTemp; // current room temp
uint8_t mode; // 0=low, 1=manual, 2=auto
@@ -215,17 +288,11 @@ typedef void (*EMS_processType_cb)(uint8_t * data, uint8_t length);
// Definition for each EMS type, including the relative callback function
typedef struct {
uint8_t src;
_EMS_MODEL_ID model_id;
uint8_t type;
const char typeString[50];
EMS_processType_cb processType_cb;
} _EMS_Types;
// Definition for thermostat type
typedef struct {
uint8_t id;
const char typeString[50];
} _Thermostat_Types;
} _EMS_Type;
// ANSI Colors
#define COLOR_RESET "\x1B[0m"
@@ -255,28 +322,37 @@ void ems_setExperimental(uint8_t value);
void ems_setPoll(bool b);
void ems_setTxEnabled(bool b);
void ems_setThermostatEnabled(bool b);
void ems_setBoilerEnabled(bool b);
void ems_setLogging(_EMS_SYS_LOGGING loglevel);
void ems_setEmsRefreshed(bool b);
void ems_setBusConnected(bool b);
void ems_setWarmWaterModeComfort(bool comfort);
void ems_getThermostatValues();
void ems_getBoilerValues();
bool ems_getPoll();
bool ems_getTxEnabled();
bool ems_getThermostatEnabled();
bool ems_getBoilerEnabled();
bool ems_getBusConnected();
_EMS_SYS_LOGGING ems_getLogging();
uint8_t ems_getEmsTypesCount();
uint8_t ems_getThermostatTypesCount();
bool ems_getEmsRefreshed();
void ems_getVersions();
_EMS_MODEL_ID ems_getThermostatModel();
void ems_printAllTypes();
void ems_printThermostatType();
char * ems_getThermostatType(char * buffer);
void ems_printTxQueue();
char * ems_getBoilerType(char * buffer);
// private functions
uint8_t _crcCalculator(uint8_t * data, uint8_t len);
void _processType(uint8_t * telegram, uint8_t length);
void _debugPrintPackage(const char * prefix, uint8_t * data, uint8_t len, const char * color);
void _ems_clearTxData();
int _ems_findModel(_EMS_MODEL_ID model_id);
char * _ems_buildModelString(char * buffer, uint8_t size, _EMS_MODEL_ID model_id);
// global so can referenced in other classes
extern _EMS_Sys_Status EMS_Sys_Status;

View File

@@ -9,31 +9,25 @@
// these are set as -D build flags during compilation
// they can be set in platformio.ini or alternatively hard coded here
/*
#define WIFI_SSID "<my_ssid>"
#define WIFI_PASSWORD "<my_password>"
#define MQTT_IP "<broker_ip>"
#define MQTT_USER "<broker_username>"
#define MQTT_PASS "<broker_password>"
*/
// WIFI settings
//#define WIFI_SSID "<my_ssid>"
//#define WIFI_PASSWORD "<my_password>"
// MQTT settings
// Note port is the default 1883
//#define MQTT_IP "<broker_ip>"
//#define MQTT_USER "<broker_username>"
//#define MQTT_PASS "<broker_password>"
// default values
#define BOILER_THERMOSTAT_ENABLED 1 // thermostat support is enabled (1)
// default values for shower logic on/off
#define BOILER_SHOWER_TIMER 1 // monitors how long a shower has taken
#define BOILER_SHOWER_ALERT 0 // send alert if showetime exceeded
// define here the Thermostat type. see ems.h for the supported types
#define EMS_ID_THERMOSTAT EMS_ID_THERMOSTAT_RC20
//#define EMS_ID_THERMOSTAT EMS_ID_THERMOSTAT_RC30
//#define EMS_ID_THERMOSTAT EMS_ID_THERMOSTAT_EASY
// trigger settings to determine if hot tap water or the heating is active
#define EMS_BOILER_BURNPOWER_TAPWATER 100
#define EMS_BOILER_SELFLOWTEMP_HEATING 70
//define maximum settable tapwater temperature, not every installation supports 90 degrees
#define EMS_BOILER_TAPWATER_TEMPERATURE_MAX 60
// if using the shower timer, change these settings
#define SHOWER_PAUSE_TIME 15000 // in ms. 15 seconds, max time if water is switched off & on during a shower
#define SHOWER_MIN_DURATION 120000 // in ms. 2 minutes, before recognizing its a shower
@@ -41,8 +35,10 @@
#define SHOWER_OFFSET_TIME 5000 // in ms. 5 seconds grace time, to calibrate actual time under the shower
#define SHOWER_COLDSHOT_DURATION 10 // in seconds. 10 seconds for cold water before turning back hot water
// if using LEDs to show traffic, configure the GPIOs here
// only works if -DUSE_LED is set in platformio.ini
#define LED_RX D1 // GPIO5
#define LED_TX D2 // GPIO4
#define LED_ERR D3 // GPIO0
// The LED for showing connection errors, either onboard or via an external pull-up LED
// can be disabled using -DNO_LED build flag in platformio.ini
#define BOILER_LED LED_BUILTIN // LED_BULLETIN for onboard LED
//#define BOILER_LED D1 // for external LED like on the latest bbqkees boards
// set this if using an external temperature sensor like a DS18B20
#define TEMPERATURE_SENSOR_PIN D7

View File

@@ -1,2 +1,5 @@
#pragma once
#define APP_NAME "EMS-ESP-Boiler"
#define APP_VERSION "1.1.0"
#define APP_VERSION "1.2.0"
#define APP_HOSTNAME "boiler"