diff --git a/README.md b/README.md index 21b4929ba..4d10d3ef4 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,11 @@ The original intention for this home project was to build a custom smart thermos Acknowledgments and kudos to the following people who have open-sourced their projects: - **susisstrolch** - One of the first working versions of the EMS bridge circuit I found designed for specifically for the ESP8266. I borrowed Juergen's [schematic](https://github.com/susisstrolch/EMS-ESP12) and parts of his code ideas for reading telegrams. +**susisstrolch** - One of the first working versions of the EMS bridge circuit I found designed for specifically for the ESP8266. I borrowed Juergen's [schematic](https://github.com/susisstrolch/EMS-ESP12) and parts of his code ideas for reading telegrams. - **bbqkees** - Kees built a working [circuit](https://shop.hotgoodies.nl/ems/) and his SMD board is available for purchase on his website. +**bbqkees** - Kees built a working [circuit](https://shop.hotgoodies.nl/ems/) and his SMD board is available for purchase on his website. - **EMS Wiki** - A comprehensive [reference](https://emswiki.thefischer.net/doku.php?id=wiki:ems:telegramme) (in German) for the EMS bus which is a little outdated, not always 100% accurate and sadly no longer maintained. +**EMS Wiki** - A comprehensive [reference](https://emswiki.thefischer.net/doku.php?id=wiki:ems:telegramme) (in German) for the EMS bus which is a little outdated, not always 100% accurate and sadly no longer maintained. ## Supported EMS Devices @@ -70,25 +70,26 @@ The code and circuit has been tested with a few ESP8266 development boards such 3. Optionally add external Dallas temperature sensors and an external LED. The default pins for these are D1 and D5 respectively. 4. Decide whether to compile and upload the code yourself using PlatformIO or just upload the pre-baked firmware using the esptool (read these [instructions](#using-the-pre-built-firmware)). If you want to build yourself now is the time to customize your settings in `my_custom.h`. Upload the firmware. 5. Connect a USB 5v power supply to the ESP8266 board, either via laptop/PC or external power supply. -7. When the ESP8266 starts up for the first time the onboard LED will be flashing. This is because the EMS bus is not yet connected. -8. If you haven't hardcoded the WiFi credentials in step 4, the ESP8266 will boot up in a WiFi Access Point (AP) mode with the ssid name `ems-esp`. Now you can either use a laptop and connect to this AP using Telnet to `192.168.1.4` or if its powered from a computers USB use a Serial monitor tool to the ESP's COM port. Tip: to enable Telnet on Windows 10 run `dism /online /Enable-Feature /FeatureName:TelnetClient` or install something like [putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html). -9. Next is to change some of the settings. Type `set` to list the current stored settings. Use `set wifi` to add your wifi credentials and if you're using MQTT set the host, username and password. There is no need to reboot the device. -10. The `led_gpio` will default to the onboard LED (which is probably blinking now). Ignore `thermostat_type` and `boiler_type` as these will be auto-detected hopefully later on. -11. **Important**: If `serial` is set to `on` set it to `off` using `set serial off`. The EMS bus is disabled when the serial is on. This mode is only used for setting up a new board or debugging startup issues. -12. Hook up the ESP to the EMS board as follows: - -| EMS board | ESP8266 dev board | -| ----------|------------------ | -| Ground/G/J2| GND/G | -| Rx/J2 | D7 | -| Tx/J2 | D8 | -| VC/J2 | 3v3 or 5v | -13. Connect the EMS lines to the ESP. This can be done via the two EMS wires or via the 3.5" service jack if you have an bbqkees board. -14. Reboot the ESP, either by the reset switch or pulling the power. -15. The ESP will first perform an autodetect to try and discover the EMS devices attached. If your boiler and thermostat are recognized it will set these types and store them for ever and ever. You can trace the output by telnet'ing to the board `telnet ems-esp.local`. Also type `info` to check what happened. -16. If your boiler/thermostat is not discovered create a GitHub issue stating the type and product ID. These will be added to the file `ems_devices.h` in a future release. -17. If all is well and there is traffic on the EMS bus the onboard LED will stop blinking and be permanently on. If this is annoying you can disable with `set led off`. To see the EMS messages type `set log v` for verbose logging. -18. And all is not well, check the wiring, make sure serial is off and look at the telnet session for errors. If in doubt, wipe the ESP with `pio run -t erase` and start again with step #3 +6. When the ESP8266 starts up for the first time the onboard LED will be flashing. This is because the EMS bus is not yet connected. +7. If you haven't hardcoded the WiFi credentials in step 4, the ESP8266 will boot up in a WiFi Access Point (AP) mode with the ssid name `ems-esp`. Now you can either use a laptop and connect to this AP using Telnet to `192.168.1.4` or if its powered from a computers USB use a Serial monitor tool to the ESP's COM port. Tip: to enable Telnet on Windows 10 run `dism /online /Enable-Feature /FeatureName:TelnetClient` or install something like [putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html). +8. Next is to change some of the settings. Type `set` to list the current stored settings. Use `set wifi` to add your wifi credentials and if you're using MQTT set the host, username and password. There is no need to reboot the device. +9. The `led_gpio` will default to the onboard LED (which is probably blinking now). Ignore `thermostat_type` and `boiler_type` as these will be auto-detected hopefully later on. +10. **Important**: If `serial` is set to `on` set it to `off` using `set serial off`. The EMS bus is disabled when the serial is on. This mode is only used for setting up a new board or debugging startup issues. +11. Hook up the ESP to the EMS board as follows: + +| EMS board | ESP8266 dev board | +| ----------- | ----------------- | +| Ground/G/J2 | GND/G | +| Rx/J2 | D7 | +| Tx/J2 | D8 | +| VC/J2 | 3v3 or 5v | + +13. Connect the EMS lines to the ESP. This can be done via the two EMS wires or via the 3.5" service jack if you have an bbqkees board. +14. Reboot the ESP, either by the reset switch or pulling the power. +15. The ESP will first perform an autodetect to try and discover the EMS devices attached. If your boiler and thermostat are recognized it will set these types and store them for ever and ever. You can trace the output by telnet'ing to the board `telnet ems-esp.local`. Also type `info` to check what happened. +16. If your boiler/thermostat is not discovered create a GitHub issue stating the type and product ID. These will be added to the file `ems_devices.h` in a future release. +17. If all is well and there is traffic on the EMS bus the onboard LED will stop blinking and be permanently on. If this is annoying you can disable with `set led off`. To see the EMS messages type `set log v` for verbose logging. +18. And all is not well, check the wiring, make sure serial is off and look at the telnet session for errors. If in doubt, wipe the ESP with `pio run -t erase` and start again with step #3 ## Monitoring The Output @@ -118,7 +119,7 @@ The schematic used: ![Schematic](doc/schematics/circuit.png) -*Optionally I've also added 2 0.5A/72V polyfuses between the EMS and the two inductors L1 and L2 for extra protection.* +_Optionally I've also added 2 0.5A/72V polyfuses between the EMS and the two inductors L1 and L2 for extra protection._ And here's a version using an early prototype board from **bbqkees**: @@ -133,8 +134,8 @@ The EMS circuit will work with both 3.3V and 5V. It's easiest though to power di - powering from the 3.5" service jack on the boiler. This will give you 8V so you need a buck converter (like a [Pololu D24C22F5](https://www.pololu.com/product/2858)) to step this down to 5V to provide enough power to the ESP8266 (250mA at least) - powering from the EMS line, which is 15V A/C and using a buck converter as described above. Note the current design has stability issues when sending packages in this configuration so this is not recommended yet if you plan to many send commands to the thermostat or boiler. -| With Power Circuit | -| ------------------------------------------ | +| With Power Circuit | +| --------------------------------------------------------------- | | ![Power circuit](doc/schematics/Schematic_EMS-ESP-supercap.png) | ## Adding external temperature sensors @@ -147,9 +148,10 @@ Packages are streamed to the EMS "bus" from any other compatible connected devic A package can be a single byte (see Polling below) or a string of 6 or more bytes making up an actual data telegram. A telegram is always in the format: -``[src] [dest] [type] [offset] [data] [crc] `` +`[src] [dest] [type] [offset] [data] [crc] ` + +The first 4 bytes is referenced as the _header_ in this document. -The first 4 bytes is referenced as the *header* in this document. ### EMS IDs Each device has a unique ID. @@ -200,18 +202,22 @@ Following a write request, the `[dest]` doesn't have the 8th bit set and after t Every telegram sent is echo'd back to Rx, along the same Bus used for all Rx/Tx transmissions. ## Ems Plus + In this chapter we will report our findings on the ems plus. + ### Message layout -| 0 | 1 | 2 | 3 | 4 | 5 | n....n-1 | n | -| - | - | - | - | - | - | - | - | -|transmitter| receiver | ems plus mark | message type | offset | device intended for | data | cnc -| 18 | 00 | FF | 03 | 01 | A5 | 28 | 46| + +| 0 | 1 | 2 | 3 | 4 | 5 | n....n-1 | n | +| ----------- | -------- | ------------- | ------------ | ------ | ------------------- | -------- | --- | +| transmitter | receiver | ems plus mark | message type | offset | device intended for | data | cnc | +| 18 | 00 | FF | 03 | 01 | A5 | 28 | 46 | ### Message types -| Message type | Definition | -|--------------|---------------------| -| 03 | Set temperature | -| 00 | Status message | + +| Message type | Definition | +| ------------ | --------------- | +| 03 | Set temperature | +| 00 | Status message | ## The ESP8266 Source Code @@ -238,7 +244,7 @@ In this chapter we will report our findings on the ems plus. | Boiler (0x08) | 0x15 | UBAMaintenanceSettingsMessage | | | Boiler (0x08) | 0x16 | UBAParametersMessage | | -In `ems.cpp` you can add scheduled calls to specific EMS types in the functions `ems_getThermostatValues()` and `ems_getBoilerValues()`. +In `ems.cpp` you can add scheduled calls to specific EMS types in the functions `ems_getThermostatValues()` and `ems_getBoilerValues()`. ### Which thermostats are supported? @@ -314,6 +320,7 @@ You can find the .yaml configuration files under `doc/ha`. See also this [HA for **On Linux (e.g. Ubuntu under Windows 10):** Make sure Python 2.7 is installed, then... + ```python % pip install -U platformio % sudo platformio upgrade @@ -323,7 +330,9 @@ Make sure Python 2.7 is installed, then... % cd EMS-ESP % cp platformio.ini-example platformio.ini ``` + edit `platformio.ini` to set `env_default` to your board type, then + ```c % platformio run -t upload ``` diff --git a/checkcode.py b/checkcode.py new file mode 100644 index 000000000..53da63054 --- /dev/null +++ b/checkcode.py @@ -0,0 +1,26 @@ +#!/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", "lib/myESP"]) + print("\n** Finished cpplint...") + +#my_flags = env.ParseFlags(env['BUILD_FLAGS']) +#defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")} +# print defines +# print env.Dump() + +# 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']) diff --git a/clean_fw.py b/clean_fw.py new file mode 100644 index 000000000..140f23ee4 --- /dev/null +++ b/clean_fw.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +from subprocess import call +import os +Import("env") + +def clean(source, target, env): + print("\n** Starting clean...") + call(["pio", "run", "-t", "erase"]) + call(["esptool.py", "-p COM6", "write_flash 0x00000", os.getcwd()+"../firmware/*.bin"]) + print("\n** Finished clean.") + +# built in targets: (buildprog, size, upload, program, buildfs, uploadfs, uploadfsota) +env.AddPreAction("buildprog", clean) + diff --git a/debug.py b/debug.py new file mode 100644 index 000000000..5c8a66030 --- /dev/null +++ b/debug.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +from subprocess import call +import os + +# python decoder.py -p ESP8266 -t C:\Users\Paul\.platformio\packages\toolchain-xtensa -e .pioenvs/nodemcuv2/firmware.elf stackdmp.txt +# java -jar .\EspStackTraceDecoder.jar C:\Users\Paul\.platformio\packages\toolchain-xtensa\bin\xtensa-lx106-elf-addr2line.exe .pioenvs/nodemcuv2/firmware.elf stackdmp.txt + +# example stackdmp.txt would contain text like below copied & pasted from a 'crash dump' command +# >>>stack>>> +# 3fffff20: 3fff32f0 00000003 3fff3028 402101b2 +# 3fffff30: 3fffdad0 3fff3280 0000000d 402148aa +# 3fffff40: 3fffdad0 3fff3280 3fff326c 3fff32f0 +# 3fffff50: 0000000d 3fff326c 3fff3028 402103bd +# 3fffff60: 0000000d 3fff34cc 40211de4 3fff34cc +# 3fffff70: 3fff3028 3fff14c4 3fff301c 3fff34cc +# 3fffff80: 3fffdad0 3fff14c4 3fff3028 40210493 +# 3fffff90: 3fffdad0 00000000 3fff14c4 4020a738 +# 3fffffa0: 3fffdad0 00000000 3fff349c 40211e90 +# 3fffffb0: feefeffe feefeffe 3ffe8558 40100b01 +# <<[0-9]*)\\):$") +COUNTER_REGEX = re.compile('^epc1=(?P0x[0-9a-f]+) epc2=(?P0x[0-9a-f]+) epc3=(?P0x[0-9a-f]+) ' + 'excvaddr=(?P0x[0-9a-f]+) depc=(?P0x[0-9a-f]+)$') +CTX_REGEX = re.compile("^ctx: (?P.+)$") +POINTER_REGEX = re.compile('^sp: (?P[0-9a-f]+) end: (?P[0-9a-f]+) offset: (?P[0-9a-f]+)$') +STACK_BEGIN = '>>>stack>>>' +STACK_END = '<<[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+)(\W.*)?$') + +StackLine = namedtuple("StackLine", ["offset", "content"]) + + +class ExceptionDataParser(object): + def __init__(self): + self.exception = None + + self.epc1 = None + self.epc2 = None + self.epc3 = None + self.excvaddr = None + self.depc = None + + self.ctx = None + + self.sp = None + self.end = None + self.offset = None + + self.stack = [] + + def _parse_exception(self, line): + match = EXCEPTION_REGEX.match(line) + if match is not None: + self.exception = int(match.group('exc')) + return self._parse_counters + return self._parse_exception + + def _parse_counters(self, line): + match = COUNTER_REGEX.match(line) + if match is not None: + self.epc1 = match.group("epc1") + self.epc2 = match.group("epc2") + self.epc3 = match.group("epc3") + self.excvaddr = match.group("excvaddr") + self.depc = match.group("depc") + return self._parse_ctx + return self._parse_counters + + def _parse_ctx(self, line): + match = CTX_REGEX.match(line) + if match is not None: + self.ctx = match.group("ctx") + return self._parse_pointers + return self._parse_ctx + + def _parse_pointers(self, line): + match = POINTER_REGEX.match(line) + if match is not None: + self.sp = match.group("sp") + self.end = match.group("end") + self.offset = match.group("offset") + return self._parse_stack_begin + return self._parse_pointers + + def _parse_stack_begin(self, line): + if line == STACK_BEGIN: + return self._parse_stack_line + return self._parse_stack_begin + + def _parse_stack_line(self, line): + if line != STACK_END: + match = STACK_REGEX.match(line) + if match is not None: + self.stack.append(StackLine(offset=match.group("off"), + content=(match.group("c1"), match.group("c2"), match.group("c3"), + match.group("c4")))) + return self._parse_stack_line + return None + + def parse_file(self, file, stack_only=False): + func = self._parse_exception + if stack_only: + func = self._parse_stack_begin + + for line in file: + func = func(line.strip()) + if func is None: + break + + if func is not None: + print("ERROR: Parser not complete!") + sys.exit(1) + + +class AddressResolver(object): + def __init__(self, tool_path, elf_path): + self._tool = tool_path + self._elf = elf_path + self._address_map = {} + + def _lookup(self, addresses): + cmd = [self._tool, "-aipfC", "-e", self._elf] + [addr for addr in addresses if addr is not None] + + if sys.version_info[0] < 3: + output = subprocess.check_output(cmd) + else: + output = subprocess.check_output(cmd, encoding="utf-8") + + line_regex = re.compile("^(?P[0-9a-fx]+): (?P.+)$") + + last = None + for line in output.splitlines(): + line = line.strip() + match = line_regex.match(line) + + if match is None: + if last is not None and line.startswith('(inlined by)'): + line = line [12:].strip() + self._address_map[last] += ("\n \-> inlined by: " + line) + continue + + if match.group("result") == '?? ??:0': + continue + + self._address_map[match.group("addr")] = match.group("result") + last = match.group("addr") + + def fill(self, parser): + addresses = [parser.epc1, parser.epc2, parser.epc3, parser.excvaddr, parser.sp, parser.end, parser.offset] + for line in parser.stack: + addresses.extend(line.content) + + self._lookup(addresses) + + def _sanitize_addr(self, addr): + if addr.startswith("0x"): + addr = addr[2:] + + fill = "0" * (8 - len(addr)) + return "0x" + fill + addr + + def resolve_addr(self, addr): + out = self._sanitize_addr(addr) + + if out in self._address_map: + out += ": " + self._address_map[out] + + return out + + def resolve_stack_addr(self, addr, full=True): + addr = self._sanitize_addr(addr) + if addr in self._address_map: + return addr + ": " + self._address_map[addr] + + if full: + return "[DATA (0x" + addr + ")]" + + return None + + +def print_addr(name, value, resolver): + print("{}:{} {}".format(name, " " * (8 - len(name)), resolver.resolve_addr(value))) + + +def print_stack_full(lines, resolver): + print("stack:") + for line in lines: + print(line.offset + ":") + for content in line.content: + print(" " + resolver.resolve_stack_addr(content)) + + +def print_stack(lines, resolver): + print("stack:") + for line in lines: + for content in line.content: + out = resolver.resolve_stack_addr(content, full=False) + if out is None: + continue + print(out) + + +def print_result(parser, resolver, full=True, stack_only=False): + if not stack_only: + print('Exception: {} ({})'.format(parser.exception, EXCEPTIONS[parser.exception])) + + print("") + print_addr("epc1", parser.epc1, resolver) + print_addr("epc2", parser.epc2, resolver) + print_addr("epc3", parser.epc3, resolver) + print_addr("excvaddr", parser.excvaddr, resolver) + print_addr("depc", parser.depc, resolver) + + print("") + print("ctx: " + parser.ctx) + + print("") + print_addr("sp", parser.sp, resolver) + print_addr("end", parser.end, resolver) + print_addr("offset", parser.offset, resolver) + + print("") + if full: + print_stack_full(parser.stack, resolver) + else: + print_stack(parser.stack, resolver) + + +def parse_args(): + parser = argparse.ArgumentParser(description="decode ESP Stacktraces.") + + parser.add_argument("-p", "--platform", help="The platform to decode from", choices=PLATFORMS.keys(), + default="ESP8266") + parser.add_argument("-t", "--tool", help="Path to the xtensa toolchain", + default="~/.platformio/packages/toolchain-xtensa/") + parser.add_argument("-e", "--elf", help="path to elf file", required=True) + parser.add_argument("-f", "--full", help="Print full stack dump", action="store_true") + parser.add_argument("-s", "--stack_only", help="Decode only a stractrace", action="store_true") + parser.add_argument("file", help="The file to read the exception data from ('-' for STDIN)", default="-") + + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + + if args.file == "-": + file = sys.stdin + else: + if not os.path.exists(args.file): + print("ERROR: file " + args.file + " not found") + sys.exit(1) + file = open(args.file, "r") + + addr2line = os.path.join(os.path.abspath(os.path.expanduser(args.tool)), + "bin/xtensa-" + PLATFORMS[args.platform] + "-elf-addr2line.exe") + if not os.path.exists(addr2line): + print("ERROR: addr2line not found (" + addr2line + ")") + + elf_file = os.path.abspath(os.path.expanduser(args.elf)) + if not os.path.exists(elf_file): + print("ERROR: elf file not found (" + elf_file + ")") + + parser = ExceptionDataParser() + resolver = AddressResolver(addr2line, elf_file) + + parser.parse_file(file, args.stack_only) + resolver.fill(parser) + + print_result(parser, resolver, args.full, args.stack_only) diff --git a/doc/Domoticz/readme.txt b/doc/Domoticz/readme.txt index cc8965ab6..0f39859be 100644 --- a/doc/Domoticz/readme.txt +++ b/doc/Domoticz/readme.txt @@ -1,10 +1,10 @@ -to install the plugin: -- copy the directory 'nefit' to the domoticz/plugins directory -- make sure that 'Accept new Hardware Devices' is enabeled in settings/sysem -- create new hardware with type 'Nefit EMS-ESP with Proddy firmware' -- set MQTT server and port - -The plugin crrently creates 3 devices: -- a room temperature meter -- a system pressure meter +to install the plugin: +- copy the directory 'nefit' to the domoticz/plugins directory +- make sure that 'Accept new Hardware Devices' is enabeled in settings/sysem +- create new hardware with type 'Nefit EMS-ESP with Proddy firmware' +- set MQTT server and port + +The plugin crrently creates 3 devices: +- a room temperature meter +- a system pressure meter - a thermostat setpoint control \ No newline at end of file diff --git a/lib/MyESP/MyESP.cpp b/lib/MyESP/MyESP.cpp new file mode 100644 index 000000000..d4378fe88 --- /dev/null +++ b/lib/MyESP/MyESP.cpp @@ -0,0 +1,1331 @@ +/* + * MyESP - my ESP helper class to handle Wifi, MQTT and Telnet + * + * Paul Derbyshire - December 2018 + * + * Ideas borrowed from Espurna https://github.com/xoseperez/espurna + */ + +#include "MyESP.h" + +#define RTC_LEAP_YEAR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0)) + +/* Days in a month */ +static uint8_t RTC_Months[2][12] = { + {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, /* Not leap year */ + {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} /* Leap year */ +}; + +// constructor +MyESP::MyESP() { + _app_hostname = strdup("MyESP"); + _app_name = strdup("MyESP"); + _app_version = strdup(MYESP_VERSION); + + _boottime = strdup(""); + _load_average = 100; // calculated load average + + _telnetcommand_callback = NULL; + _telnet_callback = NULL; + + _fs_callback = NULL; + _fs_settings_callback = NULL; + + _helpProjectCmds = NULL; + _helpProjectCmds_count = 0; + + _use_serial = false; + _mqtt_host = NULL; + _mqtt_password = NULL; + _mqtt_username = NULL; + _mqtt_retain = false; + _mqtt_keepalive = 300; + _mqtt_will_topic = NULL; + _mqtt_will_online_payload = NULL; + _mqtt_will_offline_payload = NULL; + _mqtt_base = NULL; + _mqtt_topic = NULL; + _mqtt_qos = 0; + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + _mqtt_last_connection = 0; + _mqtt_connecting = false; + + _wifi_password = NULL; + _wifi_ssid = NULL; + _wifi_callback = NULL; + _wifi_connected = false; + + _suspendOutput = false; +} + +MyESP::~MyESP() { + end(); +} + +// end +void MyESP::end() { + SerialAndTelnet.end(); + jw.disconnect(); +} + +// general debug to the telnet or serial channels +void MyESP::myDebug(const char * format, ...) { + if (_suspendOutput) + return; + + 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, ...) { + if (_suspendOutput) + return; + + 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); + + // capture & print timestamp + char timestamp[10] = {0}; + snprintf_P(timestamp, sizeof(timestamp), PSTR("[%06lu] "), millis() % 1000000); + SerialAndTelnet.print(timestamp); + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + +// use Serial? +bool MyESP::getUseSerial() { + return (_use_serial); +} + +// called when WiFi is connected, and used to start OTA, MQTT +void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { + if ((code == MESSAGE_CONNECTED)) { +#if defined(ARDUINO_ARCH_ESP32) + String hostname = String(WiFi.getHostname()); +#else + String hostname = WiFi.hostname(); +#endif + + 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()); + + // start OTA + ArduinoOTA.begin(); // moved to support esp32 + myDebug_P(PSTR("[OTA] listening to %s.local:%u"), ArduinoOTA.getHostname().c_str(), OTA_PORT); + + // MQTT Setup + _mqtt_setup(); + + _wifi_connected = true; + + // finally if we don't want Serial anymore, turn it off + if (!_use_serial) { + Serial.println("Disabling serial port"); + Serial.flush(); + Serial.end(); + SerialAndTelnet.setSerial(NULL); + } else { + Serial.println("Using serial port output"); + } + + // call any final custom settings + if (_wifi_callback) { + _wifi_callback(); + } + + jw.enableAPFallback(false); // Disable AP mode after initial connect was succesfull. Thanks @JewelZB + } + + if (code == MESSAGE_ACCESSPOINT_CREATED) { + 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()); + + // call any final custom settings + if (_wifi_callback) { + _wifi_callback(); + } + } + + if (code == MESSAGE_CONNECTING) { + myDebug_P(PSTR("[WIFI] Connecting to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_CONNECT_FAILED) { + myDebug_P(PSTR("[WIFI] Could not connect to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_DISCONNECTED) { + myDebug_P(PSTR("[WIFI] Disconnected")); + _wifi_connected = false; + } +} + +// received MQTT message +// we send this to the call back function. Important to parse are the event strings such as MQTT_MESSAGE_EVENT and MQTT_CONNECT_EVENT +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); // enable for debugging + + // topics are in format MQTT_BASE/HOSTNAME/TOPIC + char * topic_magnitude = strrchr(topic, '/'); // strip out everything until last / + if (topic_magnitude != nullptr) { + topic = topic_magnitude + 1; + } + + // Send message event to custom service + (_mqtt_callback)(MQTT_MESSAGE_EVENT, topic, message); +} + +// MQTT subscribe +// to MQTT_BASE/app_hostname/topic +void MyESP::mqttSubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.subscribe(_mqttTopic(topic), _mqtt_qos); + myDebug_P(PSTR("[MQTT] Subscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT unsubscribe +// to MQTT_BASE/app_hostname/topic +void MyESP::mqttUnsubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.unsubscribe(_mqttTopic(topic)); + myDebug_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT Publish +void MyESP::mqttPublish(const char * topic, const char * payload) { + // myDebug_P(PSTR("[MQTT] Sending pubish to %s with payload %s"), _mqttTopic(topic), payload); + mqttClient.publish(_mqttTopic(topic), _mqtt_qos, _mqtt_retain, payload); +} + +// MQTT onConnect - when a connect is established +void MyESP::_mqttOnConnect() { + myDebug_P(PSTR("[MQTT] Connected")); + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + + _mqtt_last_connection = millis(); + + // say we're alive to the Last Will topic + mqttClient.publish(_mqttTopic(_mqtt_will_topic), 1, true, _mqtt_will_online_payload); + + // call custom function to handle mqtt receives + (_mqtt_callback)(MQTT_CONNECT_EVENT, NULL, NULL); +} + +// MQTT setup +void MyESP::_mqtt_setup() { + if (!_mqtt_host) { + myDebug_P(PSTR("[MQTT] disabled")); + } + + mqttClient.onConnect([this](bool sessionPresent) { _mqttOnConnect(); }); + + mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { + if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) { + myDebug_P(PSTR("[MQTT] TCP Disconnected. Check mqtt logs.")); + (_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")); + } + + // Reset reconnection delay + _mqtt_last_connection = millis(); + _mqtt_connecting = false; + }); + + //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 + 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 +} + +// set the callback function for the OTA onstart +void MyESP::setOTA(ota_callback_f OTACallback) { + _ota_callback = OTACallback; +} + +// OTA callback when the upload process starts +void MyESP::_OTACallback() { + myDebug_P(PSTR("[OTA] Start")); + SerialAndTelnet.handle(); // force flush + if (_ota_callback) { + (_ota_callback)(); // call custom function to handle mqtt receives + } +} + +// OTA Setup +void MyESP::_ota_setup() { + if (!_wifi_ssid) { + return; + } + + //ArduinoOTA.setPort(OTA_PORT); + ArduinoOTA.setHostname(_app_hostname); + + ArduinoOTA.onStart([this]() { _OTACallback(); }); + 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("[OTA] Auth Failed")); + else if (error == OTA_BEGIN_ERROR) + myDebug_P(PSTR("[OTA] Begin Failed")); + else if (error == OTA_CONNECT_ERROR) + myDebug_P(PSTR("[OTA] Connect Failed")); + else if (error == OTA_RECEIVE_ERROR) + myDebug_P(PSTR("[OTA] Receive Failed")); + else if (error == OTA_END_ERROR) + myDebug_P(PSTR("[OTA] End Failed")); + }); +} + +// sets boottime +void MyESP::setBoottime(const char * boottime) { + if (_boottime) { + free(_boottime); + } + _boottime = strdup(boottime); +} + +// Set callback of sketch function to process project messages +void MyESP::setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback) { + _helpProjectCmds = cmds; // command list + _helpProjectCmds_count = count; // number of commands + _telnetcommand_callback = callback_cmd; // external function to handle commands + _telnet_callback = callback; +} + +void MyESP::_telnetConnected() { + myDebug_P(PSTR("[TELNET] Telnet connection established")); + _consoleShowHelp(); // Show the initial message + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_CONNECT); // call callback + } +} + +void MyESP::_telnetDisconnected() { + myDebug_P(PSTR("[TELNET] Telnet connection closed")); + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_DISCONNECT); // call callback + } +} + +// Initialize the telnet server +void MyESP::_telnet_setup() { + SerialAndTelnet.setWelcomeMsg(""); + SerialAndTelnet.setCallbackOnConnect([this]() { _telnetConnected(); }); + SerialAndTelnet.setCallbackOnDisconnect([this]() { _telnetDisconnected(); }); + SerialAndTelnet.setDebugOutput(false); + SerialAndTelnet.begin(TELNET_SERIAL_BAUD); // default baud is 115200 + + // init command buffer for console commands + memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); +} + +// https://stackoverflow.com/questions/43063071/the-arduino-ntp-i-want-print-out-datadd-mm-yyyy +void MyESP::_printBuildTime(unsigned long unix) { + // compensate for summer/winter time and CET. Can't be bothered to work out DST. + // add 3600 to the UNIX time during winter, (3600 s = 1 h), and 7200 during summer (DST). + unix += 3600; // add 1 hour + + uint8_t Day, Month; + + uint8_t Seconds = unix % 60; /* Get seconds from unix */ + unix /= 60; /* Go to minutes */ + uint8_t Minutes = unix % 60; /* Get minutes */ + unix /= 60; /* Go to hours */ + uint8_t Hours = unix % 24; /* Get hours */ + unix /= 24; /* Go to days */ + + uint16_t year = 1970; /* Process year */ + while (1) { + if (RTC_LEAP_YEAR(year)) { + if (unix >= 366) { + unix -= 366; + } else { + break; + } + } else if (unix >= 365) { + unix -= 365; + } else { + break; + } + year++; + } + + /* Get year in xx format */ + uint8_t Year = (uint8_t)(year - 2000); + /* Get month */ + for (Month = 0; Month < 12; Month++) { + if (RTC_LEAP_YEAR(year)) { + if (unix >= (uint32_t)RTC_Months[1][Month]) { + unix -= RTC_Months[1][Month]; + } else { + break; + } + } else if (unix >= (uint32_t)RTC_Months[0][Month]) { + unix -= RTC_Months[0][Month]; + } else { + break; + } + } + + Month++; /* Month starts with 1 */ + Day = unix + 1; /* Date starts with 1 */ + + SerialAndTelnet.printf("%02d:%02d:%02d %d/%d/%d", Hours, Minutes, Seconds, Day, Month, Year); +} + +// Show help of commands +void MyESP::_consoleShowHelp() { + SerialAndTelnet.println(); + SerialAndTelnet.printf("* Connected to: %s version %s", _app_name, _app_version); + SerialAndTelnet.println(); + + if (WiFi.getMode() & WIFI_AP) { + SerialAndTelnet.printf("* ESP is in AP mode with SSID %s", jw.getAPSSID().c_str()); + SerialAndTelnet.println(); + } else { +#if defined(ARDUINO_ARCH_ESP32) + String hostname = String(WiFi.getHostname()); +#else + String hostname = WiFi.hostname(); +#endif + SerialAndTelnet.printf("* Hostname: %s IP: %s MAC: %s", hostname.c_str(), WiFi.localIP().toString().c_str(), WiFi.macAddress().c_str()); +#ifdef ARDUINO_BOARD + SerialAndTelnet.printf(" Board: %s", ARDUINO_BOARD); +#endif + SerialAndTelnet.printf(" (MyESP v%s)", MYESP_VERSION); + +#ifdef BUILD_TIME + SerialAndTelnet.print(" (Build "); + _printBuildTime(BUILD_TIME); + SerialAndTelnet.print(")"); +#endif + SerialAndTelnet.println(); + SerialAndTelnet.printf("* Connected to WiFi SSID: %s (signal %d%%)", WiFi.SSID().c_str(), getWifiQuality()); + SerialAndTelnet.println(); + SerialAndTelnet.printf("* MQTT is %s", mqttClient.connected() ? "connected" : "disconnected"); + SerialAndTelnet.println(); + SerialAndTelnet.printf("* Boot time: %s", _boottime); + SerialAndTelnet.println(); + } + + SerialAndTelnet.printf("* Free RAM: %d KB Load: %d%%", (ESP.getFreeHeap() / 1024), getSystemLoadAverage()); + SerialAndTelnet.println(); + // for battery power is ESP.getVcc() + + SerialAndTelnet.println(FPSTR("*")); + SerialAndTelnet.println(FPSTR("* Commands:")); + SerialAndTelnet.println(FPSTR("* ?=help, CTRL-D=quit")); + SerialAndTelnet.println(FPSTR("* reboot")); + SerialAndTelnet.println(FPSTR("* crash ")); + SerialAndTelnet.println(FPSTR("* set")); + SerialAndTelnet.println(FPSTR("* set wifi [ssid] [password]")); + SerialAndTelnet.println(FPSTR("* set [value]")); + SerialAndTelnet.println(FPSTR("* set erase")); + SerialAndTelnet.println(FPSTR("* set serial")); + + // print custom commands if available. Taken from progmem + if (_telnetcommand_callback) { + // find the longest key length so we can right align it + uint8_t max_len = 0; + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if (strlen(_helpProjectCmds[i].key) > max_len) + max_len = strlen(_helpProjectCmds[i].key); + } + + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + SerialAndTelnet.print(FPSTR("* ")); + SerialAndTelnet.print(FPSTR(_helpProjectCmds[i].key)); + for (uint8_t j = 0; j < ((max_len + 5) - strlen(_helpProjectCmds[i].key)); j++) { // account for longest string length + SerialAndTelnet.print(FPSTR(" ")); // padding + } + SerialAndTelnet.println(FPSTR(_helpProjectCmds[i].description)); + } + } + + SerialAndTelnet.println(); // newline +} + +// reset / restart +void MyESP::resetESP() { + myDebug_P(PSTR("* Reboot ESP...")); + end(); +#if defined(ARDUINO_ARCH_ESP32) + ESP.restart(); +#else + ESP.restart(); +#endif +} + +// read next word from string buffer +char * MyESP::_telnet_readWord() { + return (strtok(NULL, ", \n")); +} + +// change setting for 2 params (set ) +void MyESP::_changeSetting2(const char * setting, const char * value1, const char * value2) { + if (strcmp(setting, "wifi") == 0) { + if (_wifi_ssid) + free(_wifi_ssid); + if (_wifi_password) + free(_wifi_password); + _wifi_ssid = NULL; + _wifi_password = NULL; + + if (value1) { + _wifi_ssid = strdup(value1); + } + + if (value2) { + _wifi_password = strdup(value2); + } + + (void)fs_saveConfig(); + SerialAndTelnet.println("WiFi settings changed. Reconnecting..."); + jw.disconnect(); + jw.cleanNetworks(); + jw.addNetwork(_wifi_ssid, _wifi_password); + } +} + +// change settings - always as strings +// messy code but effective since we don't have too many settings +// wc is word count, number of parameters after the 'set' command +void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) { + bool ok = false; + + // check for our internal commands first + if (strcmp(setting, "erase") == 0) { + _fs_eraseConfig(); + return; + } else if ((strcmp(setting, "wifi") == 0) && (wc == 1)) { // erase wifi settings + if (_wifi_ssid) + free(_wifi_ssid); + if (_wifi_password) + free(_wifi_password); + _wifi_ssid = NULL; + _wifi_password = NULL; + ok = true; + } else if (strcmp(setting, "mqtt_host") == 0) { + if (_mqtt_host) + free(_mqtt_host); + _mqtt_host = NULL; // just to be sure + if (value) { + _mqtt_host = strdup(value); + } + ok = true; + } else if (strcmp(setting, "mqtt_username") == 0) { + if (_mqtt_username) + free(_mqtt_username); + _mqtt_username = NULL; // just to be sure + if (value) { + _mqtt_username = strdup(value); + } + ok = true; + } else if (strcmp(setting, "mqtt_password") == 0) { + if (_mqtt_password) + free(_mqtt_password); + _mqtt_password = NULL; // just to be sure + if (value) { + _mqtt_password = strdup(value); + } + ok = true; + } else if (strcmp(setting, "serial") == 0) { + ok = true; + _use_serial = false; + if (value) { + if (strcmp(value, "on") == 0) { + _use_serial = true; + ok = true; + } else if (strcmp(value, "off") == 0) { + _use_serial = false; + ok = true; + } else { + ok = false; + } + } + } else { + // finally check for any custom commands + ok = (_fs_settings_callback)(MYESP_FSACTION_SET, wc, setting, value); + } + + if (!ok) { + SerialAndTelnet.println("\nInvalid parameter for set command."); + return; + } + + // check for 2 params + if (value == nullptr) { + SerialAndTelnet.printf("%s setting reset to its default value.", setting); + } else { + // must be 3 params + SerialAndTelnet.printf("%s changed.", setting); + } + SerialAndTelnet.println(); + + (void)fs_saveConfig(); +} + +void MyESP::_telnetCommand(char * commandLine) { + // count the number of arguments + char * str = commandLine; + bool state = false; + unsigned wc = 0; + while (*str) { + if (*str == ' ' || *str == '\n' || *str == '\t') { + state = false; + } else if (state == false) { + state = true; + ++wc; + } + ++str; + } + + // check first for reserved commands + char * temp = strdup(commandLine); // because strotok kills original string buffer + char * ptrToCommandName = strtok((char *)temp, ", \n"); + + // set command + if (strcmp(ptrToCommandName, "set") == 0) { + if (wc == 1) { + SerialAndTelnet.println(); + SerialAndTelnet.println("Stored settings:"); + SerialAndTelnet.printf(" wifi=%s ", (!_wifi_ssid) ? "" : _wifi_ssid); + if (!_wifi_password) { + SerialAndTelnet.print(""); + } else { + for (uint8_t i = 0; i < strlen(_wifi_password); i++) + SerialAndTelnet.print("*"); + } + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_host=%s", (!_mqtt_host) ? "" : _mqtt_host); + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_username=%s", (!_mqtt_username) ? "" : _mqtt_username); + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_password="); + if (!_mqtt_password) { + SerialAndTelnet.print(""); + } else { + for (uint8_t i = 0; i < strlen(_mqtt_password); i++) + SerialAndTelnet.print("*"); + } + + SerialAndTelnet.println(); + SerialAndTelnet.printf(" serial=%s", (_use_serial) ? "on" : "off"); + + SerialAndTelnet.println(); + + // print custom settings + (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); + + SerialAndTelnet.println(); + SerialAndTelnet.println("Usage: set [value...]"); + } else if (wc == 2) { + char * setting = _telnet_readWord(); + _changeSetting(1, setting, NULL); + } else if (wc == 3) { + char * setting = _telnet_readWord(); + char * value = _telnet_readWord(); + _changeSetting(2, setting, value); + } else if (wc == 4) { + char * setting = _telnet_readWord(); + char * value1 = _telnet_readWord(); + char * value2 = _telnet_readWord(); + _changeSetting2(setting, value1, value2); + } + return; + } + + // reboot command + if ((strcmp(ptrToCommandName, "reboot") == 0) && (wc == 1)) { + resetESP(); + } + + // crash command + if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) { + char * cmd = _telnet_readWord(); + if (strcmp(cmd, "dump") == 0) { + crashDump(); + } else if (strcmp(cmd, "clear") == 0) { + crashClear(); + } else if ((strcmp(cmd, "test") == 0) && (wc == 3) ) { + char * value = _telnet_readWord(); + crashTest(atoi(value)); + } + return; + } + + // call callback function + (_telnetcommand_callback)(wc, commandLine); +} + +// handler for Telnet +void MyESP::_telnetHandle() { + SerialAndTelnet.handle(); + + static uint8_t charsRead = 0; + // read asynchronously until full command input + while (SerialAndTelnet.available()) { + char c = SerialAndTelnet.read(); + + SerialAndTelnet.serialPrint(c); // echo to Serial if connected + + switch (c) { + case '\r': // likely have full command in buffer now, commands are terminated by CR and/or LF + case '\n': + _command[charsRead] = '\0'; // null terminate our command char array + if (charsRead > 0) { + charsRead = 0; // is static, so have to reset + _suspendOutput = false; + if (_use_serial) { + SerialAndTelnet.serialPrint('\n'); // force newline if in Serial + } + _telnetCommand(_command); + } + break; + + case '\b': // (^H) handle backspace in input: put a space in last char - coded by Simon Arlott + case 0x7F: // (^?) + + if (charsRead > 0) { + _command[--charsRead] = '\0'; + + SerialAndTelnet.write(' '); + SerialAndTelnet.write('\b'); + } + + break; + + case '?': + if (!_suspendOutput) { + _consoleShowHelp(); + } else { + _command[charsRead++] = c; // add it to buffer as its part of the string entered + } + break; + case 0x04: // EOT, CTRL-D + myDebug_P(PSTR("[TELNET] exiting telnet session")); + SerialAndTelnet.disconnectClient(); + break; + default: + _suspendOutput = true; + if (charsRead < TELNET_MAX_COMMAND_LENGTH) { + _command[charsRead++] = c; + } + _command[charsRead] = '\0'; // just in case + break; + } + } +} + +// ensure we have a connection to MQTT broker +void MyESP::_mqttConnect() { + if (!_mqtt_host) + return; // MQTT not enabled + + // Do not connect if already connected or still trying to connect + if (mqttClient.connected() || _mqtt_connecting || (WiFi.status() != WL_CONNECTED)) { + return; + } + + // Check reconnect interval + if (millis() - _mqtt_last_connection < _mqtt_reconnect_delay) { + return; + } + + _mqtt_connecting = true; // we're doing a connection + + // 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); + mqttClient.setKeepAlive(_mqtt_keepalive); + mqttClient.setCleanSession(false); + + // last will + if (_mqtt_will_topic) { + //myDebug_P(PSTR("[MQTT] Setting last will topic %s"), _mqttTopic(_mqtt_will_topic)); + mqttClient.setWill(_mqttTopic(_mqtt_will_topic), 1, true, + _mqtt_will_offline_payload); // retain always true + } + + if (_mqtt_username && _mqtt_password) { + myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s..."), _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::setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback) { + // 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 || *wifi_ssid == 0x00 || strlen(wifi_password) > 31) { + _wifi_password = NULL; + } else { + _wifi_password = strdup(wifi_password); + } + + // callback + _wifi_callback = callback; +} + +// init MQTT settings +void MyESP::setMQTT(const char * mqtt_host, const char * mqtt_username, const char * mqtt_password, const char * mqtt_base, unsigned long mqtt_keepalive, unsigned char mqtt_qos, bool mqtt_retain, const char * mqtt_will_topic, const char * mqtt_will_online_payload, const char * mqtt_will_offline_payload, mqtt_callback_f callback) { + // 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); + } + + // base + if (_mqtt_base) { + free(_mqtt_base); + } + _mqtt_base = strdup(mqtt_base); + + // callback + _mqtt_callback = callback; + + // various mqtt settings + _mqtt_keepalive = mqtt_keepalive; + _mqtt_qos = mqtt_qos; + _mqtt_retain = mqtt_retain; + + // last will + if (!mqtt_will_topic || *mqtt_will_topic == 0x00) { + _mqtt_will_topic = NULL; + } else { + _mqtt_will_topic = strdup(mqtt_will_topic); + } + + if (!mqtt_will_online_payload || *mqtt_will_online_payload == 0x00) { + _mqtt_will_online_payload = NULL; + } else { + _mqtt_will_online_payload = strdup(mqtt_will_online_payload); + } + + if (!mqtt_will_offline_payload || *mqtt_will_offline_payload == 0x00) { + _mqtt_will_offline_payload = NULL; + } else { + _mqtt_will_offline_payload = strdup(mqtt_will_offline_payload); + } +} + +// builds up a topic by prefixing the base and hostname +char * MyESP::_mqttTopic(const char * topic) { + char buffer[MQTT_MAX_TOPIC_SIZE] = {0}; + + strlcpy(buffer, _mqtt_base, sizeof(buffer)); + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, _app_hostname, sizeof(buffer)); + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, topic, sizeof(buffer)); + + if (_mqtt_topic) { + free(_mqtt_topic); + } + _mqtt_topic = strdup(buffer); + + return _mqtt_topic; +} + + +// print contents of file +// assumes Serial is open +void MyESP::_fs_printConfig() { + myDebug_P(PSTR("[FS] Contents:")); + + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); + if (!configFile) { + Serial.println(F("[FS] Failed to read file for printing")); + return; + } + + while (configFile.available()) { + SerialAndTelnet.print((char)configFile.read()); + } + SerialAndTelnet.println(); + + configFile.close(); +} + +// format File System +void MyESP::_fs_eraseConfig() { + myDebug_P(PSTR("[FS] Erasing settings, please wait a few seconds. ESP will " + "automatically restart when finished.")); + + if (SPIFFS.format()) { + delay(1000); // wait 1 seconds + resetESP(); + } +} + +void MyESP::setSettings(fs_callback_f callback_fs, fs_settings_callback_f callback_settings_fs) { + _fs_callback = callback_fs; + _fs_settings_callback = callback_settings_fs; +} + +// load from spiffs +bool MyESP::_fs_loadConfig() { + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); + + size_t size = configFile.size(); + if (size > 1024) { + myDebug_P(PSTR("[FS] Config file size is too large")); + return false; + } else if (size == 0) { + myDebug_P(PSTR("[FS] Failed to open config file")); + // file does not exist, so assume its the first install. Set serial to on + _use_serial = true; + return false; + } + + StaticJsonDocument doc; + JsonObject json = doc.to(); + + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, configFile); + if (error) { + Serial.println(F("[FS] Failed to read file")); + return false; + } + + const char * value; + + value = json["wifi_ssid"]; + _wifi_ssid = (value) ? strdup(value) : NULL; + + value = json["wifi_password"]; + _wifi_password = (value) ? strdup(value) : NULL; + + value = json["mqtt_host"]; + _mqtt_host = (value) ? strdup(value) : NULL; + + value = json["mqtt_username"]; + _mqtt_username = (value) ? strdup(value) : NULL; + + value = json["mqtt_password"]; + _mqtt_password = (value) ? strdup(value) : NULL; + + _use_serial = (bool)json["use_serial"]; + + // callback for loading custom settings + // ok is false if there's a problem loading a custom setting (e.g. does not exist) + bool ok = (_fs_callback)(MYESP_FSACTION_LOAD, json); + + configFile.close(); + + return ok; +} + +// save settings to spiffs +bool MyESP::fs_saveConfig() { + StaticJsonDocument doc; + JsonObject json = doc.to(); + + json["app_version"] = _app_version; + json["wifi_ssid"] = _wifi_ssid; + json["wifi_password"] = _wifi_password; + json["mqtt_host"] = _mqtt_host; + json["mqtt_username"] = _mqtt_username; + json["mqtt_password"] = _mqtt_password; + json["use_serial"] = _use_serial; + + // callback for saving custom settings + (void)(_fs_callback)(MYESP_FSACTION_SAVE, json); + + // if file exists, remove it just to be safe + if (SPIFFS.exists(MYEMS_CONFIG_FILE)) { + // delete it + SPIFFS.remove(MYEMS_CONFIG_FILE); + } + + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "w"); + if (!configFile) { + Serial.println("[FS] Failed to open config file for writing"); + return false; + } + + // Serialize JSON to file + if (serializeJson(json, configFile) == 0) { + Serial.println(F("[FS] Failed to write to file")); + } + + configFile.close(); + + return true; +} + +// init the SPIFF file system and load the config +// if it doesn't exist try and create it +// force Serial for debugging, and turn it off afterwards +void MyESP::_fs_setup() { + if (!SPIFFS.begin()) { + Serial.println("[FS] Failed to mount the file system"); + _fs_eraseConfig(); // fix for ESP32 + return; + } + + // load the config file. if it doesn't exist (function returns false) create it + if (!_fs_loadConfig()) { + // Serial.println("[FS] Re-creating config file"); + fs_saveConfig(); + } + + //_fs_printConfig(); // TODO: for debugging +} + +uint16_t MyESP::getSystemLoadAverage() { + return _load_average; +} + +// calculate load average +void MyESP::_calculateLoad() { + static unsigned long last_loadcheck = 0; + static unsigned long load_counter_temp = 0; + load_counter_temp++; + + if (millis() - last_loadcheck > LOADAVG_INTERVAL) { + static unsigned long load_counter = 0; + static unsigned long load_counter_max = 1; + + load_counter = load_counter_temp; + load_counter_temp = 0; + if (load_counter > load_counter_max) { + load_counter_max = load_counter; + } + _load_average = 100 - (100 * load_counter / load_counter_max); + last_loadcheck = millis(); + } +} + +// returns true is MQTT is alive +bool MyESP::isMQTTConnected() { + return mqttClient.connected(); +} + +// return true if wifi is connected +// WL_NO_SHIELD = 255, // for compatibility with WiFi Shield library +// WL_IDLE_STATUS = 0, +// WL_NO_SSID_AVAIL = 1, +// WL_SCAN_COMPLETED = 2, +// WL_CONNECTED = 3, +// WL_CONNECT_FAILED = 4, +// WL_CONNECTION_LOST = 5, +// WL_DISCONNECTED = 6 +bool MyESP::isWifiConnected() { + return (_wifi_connected); +} + +/* + Return the quality (Received Signal Strength Indicator) + of the WiFi network. + Returns a number between 0 and 100 if WiFi is connected. + Returns -1 if WiFi is disconnected. + + High quality: 90% ~= -55dBm + Medium quality: 50% ~= -75dBm + Low quality: 30% ~= -85dBm + Unusable quality: 8% ~= -96dBm +*/ +int MyESP::getWifiQuality() { + if (WiFi.status() != WL_CONNECTED) + return -1; + int dBm = WiFi.RSSI(); + if (dBm <= -100) + return 0; + if (dBm >= -50) + return 100; + return 2 * (dBm + 100); +} + +/** + * Save crash information in EEPROM + * This function is called automatically if ESP8266 suffers an exception + * It should be kept quick / consise to be able to execute before hardware wdt may kick in + */ +extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end) { + + // Note that 'EEPROM.begin' method is reserving a RAM buffer + // The buffer size is SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_SPACE_SIZE + EEPROM.begin(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EEPROM_SIZE); + + // write crash time to EEPROM + uint32_t crash_time = millis(); + EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + + // write reset info to EEPROM + EEPROM.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); + EEPROM.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); + + // write epc1, epc2, epc3, excvaddr and depc to EEPROM + EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); + EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); + EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); + EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); + EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); + + // write stack start and end address to EEPROM + EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + + // starting address + const uint16_t settings_start = SPI_FLASH_SEC_SIZE - SAVE_CRASH_EEPROM_SIZE - 0x10; + + // write stack trace to EEPROM and avoid overwriting settings + int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; + for (uint32_t i = stack_start; i < stack_end; i++) { + if (current_address >= settings_start) + break; + byte * byteValue = (byte *)i; + EEPROM.write(current_address++, *byteValue); + } + + EEPROM.commit(); +} + +void MyESP::crashTest(uint8_t t) { + if (t == 1) { + myDebug("Attempting to divide by zero ..."); + int result, zero; + zero = 0; + result = 1 / zero; + myDebug("Result = %d", result); + } + + if (t == 2) { + myDebug("Attempting to read through a pointer to no object ..."); + int * nullPointer; + nullPointer = NULL; + // null pointer dereference - read + // attempt to read a value through a null pointer + Serial.println(*nullPointer); + } + + if (t == 3) { + Serial.printf("Crashing with hardware WDT (%ld ms) ...\n", millis()); + ESP.wdtDisable(); + while (true) { + // stay in an infinite loop doing nothing + // this way other process can not be executed + // + // Note: + // Hardware wdt kicks in if software wdt is unable to perfrom + // Nothing will be saved in EEPROM for the hardware wdt + } + } + + if (t == 4) { + Serial.printf("Crashing with software WDT (%ld ms) ...\n", millis()); + while (true) { + // stay in an infinite loop doing nothing + // this way other process can not be executed + } + } +} + +/** + * Clears crash info + */ +void MyESP::crashClear() { + EEPROM.begin(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EEPROM_SIZE); + + uint32_t crash_time = 0xFFFFFFFF; + EEPROM.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + EEPROM.commit(); +} + +/** + * Print out crash information that has been previously saved in EEPROM + */ +void MyESP::crashDump() { + EEPROM.begin(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EEPROM_SIZE); + + uint32_t crash_time; + EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + if ((crash_time == 0) || (crash_time == 0xFFFFFFFF)) { + myDebug_P(PSTR("[CRASH] No crash info")); + return; + } + + myDebug_P(PSTR("[CRASH] Latest crash was at %lu ms after boot"), crash_time); + myDebug_P(PSTR("[CRASH] Reason of restart: %u"), EEPROM.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON)); + myDebug_P(PSTR("[CRASH] Exception cause: %u"), EEPROM.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE)); + + uint32_t epc1, epc2, epc3, excvaddr, depc; + EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1); + EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2); + EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3); + EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr); + EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc); + + myDebug_P(PSTR("[CRASH] epc1=0x%08x epc2=0x%08x epc3=0x%08x"), epc1, epc2, epc3); + myDebug_P(PSTR("[CRASH] excvaddr=0x%08x depc=0x%08x"), excvaddr, depc); + + uint32_t stack_start, stack_end; + EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROM.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + + myDebug_P(PSTR("[CRASH] sp=0x%08x end=0x%08x"), stack_start, stack_end); + + int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; + int16_t stack_len = stack_end - stack_start; + + uint32_t stack_trace; + + myDebug(">>>stack>>>"); + + for (int16_t i = 0; i < stack_len; i += 0x10) { + SerialAndTelnet.printf("%08x: ", stack_start + i); + for (byte j = 0; j < 4; j++) { + EEPROM.get(current_address, stack_trace); + SerialAndTelnet.printf("%08x ", stack_trace); + current_address += 4; + } + SerialAndTelnet.println(); + } + myDebug("<< +#include +#include // https://github.com/marvinroger/async-mqtt-client and for ESP32 see https://github.com/marvinroger/async-mqtt-client/issues/127 +#include +#include +#include // https://github.com/xoseperez/justwifi +#include // modified from https://github.com/yasheena/telnetspy + +#include "EEPROM.h" +extern "C" { + void custom_crash_callback(struct rst_info*, uint32_t, uint32_t); +} + +#if defined(ARDUINO_ARCH_ESP32) +//#include +#include // added for ESP32 +#define ets_vsnprintf vsnprintf // added for ESP32 +#define OTA_PORT 8266 +#else +//#include +#include +#define OTA_PORT 3232 +#endif + +#define MYEMS_CONFIG_FILE "/config.json" + +#define LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) + +// 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 + +// MQTT +#define MQTT_PORT 1883 // MQTT port +#define MQTT_RECONNECT_DELAY_MIN 2000 // Try to reconnect in 3 seconds upon disconnection +#define MQTT_RECONNECT_DELAY_STEP 3000 // Increase the reconnect delay in 3 seconds after each failed attempt +#define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most +#define MQTT_MAX_SIZE 600 // max length of MQTT message +#define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT message + +// Internal MQTT events +#define MQTT_CONNECT_EVENT 0 +#define MQTT_DISCONNECT_EVENT 1 +#define MQTT_MESSAGE_EVENT 2 + +// Telnet +#define TELNET_SERIAL_BAUD 115200 +#define TELNET_MAX_COMMAND_LENGTH 80 // length of a command +#define TELNET_EVENT_CONNECT 1 +#define TELNET_EVENT_DISCONNECT 0 + +// 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" +#define COLOR_BOLD_ON "\x1B[1m" +#define COLOR_BOLD_OFF "\x1B[22m" // fixed by Scott Arlott + +// SPIFFS +#define SPIFFS_MAXSIZE 500 // https://arduinojson.org/v5/assistant/ + +// CRASH +#define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data +#define SAVE_CRASH_EEPROM_SIZE 0x0200 // size +#define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes +#define SAVE_CRASH_RESTART_REASON 0x04 // 1 byte +#define SAVE_CRASH_EXCEPTION_CAUSE 0x05 // 1 byte +#define SAVE_CRASH_EPC1 0x06 // 4 bytes +#define SAVE_CRASH_EPC2 0x0A // 4 bytes +#define SAVE_CRASH_EPC3 0x0E // 4 bytes +#define SAVE_CRASH_EXCVADDR 0x12 // 4 bytes +#define SAVE_CRASH_DEPC 0x16 // 4 bytes +#define SAVE_CRASH_STACK_START 0x1A // 4 bytes +#define SAVE_CRASH_STACK_END 0x1E // 4 bytes +#define SAVE_CRASH_STACK_TRACE 0x22 // variable + +typedef struct { + char key[40]; + char description[100]; +} command_t; + +typedef enum { MYESP_FSACTION_SET, MYESP_FSACTION_LIST, MYESP_FSACTION_SAVE, MYESP_FSACTION_LOAD } MYESP_FSACTION; + +typedef std::function mqtt_callback_f; +typedef std::function wifi_callback_f; +typedef std::function ota_callback_f; +typedef std::function telnetcommand_callback_f; +typedef std::function telnet_callback_f; +typedef std::function fs_callback_f; +typedef std::function fs_settings_callback_f; + +// calculates size of an 2d array at compile time +template +constexpr size_t ArraySize(T (&)[N]) { + return N; +} + +// class definition +class MyESP { + public: + MyESP(); + ~MyESP(); + + // wifi + void setWIFICallback(void (*callback)()); + void setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback); + bool isWifiConnected(); + + // mqtt + bool isMQTTConnected(); + void mqttSubscribe(const char * topic); + void mqttUnsubscribe(const char * topic); + void mqttPublish(const char * topic, const char * payload); + void setMQTT(const char * mqtt_host, + const char * mqtt_username, + const char * mqtt_password, + const char * mqtt_base, + unsigned long mqtt_keepalive, + unsigned char mqtt_qos, + bool mqtt_retain, + const char * mqtt_will_topic, + const char * mqtt_will_online_payload, + const char * mqtt_will_offline_payload, + mqtt_callback_f callback); + + // OTA + void setOTA(ota_callback_f OTACallback); + + // debug & telnet + void myDebug(const char * format, ...); + void myDebug_P(PGM_P format_P, ...); + void setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback); + bool getUseSerial(); + + // FS + void setSettings(fs_callback_f callback, fs_settings_callback_f fs_settings_callback); + bool fs_saveConfig(); + + // CRASH + void crashClear(); + void crashDump(); + void crashTest(uint8_t t); + + // general + void end(); + void loop(); + void begin(const char * app_hostname, const char * app_name, const char * app_version); + void setBoottime(const char * boottime); + void resetESP(); + uint16_t getSystemLoadAverage(); + int getWifiQuality(); + + + 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 * _mqttTopic(const char * topic); + char * _mqtt_host; + char * _mqtt_username; + char * _mqtt_password; + char * _mqtt_base; + unsigned long _mqtt_keepalive; + unsigned char _mqtt_qos; + bool _mqtt_retain; + char * _mqtt_will_topic; + char * _mqtt_will_online_payload; + char * _mqtt_will_offline_payload; + char * _mqtt_topic; + unsigned long _mqtt_last_connection; + bool _mqtt_connecting; + + // wifi + DNSServer dnsServer; // For Access Point (AP) support + void _wifiCallback(justwifi_messages_t code, char * parameter); + void _wifi_setup(); + wifi_callback_f _wifi_callback; + char * _wifi_ssid; + char * _wifi_password; + bool _wifi_connected; + + // ota + ota_callback_f _ota_callback; + void _ota_setup(); + void _OTACallback(); + + // telnet & debug + TelnetSpy SerialAndTelnet; + void _telnetConnected(); + void _telnetDisconnected(); + void _telnetHandle(); + void _telnetCommand(char * commandLine); + char * _telnet_readWord(); + void _telnet_setup(); + char _command[TELNET_MAX_COMMAND_LENGTH]; // 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(); + telnetcommand_callback_f _telnetcommand_callback; // Callable for projects commands + telnet_callback_f _telnet_callback; // callback for connect/disconnect + void _changeSetting(uint8_t wc, const char * setting, const char * value); + void _changeSetting2(const char * setting, const char * value1, const char * value2); + + // fs + void _fs_setup(); + bool _fs_loadConfig(); + void _fs_printConfig(); + void _fs_eraseConfig(); + + fs_callback_f _fs_callback; + fs_settings_callback_f _fs_settings_callback; + + // general + char * _app_hostname; + char * _app_name; + char * _app_version; + char * _boottime; + bool _suspendOutput; + bool _use_serial; + void _printBuildTime(unsigned long rawTime); + + // load average (0..100) + void _calculateLoad(); + unsigned short int _load_average; +}; + +extern MyESP myESP; + +#endif diff --git a/platformio.ini-example b/platformio.ini-example index fdbbeb257..3f36aa23f 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -4,8 +4,11 @@ env_default = d1_mini [common] platform = espressif8266 flash_mode = dout -build_flags = -g -w -;build_flags = -g -w -DBUILD_TIME=$UNIX_TIME + +build_flags_debug = -ggdb3 -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable +;build_flags_prod = -Os -DBUILD_TIME=$UNIX_TIME + +build_flags = ${common.build_flags_debug} wifi_settings = ; hard code if you prefer. Recommendation is to set from within the app when in Serial or AP mode @@ -17,7 +20,6 @@ lib_deps = JustWifi AsyncMqttClient ArduinoJson -; https://github.com/bblanchon/ArduinoJson#v5.13.5 OneWire [env:d1_mini] @@ -32,6 +34,6 @@ monitor_speed = 115200 ; for OTA comment out these sections ;upload_protocol = espota -;upload_port = ems-esp.local +;upload_port = diff --git a/rename_fw.py b/rename_fw.py new file mode 100644 index 000000000..7b4d42299 --- /dev/null +++ b/rename_fw.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +from subprocess import call +import os +Import("env") + +# 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']) diff --git a/src/ems-esp.ino b/src/ems-esp.ino index 1c1b733c0..a9f450476 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.ino @@ -993,7 +993,7 @@ void initEMSESP() { // call PublishValues without forcing, so using CRC to see if we really need to publish void do_publishValues() { // don't publish if we're not connected to the EMS bus - if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + if ((ems_getBusConnected()) && (!myESP.getUseSerial()) && myESP.isMQTTConnected()) { publishValues(false); } } diff --git a/src/ems.cpp b/src/ems.cpp index 9a448f76f..013fe1cea 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -25,6 +25,7 @@ CircularBuffer<_EMS_TxTelegram, EMS_TX_TELEGRAM_QUEUE_MAX> EMS_TxQueue; // FIFO // generic void _process_Version(uint8_t type, uint8_t * data, uint8_t length); +void _printMessage(uint8_t * telegram, uint8_t length); // Boiler and Buderus devices void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length); @@ -61,8 +62,6 @@ void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length); void _process_RC1010StatusMessage(uint8_t type, uint8_t * data, uint8_t length); void _process_RC1010SetMessage(uint8_t type, uint8_t * data, uint8_t length); -//debug messages -void _printMessage(uint8_t * telegram, uint8_t length); /* * Recognized EMS types and the functions they call to process the telegrams * Format: MODEL ID, TYPE ID, Description, function @@ -247,7 +246,7 @@ void ems_init() { strlcpy(EMS_Thermostat.version, "not set", sizeof(EMS_Thermostat.version)); // default logging is none - ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); + ems_setLogging(EMS_SYS_LOGGING_DEFAULT); } // Getters and Setters for parameters @@ -791,6 +790,40 @@ void _printMessage(uint8_t * telegram, uint8_t length) { _debugPrintTelegram(output_str, telegram, length, color_s); } } + + // see if we recognize the type first by scanning our known EMS types list + // trying to match the type ID + bool commonType = false; + bool typeFound = false; + bool forUs = false; + int i = 0; + + while (i < _EMS_Types_max) { + if (EMS_Types[i].type == type) { + typeFound = true; + commonType = (EMS_Types[i].model_id == EMS_MODEL_ALL); // is it common type for everyone? + forUs = (src == EMS_Boiler.type_id) || (src == EMS_Thermostat.type_id); // is it for us? So the src must match + break; + } + i++; + } + + // if it's a common type (across ems devices) or something specifically for us process it. + // dest will be EMS_ID_NONE and offset 0x00 for a broadcast message + if (typeFound && (commonType || forUs)) { + if ((EMS_Types[i].processType_cb) != (void *)NULL) { + // print non-verbose message + if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) || (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE)) { + myDebug("<--- %s(0x%02X) received", EMS_Types[i].typeString, type); + } + // call callback function to process it + // as we only handle complete telegrams (not partial) check that the offset is 0 + if (offset == EMS_ID_NONE) { + (void)EMS_Types[i].processType_cb(type, data, length - 5); + } + } + } + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; } /** @@ -1054,10 +1087,8 @@ void _process_RC20StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { } /** - * type 0x41 - data from the RC30 thermostat (0x10) - 14 bytes long - * For reading the temp values only - * received every 60 seconds - */ + *type 0x41 - data from the RC30 thermostat(0x10) - 14 bytes long * For reading the temp values only * received every 60 seconds +*/ void _process_RC30StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC30StatusMessage_setpoint]) / (float)2; EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC30StatusMessage_curr, data); @@ -1090,8 +1121,7 @@ void _process_RC35StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length) { EMS_Thermostat.curr_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_curr] << 8) + data[9]))) / 100; EMS_Thermostat.setpoint_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_setpoint] << 8) + data[11]))) / 100; - - EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } /** * type 0x00 - data from the Nefit RC1010 thermostat (0x18) - 24 bytes long @@ -1580,7 +1610,7 @@ void ems_printAllTypes() { */ void ems_doReadCommand(uint8_t type, uint8_t dest, bool forceRefresh) { // if not a valid type of boiler is not accessible then quits - if (type == EMS_ID_NONE) { + if ((type == EMS_ID_NONE) || (dest == EMS_ID_NONE)) { return; } @@ -1887,4 +1917,4 @@ void ems_setWarmTapWaterActivated(bool activated) { } EMS_TxQueue.push(EMS_TxTelegram); // add to queue -} \ No newline at end of file +} diff --git a/src/ems_devices.h b/src/ems_devices.h index 7132d50e5..d6ce66f7f 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -108,7 +108,8 @@ typedef enum { EMS_MODEL_BOSCHEASY, EMS_MODEL_RC310, EMS_MODEL_CW100, - EMS_MODEL_RC1010 + EMS_MODEL_RC1010, + EMS_MODEL_OT } _EMS_MODEL_ID; @@ -147,4 +148,5 @@ const _Thermostat_Type Thermostat_Types[] = { {EMS_MODEL_RC310, 158, 0x10, "RC310", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_CW100, 255, 0x18, "Bosch CW100", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_RC1010, 165, 0x18, "RC1010/Nefit Moduline 1010)", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_OT, 171, 0x02, "EMS-OT OpenTherm converter", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, }; diff --git a/src/version.h b/src/version.h index c078e0432..44312b32f 100644 --- a/src/version.h +++ b/src/version.h @@ -5,6 +5,6 @@ #pragma once -#define APP_NAME "EMS-ESP" -#define APP_VERSION "1.5.6" -#define APP_HOSTNAME "ems-esp" +#define APP_NAME "EMS-HEERENVEEN-1" +#define APP_VERSION "1.5.7b" +#define APP_HOSTNAME "ems-heerenveen-1"