diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..e9726a4c3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +*Before creating a new issue please check that you have:* + +* *searched the existing [issues](https://github.com/proddy/EMS-ESP/issues) (both open and closed)* +* *searched the [doc](https://github.com/proddy/EMS-ESP/blob/master/README.md)* + +*Fulfilling this template will help developers and contributors to address the issue. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.* + +*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots) then you can delete them.* + +**Bug description** +*A clear and concise description of what the bug is.* + +**Steps to reproduce** +*Steps to reproduce the behavior.* + +**Expected behavior** +*A clear and concise description of what you expected to happen.* + +**Screenshots** +*If applicable, add screenshots to help explain your problem.* + +**Device information** +*Copy-paste here the information as it is outputted by the device. You can get this information by from the telnet session with the logging set to Verbose mode.* + +**Additional context** +*Add any other context about the problem here.* diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..cb1920434 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,26 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +*Before creating a new feature request please check that you have searched the existing [issues](https://github.com/proddy/EMS-ESP/issues) (both open and closed)* + +*Fulfilling this template will help developers and contributors evaluating the feature. If the information provided is not enough the issue will likely be closed.* + +*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the request then you can delete them.* + +**Is your feature request related to a problem? Please describe.** +*A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]* + +**Describe the solution you'd like** +*A clear and concise description of what you want to happen.* + +**Describe alternatives you've considered** +*A clear and concise description of any alternative solutions or features you've considered.* + +**Additional context** +*Add any other context or screenshots about the feature request here.* diff --git a/.github/ISSUE_TEMPLATE/questions---troubleshooting.md b/.github/ISSUE_TEMPLATE/questions---troubleshooting.md new file mode 100644 index 000000000..e21908dd2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/questions---troubleshooting.md @@ -0,0 +1,30 @@ +--- +name: Questions & troubleshooting +about: Anything not a bug or feature request +title: '' +labels: question +assignees: '' + +--- + +*Before creating a new issue please check that you have:* + +* *searched the existing [issues](https://github.com/proddy/EMS-ESP/issues) (both open and closed)* +* *searched the [doc](https://github.com/proddy/EMS-ESP/blob/master/README.md)* + + +*Fulfilling this template will help developers and contributors help you. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.* + +*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots) then you can delete them.* + +**Question** +*A clear and concise description of what the problem/doubt is.* + +**Screenshots** +*If applicable, add screenshots to help explain your problem.* + +**Device information** +*Copy-paste here the information as it is outputted by the device. You can get this information by from the telnet session with the logging set to Verbose mode.* + +**Additional context** +*Add any other context about the problem here.* diff --git a/.github/contribute.md b/.github/contribute.md new file mode 100644 index 000000000..a81ee132a --- /dev/null +++ b/.github/contribute.md @@ -0,0 +1,10 @@ +Do you want to do a pull request? + +Excellent! Thanks for contributing! + +Please do keep in mind these basic rules: + +## Pull request ## +* Do the pull request against the **`dev` branch** +* **Only touch relevant files** (beware if your editor has auto-formatting feature enabled) + diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..046b9c15a --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,41 @@ +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 60 + +# Number of days of inactivity before a stale Issue or Pull Request is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 7 + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - enhancement + - bug + - staged for release + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Label to use when marking as stale +staleLabel: stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed in 7 days if no further activity occurs. + Thank you for your contributions. + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +closeComment: > + This issue will be auto-closed because there hasn't been any activity for two months. Feel free to open a new one if you still experience this problem. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 + +# Limit to only `issues` or `pulls` +only: issues diff --git a/.gitignore b/.gitignore index 074281300..e8b8e75f2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ platformio.ini lib/readme.txt .travis.yml stackdmp.txt -*.jar \ No newline at end of file +*.jar diff --git a/README.md b/README.md index 4aba19f25..a9d541cda 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ There are 3 parts to this project, first the design of the circuit, secondly the - [EMS Polling](#ems-polling) - [EMS Broadcasting](#ems-broadcasting) - [EMS Reading and Writing](#ems-reading-and-writing) + - [EMS Plus](#ems-plus) + - [Message layout](#message-layout) + - [Message types](#message-types) - [The ESP8266 Source Code](#the-esp8266-source-code) - [Special EMS Types](#special-ems-types) - [Which thermostats are supported?](#which-thermostats-are-supported) @@ -43,11 +46,11 @@ The original intention for this home project was to build a custom smart thermos Acknowledgments and kudos to the following people who have open-sourced their projects: - **susisstrolch** - One of the first working versions of the EMS bridge circuit I found designed for specifically for the ESP8266. I borrowed Juergen's [schematic](https://github.com/susisstrolch/EMS-ESP12) and parts of his code ideas for reading telegrams. +**susisstrolch** - One of the first working versions of the EMS bridge circuit I found designed for specifically for the ESP8266. I borrowed Juergen's [schematic](https://github.com/susisstrolch/EMS-ESP12) and parts of his code ideas for reading telegrams. - **bbqkees** - Kees built a working [circuit](https://shop.hotgoodies.nl/ems/) and his SMD board is available for purchase on his website. +**bbqkees** - Kees built a working [circuit](https://shop.hotgoodies.nl/ems/) and his SMD board is available for purchase on his website. - **EMS Wiki** - A comprehensive [reference](https://emswiki.thefischer.net/doku.php?id=wiki:ems:telegramme) (in German) for the EMS bus which is a little outdated, not always 100% accurate and sadly no longer maintained. +**EMS Wiki** - A comprehensive [reference](https://emswiki.thefischer.net/doku.php?id=wiki:ems:telegramme) (in German) for the EMS bus which is a little outdated, not always 100% accurate and sadly no longer maintained. ## Supported EMS Devices @@ -63,6 +66,31 @@ 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. +<<<<<<< HEAD +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. +6. When the ESP8266 starts up for the first time the onboard LED will be flashing. This is because the EMS bus is not yet connected. +7. If you haven't hardcoded the WiFi credentials in step 4, the ESP8266 will boot up in a WiFi Access Point (AP) mode with the ssid name `ems-esp`. Now you can either use a laptop and connect to this AP using Telnet to `192.168.1.4` or if its powered from a computers USB use a Serial monitor tool to the ESP's COM port. Tip: to enable Telnet on Windows 10 run `dism /online /Enable-Feature /FeatureName:TelnetClient` or install something like [putty](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html). +8. Next is to change some of the settings. Type `set` to list the current stored settings. Use `set wifi` to add your wifi credentials and if you're using MQTT set the host, username and password. There is no need to reboot the device. +9. The `led_gpio` will default to the onboard LED (which is probably blinking now). Ignore `thermostat_type` and `boiler_type` as these will be auto-detected hopefully later on. +10. **Important**: If `serial` is set to `on` set it to `off` using `set serial off`. The EMS bus is disabled when the serial is on. This mode is only used for setting up a new board or debugging startup issues. +11. Hook up the ESP to the EMS board as follows: + +| EMS board | ESP8266 dev board | +| ----------- | ----------------- | +| Ground/G/J2 | GND/G | +| Rx/J2 | D7 | +| Tx/J2 | D8 | +| VC/J2 | 3v3 or 5v | + +13. Connect the EMS lines to the ESP. This can be done via the two EMS wires or via the 3.5" service jack if you have an bbqkees board. +14. Reboot the ESP, either by the reset switch or pulling the power. +15. The ESP will first perform an autodetect to try and discover the EMS devices attached. If your boiler and thermostat are recognized it will set these types and store them for ever and ever. You can trace the output by telnet'ing to the board `telnet ems-esp.local`. Also type `info` to check what happened. +16. If your boiler/thermostat is not discovered create a GitHub issue stating the type and product ID. These will be added to the file `ems_devices.h` in a future release. +17. If all is well and there is traffic on the EMS bus the onboard LED will stop blinking and be permanently on. If this is annoying you can disable with `set led off`. To see the EMS messages type `set log v` for verbose logging. +18. And all is not well, check the wiring, make sure serial is off and look at the telnet session for errors. If in doubt, wipe the ESP with `pio run -t erase` and start again with step #3 +======= 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. @@ -85,6 +113,7 @@ The code and circuit has been tested with a few ESP8266 development boards such 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 +>>>>>>> upstream/dev ## Monitoring The Output @@ -114,7 +143,7 @@ The schematic used: ![Schematic](doc/schematics/circuit.png) -*Optionally I've also added 2 0.5A/72V polyfuses between the EMS and the two inductors L1 and L2 for extra protection.* +_Optionally I've also added 2 0.5A/72V polyfuses between the EMS and the two inductors L1 and L2 for extra protection._ And here's a version using an early prototype board from **bbqkees**: @@ -129,8 +158,8 @@ The EMS circuit will work with both 3.3V and 5V. It's easiest though to power di - 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 | -| ------------------------------------------ | +| With Power Circuit | +| --------------------------------------------------------------- | | ![Power circuit](doc/schematics/Schematic_EMS-ESP-supercap.png) | ## Adding external temperature sensors @@ -143,9 +172,9 @@ Packages are streamed to the EMS "bus" from any other compatible connected devic A package can be a single byte (see Polling below) or a string of 6 or more bytes making up an actual data telegram. A telegram is always in the format: -``[src] [dest] [type] [offset] [data] [crc] `` +`[src] [dest] [type] [offset] [data] [crc] ` -The first 4 bytes is referenced as the *header* in this document. +The first 4 bytes is referenced as the _header_ in this document. ### EMS IDs @@ -196,6 +225,24 @@ Following a write request, the `[dest]` doesn't have the 8th bit set and after t Every telegram sent is echo'd back to Rx, along the same Bus used for all Rx/Tx transmissions. +## Ems Plus + +In this chapter we will report our findings on the ems plus. + +### Message layout + +| 0 | 1 | 2 | 3 | 4 | 5 | n....n-1 | n | +| ----------- | -------- | ------------- | ------------ | ------ | ------------------- | -------- | --- | +| transmitter | receiver | ems plus mark | message type | offset | device intended for | data | cnc | +| 18 | 00 | FF | 03 | 01 | A5 | 28 | 46 | + +### Message types + +| Message type | Definition | +| ------------ | --------------- | +| 03 | Set temperature | +| 00 | Status message | + ## The ESP8266 Source Code `emsuart.cpp` handles the low level UART read and write logic to the bus. You shouldn't need to touch this. All receive commands from the EMS bus are handled asynchronously using a circular buffer via an interrupt. A separate function processes the buffer and extracts the telegrams. @@ -223,7 +270,7 @@ Every telegram sent is echo'd back to Rx, along the same Bus used for all Rx/Tx | Boiler (0x08) | 0x15 | UBAMaintenanceSettingsMessage | | | Boiler (0x08) | 0x16 | UBAParametersMessage | | -In `ems.cpp` you can add scheduled calls to specific EMS types in the functions `ems_getThermostatValues()` and `ems_getBoilerValues()`. +In `ems.cpp` you can add scheduled calls to specific EMS types in the functions `ems_getThermostatValues()` and `ems_getBoilerValues()`. ### Which thermostats are supported? @@ -296,6 +343,7 @@ You can find the .yaml configuration files under `doc/ha`. See also this [HA for **On Linux (e.g. Ubuntu under Windows 10):** Make sure Python 2.7 is installed, then... + ```python % pip install -U platformio % sudo platformio upgrade @@ -306,7 +354,9 @@ Make sure Python 2.7 is installed, then... % cd EMS-ESP % cp platformio.ini-example platformio.ini ``` + edit `platformio.ini` to set `env_default` to your board type, then + ```c % platformio run -t upload ``` diff --git a/checkcode.py b/checkcode.py deleted file mode 100644 index 53da63054..000000000 --- a/checkcode.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/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 deleted file mode 100644 index 140f23ee4..000000000 --- a/clean_fw.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/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 deleted file mode 100644 index c54d05902..000000000 --- a/debug.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/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/lib/MyESP/MyESP.h b/lib/MyESP/MyESP.h index fc7a7a7c5..f2768b1f8 100644 --- a/lib/MyESP/MyESP.h +++ b/lib/MyESP/MyESP.h @@ -73,6 +73,14 @@ void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); #define COLOR_MAGENTA "\x1B[0;35m" #define COLOR_CYAN "\x1B[0;36m" #define COLOR_WHITE "\x1B[0;37m" +#define COLOR_BRIGHT_BLACK "\x1B[0;90m" +#define COLOR_BRIGHT_RED "\x1B[0;91m" +#define COLOR_BRIGHT_GREEN "\x1B[0;92m" +#define COLOR_BRIGHT_YELLOW "\x1B[0;99m" +#define COLOR_BRIGHT_BLUE "\x1B[0;94m" +#define COLOR_BRIGHT_MAGENTA "\x1B[0;95m" +#define COLOR_BRIGHT_CYAN "\x1B[0;96m" +#define COLOR_BRIGHT_WHITE "\x1B[0;97m" #define COLOR_BOLD_ON "\x1B[1m" #define COLOR_BOLD_OFF "\x1B[22m" // fix by Scott Arlott to support Linux @@ -80,6 +88,10 @@ void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); #define SPIFFS_MAXSIZE 600 // https://arduinojson.org/v6/assistant/ // CRASH +<<<<<<< HEAD +#define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data +#define SAVE_CRASH_EEPROM_SIZE 0x0200 // size +======= /** * Structure of the single crash data set * @@ -97,6 +109,7 @@ void custom_crash_callback(struct rst_info *, uint32_t, uint32_t); * ... */ #define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data +>>>>>>> upstream/dev #define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes #define SAVE_CRASH_RESTART_REASON 0x04 // 1 byte #define SAVE_CRASH_EXCEPTION_CAUSE 0x05 // 1 byte diff --git a/lib/myESP/MyESP.cpp b/lib/myESP/MyESP.cpp new file mode 100644 index 000000000..c84e64b6a --- /dev/null +++ b/lib/myESP/MyESP.cpp @@ -0,0 +1,1183 @@ +/* + * MyESP - my ESP helper class to handle Wifi, MQTT and Telnet + * + * Paul Derbyshire - December 2018 + * + * Ideas borrowed from Espurna https://github.com/xoseperez/espurna + */ + +#include "MyESP.h" + +#define RTC_LEAP_YEAR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0)) + +/* Days in a month */ +static uint8_t RTC_Months[2][12] = { + {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, /* Not leap year */ + {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} /* Leap year */ +}; + +// constructor +MyESP::MyESP() { + _app_hostname = strdup("MyESP"); + _app_name = strdup("MyESP"); + _app_version = strdup(MYESP_VERSION); + + _boottime = strdup(""); + _load_average = 100; // calculated load average + + _telnetcommand_callback = NULL; + _telnet_callback = NULL; + + _fs_callback = NULL; + _fs_settings_callback = NULL; + + _helpProjectCmds = NULL; + _helpProjectCmds_count = 0; + + _use_serial = true; + _mqtt_host = NULL; + _mqtt_password = NULL; + _mqtt_username = NULL; + _mqtt_retain = false; + _mqtt_keepalive = 300; + _mqtt_will_topic = NULL; + _mqtt_will_online_payload = NULL; + _mqtt_will_offline_payload = NULL; + _mqtt_base = NULL; + _mqtt_topic = NULL; + _mqtt_qos = 0; + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + _mqtt_last_connection = 0; + _mqtt_connecting = false; + + _wifi_password = NULL; + _wifi_ssid = NULL; + _wifi_callback = NULL; + _wifi_connected = false; + + _suspendOutput = false; +} + +MyESP::~MyESP() { + end(); +} + +// end +void MyESP::end() { + SerialAndTelnet.end(); + jw.disconnect(); +} + +// general debug to the telnet or serial channels +void MyESP::myDebug(const char * format, ...) { + if (_suspendOutput) + return; + + va_list args; + va_start(args, format); + char test[1]; + + int len = ets_vsnprintf(test, 1, format, args) + 1; + + char * buffer = new char[len]; + ets_vsnprintf(buffer, len, format, args); + va_end(args); + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + + +// for flashmemory. Must use PSTR() +void MyESP::myDebug_P(PGM_P format_P, ...) { + if (_suspendOutput) + return; + + char format[strlen_P(format_P) + 1]; + memcpy_P(format, format_P, sizeof(format)); + + va_list args; + va_start(args, format_P); + char test[1]; + int len = ets_vsnprintf(test, 1, format, args) + 1; + + char * buffer = new char[len]; + ets_vsnprintf(buffer, len, format, args); + + va_end(args); + + // capture & print timestamp + char timestamp[10] = {0}; + snprintf_P(timestamp, sizeof(timestamp), PSTR("[%06lu] "), millis() % 1000000); + SerialAndTelnet.print(timestamp); + + SerialAndTelnet.println(buffer); + + delete[] buffer; +} + +// use Serial? +bool MyESP::getUseSerial() { + return (_use_serial); +} + +// called when WiFi is connected, and used to start OTA, MQTT +void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) { + if ((code == MESSAGE_CONNECTED)) { +#if defined(ARDUINO_ARCH_ESP32) + String hostname = String(WiFi.getHostname()); +#else + String hostname = WiFi.hostname(); +#endif + + myDebug_P(PSTR("[WIFI] SSID %s"), WiFi.SSID().c_str()); + myDebug_P(PSTR("[WIFI] CH %d"), WiFi.channel()); + myDebug_P(PSTR("[WIFI] RSSI %d"), WiFi.RSSI()); + myDebug_P(PSTR("[WIFI] IP %s"), WiFi.localIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.macAddress().c_str()); + myDebug_P(PSTR("[WIFI] GW %s"), WiFi.gatewayIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MASK %s"), WiFi.subnetMask().toString().c_str()); + myDebug_P(PSTR("[WIFI] DNS %s"), WiFi.dnsIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] HOST %s"), hostname.c_str()); + + // start OTA + ArduinoOTA.begin(); // moved to support esp32 + myDebug_P(PSTR("[OTA] listening to %s.local:%u"), ArduinoOTA.getHostname().c_str(), OTA_PORT); + + // MQTT Setup + _mqtt_setup(); + + _wifi_connected = true; + + // finally if we don't want Serial anymore, turn it off + if (!_use_serial) { + Serial.println("Disabling serial port"); + Serial.flush(); + Serial.end(); + SerialAndTelnet.setSerial(NULL); + } else { + Serial.println("Using serial port output"); + } + + // call any final custom settings + if (_wifi_callback) { + _wifi_callback(); + } + } + + if (code == MESSAGE_ACCESSPOINT_CREATED) { + myDebug_P(PSTR("[WIFI] MODE AP --------------------------------------")); + myDebug_P(PSTR("[WIFI] SSID %s"), jw.getAPSSID().c_str()); + myDebug_P(PSTR("[WIFI] IP %s"), WiFi.softAPIP().toString().c_str()); + myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.softAPmacAddress().c_str()); + + // call any final custom settings + if (_wifi_callback) { + _wifi_callback(); + } + } + + if (code == MESSAGE_CONNECTING) { + myDebug_P(PSTR("[WIFI] Connecting to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_CONNECT_FAILED) { + myDebug_P(PSTR("[WIFI] Could not connect to %s"), parameter); + _wifi_connected = false; + } + + if (code == MESSAGE_DISCONNECTED) { + myDebug_P(PSTR("[WIFI] Disconnected")); + _wifi_connected = false; + } +} + +// received MQTT message +// we send this to the call back function. Important to parse are the event strings such as MQTT_MESSAGE_EVENT and MQTT_CONNECT_EVENT +void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) { + if (len == 0) + return; + + char message[len + 1]; + strlcpy(message, (char *)payload, len + 1); + + // myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message); // enable for debugging + + // topics are in format MQTT_BASE/HOSTNAME/TOPIC + char * topic_magnitude = strrchr(topic, '/'); // strip out everything until last / + if (topic_magnitude != nullptr) { + topic = topic_magnitude + 1; + } + + // Send message event to custom service + (_mqtt_callback)(MQTT_MESSAGE_EVENT, topic, message); +} + +// MQTT subscribe +// to MQTT_BASE/app_hostname/topic +void MyESP::mqttSubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.subscribe(_mqttTopic(topic), _mqtt_qos); + myDebug_P(PSTR("[MQTT] Subscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT unsubscribe +// to MQTT_BASE/app_hostname/topic +void MyESP::mqttUnsubscribe(const char * topic) { + if (mqttClient.connected() && (strlen(topic) > 0)) { + unsigned int packetId = mqttClient.unsubscribe(_mqttTopic(topic)); + myDebug_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)"), _mqttTopic(topic), packetId); + } +} + +// MQTT Publish +void MyESP::mqttPublish(const char * topic, const char * payload) { + // myDebug_P(PSTR("[MQTT] Sending pubish to %s with payload %s"), _mqttTopic(topic), payload); + mqttClient.publish(_mqttTopic(topic), _mqtt_qos, _mqtt_retain, payload); +} + +// MQTT onConnect - when a connect is established +void MyESP::_mqttOnConnect() { + myDebug_P(PSTR("[MQTT] Connected")); + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; + + _mqtt_last_connection = millis(); + + // say we're alive to the Last Will topic + mqttClient.publish(_mqttTopic(_mqtt_will_topic), 1, true, _mqtt_will_online_payload); + + // call custom function to handle mqtt receives + (_mqtt_callback)(MQTT_CONNECT_EVENT, NULL, NULL); +} + +// MQTT setup +void MyESP::_mqtt_setup() { + if (!_mqtt_host) { + myDebug_P(PSTR("[MQTT] disabled")); + } + + mqttClient.onConnect([this](bool sessionPresent) { _mqttOnConnect(); }); + + mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { + if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) { + myDebug_P(PSTR("[MQTT] TCP Disconnected. Check mqtt logs.")); + (_mqtt_callback)(MQTT_DISCONNECT_EVENT, NULL, + NULL); // call callback with disconnect + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) { + myDebug_P(PSTR("[MQTT] Identifier Rejected")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) { + myDebug_P(PSTR("[MQTT] Server unavailable")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) { + myDebug_P(PSTR("[MQTT] Malformed credentials")); + } + if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) { + myDebug_P(PSTR("[MQTT] Not authorized")); + } + + // Reset reconnection delay + _mqtt_last_connection = millis(); + _mqtt_connecting = false; + }); + + //mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { myDebug_P(PSTR("[MQTT] Subscribe ACK for PID %d"), packetId); }); + + //mqttClient.onPublish([this](uint16_t packetId) { myDebug_P(PSTR("[MQTT] Publish ACK for PID %d"), packetId); }); + + mqttClient.onMessage( + [this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { + _mqttOnMessage(topic, payload, len); + }); +} + +// WiFI setup +void MyESP::_wifi_setup() { + jw.setHostname(_app_hostname); // Set WIFI hostname (otherwise it would be ESP-XXXXXX) + jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); }); + jw.enableAP(false); + jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); + jw.setReconnectTimeout(WIFI_RECONNECT_INTERVAL); + jw.enableAPFallback(true); // AP mode only as fallback + jw.enableSTA(true); // Enable STA mode (connecting to a router) + jw.enableScan(false); // Configure it to scan available networks and connect in order of dBm + jw.cleanNetworks(); // Clean existing network configuration + jw.addNetwork(_wifi_ssid, _wifi_password); // Add a network +} + +// set the callback function for the OTA onstart +void MyESP::setOTA(ota_callback_f OTACallback) { + _ota_callback = OTACallback; +} + +// OTA callback when the upload process starts +void MyESP::_OTACallback() { + myDebug_P(PSTR("[OTA] Start")); + SerialAndTelnet.handle(); // force flush + if (_ota_callback) { + (_ota_callback)(); // call custom function to handle mqtt receives + } +} +ESP8266WebServer httpServer(80); +ESP8266HTTPUpdateServer httpUpdater; +// OTA Setup +void MyESP::_ota_setup() { + if (!_wifi_ssid) { + return; + } + MDNS.begin(_app_hostname); + httpUpdater.setup(&httpServer); + httpServer.begin(); + MDNS.addService("http", "tcp", 80); + //ArduinoOTA.setPort(OTA_PORT); + ArduinoOTA.setHostname(_app_hostname); + + ArduinoOTA.onStart([this]() { _OTACallback(); }); + ArduinoOTA.onEnd([this]() { myDebug_P(PSTR("[OTA] Done, restarting...")); }); + ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) { + static unsigned int _progOld; + unsigned int _prog = (progress / (total / 100)); + if (_prog != _progOld) { + myDebug_P(PSTR("[OTA] Progress: %u%%\r"), _prog); + _progOld = _prog; + } + }); + + ArduinoOTA.onError([this](ota_error_t error) { + if (error == OTA_AUTH_ERROR) + myDebug_P(PSTR("[OTA] Auth Failed")); + else if (error == OTA_BEGIN_ERROR) + myDebug_P(PSTR("[OTA] Begin Failed")); + else if (error == OTA_CONNECT_ERROR) + myDebug_P(PSTR("[OTA] Connect Failed")); + else if (error == OTA_RECEIVE_ERROR) + myDebug_P(PSTR("[OTA] Receive Failed")); + else if (error == OTA_END_ERROR) + myDebug_P(PSTR("[OTA] End Failed")); + }); +} + +// sets boottime +void MyESP::setBoottime(const char * boottime) { + if (_boottime) { + free(_boottime); + } + _boottime = strdup(boottime); +} + +// Set callback of sketch function to process project messages +void MyESP::setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback) { + _helpProjectCmds = cmds; // command list + _helpProjectCmds_count = count; // number of commands + _telnetcommand_callback = callback_cmd; // external function to handle commands + _telnet_callback = callback; +} + +void MyESP::_telnetConnected() { + myDebug_P(PSTR("[TELNET] Telnet connection established")); + _consoleShowHelp(); // Show the initial message + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_CONNECT); // call callback + } +} + +void MyESP::_telnetDisconnected() { + myDebug_P(PSTR("[TELNET] Telnet connection closed")); + if (_telnet_callback) { + (_telnet_callback)(TELNET_EVENT_DISCONNECT); // call callback + } +} + +// Initialize the telnet server +void MyESP::_telnet_setup() { + SerialAndTelnet.setWelcomeMsg(""); + SerialAndTelnet.setCallbackOnConnect([this]() { _telnetConnected(); }); + SerialAndTelnet.setCallbackOnDisconnect([this]() { _telnetDisconnected(); }); + SerialAndTelnet.setDebugOutput(false); + SerialAndTelnet.begin(TELNET_SERIAL_BAUD); // default baud is 115200 + + // init command buffer for console commands + memset(_command, 0, TELNET_MAX_COMMAND_LENGTH); +} + +// https://stackoverflow.com/questions/43063071/the-arduino-ntp-i-want-print-out-datadd-mm-yyyy +void MyESP::_printBuildTime(unsigned long unix) { + // compensate for summer/winter time and CET. Can't be bothered to work out DST. + // add 3600 to the UNIX time during winter, (3600 s = 1 h), and 7200 during summer (DST). + unix += 3600; // add 1 hour + + uint8_t Day, Month; + + uint8_t Seconds = unix % 60; /* Get seconds from unix */ + unix /= 60; /* Go to minutes */ + uint8_t Minutes = unix % 60; /* Get minutes */ + unix /= 60; /* Go to hours */ + uint8_t Hours = unix % 24; /* Get hours */ + unix /= 24; /* Go to days */ + 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(); + + if (WiFi.getMode() & WIFI_AP) { + SerialAndTelnet.printf("* ESP is in AP mode with SSID %s", jw.getAPSSID().c_str()); + SerialAndTelnet.println(); + } else { +#if defined(ARDUINO_ARCH_ESP32) + String hostname = String(WiFi.getHostname()); +#else + String hostname = WiFi.hostname(); +#endif + SerialAndTelnet.printf("* Hostname: %s IP: %s MAC: %s", + hostname.c_str(), + WiFi.localIP().toString().c_str(), + WiFi.macAddress().c_str()); +#ifdef ARDUINO_BOARD + SerialAndTelnet.printf(" Board: %s", ARDUINO_BOARD); +#endif + SerialAndTelnet.printf(" (MyESP v%s)", MYESP_VERSION); + +#ifdef BUILD_TIME + SerialAndTelnet.print(" (Build "); + _printBuildTime(BUILD_TIME); + SerialAndTelnet.print(")"); +#endif + SerialAndTelnet.println(); + SerialAndTelnet.printf("* Connected to WiFi SSID: %s (signal %d%%)", WiFi.SSID().c_str(), getWifiQuality()); + SerialAndTelnet.println(); + SerialAndTelnet.printf("* MQTT is %s", mqttClient.connected() ? "connected" : "disconnected"); + SerialAndTelnet.println(); + SerialAndTelnet.printf("* Boot time: %s", _boottime); + SerialAndTelnet.println(); + } + + SerialAndTelnet.printf("* Free RAM: %d KB Load: %d%%", (ESP.getFreeHeap() / 1024), getSystemLoadAverage()); + SerialAndTelnet.println(); + // for battery power is ESP.getVcc() + + SerialAndTelnet.println(FPSTR("*")); + SerialAndTelnet.println(FPSTR("* Commands:")); + SerialAndTelnet.println(FPSTR("* ?=help, CTRL-D=quit")); + SerialAndTelnet.println(FPSTR("* reboot")); + SerialAndTelnet.println(FPSTR("* set")); + SerialAndTelnet.println(FPSTR("* set wifi [ssid] [password]")); + SerialAndTelnet.println(FPSTR("* set [value]")); + SerialAndTelnet.println(FPSTR("* set erase")); + SerialAndTelnet.println(FPSTR("* set serial")); + + // print custom commands if available. Taken from progmem + if (_telnetcommand_callback) { + // find the longest key length so we can right align it + uint8_t max_len = 0; + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + if (strlen(_helpProjectCmds[i].key) > max_len) + max_len = strlen(_helpProjectCmds[i].key); + } + + for (uint8_t i = 0; i < _helpProjectCmds_count; i++) { + SerialAndTelnet.print(FPSTR("* ")); + SerialAndTelnet.print(FPSTR(_helpProjectCmds[i].key)); + for (uint8_t j = 0; j < ((max_len + 5) - strlen(_helpProjectCmds[i].key)); j++) { // account for longest string length + SerialAndTelnet.print(FPSTR(" ")); // padding + } + SerialAndTelnet.println(FPSTR(_helpProjectCmds[i].description)); + } + } + + SerialAndTelnet.println(); // newline +} + +// reset / restart +void MyESP::resetESP() { + myDebug_P(PSTR("* Reboot ESP...")); + end(); +#if defined(ARDUINO_ARCH_ESP32) + ESP.restart(); +#else + ESP.restart(); +#endif +} + +// read next word from string buffer +char * MyESP::_telnet_readWord() { + return (strtok(NULL, ", \n")); +} + +// change setting for 2 params (set ) +void MyESP::_changeSetting2(const char * setting, const char * value1, const char * value2) { + if (strcmp(setting, "wifi") == 0) { + if (_wifi_ssid) + free(_wifi_ssid); + if (_wifi_password) + free(_wifi_password); + _wifi_ssid = NULL; + _wifi_password = NULL; + + if (value1) { + _wifi_ssid = strdup(value1); + } + + if (value2) { + _wifi_password = strdup(value2); + } + + (void)fs_saveConfig(); + SerialAndTelnet.println("WiFi settings changed. Reconnecting..."); + jw.disconnect(); + jw.cleanNetworks(); + jw.addNetwork(_wifi_ssid, _wifi_password); + } +} + +// change settings - always as strings +// messy code but effective since we don't have too many settings +// wc is word count, number of parameters after the 'set' command +void MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) { + bool ok = false; + + // check for our internal commands first + if (strcmp(setting, "erase") == 0) { + _fs_eraseConfig(); + return; + } else if ((strcmp(setting, "wifi") == 0) && (wc == 1)) { // erase wifi settings + if (_wifi_ssid) + free(_wifi_ssid); + if (_wifi_password) + free(_wifi_password); + _wifi_ssid = NULL; + _wifi_password = NULL; + ok = true; + } else if (strcmp(setting, "mqtt_host") == 0) { + if (_mqtt_host) + free(_mqtt_host); + _mqtt_host = NULL; // just to be sure + if (value) { + _mqtt_host = strdup(value); + } + ok = true; + } else if (strcmp(setting, "mqtt_username") == 0) { + if (_mqtt_username) + free(_mqtt_username); + _mqtt_username = NULL; // just to be sure + if (value) { + _mqtt_username = strdup(value); + } + ok = true; + } else if (strcmp(setting, "mqtt_password") == 0) { + if (_mqtt_password) + free(_mqtt_password); + _mqtt_password = NULL; // just to be sure + if (value) { + _mqtt_password = strdup(value); + } + ok = true; + } else if (strcmp(setting, "serial") == 0) { + ok = true; + _use_serial = false; + if (value) { + if (strcmp(value, "on") == 0) { + _use_serial = true; + ok = true; + } else if (strcmp(value, "off") == 0) { + _use_serial = false; + ok = true; + } else { + ok = false; + } + } + } else { + // finally check for any custom commands + ok = (_fs_settings_callback)(MYESP_FSACTION_SET, wc, setting, value); + } + + if (!ok) { + SerialAndTelnet.println("\nInvalid parameter for set command."); + return; + } + + // check for 2 params + if (value == nullptr) { + SerialAndTelnet.printf("%s setting reset to its default value.", setting); + } else { + // must be 3 params + SerialAndTelnet.printf("%s changed.", setting); + } + SerialAndTelnet.println(); + + (void)fs_saveConfig(); +} + +void MyESP::_telnetCommand(char * commandLine) { + // count the number of arguments + char * str = commandLine; + bool state = false; + unsigned wc = 0; + while (*str) { + if (*str == ' ' || *str == '\n' || *str == '\t') { + state = false; + } else if (state == false) { + state = true; + ++wc; + } + ++str; + } + + // check first for reserved commands + char * temp = strdup(commandLine); // because strotok kills original string buffer + char * ptrToCommandName = strtok((char *)temp, ", \n"); + + // set command + if (strcmp(ptrToCommandName, "set") == 0) { + if (wc == 1) { + SerialAndTelnet.println(); + SerialAndTelnet.println("Stored settings:"); + SerialAndTelnet.printf(" wifi=%s ", (!_wifi_ssid) ? "" : _wifi_ssid); + if (!_wifi_password) { + SerialAndTelnet.print(""); + } else { + for (uint8_t i = 0; i < strlen(_wifi_password); i++) + SerialAndTelnet.print("*"); + } + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_host=%s", (!_mqtt_host) ? "" : _mqtt_host); + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_username=%s", (!_mqtt_username) ? "" : _mqtt_username); + SerialAndTelnet.println(); + SerialAndTelnet.printf(" mqtt_password="); + if (!_mqtt_password) { + SerialAndTelnet.print(""); + } else { + for (uint8_t i = 0; i < strlen(_mqtt_password); i++) + SerialAndTelnet.print("*"); + } + + SerialAndTelnet.println(); + SerialAndTelnet.printf(" serial=%s", (_use_serial) ? "on" : "off"); + + SerialAndTelnet.println(); + + // print custom settings + (_fs_settings_callback)(MYESP_FSACTION_LIST, 0, NULL, NULL); + + SerialAndTelnet.println(); + SerialAndTelnet.println("Usage: set [value...]"); + } else if (wc == 2) { + char * setting = _telnet_readWord(); + _changeSetting(1, setting, NULL); + } else if (wc == 3) { + char * setting = _telnet_readWord(); + char * value = _telnet_readWord(); + _changeSetting(2, setting, value); + } else if (wc == 4) { + char * setting = _telnet_readWord(); + char * value1 = _telnet_readWord(); + char * value2 = _telnet_readWord(); + _changeSetting2(setting, value1, value2); + } + return; + } + + // reboot command + if ((strcmp(ptrToCommandName, "reboot") == 0) && (wc == 1)) { + resetESP(); + } + + // call callback function + (_telnetcommand_callback)(wc, commandLine); +} + +// handler for Telnet +void MyESP::_telnetHandle() { + SerialAndTelnet.handle(); + + static uint8_t charsRead = 0; + // read asynchronously until full command input + while (SerialAndTelnet.available()) { + char c = SerialAndTelnet.read(); + + SerialAndTelnet.serialPrint(c); // echo to Serial if connected + + switch (c) { + case '\r': // likely have full command in buffer now, commands are terminated by CR and/or LF + case '\n': + _command[charsRead] = '\0'; // null terminate our command char array + if (charsRead > 0) { + charsRead = 0; // is static, so have to reset + _suspendOutput = false; + if (_use_serial) { + SerialAndTelnet.serialPrint('\n'); // force newline if in Serial + } + _telnetCommand(_command); + } + break; + + case '\b': // (^H) handle backspace in input: put a space in last char - coded by Simon Arlott + case 0x7F: // (^?) + + if (charsRead > 0) { + _command[--charsRead] = '\0'; + + SerialAndTelnet.write(' '); + SerialAndTelnet.write('\b'); + } + + break; + + case '?': + if (!_suspendOutput) { + _consoleShowHelp(); + } else { + _command[charsRead++] = c; // add it to buffer as its part of the string entered + } + break; + case 0x04: // EOT, CTRL-D + myDebug_P(PSTR("[TELNET] exiting telnet session")); + SerialAndTelnet.disconnectClient(); + break; + default: + _suspendOutput = true; + if (charsRead < TELNET_MAX_COMMAND_LENGTH) { + _command[charsRead++] = c; + } + _command[charsRead] = '\0'; // just in case + break; + } + } +} + +// ensure we have a connection to MQTT broker +void MyESP::_mqttConnect() { + if (!_mqtt_host) + return; // MQTT not enabled + + // Do not connect if already connected or still trying to connect + if (mqttClient.connected() || _mqtt_connecting || (WiFi.status() != WL_CONNECTED)) { + return; + } + + // Check reconnect interval + if (millis() - _mqtt_last_connection < _mqtt_reconnect_delay) { + return; + } + + _mqtt_connecting = true; // we're doing a connection + + // Increase the reconnect delay + _mqtt_reconnect_delay += MQTT_RECONNECT_DELAY_STEP; + if (_mqtt_reconnect_delay > MQTT_RECONNECT_DELAY_MAX) { + _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX; + } + + mqttClient.setServer(_mqtt_host, MQTT_PORT); + mqttClient.setClientId(_app_hostname); + mqttClient.setKeepAlive(_mqtt_keepalive); + mqttClient.setCleanSession(false); + + // last will + if (_mqtt_will_topic) { + //myDebug_P(PSTR("[MQTT] Setting last will topic %s"), _mqttTopic(_mqtt_will_topic)); + mqttClient.setWill(_mqttTopic(_mqtt_will_topic), 1, true, + _mqtt_will_offline_payload); // retain always true + } + + if (_mqtt_username && _mqtt_password) { + myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s..."), _mqtt_username); + mqttClient.setCredentials(_mqtt_username, _mqtt_password); + } else { + myDebug_P(PSTR("[MQTT] Connecting to MQTT...")); + } + + // Connect to the MQTT broker + mqttClient.connect(); +} + +// Setup everything we need +void MyESP::setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback) { + // Check SSID too long or missing + if (!wifi_ssid || *wifi_ssid == 0x00 || strlen(wifi_ssid) > 31) { + _wifi_ssid = NULL; + } else { + _wifi_ssid = strdup(wifi_ssid); + } + + // Check PASS too long + if (!wifi_password || *wifi_ssid == 0x00 || strlen(wifi_password) > 31) { + _wifi_password = NULL; + } else { + _wifi_password = strdup(wifi_password); + } + + // callback + _wifi_callback = callback; +} + +// init MQTT settings +void MyESP::setMQTT(const char * mqtt_host, + const char * mqtt_username, + const char * mqtt_password, + const char * mqtt_base, + unsigned long mqtt_keepalive, + unsigned char mqtt_qos, + bool mqtt_retain, + const char * mqtt_will_topic, + const char * mqtt_will_online_payload, + const char * mqtt_will_offline_payload, + mqtt_callback_f callback) { + // can be empty + if (!mqtt_host || *mqtt_host == 0x00) { + _mqtt_host = NULL; + } else { + _mqtt_host = strdup(mqtt_host); + } + + // mqtt username and password can be empty + if (!mqtt_username || *mqtt_username == 0x00) { + _mqtt_username = NULL; + } else { + _mqtt_username = strdup(mqtt_username); + } + + // can be empty + if (!mqtt_password || *mqtt_password == 0x00) { + _mqtt_password = NULL; + } else { + _mqtt_password = strdup(mqtt_password); + } + + // base + if (_mqtt_base) { + free(_mqtt_base); + } + _mqtt_base = strdup(mqtt_base); + + // callback + _mqtt_callback = callback; + + // various mqtt settings + _mqtt_keepalive = mqtt_keepalive; + _mqtt_qos = mqtt_qos; + _mqtt_retain = mqtt_retain; + + // last will + if (!mqtt_will_topic || *mqtt_will_topic == 0x00) { + _mqtt_will_topic = NULL; + } else { + _mqtt_will_topic = strdup(mqtt_will_topic); + } + + if (!mqtt_will_online_payload || *mqtt_will_online_payload == 0x00) { + _mqtt_will_online_payload = NULL; + } else { + _mqtt_will_online_payload = strdup(mqtt_will_online_payload); + } + + if (!mqtt_will_offline_payload || *mqtt_will_offline_payload == 0x00) { + _mqtt_will_offline_payload = NULL; + } else { + _mqtt_will_offline_payload = strdup(mqtt_will_offline_payload); + } +} + +// builds up a topic by prefixing the base and hostname +char * MyESP::_mqttTopic(const char * topic) { + char buffer[MQTT_MAX_TOPIC_SIZE] = {0}; + + strlcpy(buffer, _mqtt_base, sizeof(buffer)); + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, _app_hostname, sizeof(buffer)); + strlcat(buffer, "/", sizeof(buffer)); + strlcat(buffer, topic, sizeof(buffer)); + + if (_mqtt_topic) { + free(_mqtt_topic); + } + _mqtt_topic = strdup(buffer); + + return _mqtt_topic; +} + + +// print contents of file +// assumes Serial is open +void MyESP::_fs_printConfig() { + myDebug_P(PSTR("[FS] Contents:")); + + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); + if (!configFile) { + Serial.println(F("[FS] Failed to read file for printing")); + return; + } + + while (configFile.available()) { + SerialAndTelnet.print((char)configFile.read()); + } + SerialAndTelnet.println(); + + configFile.close(); +} + +// format File System +void MyESP::_fs_eraseConfig() { + myDebug_P(PSTR("[FS] Erasing settings, please wait a few seconds. ESP will " + "automatically restart when finished.")); + + if (SPIFFS.format()) { + delay(1000); // wait 1 seconds + resetESP(); + } +} + +void MyESP::setSettings(fs_callback_f callback_fs, fs_settings_callback_f callback_settings_fs) { + _fs_callback = callback_fs; + _fs_settings_callback = callback_settings_fs; +} + +// load from spiffs +bool MyESP::_fs_loadConfig() { + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "r"); + + size_t size = configFile.size(); + if (size > 1024) { + myDebug_P(PSTR("[FS] Config file size is too large")); + return false; + } else if (size == 0) { + myDebug_P(PSTR("[FS] Failed to open config file")); + // file does not exist, so assume its the first install. Set serial to on + _use_serial = true; + return false; + } + + StaticJsonDocument doc; + JsonObject json = doc.to(); + + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, configFile); + if (error) { + Serial.println(F("[FS] Failed to read file")); + return false; + } + + const char * value; + + value = json["wifi_ssid"]; + _wifi_ssid = (value) ? strdup(value) : NULL; + + value = json["wifi_password"]; + _wifi_password = (value) ? strdup(value) : NULL; + + value = json["mqtt_host"]; + _mqtt_host = (value) ? strdup(value) : NULL; + + value = json["mqtt_username"]; + _mqtt_username = (value) ? strdup(value) : NULL; + + value = json["mqtt_password"]; + _mqtt_password = (value) ? strdup(value) : NULL; + + _use_serial = (bool)json["use_serial"]; + + // callback for loading custom settings + // ok is false if there's a problem loading a custom setting (e.g. does not exist) + bool ok = (_fs_callback)(MYESP_FSACTION_LOAD, json); + + configFile.close(); + + return ok; +} + +// save settings to spiffs +bool MyESP::fs_saveConfig() { + StaticJsonDocument doc; + JsonObject json = doc.to(); + + json["app_version"] = _app_version; + json["wifi_ssid"] = _wifi_ssid; + json["wifi_password"] = _wifi_password; + json["mqtt_host"] = _mqtt_host; + json["mqtt_username"] = _mqtt_username; + json["mqtt_password"] = _mqtt_password; + json["use_serial"] = _use_serial; + + // callback for saving custom settings + (void)(_fs_callback)(MYESP_FSACTION_SAVE, json); + + // if file exists, remove it just to be safe + if (SPIFFS.exists(MYEMS_CONFIG_FILE)) { + // delete it + SPIFFS.remove(MYEMS_CONFIG_FILE); + } + + File configFile = SPIFFS.open(MYEMS_CONFIG_FILE, "w"); + if (!configFile) { + Serial.println("[FS] Failed to open config file for writing"); + return false; + } + + // Serialize JSON to file + if (serializeJson(json, configFile) == 0) { + Serial.println(F("[FS] Failed to write to file")); + } + + configFile.close(); + + return true; +} + +// init the SPIFF file system and load the config +// if it doesn't exist try and create it +// force Serial for debugging, and turn it off afterwards +void MyESP::_fs_setup() { + if (!SPIFFS.begin()) { + Serial.println("[FS] Failed to mount the file system"); + _fs_eraseConfig(); // fix for ESP32 + return; + } + + // load the config file. if it doesn't exist (function returns false) create it + if (!_fs_loadConfig()) { + // Serial.println("[FS] Re-creating config file"); + fs_saveConfig(); + } + + //_fs_printConfig(); // TODO: for debugging +} + +uint16_t MyESP::getSystemLoadAverage() { + return _load_average; +} + +// calculate load average +void MyESP::_calculateLoad() { + static unsigned long last_loadcheck = 0; + static unsigned long load_counter_temp = 0; + load_counter_temp++; + + if (millis() - last_loadcheck > LOADAVG_INTERVAL) { + static unsigned long load_counter = 0; + static unsigned long load_counter_max = 1; + + load_counter = load_counter_temp; + load_counter_temp = 0; + if (load_counter > load_counter_max) { + load_counter_max = load_counter; + } + _load_average = 100 - (100 * load_counter / load_counter_max); + last_loadcheck = millis(); + } +} + +// return true if wifi is connected +// WL_NO_SHIELD = 255, // for compatibility with WiFi Shield library +// WL_IDLE_STATUS = 0, +// WL_NO_SSID_AVAIL = 1, +// WL_SCAN_COMPLETED = 2, +// WL_CONNECTED = 3, +// WL_CONNECT_FAILED = 4, +// WL_CONNECTION_LOST = 5, +// WL_DISCONNECTED = 6 +bool MyESP::isWifiConnected() { + return (_wifi_connected); +} + +/* + Return the quality (Received Signal Strength Indicator) + of the WiFi network. + Returns a number between 0 and 100 if WiFi is connected. + Returns -1 if WiFi is disconnected. + + High quality: 90% ~= -55dBm + Medium quality: 50% ~= -75dBm + Low quality: 30% ~= -85dBm + Unusable quality: 8% ~= -96dBm +*/ +int MyESP::getWifiQuality() { + if (WiFi.status() != WL_CONNECTED) + return -1; + int dBm = WiFi.RSSI(); + if (dBm <= -100) + return 0; + if (dBm >= -50) + return 100; + return 2 * (dBm + 100); +} + +// register new instance +void MyESP::begin(const char * app_hostname, const char * app_name, const char * app_version) { + _app_hostname = strdup(app_hostname); + _app_name = strdup(app_name); + _app_version = strdup(app_version); + + _telnet_setup(); // Telnet setup + _fs_setup(); // SPIFFS setup, do this first to get values + _wifi_setup(); // WIFI setup + _ota_setup(); +} + +/* + * Loop. This is called as often as possible and it handles wifi, telnet, mqtt etc + */ +void MyESP::loop() { + _calculateLoad(); + _telnetHandle(); // Telnet/Debugger + httpServer.handleClient(); + MDNS.update(); + jw.loop(); // WiFi + + // do nothing else until we've got a wifi connection + if (WiFi.getMode() & WIFI_AP) { + return; + } + + ArduinoOTA.handle(); // OTA + _mqttConnect(); // MQTT + + yield(); // ...and breath +} + +MyESP myESP; // create instance diff --git a/lib/myESP/MyESP.h b/lib/myESP/MyESP.h new file mode 100644 index 000000000..1862896e4 --- /dev/null +++ b/lib/myESP/MyESP.h @@ -0,0 +1,238 @@ +/* + * MyESP.h + * + * Paul Derbyshire - December 2018 + */ + +#pragma once + +#ifndef MyEMS_h +#define MyEMS_h + +#define MYESP_VERSION "1.1.5" + +#include +#include +#include // https://github.com/marvinroger/async-mqtt-client and for ESP32 see https://github.com/marvinroger/async-mqtt-client/issues/127 +#include + +#include +#include // https://github.com/xoseperez/justwifi +#include // modified from https://github.com/yasheena/telnetspy +#if defined(ARDUINO_ARCH_ESP32) +#include +#include // added for ESP32 +#define ets_vsnprintf vsnprintf // added for ESP32 +#define OTA_PORT 8266 +#else +#include +#include +#include +#include +#define OTA_PORT 8266 +#endif + +#define MYEMS_CONFIG_FILE "/config.json" + +#define LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) + +// WIFI +#define WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms +#define WIFI_RECONNECT_INTERVAL 60000 // If could not connect to WIFI, retry after this time in ms + +// MQTT +#define MQTT_PORT 1883 // MQTT port +#define MQTT_RECONNECT_DELAY_MIN 2000 // Try to reconnect in 3 seconds upon disconnection +#define MQTT_RECONNECT_DELAY_STEP 3000 // Increase the reconnect delay in 3 seconds after each failed attempt +#define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most +#define MQTT_MAX_SIZE 600 // max length of MQTT message +#define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT message + +// Internal MQTT events +#define MQTT_CONNECT_EVENT 0 +#define MQTT_DISCONNECT_EVENT 1 +#define MQTT_MESSAGE_EVENT 2 + +// Telnet +#define TELNET_SERIAL_BAUD 115200 +#define TELNET_MAX_COMMAND_LENGTH 80 // length of a command +#define TELNET_EVENT_CONNECT 1 +#define TELNET_EVENT_DISCONNECT 0 + +// ANSI Colors +#define COLOR_RESET "\x1B[0m" +#define COLOR_BLACK "\x1B[0;30m" +#define COLOR_RED "\x1B[0;31m" +#define COLOR_GREEN "\x1B[0;32m" +#define COLOR_YELLOW "\x1B[0;33m" +#define COLOR_BLUE "\x1B[0;34m" +#define COLOR_MAGENTA "\x1B[0;35m" +#define COLOR_CYAN "\x1B[0;36m" +#define COLOR_WHITE "\x1B[0;37m" +#define COLOR_BRIGHT_BLACK "\x1B[0;90m" +#define COLOR_BRIGHT_RED "\x1B[0;91m" +#define COLOR_BRIGHT_GREEN "\x1B[0;92m" +#define COLOR_BRIGHT_YELLOW "\x1B[0;99m" +#define COLOR_BRIGHT_BLUE "\x1B[0;94m" +#define COLOR_BRIGHT_MAGENTA "\x1B[0;95m" +#define COLOR_BRIGHT_CYAN "\x1B[0;96m" +#define COLOR_BRIGHT_WHITE "\x1B[0;97m" +#define COLOR_BOLD_ON "\x1B[1m" +#define COLOR_BOLD_OFF "\x1B[22m" // fixed by Scott Arlott + +// SPIFFS +#define SPIFFS_MAXSIZE 500 // https://arduinojson.org/v5/assistant/ + +typedef struct { + char key[40]; + char description[100]; +} command_t; + +typedef enum { MYESP_FSACTION_SET, MYESP_FSACTION_LIST, MYESP_FSACTION_SAVE, MYESP_FSACTION_LOAD } MYESP_FSACTION; + +typedef std::function mqtt_callback_f; +typedef std::function wifi_callback_f; +typedef std::function ota_callback_f; +typedef std::function telnetcommand_callback_f; +typedef std::function telnet_callback_f; +typedef std::function fs_callback_f; +typedef std::function fs_settings_callback_f; + +// calculates size of an 2d array at compile time +template +constexpr size_t ArraySize(T (&)[N]) { + return N; +} + +// class definition +class MyESP { + public: + MyESP(); + ~MyESP(); + + // wifi + void setWIFICallback(void (*callback)()); + void setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback); + bool isWifiConnected(); + + // mqtt + void mqttSubscribe(const char * topic); + void mqttUnsubscribe(const char * topic); + void mqttPublish(const char * topic, const char * payload); + void setMQTT(const char * mqtt_host, + const char * mqtt_username, + const char * mqtt_password, + const char * mqtt_base, + unsigned long mqtt_keepalive, + unsigned char mqtt_qos, + bool mqtt_retain, + const char * mqtt_will_topic, + const char * mqtt_will_online_payload, + const char * mqtt_will_offline_payload, + mqtt_callback_f callback); + + // OTA + void setOTA(ota_callback_f OTACallback); + + // debug & telnet + void myDebug(const char * format, ...); + void myDebug_P(PGM_P format_P, ...); + void setTelnet(command_t * cmds, uint8_t count, telnetcommand_callback_f callback_cmd, telnet_callback_f callback); + bool getUseSerial(); + + // FS + void setSettings(fs_callback_f callback, fs_settings_callback_f fs_settings_callback); + bool fs_saveConfig(); + + // general + void end(); + void loop(); + void begin(const char * app_hostname, const char * app_name, const char * app_version); + void setBoottime(const char * boottime); + void resetESP(); + uint16_t getSystemLoadAverage(); + int getWifiQuality(); + + + private: + // mqtt + AsyncMqttClient mqttClient; + unsigned long _mqtt_reconnect_delay; + void _mqttOnMessage(char * topic, char * payload, size_t len); + void _mqttConnect(); + void _mqtt_setup(); + mqtt_callback_f _mqtt_callback; + void _mqttOnConnect(); + void _sendStart(); + char * _mqttTopic(const char * topic); + char * _mqtt_host; + char * _mqtt_username; + char * _mqtt_password; + char * _mqtt_base; + unsigned long _mqtt_keepalive; + unsigned char _mqtt_qos; + bool _mqtt_retain; + char * _mqtt_will_topic; + char * _mqtt_will_online_payload; + char * _mqtt_will_offline_payload; + char * _mqtt_topic; + unsigned long _mqtt_last_connection; + bool _mqtt_connecting; + + // wifi + DNSServer dnsServer; // For Access Point (AP) support + void _wifiCallback(justwifi_messages_t code, char * parameter); + void _wifi_setup(); + wifi_callback_f _wifi_callback; + char * _wifi_ssid; + char * _wifi_password; + bool _wifi_connected; + + // ota + ota_callback_f _ota_callback; + void _ota_setup(); + void _OTACallback(); + + // telnet & debug + TelnetSpy SerialAndTelnet; + void _telnetConnected(); + void _telnetDisconnected(); + void _telnetHandle(); + void _telnetCommand(char * commandLine); + char * _telnet_readWord(); + void _telnet_setup(); + char _command[TELNET_MAX_COMMAND_LENGTH]; // the input command from either Serial or Telnet + command_t * _helpProjectCmds; // Help of commands setted by project + uint8_t _helpProjectCmds_count; // # available commands + void _consoleShowHelp(); + telnetcommand_callback_f _telnetcommand_callback; // Callable for projects commands + telnet_callback_f _telnet_callback; // callback for connect/disconnect + void _changeSetting(uint8_t wc, const char * setting, const char * value); + void _changeSetting2(const char * setting, const char * value1, const char * value2); + + // fs + void _fs_setup(); + bool _fs_loadConfig(); + void _fs_printConfig(); + void _fs_eraseConfig(); + + fs_callback_f _fs_callback; + fs_settings_callback_f _fs_settings_callback; + + // general + char * _app_hostname; + char * _app_name; + char * _app_version; + char * _boottime; + bool _suspendOutput; + bool _use_serial; + void _printBuildTime(unsigned long unix); + + // load average (0..100) + void _calculateLoad(); + unsigned short int _load_average; +}; + +extern MyESP myESP; + +#endif diff --git a/platformio.ini-example b/platformio.ini-example index 0a597bab5..bc1e25bf9 100644 --- a/platformio.ini-example +++ b/platformio.ini-example @@ -34,7 +34,11 @@ upload_speed = 921600 monitor_speed = 115200 ; for OTA comment out these sections ;upload_protocol = espota +<<<<<<< HEAD +;upload_port = +======= ;upload_port = ems-esp.local ;upload_port = +>>>>>>> upstream/dev diff --git a/rename_fw.py b/rename_fw.py deleted file mode 100644 index 7b4d42299..000000000 --- a/rename_fw.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -from subprocess import call -import os -Import("env") - -# see http://docs.platformio.org/en/latest/projectconf/advanced_scripting.html#before-pre-and-after-post-actions -# env.Replace(PROGNAME="firmware_%s" % defines.get("VERSION")) -env.Replace(PROGNAME="firmware_%s" % env['BOARD']) diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp index bf683b5fd..e991b2204 100644 --- a/src/ems-esp.cpp +++ b/src/ems-esp.cpp @@ -1346,6 +1346,77 @@ void initEMSESP() { EMSESP_Shower.doingColdShot = false; } +<<<<<<< HEAD:src/ems-esp.ino +// 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_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 + } +} + +======= +>>>>>>> upstream/dev:src/ems-esp.cpp /* * Shower Logic */ diff --git a/src/ems.cpp b/src/ems.cpp index 5425f6d63..45643f3db 100644 --- a/src/ems.cpp +++ b/src/ems.cpp @@ -32,7 +32,12 @@ CircularBuffer<_EMS_TxTelegram, EMS_TX_TELEGRAM_QUEUE_MAX> EMS_TxQueue; // FIFO #define _bitRead(i, bit) (((data[i]) >> (bit)) & 0x01) // generic +<<<<<<< HEAD +void _process_Version(uint8_t type, uint8_t * data, uint8_t length); +void _printMessage(uint8_t * telegram, uint8_t length); +======= void _process_Version(uint8_t src, uint8_t * data, uint8_t length); +>>>>>>> upstream/dev // Boiler and Buderus devices void _process_UBAMonitorFast(uint8_t src, uint8_t * data, uint8_t length); @@ -65,7 +70,14 @@ 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 +<<<<<<< HEAD +void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length); +//RC1010 +void _process_RC1010StatusMessage(uint8_t type, uint8_t * data, uint8_t length); +void _process_RC1010SetMessage(uint8_t type, uint8_t * data, uint8_t length); +======= void _process_EasyStatusMessage(uint8_t src, uint8_t * data, uint8_t length); +>>>>>>> upstream/dev /* * Recognized EMS types and the functions they call to process the telegrams @@ -74,60 +86,69 @@ void _process_EasyStatusMessage(uint8_t src, uint8_t * data, uint8_t length); const _EMS_Type EMS_Types[] = { // common - {EMS_MODEL_ALL, EMS_TYPE_Version, "Version", _process_Version}, + {EMS_MODEL_ALL, EMS_TYPE_Version, "Version", _process_Version, false}, // Boiler commands - {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorFast, "UBAMonitorFast", _process_UBAMonitorFast}, - {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorSlow, "UBAMonitorSlow", _process_UBAMonitorSlow}, - {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", _process_UBAMonitorWWMessage}, - {EMS_MODEL_UBA, EMS_TYPE_UBAParameterWW, "UBAParameterWW", _process_UBAParameterWW}, - {EMS_MODEL_UBA, EMS_TYPE_UBATotalUptimeMessage, "UBATotalUptimeMessage", _process_UBATotalUptimeMessage}, - {EMS_MODEL_UBA, EMS_TYPE_UBAMaintenanceSettingsMessage, "UBAMaintenanceSettingsMessage", NULL}, - {EMS_MODEL_UBA, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", _process_UBAParametersMessage}, - {EMS_MODEL_UBA, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorFast, "UBAMonitorFast", _process_UBAMonitorFast, false}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorSlow, "UBAMonitorSlow", _process_UBAMonitorSlow, false}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMonitorWWMessage, "UBAMonitorWWMessage", _process_UBAMonitorWWMessage, false}, + {EMS_MODEL_UBA, EMS_TYPE_UBAParameterWW, "UBAParameterWW", _process_UBAParameterWW, false}, + {EMS_MODEL_UBA, EMS_TYPE_UBATotalUptimeMessage, "UBATotalUptimeMessage", _process_UBATotalUptimeMessage, false}, + {EMS_MODEL_UBA, EMS_TYPE_UBAMaintenanceSettingsMessage, "UBAMaintenanceSettingsMessage", NULL, false}, + {EMS_MODEL_UBA, EMS_TYPE_UBAParametersMessage, "UBAParametersMessage", _process_UBAParametersMessage, false}, + {EMS_MODEL_UBA, EMS_TYPE_UBASetPoints, "UBASetPoints", _process_SetPoints, false}, // 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}, - {EMS_MODEL_RC10, EMS_TYPE_RC10StatusMessage, "RC10StatusMessage", _process_RC10StatusMessage}, + {EMS_MODEL_RC10, EMS_TYPE_RCTime, "RCTime", _process_RCTime, false}, + {EMS_MODEL_RC10, EMS_TYPE_RC10Set, "RC10Set", _process_RC10Set, false}, + {EMS_MODEL_RC10, EMS_TYPE_RC10StatusMessage, "RC10StatusMessage", _process_RC10StatusMessage, false}, // RC20 and RC20F - {EMS_MODEL_RC20, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_RC20, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_RC20, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, - {EMS_MODEL_RC20, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, + {EMS_MODEL_RC20, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage, false}, + {EMS_MODEL_RC20, EMS_TYPE_RCTime, "RCTime", _process_RCTime, false}, + {EMS_MODEL_RC20, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set, false}, + {EMS_MODEL_RC20, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage, false}, - {EMS_MODEL_RC20F, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_RC20F, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_RC20F, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set}, - {EMS_MODEL_RC20F, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage}, + {EMS_MODEL_RC20F, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage, false}, + {EMS_MODEL_RC20F, EMS_TYPE_RCTime, "RCTime", _process_RCTime, false}, + {EMS_MODEL_RC20F, EMS_TYPE_RC20Set, "RC20Set", _process_RC20Set, false}, + {EMS_MODEL_RC20F, EMS_TYPE_RC20StatusMessage, "RC20StatusMessage", _process_RC20StatusMessage, false}, // RC30 - {EMS_MODEL_RC30, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_RC30, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_RC30, EMS_TYPE_RC30Set, "RC30Set", _process_RC30Set}, - {EMS_MODEL_RC30, EMS_TYPE_RC30StatusMessage, "RC30StatusMessage", _process_RC30StatusMessage}, + {EMS_MODEL_RC30, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage, false}, + {EMS_MODEL_RC30, EMS_TYPE_RCTime, "RCTime", _process_RCTime, false}, + {EMS_MODEL_RC30, EMS_TYPE_RC30Set, "RC30Set", _process_RC30Set, false}, + {EMS_MODEL_RC30, EMS_TYPE_RC30StatusMessage, "RC30StatusMessage", _process_RC30StatusMessage, false}, // RC35 - {EMS_MODEL_RC35, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_RC35, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_RC35, EMS_TYPE_RC35Set, "RC35Set", _process_RC35Set}, - {EMS_MODEL_RC35, EMS_TYPE_RC35StatusMessage, "RC35StatusMessage", _process_RC35StatusMessage}, + {EMS_MODEL_RC35, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage, false}, + {EMS_MODEL_RC35, EMS_TYPE_RCTime, "RCTime", _process_RCTime, false}, + {EMS_MODEL_RC35, EMS_TYPE_RC35Set, "RC35Set", _process_RC35Set, false}, + {EMS_MODEL_RC35, EMS_TYPE_RC35StatusMessage, "RC35StatusMessage", _process_RC35StatusMessage, false}, // ES73 - {EMS_MODEL_ES73, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage}, - {EMS_MODEL_ES73, EMS_TYPE_RCTime, "RCTime", _process_RCTime}, - {EMS_MODEL_ES73, EMS_TYPE_RC35Set, "RC35Set", _process_RC35Set}, - {EMS_MODEL_ES73, EMS_TYPE_RC35StatusMessage, "RC35StatusMessage", _process_RC35StatusMessage}, + {EMS_MODEL_ES73, EMS_TYPE_RCOutdoorTempMessage, "RCOutdoorTempMessage", _process_RCOutdoorTempMessage, false}, + {EMS_MODEL_ES73, EMS_TYPE_RCTime, "RCTime", _process_RCTime, false}, + {EMS_MODEL_ES73, EMS_TYPE_RC35Set, "RC35Set", _process_RC35Set, false}, + {EMS_MODEL_ES73, EMS_TYPE_RC35StatusMessage, "RC35StatusMessage", _process_RC35StatusMessage, false}, // Easy +<<<<<<< HEAD + {EMS_MODEL_EASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage, false}, + {EMS_MODEL_BOSCHEASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage, false}, + //Ems plus + //Nefit 1010 + {EMS_MODEL_RC1010, EMS_TYPE_RC1010StatusMessage, "RC1010StatusMessage", _process_RC1010StatusMessage, true}, + {EMS_MODEL_RC1010, EMS_TYPE_RC1010Set, "RC1010SetMessage", _process_RC1010SetMessage, true}}; +======= {EMS_MODEL_EASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, {EMS_MODEL_BOSCHEASY, EMS_TYPE_EasyStatusMessage, "EasyStatusMessage", _process_EasyStatusMessage}, }; +>>>>>>> upstream/dev // calculate sizes of arrays at compile uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types @@ -696,6 +717,69 @@ void _ems_readTelegram(uint8_t * telegram, uint8_t length) { */ void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { // header +<<<<<<< HEAD + 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 4 + uint8_t ptype = telegram[3]; + uint8_t poffset = telegram[4]; + uint8_t * pdata = &telegram[5 + poffset]; // data block starts at position 5 plus the offset + _printMessage(telegram, length); + + // see if we recognize the type first by scanning our known EMS types list + // trying to match the type ID + bool commonType = false; + bool typeFound = false; + bool forUs = false; + int i = 0; + + while (i < _EMS_Types_max) { + if ((EMS_Types[i].type == type) || (EMS_Types[i].emsplus && type >= 240 && EMS_Types[i].type == ptype)) { + typeFound = true; + commonType = (EMS_Types[i].model_id == EMS_MODEL_ALL); // is it common type for everyone? + forUs = (src == EMS_Boiler.type_id) || (src == EMS_Thermostat.type_id); // is it for us? So the src must match + break; + } + i++; + } + + // if it's a common type (across ems devices) or something specifically for us process it. + // dest will be EMS_ID_NONE and offset 0x00 for a broadcast message + if (typeFound && (commonType || forUs)) { + if ((EMS_Types[i].processType_cb) != (void *)NULL) { + // print non-verbose message + if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_BASIC) || (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE)) { + if (EMS_Types[i].emsplus) + myDebug("<--- %s(0x%02X) received", EMS_Types[i].typeString, ptype); + else + myDebug("<--- %s(0x%02X) received", EMS_Types[i].typeString, type); + } + // call callback function to process it + if (EMS_Types[i].emsplus && poffset == EMS_PLUS_ID_NONE) + (void)EMS_Types[i].processType_cb(ptype, pdata, length - 6 - poffset); + // as we only handle complete telegrams (not partial) check that the offset is 0 + else if (offset == EMS_ID_NONE && !EMS_Types[i].emsplus) { + (void)EMS_Types[i].processType_cb(type, data, length - 5); + } + } + } +} +void _printMessage(uint8_t * telegram, uint8_t length) { + bool emsp = false; + 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 + if (type >= 240) { + type = telegram[3]; + offset = telegram[4]; + data = &telegram[5 + offset]; + emsp = true; + } +======= 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 @@ -703,6 +787,7 @@ void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { uint8_t offset = telegram[3]; uint8_t * data = telegram + 4; // data block starts at position 5 +>>>>>>> upstream/dev // print detailed telegram data if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_THERMOSTAT) { char output_str[200] = {0}; @@ -713,9 +798,16 @@ void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { if (src == EMS_Boiler.type_id) { strlcpy(output_str, "Boiler", sizeof(output_str)); } else if (src == EMS_Thermostat.type_id) { +<<<<<<< HEAD + if (emsp) + strlcpy(output_str, "Thermostat+", sizeof(output_str)); + else + strlcpy(output_str, "Thermostat", sizeof(output_str)); +======= strlcpy(output_str, "Thermostat", sizeof(output_str)); } else if (src == EMS_ID_SM10) { strlcpy(output_str, "SM10", sizeof(output_str)); +>>>>>>> upstream/dev } else { strlcpy(output_str, "0x", sizeof(output_str)); strlcat(output_str, _hextoa(src, buffer), sizeof(output_str)); @@ -725,24 +817,55 @@ void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { // destination if (dest == EMS_ID_ME) { - strlcat(output_str, "me", sizeof(output_str)); - strlcpy(color_s, COLOR_YELLOW, sizeof(color_s)); + if (emsp) { + strlcat(output_str, "me", sizeof(output_str)); + strlcpy(color_s, COLOR_BRIGHT_YELLOW, sizeof(color_s)); + } else { + strlcat(output_str, "me", sizeof(output_str)); + strlcpy(color_s, COLOR_YELLOW, sizeof(color_s)); + } } else if (dest == EMS_ID_NONE) { - strlcat(output_str, "all", sizeof(output_str)); - strlcpy(color_s, COLOR_GREEN, sizeof(color_s)); + if (emsp) { + strlcat(output_str, "all", sizeof(output_str)); + strlcpy(color_s, COLOR_BRIGHT_GREEN, sizeof(color_s)); + } else { + strlcat(output_str, "all", sizeof(output_str)); + strlcpy(color_s, COLOR_GREEN, sizeof(color_s)); + } } else if (dest == EMS_Boiler.type_id) { +<<<<<<< HEAD + if (emsp) { + strlcat(output_str, "Boiler", sizeof(output_str)); + strlcpy(color_s, COLOR_BRIGHT_MAGENTA, sizeof(color_s)); + } else { + strlcat(output_str, "Boiler", sizeof(output_str)); + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + } +======= 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)); +>>>>>>> upstream/dev } else if (dest == EMS_Thermostat.type_id) { - strlcat(output_str, "Thermostat", sizeof(output_str)); - strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + if (emsp) { + strlcat(output_str, "Thermostat", sizeof(output_str)); + strlcpy(color_s, COLOR_BRIGHT_MAGENTA, sizeof(color_s)); + } else { + strlcat(output_str, "Thermostat", sizeof(output_str)); + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + } } else { - strlcat(output_str, "0x", sizeof(output_str)); - strlcat(output_str, _hextoa(dest, buffer), sizeof(output_str)); - strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + if (emsp) { + strlcat(output_str, "0x", sizeof(output_str)); + strlcat(output_str, _hextoa(dest, buffer), sizeof(output_str)); + strlcpy(color_s, COLOR_BRIGHT_MAGENTA, sizeof(color_s)); + } else { + strlcat(output_str, "0x", sizeof(output_str)); + strlcat(output_str, _hextoa(dest, buffer), sizeof(output_str)); + strlcpy(color_s, COLOR_MAGENTA, sizeof(color_s)); + } } // type @@ -792,6 +915,7 @@ void _ems_processTelegram(_EMS_RxTelegram * EMS_RxTelegram) { } } } + EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE; } /** @@ -1061,6 +1185,13 @@ void _process_RC20StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { } /** +<<<<<<< HEAD + *type 0x41 - data from the RC30 thermostat(0x10) - 14 bytes long * For reading the temp values only * received every 60 seconds +*/ +void _process_RC30StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC30StatusMessage_setpoint]) / (float)2; + EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC30StatusMessage_curr, data); +======= * type 0x41 - data from the RC30 thermostat (0x10) - 14 bytes long * For reading the temp values only * received every 60 seconds @@ -1068,6 +1199,7 @@ void _process_RC20StatusMessage(uint8_t src, uint8_t * data, uint8_t length) { 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 +>>>>>>> upstream/dev EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } @@ -1094,13 +1226,32 @@ void _process_RC35StatusMessage(uint8_t src, 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 must be divided by 100 */ +<<<<<<< HEAD +void _process_EasyStatusMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.curr_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_curr] << 8) + data[9]))) / 100; + EMS_Thermostat.setpoint_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_setpoint] << 8) + data[11]))) / 100; + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT +} +/** + * type 0x00 - data from the Nefit RC1010 thermostat (0x18) - 24 bytes long + * The 1010 has a digital precision of its floats to 1 decimal places for the current temperature, so values is divided by 10 + * The 1010 has a digital precision of its floats to 1 decimal places for the set temperature, so values is divided by 2 + */ +void _process_RC1010StatusMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.curr_roomTemp = ((float)data[EMS_TYPE_RC1010StatusMessage_curr]) / (float)10; + EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC1010StatusMessage_set]) / (float)2; +======= 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 +>>>>>>> upstream/dev EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT } - +void _process_RC1010SetMessage(uint8_t type, uint8_t * data, uint8_t length) { + EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC1010Set]) / (float)2; + EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT +} /** * type 0xB0 - for reading the mode from the RC10 thermostat (0x17) * received only after requested @@ -1375,6 +1526,43 @@ void _ems_setThermostatModel(uint8_t thermostat_modelid) { } /** +<<<<<<< HEAD + * 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 + } + if (length > 5) { + 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]; + } +} + +/** +======= +>>>>>>> upstream/dev * Print the Tx queue - for debugging */ void ems_printTxQueue() { @@ -1629,12 +1817,15 @@ 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) || (dest == EMS_ID_NONE)) { +<<<<<<< HEAD +======= return; } // if we're preventing all outbound traffic, quit if (EMS_Sys_Status.emsTxDisabled) { myDebug("in Silent Mode. All Tx is disabled."); +>>>>>>> upstream/dev return; } diff --git a/src/ems.h b/src/ems.h index 73551e1cc..92752d23a 100644 --- a/src/ems.h +++ b/src/ems.h @@ -13,8 +13,9 @@ #include // EMS IDs -#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_NONE 0x00 // Fixed - used as a dest in broadcast messages and empty type IDs +#define EMS_PLUS_ID_NONE 0x01 // 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 @@ -262,6 +263,7 @@ typedef struct { uint8_t type; const char typeString[50]; EMS_processType_cb processType_cb; + bool emsplus; } _EMS_Type; // function definitions diff --git a/src/ems_devices.h b/src/ems_devices.h index e3cda412c..1085ae7d9 100644 --- a/src/ems_devices.h +++ b/src/ems_devices.h @@ -87,7 +87,12 @@ #define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat #define EMS_TYPE_EasyStatusMessage_setpoint 10 // setpoint temp #define EMS_TYPE_EasyStatusMessage_curr 8 // current temp - +// RC1010 specific +#define EMS_TYPE_RC1010StatusMessage 0x00 // is an automatic thermostat broadcast giving us temps +#define EMS_TYPE_RC1010StatusMessage_curr 1 // current temp +#define EMS_TYPE_RC1010StatusMessage_set 3 // setpoint temp +#define EMS_TYPE_RC1010Set 0x03 // setpoint temp message +#define EMS_TYPE_RC1010Set_set 0 // setpoint temp // Known EMS types typedef enum { EMS_MODEL_NONE, @@ -110,6 +115,7 @@ typedef enum { EMS_MODEL_BOSCHEASY, EMS_MODEL_RC310, EMS_MODEL_CW100, + EMS_MODEL_RC1010, EMS_MODEL_OT } _EMS_MODEL_ID; @@ -124,6 +130,16 @@ const _Boiler_Type Boiler_Types[] = { {EMS_MODEL_UBA, 203, 0x08, "Buderus Logamax U122"}, {EMS_MODEL_UBA, 208, 0x08, "Buderus Logamax plus"}, {EMS_MODEL_UBA, 64, 0x08, "Sieger BK15 Boiler/Nefit Smartline"}, +<<<<<<< HEAD + {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, 205, 0x08, "Nefit Moduline Easy Connect"}, + {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"} }; @@ -140,6 +156,7 @@ const _Other_Type Other_Types[] = { {EMS_MODEL_OTHER, 205, 0x02, "Nefit Moduline Easy Connect"}, {EMS_MODEL_OTHER, 73, EMS_ID_SM10, "SM10 Solar Module"} +>>>>>>> upstream/dev }; /* @@ -157,7 +174,12 @@ const _Thermostat_Type Thermostat_Types[] = { {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}, +<<<<<<< HEAD + {EMS_MODEL_RC1010, 165, 0x18, "RC1010/Nefit Moduline 1010)", EMS_THERMOSTAT_READ_NO, EMS_THERMOSTAT_WRITE_NO}, + {EMS_MODEL_OT, 171, 0x02, "EMS-OT OpenTherm converter", EMS_THERMOSTAT_READ_YES, EMS_THERMOSTAT_WRITE_YES}, +======= {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} +>>>>>>> upstream/dev }; diff --git a/src/my_config.h b/src/my_config.h index 0cc8fb30a..8dd0b2b10 100644 --- a/src/my_config.h +++ b/src/my_config.h @@ -53,10 +53,17 @@ #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 +<<<<<<< HEAD +// default values for shower logic on/off +#define BOILER_SHOWER_TIMER 0 // 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 +>>>>>>> upstream/dev //////////////////////////////////////////////////////////////////////////////////////////////////// // THESE DEFAULT VALUES CAN ALSO BE SET AND STORED WITHTIN THE APPLICATION (see 'set' command) // diff --git a/src/version.h b/src/version.h index 11aef61d0..6f9c9945a 100644 --- a/src/version.h +++ b/src/version.h @@ -5,6 +5,12 @@ #pragma once +<<<<<<< HEAD +#define APP_NAME "EMS-HEERENVEEN-1" +#define APP_VERSION "1.5.7b" +#define APP_HOSTNAME "ems-heerenveen-1" +======= #define APP_NAME "EMS-ESP" #define APP_VERSION "1.6.1b1" #define APP_HOSTNAME "ems-esp" +>>>>>>> upstream/dev