diff --git a/.gitignore b/.gitignore index e93465a4..07428130 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ platformio.ini lib/readme.txt .travis.yml -*.py +stackdmp.txt +*.jar \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f059cb5f..12c35617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ 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). +## [1.6.0] 2019-03-24 + +### Added + +- `system` command to show ESP8266 stats +- `crash` command to see stack of last system crash, with .py files to track stack dump (compile with `-DCRASH`) +- publish dallas external temp sensors to MQTT (thanks @JewelZB) +- shower timer and shower alert options available via set commands +- added support for warm water modes Hot, Comfort and Intelligent [(issue 67)](https://github.com/proddy/EMS-ESP/issues/67) +- added `set publish_time` to set how often to publish MQTT +- support for SM10 Solar Module including MQTT [(issue 77)](https://github.com/proddy/EMS-ESP/issues/77) +- `refresh` command to force a fetch of all known data from the connected EMS devices + +### Fixed + +- incorrect rendering of null temperature values (the -3200 degrees issue) +- OTA is more stable +- Added a hack to overcome WiFi power issues in arduino core 2.5.0 libraries causing constant wifi re-connects +- Performance issues with telnet output + +### Changed + +- included various fixes and suggestions from @nomis +- upgraded MyESP library with many optimizations +- `test_mode` renamed to `silent_mode` +- `set wifi` replaced with `set wifi_ssid` and `set wifi_password` to allow values with spaces +- EMS values are stored in the raw format and only converted to strings when displayed or published, removing the need for parsing floats +- All floating point temperatures are to one decimal place [(issue 79)](https://github.com/proddy/EMS-ESP/issues/79) + ## [1.5.6] 2019-03-09 ### Added @@ -16,7 +45,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - upgraded MyESP library - minor changes - ## [1.5.5] 2019-03-07 ### Fixed diff --git a/README.md b/README.md index f9aad9cf..d54127a5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ EMS-ESP is a project to build an electronic controller circuit using an Espressi There are 3 parts to this project, first the design of the circuit, secondly the code for the ESP8266 microcontroller firmware with telnet and MQTT support, and lastly an example configuration for Home Assistant to monitor the data and issue direct commands via a MQTT broker. [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b8880625bdf841d4adb2829732030887)](https://app.codacy.com/app/proddy/EMS-ESP?utm_source=github.com&utm_medium=referral&utm_content=proddy/EMS-ESP&utm_campaign=Badge_Grade_Settings) -[![version](https://img.shields.io/badge/version-1.5.5-brightgreen.svg)](CHANGELOG.md) +[![version](https://img.shields.io/badge/version-1.6.0-brightgreen.svg)](CHANGELOG.md) - [EMS-ESP](#ems-esp) - [Introduction](#introduction) @@ -30,7 +30,6 @@ There are 3 parts to this project, first the design of the circuit, secondly the - [Home Assistant Configuration](#home-assistant-configuration) - [Building The Firmware](#building-the-firmware) - [Using PlatformIO Standalone](#using-platformio-standalone) - - [Building Using Arduino IDE](#building-using-arduino-ide) - [Using the Pre-built Firmware](#using-the-pre-built-firmware) - [Troubleshooting](#troubleshooting) - [Known Issues](#known-issues) @@ -64,14 +63,14 @@ The code and circuit has been tested with a few ESP8266 development boards such 1. Either build the circuit described below or purchase a ready built board from bbqkees. 2. Grab any ESP8266 dev board. The latest bbqkees boards have a Wemos D1 pre-mounted with a copy of this firmware. -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. +3. Optionally add external Dallas temperature sensors (to D1) and an external LED (to D5). +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 via USB. +5. Connect an external USB 5v power adapter to the ESP8266 board. +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 and receiving data. 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. +9. Next is to customize some of the onboard settings. Type `set` to list the current stored settings and `?` to see the syntax. Use `set wifi_ssid` and `set wifi_password` to add your WiFi credentials and if you're using MQTT set the host, username and password. There is no need to reboot the ESP. 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. +11. **Important**: By default the serial port is enabled and the EMS bus disabled. This is to allow users to configure their ESP via the serial monitor when pluged into a PC/laptop. You must disable serial with `set serial off` to get the EMS transmission working. 12. Hook up the ESP to the EMS board as follows: | EMS board | ESP8266 dev board | @@ -79,11 +78,11 @@ The code and circuit has been tested with a few ESP8266 development boards such | 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. +| VC/J2 | 3v3 | +13. Connect the EMS lines to the ESP. This can be done via the two EMS wires or via the 3.5mm 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. +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 use `info` to check the status. +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 @@ -127,8 +126,8 @@ The EMS circuit will work with both 3.3V and 5V. It's easiest though to power di - via the USB if your dev board has one - using an external 5V power supply into the 5V vin on the board -- 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. +- powering from the 3.5mm service jack (stereo 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 direct from the EMS line, which is 15V DC and using a buck converter as described above. | With Power Circuit | | ------------------------------------------ | @@ -203,13 +202,15 @@ Every telegram sent is echo'd back to Rx, along the same Bus used for all Rx/Tx `ems.cpp` is the logic to read the EMS data packets (telegrams), validates them and process them based on the type. -`ems-esp.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. +`ems-esp.cpp` 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. `my_config.h` has all the custom settings tailored to your environment. Specific values here are also stored in the ESP's SPIFFs (File system). `ems_devices.h` has all the configuration for the known EMS devices currently supported. -`MyESP.cpp` is my custom library to handle WiFi, MQTT and Telnet. Uses a modified version of [TelnetSpy](https://github.com/yasheena/telnetspy) +`MyESP.cpp` is my custom library to handle WiFi, MQTT and Telnet. Uses a modified version of [TelnetSpy](https://github.com/yasheena/telnetspy). + +`ds18.*` are the Dallas libraries for any external temperature sensors. ### Special EMS Types @@ -228,18 +229,15 @@ In `ems.cpp` you can add scheduled calls to specific EMS types in the functions I am still working on adding more support to known thermostats. Any contributions here are welcome. The know types are listed in `ems_devices.h` and include -- RC20 and RC30, both are fully supported -- RC10 support is being added +- RC10, RC20 and RC30 are fully supported - RC35 with support for the 1st heating circuit (HC1) -- TC100/TC200/Easy but only with support for reading the temperatures. There seems to be no way to set settings using EMS bus messages that I know of. One option is to send XMPP messages but a special server is needed and out of scope for this project. +- TC100/TC200/Easy but only with support for *reading* the temperature values. There seems to be no way to set settings using EMS bus messages that I know of. One option is to send XMPP messages but a special server is needed and out of scope for this project. ### Customizing The Code -- To configure for your thermostat and specific boiler settings, modify `my_config.h`. Here you can - - set flags for enabled/disabling functionality such as `BOILER_SHOWER_ENABLED` and `BOILER_SHOWER_TIMER`. - - Set WIFI and MQTT settings. The values can also be set from the telnet command menu using the **set** command. -- 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` -- To add new devices modify `ems_devices.h` +- To configure for your thermostat and specific boiler settings, modify `my_config.h`. +- Most values can also be set from the telnet command menu using the **set** command. +- 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`. Also add to `ems_devices.h`. ### Using MQTT @@ -302,6 +300,7 @@ Make sure Python 2.7 is installed, then... % pip install -U platformio % sudo platformio upgrade % platformio platform update +% platformio lib upgrade % git clone https://github.com/proddy/EMS-ESP.git % cd EMS-ESP @@ -312,17 +311,6 @@ edit `platformio.ini` to set `env_default` to your board type, then % platformio run -t upload ``` -### Building Using Arduino IDE - -Porting to the Arduino IDE can be a little tricky but it did it once. Something along these lines: - -- 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. Make sure your board supports SPIFFS. -- Select your ESP8266 from Tools->Boards and the correct port with Tools->Port -- From the Library Manager install the needed libraries from platformio.ini. Note make sure you pick ArduinoJson v5 (5.13.4 and above) and not v6. See https://arduinojson.org/v5/doc/ -- Put all the files in a single sketch folder -- cross your fingers and hit CTRL-R to compile - ## Using the Pre-built Firmware pre-baked firmware for the Wemos D1 mini is available in the GitHub [releases](https://github.com/proddy/EMS-ESP/releases) which you can upload yourself using the [esptool](https://github.com/espressif/esptool) bootloader like `esptool.py -p write_flash 0x00000 `. Here's how to set it up on Windows: @@ -330,7 +318,7 @@ pre-baked firmware for the Wemos D1 mini is available in the GitHub [releases](h 1. Check if you have **python 2.7** installed. If not [download it](https://www.python.org/downloads/) and make sure you select the option to add Python to the windows PATH 2. Then install the ESPTool by running `pip install esptool` from a command prompt -The ESP8266 will start in Access Point (AP) mode. Connect via WiFi to the SSID **EMS-ESP** and telnet to **192.168.4.1**. Then use the `set wifi` command to configure your own network settings like `set wifi your_ssid your_password`. Alternatively connect the ESP8266 to your PC and open a Serial monitor (with baud 115200) to configure the settings. Make sure you disable Serial support before connecting the EMS lines using `set serial off`. +The ESP8266 will start in Access Point (AP) mode. Connect via WiFi to the SSID **EMS-ESP** and telnet to **192.168.4.1**. Then use the `set wifi_ssid/set wifi_password` command to configure your own network settings. Alternatively connect the ESP8266 to your PC and open a Serial monitor (with baud 115200) to configure the settings. Make sure you disable Serial support before connecting the EMS lines using `set serial off`. `set` wil list all currently stored settings. diff --git a/checkcode.py b/checkcode.py new file mode 100644 index 00000000..53da6305 --- /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 00000000..140f23ee --- /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 00000000..c54d0590 --- /dev/null +++ b/debug.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +from subprocess import call +import os + +# 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/telnet/telnet_menu.jpg b/doc/telnet/telnet_menu.jpg index 9355e493..563fdd8b 100644 Binary files a/doc/telnet/telnet_menu.jpg and b/doc/telnet/telnet_menu.jpg differ diff --git a/doc/telnet/telnet_stats.PNG b/doc/telnet/telnet_stats.PNG index 892d66f7..2e13c456 100644 Binary files a/doc/telnet/telnet_stats.PNG and b/doc/telnet/telnet_stats.PNG differ diff --git a/lib/myESP/MyESP.cpp b/lib/myESP/MyESP.cpp index 566a0c88..0072d382 100644 --- a/lib/myESP/MyESP.cpp +++ b/lib/myESP/MyESP.cpp @@ -1,5 +1,5 @@ /* - * MyESP - my ESP helper class to handle Wifi, MQTT and Telnet + * MyESP - my ESP helper class to handle WiFi, MQTT and Telnet * * Paul Derbyshire - December 2018 * @@ -8,13 +8,9 @@ #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 */ -}; +#ifdef CRASH +EEPROM_Rotate EEPROMr; +#endif // constructor MyESP::MyESP() { @@ -22,12 +18,14 @@ MyESP::MyESP() { _app_name = strdup("MyESP"); _app_version = strdup(MYESP_VERSION); - _boottime = strdup(""); + _boottime = NULL; _load_average = 100; // calculated load average _telnetcommand_callback = NULL; _telnet_callback = NULL; + _command[0] = '\0'; + _fs_callback = NULL; _fs_settings_callback = NULL; @@ -55,6 +53,9 @@ MyESP::MyESP() { _wifi_callback = NULL; _wifi_connected = false; + _ota_pre_callback = NULL; + _ota_post_callback = NULL; + _suspendOutput = false; } @@ -88,7 +89,6 @@ void MyESP::myDebug(const char * format, ...) { delete[] buffer; } - // for flashmemory. Must use PSTR() void MyESP::myDebug_P(PGM_P format_P, ...) { if (_suspendOutput) @@ -107,10 +107,12 @@ void MyESP::myDebug_P(PGM_P format_P, ...) { va_end(args); +#ifdef MYESP_TIMESTAMP // capture & print timestamp char timestamp[10] = {0}; snprintf_P(timestamp, sizeof(timestamp), PSTR("[%06lu] "), millis() % 1000000); SerialAndTelnet.print(timestamp); +#endif SerialAndTelnet.println(buffer); @@ -152,26 +154,38 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { // finally if we don't want Serial anymore, turn it off if (!_use_serial) { - Serial.println("Disabling serial port"); + myDebug_P(PSTR("Disabling serial port")); Serial.flush(); Serial.end(); SerialAndTelnet.setSerial(NULL); } else { - Serial.println("Using serial port output"); + myDebug_P(PSTR("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) { + _wifi_connected = true; + 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()); + // we could be in panic mode so enable Serial again + if (!_use_serial) { + SerialAndTelnet.setSerial(&Serial); + _use_serial = true; + } + + myDebug_P(PSTR("Enabling serial port output")); + // call any final custom settings if (_wifi_callback) { _wifi_callback(); @@ -194,6 +208,12 @@ void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { } } +// return true if in WiFi AP mode +// does not work after wifi reset on ESP32 yet. See https://github.com/espressif/arduino-esp32/issues/1306 +bool MyESP::isAPmode() { + return (WiFi.getMode() & WIFI_AP); +} + // 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) { @@ -263,9 +283,8 @@ void MyESP::_mqtt_setup() { 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 + 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")); @@ -297,7 +316,7 @@ void MyESP::_mqtt_setup() { // WiFI setup void MyESP::_wifi_setup() { - jw.setHostname(_app_hostname); // Set WIFI hostname (otherwise it would be ESP-XXXXXX) + jw.setHostname(_app_hostname); // Set WIFI hostname jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); }); jw.enableAP(false); jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); @@ -307,19 +326,39 @@ void MyESP::_wifi_setup() { 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 + +#if defined(ESP8266) + WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0 +#endif } // set the callback function for the OTA onstart -void MyESP::setOTA(ota_callback_f OTACallback) { - _ota_callback = OTACallback; +void MyESP::setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post) { + _ota_pre_callback = OTACallback_pre; + _ota_post_callback = OTACallback_post; } // 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 + +#ifdef CRASH + // If we are not specifically reserving the sectors we are using as + // EEPROM in the memory layout then any OTA upgrade will overwrite + // all but the last one. + // Calling rotate(false) disables rotation so all writes will be done + // to the last sector. It also sets the dirty flag to true so the next commit() + // will actually persist current configuration to that last sector. + // Calling rotate(false) will also prevent any other EEPROM write + // to overwrite the OTA image. + // In case the OTA process fails, reenable rotation. + // See onError callback below. + EEPROMr.rotate(false); + EEPROMr.commit(); +#endif + + if (_ota_pre_callback) { + (_ota_pre_callback)(); // call custom function } } @@ -354,6 +393,11 @@ void MyESP::_ota_setup() { myDebug_P(PSTR("[OTA] Receive Failed")); else if (error == OTA_END_ERROR) myDebug_P(PSTR("[OTA] End Failed")); + +#ifdef CRASH + // There's been an error, reenable rotation + EEPROMr.rotate(true); +#endif }); } @@ -365,6 +409,14 @@ void MyESP::setBoottime(const char * boottime) { _boottime = strdup(boottime); } +// eeprom +void MyESP::_eeprom_setup() { +#ifdef CRASH + EEPROMr.size(4); + EEPROMr.begin(SPI_FLASH_SEC_SIZE); +#endif +} + // 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 @@ -400,133 +452,114 @@ void MyESP::_telnet_setup() { 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 */ - uint8_t WeekDay = (unix + 3) % 7 + 1; /* Get week day, monday is first day */ - - 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(); + myDebug_P(PSTR("")); + myDebug_P(PSTR("* Connected to: %s version %s"), _app_name, _app_version); - if (WiFi.getMode() & WIFI_AP) { - SerialAndTelnet.printf("* ESP is in AP mode with SSID %s", jw.getAPSSID().c_str()); - SerialAndTelnet.println(); + if (isAPmode()) { + myDebug_P(PSTR("* Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); } 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); + myDebug_P(PSTR("* Hostname: %s (%s)"), _getESPhostname().c_str(), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("* WiFi SSID: %s (signal %d%%)"), WiFi.SSID().c_str(), getWifiQuality()); + myDebug_P(PSTR("* MQTT is %s"), mqttClient.connected() ? "connected" : "disconnected"); + } -#ifdef BUILD_TIME - SerialAndTelnet.print(" (Build "); - _printBuildTime(BUILD_TIME); - SerialAndTelnet.print(")"); + myDebug_P(PSTR("*")); + myDebug_P(PSTR("* Commands:")); + myDebug_P(PSTR("* ?=help, CTRL-D=quit telnet")); + myDebug_P(PSTR("* set, system, reboot")); +#ifdef CRASH + myDebug_P(PSTR("* crash ")); #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() + // 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) && (!_helpProjectCmds[i].set)) { + max_len = strlen(_helpProjectCmds[i].key); + } + } + + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if (!_helpProjectCmds[i].set) { + 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)); + } + } + } + myDebug_P(PSTR("")); // newline +} - SerialAndTelnet.println(FPSTR("*")); - SerialAndTelnet.println(FPSTR("* Commands:")); - SerialAndTelnet.println(FPSTR("* ?=help, CTRL-D=quit")); - SerialAndTelnet.println(FPSTR("* reboot")); - 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 all set commands and current values +void MyESP::_printSetCommands() { + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("The following set commands are available:")); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("* set erase")); + myDebug_P(PSTR("* set [value]")); + myDebug_P(PSTR("* set [value]")); + myDebug_P(PSTR("* 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) + if ((strlen(_helpProjectCmds[i].key) > max_len) && (_helpProjectCmds[i].set)) { 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 + if (_helpProjectCmds[i].set) { + SerialAndTelnet.print(FPSTR("* set ")); + 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(FPSTR(_helpProjectCmds[i].description)); } } - SerialAndTelnet.println(); // newline + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR("Stored settings:")); + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR(" wifi_ssid=%s "), (!_wifi_ssid) ? "" : _wifi_ssid); + SerialAndTelnet.print(FPSTR(" wifi_password=")); + if (!_wifi_password) { + SerialAndTelnet.print(FPSTR("")); + } else { + for (uint8_t i = 0; i < strlen(_wifi_password); i++) { + SerialAndTelnet.print(FPSTR("*")); + } + } + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR(" mqtt_host=%s"), (!_mqtt_host) ? "" : _mqtt_host); + myDebug_P(PSTR(" mqtt_username=%s"), (!_mqtt_username) ? "" : _mqtt_username); + SerialAndTelnet.print(FPSTR(" mqtt_password=")); + if (!_mqtt_password) { + SerialAndTelnet.print(FPSTR("")); + } else { + for (uint8_t i = 0; i < strlen(_mqtt_password); i++) { + SerialAndTelnet.print(FPSTR("*")); + } + } + + myDebug_P(PSTR("")); // newline + myDebug_P(PSTR(" serial=%s"), (_use_serial) ? "on" : "off"); + + // print any custom settings + (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); + + myDebug_P(PSTR("")); // newline } // reset / restart @@ -541,54 +574,47 @@ void MyESP::resetESP() { } // 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); +// if parameter true then a word is only terminated by a newline +char * MyESP::_telnet_readWord(bool allow_all_chars) { + if (allow_all_chars) { + return (strtok(NULL, "\n")); // allow only newline + } else { + return (strtok(NULL, ", \n")); // allow space and comma } } // 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 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 + return true; + + } else if (strcmp(setting, "wifi_ssid") == 0) { if (_wifi_ssid) free(_wifi_ssid); + _wifi_ssid = NULL; // just to be sure + if (value) { + _wifi_ssid = strdup(value); + } + ok = true; + jw.enableSTA(false); + myDebug_P(PSTR("Note: please reboot to apply new WiFi settings")); + } else if (strcmp(setting, "wifi_password") == 0) { if (_wifi_password) free(_wifi_password); - _wifi_ssid = NULL; - _wifi_password = NULL; - ok = true; + _wifi_password = NULL; // just to be sure + if (value) { + _wifi_password = strdup(value); + } + ok = true; + jw.enableSTA(false); + myDebug_P(PSTR("Note: please reboot to apply new WiFi settings")); + } else if (strcmp(setting, "mqtt_host") == 0) { if (_mqtt_host) free(_mqtt_host); @@ -613,6 +639,7 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) _mqtt_password = strdup(value); } ok = true; + } else if (strcmp(setting, "serial") == 0) { ok = true; _use_serial = false; @@ -620,9 +647,11 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) if (strcmp(value, "on") == 0) { _use_serial = true; ok = true; + myDebug_P(PSTR("Reboot ESP to activate Serial mode.")); } else if (strcmp(value, "off") == 0) { _use_serial = false; ok = true; + myDebug_P(PSTR("Reboot ESP to deactivate Serial mode.")); } else { ok = false; } @@ -632,28 +661,30 @@ void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) ok = (_fs_settings_callback)(MYESP_FSACTION_SET, wc, setting, value); } - if (!ok) { - SerialAndTelnet.println("\nInvalid parameter for set command."); - return; - } + // if we were able to recognize the set command, continue + if (ok) { + // check for 2 params + if (value == nullptr) { + myDebug_P(PSTR("%s setting reset to its default value."), setting); + } else { + // must be 3 params + myDebug_P(PSTR("%s changed."), setting); + } - // 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); + myDebug_P(PSTR("")); // newline + + (void)fs_saveConfig(); // always save the values } - SerialAndTelnet.println(); - (void)fs_saveConfig(); + return ok; } void MyESP::_telnetCommand(char * commandLine) { + char * str = commandLine; + bool state = false; + // count the number of arguments - char * str = commandLine; - bool state = false; - unsigned wc = 0; + unsigned wc = 0; while (*str) { if (*str == ' ' || *str == '\n' || *str == '\t') { state = false; @@ -665,57 +696,28 @@ void MyESP::_telnetCommand(char * commandLine) { } // check first for reserved commands - char * temp = strdup(commandLine); // because strotok kills original string buffer - char * ptrToCommandName = strtok((char *)temp, ", \n"); + char * temp = strdup(commandLine); // because strotok kills original string buffer + char * ptrToCommandName = strtok((char *)temp, " \n"); // space and newline // set command if (strcmp(ptrToCommandName, "set") == 0) { + bool ok = false; 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("*"); - } + _printSetCommands(); + ok = true; + } else if (wc == 2) { // set + char * setting = _telnet_readWord(false); + ok = _changeSetting(wc - 1, setting, NULL); + } else { // set + char * setting = _telnet_readWord(false); + char * value = _telnet_readWord(true); // allow strange characters + ok = _changeSetting(wc - 1, setting, value); + } - 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); + if (!ok) { + myDebug_P(PSTR("\nInvalid parameter for set command.")); } + return; } @@ -724,10 +726,170 @@ void MyESP::_telnetCommand(char * commandLine) { resetESP(); } + // show system stats + if ((strcmp(ptrToCommandName, "system") == 0) && (wc == 1)) { + showSystemStats(); + return; + } + +// crash command +#ifdef CRASH + if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) { + char * cmd = _telnet_readWord(false); + 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(false); + crashTest(atoi(value)); + } + return; // don't call custom command line callback + } +#endif + // call callback function (_telnetcommand_callback)(wc, commandLine); } +// returns WiFi hostname as a String object +String MyESP::_getESPhostname() { + String hostname; + +#if defined(ARDUINO_ARCH_ESP32) + hostname = String(WiFi.getHostname()); +#else + hostname = WiFi.hostname(); +#endif + + return (hostname); +} + +// returns build time as a String - copied for espurna. see (c) +// takes the time from the gcc during compilation +String MyESP::_buildTime() { + const char time_now[] = __TIME__; // hh:mm:ss + unsigned int hour = atoi(&time_now[0]); + unsigned int minute = atoi(&time_now[3]); + unsigned int second = atoi(&time_now[6]); + + const char date_now[] = __DATE__; // Mmm dd yyyy + const char * months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + unsigned int month = 0; + for (int i = 0; i < 12; i++) { + if (strncmp(date_now, months[i], 3) == 0) { + month = i + 1; + break; + } + } + unsigned int day = atoi(&date_now[3]); + unsigned int year = atoi(&date_now[7]); + + char buffer[20]; + snprintf_P(buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"), year, month, day, hour, minute, second); + + return String(buffer); +} + +// returns system uptime in seconds - copied for espurna. see (c) +unsigned long MyESP::_getUptime() { + static unsigned long last_uptime = 0; + static unsigned char uptime_overflows = 0; + + if (millis() < last_uptime) + ++uptime_overflows; + last_uptime = millis(); + unsigned long uptime_seconds = uptime_overflows * (UPTIME_OVERFLOW / 1000) + (last_uptime / 1000); + + return uptime_seconds; +} + +// print out ESP system stats +// for battery power is ESP.getVcc() +void MyESP::showSystemStats() { +#if defined(ESP8266) + myDebug_P(PSTR("%sESP8266 System stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF); +#else + myDebug_P(PSTR("ESP32 System stats:")); +#endif + myDebug_P(PSTR("")); + + myDebug_P(PSTR(" [APP] %s version: %s"), _app_name, _app_version); + myDebug_P(PSTR(" [APP] MyESP version: %s"), MYESP_VERSION); + myDebug_P(PSTR(" [APP] Build timestamp: %s"), _buildTime().c_str()); + if (_boottime != NULL) { + myDebug_P(PSTR(" [APP] Boot time: %s"), _boottime); + } + uint32_t t = _getUptime(); // seconds + uint32_t h = (uint32_t)t / (uint32_t)3600L; + uint32_t rem = (uint32_t)t % (uint32_t)3600L; + uint32_t m = rem / 60; + uint32_t s = rem % 60; + myDebug_P(PSTR(" [APP] Uptime: %d seconds (%02d:%02d:%02d)"), t, h, m, s); + myDebug_P(PSTR(" [APP] System Load: %d%%"), getSystemLoadAverage()); + + if (isAPmode()) { + myDebug_P(PSTR(" [WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str()); + } else { + myDebug_P(PSTR(" [WIFI] WiFi Hostname: %s"), _getESPhostname().c_str()); + myDebug_P(PSTR(" [WIFI] WiFi IP: %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR(" [WIFI] WiFi signal strength: %d%%"), getWifiQuality()); + } + + myDebug_P(PSTR(" [WIFI] WiFi MAC: %s"), WiFi.macAddress().c_str()); + +#ifdef CRASH + char output_str[80] = {0}; + char buffer[16] = {0}; + /* Crash info */ + myDebug_P(PSTR(" [EEPROM] EEPROM size: %u"), EEPROMr.reserved() * SPI_FLASH_SEC_SIZE); + strlcpy(output_str, PSTR(" [EEPROM] EEPROM Sector pool size is "), sizeof(output_str)); + strlcat(output_str, itoa(EEPROMr.size(), buffer, 10), sizeof(output_str)); + strlcat(output_str, PSTR(", and in use are: "), sizeof(output_str)); + for (uint32_t i = 0; i < EEPROMr.size(); i++) { + strlcat(output_str, itoa(EEPROMr.base() - i, buffer, 10), sizeof(output_str)); + strlcat(output_str, PSTR(" "), sizeof(output_str)); + } + myDebug_P(output_str); +#endif + +#ifdef ARDUINO_BOARD + myDebug_P(PSTR(" [SYSTEM] Board: %s"), ARDUINO_BOARD); +#endif + + myDebug_P(PSTR(" [SYSTEM] CPU frequency: %u MHz"), ESP.getCpuFreqMHz()); + myDebug_P(PSTR(" [SYSTEM] SDK version: %s"), ESP.getSdkVersion()); + +#if defined(ESP8266) + myDebug_P(PSTR(" [SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId()); + myDebug_P(PSTR(" [SYSTEM] Core version: %s"), ESP.getCoreVersion().c_str()); + myDebug_P(PSTR(" [SYSTEM] Boot version: %d"), ESP.getBootVersion()); + myDebug_P(PSTR(" [SYSTEM] Boot mode: %d"), ESP.getBootMode()); + //myDebug_P(PSTR("[SYSTEM] Firmware MD5: %s"), (char *)ESP.getSketchMD5().c_str()); +#endif + + FlashMode_t mode = ESP.getFlashChipMode(); +#if defined(ESP8266) + myDebug_P(PSTR(" [FLASH] Flash chip ID: 0x%06X"), ESP.getFlashChipId()); +#endif + myDebug_P(PSTR(" [FLASH] Flash speed: %u Hz"), ESP.getFlashChipSpeed()); + myDebug_P(PSTR(" [FLASH] Flash mode: %s"), + mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN"); +#if defined(ESP8266) + myDebug_P(PSTR(" [FLASH] Flash size (CHIP): %d"), ESP.getFlashChipRealSize()); +#endif + myDebug_P(PSTR(" [FLASH] Flash size (SDK): %d"), ESP.getFlashChipSize()); + myDebug_P(PSTR(" [FLASH] Flash Reserved: %d"), 1 * SPI_FLASH_SEC_SIZE); + myDebug_P(PSTR(" [MEM] Firmware size: %d"), ESP.getSketchSize()); + myDebug_P(PSTR(" [MEM] Max OTA size: %d"), (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); + myDebug_P(PSTR(" [MEM] OTA Reserved: %d"), 4 * SPI_FLASH_SEC_SIZE); + myDebug_P(PSTR(" [MEM] Free Heap: %d"), ESP.getFreeHeap()); +#if defined(ESP8266) + myDebug_P(PSTR(" [MEM] Stack: %d"), ESP.getFreeContStack()); +#endif + myDebug_P(PSTR("")); +} + // handler for Telnet void MyESP::_telnetHandle() { SerialAndTelnet.handle(); @@ -737,7 +899,7 @@ void MyESP::_telnetHandle() { while (SerialAndTelnet.available()) { char c = SerialAndTelnet.read(); - SerialAndTelnet.serialPrint(c); // echo to Serial if connected + 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 @@ -753,9 +915,8 @@ void MyESP::_telnetHandle() { } break; - case '\b': // (^H) handle backspace in input: put a space in last char - coded by Simon Arlott + case '\b': // (^H) case 0x7F: // (^?) - if (charsRead > 0) { _command[--charsRead] = '\0'; @@ -938,7 +1099,6 @@ char * MyESP::_mqttTopic(const char * topic) { return _mqtt_topic; } - // print contents of file // assumes Serial is open void MyESP::_fs_printConfig() { @@ -946,14 +1106,14 @@ void MyESP::_fs_printConfig() { File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); if (!configFile) { - Serial.println(F("[FS] Failed to read file for printing")); + myDebug_P(PSTR("[FS] Failed to read file for printing")); return; } while (configFile.available()) { SerialAndTelnet.print((char)configFile.read()); } - SerialAndTelnet.println(); + myDebug_P(PSTR("")); // newline configFile.close(); } @@ -1001,6 +1161,7 @@ bool MyESP::_fs_loadConfig() { const char * value; + // fetch the standard system parameters value = json["wifi_ssid"]; _wifi_ssid = (value) ? strdup(value) : NULL; @@ -1029,6 +1190,13 @@ bool MyESP::_fs_loadConfig() { // save settings to spiffs bool MyESP::fs_saveConfig() { + bool ok = true; + + // call any custom functions before handling SPIFFS + if (_ota_pre_callback) { + (_ota_pre_callback)(); + } + StaticJsonDocument doc; JsonObject json = doc.to(); @@ -1045,43 +1213,49 @@ bool MyESP::fs_saveConfig() { // if file exists, remove it just to be safe if (SPIFFS.exists(MYEMS_CONFIG_FILE)) { - // delete it SPIFFS.remove(MYEMS_CONFIG_FILE); } + // open for writing File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "w"); if (!configFile) { - Serial.println("[FS] Failed to open config file for writing"); + myDebug_P(PSTR("[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")); + myDebug_P(PSTR("[FS] Failed to write config file")); + ok = false; } configFile.close(); - return true; + // call any custom functions before handling SPIFFS + if (_ota_post_callback) { + (_ota_post_callback)(); + } + + return ok; // it worked } // 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"); + myDebug_P(PSTR("[FS] Failed to mount the file system. Erasing...")); _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"); + //myDebug_P(PSTR("[FS] Re-creating config file")); fs_saveConfig(); } - //_fs_printConfig(); // TODO: for debugging + // _fs_printConfig(); // enable for debugging } uint16_t MyESP::getSystemLoadAverage() { @@ -1108,6 +1282,11 @@ void MyESP::_calculateLoad() { } } +// 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, @@ -1143,13 +1322,160 @@ int MyESP::getWifiQuality() { return 2 * (dBm + 100); } -// register new instance +#ifdef CRASH +/** + * 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) { + // write crash time to EEPROM + uint32_t crash_time = millis(); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + + // write reset info to EEPROM + EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); + EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); + + // write epc1, epc2, epc3, excvaddr and depc to EEPROM + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); + + // write stack start and end address to EEPROM + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + + // 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++) { + byte * byteValue = (byte *)i; + EEPROMr.write(current_address++, *byteValue); + } + + EEPROMr.commit(); +} + +void MyESP::crashTest(uint8_t t) { + if (t == 1) { + myDebug("[CRASH] Attempting to divide by zero ..."); + int result, zero; + zero = 0; + result = 1 / zero; + myDebug("Result = %d", result); + } + + if (t == 2) { + myDebug("[CRASH] 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("[CRASH] 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("[CRASH] 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() { + myDebug_P(PSTR("[CRASH] Clearing crash dump")); + uint32_t crash_time = 0xFFFFFFFF; + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); + EEPROMr.commit(); +} + +/** + * Print out crash information that has been previously saved in EEPROM + */ +void MyESP::crashDump() { + uint32_t crash_time; + EEPROMr.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"), EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON)); + myDebug_P(PSTR("[CRASH] Exception cause: %u"), EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE)); + + uint32_t epc1, epc2, epc3, excvaddr, depc; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr); + EEPROMr.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; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); + EEPROMr.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++) { + EEPROMr.get(current_address, stack_trace); + SerialAndTelnet.printf("%08x ", stack_trace); + current_address += 4; + } + SerialAndTelnet.println(); + } + myDebug("<< #include @@ -19,6 +19,13 @@ #include // https://github.com/xoseperez/justwifi #include // modified from https://github.com/yasheena/telnetspy +#ifdef CRASH +#include +extern "C" { +void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); +} +#endif + #if defined(ARDUINO_ARCH_ESP32) //#include #include // added for ESP32 @@ -68,13 +75,44 @@ #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 +#define COLOR_BOLD_OFF "\x1B[22m" // fix by Scott Arlott to support Linux // SPIFFS -#define SPIFFS_MAXSIZE 500 // https://arduinojson.org/v5/assistant/ +#define SPIFFS_MAXSIZE 600 // https://arduinojson.org/v6/assistant/ + +// CRASH +/** + * Structure of the single crash data set + * + * 1. Crash time + * 2. Restart reason + * 3. Exception cause + * 4. epc1 + * 5. epc2 + * 6. epc3 + * 7. excvaddr + * 8. depc + * 9. address of stack start + * 10. address of stack end + * 11. stack trace bytes + * ... + */ +#define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data +#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]; + bool set; // is it a set command + char key[50]; char description[100]; } command_t; @@ -94,6 +132,8 @@ constexpr size_t ArraySize(T (&)[N]) { return N; } +#define UPTIME_OVERFLOW 4294967295 // Uptime overflow value + // class definition class MyESP { public: @@ -104,8 +144,10 @@ class MyESP { void setWIFICallback(void (*callback)()); void setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback); bool isWifiConnected(); + bool isAPmode(); // mqtt + bool isMQTTConnected(); void mqttSubscribe(const char * topic); void mqttUnsubscribe(const char * topic); void mqttPublish(const char * topic, const char * payload); @@ -122,7 +164,7 @@ class MyESP { mqtt_callback_f callback); // OTA - void setOTA(ota_callback_f OTACallback); + void setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post); // debug & telnet void myDebug(const char * format, ...); @@ -134,6 +176,12 @@ class MyESP { 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); + void crashInfo(); + // general void end(); void loop(); @@ -142,7 +190,7 @@ class MyESP { void resetESP(); uint16_t getSystemLoadAverage(); int getWifiQuality(); - + void showSystemStats(); private: // mqtt @@ -177,19 +225,24 @@ class MyESP { char * _wifi_ssid; char * _wifi_password; bool _wifi_connected; + String _getESPhostname(); // ota - ota_callback_f _ota_callback; + ota_callback_f _ota_pre_callback; + ota_callback_f _ota_post_callback; void _ota_setup(); void _OTACallback(); + // crash + void _eeprom_setup(); + // telnet & debug TelnetSpy SerialAndTelnet; void _telnetConnected(); void _telnetDisconnected(); void _telnetHandle(); void _telnetCommand(char * commandLine); - char * _telnet_readWord(); + char * _telnet_readWord(bool allow_all_chars); 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 @@ -197,8 +250,7 @@ class MyESP { 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); + bool _changeSetting(uint8_t wc, const char * setting, const char * value); // fs void _fs_setup(); @@ -206,17 +258,20 @@ class MyESP { void _fs_printConfig(); void _fs_eraseConfig(); + // settings fs_callback_f _fs_callback; fs_settings_callback_f _fs_settings_callback; + void _printSetCommands(); // general - char * _app_hostname; - char * _app_name; - char * _app_version; - char * _boottime; - bool _suspendOutput; - bool _use_serial; - void _printBuildTime(unsigned long rawTime); + char * _app_hostname; + char * _app_name; + char * _app_version; + char * _boottime; + bool _suspendOutput; + bool _use_serial; + unsigned long _getUptime(); + String _buildTime(); // load average (0..100) void _calculateLoad(); diff --git a/platformio.ini-example b/platformio.ini-example index fdbbeb25..0a597bab 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -1,11 +1,14 @@ [platformio] +; add here your board, e.g. nodemcuv2, d1_mini, d1_mini_pro env_default = d1_mini [common] -platform = espressif8266 flash_mode = dout + build_flags = -g -w -;build_flags = -g -w -DBUILD_TIME=$UNIX_TIME + +; for debug use these... +; build_flags = -g -Wall -Wextra -Werror -Wno-missing-field-initializers -Wno-unused-parameter -Wno-unused-variable -DCRASH wifi_settings = ; hard code if you prefer. Recommendation is to set from within the app when in Serial or AP mode @@ -17,21 +20,21 @@ lib_deps = JustWifi AsyncMqttClient ArduinoJson -; https://github.com/bblanchon/ArduinoJson#v5.13.5 OneWire + EEPROM_rotate [env:d1_mini] board = d1_mini -platform = ${common.platform} +platform = espressif8266 framework = arduino lib_deps = ${common.lib_deps} build_flags = ${common.build_flags} ${common.wifi_settings} board_build.flash_mode = ${common.flash_mode} upload_speed = 921600 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 00000000..7b4d4229 --- /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/ds18.cpp b/src/ds18.cpp index a95c6044..6a827970 100644 --- a/src/ds18.cpp +++ b/src/ds18.cpp @@ -4,9 +4,6 @@ * * Paul Derbyshire - https://github.com/proddy/EMS-ESP * - * See ChangeLog.md for history - * See README.md for Acknowledgments - * */ #include "ds18.h" @@ -14,9 +11,10 @@ std::vector _devices; DS18::DS18() { - _wire = NULL; - _count = 0; - _gpio = GPIO_NONE; + _wire = NULL; + _count = 0; + _gpio = GPIO_NONE; + _parasite = 0; } DS18::~DS18() { @@ -25,10 +23,11 @@ DS18::~DS18() { } // init -uint8_t DS18::setup(uint8_t gpio) { +uint8_t DS18::setup(uint8_t gpio, bool parasite) { uint8_t count; - _gpio = gpio; + _gpio = gpio; + _parasite = (parasite ? 1 : 0); // OneWire if (_wire) @@ -62,8 +61,7 @@ void DS18::loop() { // Start conversion _wire->reset(); _wire->skip(); - _wire->write(DS18_CMD_START_CONVERSION, DS18_PARASITE); - + _wire->write(DS18_CMD_START_CONVERSION, _parasite); } else { // Read scratchpads for (unsigned char index = 0; index < _devices.size(); index++) { @@ -117,7 +115,7 @@ char * DS18::getDeviceString(char * buffer, unsigned char index) { char a[30] = {0}; snprintf(a, sizeof(a), - "(%02X%02X%02X%02X%02X%02X%02X%02X) @ GPIO%d", + " (%02X%02X%02X%02X%02X%02X%02X%02X) @ GPIO%d", address[0], address[1], address[2], @@ -136,7 +134,6 @@ char * DS18::getDeviceString(char * buffer, unsigned char index) { return buffer; } - /* * Read sensor values * @@ -154,14 +151,14 @@ char * DS18::getDeviceString(char * buffer, unsigned char index) { DS18B20 & DS1822: store for crc byte 8: SCRATCHPAD_CRC */ -double DS18::getValue(unsigned char index) { +int16_t DS18::getRawValue(unsigned char index) { if (index >= _count) return 0; uint8_t * data = _devices[index].data; if (OneWire::crc8(data, DS18_DATA_SIZE - 1) != data[DS18_DATA_SIZE - 1]) { - return 0; + return DS18_CRC_ERROR; } int16_t raw = (data[1] << 8) | data[0]; @@ -181,11 +178,13 @@ double DS18::getValue(unsigned char index) { // 12 bit res, 750 ms } - double value = (float)raw / 16.0; - if (value == DS18_DISCONNECTED) { - return 0; - } + return raw; +} +// return real value as a double +// The raw temperature data is in units of sixteenths of a degree, so the value must be divided by 16 in order to convert it to degrees. +double DS18::getValue(unsigned char index) { + double value = (float)getRawValue(index) / 16.0; return value; } diff --git a/src/ds18.h b/src/ds18.h index d4dd9cfe..e6e97b30 100644 --- a/src/ds18.h +++ b/src/ds18.h @@ -4,9 +4,6 @@ * * Paul Derbyshire - https://github.com/proddy/EMS-ESP * - * See ChangeLog.md for history - * See README.md for Acknowledgments - * */ #pragma once @@ -20,8 +17,8 @@ #define DS18_CHIP_DS1825 0x3B #define DS18_DATA_SIZE 9 -#define DS18_PARASITE 1 #define DS18_DISCONNECTED -127 +#define DS18_CRC_ERROR -126 #define GPIO_NONE 0x99 #define DS18_READ_INTERVAL 2000 // Force sensor read & cache every 2 seconds @@ -39,10 +36,11 @@ class DS18 { DS18(); ~DS18(); - uint8_t setup(uint8_t gpio); + uint8_t setup(uint8_t gpio, bool parasite); void loop(); char * getDeviceString(char * s, unsigned char index); double getValue(unsigned char index); + int16_t getRawValue(unsigned char index); // raw values, needs / 16 protected: bool validateID(unsigned char id); @@ -50,6 +48,7 @@ class DS18 { uint8_t loadDevices(); OneWire * _wire; - uint8_t _count; // # devices - uint8_t _gpio; // the sensor pin + uint8_t _count; // # devices + uint8_t _gpio; // the sensor pin + uint8_t _parasite; // parasite mode }; diff --git a/src/ems-esp.ino b/src/ems-esp.cpp similarity index 58% rename from src/ems-esp.ino rename to src/ems-esp.cpp index 1c1b733c..b03ef638 100644 --- a/src/ems-esp.ino +++ b/src/ems-esp.cpp @@ -31,9 +31,13 @@ DS18 ds18; #define myDebug(...) myESP.myDebug(__VA_ARGS__) #define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__) +// set to value >0 if the ESP is overheating or there are timing issues. Recommend a value of 1. +#define EMSESP_DELAY 1 // initially set to 0 for no delay + // timers, all values are in seconds -#define PUBLISHVALUES_TIME 120 // every 2 minutes publish MQTT values +#define DEFAULT_PUBLISHWAIT 120 // every 2 minutes publish MQTT values, including Dallas sensors Ticker publishValuesTimer; +Ticker publishSensorValuesTimer; #define SYSTEMCHECK_TIME 20 // every 20 seconds check if Boiler is online Ticker systemCheckTimer; @@ -56,17 +60,21 @@ Ticker showerColdShotStopTimer; #define SHOWER_MIN_DURATION 120000 // in ms. 2 minutes, before recognizing its a shower #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 +#define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water typedef struct { - bool shower_timer; // true if we want to report back on shower times - bool shower_alert; // true if we want the alert of cold water - bool led_enabled; // LED on/off - bool test_mode; // test mode to stop automatic Tx on/off - unsigned long timestamp; // for internal timings, via millis() uint8_t dallas_sensors; // count of dallas sensors - uint8_t led_gpio; - uint8_t dallas_gpio; + + // custom params + bool shower_timer; // true if we want to report back on shower times + bool shower_alert; // true if we want the alert of cold water + bool led; // LED on/off + bool silent_mode; // stop automatic Tx on/off + uint16_t publish_wait; // frequency of MQTT publish in seconds + uint8_t led_gpio; + uint8_t dallas_gpio; + uint8_t dallas_parasite; } _EMSESP_Status; typedef struct { @@ -79,29 +87,34 @@ typedef struct { command_t PROGMEM project_cmds[] = { - {"set led ", "toggle status LED on/off"}, - {"set led_gpio ", "set the LED pin. Default is the onboard LED (D1=5)"}, - {"set dallas_gpio ", "set the pin for external Dallas temperature sensors (D5=14)"}, - {"set thermostat_type ", "set the thermostat type id (e.g. 10 for 0x10)"}, - {"set boiler_type ", "set the boiler type id (e.g. 8 for 0x08)"}, - {"set test_mode ", "test_mode turns off all automatic reads"}, - {"info", "show data captured on the EMS bus"}, - {"log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, - {"publish", "publish all values to MQTT"}, - {"types", "list supported EMS telegram type IDs"}, - {"queue", "show current Tx queue"}, - {"autodetect", "discover EMS devices and attempt to automatically set boiler and thermostat"}, - {"shower ", "toggle either timer or alert on/off"}, - {"send XX ...", "send raw telegram data as hex to EMS bus"}, - {"thermostat read ", "send read request to the thermostat"}, - {"thermostat temp ", "set current thermostat temperature"}, - {"thermostat mode ", "set mode (0=low/night, 1=manual/day, 2=auto)"}, - {"thermostat scan ", "do a read on all type IDs"}, - {"boiler read ", "send read request to boiler"}, - {"boiler wwtemp ", "set boiler warm water temperature"}, - {"boiler tapwater ", "set boiler warm tap water on/off"} - -}; + {true, "led ", "toggle status LED on/off"}, + {true, "led_gpio ", "set the LED pin. Default is the onboard LED (D1=5)"}, + {true, "dallas_gpio ", "set the pin for external Dallas temperature sensors (D5=14)"}, + {true, "dallas_parasite ", "set to on if powering Dallas via parasite"}, + {true, "thermostat_type ", "set the thermostat type id (e.g. 10 for 0x10)"}, + {true, "boiler_type ", "set the boiler type id (e.g. 8 for 0x08)"}, + {true, "silent_mode ", "when on all automatic Tx is disabled"}, + {true, "shower_timer ", "notify via MQTT all shower durations"}, + {true, "shower_alert ", "send a warning of cold water after shower time is exceeded"}, + {true, "publish_wait ", "set frequency for publishing to MQTT"}, + + {false, "info", "show data captured on the EMS bus"}, + {false, "log ", "set logging mode to none, basic, thermostat only, raw or verbose"}, + {false, "publish", "publish all values to MQTT"}, + {false, "refresh", "fetch values from the EMS devices"}, + {false, "types", "list supported EMS telegram type IDs"}, + {false, "queue", "show current Tx queue"}, + {false, "autodetect", "detect EMS devices and attempt to automatically set boiler and thermostat types"}, + {false, "shower ", "toggle either timer or alert on/off"}, + {false, "send XX ...", "send raw telegram data as hex to EMS bus"}, + {false, "thermostat read ", "send read request to the thermostat"}, + {false, "thermostat temp ", "set current thermostat temperature"}, + {false, "thermostat mode ", "set mode (0=low/night, 1=manual/day, 2=auto)"}, + {false, "thermostat scan ", "do a read on all type IDs"}, + {false, "boiler read ", "send read request to boiler"}, + {false, "boiler wwtemp ", "set boiler warm water temperature"}, + {false, "boiler tapwater ", "set boiler warm tap water on/off"}, + {false, "boiler comfort ", "set boiler warm water comfort setting"}}; // store for overall system status _EMSESP_Status EMSESP_Status; @@ -120,7 +133,7 @@ char * _float_to_char(char * a, float f, uint8_t precision = 2) { char * ret = a; // check for 0x8000 (sensor missing) - if (f == EMS_VALUE_FLOAT_NOTSET) { + if (f == EMS_VALUE_SHORT_NOTSET) { strlcpy(ret, "?", sizeof(ret)); } else { long whole = (long)f; @@ -146,63 +159,96 @@ char * _bool_to_char(char * s, uint8_t value) { return s; } -// convert int (single byte) to text value -char * _int_to_char(char * s, uint8_t value) { - if (value == EMS_VALUE_INT_NOTSET) { +// convert short (two bytes) to text value +// negative values are assumed stored as 1-compliment (https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c) +char * _short_to_char(char * s, int16_t value, uint8_t decimals = 1) { + // remove errors on invalid values + if (abs(value) >= EMS_VALUE_SHORT_NOTSET) { strlcpy(s, "?", sizeof(s)); - } else { + return (s); + } + + if (decimals == 0) { itoa(value, s, 10); + return (s); + } + + // floating point + char s2[5] = {0}; + // check for negative values + if (value < 0) { + strlcpy(s, "-", 2); + value = abs(value); } + strlcpy(s, itoa(value / (decimals * 10), s2, 10), 5); + strlcat(s, ".", sizeof(s)); + strlcat(s, itoa(value % (decimals * 10), s2, 10), 5); + return s; } -// takes a float value at prints it to debug log -void _renderFloatValue(const char * prefix, const char * postfix, float value) { +// takes a short value (2 bytes), converts to a fraction +// most values stored a s short are either *10 or *100 +void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t decimals = 1) { char buffer[200] = {0}; char s[20] = {0}; strlcpy(buffer, " ", sizeof(buffer)); strlcat(buffer, prefix, sizeof(buffer)); strlcat(buffer, ": ", sizeof(buffer)); - strlcat(buffer, _float_to_char(s, value), sizeof(buffer)); + + strlcat(buffer, _short_to_char(s, value, decimals), sizeof(buffer)); if (postfix != NULL) { strlcat(buffer, " ", sizeof(buffer)); strlcat(buffer, postfix, sizeof(buffer)); } + myDebug(buffer); } -// takes an int (single byte) value at prints it to debug log -void _renderIntValue(const char * prefix, const char * postfix, uint8_t value) { - char buffer[200] = {0}; - char s[20] = {0}; - strlcpy(buffer, " ", sizeof(buffer)); - strlcat(buffer, prefix, sizeof(buffer)); - strlcat(buffer, ": ", sizeof(buffer)); - strlcat(buffer, _int_to_char(s, value), sizeof(buffer)); +// convert int (single byte) to text value +char * _int_to_char(char * s, uint8_t value, uint8_t div = 1) { + if (value == EMS_VALUE_INT_NOTSET) { + strlcpy(s, "?", sizeof(s)); + return (s); + } - if (postfix != NULL) { - strlcat(buffer, " ", sizeof(buffer)); - strlcat(buffer, postfix, sizeof(buffer)); + char s2[5] = {0}; + + switch (div) { + case 1: + itoa(value, s, 10); + break; + + case 2: + strlcpy(s, itoa(value >> 1, s2, 10), 5); + strlcat(s, ".", sizeof(s)); + strlcat(s, ((value & 0x01) ? "5" : "0"), 5); + break; + + case 10: + strlcpy(s, itoa(value / 10, s2, 10), 5); + strlcat(s, ".", sizeof(s)); + strlcat(s, itoa(value % 10, s2, 10), 5); + break; + + default: + itoa(value, s, 10); + break; } - myDebug(buffer); + + return s; } -// takes an int value, converts to a fraction -void _renderIntfractionalValue(const char * prefix, const char * postfix, uint8_t value, uint8_t decimals) { +// takes an int value (1 byte), converts to a fraction +void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, uint8_t div = 1) { char buffer[200] = {0}; char s[20] = {0}; strlcpy(buffer, " ", sizeof(buffer)); strlcat(buffer, prefix, sizeof(buffer)); strlcat(buffer, ": ", sizeof(buffer)); - if (value == EMS_VALUE_INT_NOTSET) { - strlcat(buffer, "?", sizeof(buffer)); - } else { - strlcat(buffer, _int_to_char(s, value / (decimals * 10)), sizeof(buffer)); - strlcat(buffer, ".", sizeof(buffer)); - strlcat(buffer, _int_to_char(s, value % (decimals * 10)), sizeof(buffer)); - } + strlcat(buffer, _int_to_char(s, value, div), sizeof(buffer)); if (postfix != NULL) { strlcat(buffer, " ", sizeof(buffer)); @@ -251,7 +297,9 @@ void _renderBoolValue(const char * prefix, uint8_t value) { void showInfo() { // General stats from EMS bus - myDebug("%sEMS-ESP System stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + char buffer_type[128] = {0}; + + myDebug("%sEMS-ESP system stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); _EMS_SYS_LOGGING sysLog = ems_getLogging(); if (sysLog == EMS_SYS_LOGGING_BASIC) { myDebug(" System logging set to Basic"); @@ -263,10 +311,8 @@ void showInfo() { myDebug(" System logging set to None"); } - myDebug(" LED is %s", EMSESP_Status.led_enabled ? "on" : "off"); - myDebug(" Test Mode is %s", EMSESP_Status.test_mode ? "on" : "off"); - - myDebug(" # connected Dallas temperature sensors=%d", EMSESP_Status.dallas_sensors); + myDebug(" LED is %s, Silent mode is %s", EMSESP_Status.led ? "on" : "off", EMSESP_Status.silent_mode ? "on" : "off"); + myDebug(" %d external temperature sensor%s connected", EMSESP_Status.dallas_sensors, (EMSESP_Status.dallas_sensors > 1) ? "s" : ""); myDebug(" Thermostat is %s, Boiler is %s, Shower Timer is %s, Shower Alert is %s", (ems_getThermostatEnabled() ? "enabled" : "disabled"), @@ -274,37 +320,48 @@ void showInfo() { ((EMSESP_Status.shower_timer) ? "enabled" : "disabled"), ((EMSESP_Status.shower_alert) ? "enabled" : "disabled")); - myDebug("\n%sEMS Bus Stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); - myDebug(" Bus Connected=%s, # Rx telegrams=%d, # Tx telegrams=%d, # Crc Errors=%d", + myDebug("\n%sEMS Bus stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + myDebug(" Bus Connected=%s, Tx is %s, # Rx telegrams=%d, # Tx telegrams=%d, # Crc Errors=%d", (ems_getBusConnected() ? "yes" : "no"), + (ems_getTxCapable() ? "active" : "not active"), EMS_Sys_Status.emsRxPgks, EMS_Sys_Status.emsTxPkgs, EMS_Sys_Status.emxCrcErr); myDebug(""); - myDebug("%sBoiler stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); // version details - char buffer_type[64]; myDebug(" Boiler type: %s", ems_getBoilerDescription(buffer_type)); // active stats if (ems_getBusConnected()) { - myDebug(" Hot tap water is %s", (EMS_Boiler.tapwaterActive ? "running" : "off")); - myDebug(" Central Heating is %s", (EMS_Boiler.heatingActive ? "active" : "off")); + if (EMS_Boiler.tapwaterActive != EMS_VALUE_INT_NOTSET) { + myDebug(" Hot tap water: %s", EMS_Boiler.tapwaterActive ? "running" : "off"); + } + + if (EMS_Boiler.heatingActive != EMS_VALUE_INT_NOTSET) { + myDebug(" Central heating: %s", EMS_Boiler.heatingActive ? "active" : "off"); + } } // UBAParameterWW _renderBoolValue("Warm Water activated", EMS_Boiler.wWActivated); _renderBoolValue("Warm Water circulation pump available", EMS_Boiler.wWCircPump); - myDebug(" Warm Water is set to %s", (EMS_Boiler.wWComfort ? "Comfort" : "ECO")); + if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { + myDebug(" Warm Water comfort setting: Hot"); + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { + myDebug(" Warm Water comfort setting: Eco"); + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { + myDebug(" Warm Water comfort setting: Intelligent"); + } + _renderIntValue("Warm Water selected temperature", "C", EMS_Boiler.wWSelTemp); _renderIntValue("Warm Water desired temperature", "C", EMS_Boiler.wWDesiredTemp); // UBAMonitorWWMessage - _renderFloatValue("Warm Water current temperature", "C", EMS_Boiler.wWCurTmp); - _renderIntfractionalValue("Warm Water current tap water flow", "l/min", EMS_Boiler.wWCurFlow, 1); + _renderShortValue("Warm Water current temperature", "C", EMS_Boiler.wWCurTmp); + _renderIntValue("Warm Water current tap water flow", "l/min", EMS_Boiler.wWCurFlow, 10); _renderLongValue("Warm Water # starts", "times", EMS_Boiler.wWStarts); if (EMS_Boiler.wWWorkM != EMS_VALUE_LONG_NOTSET) { myDebug(" Warm Water active time: %d days %d hours %d minutes", @@ -316,8 +373,8 @@ void showInfo() { // UBAMonitorFast _renderIntValue("Selected flow temperature", "C", EMS_Boiler.selFlowTemp); - _renderFloatValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); - _renderFloatValue("Return temperature", "C", EMS_Boiler.retTemp); + _renderShortValue("Current flow temperature", "C", EMS_Boiler.curFlowTemp); + _renderShortValue("Return temperature", "C", EMS_Boiler.retTemp); _renderBoolValue("Gas", EMS_Boiler.burnGas); _renderBoolValue("Boiler pump", EMS_Boiler.heatPmp); _renderBoolValue("Fan", EMS_Boiler.fanWork); @@ -325,22 +382,26 @@ void showInfo() { _renderBoolValue("Circulation pump", EMS_Boiler.wWCirc); _renderIntValue("Burner selected max power", "%", EMS_Boiler.selBurnPow); _renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow); - _renderFloatValue("Flame current", "uA", EMS_Boiler.flameCurr); - _renderFloatValue("System pressure", "bar", EMS_Boiler.sysPress); - myDebug(" Current System Service Code: %s", EMS_Boiler.serviceCodeChar); + _renderShortValue("Flame current", "uA", EMS_Boiler.flameCurr); + _renderIntValue("System pressure", "bar", EMS_Boiler.sysPress, 10); + if (EMS_Boiler.serviceCode == EMS_VALUE_SHORT_NOTSET) { + myDebug(" System service code: %s", EMS_Boiler.serviceCodeChar); + } else { + myDebug(" System service code: %s (%d)", EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode); + } // UBAParametersMessage _renderIntValue("Heating temperature setting on the boiler", "C", EMS_Boiler.heating_temp); - _renderIntValue("Boiler circuit pump modulation max. power", "%", EMS_Boiler.pump_mod_max); - _renderIntValue("Boiler circuit pump modulation min. power", "%", EMS_Boiler.pump_mod_min); + _renderIntValue("Boiler circuit pump modulation max power", "%", EMS_Boiler.pump_mod_max); + _renderIntValue("Boiler circuit pump modulation min power", "%", EMS_Boiler.pump_mod_min); // UBAMonitorSlow - if (EMS_Boiler.extTemp != EMS_VALUE_FLOAT_NOTSET) { - _renderFloatValue("Outside temperature", "C", EMS_Boiler.extTemp); + if (EMS_Boiler.extTemp != EMS_VALUE_SHORT_NOTSET) { + _renderShortValue("Outside temperature", "C", EMS_Boiler.extTemp); } - _renderFloatValue("Boiler temperature", "C", EMS_Boiler.boilTemp); + _renderShortValue("Boiler temperature", "C", EMS_Boiler.boilTemp); _renderIntValue("Pump modulation", "%", EMS_Boiler.pumpMod); - _renderLongValue("Burner # restarts", "times", EMS_Boiler.burnStarts); + _renderLongValue("Burner # starts", "times", EMS_Boiler.burnStarts); if (EMS_Boiler.burnWorkMin != EMS_VALUE_LONG_NOTSET) { myDebug(" Total burner operating time: %d days %d hours %d minutes", EMS_Boiler.burnWorkMin / 1440, @@ -360,15 +421,35 @@ void showInfo() { EMS_Boiler.UBAuptime % 60); } - myDebug(""); // newline + // For SM10 Solar Module + if (EMS_Other.SM10) { + myDebug(""); // newline + myDebug("%sSolar Module stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); + _renderShortValue(" Collector temperature", "C", EMS_Other.SM10collectorTemp); + _renderShortValue(" Bottom temperature", "C", EMS_Other.SM10bottomTemp); + _renderIntValue(" Pump modulation", "%", EMS_Other.SM10pumpModulation); + _renderBoolValue(" Pump active", EMS_Other.SM10pump); + } // Thermostat stats if (ems_getThermostatEnabled()) { + myDebug(""); // newline myDebug("%sThermostat stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); myDebug(" Thermostat type: %s", ems_getThermostatDescription(buffer_type)); - _renderFloatValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp); - _renderFloatValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp); - if ((ems_getThermostatModel() != EMS_MODEL_EASY) && (ems_getThermostatModel() != EMS_MODEL_BOSCHEASY)) { + if ((ems_getThermostatModel() == EMS_MODEL_EASY) || (ems_getThermostatModel() == EMS_MODEL_BOSCHEASY)) { + // for easy temps are * 100 + // also we don't have the time or mode + _renderShortValue("Set room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 10); + _renderShortValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 10); + } else { + // because we store in 2 bytes short, when converting to a single byte we'll loose the negative value if its unset + if ((EMS_Thermostat.setpoint_roomTemp <= 0) || (EMS_Thermostat.curr_roomTemp <= 0)) { + EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_INT_NOTSET; + EMS_Thermostat.curr_roomTemp = EMS_VALUE_INT_NOTSET; + } + _renderIntValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp, 2); // convert to a single byte * 2 + _renderIntValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp, 10); // is *10 + myDebug(" Thermostat time is %02d:%02d:%02d %d/%d/%d", EMS_Thermostat.hour, EMS_Thermostat.minute, @@ -387,27 +468,55 @@ void showInfo() { myDebug(" Mode is set to ?"); } } + myDebug(""); // newline } // Dallas if (EMSESP_Status.dallas_sensors != 0) { + myDebug(""); // newline + char buffer[128] = {0}; + char valuestr[8] = {0}; // for formatting temp myDebug("%sExternal temperature sensors:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { - char s[80] = {0}; - snprintf(s, sizeof(s), "Sensor #%d", i + 1); - _renderFloatValue(s, "C", ds18.getValue(i)); + myDebug(" Sensor #%d %s: %s C", i + 1, ds18.getDeviceString(buffer, i), _float_to_char(valuestr, ds18.getValue(i))); } - myDebug(""); // newline } // show the Shower Info if (EMSESP_Status.shower_timer) { + myDebug(""); // newline myDebug("%sShower stats:%s", COLOR_BOLD_ON, COLOR_BOLD_OFF); myDebug(" Shower is %s", (EMSESP_Shower.showerOn ? "running" : "off")); } } +// send all dallas sensor values as a JSON package to MQTT +void publishSensorValues() { + StaticJsonDocument doc; + JsonObject sensors = doc.to(); + + bool hasdata = false; + char label[8] = {0}; + char valuestr[8] = {0}; // for formatting temp + + // see if the sensor values have changed, if so send + for (uint8_t i = 0; i < EMSESP_Status.dallas_sensors; i++) { + double sensorValue = ds18.getValue(i); + if (sensorValue != DS18_DISCONNECTED && sensorValue != DS18_CRC_ERROR) { + sprintf(label, PAYLOAD_EXTERNAL_SENSORS, (i + 1)); + sensors[label] = _float_to_char(valuestr, sensorValue); + hasdata = true; + } + } + + if (hasdata) { + char data[MQTT_MAX_SIZE] = {0}; + serializeJson(doc, data, sizeof(data)); + myESP.mqttPublish(TOPIC_EXTERNAL_SENSORS, data); + } +} + // send values via MQTT // a json object is created for the boiler and one for the thermostat // CRC check is done to see if there are changes in the values since the last send to avoid too much wifi traffic @@ -419,33 +528,42 @@ void publishValues(bool force) { uint32_t fchecksum; static uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off - static uint32_t previousBoilerPublishCRC = 0; // CRC check - static uint32_t previousThermostatPublishCRC = 0; // CRC check + static uint32_t previousBoilerPublishCRC = 0; // CRC check for boiler values + static uint32_t previousThermostatPublishCRC = 0; // CRC check for thermostat values + static uint32_t previousOtherPublishCRC = 0; // CRC check for other values (e.g. SM10) JsonObject rootBoiler = doc.to(); rootBoiler["wWSelTemp"] = _int_to_char(s, EMS_Boiler.wWSelTemp); - rootBoiler["selFlowTemp"] = _float_to_char(s, EMS_Boiler.selFlowTemp); - rootBoiler["outdoorTemp"] = _float_to_char(s, EMS_Boiler.extTemp); + rootBoiler["selFlowTemp"] = _int_to_char(s, EMS_Boiler.selFlowTemp); + rootBoiler["outdoorTemp"] = _short_to_char(s, EMS_Boiler.extTemp); rootBoiler["wWActivated"] = _bool_to_char(s, EMS_Boiler.wWActivated); - rootBoiler["wWComfort"] = EMS_Boiler.wWComfort ? "Comfort" : "ECO"; - rootBoiler["wWCurTmp"] = _float_to_char(s, EMS_Boiler.wWCurTmp); - snprintf(s, sizeof(s), "%i.%i", EMS_Boiler.wWCurFlow / 10, EMS_Boiler.wWCurFlow % 10); - rootBoiler["wWCurFlow"] = s; - rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); - rootBoiler["curFlowTemp"] = _float_to_char(s, EMS_Boiler.curFlowTemp); - rootBoiler["retTemp"] = _float_to_char(s, EMS_Boiler.retTemp); - rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); - rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); - rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); - rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); - rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); - rootBoiler["selBurnPow"] = _int_to_char(s, EMS_Boiler.selBurnPow); - rootBoiler["curBurnPow"] = _int_to_char(s, EMS_Boiler.curBurnPow); - rootBoiler["sysPress"] = _float_to_char(s, EMS_Boiler.sysPress); - rootBoiler["boilTemp"] = _float_to_char(s, EMS_Boiler.boilTemp); - rootBoiler["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); - rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; + + if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Hot) { + rootBoiler["wWComfort"] = "Hot"; + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Eco) { + rootBoiler["wWComfort"] = "Eco"; + } else if (EMS_Boiler.wWComfort == EMS_VALUE_UBAParameterWW_wwComfort_Intelligent) { + rootBoiler["wWComfort"] = "Intelligent"; + } + + rootBoiler["wWCurTmp"] = _short_to_char(s, EMS_Boiler.wWCurTmp); + rootBoiler["wWCurFlow"] = _int_to_char(s, EMS_Boiler.wWCurFlow, 10); + rootBoiler["wWHeat"] = _bool_to_char(s, EMS_Boiler.wWHeat); + rootBoiler["curFlowTemp"] = _short_to_char(s, EMS_Boiler.curFlowTemp); + rootBoiler["retTemp"] = _short_to_char(s, EMS_Boiler.retTemp); + rootBoiler["burnGas"] = _bool_to_char(s, EMS_Boiler.burnGas); + rootBoiler["heatPmp"] = _bool_to_char(s, EMS_Boiler.heatPmp); + rootBoiler["fanWork"] = _bool_to_char(s, EMS_Boiler.fanWork); + rootBoiler["ignWork"] = _bool_to_char(s, EMS_Boiler.ignWork); + rootBoiler["wWCirc"] = _bool_to_char(s, EMS_Boiler.wWCirc); + rootBoiler["selBurnPow"] = _int_to_char(s, EMS_Boiler.selBurnPow); + rootBoiler["curBurnPow"] = _int_to_char(s, EMS_Boiler.curBurnPow); + rootBoiler["sysPress"] = _int_to_char(s, EMS_Boiler.sysPress, 10); + rootBoiler["boilTemp"] = _short_to_char(s, EMS_Boiler.boilTemp); + rootBoiler["pumpMod"] = _int_to_char(s, EMS_Boiler.pumpMod); + rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar; + rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode; serializeJson(doc, data, sizeof(data)); @@ -475,15 +593,20 @@ void publishValues(bool force) { // handle the thermostat values separately if (ems_getThermostatEnabled()) { // only send thermostat values if we actually have them - if (((int)EMS_Thermostat.curr_roomTemp == (int)0) || ((int)EMS_Thermostat.setpoint_roomTemp == (int)0)) + if ((EMS_Thermostat.curr_roomTemp <= 0) || (EMS_Thermostat.setpoint_roomTemp <= 0)) return; // build new json object doc.clear(); JsonObject rootThermostat = doc.to(); - rootThermostat[THERMOSTAT_CURRTEMP] = _float_to_char(s, EMS_Thermostat.curr_roomTemp); - rootThermostat[THERMOSTAT_SELTEMP] = _float_to_char(s, EMS_Thermostat.setpoint_roomTemp); + if ((ems_getThermostatModel() == EMS_MODEL_EASY) || (ems_getThermostatModel() == EMS_MODEL_BOSCHEASY)) { + rootThermostat[THERMOSTAT_SELTEMP] = _short_to_char(s, EMS_Thermostat.setpoint_roomTemp, 10); + rootThermostat[THERMOSTAT_CURRTEMP] = _short_to_char(s, EMS_Thermostat.curr_roomTemp, 10); + } else { + rootThermostat[THERMOSTAT_SELTEMP] = _int_to_char(s, EMS_Thermostat.setpoint_roomTemp, 2); + rootThermostat[THERMOSTAT_CURRTEMP] = _int_to_char(s, EMS_Thermostat.curr_roomTemp, 10); + } // RC20 has different mode settings if (ems_getThermostatModel() == EMS_MODEL_RC20) { @@ -512,15 +635,45 @@ void publishValues(bool force) { for (size_t i = 0; i < measureJson(doc) - 1; i++) { crc.update(data[i]); } - uint32_t checksum = crc.finalize(); - if ((previousThermostatPublishCRC != checksum) || force) { - previousThermostatPublishCRC = checksum; + fchecksum = crc.finalize(); + if ((previousThermostatPublishCRC != fchecksum) || force) { + previousThermostatPublishCRC = fchecksum; myDebugLog("Publishing thermostat data via MQTT"); // send values via MQTT myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data); } } + + // handle the other values separately + // For SM10 Solar Module + if (EMS_Other.SM10) { + // build new json object + doc.clear(); + JsonObject rootSM10 = doc.to(); + + rootSM10[SM10_COLLECTORTEMP] = _short_to_char(s, EMS_Other.SM10collectorTemp); + rootSM10[SM10_BOTTOMTEMP] = _short_to_char(s, EMS_Other.SM10bottomTemp); + rootSM10[SM10_PUMPMODULATION] = _int_to_char(s, EMS_Other.SM10pumpModulation); + rootSM10[SM10_PUMP] = _bool_to_char(s, EMS_Other.SM10pump); + + data[0] = '\0'; // reset data for next package + serializeJson(doc, data, sizeof(data)); + + // calculate new CRC + crc.reset(); + for (size_t i = 0; i < measureJson(doc) - 1; i++) { + crc.update(data[i]); + } + fchecksum = crc.finalize(); + if ((previousOtherPublishCRC != fchecksum) || force) { + previousOtherPublishCRC = fchecksum; + myDebugLog("Publishing SM10 data via MQTT"); + + // send values via MQTT + myESP.mqttPublish(TOPIC_SM10_DATA, data); + } + } } // sets the shower timer on/off @@ -570,6 +723,61 @@ char * _readWord() { return word; } +// publish external dallas sensor temperature values to MQTT +void do_publishSensorValues() { + if (EMSESP_Status.dallas_sensors != 0) { + publishSensorValues(); + } +} + +// 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()) && myESP.isMQTTConnected()) { + publishValues(false); + } +} + +// callback to light up the LED, called via Ticker every second +// fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off +void do_ledcheck() { + if (EMSESP_Status.led) { + if (ems_getBusConnected()) { + digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? LOW : HIGH); // light on. For onboard LED high=off + } else { + int state = digitalRead(EMSESP_Status.led_gpio); + digitalWrite(EMSESP_Status.led_gpio, !state); + } + } +} + +// Thermostat scan +void do_scanThermostat() { + if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + myDebug("> Scanning thermostat message type #0x%02X...", scanThermostat_count); + ems_doReadCommand(scanThermostat_count, EMS_Thermostat.type_id); + scanThermostat_count++; + } +} + +// do a system health check every now and then to see if we all connections +void do_systemCheck() { + if ((!ems_getBusConnected()) && (!myESP.getUseSerial())) { + myDebug("Error! Unable to read from EMS bus. Retrying in %d seconds...", SYSTEMCHECK_TIME); + } +} + +// force calls to get data from EMS for the types that aren't sent as broadcasts +// only if we have a EMS connection +void do_regularUpdates() { + if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { + myDebugLog("Calling scheduled data refresh from EMS devices..."); + ems_getThermostatValues(); + ems_getBoilerValues(); + ems_getOtherValues(); + } +} + // initiate a force scan by sending type read requests from 0 to FF to the thermostat // used to analyze responses for debugging void startThermostatScan(uint8_t start) { @@ -582,13 +790,34 @@ void startThermostatScan(uint8_t start) { scanThermostat.attach(SCANTHERMOSTAT_TIME, do_scanThermostat); } +// turn back on the hot water for the shower +void _showerColdShotStop() { + if (EMSESP_Shower.doingColdShot) { + myDebugLog("[Shower] finished shot of cold. hot water back on"); + ems_setWarmTapWaterActivated(true); + EMSESP_Shower.doingColdShot = false; + showerColdShotStopTimer.detach(); // disable the timer + } +} + +// turn off hot water to send a shot of cold +void _showerColdShotStart() { + if (EMSESP_Status.shower_alert) { + myDebugLog("[Shower] doing a shot of cold water"); + ems_setWarmTapWaterActivated(false); + EMSESP_Shower.doingColdShot = true; + // start the timer for n seconds which will reset the water back to hot + showerColdShotStopTimer.attach(SHOWER_COLDSHOT_DURATION, _showerColdShotStop); + } +} + // callback for loading/saving settings to the file system (SPIFFS) bool FSCallback(MYESP_FSACTION action, const JsonObject json) { + bool recreate_config = true; + if (action == MYESP_FSACTION_LOAD) { // led - if (!(EMSESP_Status.led_enabled = json["led"])) { - EMSESP_Status.led_enabled = LED_BUILTIN; // default value - } + EMSESP_Status.led = json["led"]; // led_gpio if (!(EMSESP_Status.led_gpio = json["led_gpio"])) { @@ -600,6 +829,11 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { EMSESP_Status.dallas_gpio = EMSESP_DALLAS_GPIO; // default value } + // dallas_parasite + if (!(EMSESP_Status.dallas_parasite = json["dallas_parasite"])) { + EMSESP_Status.dallas_parasite = EMSESP_DALLAS_PARASITE; // default value + } + // thermostat_type if (!(EMS_Thermostat.type_id = json["thermostat_type"])) { EMS_Thermostat.type_id = EMSESP_THERMOSTAT_TYPE; // set default @@ -610,21 +844,35 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { EMS_Boiler.type_id = EMSESP_BOILER_TYPE; // set default } - // test mode - if (!(EMSESP_Status.test_mode = json["test_mode"])) { - EMSESP_Status.test_mode = false; // default value + // silent mode + EMSESP_Status.silent_mode = json["silent_mode"]; + ems_setTxDisabled(EMSESP_Status.silent_mode); + + // shower_timer + EMSESP_Status.shower_timer = json["shower_timer"]; + + // shower_alert + EMSESP_Status.shower_alert = json["shower_alert"]; + + // publish_wait + if (!(EMSESP_Status.publish_wait = json["publish_wait"])) { + EMSESP_Status.publish_wait = DEFAULT_PUBLISHWAIT; // default value } - return false; // always save the settings + return recreate_config; // return false if some settings are missing and we need to rebuild the file } if (action == MYESP_FSACTION_SAVE) { - json["led"] = EMSESP_Status.led_enabled; + json["led"] = EMSESP_Status.led; json["led_gpio"] = EMSESP_Status.led_gpio; json["dallas_gpio"] = EMSESP_Status.dallas_gpio; + json["dallas_parasite"] = EMSESP_Status.dallas_parasite; json["thermostat_type"] = EMS_Thermostat.type_id; json["boiler_type"] = EMS_Boiler.type_id; - json["test_mode"] = EMSESP_Status.test_mode; + json["silent_mode"] = EMSESP_Status.silent_mode; + json["shower_timer"] = EMSESP_Status.shower_timer; + json["shower_alert"] = EMSESP_Status.shower_alert; + json["publish_wait"] = EMSESP_Status.publish_wait; return true; } @@ -632,9 +880,9 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) { return false; } -// callback for custom settings when showing Stored Settings +// callback for custom settings when showing Stored Settings with the 'set' command // wc is number of arguments after the 'set' command -// returns true if the setting was recognized and changed +// returns true if the setting was recognized and changed and should be saved back to SPIFFs bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, const char * value) { bool ok = false; @@ -642,25 +890,32 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c // led if ((strcmp(setting, "led") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { - EMSESP_Status.led_enabled = true; - ok = true; + EMSESP_Status.led = true; + ok = true; } else if (strcmp(value, "off") == 0) { - EMSESP_Status.led_enabled = false; - ok = true; - // let's make sure LED is really off - digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); // light off. For onboard high=off + EMSESP_Status.led = false; + ok = true; + // let's make sure LED is really off - For onboard high=off + digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? HIGH : LOW); + } else { + myDebug("Error. Usage: set led "); } } // test mode - if ((strcmp(setting, "test_mode") == 0) && (wc == 2)) { + if ((strcmp(setting, "silent_mode") == 0) && (wc == 2)) { if (strcmp(value, "on") == 0) { - EMSESP_Status.test_mode = true; - ok = true; - myDebug("* Reboot to go into test mode."); + EMSESP_Status.silent_mode = true; + ok = true; + myDebug("* in Silent mode. All Tx is disabled."); + ems_setTxDisabled(true); } else if (strcmp(value, "off") == 0) { - EMSESP_Status.test_mode = false; - ok = true; + EMSESP_Status.silent_mode = false; + ok = true; + ems_setTxDisabled(false); + myDebug("* out of Silent mode. Tx is enabled."); + } else { + myDebug("Error. Usage: set silent_mode "); } } @@ -679,6 +934,19 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c ok = true; } + // dallas_parasite + if ((strcmp(setting, "dallas_parasite") == 0) && (wc == 2)) { + if (strcmp(value, "on") == 0) { + EMSESP_Status.dallas_parasite = true; + ok = true; + } else if (strcmp(value, "off") == 0) { + EMSESP_Status.dallas_parasite = false; + ok = true; + } else { + myDebug("Error. Usage: set dallas_parasite "); + } + } + // thermostat_type if (strcmp(setting, "thermostat_type") == 0) { EMS_Thermostat.type_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE); @@ -690,13 +958,45 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c EMS_Boiler.type_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE); ok = true; } + + // shower timer + if ((strcmp(setting, "shower_timer") == 0) && (wc == 2)) { + if (strcmp(value, "on") == 0) { + EMSESP_Status.shower_timer = true; + ok = true; + } else if (strcmp(value, "off") == 0) { + EMSESP_Status.shower_timer = false; + ok = true; + } else { + myDebug("Error. Usage: set shower_timer "); + } + } + + // shower alert + if ((strcmp(setting, "shower_alert") == 0) && (wc == 2)) { + if (strcmp(value, "on") == 0) { + EMSESP_Status.shower_alert = true; + ok = true; + } else if (strcmp(value, "off") == 0) { + EMSESP_Status.shower_alert = false; + ok = true; + } else { + myDebug("Error. Usage: set shower_alert "); + } + } + + // publish_wait + if ((strcmp(setting, "publish_wait") == 0) && (wc == 2)) { + EMSESP_Status.publish_wait = atoi(value); + ok = true; + } } if (action == MYESP_FSACTION_LIST) { - myDebug(" test_mode=%s", EMSESP_Status.test_mode ? "on" : "off"); - myDebug(" led=%s", EMSESP_Status.led_enabled ? "on" : "off"); + myDebug(" led=%s", EMSESP_Status.led ? "on" : "off"); myDebug(" led_gpio=%d", EMSESP_Status.led_gpio); myDebug(" dallas_gpio=%d", EMSESP_Status.dallas_gpio); + myDebug(" dallas_parasite=%s", EMSESP_Status.dallas_parasite ? "on" : "off"); if (EMS_Thermostat.type_id == EMS_ID_NONE) { myDebug(" thermostat_type="); @@ -711,6 +1011,11 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c } else { myDebug(" boiler_type=%02X", EMS_Boiler.type_id); } + + myDebug(" silent_mode=%s", EMSESP_Status.silent_mode ? "on" : "off"); + myDebug(" shower_timer=%s", EMSESP_Status.shower_timer ? "on" : "off"); + myDebug(" shower_alert=%s", EMSESP_Status.shower_alert ? "on" : "off"); + myDebug(" publish_wait=%d", EMSESP_Status.publish_wait); } return ok; @@ -743,6 +1048,12 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { ok = true; } + if (strcmp(first_cmd, "refresh") == 0) { + myDebug("Fetching data from EMS devices..."); + do_regularUpdates(); + ok = true; + } + if (strcmp(first_cmd, "types") == 0) { ems_printAllTypes(); ok = true; @@ -758,84 +1069,93 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { ok = true; } + if (strcmp(first_cmd, "startup") == 0) { + ems_startupTelegrams(); + ok = true; + } + // shower settings - if (strcmp(first_cmd, "shower") == 0) { - if (wc == 2) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "timer") == 0) { - EMSESP_Status.shower_timer = !EMSESP_Status.shower_timer; - myESP.mqttPublish(TOPIC_SHOWER_TIMER, EMSESP_Status.shower_timer ? "1" : "0"); - ok = true; - } else if (strcmp(second_cmd, "alert") == 0) { - EMSESP_Status.shower_alert = !EMSESP_Status.shower_alert; - myESP.mqttPublish(TOPIC_SHOWER_ALERT, EMSESP_Status.shower_alert ? "1" : "0"); - ok = true; - } + if ((strcmp(first_cmd, "shower") == 0) && (wc == 2)) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "timer") == 0) { + EMSESP_Status.shower_timer = !EMSESP_Status.shower_timer; + myESP.mqttPublish(TOPIC_SHOWER_TIMER, EMSESP_Status.shower_timer ? "1" : "0"); + ok = true; + } else if (strcmp(second_cmd, "alert") == 0) { + EMSESP_Status.shower_alert = !EMSESP_Status.shower_alert; + myESP.mqttPublish(TOPIC_SHOWER_ALERT, EMSESP_Status.shower_alert ? "1" : "0"); + ok = true; } } // logging - if (strcmp(first_cmd, "log") == 0) { - if (wc == 2) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "v") == 0) { - ems_setLogging(EMS_SYS_LOGGING_VERBOSE); - ok = true; - } else if (strcmp(second_cmd, "b") == 0) { - ems_setLogging(EMS_SYS_LOGGING_BASIC); - ok = true; - } else if (strcmp(second_cmd, "t") == 0) { - ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); - ok = true; - } else if (strcmp(second_cmd, "r") == 0) { - ems_setLogging(EMS_SYS_LOGGING_RAW); - ok = true; - } else if (strcmp(second_cmd, "n") == 0) { - ems_setLogging(EMS_SYS_LOGGING_NONE); - ok = true; - } + if ((strcmp(first_cmd, "log") == 0) && (wc == 2)) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "v") == 0) { + ems_setLogging(EMS_SYS_LOGGING_VERBOSE); + ok = true; + } else if (strcmp(second_cmd, "b") == 0) { + ems_setLogging(EMS_SYS_LOGGING_BASIC); + ok = true; + } else if (strcmp(second_cmd, "t") == 0) { + ems_setLogging(EMS_SYS_LOGGING_THERMOSTAT); + ok = true; + } else if (strcmp(second_cmd, "r") == 0) { + ems_setLogging(EMS_SYS_LOGGING_RAW); + ok = true; + } else if (strcmp(second_cmd, "n") == 0) { + ems_setLogging(EMS_SYS_LOGGING_NONE); + ok = true; } } // thermostat commands - if (strcmp(first_cmd, "thermostat") == 0) { - if (wc == 3) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "temp") == 0) { - ems_setThermostatTemp(_readFloatNumber()); - ok = true; - } else if (strcmp(second_cmd, "mode") == 0) { - ems_setThermostatMode(_readIntNumber()); - ok = true; - } else if (strcmp(second_cmd, "read") == 0) { - ems_doReadCommand(_readHexNumber(), EMS_Thermostat.type_id); - ok = true; - } else if (strcmp(second_cmd, "scan") == 0) { - startThermostatScan(_readIntNumber()); - ok = true; - } + if ((strcmp(first_cmd, "thermostat") == 0) && (wc == 3)) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "temp") == 0) { + ems_setThermostatTemp(_readFloatNumber()); + ok = true; + } else if (strcmp(second_cmd, "mode") == 0) { + ems_setThermostatMode(_readIntNumber()); + ok = true; + } else if (strcmp(second_cmd, "read") == 0) { + ems_doReadCommand(_readHexNumber(), EMS_Thermostat.type_id); + ok = true; + } else if (strcmp(second_cmd, "scan") == 0) { + startThermostatScan(_readIntNumber()); + ok = true; } } // boiler commands - if (strcmp(first_cmd, "boiler") == 0) { - if (wc == 3) { - char * second_cmd = _readWord(); - if (strcmp(second_cmd, "wwtemp") == 0) { - ems_setWarmWaterTemp(_readIntNumber()); + if ((strcmp(first_cmd, "boiler") == 0) && (wc == 3)) { + char * second_cmd = _readWord(); + if (strcmp(second_cmd, "wwtemp") == 0) { + ems_setWarmWaterTemp(_readIntNumber()); + ok = true; + } else if (strcmp(second_cmd, "comfort") == 0) { + char * third_cmd = _readWord(); + if (strcmp(third_cmd, "hot") == 0) { + ems_setWarmWaterModeComfort(1); ok = true; - } else if (strcmp(second_cmd, "read") == 0) { - ems_doReadCommand(_readHexNumber(), EMS_Boiler.type_id); + } else if (strcmp(third_cmd, "eco") == 0) { + ems_setWarmWaterModeComfort(2); + ok = true; + } else if (strcmp(third_cmd, "intelligent") == 0) { + ems_setWarmWaterModeComfort(3); + ok = true; + } + } else if (strcmp(second_cmd, "read") == 0) { + ems_doReadCommand(_readHexNumber(), EMS_Boiler.type_id); + ok = true; + } else if (strcmp(second_cmd, "tapwater") == 0) { + char * third_cmd = _readWord(); + if (strcmp(third_cmd, "on") == 0) { + ems_setWarmTapWaterActivated(true); + ok = true; + } else if (strcmp(third_cmd, "off") == 0) { + ems_setWarmTapWaterActivated(false); ok = true; - } else if (strcmp(second_cmd, "tapwater") == 0) { - char * third_cmd = _readWord(); - if (strcmp(third_cmd, "on") == 0) { - ems_setWarmTapWaterActivated(true); - ok = true; - } else if (strcmp(third_cmd, "off") == 0) { - ems_setWarmTapWaterActivated(false); - ok = true; - } } } } @@ -854,10 +1174,16 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) { // OTA callback when the OTA process starts // so we can disable the EMS to avoid any noise -void OTACallback() { +void OTACallback_pre() { emsuart_stop(); } +// OTA callback when the OTA process finishes +// so we can re-enable the UART +void OTACallback_post() { + emsuart_start(); +} + // MQTT Callback to handle incoming/outgoing changes void MQTTCallback(unsigned int type, const char * topic, const char * message) { // we're connected. lets subscribe to some topics @@ -866,6 +1192,7 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_MODE); myESP.mqttSubscribe(TOPIC_BOILER_WWACTIVATED); myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP); + myESP.mqttSubscribe(TOPIC_BOILER_CMD_COMFORT); myESP.mqttSubscribe(TOPIC_SHOWER_TIMER); myESP.mqttSubscribe(TOPIC_SHOWER_ALERT); myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT); @@ -920,13 +1247,25 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) { // boiler wwtemp changes if (strcmp(topic, TOPIC_BOILER_CMD_WWTEMP) == 0) { - float f = strtof((char *)message, 0); - char s[10] = {0}; - myDebug("MQTT topic: boiler warm water temperature value %s", _float_to_char(s, f)); - ems_setWarmWaterTemp(f); + uint8_t t = atoi((char *)message); + myDebug("MQTT topic: boiler warm water temperature value %d", t); + ems_setWarmWaterTemp(t); publishValues(true); // publish back immediately } + // boiler ww comfort setting + if (strcmp(topic, TOPIC_BOILER_CMD_COMFORT) == 0) { + myDebug("MQTT topic: boiler warm water comfort value is %s", message); + if (strcmp((char *)message, "hot") == 0) { + ems_setWarmWaterModeComfort(1); + } else if (strcmp((char *)message, "comfort") == 0) { + ems_setWarmWaterModeComfort(2); + } else if (strcmp((char *)message, "intelligent") == 0) { + ems_setWarmWaterModeComfort(3); + } + // publishValues(true); // publish back immediately + } + // shower timer if (strcmp(topic, TOPIC_SHOWER_TIMER) == 0) { if (message[0] == '1') { @@ -960,22 +1299,26 @@ void WIFICallback() { // This is done after we have a WiFi signal to avoid any resource conflicts if (myESP.getUseSerial()) { - myDebug("Warning! EMS bus disabled when in Serial mode. Use 'set serial off' to enable."); + myDebug("Warning! EMS bus disabled when in Serial mode. Use 'set serial off' to start EMS."); } else { emsuart_init(); myDebug("[UART] Opened Rx/Tx connection"); - // go and find the boiler and thermostat types - ems_discoverModels(); + if (!EMSESP_Status.silent_mode) { + // go and find the boiler and thermostat types, if not in silent mode + ems_discoverModels(); + } } } // Initialize the boiler settings and shower settings +// Most of these will be overwritten after the SPIFFS config file is loaded void initEMSESP() { // general settings - EMSESP_Status.shower_timer = BOILER_SHOWER_TIMER; - EMSESP_Status.shower_alert = BOILER_SHOWER_ALERT; - EMSESP_Status.led_enabled = true; // LED is on by default - EMSESP_Status.test_mode = false; + EMSESP_Status.shower_timer = false; + EMSESP_Status.shower_alert = false; + EMSESP_Status.led = true; // LED is on by default + EMSESP_Status.silent_mode = false; + EMSESP_Status.publish_wait = DEFAULT_PUBLISHWAIT; EMSESP_Status.timestamp = millis(); EMSESP_Status.dallas_sensors = 0; @@ -990,74 +1333,6 @@ void initEMSESP() { EMSESP_Shower.doingColdShot = false; } -// 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())) { - publishValues(false); - } -} - -// callback to light up the LED, called via Ticker every second -// fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off -void do_ledcheck() { - if (EMSESP_Status.led_enabled) { - if (ems_getBusConnected()) { - digitalWrite(EMSESP_Status.led_gpio, (EMSESP_Status.led_gpio == LED_BUILTIN) ? LOW : HIGH); // light on. For onboard LED high=off - } else { - int state = digitalRead(EMSESP_Status.led_gpio); - digitalWrite(EMSESP_Status.led_gpio, !state); - } - } -} - -// Thermostat scan -void do_scanThermostat() { - if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { - myDebug("> Scanning thermostat message type #0x%02X..", scanThermostat_count); - ems_doReadCommand(scanThermostat_count, EMS_Thermostat.type_id); - scanThermostat_count++; - } -} - -// do a system health check every now and then to see if we all connections -void do_systemCheck() { - if ((!ems_getBusConnected()) && (!myESP.getUseSerial())) { - myDebug("Error! Unable to read from EMS bus. Retrying in %d seconds...", SYSTEMCHECK_TIME); - } -} - -// force calls to get data from EMS for the types that aren't sent as broadcasts -// only if we have a EMS connection -void do_regularUpdates() { - if ((ems_getBusConnected()) && (!myESP.getUseSerial())) { - myDebugLog("Calling scheduled data refresh from EMS devices.."); - ems_getThermostatValues(); - ems_getBoilerValues(); - } -} - -// turn off hot water to send a shot of cold -void _showerColdShotStart() { - if (EMSESP_Status.shower_alert) { - myDebugLog("[Shower] doing a shot of cold water"); - ems_setWarmTapWaterActivated(false); - EMSESP_Shower.doingColdShot = true; - // start the timer for n seconds which will reset the water back to hot - showerColdShotStopTimer.attach(SHOWER_COLDSHOT_DURATION, _showerColdShotStop); - } -} - -// turn back on the hot water for the shower -void _showerColdShotStop() { - if (EMSESP_Shower.doingColdShot) { - myDebugLog("[Shower] finished shot of cold. hot water back on"); - ems_setWarmTapWaterActivated(true); - EMSESP_Shower.doingColdShot = false; - showerColdShotStopTimer.detach(); // disable the timer - } -} - /* * Shower Logic */ @@ -1065,7 +1340,7 @@ void showerCheck() { // if already in cold mode, ignore all this logic until we're out of the cold blast if (!EMSESP_Shower.doingColdShot) { // is the hot water running? - if (EMS_Boiler.tapwaterActive) { + if (EMS_Boiler.tapwaterActive == 1) { // if heater was previously off, start the timer if (EMSESP_Shower.timerStart == 0) { // hot water just started... @@ -1157,8 +1432,8 @@ void setup() { MQTT_WILL_OFFLINE_PAYLOAD, MQTTCallback); - // OTA callback which is called when OTA is starting - myESP.setOTA(OTACallback); + // OTA callback which is called when OTA is starting and stopping + myESP.setOTA(OTACallback_pre, OTACallback_post); // custom settings in SPIFFS myESP.setSettings(FSCallback, SettingsCallback); @@ -1166,10 +1441,13 @@ void setup() { // start up all the services myESP.begin(APP_HOSTNAME, APP_NAME, APP_VERSION); + // at this point we have the settings from our internall SPIFFS config file + // enable regular checks if not in test mode - if (!EMSESP_Status.test_mode) { - publishValuesTimer.attach(PUBLISHVALUES_TIME, do_publishValues); // post MQTT values - regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS + if (!EMSESP_Status.silent_mode) { + publishValuesTimer.attach(EMSESP_Status.publish_wait, do_publishValues); // post MQTT EMS values + publishSensorValuesTimer.attach(EMSESP_Status.publish_wait, do_publishSensorValues); // post MQTT sensor values + regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS } // set pin for LED @@ -1180,7 +1458,7 @@ void setup() { } // check for Dallas sensors - EMSESP_Status.dallas_sensors = ds18.setup(EMSESP_Status.dallas_gpio); // returns #sensors + EMSESP_Status.dallas_sensors = ds18.setup(EMSESP_Status.dallas_gpio, EMSESP_Status.dallas_parasite); // returns #sensors } // @@ -1192,14 +1470,15 @@ void loop() { // the main loop myESP.loop(); - // check Dallas sensors + // check Dallas sensors, every 2 seconds + // these values are published to MQTT seperately via the timer publishSensorValuesTimer if (EMSESP_Status.dallas_sensors != 0) { ds18.loop(); } // publish the values to MQTT, only if the values have changed // although we don't want to publish when doing a deep scan of the thermostat - if (ems_getEmsRefreshed() && (scanThermostat_count == 0) && (!EMSESP_Status.test_mode)) { + if (ems_getEmsRefreshed() && (scanThermostat_count == 0) && (!EMSESP_Status.silent_mode)) { publishValues(false); ems_setEmsRefreshed(false); // reset } @@ -1208,4 +1487,8 @@ void loop() { if (EMSESP_Status.shower_timer) { showerCheck(); } + + if (EMSESP_DELAY != 0) { + delay(EMSESP_DELAY); // some time to WiFi and everything else to catch up, and prevent overheating + } } diff --git a/src/ems.cpp b/src/ems.cpp index 76b263e3..2e640915 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -14,49 +14,58 @@ #include #include // std::list -// myESP +// myESP for logging to telnet and serial #define myDebug(...) myESP.myDebug(__VA_ARGS__) _EMS_Sys_Status EMS_Sys_Status; // EMS Status CircularBuffer<_EMS_TxTelegram, EMS_TX_TELEGRAM_QUEUE_MAX> EMS_TxQueue; // FIFO queue for Tx send buffer -// callbacks per type +// +// process callbacks per type +// + +// macros used in the _process* functions +#define _toByte(i) (data[i]) +#define _toShort(i) ((data[i] << 8) + data[i + 1]) +#define _toLong(i) ((data[i] << 16) + (data[i + 1] << 8) + (data[i + 2])) +#define _bitRead(i, bit) (((data[i]) >> (bit)) & 0x01) // generic -void _process_Version(uint8_t type, uint8_t * data, uint8_t length); +void _process_Version(uint8_t src, uint8_t * data, uint8_t length); // Boiler and Buderus devices -void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBAMonitorSlow(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBAMonitorWWMessage(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBATotalUptimeMessage(uint8_t type, uint8_t * data, uint8_t length); -void _process_UBAParametersMessage(uint8_t type, uint8_t * data, uint8_t length); -void _process_SetPoints(uint8_t type, uint8_t * data, uint8_t length); +void _process_UBAMonitorFast(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBAMonitorSlow(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBAMonitorWWMessage(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBAParameterWW(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBATotalUptimeMessage(uint8_t src, uint8_t * data, uint8_t length); +void _process_UBAParametersMessage(uint8_t src, uint8_t * data, uint8_t length); +void _process_SetPoints(uint8_t src, uint8_t * data, uint8_t length); +void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length); // Common for most thermostats -void _process_RCTime(uint8_t type, uint8_t * data, uint8_t length); -void _process_RCOutdoorTempMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RCTime(uint8_t src, uint8_t * data, uint8_t length); +void _process_RCOutdoorTempMessage(uint8_t src, uint8_t * data, uint8_t length); // RC10 -void _process_RC10Set(uint8_t type, uint8_t * data, uint8_t length); -void _process_RC10StatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC10Set(uint8_t src, uint8_t * data, uint8_t length); +void _process_RC10StatusMessage(uint8_t src, uint8_t * data, uint8_t length); // RC20 -void _process_RC20Set(uint8_t type, uint8_t * data, uint8_t length); -void _process_RC20StatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC20Set(uint8_t src, uint8_t * data, uint8_t length); +void _process_RC20StatusMessage(uint8_t src, uint8_t * data, uint8_t length); // RC30 -void _process_RC30Set(uint8_t type, uint8_t * data, uint8_t length); -void _process_RC30StatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC30Set(uint8_t src, uint8_t * data, uint8_t length); +void _process_RC30StatusMessage(uint8_t src, uint8_t * data, uint8_t length); // RC35 -void _process_RC35Set(uint8_t type, uint8_t * data, uint8_t length); -void _process_RC35StatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC35Set(uint8_t src, uint8_t * data, uint8_t length); +void _process_RC35StatusMessage(uint8_t src, uint8_t * data, uint8_t length); // Easy -void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_EasyStatusMessage(uint8_t src, uint8_t * data, uint8_t length); /* * Recognized EMS types and the functions they call to process the telegrams @@ -77,6 +86,9 @@ const _EMS_Type EMS_Types[] = { {EMS_MODEL_UBA, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", _process_UBAParametersMessage}, {EMS_MODEL_UBA, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, + // Other devices + {EMS_MODEL_OTHER, EMS_TYPE_SM10Monitor, "SM10Monitor", _process_SM10Monitor}, + // RC10 {EMS_MODEL_RC10, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, {EMS_MODEL_RC10, EMS_TYPE_RC10Set, "RC10Set", _process_RC10Set}, @@ -115,17 +127,18 @@ const _EMS_Type EMS_Types[] = { {EMS_MODEL_EASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, {EMS_MODEL_BOSCHEASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, - }; -// calculate sizes of arrays +// calculate sizes of arrays at compile uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types -uint8_t _Boiler_Types_max = ArraySize(Boiler_Types); // number of models +uint8_t _Boiler_Types_max = ArraySize(Boiler_Types); // number of boiler models +uint8_t _Other_Types_max = ArraySize(Other_Types); // number of other ems devices uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types // these structs contain the data we store from the Boiler and Thermostat -_EMS_Boiler EMS_Boiler; -_EMS_Thermostat EMS_Thermostat; +_EMS_Boiler EMS_Boiler; // for boiler +_EMS_Thermostat EMS_Thermostat; // for thermostat +_EMS_Other EMS_Other; // for other known EMS devices // CRC lookup table with poly 12 for faster checking const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, @@ -162,12 +175,13 @@ void ems_init() { EMS_Sys_Status.emsBusConnected = false; EMS_Sys_Status.emsRxTimestamp = 0; EMS_Sys_Status.emsTxCapable = false; + EMS_Sys_Status.emsTxDisabled = false; EMS_Sys_Status.emsPollTimestamp = 0; EMS_Sys_Status.txRetryCount = 0; // thermostat - EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_FLOAT_NOTSET; - EMS_Thermostat.curr_roomTemp = EMS_VALUE_FLOAT_NOTSET; + EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_SHORT_NOTSET; + EMS_Thermostat.curr_roomTemp = EMS_VALUE_SHORT_NOTSET; EMS_Thermostat.hour = 0; EMS_Thermostat.minute = 0; EMS_Thermostat.second = 0; @@ -190,8 +204,8 @@ void ems_init() { // UBAMonitorFast EMS_Boiler.selFlowTemp = EMS_VALUE_INT_NOTSET; // Selected flow temperature - EMS_Boiler.curFlowTemp = EMS_VALUE_FLOAT_NOTSET; // Current flow temperature - EMS_Boiler.retTemp = EMS_VALUE_FLOAT_NOTSET; // Return temperature + EMS_Boiler.curFlowTemp = EMS_VALUE_SHORT_NOTSET; // Current flow temperature + EMS_Boiler.retTemp = EMS_VALUE_SHORT_NOTSET; // Return temperature EMS_Boiler.burnGas = EMS_VALUE_INT_NOTSET; // Gas on/off EMS_Boiler.fanWork = EMS_VALUE_INT_NOTSET; // Fan on/off EMS_Boiler.ignWork = EMS_VALUE_INT_NOTSET; // Ignition on/off @@ -200,20 +214,21 @@ void ems_init() { EMS_Boiler.wWCirc = EMS_VALUE_INT_NOTSET; // Circulation on/off EMS_Boiler.selBurnPow = EMS_VALUE_INT_NOTSET; // Burner max power EMS_Boiler.curBurnPow = EMS_VALUE_INT_NOTSET; // Burner current power - EMS_Boiler.flameCurr = EMS_VALUE_FLOAT_NOTSET; // Flame current in micro amps - EMS_Boiler.sysPress = EMS_VALUE_FLOAT_NOTSET; // System pressure + EMS_Boiler.flameCurr = EMS_VALUE_SHORT_NOTSET; // Flame current in micro amps + EMS_Boiler.sysPress = EMS_VALUE_INT_NOTSET; // System pressure strlcpy(EMS_Boiler.serviceCodeChar, "??", sizeof(EMS_Boiler.serviceCodeChar)); + EMS_Boiler.serviceCode = EMS_VALUE_SHORT_NOTSET; // UBAMonitorSlow - EMS_Boiler.extTemp = EMS_VALUE_FLOAT_NOTSET; // Outside temperature - EMS_Boiler.boilTemp = EMS_VALUE_FLOAT_NOTSET; // Boiler temperature + EMS_Boiler.extTemp = EMS_VALUE_SHORT_NOTSET; // Outside temperature + EMS_Boiler.boilTemp = EMS_VALUE_SHORT_NOTSET; // Boiler temperature EMS_Boiler.pumpMod = EMS_VALUE_INT_NOTSET; // Pump modulation EMS_Boiler.burnStarts = EMS_VALUE_LONG_NOTSET; // # burner restarts EMS_Boiler.burnWorkMin = EMS_VALUE_LONG_NOTSET; // Total burner operating time EMS_Boiler.heatWorkMin = EMS_VALUE_LONG_NOTSET; // Total heat operating time // UBAMonitorWWMessage - EMS_Boiler.wWCurTmp = EMS_VALUE_FLOAT_NOTSET; // Warm Water current temperature: + EMS_Boiler.wWCurTmp = EMS_VALUE_SHORT_NOTSET; // Warm Water current temperature: EMS_Boiler.wWStarts = EMS_VALUE_LONG_NOTSET; // Warm Water # starts EMS_Boiler.wWWorkM = EMS_VALUE_LONG_NOTSET; // Warm Water # minutes EMS_Boiler.wWOneTime = EMS_VALUE_INT_NOTSET; // Warm Water one time function on/off @@ -227,18 +242,27 @@ void ems_init() { EMS_Boiler.pump_mod_max = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation max. power EMS_Boiler.pump_mod_min = EMS_VALUE_INT_NOTSET; // Boiler circuit pump modulation min. power + // Other EMS devices values + EMS_Other.SM10collectorTemp = EMS_VALUE_SHORT_NOTSET; // collector temp from SM10 + EMS_Other.SM10bottomTemp = EMS_VALUE_SHORT_NOTSET; // bottom temp from SM10 + EMS_Other.SM10pumpModulation = EMS_VALUE_INT_NOTSET; // modulation solar pump SM10 + EMS_Other.SM10pump = EMS_VALUE_INT_NOTSET; // pump active + // calculated values EMS_Boiler.tapwaterActive = EMS_VALUE_INT_NOTSET; // Hot tap water is on/off EMS_Boiler.heatingActive = EMS_VALUE_INT_NOTSET; // Central heating is on/off // set boiler type EMS_Boiler.product_id = 0; - strlcpy(EMS_Boiler.version, "not set", sizeof(EMS_Boiler.version)); + strlcpy(EMS_Boiler.version, "?", sizeof(EMS_Boiler.version)); // set thermostat model EMS_Thermostat.model_id = EMS_MODEL_NONE; EMS_Thermostat.product_id = 0; - strlcpy(EMS_Thermostat.version, "not set", sizeof(EMS_Thermostat.version)); + strlcpy(EMS_Thermostat.version, "?", sizeof(EMS_Thermostat.version)); + + // set other types + EMS_Other.SM10 = false; // default logging is none ems_setLogging(EMS_SYS_LOGGING_DEFAULT); @@ -274,6 +298,10 @@ uint8_t ems_getThermostatModel() { return (EMS_Thermostat.model_id); } +void ems_setTxDisabled(bool b) { + EMS_Sys_Status.emsTxDisabled = b; +} + bool ems_getTxCapable() { if ((millis() - EMS_Sys_Status.emsPollTimestamp) > EMS_POLL_TIMEOUT) { EMS_Sys_Status.emsTxCapable = false; @@ -326,35 +354,39 @@ uint8_t _crcCalculator(uint8_t * data, uint8_t len) { return crc; } -/** - * function to turn a telegram int (2 bytes) to a float. The source is *10 - * negative values are stored as 1-compliment (https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c) - */ -float _toFloat(uint8_t i, uint8_t * data) { - // if the MSB is set, it's a negative number or an error - if ((data[i] & 0x80) == 0x80) { - // check if its an invalid number - // 0x8000 is used when sensor is missing - if ((data[i] == 0x80) && (data[i + 1] == 0)) { - return (float)EMS_VALUE_FLOAT_NOTSET; // return -1 to indicate that is unknown - } - // its definitely a negative number - // assume its 1-compliment, otherwise we need add 1 to the total for 2-compliment - int16_t x = (data[i] << 8) + data[i + 1]; - return ((float)(x)) / 10; - } else { - // ...a positive number - return ((float)(((data[i] << 8) + data[i + 1]))) / 10; - } +// like itoa but for hex, and quicker +char * _hextoa(uint8_t value, char * buffer) { + char * p = buffer; + byte nib1 = (value >> 4) & 0x0F; + byte nib2 = (value >> 0) & 0x0F; + *p++ = nib1 < 0xA ? '0' + nib1 : 'A' + nib1 - 0xA; + *p++ = nib2 < 0xA ? '0' + nib2 : 'A' + nib2 - 0xA; + *p = '\0'; // null terminate just in case + return buffer; +} + +// for decimals 0 to 99, printed as a string +char * _smallitoa(uint8_t value, char * buffer) { + buffer[0] = ((value / 10) == 0) ? '0' : (value / 10) + '0'; + buffer[1] = (value % 10) + '0'; + buffer[2] = '\0'; + return buffer; } -// function to turn a telegram long (3 bytes) to a long int -uint32_t _toLong(uint8_t i, uint8_t * data) { - return (((data[i]) << 16) + ((data[i + 1]) << 8) + (data[i + 2])); +/* for decimals 0 to 999, printed as a string + * From @nomis + */ +char * _smallitoa3(uint16_t value, char * buffer) { + buffer[0] = ((value / 100) == 0) ? '0' : (value / 100) + '0'; + buffer[1] = (((value % 100) / 10) == 0) ? '0' : ((value % 100) / 10) + '0'; + buffer[2] = (value % 10) + '0'; + buffer[3] = '\0'; + return buffer; } /** * Find the pointer to the EMS_Types array for a given type ID + * or -1 if not found */ int _ems_findType(uint8_t type) { uint8_t i = 0; @@ -371,44 +403,28 @@ int _ems_findType(uint8_t type) { return (typeFound ? i : -1); } -// like itoa but for hex, and quick -char * _hextoa(uint8_t value, char * buffer) { - char * p = buffer; - byte nib1 = (value >> 4) & 0x0F; - byte nib2 = (value >> 0) & 0x0F; - *p++ = nib1 < 0xA ? '0' + nib1 : 'A' + nib1 - 0xA; - *p++ = nib2 < 0xA ? '0' + nib2 : 'A' + nib2 - 0xA; - *p = '\0'; // null terminate just in case - return buffer; -} - -// for decimals 0 to 99, printed as a string -char * _smallitoa(uint8_t value, char * buffer) { - buffer[0] = ((value / 10) == 0) ? '0' : (value / 10) + '0'; - buffer[1] = (value % 10) + '0'; - buffer[2] = '\0'; - return buffer; -} - /** * debug print a telegram to telnet/serial including the CRC * len is length in bytes including the CRC */ -void _debugPrintTelegram(const char * prefix, uint8_t * data, uint8_t len, const char * color) { +void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, const char * color) { if (EMS_Sys_Status.emsLogging <= EMS_SYS_LOGGING_BASIC) return; - char output_str[300] = {0}; // roughly EMS_MAX_TELEGRAM_LENGTH*3 + 20 - char buffer[16] = {0}; + char output_str[200] = {0}; + char buffer[16] = {0}; + uint8_t len = EMS_RxTelegram->length; + uint8_t * data = EMS_RxTelegram->telegram; - unsigned long upt = millis(); strlcpy(output_str, "(", sizeof(output_str)); strlcat(output_str, COLOR_CYAN, sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((upt / 3600000) % 24), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram->timestamp / 3600000) % 24), buffer), sizeof(output_str)); strlcat(output_str, ":", sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((upt / 60000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram->timestamp / 60000) % 60), buffer), sizeof(output_str)); strlcat(output_str, ":", sizeof(output_str)); - strlcat(output_str, _smallitoa((uint8_t)((upt / 1000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, _smallitoa((uint8_t)((EMS_RxTelegram->timestamp / 1000) % 60), buffer), sizeof(output_str)); + strlcat(output_str, ".", sizeof(output_str)); + strlcat(output_str, _smallitoa3(EMS_RxTelegram->timestamp % 1000, buffer), sizeof(output_str)); strlcat(output_str, COLOR_RESET, sizeof(output_str)); strlcat(output_str, ") ", sizeof(output_str)); @@ -450,21 +466,26 @@ void _ems_sendTelegram() { // we don't remove from the queue yet _EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first(); - // if we're in raw mode just fire and forget - if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_RAW) { - EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC - _debugPrintTelegram("Sending raw", EMS_TxTelegram.data, EMS_TxTelegram.length, COLOR_CYAN); // always show - emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx - EMS_TxQueue.shift(); // remove from queue - return; - } - // if there is no destination, also delete it from the queue if (EMS_TxTelegram.dest == EMS_ID_NONE) { EMS_TxQueue.shift(); // remove from queue return; } + + // if we're in raw mode just fire and forget + if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_RAW) { + EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC + _EMS_RxTelegram EMS_RxTelegram; + EMS_RxTelegram.length = EMS_TxTelegram.length; + EMS_RxTelegram.telegram = EMS_TxTelegram.data; + EMS_RxTelegram.timestamp = millis(); // now + _debugPrintTelegram("Sending raw", &EMS_RxTelegram, COLOR_CYAN); // always show + emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx + EMS_TxQueue.shift(); // remove from queue + return; + } + // create header EMS_TxTelegram.data[0] = EMS_ID_ME; // src // dest @@ -499,7 +520,11 @@ void _ems_sendTelegram() { snprintf(s, sizeof(s), "Sending validate of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F); } - _debugPrintTelegram(s, EMS_TxTelegram.data, EMS_TxTelegram.length, COLOR_CYAN); + _EMS_RxTelegram EMS_RxTelegram; + EMS_RxTelegram.length = EMS_TxTelegram.length; + EMS_RxTelegram.telegram = EMS_TxTelegram.data; + EMS_RxTelegram.timestamp = millis(); // now + _debugPrintTelegram(s, &EMS_RxTelegram, COLOR_CYAN); } // send the telegram to the UART Tx @@ -552,25 +577,46 @@ void _createValidate() { EMS_TxQueue.unshift(new_EMS_TxTelegram); // add back to queue making it first to be picked up next (FIFO) } -/** - * the main logic that parses the telegram message, triggered by an interrupt in emsuart.cpp + +/* + * Entry point triggered by an interrupt in emsuart.cpp * length is only data bytes, excluding the BRK * Read commands are asynchronous as they're handled by the interrupt - * When we receive a Poll Request we need to send any Tx packages quickly within a 200ms window + * When a telegram is processed we forcefully erase it from the stack to prevent overflow */ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { + if ((length != 0) && (telegram[0] != 0x00)) { + _ems_readTelegram(telegram, length); + } + // now clear the Rx buffer just be safe and prevent duplicates + for (uint8_t i = 0; i < EMS_MAXBUFFERSIZE; telegram[i++] = 0x00) + ; +} + +/** + * the main logic that parses the telegram message + * When we receive a Poll Request we need to send any Tx packages quickly within a 200ms window + */ +void _ems_readTelegram(uint8_t * telegram, uint8_t length) { + // create the Rx package + static _EMS_RxTelegram EMS_RxTelegram; + EMS_RxTelegram.length = length; + EMS_RxTelegram.telegram = telegram; + EMS_RxTelegram.timestamp = millis(); + // check if we just received a single byte - // it could well be a Poll request from the boiler to us which will have a value of 0x8B (0x0B | 0x80) + // it could well be a Poll request from the boiler for us, which will have a value of 0x8B (0x0B | 0x80) // or either a return code like 0x01 or 0x04 from the last Write command if (length == 1) { uint8_t value = telegram[0]; // 1st byte of data package // check first for a Poll for us if (value == (EMS_ID_ME | 0x80)) { - EMS_Sys_Status.emsPollTimestamp = millis(); // store when we received a last poll + EMS_Sys_Status.emsPollTimestamp = EMS_RxTelegram.timestamp; // store when we received a last poll EMS_Sys_Status.emsTxCapable = true; - // do we have something to send thats waiting in the Tx queue? if so send it if the Queue is not in a wait state + // do we have something to send thats waiting in the Tx queue? + // if so send it if the Queue is not in a wait state if ((!EMS_TxQueue.isEmpty()) && (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE)) { _ems_sendTelegram(); // perform the read/write command immediately } else { @@ -602,7 +648,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // ignore anything that doesn't resemble a proper telegram package // minimal is 5 bytes, excluding CRC at the end if (length <= 4) { - //_debugPrintTelegram("Noisy data:", telegram, length, COLOR_RED); + //_debugPrintTelegram("Noisy data:", &EMS_RxTelegram COLOR_RED); return; } @@ -612,7 +658,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { if (telegram[length - 1] != crc) { EMS_Sys_Status.emxCrcErr++; if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { - _debugPrintTelegram("Corrupt telegram:", telegram, length, COLOR_RED); + _debugPrintTelegram("Corrupt telegram:", &EMS_RxTelegram, COLOR_RED); } return; } @@ -631,28 +677,29 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) { // here we know its a valid incoming telegram of at least 6 bytes // we use this to see if we always have a connection to the boiler, in case of drop outs - EMS_Sys_Status.emsRxTimestamp = millis(); // timestamp of last read + EMS_Sys_Status.emsRxTimestamp = EMS_RxTelegram.timestamp; // timestamp of last read EMS_Sys_Status.emsBusConnected = true; // now lets process it and see what to do next - _processType(telegram, length); + _processType(&EMS_RxTelegram); } /** * print detailed telegram * and then call its callback if there is one defined */ -void _ems_processTelegram(uint8_t * telegram, uint8_t length) { +void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { // header - uint8_t src = telegram[0] & 0x7F; - uint8_t dest = telegram[1] & 0x7F; // remove 8th bit to handle both reads and writes - uint8_t type = telegram[2]; - uint8_t offset = telegram[3]; - uint8_t * data = telegram + 4; // data block starts at position 5 + uint8_t * telegram = EMS_RxTelegram->telegram; + uint8_t src = telegram[0] & 0x7F; + uint8_t dest = telegram[1] & 0x7F; // remove 8th bit to handle both reads and writes + uint8_t type = telegram[2]; + uint8_t offset = telegram[3]; + uint8_t * data = telegram + 4; // data block starts at position 5 // print detailed telegram data if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { - char output_str[300] = {0}; // roughly EMS_MAX_TELEGRAM_LENGTH*3 + 20 + char output_str[200] = {0}; char buffer[16] = {0}; char color_s[20] = {0}; @@ -661,6 +708,8 @@ void _ems_processTelegram(uint8_t * telegram, uint8_t length) { strlcpy(output_str, "Boiler", sizeof(output_str)); } else if (src == EMS_Thermostat.type_id) { strlcpy(output_str, "Thermostat", sizeof(output_str)); + } else if (src == EMS_ID_SM10) { + strlcpy(output_str, "SM10", sizeof(output_str)); } else { strlcpy(output_str, "0x", sizeof(output_str)); strlcat(output_str, _hextoa(src, buffer), sizeof(output_str)); @@ -678,6 +727,9 @@ void _ems_processTelegram(uint8_t * telegram, uint8_t length) { } else if (dest == EMS_Boiler.type_id) { strlcat(output_str, "Boiler", sizeof(output_str)); strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + } else if (dest == EMS_ID_SM10) { + strlcat(output_str, "SM10", sizeof(output_str)); + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); } else if (dest == EMS_Thermostat.type_id) { strlcat(output_str, "Thermostat", sizeof(output_str)); strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); @@ -694,43 +746,43 @@ void _ems_processTelegram(uint8_t * telegram, uint8_t length) { if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_THERMOSTAT) { // only print ones to/from thermostat if logging is set to thermostat only if ((src == EMS_Thermostat.type_id) || (dest == EMS_Thermostat.type_id)) { - _debugPrintTelegram(output_str, telegram, length, color_s); + _debugPrintTelegram(output_str, EMS_RxTelegram, color_s); } } else { // always print - _debugPrintTelegram(output_str, telegram, length, color_s); + _debugPrintTelegram(output_str, EMS_RxTelegram, 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; + bool typeFound = false; + uint8_t 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; + // is it common type for everyone? + // is it for us? So the src must match with either the boiler, thermostat or other devices + if ((EMS_Types[i].model_id == EMS_MODEL_ALL) + || ((src == EMS_Boiler.type_id) || (src == EMS_Thermostat.type_id) || (src == EMS_ID_SM10))) { + typeFound = true; + 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 (typeFound) { 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)) { + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) { 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); + if (offset == 0) { + (void)EMS_Types[i].processType_cb(src, data, EMS_RxTelegram->length - 5); } } } @@ -751,19 +803,21 @@ void _removeTxQueue() { * length is only data bytes, excluding the BRK * We only remove from the Tx queue if the read or write was successful */ -void _processType(uint8_t * telegram, uint8_t length) { +void _processType(_EMS_RxTelegram * EMS_RxTelegram) { + uint8_t * telegram = EMS_RxTelegram->telegram; + // header uint8_t src = telegram[0] & 0x7F; // removing 8th bit as we deal with both reads and writes here // if its an echo of ourselves from the master UBA, ignore if (src == EMS_ID_ME) { - //_debugPrintTelegram("Telegram echo:", telegram, length, COLOR_BLUE); + // _debugPrintTelegram("echo:", EMS_RxTelegram, COLOR_WHITE); return; } // if its a broadcast and we didn't just send anything, process it and exit if (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE) { - _ems_processTelegram(telegram, length); + _ems_processTelegram(EMS_RxTelegram); return; } @@ -775,13 +829,13 @@ void _processType(uint8_t * telegram, uint8_t length) { // and if not we probably didn't get any response so remove the last Tx from the queue and process the telegram anyway if ((telegram[1] & 0x7F) != EMS_ID_ME) { _removeTxQueue(); - _ems_processTelegram(telegram, length); + _ems_processTelegram(EMS_RxTelegram); return; } // first double check we actually have something in the queue if (EMS_TxQueue.isEmpty()) { - _ems_processTelegram(telegram, length); + _ems_processTelegram(EMS_RxTelegram); return; } @@ -816,7 +870,7 @@ void _processType(uint8_t * telegram, uint8_t length) { } } } - _ems_processTelegram(telegram, length); // process it always + _ems_processTelegram(EMS_RxTelegram); // process it always } if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) { @@ -871,23 +925,27 @@ void _processType(uint8_t * telegram, uint8_t length) { * using a quick hack for checking the heating. Selected Flow Temp >= 70 */ void _checkActive() { - // hot tap water, using flow to check insread of the burner power - EMS_Boiler.tapwaterActive = ((EMS_Boiler.wWCurFlow != 0) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + // hot tap water, using flow to check instead of the burner power + if (EMS_Boiler.wWCurFlow != EMS_VALUE_INT_NOTSET && EMS_Boiler.burnGas != EMS_VALUE_INT_NOTSET) { + EMS_Boiler.tapwaterActive = ((EMS_Boiler.wWCurFlow != 0) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + } // heating - EMS_Boiler.heatingActive = ((EMS_Boiler.selFlowTemp >= EMS_BOILER_SELFLOWTEMP_HEATING) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET && EMS_Boiler.burnGas != EMS_VALUE_INT_NOTSET) { + EMS_Boiler.heatingActive = ((EMS_Boiler.selFlowTemp >= EMS_BOILER_SELFLOWTEMP_HEATING) && (EMS_Boiler.burnGas == EMS_VALUE_INT_ON)); + } } /** * UBAParameterWW - type 0x33 - warm water parameters * received only after requested (not broadcasted) */ -void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Boiler.wWActivated = (data[1] == 0xFF); // 0xFF means on - EMS_Boiler.wWSelTemp = data[2]; - EMS_Boiler.wWCircPump = (data[6] == 0xFF); // 0xFF means on - EMS_Boiler.wWDesiredTemp = data[8]; - EMS_Boiler.wWComfort = (data[EMS_OFFSET_UBAParameterWW_wwComfort] == 0x00); +void _process_UBAParameterWW(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Boiler.wWActivated = (_toByte(1) == 0xFF); // 0xFF means on + EMS_Boiler.wWSelTemp = _toByte(2); + EMS_Boiler.wWCircPump = (_toByte(6) == 0xFF); // 0xFF means on + EMS_Boiler.wWDesiredTemp = _toByte(8); + EMS_Boiler.wWComfort = _toByte(EMS_OFFSET_UBAParameterWW_wwComfort); EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish } @@ -896,63 +954,63 @@ void _process_UBAParameterWW(uint8_t type, uint8_t * data, uint8_t length) { * UBATotalUptimeMessage - type 0x14 - total uptime * received only after requested (not broadcasted) */ -void _process_UBATotalUptimeMessage(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Boiler.UBAuptime = _toLong(0, data); +void _process_UBATotalUptimeMessage(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Boiler.UBAuptime = _toLong(0); EMS_Sys_Status.emsRefreshed = true; // when we receieve this, lets force an MQTT publish } /* * UBAParametersMessage - type 0x16 */ -void _process_UBAParametersMessage(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Boiler.heating_temp = data[1]; - EMS_Boiler.pump_mod_max = data[9]; - EMS_Boiler.pump_mod_min = data[10]; +void _process_UBAParametersMessage(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Boiler.heating_temp = _toByte(1); + EMS_Boiler.pump_mod_max = _toByte(9); + EMS_Boiler.pump_mod_min = _toByte(10); } /** * UBAMonitorWWMessage - type 0x34 - warm water monitor. 19 bytes long * received every 10 seconds */ -void _process_UBAMonitorWWMessage(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Boiler.wWCurTmp = _toFloat(1, data); - EMS_Boiler.wWStarts = _toLong(13, data); - EMS_Boiler.wWWorkM = _toLong(10, data); - EMS_Boiler.wWOneTime = bitRead(data[5], 1); - EMS_Boiler.wWCurFlow = data[9]; +void _process_UBAMonitorWWMessage(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Boiler.wWCurTmp = _toShort(1); + EMS_Boiler.wWStarts = _toLong(13); + EMS_Boiler.wWWorkM = _toLong(10); + EMS_Boiler.wWOneTime = _bitRead(5, 1); + EMS_Boiler.wWCurFlow = _toByte(9); } /** * UBAMonitorFast - type 0x18 - central heating monitor part 1 (25 bytes long) * received every 10 seconds */ -void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Boiler.selFlowTemp = data[0]; - EMS_Boiler.curFlowTemp = _toFloat(1, data); - EMS_Boiler.retTemp = _toFloat(13, data); +void _process_UBAMonitorFast(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Boiler.selFlowTemp = _toByte(0); + EMS_Boiler.curFlowTemp = _toShort(1); + EMS_Boiler.retTemp = _toShort(13); - uint8_t v = data[7]; - EMS_Boiler.burnGas = bitRead(v, 0); - EMS_Boiler.fanWork = bitRead(v, 2); - EMS_Boiler.ignWork = bitRead(v, 3); - EMS_Boiler.heatPmp = bitRead(v, 5); - EMS_Boiler.wWHeat = bitRead(v, 6); - EMS_Boiler.wWCirc = bitRead(v, 7); + EMS_Boiler.burnGas = _bitRead(7, 0); + EMS_Boiler.fanWork = _bitRead(7, 2); + EMS_Boiler.ignWork = _bitRead(7, 3); + EMS_Boiler.heatPmp = _bitRead(7, 5); + EMS_Boiler.wWHeat = _bitRead(7, 6); + EMS_Boiler.wWCirc = _bitRead(7, 7); - EMS_Boiler.curBurnPow = data[4]; - EMS_Boiler.selBurnPow = data[3]; // burn power max setting + EMS_Boiler.curBurnPow = _toByte(4); + EMS_Boiler.selBurnPow = _toByte(3); // burn power max setting - EMS_Boiler.flameCurr = _toFloat(15, data); + EMS_Boiler.flameCurr = _toShort(15); // read the service code / installation status as appears on the display - EMS_Boiler.serviceCodeChar[0] = char(data[18]); // ascii character 1 - EMS_Boiler.serviceCodeChar[1] = char(data[19]); // ascii character 2 + EMS_Boiler.serviceCodeChar[0] = char(_toByte(18)); // ascii character 1 + EMS_Boiler.serviceCodeChar[1] = char(_toByte(19)); // ascii character 2 + EMS_Boiler.serviceCodeChar[2] = '\0'; // null terminate string - if (data[17] == 0xFF) { // missing value for system pressure - EMS_Boiler.sysPress = 0; - } else { - EMS_Boiler.sysPress = (((float)data[17]) / (float)10); - } + // read error code + EMS_Boiler.serviceCode = _toShort(20); + + // system pressure. FF means missing + EMS_Boiler.sysPress = _toByte(17); // this is *10 // at this point do a quick check to see if the hot water or heating is active _checkActive(); @@ -962,24 +1020,24 @@ void _process_UBAMonitorFast(uint8_t type, uint8_t * data, uint8_t length) { * UBAMonitorSlow - type 0x19 - central heating monitor part 2 (27 bytes long) * received every 60 seconds */ -void _process_UBAMonitorSlow(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Boiler.extTemp = _toFloat(0, data); // 0x8000 if not available - EMS_Boiler.boilTemp = _toFloat(2, data); // 0x8000 if not available - EMS_Boiler.pumpMod = data[9]; - EMS_Boiler.burnStarts = _toLong(10, data); - EMS_Boiler.burnWorkMin = _toLong(13, data); - EMS_Boiler.heatWorkMin = _toLong(19, data); +void _process_UBAMonitorSlow(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Boiler.extTemp = _toShort(0); // 0x8000 if not available + EMS_Boiler.boilTemp = _toShort(2); // 0x8000 if not available + EMS_Boiler.pumpMod = _toByte(9); + EMS_Boiler.burnStarts = _toLong(10); + EMS_Boiler.burnWorkMin = _toLong(13); + EMS_Boiler.heatWorkMin = _toLong(19); } - /** * type 0xB1 - data from the RC10 thermostat (0x17) * For reading the temp values only * received every 60 seconds + * e.g. 17 0B 91 00 80 1E 00 CB 27 00 00 00 00 05 01 00 CB 00 (CRC=47), #data=14 */ -void _process_RC10StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC10StatusMessage_setpoint]) / (float)2; - EMS_Thermostat.curr_roomTemp = ((float)data[EMS_TYPE_RC10StatusMessage_curr]) / (float)10; +void _process_RC10StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_TYPE_RC10StatusMessage_setpoint); // is * 2 + EMS_Thermostat.curr_roomTemp = _toByte(EMS_TYPE_RC10StatusMessage_curr); // is * 10 EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -989,9 +1047,9 @@ void _process_RC10StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { * For reading the temp values only * received every 60 seconds */ -void _process_RC20StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC20StatusMessage_setpoint]) / (float)2; - EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC20StatusMessage_curr, data); +void _process_RC20StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_TYPE_RC20StatusMessage_setpoint); // is * 2 + EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_RC20StatusMessage_curr); // is * 10 EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1001,9 +1059,9 @@ void _process_RC20StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { * 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); +void _process_RC30StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_TYPE_RC30StatusMessage_setpoint); // is * 2 + EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_RC30StatusMessage_curr); // note, its 2 bytes here EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1013,14 +1071,14 @@ void _process_RC30StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { * For reading the temp values only * received every 60 seconds */ -void _process_RC35StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC35StatusMessage_setpoint]) / (float)2; +void _process_RC35StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_TYPE_RC35StatusMessage_setpoint); // is * 2 // check if temp sensor is unavailable if ((data[0] == 0x7D) && (data[1] = 0x00)) { - EMS_Thermostat.curr_roomTemp = EMS_VALUE_FLOAT_NOTSET; + EMS_Thermostat.curr_roomTemp = EMS_VALUE_SHORT_NOTSET; } else { - EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC35StatusMessage_curr, data); + EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_RC35StatusMessage_curr); } EMS_Thermostat.day_mode = bitRead(data[EMS_OFFSET_RC35Get_mode_day], 1); //get day mode flag EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT @@ -1028,11 +1086,11 @@ void _process_RC35StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { /** * type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long - * The Easy has a digital precision of its floats to 2 decimal places, so values is divided by 100 + * The Easy has a digital precision of its floats to 2 decimal places, so values must be divided by 100 */ -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; +void _process_EasyStatusMessage(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Thermostat.curr_roomTemp = _toShort(EMS_TYPE_EasyStatusMessage_curr); // is *100 + EMS_Thermostat.setpoint_roomTemp = _toShort(EMS_TYPE_EasyStatusMessage_setpoint); // is *100 EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1041,7 +1099,7 @@ void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length) { * type 0xB0 - for reading the mode from the RC10 thermostat (0x17) * received only after requested */ -void _process_RC10Set(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RC10Set(uint8_t src, uint8_t * data, uint8_t length) { // mode not implemented yet } @@ -1049,16 +1107,16 @@ void _process_RC10Set(uint8_t type, uint8_t * data, uint8_t length) { * type 0xA8 - for reading the mode from the RC20 thermostat (0x17) * received only after requested */ -void _process_RC20Set(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Thermostat.mode = data[EMS_OFFSET_RC20Set_mode]; +void _process_RC20Set(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Thermostat.mode = _toByte(EMS_OFFSET_RC20Set_mode); } /** * type 0xA7 - for reading the mode from the RC30 thermostat (0x10) * received only after requested */ -void _process_RC30Set(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Thermostat.mode = data[EMS_OFFSET_RC30Set_mode]; +void _process_RC30Set(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Thermostat.mode = _toByte(EMS_OFFSET_RC30Set_mode); } /** @@ -1066,31 +1124,75 @@ void _process_RC30Set(uint8_t type, uint8_t * data, uint8_t length) { * Working Mode Heating Circuit 1 (HC1) * received only after requested */ -void _process_RC35Set(uint8_t type, uint8_t * data, uint8_t length) { - EMS_Thermostat.mode = data[EMS_OFFSET_RC35Set_mode]; +void _process_RC35Set(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Thermostat.mode = _toByte(EMS_OFFSET_RC35Set_mode); } /** * type 0xA3 - for external temp settings from the the RC* thermostats */ -void _process_RCOutdoorTempMessage(uint8_t type, uint8_t * data, uint8_t length) { +void _process_RCOutdoorTempMessage(uint8_t src, uint8_t * data, uint8_t length) { // add support here if you're reading external sensors } +/* + * SM10Monitor - type 0x97 + */ +void _process_SM10Monitor(uint8_t src, uint8_t * data, uint8_t length) { + EMS_Other.SM10collectorTemp = _toShort(2); // collector temp from SM10, is *10 + EMS_Other.SM10bottomTemp = _toShort(5); // bottom temp from SM10, is *10 + EMS_Other.SM10pumpModulation = _toByte(4); // modulation solar pump + EMS_Other.SM10pump = _bitRead(7, 1); // active if bit 1 is set + + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT +} + +/** + * UBASetPoint 0x1A + */ +void _process_SetPoints(uint8_t src, uint8_t * data, uint8_t length) { + /* + if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { + if (length != 0) { + uint8_t setpoint = data[0]; + uint8_t hk_power = data[1]; + uint8_t ww_power = data[2]; + myDebug(" SetPoint=%d, hk_power=%d, ww_power=%d", setpoint, hk_power, ww_power); + } + } + */ +} + +/** + * process_RCTime - type 0x06 - date and time from a thermostat - 14 bytes long + * common for all thermostats + */ +void _process_RCTime(uint8_t src, uint8_t * data, uint8_t length) { + if ((EMS_Thermostat.model_id == EMS_MODEL_EASY) || (EMS_Thermostat.model_id == EMS_MODEL_BOSCHEASY)) { + return; // not supported + } + + EMS_Thermostat.hour = _toByte(2); + EMS_Thermostat.minute = _toByte(4); + EMS_Thermostat.second = _toByte(5); + EMS_Thermostat.day = _toByte(3); + EMS_Thermostat.month = _toByte(1); + EMS_Thermostat.year = _toByte(0); +} + /** * type 0x02 - get the firmware version and type of an EMS device * look up known devices via the product id and setup if not already set */ -void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { +void _process_Version(uint8_t src, uint8_t * data, uint8_t length) { // ignore short messages that we can't interpret if (length < 3) { return; } - bool do_save = false; - uint8_t product_id = data[0]; + uint8_t product_id = _toByte(0); char version[10] = {0}; - snprintf(version, sizeof(version), "%02d.%02d", data[1], data[2]); + snprintf(version, sizeof(version), "%02d.%02d", _toByte(1), _toByte(2)); // see if its a known boiler int i = 0; @@ -1105,7 +1207,7 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { if (typeFound) { // its a boiler - myDebug("Boiler type device found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + myDebug("Boiler found. Model %s with TypeID 0x%02X, ProductID %d, Version %s", Boiler_Types[i].model_string, Boiler_Types[i].type_id, product_id, @@ -1114,7 +1216,7 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { // if its a boiler set it // it will take the first one found in the list if ((EMS_Boiler.type_id == EMS_ID_NONE) || (EMS_Boiler.type_id == Boiler_Types[i].type_id)) { - myDebug("* Setting Boiler type to Model %s, TypeID 0x%02X, Product ID %d, Version %s", + myDebug("* Setting Boiler type to Model %s, TypeID 0x%02X, ProductID %d, Version %s", Boiler_Types[i].model_string, Boiler_Types[i].type_id, product_id, @@ -1124,7 +1226,7 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { EMS_Boiler.product_id = Boiler_Types[i].product_id; strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version)); - do_save = true; + myESP.fs_saveConfig(); // save config to SPIFFS ems_getBoilerValues(); // get Boiler values that we would usually have to wait for } @@ -1144,7 +1246,7 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { if (typeFound) { // its a known thermostat if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Thermostat found. Model %s with TypeID 0x%02X, Product ID %d, Version %s", + myDebug("Thermostat found. Model %s with TypeID 0x%02X, ProductID %d, Version %s", Thermostat_Types[i].model_string, Thermostat_Types[i].type_id, product_id, @@ -1154,7 +1256,7 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { // if we don't have a thermostat set, use this one if ((EMS_Thermostat.type_id == EMS_ID_NONE) || (EMS_Thermostat.model_id == EMS_MODEL_NONE) || (EMS_Thermostat.type_id == Thermostat_Types[i].type_id)) { - myDebug("* Setting Thermostat type to Model %s, TypeID 0x%02X, Product ID %d, Version %s", + myDebug("* Setting Thermostat type to Model %s, TypeID 0x%02X, ProductID %d, Version %s", Thermostat_Types[i].model_string, Thermostat_Types[i].type_id, product_id, @@ -1167,18 +1269,43 @@ void _process_Version(uint8_t type, uint8_t * data, uint8_t length) { EMS_Thermostat.product_id = product_id; strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version)); - do_save = true; + myESP.fs_saveConfig(); // save config to SPIFFS // get Thermostat values (if supported) ems_getThermostatValues(); } - } else { - myDebug("Unrecognized device found. TypeID 0x%02X, Product ID %d, Version %s", type, product_id, version); + return; } - // if the boiler or thermostat values have changed, save them to SPIFFS - if (do_save) { - myESP.fs_saveConfig(); + // finally look for the other EMS devices + i = 0; + while (i < _Other_Types_max) { + if (Other_Types[i].product_id == product_id) { + typeFound = true; // we have a matching product id. i is the index. + break; + } + i++; + } + + if (typeFound) { + myDebug("Device found. Model %s with TypeID 0x%02X, ProductID %d, Version %s", + Other_Types[i].model_string, + Other_Types[i].type_id, + product_id, + version); + + // see if this is a Solar Module SM10 + if (Other_Types[i].type_id == EMS_ID_SM10) { + EMS_Other.SM10 = true; // we have detected a SM10 + myDebug("SM10 Solar Module support enabled."); + } + + // fetch other values + ems_getOtherValues(); + return; + + } else { + myDebug("Unrecognized device found. TypeID 0x%02X, ProductID %d, Version %s", src, product_id, version); } } @@ -1189,6 +1316,9 @@ void ems_discoverModels() { // boiler ems_doReadCommand(EMS_TYPE_Version, EMS_Boiler.type_id); // get version details of boiler + // solar module + ems_doReadCommand(EMS_TYPE_Version, EMS_ID_SM10); // check if there is Solar Module available + // thermostat // if it hasn't been set, auto discover it if (EMS_Thermostat.type_id == EMS_ID_NONE) { @@ -1225,7 +1355,7 @@ void _ems_setThermostatModel(uint8_t thermostat_modelid) { // set the thermostat if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) { - myDebug("Setting Thermostat. Model %s with TypeID 0x%02X, Product ID %d", + myDebug("Setting Thermostat. Model %s with TypeID 0x%02X, ProductID %d", thermostat_type->model_string, thermostat_type->type_id, thermostat_type->product_id); @@ -1238,39 +1368,6 @@ void _ems_setThermostatModel(uint8_t thermostat_modelid) { EMS_Thermostat.write_supported = thermostat_type->write_supported; } -/** - * UBASetPoint 0x1A - */ -void _process_SetPoints(uint8_t type, uint8_t * data, uint8_t length) { - /* - if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) { - if (length != 0) { - uint8_t setpoint = data[0]; - uint8_t hk_power = data[1]; - uint8_t ww_power = data[2]; - myDebug(" SetPoint=%d, hk_power=%d, ww_power=%d", setpoint, hk_power, ww_power); - } - } - */ -} - -/** - * process_RCTime - type 0x06 - date and time from a thermostat - 14 bytes long - * common for all thermostats - */ -void _process_RCTime(uint8_t type, uint8_t * data, uint8_t length) { - if ((EMS_Thermostat.model_id == EMS_MODEL_EASY) || (EMS_Thermostat.model_id == EMS_MODEL_BOSCHEASY)) { - return; // not supported - } - - EMS_Thermostat.hour = data[2]; - EMS_Thermostat.minute = data[4]; - EMS_Thermostat.second = data[5]; - EMS_Thermostat.day = data[3]; - EMS_Thermostat.month = data[1]; - EMS_Thermostat.year = data[0]; -} - /** * Print the Tx queue - for debugging */ @@ -1367,6 +1464,15 @@ void ems_getBoilerValues() { ems_doReadCommand(EMS_TYPE_UBATotalUptimeMessage, EMS_Boiler.type_id); // get uptime from boiler } +/* + * Get other values from EMS devices + */ +void ems_getOtherValues() { + if (EMS_Other.SM10) { + ems_doReadCommand(EMS_TYPE_SM10Monitor, EMS_ID_SM10); // fetch all from SM10Monitor, e.g. 0B B0 97 00 16 + } +} + /** * returns current thermostat type as a string */ @@ -1375,9 +1481,9 @@ char * ems_getThermostatDescription(char * buffer) { if (!ems_getThermostatEnabled()) { strlcpy(buffer, "", size); } else { - // find the boiler details - int i = 0; - bool found = false; + int i = 0; + bool found = false; + char tmp[6] = {0}; // scan through known ID types while (i < _Thermostat_Types_max) { @@ -1387,19 +1493,19 @@ char * ems_getThermostatDescription(char * buffer) { } i++; } + if (found) { strlcpy(buffer, Thermostat_Types[i].model_string, size); } else { - strlcpy(buffer, "Generic Type", size); + strlcpy(buffer, "TypeID: 0x", size); + strlcat(buffer, _hextoa(EMS_Thermostat.type_id, tmp), size); } - char tmp[6] = {0}; - strlcat(buffer, " [Type ID: 0x", size); - strlcat(buffer, _hextoa(EMS_Thermostat.type_id, tmp), size); - strlcat(buffer, "] Product ID:", size); + strlcat(buffer, " (ProductID:", size); strlcat(buffer, itoa(EMS_Thermostat.product_id, tmp, 10), size); strlcat(buffer, " Version:", size); strlcat(buffer, EMS_Thermostat.version, size); + strlcat(buffer, ")", size); } return buffer; @@ -1413,9 +1519,9 @@ char * ems_getBoilerDescription(char * buffer) { if (!ems_getBoilerEnabled()) { strlcpy(buffer, "", size); } else { - // find the boiler details - int i = 0; - bool found = false; + int i = 0; + bool found = false; + char tmp[6] = {0}; // scan through known ID types while (i < _Boiler_Types_max) { @@ -1428,16 +1534,15 @@ char * ems_getBoilerDescription(char * buffer) { if (found) { strlcpy(buffer, Boiler_Types[i].model_string, size); } else { - strlcpy(buffer, "Generic Type", size); + strlcpy(buffer, "TypeID: 0x", size); + strlcat(buffer, _hextoa(EMS_Boiler.type_id, tmp), size); } - char tmp[6] = {0}; - strlcat(buffer, " [Type ID: 0x", size); - strlcat(buffer, _hextoa(EMS_Boiler.type_id, tmp), size); - strlcat(buffer, "] Product ID:", size); + strlcat(buffer, " (ProductID:", size); strlcat(buffer, itoa(EMS_Boiler.product_id, tmp, 10), size); strlcat(buffer, " Version:", size); strlcat(buffer, EMS_Boiler.version, size); + strlcat(buffer, ")", size); } return buffer; @@ -1449,7 +1554,7 @@ char * ems_getBoilerDescription(char * buffer) { void ems_scanDevices() { myDebug("Started scan of EMS bus for known devices"); - std::list Device_Ids; // new list + std::list Device_Ids; // create a new list // copy over boilers for (_Boiler_Type bt : Boiler_Types) { @@ -1460,6 +1565,12 @@ void ems_scanDevices() { for (_Thermostat_Type tt : Thermostat_Types) { Device_Ids.push_back(tt.type_id); } + + // copy over others + for (_Other_Type ot : Other_Types) { + Device_Ids.push_back(ot.type_id); + } + // remove duplicates and reserved IDs (like our own device) Device_Ids.sort(); Device_Ids.unique(); @@ -1478,13 +1589,16 @@ void ems_printAllTypes() { uint8_t i; myDebug("\nThese %d boiler type devices are in the library:", _Boiler_Types_max); - for (i = 0; i < _Boiler_Types_max; i++) { - myDebug(" %s, type ID:0x%02X Product ID:%d", Boiler_Types[i].model_string, Boiler_Types[i].type_id, Boiler_Types[i].product_id); + myDebug(" %s, type ID:0x%02X ProductID:%d", Boiler_Types[i].model_string, Boiler_Types[i].type_id, Boiler_Types[i].product_id); } - myDebug("\nThese telegram type IDs are recognized for the selected boiler:"); + myDebug("\nThese %d EMS devices are in the library:", _Other_Types_max); + for (i = 0; i < _Other_Types_max; i++) { + myDebug(" %s, type ID:0x%02X ProductID:%d", Other_Types[i].model_string, Other_Types[i].type_id, Other_Types[i].product_id); + } + myDebug("\nThese telegram type IDs are recognized for the selected boiler:"); for (i = 0; i < _EMS_Types_max; i++) { if ((EMS_Types[i].model_id == EMS_MODEL_ALL) || (EMS_Types[i].model_id == EMS_MODEL_UBA)) { myDebug(" type %02X (%s)", EMS_Types[i].type, EMS_Types[i].typeString); @@ -1493,7 +1607,7 @@ void ems_printAllTypes() { myDebug("\nThese %d thermostats models are supported:", _Thermostat_Types_max); for (i = 0; i < _Thermostat_Types_max; i++) { - myDebug(" %s, type ID:0x%02X Product ID:%d Read/Write support:%c%c", + myDebug(" %s, type ID:0x%02X ProductID:%d Read/Write support:%c%c", Thermostat_Types[i].model_string, Thermostat_Types[i].type_id, Thermostat_Types[i].product_id, @@ -1508,7 +1622,13 @@ 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; + } + + // if we're preventing all outbound traffic, quit + if (EMS_Sys_Status.emsTxDisabled) { + myDebug("in Silent Mode. All Tx is disabled."); return; } @@ -1550,18 +1670,22 @@ void ems_sendRawTelegram(char * telegram) { char * p; char value[10] = {0}; + if (EMS_Sys_Status.emsTxDisabled) { + return; // user has disabled all Tx + } + _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_Sys_Status.txRetryCount = 0; // reset retry counter // get first value, which should be the src - if ( (p = strtok(telegram, " ,")) ) { // delimiter + if ((p = strtok(telegram, " ,"))) { // delimiter strlcpy(value, p, sizeof(value)); EMS_TxTelegram.data[0] = (uint8_t)strtol(value, 0, 16); } // and interate until end while (p != 0) { - if ( (p = strtok(NULL, " ,")) ) { + if ((p = strtok(NULL, " ,"))) { strlcpy(value, p, sizeof(value)); uint8_t val = (uint8_t)strtol(value, 0, 16); EMS_TxTelegram.data[++count] = val; @@ -1575,6 +1699,10 @@ void ems_sendRawTelegram(char * telegram) { } } + if (count == 0) { + return; // nothing to send + } + // calculate length including header and CRC EMS_TxTelegram.length = count + 2; EMS_TxTelegram.type_validate = EMS_ID_NONE; @@ -1726,22 +1854,32 @@ void ems_setWarmWaterTemp(uint8_t temperature) { /** * Set the warm water mode to comfort to Eco/Comfort + * 1 = Hot, 2 = Eco, 3 = Intelligent */ -void ems_setWarmWaterModeComfort(bool comfort) { - myDebug("Setting boiler warm water to comfort mode %s\n", comfort ? "Comfort" : "Eco"); - +void ems_setWarmWaterModeComfort(uint8_t comfort) { _EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx EMS_TxTelegram.timestamp = millis(); // set timestamp EMS_Sys_Status.txRetryCount = 0; // reset retry counter + if (comfort == 1) { + myDebug("Setting boiler warm water comfort mode to Hot"); + EMS_TxTelegram.dataValue = EMS_VALUE_UBAParameterWW_wwComfort_Hot; + } else if (comfort == 2) { + myDebug("Setting boiler warm water comfort mode to Eco"); + EMS_TxTelegram.dataValue = EMS_VALUE_UBAParameterWW_wwComfort_Eco; + } else if (comfort == 3) { + myDebug("Setting boiler warm water comfort mode to Intelligent"); + EMS_TxTelegram.dataValue = EMS_VALUE_UBAParameterWW_wwComfort_Intelligent; + } else { + return; // invalid comfort value + } + EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE; EMS_TxTelegram.dest = EMS_Boiler.type_id; EMS_TxTelegram.type = EMS_TYPE_UBAParameterWW; EMS_TxTelegram.offset = EMS_OFFSET_UBAParameterWW_wwComfort; EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH; EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate - EMS_TxTelegram.dataValue = - (comfort ? EMS_VALUE_UBAParameterWW_wwComfort_Comfort : EMS_VALUE_UBAParameterWW_wwComfort_Eco); // 0x00 is on, 0xD8 is off EMS_TxQueue.push(EMS_TxTelegram); } @@ -1816,3 +1954,26 @@ void ems_setWarmTapWaterActivated(bool activated) { EMS_TxQueue.push(EMS_TxTelegram); // add to queue } + +/* + * Start up sequence for UBA Master, hopefully to initialize a handshake + * Still experimental + */ +void ems_startupTelegrams() { + if ((EMS_Sys_Status.emsTxDisabled) || (!EMS_Sys_Status.emsBusConnected)) { + myDebug("Unable to send startup sequence when in silent mode or bus is disabled"); + } + + myDebug("Sending startup sequence..."); + char s[20] = {0}; + + // (00:07:27.512) Telegram echo: telegram: 0B 08 1D 00 00 (CRC=84), #data=1 + // Write type 0x1D to get out of function test mode + snprintf(s, sizeof(s), "%02X %02X 1D 00 00", EMS_ID_ME, EMS_Boiler.type_id); + ems_sendRawTelegram(s); + + // (00:07:35.555) Telegram echo: telegram: 0B 88 01 00 1B (CRC=8B), #data=1 + // Read type 0x01 + snprintf(s, sizeof(s), "%02X %02X 01 00 1B", EMS_ID_ME, EMS_Boiler.type_id | 0x80); + ems_sendRawTelegram(s); +} diff --git a/src/ems.h b/src/ems.h index 5147223b..e1e97f33 100644 --- a/src/ems.h +++ b/src/ems.h @@ -16,18 +16,19 @@ #define EMS_ID_NONE 0x00 // Fixed - used as a dest in broadcast messages and empty type IDs #define EMS_ID_ME 0x0B // Fixed - our device, hardcoded as the "Service Key" #define EMS_ID_DEFAULT_BOILER 0x08 +#define EMS_ID_SM10 0x30 // Solar Module SM10 #define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC // max length of a telegram, including CRC, for Rx and Tx. -#define EMS_MAX_TELEGRAM_LENGTH 99 +#define EMS_MAX_TELEGRAM_LENGTH 32 // default values #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_SHORT_NOTSET 0x8000 // for 2-byte shorts #define EMS_VALUE_LONG_NOTSET 0xFFFFFF // for 3-byte longs -#define EMS_VALUE_FLOAT_NOTSET -255 // float #define EMS_THERMOSTAT_READ_YES true #define EMS_THERMOSTAT_READ_NO false @@ -91,6 +92,7 @@ typedef struct { unsigned long emsRxTimestamp; // timestamp of last EMS message received unsigned long emsPollTimestamp; // timestamp of last EMS poll sent to us bool emsTxCapable; // able to send via Tx + bool emsTxDisabled; // true to prevent all Tx uint8_t txRetryCount; // # times the last Tx was re-sent } _EMS_Sys_Status; @@ -111,6 +113,12 @@ typedef struct { uint8_t data[EMS_MAX_TELEGRAM_LENGTH]; } _EMS_TxTelegram; +// The Rx receive package +typedef struct { + uint32_t timestamp; // timestamp from millis() + uint8_t * telegram; // the full data package + uint8_t length; // length in bytes +} _EMS_RxTelegram; // default empty Tx @@ -137,6 +145,13 @@ typedef struct { char model_string[50]; } _Boiler_Type; +typedef struct { + uint8_t model_id; + uint8_t product_id; + uint8_t type_id; + char model_string[50]; +} _Other_Type; + // Definition for thermostat type typedef struct { uint8_t model_id; @@ -158,31 +173,32 @@ typedef struct { // UBAParameterWW uint8_t wWComfort; // Warm water comfort or ECO mode // UBAMonitorFast - uint8_t selFlowTemp; // Selected flow temperature - float curFlowTemp; // Current flow temperature - float retTemp; // Return temperature - uint8_t burnGas; // Gas on/off - uint8_t fanWork; // Fan on/off - uint8_t ignWork; // Ignition on/off - uint8_t heatPmp; // Circulating pump on/off - uint8_t wWHeat; // 3-way valve on WW - uint8_t wWCirc; // Circulation on/off - uint8_t selBurnPow; // Burner max power - uint8_t curBurnPow; // Burner current power - float flameCurr; // Flame current in micro amps - float sysPress; // System pressure - char serviceCodeChar[2]; // 2 character status/service code + uint8_t selFlowTemp; // Selected flow temperature + int16_t curFlowTemp; // Current flow temperature + int16_t retTemp; // Return temperature + uint8_t burnGas; // Gas on/off + uint8_t fanWork; // Fan on/off + uint8_t ignWork; // Ignition on/off + uint8_t heatPmp; // Circulating pump on/off + uint8_t wWHeat; // 3-way valve on WW + uint8_t wWCirc; // Circulation on/off + uint8_t selBurnPow; // Burner max power + uint8_t curBurnPow; // Burner current power + uint16_t flameCurr; // Flame current in micro amps + uint8_t sysPress; // System pressure + char serviceCodeChar[3]; // 2 character status/service code + uint16_t serviceCode; // error/service code // UBAMonitorSlow - float extTemp; // Outside temperature - float boilTemp; // Boiler temperature + int16_t extTemp; // Outside temperature + int16_t boilTemp; // Boiler temperature uint8_t pumpMod; // Pump modulation - uint32_t burnStarts; // # burner restarts + uint32_t burnStarts; // # burner starts uint32_t burnWorkMin; // Total burner operating time uint32_t heatWorkMin; // Total heat operating time // UBAMonitorWWMessage - float wWCurTmp; // Warm Water current temperature: + int16_t 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 @@ -206,6 +222,18 @@ typedef struct { // UBAParameterWW uint8_t product_id; } _EMS_Boiler; +/* + * Telegram package defintions for Other EMS devices + */ +typedef struct { + // SM10 Solar Module - SM10Monitor + bool SM10; // set true if there is a SM10 available + int16_t SM10collectorTemp; // collector temp from SM10 + int16_t SM10bottomTemp; // bottom temp from SM10 + uint8_t SM10pumpModulation; // modulation solar pump + uint8_t SM10pump; // pump active +} _EMS_Other; + // Thermostat data typedef struct { uint8_t type_id; // the type ID of the thermostat @@ -214,8 +242,8 @@ typedef struct { bool read_supported; bool write_supported; char version[10]; - float setpoint_roomTemp; // current set temp - float curr_roomTemp; // current room temp + int16_t setpoint_roomTemp; // current set temp + int16_t curr_roomTemp; // current room temp uint8_t mode; // 0=low, 1=manual, 2=auto bool day_mode; // 0=night, 1=day uint8_t hour; @@ -227,7 +255,7 @@ typedef struct { } _EMS_Thermostat; // call back function signature for processing telegram types -typedef void (*EMS_processType_cb)(uint8_t type, uint8_t * data, uint8_t length); +typedef void (*EMS_processType_cb)(uint8_t src, uint8_t * data, uint8_t length); // Definition for each EMS type, including the relative callback function typedef struct { @@ -249,15 +277,16 @@ void ems_setWarmWaterTemp(uint8_t temperature); void ems_setWarmWaterActivated(bool activated); void ems_setWarmTapWaterActivated(bool activated); void ems_setPoll(bool b); -void ems_setTxEnabled(bool b); void ems_setLogging(_EMS_SYS_LOGGING loglevel); void ems_setEmsRefreshed(bool b); -void ems_setWarmWaterModeComfort(bool comfort); +void ems_setWarmWaterModeComfort(uint8_t comfort); bool ems_checkEMSBUSAlive(); void ems_setModels(); +void ems_setTxDisabled(bool b); void ems_getThermostatValues(); void ems_getBoilerValues(); +void ems_getOtherValues(); bool ems_getPoll(); bool ems_getTxEnabled(); bool ems_getThermostatEnabled(); @@ -275,17 +304,21 @@ char * ems_getThermostatDescription(char * buffer); void ems_printTxQueue(); char * ems_getBoilerDescription(char * buffer); +void ems_startupTelegrams(); + // 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 _processType(_EMS_RxTelegram * EMS_RxTelegram); +void _debugPrintPackage(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, const char * color); void _ems_clearTxData(); int _ems_findBoilerModel(uint8_t model_id); bool _ems_setModel(uint8_t model_id); void _ems_setThermostatModel(uint8_t thermostat_modelid); void _removeTxQueue(); +void _ems_readTelegram(uint8_t * telegram, uint8_t length); // global so can referenced in other classes extern _EMS_Sys_Status EMS_Sys_Status; extern _EMS_Boiler EMS_Boiler; extern _EMS_Thermostat EMS_Thermostat; +extern _EMS_Other EMS_Other; diff --git a/src/ems_devices.h b/src/ems_devices.h index 4ac8ab23..e959895a 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -32,11 +32,15 @@ #define EMS_TYPE_UBASetPoints 0x1A #define EMS_TYPE_UBAFunctionTest 0x1D -#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 +#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_Hot 0x00 // the value for hot +#define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco +#define EMS_VALUE_UBAParameterWW_wwComfort_Intelligent 0xEC // the value for intelligent + +// Other +#define EMS_TYPE_SM10Monitor 0x97 // SM10Monitor /* * Thermostats... @@ -92,7 +96,10 @@ typedef enum { // generic ID for the boiler EMS_MODEL_UBA, - // thermostats + // generic ID for all the other weird devices + EMS_MODEL_OTHER, + + // and finaly the thermostats EMS_MODEL_ES73, EMS_MODEL_RC10, EMS_MODEL_RC20, @@ -111,18 +118,26 @@ typedef enum { // format is MODEL_ID, PRODUCT ID, TYPE_ID, DESCRIPTION const _Boiler_Type Boiler_Types[] = { - {EMS_MODEL_UBA, 72, 0x08, "MC10"}, + {EMS_MODEL_UBA, 72, 0x08, "MC10 Module"}, {EMS_MODEL_UBA, 123, 0x08, "Buderus GB172/Nefit Trendline"}, {EMS_MODEL_UBA, 115, 0x08, "Nefit Topline Compact"}, {EMS_MODEL_UBA, 203, 0x08, "Buderus Logamax U122"}, {EMS_MODEL_UBA, 64, 0x08, "Sieger BK15 Boiler/Nefit Smartline"}, - {EMS_MODEL_UBA, 190, 0x09, "BC10 Base Controller"}, - {EMS_MODEL_UBA, 114, 0x09, "BC10 Base Controller"}, - {EMS_MODEL_UBA, 125, 0x09, "BC25 Base Controller"}, - {EMS_MODEL_UBA, 68, 0x09, "RFM20 Receiver"}, - {EMS_MODEL_UBA, 95, 0x08, "Bosch Condens 2500"}, - {EMS_MODEL_UBA, 251, 0x21, "MM10 Mixer Module"}, // warning, fake product id! - {EMS_MODEL_UBA, 250, 0x11, "WM10 Switch Module"}, // warning, fake product id! + {EMS_MODEL_UBA, 95, 0x08, "Bosch Condens 2500"} + +}; + +// Other EMS devices which are not considered boilers or thermostats +const _Other_Type Other_Types[] = { + + {EMS_MODEL_OTHER, 251, 0x21, "MM10 Mixer Module"}, // warning, fake product id! + {EMS_MODEL_OTHER, 250, 0x11, "WM10 Switch Module"}, // warning, fake product id! + {EMS_MODEL_OTHER, 68, 0x09, "RFM20 Receiver"}, + {EMS_MODEL_OTHER, 190, 0x09, "BC10 Base Controller"}, + {EMS_MODEL_OTHER, 114, 0x09, "BC10 Base Controller"}, + {EMS_MODEL_OTHER, 125, 0x09, "BC25 Base Controller"}, + {EMS_MODEL_OTHER, 205, 0x02, "Nefit Moduline Easy Connect"}, + {EMS_MODEL_OTHER, 73, EMS_ID_SM10, "SM10 Solar Module"} }; @@ -132,15 +147,16 @@ const _Boiler_Type Boiler_Types[] = { const _Thermostat_Type Thermostat_Types[] = { {EMS_MODEL_ES73, 76, 0x10, "Sieger ES73", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC10, 79, 0x17, "RC10/Nefit Moduline 100)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC20, 77, 0x17, "RC20/Nefit Moduline 300)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC10, 79, 0x17, "RC10/Nefit Moduline 100", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC20, 77, 0x17, "RC20/Nefit Moduline 300", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_RC20F, 93, 0x18, "RC20F", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, - {EMS_MODEL_RC30, 78, 0x10, "RC30/Nefit Moduline 400)", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC30, 78, 0x10, "RC30/Nefit Moduline 400", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_RC35, 86, 0x10, "RC35", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, {EMS_MODEL_EASY, 202, 0x18, "TC100/Nefit Easy", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_NO}, {EMS_MODEL_BOSCHEASY, 206, 0x02, "Bosch Easy", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_NO}, {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_OT, 171, 0x02, "EMS-OT OpenTherm converter", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES} + {EMS_MODEL_OT, 171, 0x02, "EMS-OT OpenTherm converter", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, + {EMS_MODEL_RC10, 165, 0x02, "RC10/Nefit Moduline 1010", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES} }; diff --git a/src/emsuart.cpp b/src/emsuart.cpp index 454ecb0f..65b479b0 100644 --- a/src/emsuart.cpp +++ b/src/emsuart.cpp @@ -24,8 +24,8 @@ os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue // Important: do not use ICACHE_FLASH_ATTR ! // static void emsuart_rx_intr_handler(void * para) { - static uint16_t length; - static uint8_t uart_buffer[EMS_MAXBUFFERSIZE]; + static uint8_t length; + static uint8_t uart_buffer[EMS_MAXBUFFERSIZE]; // is a new buffer? if so init the thing for a new telegram if (EMS_Sys_Status.emsRxStatus == EMS_RX_STATUS_IDLE) { @@ -67,18 +67,13 @@ static void emsuart_rx_intr_handler(void * para) { /* * system task triggered on BRK interrupt - * Read commands are all asynchronous - * When a buffer is full it is sent to the ems_parseTelegram() function in ems.cpp. This is the hook + * incoming received messages are always asynchronous + * The full buffer is sent to the ems_parseTelegram() function in ems.cpp. */ static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) { - // get next free EMS Receive buffer _EMSRxBuf * pCurrent = pEMSRxBuf; - pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS]; - - // transmit EMS buffer, excluding the BRK - if (pCurrent->writePtr > 1) { - ems_parseTelegram((uint8_t *)pCurrent->buffer, (pCurrent->writePtr) - 1); - } + ems_parseTelegram((uint8_t *)pCurrent->buffer, (pCurrent->writePtr) - 1); // transmit EMS buffer, excluding the BRK + pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS]; // next free EMS Receive buffer } /* @@ -97,12 +92,12 @@ void ICACHE_FLASH_ATTR emsuart_init() { // pin settings PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U); - PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0RXD); + PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD); PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0RXD_U); PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD); // set 9600, 8 bits, no parity check, 1 stop bit - USD(EMSUART_UART) = (ESP8266_CLOCK / EMSUART_BAUD); + USD(EMSUART_UART) = (UART_CLK_FREQ / EMSUART_BAUD); USC0(EMSUART_UART) = EMSUART_CONFIG; // 8N1 // flush everything left over in buffer, this clears both rx and tx FIFOs @@ -129,13 +124,13 @@ void ICACHE_FLASH_ATTR emsuart_init() { system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen); // disable esp debug which will go to Tx and mess up the line - // system_set_os_print(0); // https://github.com/espruino/Espruino/issues/655 - - ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, NULL); - ETS_UART_INTR_ENABLE(); + system_set_os_print(0); // https://github.com/espruino/Espruino/issues/655 // swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively system_uart_swap(); + + ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, NULL); + ETS_UART_INTR_ENABLE(); } /* @@ -143,7 +138,17 @@ void ICACHE_FLASH_ATTR emsuart_init() { */ void ICACHE_FLASH_ATTR emsuart_stop() { ETS_UART_INTR_DISABLE(); - ETS_UART_INTR_ATTACH(NULL, NULL); + //ETS_UART_INTR_ATTACH(NULL, NULL); + //system_uart_swap(); // to be sure, swap Tx/Rx back. + //detachInterrupt(digitalPinToInterrupt(D7)); + //noInterrupts(); +} + +/* + * re-start UART0 driver + */ +void ICACHE_FLASH_ATTR emsuart_start() { + ETS_UART_INTR_ENABLE(); } /* diff --git a/src/emsuart.h b/src/emsuart.h index f5053bda..e1fc9498 100644 --- a/src/emsuart.h +++ b/src/emsuart.h @@ -10,15 +10,15 @@ #include #define EMSUART_UART 0 // UART 0 -#define EMSUART_CONFIG 0x1c // 8N1 (8 bits, no stop bits, 1 parity) +#define EMSUART_CONFIG 0x1C // 8N1 (8 bits, no stop bits, 1 parity) #define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit -#define EMS_MAXBUFFERS 4 // 4 buffers for circular filling to avoid collisions -#define EMS_MAXBUFFERSIZE 128 // max size of the buffer. packets are max 32 bytes +#define EMS_MAXBUFFERS 10 // 4 buffers for circular filling to avoid collisions +#define EMS_MAXBUFFERSIZE 32 // max size of the buffer. packets are max 32 bytes -// this is how long we drop the Tx signal to create a 11-bit Break of zeros +// this is how long we drop the Tx signal to create a 11-bit Break of zeros (BRK) // At 9600 baud, 11 bits will be 1144 microseconds -// the BRK from Boiler is roughly 1.039ms, so accounting for hardware lag using around 2078 (for half-duplex) - 8 (lag) +// the BRK from Boiler master is roughly 1.039ms, so accounting for hardware lag using around 2078 (for half-duplex) - 8 (lag) #define EMS_TX_BRK_WAIT 2070 #define EMSUART_recvTaskPrio 1 @@ -31,6 +31,7 @@ typedef struct { void ICACHE_FLASH_ATTR emsuart_init(); void ICACHE_FLASH_ATTR emsuart_stop(); +void ICACHE_FLASH_ATTR emsuart_start(); void ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len); void ICACHE_FLASH_ATTR emsaurt_tx_poll(); void ICACHE_FLASH_ATTR emsuart_tx_brk(); diff --git a/src/my_config.h b/src/my_config.h index 2c23d611..8f55c891 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -37,6 +37,14 @@ #define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on #define TOPIC_BOILER_WWACTIVATED "wwactivated" // for receiving MQTT message to change water on/off #define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // for received boiler wwtemp changes via MQTT +#define TOPIC_BOILER_CMD_COMFORT "boiler_cmd_comfort" // for received boiler ww comfort setting via MQTT + +// MQTT for SM10 Solar Module +#define TOPIC_SM10_DATA "sm10_data" // topic name +#define SM10_COLLECTORTEMP "temp" // collector temp +#define SM10_BOTTOMTEMP "bottomtemp" // bottom temp +#define SM10_PUMPMODULATION "pumpmodulation" // pump modulation +#define SM10_PUMP "pump" // pump active // shower time #define TOPIC_SHOWERTIME "showertime" // for sending shower time results @@ -44,28 +52,26 @@ #define TOPIC_SHOWER_ALERT "shower_alert" // toggle switch for enabling the shower alarm logic #define TOPIC_SHOWER_COLDSHOT "shower_coldshot" // used to trigger a coldshot from an MQTT command -// default values for shower logic on/off -#define BOILER_SHOWER_TIMER 1 // enable (1) to monitor shower time -#define BOILER_SHOWER_ALERT 0 // enable (1) to send alert of cold water when shower time limit has exceeded -#define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water +// MQTT for EXTERNAL SENSORS +#define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT +#define PAYLOAD_EXTERNAL_SENSORS "temp_%d" // for formatting the payload for each external dallas sensor + //////////////////////////////////////////////////////////////////////////////////////////////////// // THESE DEFAULT VALUES CAN ALSO BE SET AND STORED WITHTIN THE APPLICATION (see 'set' command) // -// ALTHOUGH YOU MAY ALSO HARDCODE THEM HERE BUT THEY WILL BE OVERWRITTEN WITH NEW RELEASE UPDATES // //////////////////////////////////////////////////////////////////////////////////////////////////// -// Set LED pin used for showing ems bus connection status. Solid is connected, Flashing is error -// can be either the onboard LED on the ESP8266 (LED_BULLETIN) or external via an external pull-up LED -// (e.g. D1 on a bbqkees' board -// can be enabled and disabled via the 'set led' -// pin can be set by 'set led_gpio' +// Set LED pin used for showing the EMS bus connection status. Solid means EMS bus working, flashing is an error +// can be either the onboard LED on the ESP8266 (LED_BULLETIN) or external via an external pull-up LED (e.g. D1 on a bbqkees' board) +// can be enabled and disabled via the 'set led' command and pin set by 'set led_gpio' #define EMSESP_LED_GPIO LED_BUILTIN // set this if using an external temperature sensor like a DS18B20 -// D5 is the default on bbqkees' board +// D5 is the default on a bbqkees board #define EMSESP_DALLAS_GPIO D5 +#define EMSESP_DALLAS_PARASITE false -// By default the EMS bus will be scanned for known devices based on product ids in ems_devices.h +// By default the EMS bus will be scanned for known devices based on the product ids in ems_devices.h // You can override the Thermostat and Boiler types here #define EMSESP_BOILER_TYPE EMS_ID_NONE #define EMSESP_THERMOSTAT_TYPE EMS_ID_NONE diff --git a/src/version.h b/src/version.h index c078e043..d4e4d839 100644 --- a/src/version.h +++ b/src/version.h @@ -6,5 +6,5 @@ #pragma once #define APP_NAME "EMS-ESP" -#define APP_VERSION "1.5.6" +#define APP_VERSION "1.6.0" #define APP_HOSTNAME "ems-esp"