├── upload-ota.sh ├── .gitignore ├── upload-serial.sh ├── include ├── log.hpp ├── http.hpp ├── main.hpp ├── README ├── arbitration.hpp ├── track.hpp ├── bus.hpp ├── client.hpp ├── mqttha.hpp ├── busstate.hpp ├── schedule.hpp ├── mqtt.hpp └── store.hpp ├── fullflash.sh ├── auto_firmware_version.py ├── test └── README ├── upload_http_pio.py ├── src ├── log.cpp ├── arbitration.cpp ├── http.cpp ├── bus.cpp ├── mqtt.cpp ├── mqttha.cpp ├── client.cpp ├── store.cpp └── main.cpp ├── upload_http.py ├── lib └── README ├── .github └── workflows │ └── build.yml ├── platformio.ini ├── static ├── root.html └── log.html └── README.md /upload-ota.sh: -------------------------------------------------------------------------------- 1 | pio run -e esp12e-ota -t upload 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .pioenvs 3 | .piolibdeps 4 | .vscode/ 5 | .wiki/ 6 | -------------------------------------------------------------------------------- /upload-serial.sh: -------------------------------------------------------------------------------- 1 | #esptool.py erase_flash 2 | pio run -e esp12e -t upload 3 | -------------------------------------------------------------------------------- /include/log.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void addLog(String entry); 6 | String getLog(); 7 | -------------------------------------------------------------------------------- /include/http.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | void SetupHttpHandlers(); 7 | 8 | extern WebServer configServer; 9 | extern IotWebConf iotWebConf; 10 | -------------------------------------------------------------------------------- /fullflash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | OF=firmware-fullflash-HW_v5.x.bin 4 | 5 | tr '\0' '\377' < /dev/zero | dd bs=1 count=$((0x10000)) of=$OF 6 | dd if=.pio/build/esp32-c3/bootloader.bin of=$OF conv=notrunc 7 | dd if=.pio/build/esp32-c3/partitions.bin of=$OF bs=1 seek=$((0x8000)) conv=notrunc 8 | dd if=$HOME/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin of=$OF bs=1 seek=$((0xe000)) conv=notrunc 9 | dd if=.pio/build/esp32-c3/firmware.bin of=$OF bs=1 seek=$((0x10000)) 10 | 11 | -------------------------------------------------------------------------------- /auto_firmware_version.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | Import("env") 4 | 5 | def get_firmware_specifier_build_flag(): 6 | ret = subprocess.run(["git", "describe", "--tags", "--dirty"], stdout=subprocess.PIPE, text=True) #Uses any tags 7 | build_version = ret.stdout.strip() 8 | build_flag = "-D AUTO_VERSION=\\\"" + build_version + "\\\"" 9 | print ("Firmware Revision: " + build_version) 10 | return (build_flag) 11 | 12 | env.Append( 13 | BUILD_FLAGS=[get_firmware_specifier_build_flag()] 14 | ) -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | -------------------------------------------------------------------------------- /include/main.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #define MAX_WIFI_CLIENTS 4 9 | 10 | #define UART_TX 20 11 | #define UART_RX 21 12 | #if !defined(EBUS_INTERNAL) 13 | #define USE_SOFTWARE_SERIAL 1 14 | #define USE_ASYNCHRONOUS 1 // requires USE_SOFTWARE_SERIAL 15 | #endif 16 | #define AVAILABLE_THRESHOLD 0 // https://esp32.com/viewtopic.php?t=19788 17 | 18 | inline int DEBUG_LOG(const char* format, ...) { return 0; } 19 | int DEBUG_LOG_IMPL(const char* format, ...); 20 | // #define DEBUG_LOG DEBUG_LOG_IMPL 21 | 22 | char* status_string(); 23 | void restart(); 24 | const std::string getStatusJson(); 25 | void updateLastComms(); 26 | -------------------------------------------------------------------------------- /upload_http_pio.py: -------------------------------------------------------------------------------- 1 | import os 2 | from SCons.Script import DefaultEnvironment 3 | 4 | env = DefaultEnvironment() 5 | 6 | # Construct the actual firmware path PIO will generate 7 | firmware_file = os.path.join(env.subst("$BUILD_DIR"), env.subst("${PROGNAME}.bin")) 8 | 9 | def http_upload(source, target, env): 10 | firmware_path = firmware_file 11 | 12 | ip = env.GetProjectOption("upload_port") 13 | user = env.GetProjectOption("custom_upload_user") 14 | password = env.GetProjectOption("custom_upload_password") 15 | 16 | cmd = f"python3 upload_http.py {ip} {user} {password} {firmware_path}" 17 | print("Running:", cmd) 18 | 19 | return os.system(cmd) 20 | 21 | # Hook AFTER the binary is built 22 | env.AddPostAction(firmware_file, http_upload) 23 | -------------------------------------------------------------------------------- /src/log.cpp: -------------------------------------------------------------------------------- 1 | #include "log.hpp" 2 | 3 | #define MAX_LOG_ENTRIES 35 4 | String logBuffer[MAX_LOG_ENTRIES]; 5 | int logIndex = 0; 6 | int logEntries = 0; 7 | 8 | String getTimestamp() { 9 | time_t now = time(nullptr); 10 | struct tm timeinfo; 11 | localtime_r(&now, &timeinfo); 12 | 13 | char buffer[30]; 14 | snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d.%03d", 15 | timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, 16 | timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, millis() % 1000); 17 | 18 | return String(buffer); 19 | } 20 | 21 | void addLog(String entry) { 22 | String timestampedEntry = getTimestamp() + " " + entry; 23 | 24 | logBuffer[logIndex] = timestampedEntry; 25 | 26 | logIndex = (logIndex + 1) % MAX_LOG_ENTRIES; 27 | 28 | if (logEntries < MAX_LOG_ENTRIES) logEntries++; 29 | } 30 | 31 | String getLog() { 32 | String response = ""; 33 | for (int i = 0; i < logEntries; i++) { 34 | int index = (logIndex - logEntries + i + MAX_LOG_ENTRIES) % MAX_LOG_ENTRIES; 35 | response += logBuffer[index] + "\n"; 36 | } 37 | return response; 38 | } 39 | -------------------------------------------------------------------------------- /upload_http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import requests 4 | from requests.auth import HTTPBasicAuth 5 | 6 | def main(): 7 | print("Custom HTTP Upload Script") 8 | if len(sys.argv) < 5: 9 | print("Usage: upload_http.py ") 10 | sys.exit(1) 11 | 12 | ip = sys.argv[1] 13 | user = sys.argv[2] 14 | password = sys.argv[3] 15 | firmware_path = sys.argv[4] 16 | 17 | url = f"http://{ip}/firmware" 18 | 19 | print(f"Uploading {firmware_path} to {url} ...") 20 | 21 | with open(firmware_path, 'rb') as f: 22 | files = {"update": ("firmware.bin", f, "application/octet-stream")} 23 | response = requests.post( 24 | url, 25 | files=files, 26 | auth=HTTPBasicAuth(user, password), 27 | timeout=60 28 | ) 29 | 30 | if response.status_code == 200: 31 | print("✔ Upload OK") 32 | print(response.text) 33 | else: 34 | print(f"✖ Upload FAILED: {response.status_code}") 35 | print(response.text) 36 | sys.exit(1) 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: PlatformIO CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[1-9]* 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Cache pip 17 | uses: actions/cache@v3 18 | with: 19 | path: ~/.cache/pip 20 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 21 | restore-keys: | 22 | ${{ runner.os }}-pip- 23 | - name: Cache PlatformIO 24 | uses: actions/cache@v3 25 | with: 26 | path: ~/.platformio 27 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 28 | - name: Set up Python 29 | uses: actions/setup-python@v2 30 | - name: Install PlatformIO 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install --upgrade platformio 34 | - name: Prepare release dir 35 | run: mkdir release 36 | - name: Build v5.x-internal 37 | run: pio run -e esp32-c3-internal ; cp .pio/build/esp32-c3-internal/firmware.bin release/firmware-HW_v5.x-internal-${GITHUB_SHA::6}.bin 38 | - name: Build v5.x 39 | run: pio run -e esp32-c3 ; cp .pio/build/esp32-c3/firmware.bin release/firmware-HW_v5.x-${GITHUB_SHA::6}.bin 40 | - name: Assemble v5.x fullflash 41 | run: ./fullflash.sh ; mv firmware-fullflash-HW_v5.x.bin release/firmware-fullflash-HW_v5.x-${GITHUB_SHA::6}.bin 42 | - name: Upload artifacts 43 | uses: actions/upload-artifact@v4 44 | with: 45 | path: release 46 | 47 | release: 48 | if: startsWith(github.ref, 'refs/tags/') 49 | needs: build 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Download artifacts 53 | uses: actions/download-artifact@v4.1.7 54 | with: 55 | path: release 56 | - name: Create release 57 | uses: ncipollo/release-action@v1 58 | with: 59 | artifacts: "release/artifact/*" 60 | 61 | -------------------------------------------------------------------------------- /include/arbitration.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "busstate.hpp" 4 | 5 | // Implements the arbitration algorithm. Uses the state of the bus to decide 6 | // what to do. Typical usage: 7 | // - try to start the arbitration with "start" method 8 | // - pass each received value on the bus to the "data" method 9 | // which will then tell you what the state of the arbitration is 10 | class Arbitration { 11 | public: 12 | enum state { 13 | none, // no arbitration ongoing 14 | arbitrating, // arbitration ongoing 15 | won1, // won 16 | won2, // won 17 | lost1, // lost 18 | lost2, // lost 19 | error, // error 20 | restart1, // restart the arbitration 21 | restart2, // restart the arbitration 22 | }; 23 | 24 | Arbitration() 25 | : _arbitrating(false), 26 | _participateSecond(false), 27 | _arbitrationAddress(0), 28 | _restartCount(0) {} 29 | // Try to start arbitration for the specified master. 30 | // Return values: 31 | // - started : arbitration started. Make sure to pass all bus data to this 32 | // object through the "data" method 33 | // - not_started : arbitration not started. Possible reasons: 34 | // + the bus is not in a state that allows to start arbitration 35 | // + another arbitration is already ongoing 36 | // + the master address is SYN 37 | // - late : arbitration not started because the start is too late 38 | // compared to the SYN symbol received 39 | enum result { started, not_started, late }; 40 | result start(const BusState& busstate, uint8_t master, uint32_t startBitTime); 41 | 42 | // A symbol was received on the bus, what does this do to the arbitration 43 | // state? Return values: see description of state enum value 44 | Arbitration::state data(BusState& busstate, uint8_t symbol, 45 | uint32_t startBitTime); 46 | 47 | private: 48 | bool _arbitrating; 49 | bool _participateSecond; 50 | uint8_t _arbitrationAddress; 51 | int _restartCount; 52 | }; 53 | -------------------------------------------------------------------------------- /include/track.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(EBUS_INTERNAL) 4 | #include "mqtt.hpp" 5 | 6 | // The Track class can sum all kinds of primitive number types. After a minimum 7 | // time in seconds, the summed data is published under the specified mqtt topic. 8 | // After a maximum period of time, a publication is always carried out. 9 | 10 | template 11 | class Track { 12 | public: 13 | Track(const char* topic, const uint16_t minage, const uint16_t maxage = 60) 14 | : m_topic(topic), m_minage(minage), m_maxage(maxage) {} 15 | 16 | // OK xxx = 1; 17 | const Track& operator=(const T& value) { 18 | if (m_value != value) { 19 | m_value = value; 20 | publish(false); 21 | } else { 22 | if (millis() > m_last + m_maxage * 1000) publish(true); 23 | } 24 | return *this; 25 | } 26 | 27 | // OK xxx += 1; 28 | const Track& operator+=(const T& value) { 29 | m_value += value; 30 | publish(false); 31 | return *this; 32 | } 33 | 34 | // OK xxx += xxx; 35 | const Track& operator+=(const Track& rhs) { 36 | m_value += rhs.m_value; 37 | publish(false); 38 | return *this; 39 | } 40 | 41 | // OK xxx = xxx + xxx; 42 | friend Track operator+(Track lhs, const Track& rhs) { 43 | lhs += rhs; 44 | return lhs; 45 | } 46 | 47 | // OK ++xxx; 48 | const Track& operator++() { 49 | m_value++; 50 | publish(false); 51 | return *this; 52 | } 53 | 54 | // OK xxx++; 55 | const Track operator++(int) { 56 | Track old = *this; 57 | operator++(); 58 | return old; 59 | } 60 | 61 | const T& value() const { return m_value; } 62 | 63 | void publish() { publish(true); } 64 | 65 | void touch() { 66 | if (millis() > m_last + m_maxage * 1000) publish(true); 67 | } 68 | 69 | private: 70 | T m_value; 71 | const char* m_topic; 72 | const uint16_t m_minage = 0; 73 | const uint16_t m_maxage = 0; 74 | uint32_t m_last = 0; 75 | 76 | inline void publish(boolean force) { 77 | if (force || millis() > m_last + m_minage * 1000) { 78 | mqtt.publish(m_topic, 0, false, String(m_value).c_str()); 79 | m_last = millis(); 80 | } 81 | } 82 | }; 83 | #endif 84 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env] 12 | framework = arduino 13 | monitor_filters = esp32_exception_decoder 14 | extra_scripts = pre:auto_firmware_version.py 15 | lib_deps = 16 | https://github.com/prampec/IotWebConf#v3.2.1 17 | heman/AsyncMqttClient-esphome@^2.1.0 18 | bblanchon/ArduinoJson@^7.2.0 19 | 20 | build_flags = 21 | -DIOTWEBCONF_CONFIG_DONT_USE_MDNS=1 22 | -DIOTWEBCONF_PASSWORD_LEN=64 23 | board_build.embed_txtfiles = static/root.html 24 | 25 | [env:esp32-c3] 26 | platform = espressif32@6.5.0 27 | board = esp32-c3-devkitm-1 28 | board_build.partitions = min_spiffs.csv 29 | build_flags = 30 | ${env.build_flags} 31 | -DRESET_PIN=20 32 | -DPWM_PIN=6 33 | -DARDUINO_USB_MODE=1 34 | -DARDUINO_USB_CDC_ON_BOOT=1 35 | -DBusSer=Serial1 36 | -DDebugSer=Serial 37 | -DSTATUS_LED_PIN=3 38 | 39 | monitor_filters = esp32_exception_decoder 40 | lib_deps = 41 | ${env.lib_deps} 42 | https://github.com/guido4096/espsoftwareserial.git#add-startbit-timestamp 43 | 44 | [env:esp32-c3-ota] 45 | extends = env:esp32-c3 46 | upload_port = esp-ebus.local 47 | upload_protocol = espota 48 | 49 | [env:esp32-c3-internal] 50 | extends = env:esp32-c3 51 | 52 | build_flags = 53 | ${env.build_flags} 54 | -DRESET_PIN=20 55 | -DPWM_PIN=6 56 | -DARDUINO_USB_MODE=1 57 | -DARDUINO_USB_CDC_ON_BOOT=1 58 | -DBusSer=Serial1 59 | -DDebugSer=Serial 60 | -DSTATUS_LED_PIN=3 61 | -DEBUS_INTERNAL=1 62 | board_build.embed_txtfiles = 63 | ${env.board_build.embed_txtfiles} 64 | static/log.html 65 | 66 | lib_deps = 67 | ${env.lib_deps} 68 | https://github.com/yuhu-/ebus#430f61b 69 | 70 | [env:esp32-c3-internal-ota] 71 | extends = env:esp32-c3-internal 72 | upload_port = esp-ebus.local 73 | upload_protocol = espota 74 | 75 | [env:esp32-c3-internal-remote] 76 | extends = env:esp32-c3-internal 77 | extra_scripts = 78 | ${env.extra_scripts} 79 | upload_http_pio.py 80 | 81 | upload_protocol = custom 82 | upload_port = esp-ebus-remote.test # configured in hosts file 83 | custom_upload_user = admin 84 | custom_upload_password = ebusebus -------------------------------------------------------------------------------- /include/bus.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "arbitration.hpp" 8 | #include "busstate.hpp" 9 | 10 | enum responses { 11 | RESETTED = 0x0, 12 | RECEIVED = 0x1, 13 | STARTED = 0x2, 14 | INFO = 0x3, 15 | FAILED = 0xa, 16 | ERROR_EBUS = 0xb, 17 | ERROR_HOST = 0xc 18 | }; 19 | 20 | enum errors { ERR_FRAMING = 0x00, ERR_OVERRUN = 0x01 }; 21 | 22 | void getArbitrationClient(WiFiClient*& client, uint8_t& address); 23 | void clearArbitrationClient(); 24 | bool setArbitrationClient(WiFiClient*& client, uint8_t& address); 25 | 26 | void arbitrationDone(); 27 | WiFiClient* arbitrationRequested(uint8_t& address); 28 | 29 | #include "atomic" 30 | #define ATOMIC_INT std::atomic 31 | 32 | // This object retrieves data from the Serial object and let's 33 | // it flow through the arbitration process. The "read" method 34 | // will return data with meta information that tells what should 35 | // be done with the returned data. This object hides if the 36 | // underlying implementation is synchronous or asynchronous 37 | class BusType { 38 | public: 39 | // "receive" data should go to all clients that are not in arbitration mode 40 | // "enhanced" data should go only to the arbitrating client 41 | // a client is in arbitration mode if _client is not null 42 | struct data { 43 | bool _enhanced; // is this an enhanced command? 44 | uint8_t _c; // command byte, only used when in "enhanced" mode 45 | uint8_t _d; // data byte for both regular and enhanced command 46 | WiFiClient* _client; // the client that is being arbitrated 47 | WiFiClient* _logtoclient; // the client that needs to log 48 | }; 49 | BusType(); 50 | ~BusType(); 51 | 52 | // begin and end, like with Serial 53 | void begin(); 54 | void end(); 55 | 56 | // Is there a value available that should be send to a client? 57 | bool read(data& d); 58 | size_t write(uint8_t symbol); 59 | int availableForWrite(); 60 | int available(); 61 | 62 | // std::atomic seems not well supported on esp12e, besides it is also not 63 | // needed there 64 | ATOMIC_INT _nbrRestarts1; 65 | ATOMIC_INT _nbrRestarts2; 66 | ATOMIC_INT _nbrArbitrations; 67 | ATOMIC_INT _nbrLost1; 68 | ATOMIC_INT _nbrLost2; 69 | ATOMIC_INT _nbrWon1; 70 | ATOMIC_INT _nbrWon2; 71 | ATOMIC_INT _nbrErrors; 72 | ATOMIC_INT _nbrLate; 73 | 74 | private: 75 | inline void push(const data& d); 76 | void receive(uint8_t symbol, uint32_t startBitTime); 77 | BusState _busState; 78 | Arbitration _arbitration; 79 | WiFiClient* _client; 80 | 81 | #if USE_ASYNCHRONOUS 82 | // handler to be notified when there is signal change on the serial input 83 | static void IRAM_ATTR receiveHandler(); 84 | 85 | // queue from Bus to read method 86 | QueueHandle_t _queue; 87 | 88 | // task to read bytes form the serial object and process them with receive 89 | // methods 90 | TaskHandle_t _serialEventTask; 91 | 92 | static void readDataFromSoftwareSerial(void* args); 93 | #else 94 | std::queue _queue; 95 | #endif 96 | }; 97 | 98 | extern BusType Bus; 99 | -------------------------------------------------------------------------------- /include/client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | bool handleNewClient(WiFiServer* server, WiFiClient clients[]); 7 | 8 | void handleClient(WiFiClient* client); 9 | int pushClient(WiFiClient* client, uint8_t byte); 10 | 11 | void handleClientEnhanced(WiFiClient* client); 12 | int pushClientEnhanced(WiFiClient* client, uint8_t c, uint8_t d, bool log); 13 | 14 | #if defined(EBUS_INTERNAL) 15 | #include 16 | 17 | #include 18 | 19 | // C++11 compatible make_unique 20 | template 21 | std::unique_ptr make_unique(Args&&... args) { 22 | return std::unique_ptr(new T(std::forward(args)...)); 23 | } 24 | 25 | // Abstract base class for all client types 26 | class AbstractClient { 27 | public: 28 | AbstractClient(WiFiClient* client, ebus::Request* request, bool write); 29 | 30 | virtual bool available() const = 0; 31 | virtual bool readByte(uint8_t& byte) = 0; 32 | virtual bool writeBytes(const std::vector& bytes) = 0; 33 | virtual bool handleBusData(const uint8_t& byte) = 0; 34 | 35 | bool isWriteCapable() const; 36 | bool isConnected() const; 37 | void stop(); 38 | 39 | protected: 40 | WiFiClient* client; 41 | ebus::Request* request; 42 | bool write; 43 | }; 44 | 45 | // ReadOnly client: only sends, never receives 46 | class ReadOnlyClient : public AbstractClient { 47 | public: 48 | ReadOnlyClient(WiFiClient* client, ebus::Request* request); 49 | 50 | bool available() const override; 51 | bool readByte(uint8_t& byte) override; 52 | bool writeBytes(const std::vector& bytes) override; 53 | bool handleBusData(const uint8_t& byte) override; 54 | }; 55 | 56 | // Regular client: 1 byte per message 57 | class RegularClient : public AbstractClient { 58 | public: 59 | RegularClient(WiFiClient* client, ebus::Request* request); 60 | 61 | bool available() const override; 62 | bool readByte(uint8_t& byte) override; 63 | bool writeBytes(const std::vector& bytes) override; 64 | bool handleBusData(const uint8_t& byte) override; 65 | }; 66 | 67 | // Enhanced client: 1 or 2 bytes per message (protocol encoding/decoding) 68 | class EnhancedClient : public AbstractClient { 69 | public: 70 | EnhancedClient(WiFiClient* client, ebus::Request* request); 71 | 72 | bool available() const override; 73 | bool readByte(uint8_t& byte) override; 74 | bool writeBytes(const std::vector& bytes) override; 75 | bool handleBusData(const uint8_t& byte) override; 76 | }; 77 | 78 | class ClientManager { 79 | public: 80 | ClientManager(); 81 | 82 | void start(ebus::Bus* bus, ebus::Request* request, 83 | ebus::ServiceRunnerFreeRtos* serviceRunner); 84 | 85 | void stop(); 86 | 87 | private: 88 | WiFiServer readonlyServer; 89 | WiFiServer regularServer; 90 | WiFiServer enhancedServer; 91 | 92 | ebus::Queue* clientByteQueue = nullptr; 93 | volatile bool stopRunner = false; 94 | volatile bool busRequested = false; 95 | 96 | ebus::Request* request = nullptr; 97 | ebus::ServiceRunnerFreeRtos* serviceRunner = nullptr; 98 | 99 | std::vector> clients; 100 | 101 | enum class BusState { Idle, Request, Transmit, Response }; 102 | 103 | TaskHandle_t clientManagerTaskHandle; 104 | 105 | static void taskFunc(void* arg); 106 | 107 | void acceptClients(); 108 | }; 109 | 110 | extern ClientManager clientManager; 111 | #endif 112 | -------------------------------------------------------------------------------- /include/mqttha.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(EBUS_INTERNAL) 4 | #include 5 | #include 6 | 7 | // Home Assistant MQTT class for auto discovery 8 | 9 | class MqttHA { 10 | public: 11 | void setUniqueId(const std::string& id); 12 | void setRootTopic(const std::string& topic); 13 | void setWillTopic(const std::string& topic); 14 | 15 | void setEnabled(const bool enable); 16 | const bool isEnabled() const; 17 | 18 | void setThingName(const std::string& name); 19 | void setThingModel(const std::string& model); 20 | void setThingModelId(const std::string& modelId); 21 | void setThingManufacturer(const std::string& manufacturer); 22 | void setThingSwVersion(const std::string& swVersion); 23 | void setThingHwVersion(const std::string& hwVersion); 24 | void setThingConfigurationUrl(const std::string& configurationUrl); 25 | 26 | void publishDeviceInfo() const; 27 | 28 | void publishComponents() const; 29 | 30 | void publishComponent(const Command* command, const bool remove) const; 31 | 32 | private: 33 | std::string uniqueId; // e.g. "8406ac" 34 | std::string deviceIdentifiers; // e.g. "ebus8406ac" 35 | std::string rootTopic; // e.g. "ebus/8406ac/" 36 | std::string commandTopic; // e.g. "ebus/8406ac/request" 37 | std::string willTopic; // e.g. "ebus/8406ac/state/available" 38 | 39 | bool enabled = false; 40 | 41 | // Common thing data 42 | std::string thingName; 43 | std::string thingModel; 44 | std::string thingModelId; 45 | std::string thingManufacturer; 46 | std::string thingSwVersion; 47 | std::string thingHwVersion; 48 | std::string thingConfigurationUrl; 49 | 50 | struct Component { 51 | // Mandatory Home Assistant config fields 52 | std::string component; // "sensor", "number", "select" 53 | std::string objectId; // name (lowercase, space and / replaced by _) 54 | std::string uniqueId; // uniqueId + key / postfix 55 | std::string name; // display name 56 | std::string deviceIdentifiers; // e.g. "ebus8406ac" (node identifier) 57 | 58 | // Home Assistant config fields 59 | std::map fields; 60 | std::vector options; 61 | std::map device; 62 | }; 63 | 64 | void publishComponent(const Component& c, const bool remove) const; 65 | 66 | const std::string getComponentJson(const Component& c) const; 67 | 68 | std::string createStateTopic(const std::string& prefix, 69 | const std::string& topic) const; 70 | 71 | struct KeyValueMapping { 72 | std::vector options; 73 | std::string valueMap; 74 | std::string cmdMap; 75 | }; 76 | 77 | static KeyValueMapping createOptions( 78 | const std::map& ha_key_value_map, 79 | const int& ha_default_key); 80 | 81 | Component createComponent(const std::string& component, 82 | const std::string& uniqueIdKey, 83 | const std::string& name) const; 84 | 85 | Component createBinarySensor(const Command* command) const; 86 | Component createSensor(const Command* command) const; 87 | Component createNumber(const Command* command) const; 88 | Component createSelect(const Command* command) const; 89 | Component createSwitch(const Command* command) const; 90 | 91 | Component createButtonRestart() const; 92 | 93 | Component createDiagnostic(const std::string& component, 94 | const std::string& uniqueIdKey, 95 | const std::string& name) const; 96 | 97 | Component createDiagnosticUptime() const; 98 | Component createDiagnosticFreeHeap() const; 99 | Component createDiagnosticLoopDuration() const; 100 | }; 101 | 102 | extern MqttHA mqttha; 103 | #endif -------------------------------------------------------------------------------- /static/root.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | esp-eBus adapter 6 | 46 | 51 | 52 | 53 |
54 |

esp-eBus Adapter

55 | 56 |
57 | Adapter 58 |
59 | Configuration 60 | Firmware Update 61 | Status 62 | Log 63 | Restart 64 |
65 |
66 | Login:
67 | user: admin
68 | password: your configured AP mode password or default 69 |
70 |
71 | 72 |
73 | Commands 74 |
75 | List 76 | Upload 77 | Download 78 | Load 79 | Save 80 | Wipe 81 | Values 82 | Scan 83 | Scan Full 84 | Scan Vendor 85 | Scan result 86 | Reset stats 87 |
88 |
89 | 90 |
91 | Project: esp-arduino-ebus on GitHub 92 |
93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /include/busstate.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "main.hpp" 4 | 5 | enum symbols { SYN = 0xAA }; 6 | 7 | // Implements the state of the bus. The arbitration process can 8 | // only start at well defined states of the bus. To asses the 9 | // state, all data received on the bus needs to be send to this 10 | // object. The object takes care of startup of the bus and 11 | // recovery when an unexpected event happens. 12 | class BusState { 13 | public: 14 | enum eState { 15 | eStartup, // In startup mode to analyze bus state 16 | eStartupFirstSyn, // Either the bus is busy, it is arbitrating, or it is 17 | // free to start an arbitration 18 | eStartupSymbolAfterFirstSyn, 19 | eStartupSecondSyn, 20 | eReceivedFirstSYN, // Received SYN 21 | eReceivedAddressAfterFirstSYN, // Received SYN ADDRESS 22 | eReceivedSecondSYN, // Received SYN ADDRESS SYN 23 | eReceivedAddressAfterSecondSYN, // Received SYN ADDRESS SYN ADDRESS 24 | eBusy // Bus is busy; _master is master that won, _byte is first symbol 25 | // after the master address 26 | }; 27 | static const char* enumvalue(eState e) { 28 | const char* values[] = {"eStartup", 29 | "eStartupFirstSyn", 30 | "eStartupSymbolAfterFirstSyn", 31 | "eStartupSecondSyn", 32 | "eReceivedFirstSYN", 33 | "eReceivedAddressAfterFirstSYN", 34 | "eReceivedSecondSYN", 35 | "eReceivedAddressAfterSecondSYN", 36 | "eBusy"}; 37 | return values[e]; 38 | } 39 | BusState() : _state(eStartup), _previousState(eStartup) {} 40 | // Evaluate a symbol received on UART and determine what the new state of the 41 | // bus is 42 | inline void data(uint8_t symbol) { 43 | switch (_state) { 44 | case eStartup: 45 | _previousState = _state; 46 | _state = symbol == SYN ? syn(eStartupFirstSyn) : eStartup; 47 | break; 48 | case eStartupFirstSyn: 49 | _previousState = _state; 50 | _state = symbol == SYN ? syn(eReceivedFirstSYN) 51 | : eStartupSymbolAfterFirstSyn; 52 | break; 53 | case eStartupSymbolAfterFirstSyn: 54 | _previousState = _state; 55 | _state = symbol == SYN ? syn(eStartupSecondSyn) : eBusy; 56 | break; 57 | case eStartupSecondSyn: 58 | _previousState = _state; 59 | _state = symbol == SYN ? syn(eReceivedFirstSYN) : eBusy; 60 | break; 61 | case eReceivedFirstSYN: 62 | _previousState = _state; 63 | _state = symbol == SYN ? syn(eReceivedFirstSYN) 64 | : eReceivedAddressAfterFirstSYN; 65 | _master = symbol; 66 | break; 67 | case eReceivedAddressAfterFirstSYN: 68 | _previousState = _state; 69 | _state = symbol == SYN ? syn(eReceivedSecondSYN) : eBusy; 70 | _symbol = symbol; 71 | break; 72 | case eReceivedSecondSYN: 73 | _previousState = _state; 74 | _state = symbol == SYN ? error(_state, eReceivedFirstSYN) 75 | : eReceivedAddressAfterSecondSYN; 76 | _master = symbol; 77 | break; 78 | case eReceivedAddressAfterSecondSYN: 79 | _previousState = _state; 80 | _state = symbol == SYN ? error(_state, eReceivedFirstSYN) : eBusy; 81 | _symbol = symbol; 82 | break; 83 | case eBusy: 84 | _previousState = _state; 85 | _state = symbol == SYN ? syn(eReceivedFirstSYN) : eBusy; 86 | break; 87 | } 88 | } 89 | inline eState syn(eState newstate) { 90 | _previousSYNtime = _SYNtime; 91 | _SYNtime = micros(); 92 | return newstate; 93 | } 94 | eState error(eState currentstate, eState newstate) { 95 | _previousSYNtime = _SYNtime; 96 | _SYNtime = micros(); 97 | DEBUG_LOG( 98 | "unexpected SYN on bus while state is %s, setting state to %s " 99 | "m=0x%02x, b=0x%02x %lu us\n", 100 | enumvalue(currentstate), enumvalue(newstate), _master, _symbol, 101 | microsSincePreviousSyn()); 102 | return newstate; 103 | } 104 | 105 | void reset() { _state = eStartup; } 106 | 107 | const uint32_t microsSinceLastSyn() const { return micros() - _SYNtime; } 108 | 109 | const uint32_t microsSincePreviousSyn() const { 110 | return micros() - _previousSYNtime; 111 | } 112 | 113 | eState _state; 114 | eState _previousState; 115 | uint8_t _master = 0; 116 | uint8_t _symbol = 0; 117 | uint32_t _SYNtime = 0; 118 | uint32_t _previousSYNtime = 0; 119 | }; 120 | -------------------------------------------------------------------------------- /include/schedule.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(EBUS_INTERNAL) 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "store.hpp" 14 | 15 | // Active commands are sent on the eBUS at scheduled intervals, and the received 16 | // data is saved. Passive received messages are compared against defined 17 | // commands, and if they match, the received data is also saved. Furthermore, it 18 | // is possible to send individual commands to the eBUS. The results are returned 19 | // along with the command as raw data. Defined messages (filter function) can be 20 | // forwarded. Scanning of eBUS participants is also available. 21 | 22 | constexpr uint8_t VENDOR_VAILLANT = 0xb5; 23 | 24 | struct Participant { 25 | uint8_t slave; 26 | std::vector vec_070400; 27 | std::vector vec_b5090124; 28 | std::vector vec_b5090125; 29 | std::vector vec_b5090126; 30 | std::vector vec_b5090127; 31 | 32 | bool isVaillant() const { 33 | return (vec_070400.size() > 1 && vec_070400[1] == VENDOR_VAILLANT); 34 | } 35 | 36 | bool isVaillantValid() const { 37 | return (vec_b5090124.size() > 0 && vec_b5090125.size() > 0 && 38 | vec_b5090126.size() > 0 && vec_b5090127.size() > 0); 39 | } 40 | }; 41 | 42 | class Schedule { 43 | public: 44 | Schedule() = default; 45 | 46 | void start(ebus::Request* request, ebus::Handler* handler); 47 | void stop(); 48 | 49 | void setSendInquiryOfExistence(const bool enable); 50 | void setScanOnStartup(const bool enable); 51 | void setDistance(const uint8_t distance); 52 | 53 | void handleScanFull(); 54 | void handleScan(); 55 | void handleScanAddresses(const JsonArrayConst& addresses); 56 | void handleScanVendor(); 57 | 58 | void handleSend(const std::vector& command); 59 | void handleSend(const JsonArrayConst& commands); 60 | 61 | void handleWrite(const std::vector& command); 62 | 63 | void toggleForward(const bool enable); 64 | void handleForwardFilter(const JsonArrayConst& filters); 65 | 66 | void setPublishCounter(const bool enable); 67 | void resetCounter(); 68 | void fetchCounter(); 69 | const std::string getCounterJson(); 70 | 71 | void setPublishTiming(const bool enable); 72 | void resetTiming(); 73 | void fetchTiming(); 74 | const std::string getTimingJson(); 75 | 76 | static JsonDocument getParticipantJson(const Participant* participant); 77 | const std::string getParticipantsJson() const; 78 | 79 | const std::vector getParticipants(); 80 | 81 | private: 82 | ebus::Request* ebusRequest = nullptr; 83 | ebus::Handler* ebusHandler = nullptr; 84 | 85 | volatile bool stopRunner = false; 86 | 87 | bool sendInquiryOfExistence = false; 88 | bool scanOnStartup = false; 89 | 90 | enum class Mode { schedule, internal, scan, fullscan, send, read, write }; 91 | Mode mode = Mode::schedule; 92 | 93 | struct QueuedCommand { 94 | Mode mode; 95 | uint8_t priority; // higher = higher priority 96 | uint32_t timestamp; // millis() when enqueued older = higher priority 97 | std::vector command; 98 | Command* scheduleCommand = nullptr; 99 | 100 | QueuedCommand(Mode m, uint8_t p, std::vector cmd, Command* active) 101 | : mode(m), 102 | priority(p), 103 | timestamp(millis()), 104 | command(cmd), 105 | scheduleCommand(active) {} 106 | }; 107 | 108 | std::vector queuedCommands; 109 | 110 | Command* scheduleCommand = nullptr; 111 | uint32_t scheduleCommandSetTime = 0; // time when command was scheduled 112 | uint32_t scheduleCommandTimeout = 2 * 1000; // 2 seconds after schedule 113 | 114 | uint32_t distanceCommands = 0; // in milliseconds 115 | uint32_t lastCommand = 10 * 1000; // 10 seconds after start 116 | 117 | uint32_t distanceScans = 10 * 1000; // 10 seconds after start 118 | uint32_t lastScan = 0; // in milliseconds 119 | uint8_t maxScans = 5; // maximum number of scans 120 | uint8_t currentScan = 0; // current scan count 121 | 122 | bool fullScan = false; 123 | uint8_t scanIndex = 0; 124 | 125 | std::map allParticipants; 126 | 127 | bool forward = false; 128 | std::vector> forwardfilters; 129 | 130 | bool publishCounter = false; 131 | bool publishTiming = false; 132 | 133 | enum class CallbackType { telegram, error }; 134 | 135 | struct CallbackEvent { 136 | CallbackType type; 137 | Mode mode; 138 | struct { 139 | ebus::MessageType messageType; 140 | ebus::TelegramType telegramType; 141 | std::vector master; 142 | std::vector slave; 143 | std::string error; 144 | } data; 145 | }; 146 | 147 | ebus::Queue eventQueue{8}; 148 | 149 | TaskHandle_t scheduleTaskHandle; 150 | 151 | static void taskFunc(void* arg); 152 | 153 | void handleEvents(); 154 | 155 | void handleCommands(); 156 | 157 | void enqueueCommand(const QueuedCommand& cmd); 158 | 159 | void enqueueStartupScanCommands(); 160 | 161 | void enqueueScheduleCommand(); 162 | 163 | void enqueueFullScanCommand(); 164 | 165 | static void reactiveMasterSlaveCallback(const std::vector& master, 166 | std::vector* const slave); 167 | 168 | void processActive(const Mode& mode, const std::vector& master, 169 | const std::vector& slave); 170 | 171 | void processPassive(const std::vector& master, 172 | const std::vector& slave); 173 | 174 | void processScan(const std::vector& master, 175 | const std::vector& slave); 176 | }; 177 | 178 | extern Schedule schedule; 179 | #endif 180 | -------------------------------------------------------------------------------- /include/mqtt.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(EBUS_INTERNAL) 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "schedule.hpp" 14 | #include "store.hpp" 15 | 16 | enum class IncomingActionType { Insert, Remove }; 17 | 18 | struct IncomingAction { 19 | IncomingActionType type; 20 | Command command; // for Insert 21 | std::string key; // for Remove 22 | 23 | explicit IncomingAction(const Command& cmd) 24 | : type(IncomingActionType::Insert), command(cmd), key("") {} 25 | 26 | explicit IncomingAction(const std::string& k) 27 | : type(IncomingActionType::Remove), command(), key(k) {} 28 | }; 29 | 30 | enum class OutgoingActionType { Command, Participant, Component }; 31 | 32 | struct OutgoingAction { 33 | OutgoingActionType type; 34 | const Command* command; // for Command and Component 35 | const Participant* participant; // for Participant 36 | bool haRemove; // for Component 37 | 38 | explicit OutgoingAction(const Command* cmd) 39 | : type(OutgoingActionType::Command), 40 | command(cmd), 41 | participant(nullptr), 42 | haRemove(false) {} 43 | 44 | explicit OutgoingAction(const Participant* part) 45 | : type(OutgoingActionType::Participant), 46 | command(nullptr), 47 | participant(part), 48 | haRemove(false) {} 49 | 50 | explicit OutgoingAction(const Command* cmd, bool remove) 51 | : type(OutgoingActionType::Component), 52 | command(cmd), 53 | participant(nullptr), 54 | haRemove(remove) {} 55 | }; 56 | 57 | using CommandHandler = std::function; 58 | 59 | // The MQTT class acts as a wrapper for the entire MQTT subsystem. 60 | 61 | class Mqtt { 62 | public: 63 | Mqtt(); 64 | 65 | void setUniqueId(const char* id); 66 | 67 | const std::string& getUniqueId() const; 68 | const std::string& getRootTopic() const; 69 | const std::string& getWillTopic() const; 70 | 71 | void setServer(const char* host, uint16_t port); 72 | void setCredentials(const char* username, const char* password = nullptr); 73 | 74 | void setEnabled(const bool enable); 75 | const bool isEnabled() const; 76 | 77 | void connect(); 78 | const bool connected() const; 79 | 80 | void disconnect(); 81 | 82 | uint16_t publish(const char* topic, uint8_t qos, bool retain, 83 | const char* payload = nullptr, bool prefix = true); 84 | 85 | static void enqueueOutgoing(const OutgoingAction& action); 86 | 87 | static void publishData(const std::string& id, 88 | const std::vector& master, 89 | const std::vector& slave); 90 | 91 | static void publishValue(const Command* command, const JsonDocument& doc); 92 | 93 | void doLoop(); 94 | 95 | private: 96 | AsyncMqttClient client; 97 | std::string uniqueId; 98 | std::string rootTopic; 99 | std::string willTopic; 100 | 101 | bool enabled = false; 102 | 103 | std::queue incomingQueue; 104 | uint32_t lastIncoming = 0; 105 | uint32_t incomingInterval = 200; // ms 106 | 107 | std::queue outgoingQueue; 108 | uint32_t lastOutgoing = 0; 109 | uint32_t outgoingInterval = 200; // ms 110 | 111 | // Command handlers map 112 | std::unordered_map commandHandlers = { 113 | {"restart", [this](const JsonDocument& doc) { handleRestart(doc); }}, 114 | {"insert", [this](const JsonDocument& doc) { handleInsert(doc); }}, 115 | {"remove", [this](const JsonDocument& doc) { handleRemove(doc); }}, 116 | {"publish", [this](const JsonDocument& doc) { handlePublish(doc); }}, 117 | 118 | {"load", [this](const JsonDocument& doc) { handleLoad(doc); }}, 119 | {"save", [this](const JsonDocument& doc) { handleSave(doc); }}, 120 | {"wipe", [this](const JsonDocument& doc) { handleWipe(doc); }}, 121 | 122 | {"scan", [this](const JsonDocument& doc) { handleScan(doc); }}, 123 | {"participants", 124 | [this](const JsonDocument& doc) { handleParticipants(doc); }}, 125 | 126 | {"send", [this](const JsonDocument& doc) { handleSend(doc); }}, 127 | {"forward", [this](const JsonDocument& doc) { handleForward(doc); }}, 128 | 129 | {"reset", [this](const JsonDocument& doc) { handleReset(doc); }}, 130 | 131 | {"read", [this](const JsonDocument& doc) { handleRead(doc); }}, 132 | {"write", [this](const JsonDocument& doc) { handleWrite(doc); }}, 133 | }; 134 | 135 | uint16_t subscribe(const char* topic, uint8_t qos); 136 | 137 | static void onConnect(bool sessionPresent); 138 | static void onDisconnect(AsyncMqttClientDisconnectReason reason) {} 139 | 140 | static void onSubscribe(uint16_t packetId, uint8_t qos) {} 141 | static void onUnsubscribe(uint16_t packetId) {} 142 | 143 | static void onMessage(const char* topic, const char* payload, 144 | AsyncMqttClientMessageProperties properties, size_t len, 145 | size_t index, size_t total); 146 | 147 | static void onPublish(uint16_t packetId) {} 148 | 149 | // Command handlers 150 | static void handleRestart(const JsonDocument& doc); 151 | void handleInsert(const JsonDocument& doc); 152 | void handleRemove(const JsonDocument& doc); 153 | static void handlePublish(const JsonDocument& doc); 154 | 155 | static void handleLoad(const JsonDocument& doc); 156 | static void handleSave(const JsonDocument& doc); 157 | static void handleWipe(const JsonDocument& doc); 158 | 159 | void handleScan(const JsonDocument& doc); 160 | static void handleParticipants(const JsonDocument& doc); 161 | 162 | void handleSend(const JsonDocument& doc); 163 | void handleForward(const JsonDocument& doc); 164 | 165 | static void handleReset(const JsonDocument& doc); 166 | 167 | void handleRead(const JsonDocument& doc); 168 | void handleWrite(const JsonDocument& doc); 169 | 170 | void checkIncomingQueue(); 171 | void checkOutgoingQueue(); 172 | 173 | void publishResponse(const std::string& id, const std::string& status, 174 | const size_t& bytes = 0); 175 | 176 | void publishCommand(const Command* command); 177 | 178 | void publishParticipant(const Participant* participant); 179 | }; 180 | 181 | extern Mqtt mqtt; 182 | #endif 183 | -------------------------------------------------------------------------------- /include/store.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(EBUS_INTERNAL) 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // This Store class stores both active and passive eBUS commands. For permanent 13 | // storage (NVS), functions for saving, loading, and deleting commands are 14 | // available. Permanently stored commands are automatically loaded when the 15 | // device is restarted. 16 | 17 | enum FieldType { 18 | FT_String, 19 | FT_HexString, 20 | FT_Bool, 21 | FT_Int, 22 | FT_Float, 23 | FT_Uint8T, 24 | FT_Uint32T, 25 | FT_SizeT, 26 | FT_DataType, 27 | FT_KeyValueMap 28 | }; 29 | 30 | // Hash for std::vector 31 | struct VectorHash { 32 | std::size_t operator()(const std::vector& vec) const { 33 | std::size_t hash = vec.size(); 34 | for (const uint8_t& h : vec) { 35 | hash ^= std::hash()(h) + 0x9e3779b9 + (hash << 6) + (hash >> 2); 36 | } 37 | return hash; 38 | } 39 | }; 40 | 41 | // clang-format off 42 | struct Command { 43 | // Internal Fields 44 | uint32_t last = 0; // last time of the successful command 45 | std::vector data = {}; // received raw data 46 | size_t length = 1; // length of datatype 47 | bool numeric = false; // indicates numeric datatype 48 | 49 | // Command fields 50 | std::string key = ""; // unique key of command 51 | std::string name = ""; // name of the command used as mqtt topic below "values/" 52 | std::vector read_cmd = {}; // read command as vector of "ZZPBSBNNDBx" 53 | std::vector write_cmd = {}; // write command as vector of "ZZPBSBNNDBx" (OPTIONAL) 54 | bool active = false; // active sending of command 55 | uint32_t interval = 60; // minimum interval between two commands in seconds (OPTIONAL) 56 | 57 | // Data Fields 58 | bool master = false; // value of interest is in master or slave part 59 | size_t position = 1; // starting position within the data bytes, beginning with 1 60 | ebus::DataType datatype = ebus::DataType::HEX1; // ebus data type 61 | float divider = 1; // divider for value conversion (OPTIONAL) 62 | float min = 1; // minimum value (OPTIONAL) 63 | float max = 100; // maximum value (OPTIONAL) 64 | uint8_t digits = 2; // decimal digits of value (OPTIONAL) 65 | std::string unit = ""; // unit (OPTIONAL) 66 | 67 | // Home Assistant 68 | bool ha = false; // support for auto discovery (OPTIONAL) 69 | std::string ha_component = ""; // component type (OPTIONAL) 70 | std::string ha_device_class = ""; // device class (OPTIONAL) 71 | std::string ha_entity_category = ""; // entity category (OPTIONAL) 72 | std::string ha_mode = "auto"; // mode (OPTIONAL) 73 | std::map ha_key_value_map = {}; // options as pairs of "key":"value" (OPTIONAL) 74 | int ha_default_key = 0; // options default key (OPTIONAL) 75 | uint8_t ha_payload_on = 1; // payload for ON state (OPTIONAL) 76 | uint8_t ha_payload_off = 0; // payload for OFF state (OPTIONAL) 77 | std::string ha_state_class = ""; // state class (OPTIONAL) 78 | float ha_step = 1; // step value (OPTIONAL) 79 | }; 80 | // clang-format on 81 | 82 | const double getDoubleFromVector(const Command* command); 83 | const std::vector getVectorFromDouble(const Command* command, 84 | double value); 85 | 86 | const std::string getStringFromVector(const Command* command); 87 | const std::vector getVectorFromString(const Command* command, 88 | const std::string& value); 89 | 90 | class Store { 91 | public: 92 | static const std::string evaluateCommand(const JsonDocument& doc); 93 | 94 | Command createCommand(const JsonDocument& doc); 95 | 96 | void insertCommand(const Command& command); 97 | void removeCommand(const std::string& key); 98 | Command* findCommand(const std::string& key); 99 | 100 | int64_t loadCommands(); 101 | int64_t saveCommands() const; 102 | static int64_t wipeCommands(); 103 | 104 | static JsonDocument getCommandJson(const Command* command); 105 | const JsonDocument getCommandsJsonDocument() const; 106 | const std::string getCommandsJson() const; 107 | 108 | const std::vector getCommands(); 109 | 110 | const size_t getActiveCommands() const; 111 | const size_t getPassiveCommands() const; 112 | 113 | const bool active() const; 114 | 115 | Command* nextActiveCommand(); 116 | std::vector findPassiveCommands(const std::vector& master); 117 | 118 | std::vector updateData(Command* command, 119 | const std::vector& master, 120 | const std::vector& slave); 121 | 122 | static JsonDocument getValueJson(const Command* command); 123 | static const std::string getValueFullJson(const Command* command); 124 | const std::string getValuesJson() const; 125 | 126 | private: 127 | // Use unordered_map for fast key lookup 128 | std::unordered_map allCommandsByKey; 129 | // For passive commands, use command.read_cmd as key for fast lookup 130 | std::unordered_map, std::vector, VectorHash> 131 | passiveCommands; 132 | // For active commands, just keep a vector of pointers 133 | std::vector activeCommands; 134 | 135 | static const std::string isFieldValid(const JsonDocument& doc, 136 | const std::string& field, bool required, 137 | FieldType type); 138 | 139 | static const std::string isKeyValueMapValid( 140 | const JsonObjectConst ha_key_value_map); 141 | 142 | // Flexible serialization/deserialization 143 | const std::string serializeCommands() const; 144 | void deserializeCommands(const char* payload); 145 | }; 146 | 147 | extern Store store; 148 | #endif 149 | -------------------------------------------------------------------------------- /static/log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | esp-eBus log 7 | 15 | 16 | 17 |

Log

18 |
19 | 20 | 21 | 22 | 23 | Connecting… 24 |
25 |
Loading logs...
26 | 137 | 138 | -------------------------------------------------------------------------------- /src/arbitration.cpp: -------------------------------------------------------------------------------- 1 | #include "arbitration.hpp" 2 | 3 | #include "bus.hpp" 4 | 5 | // arbitration is timing sensitive. avoid communicating with WifiClient during 6 | // arbitration according 7 | // https://ebus-wiki.org/lib/exe/fetch.php/ebus/spec_test_1_v1_1_1.pdf 8 | // section 3.2 9 | // "Calculated time distance between start bit of SYN byte and 10 | // bus permission must be in the range of 4300 us - 4456,24 us ." 11 | // SYN symbol is 4167 us. If we would receive the symbol immediately, 12 | // we need to wait (4300 - 4167)=133 us after we received the SYN. 13 | Arbitration::result Arbitration::start(const BusState& busstate, uint8_t master, 14 | uint32_t startBitTime) { 15 | static int arb = 0; 16 | if (_arbitrating) { 17 | return not_started; 18 | } 19 | if (master == SYN) { 20 | return not_started; 21 | } 22 | if (busstate._state != BusState::eReceivedFirstSYN) { 23 | return not_started; 24 | } 25 | 26 | // too late if we don't have enough time to send our symbol 27 | uint32_t now = micros(); 28 | uint32_t microsSinceLastSyn = busstate.microsSinceLastSyn(); 29 | uint32_t timeSinceStartBit = now - startBitTime; 30 | if (timeSinceStartBit > 4456 || Bus.available()) { 31 | // if we are too late, don't try to participate and retry next round 32 | DEBUG_LOG("ARB LATE 0x%02x %lu us\n", BusSer.peek(), timeSinceStartBit); 33 | return late; 34 | } 35 | #if USE_ASYNCHRONOUS 36 | // When in async mode, we get immediately interrupted when a symbol is 37 | // received on the bus The earliest allowed to send is 4300 measured from the 38 | // start bit of the SYN command. We receive the exact flange of the startbit, 39 | // use that to calculate the exact time to wait. Then subtract time from the 40 | // wait to allow the uart to put the byte on the bus. Testing has shown this 41 | // requires about 700 micros on the esp32-c3. 42 | int delay = 4300 - timeSinceStartBit - 700; 43 | if (delay > 0) { 44 | delayMicroseconds(delay); 45 | } 46 | #endif 47 | Bus.write(master); 48 | // Do logging of the ARB START message after writing the symbol, so enabled or 49 | // disabled logging does not affect timing calculations. 50 | #if USE_ASYNCHRONOUS 51 | DEBUG_LOG("ARB START %04i 0x%02x %lu us %i us\n", arb++, master, 52 | microsSinceLastSyn, delay); 53 | #else 54 | DEBUG_LOG("ARB START %04i 0x%02x %lu us\n", arb++, master, 55 | microsSinceLastSyn); 56 | #endif 57 | _arbitrationAddress = master; 58 | _arbitrating = true; 59 | _participateSecond = false; 60 | return started; 61 | } 62 | 63 | Arbitration::state Arbitration::data(BusState& busstate, uint8_t symbol, 64 | uint32_t startBitTime) { 65 | if (!_arbitrating) { 66 | return none; 67 | } 68 | switch (busstate._state) { 69 | case BusState::eStartup: // error out 70 | case BusState::eStartupFirstSyn: 71 | case BusState::eStartupSymbolAfterFirstSyn: 72 | case BusState::eStartupSecondSyn: 73 | case BusState::eReceivedFirstSYN: 74 | DEBUG_LOG("ARB ERROR 0x%02x 0x%02x 0x%02x %lu us %lu us\n", 75 | busstate._master, busstate._symbol, symbol, 76 | busstate.microsSinceLastSyn(), 77 | busstate.microsSincePreviousSyn()); 78 | _arbitrating = false; 79 | // Sometimes a second SYN is received instead of an address, either 80 | // after having started the arbitration, or after participating in 81 | // the second round of arbitration. This means the address we put on 82 | // the bus got lost. Most likely this is caused by not perfect timing 83 | // of the arbitration on our side, but could also be electrical 84 | // interference or wrong implementation in another bus participant. Try to 85 | // restart arbitration maximum 2 times 86 | if (_restartCount++ < 3 && 87 | busstate._previousState == BusState::eReceivedFirstSYN) 88 | return restart1; 89 | if (_restartCount++ < 3 && 90 | busstate._previousState == BusState::eReceivedSecondSYN) 91 | return restart2; 92 | _restartCount = 0; 93 | return error; 94 | case BusState::eReceivedAddressAfterFirstSYN: // did we win 1st round of 95 | // abitration? 96 | if (symbol == _arbitrationAddress) { 97 | DEBUG_LOG("ARB WON1 0x%02x %lu us\n", symbol, 98 | busstate.microsSinceLastSyn()); 99 | _arbitrating = false; 100 | _restartCount = 0; 101 | return won1; // we won; nobody else will write to the bus 102 | } else if ((symbol & 0b00001111) == (_arbitrationAddress & 0b00001111)) { 103 | DEBUG_LOG("ARB PART SECND 0x%02x 0x%02x\n", _arbitrationAddress, 104 | symbol); 105 | _participateSecond = 106 | true; // participate in second round of arbitration if we have the 107 | // same priority class 108 | } else { 109 | DEBUG_LOG("ARB LOST1 0x%02x %lu us\n", symbol, 110 | busstate.microsSinceLastSyn()); 111 | // arbitration might be ongoing between other bus participants, so we 112 | // cannot yet know what the winning master is. Need to wait for eBusy 113 | } 114 | return arbitrating; 115 | case BusState::eReceivedSecondSYN: // did we sign up for second round 116 | // arbitration? 117 | if (_participateSecond && Bus.available() == 0) { 118 | // execute second round of arbitration 119 | uint32_t microsSinceLastSyn = busstate.microsSinceLastSyn(); 120 | #if USE_ASYNCHRONOUS 121 | // When in async mode, we get immediately interrupted when a symbol is 122 | // received on the bus The earliest allowed to send is 4300 measured 123 | // from the start bit of the SYN command. We receive the exact flange of 124 | // the startbit, use that to calculate the exact time to wait. Then 125 | // subtract time from the wait to allow the uart to put the byte on the 126 | // bus. Testing has shown this requires about 700 micros on the 127 | // esp32-c3. 128 | uint32_t timeSinceStartBit = micros() - startBitTime; 129 | int delay = 4300 - timeSinceStartBit - 700; 130 | if (delay > 0) { 131 | delayMicroseconds(delay); 132 | } 133 | #endif 134 | // Do logging of the ARB START message after writing the symbol, so 135 | // enabled or disabled logging does not affect timing calculations. 136 | Bus.write(_arbitrationAddress); 137 | DEBUG_LOG("ARB MASTER2 0x%02x %lu us\n", _arbitrationAddress, 138 | microsSinceLastSyn); 139 | } else { 140 | DEBUG_LOG("ARB SKIP 0x%02x %lu us\n", _arbitrationAddress, 141 | busstate.microsSinceLastSyn()); 142 | } 143 | return arbitrating; 144 | case BusState::eReceivedAddressAfterSecondSYN: // did we win 2nd round of 145 | // arbitration? 146 | if (symbol == _arbitrationAddress) { 147 | DEBUG_LOG("ARB WON2 0x%02x %lu us\n", symbol, 148 | busstate.microsSinceLastSyn()); 149 | _arbitrating = false; 150 | _restartCount = 0; 151 | return won2; // we won; nobody else will write to the bus 152 | } else { 153 | DEBUG_LOG("ARB LOST2 0x%02x %lu us\n", symbol, 154 | busstate.microsSinceLastSyn()); 155 | // we now know which address has won and we could exit here. 156 | // but it is easier to wait for eBusy, so after the while loop, the 157 | // "lost" state can be handled the same as when somebody lost in the 158 | // first round 159 | } 160 | return arbitrating; 161 | case BusState::eBusy: 162 | _arbitrating = false; 163 | _restartCount = 0; 164 | return _participateSecond ? lost2 : lost1; 165 | } 166 | return arbitrating; 167 | } 168 | -------------------------------------------------------------------------------- /src/http.cpp: -------------------------------------------------------------------------------- 1 | #include "http.hpp" 2 | 3 | #include "log.hpp" 4 | #include "main.hpp" 5 | #include "mqttha.hpp" 6 | #include "schedule.hpp" 7 | #include "store.hpp" 8 | 9 | WebServer configServer(80); 10 | 11 | void handleStatus() { configServer.send(200, "text/plain", status_string()); } 12 | 13 | void handleGetStatus() { 14 | configServer.send(200, "application/json;charset=utf-8", 15 | getStatusJson().c_str()); 16 | } 17 | 18 | #if defined(EBUS_INTERNAL) 19 | void handleCommandsList() { 20 | configServer.send(200, "application/json;charset=utf-8", 21 | store.getCommandsJson().c_str()); 22 | } 23 | 24 | void handleCommandsUpload() { 25 | configServer.send(200, "text/html", F(R"( 26 | esp-eBus upload 27 | 28 | 29 |

Upload Commands

30 | 31 | 53 | 54 | )")); 55 | } 56 | 57 | void handleCommandsDownload() { 58 | String s = "{\"id\":\"insert\",\"commands\":"; 59 | s += store.getCommandsJson().c_str(); 60 | s += "}"; 61 | configServer.sendHeader("Content-Disposition", 62 | "attachment; filename=esp-ebus-commands.json"); 63 | configServer.send(200, "application/json", s); 64 | } 65 | 66 | void handleCommandsEvaluate() { 67 | JsonDocument doc; 68 | String body = configServer.arg("plain"); 69 | 70 | DeserializationError error = deserializeJson(doc, body); 71 | 72 | if (error) { 73 | configServer.send(403, "text/html", "Json invalid"); 74 | } else { 75 | JsonArrayConst commands = doc["commands"].as(); 76 | if (!commands.isNull()) { 77 | for (JsonVariantConst command : commands) { 78 | std::string evalError = store.evaluateCommand(command); 79 | if (!evalError.empty()) { 80 | configServer.send(403, "text/html", evalError.c_str()); 81 | return; 82 | } 83 | } 84 | configServer.send(200, "text/html", "Ok"); 85 | } else { 86 | configServer.send(403, "text/html", "No commands"); 87 | } 88 | } 89 | } 90 | 91 | void handleCommandsInsert() { 92 | JsonDocument doc; 93 | String body = configServer.arg("plain"); 94 | 95 | DeserializationError error = deserializeJson(doc, body); 96 | 97 | if (error) { 98 | configServer.send(403, "text/html", "Json invalid"); 99 | } else { 100 | JsonArrayConst commands = doc["commands"].as(); 101 | if (!commands.isNull()) { 102 | for (JsonVariantConst command : commands) { 103 | std::string evalError = store.evaluateCommand(command); 104 | if (evalError.empty()) 105 | store.insertCommand(store.createCommand(command)); 106 | else 107 | configServer.send(403, "text/html", evalError.c_str()); 108 | } 109 | if (mqttha.isEnabled()) mqttha.publishComponents(); 110 | configServer.send(200, "text/html", "Ok"); 111 | } else { 112 | configServer.send(403, "text/html", "No commands"); 113 | } 114 | } 115 | } 116 | 117 | void handleCommandsLoad() { 118 | int64_t bytes = store.loadCommands(); 119 | if (bytes > 0) 120 | configServer.send(200, "text/html", String(bytes) + " bytes loaded"); 121 | else if (bytes < 0) 122 | configServer.send(200, "text/html", "Loading failed"); 123 | else 124 | configServer.send(200, "text/html", "No data loaded"); 125 | 126 | if (mqttha.isEnabled()) mqttha.publishComponents(); 127 | } 128 | 129 | void handleCommandsSave() { 130 | int64_t bytes = store.saveCommands(); 131 | if (bytes > 0) 132 | configServer.send(200, "text/html", String(bytes) + " bytes saved"); 133 | else if (bytes < 0) 134 | configServer.send(200, "text/html", "Saving failed"); 135 | else 136 | configServer.send(200, "text/html", "No data saved"); 137 | } 138 | 139 | void handleCommandsWipe() { 140 | int64_t bytes = store.wipeCommands(); 141 | if (bytes > 0) 142 | configServer.send(200, "text/html", String(bytes) + " bytes wiped"); 143 | else if (bytes < 0) 144 | configServer.send(200, "text/html", "Wiping failed"); 145 | else 146 | configServer.send(200, "text/html", "No data wiped"); 147 | } 148 | 149 | void handleValues() { 150 | configServer.send(200, "application/json;charset=utf-8", 151 | store.getValuesJson().c_str()); 152 | } 153 | 154 | void handleScan() { 155 | schedule.handleScan(); 156 | configServer.send(200, "text/html", "Scan initiated"); 157 | } 158 | 159 | void handleScanFull() { 160 | schedule.handleScanFull(); 161 | configServer.send(200, "text/html", "Full scan initiated"); 162 | } 163 | 164 | void handleScanVendor() { 165 | schedule.handleScanVendor(); 166 | configServer.send(200, "text/html", "Vendor scan initiated"); 167 | } 168 | 169 | void handleParticipants() { 170 | configServer.send(200, "application/json;charset=utf-8", 171 | schedule.getParticipantsJson().c_str()); 172 | } 173 | 174 | void handleGetCounter() { 175 | configServer.send(200, "application/json;charset=utf-8", 176 | schedule.getCounterJson().c_str()); 177 | } 178 | 179 | void handleGetTiming() { 180 | configServer.send(200, "application/json;charset=utf-8", 181 | schedule.getTimingJson().c_str()); 182 | } 183 | 184 | void handleResetStatistic() { 185 | schedule.resetCounter(); 186 | schedule.resetTiming(); 187 | configServer.send(200, "text/html", "Statistic reset"); 188 | } 189 | 190 | void handleLogData() { configServer.send(200, "text/plain", getLog()); } 191 | #endif 192 | 193 | void handleRoot() { 194 | // -- Let IotWebConf test and handle captive portal requests. 195 | if (iotWebConf.handleCaptivePortal()) { 196 | // -- Captive portal request were already served. 197 | return; 198 | } 199 | 200 | extern const char root_html_start[] asm("_binary_static_root_html_start"); 201 | configServer.send(200, "text/html", root_html_start); 202 | } 203 | 204 | #if defined(EBUS_INTERNAL) 205 | void handleLog() { 206 | extern const char log_html_start[] asm("_binary_static_log_html_start"); 207 | configServer.send(200, "text/html", log_html_start); 208 | } 209 | #endif 210 | 211 | void SetupHttpHandlers() { 212 | // -- Set up required URL handlers on the web server. 213 | configServer.on("/", [] { handleRoot(); }); 214 | configServer.on("/status", [] { handleStatus(); }); 215 | configServer.on("/api/v1/GetStatus", [] { handleGetStatus(); }); 216 | #if defined(EBUS_INTERNAL) 217 | configServer.on("/commands/list", [] { handleCommandsList(); }); 218 | configServer.on("/commands/download", [] { handleCommandsDownload(); }); 219 | configServer.on("/commands/upload", [] { handleCommandsUpload(); }); 220 | configServer.on("/commands/evaluate", [] { handleCommandsEvaluate(); }); 221 | configServer.on("/commands/insert", [] { handleCommandsInsert(); }); 222 | configServer.on("/commands/load", [] { handleCommandsLoad(); }); 223 | configServer.on("/commands/save", [] { handleCommandsSave(); }); 224 | configServer.on("/commands/wipe", [] { handleCommandsWipe(); }); 225 | configServer.on("/values", [] { handleValues(); }); 226 | configServer.on("/scan", [] { handleScan(); }); 227 | configServer.on("/scanfull", [] { handleScanFull(); }); 228 | configServer.on("/scanvendor", [] { handleScanVendor(); }); 229 | configServer.on("/participants", [] { handleParticipants(); }); 230 | configServer.on("/api/v1/GetCounter", [] { handleGetCounter(); }); 231 | configServer.on("/api/v1/GetTiming", [] { handleGetTiming(); }); 232 | configServer.on("/reset", [] { handleResetStatistic(); }); 233 | configServer.on("/log", [] { handleLog(); }); 234 | configServer.on("/logdata", [] { handleLogData(); }); 235 | #endif 236 | configServer.on("/restart", [] { restart(); }); 237 | configServer.on("/config", [] { iotWebConf.handleConfig(); }); 238 | configServer.onNotFound([]() { iotWebConf.handleNotFound(); }); 239 | } 240 | -------------------------------------------------------------------------------- /src/bus.cpp: -------------------------------------------------------------------------------- 1 | #include "bus.hpp" 2 | 3 | #include 4 | 5 | // For ESP's based on FreeRTOS we can optimize the arbitration timing. 6 | // With SoftwareSerial we get notified with an callback that the 7 | // signal has changed. SoftwareSerial itself can and does know the 8 | // exact timing of the start bit. Use this for the timing of the 9 | // arbitration. SoftwareSerial seems to have trouble with writing 10 | // and reading at the same time. Hence use SoftwareSerial only for 11 | // reading. For writing use HardwareSerial. 12 | #if USE_SOFTWARE_SERIAL 13 | #include 14 | SoftwareSerial mySerial; 15 | #endif 16 | 17 | BusType Bus; 18 | 19 | // On ESP8266, maximum 512 icw SoftwareSerial, otherwise you run out of heap 20 | #define RXBUFFERSIZE 512 21 | #define QUEUE_SIZE 480 22 | 23 | #define BAUD_RATE 2400 24 | #define MAX_FRAMEBITS (1 + 8 + 1) 25 | #define SERIAL_EVENT_TASK_STACK_SIZE 2048 26 | #define SERIAL_EVENT_TASK_PRIORITY (configMAX_PRIORITIES - 1) 27 | #define SERIAL_EVENT_TASK_RUNNING_CORE -1 28 | 29 | // Locking 30 | #if USE_ASYNCHRONOUS 31 | SemaphoreHandle_t getMutex() { 32 | static SemaphoreHandle_t _lock = NULL; 33 | if (_lock == NULL) { 34 | _lock = xSemaphoreCreateMutex(); 35 | if (_lock == NULL) { 36 | DEBUG_LOG("xSemaphoreCreateMutex failed"); 37 | return NULL; 38 | } 39 | } 40 | return _lock; 41 | } 42 | #define ENH_MUTEX_LOCK() \ 43 | do { \ 44 | } while (xSemaphoreTake(getMutex(), portMAX_DELAY) != pdPASS) 45 | #define ENH_MUTEX_UNLOCK() xSemaphoreGive(getMutex()) 46 | #else 47 | #define ENH_MUTEX_LOCK() 48 | #define ENH_MUTEX_UNLOCK() 49 | #endif 50 | 51 | WiFiClient* _arbitration_client = NULL; 52 | int _arbitration_address = -1; 53 | 54 | void getArbitrationClient(WiFiClient*& client, uint8_t& address) { 55 | ENH_MUTEX_LOCK(); 56 | client = _arbitration_client; 57 | address = _arbitration_address; 58 | ENH_MUTEX_UNLOCK(); 59 | } 60 | 61 | void clearArbitrationClient() { 62 | ENH_MUTEX_LOCK(); 63 | _arbitration_client = 0; 64 | _arbitration_address = -1; 65 | ENH_MUTEX_UNLOCK(); 66 | } 67 | 68 | bool setArbitrationClient(WiFiClient*& client, uint8_t& address) { 69 | bool result = true; 70 | ENH_MUTEX_LOCK(); 71 | if (_arbitration_client == NULL) { 72 | _arbitration_client = client; 73 | _arbitration_address = address; 74 | } else { 75 | result = false; 76 | client = _arbitration_client; 77 | address = _arbitration_address; 78 | } 79 | ENH_MUTEX_UNLOCK(); 80 | return result; 81 | } 82 | 83 | void arbitrationDone() { clearArbitrationClient(); } 84 | 85 | WiFiClient* arbitrationRequested(uint8_t& address) { 86 | WiFiClient* client = NULL; 87 | getArbitrationClient(client, address); 88 | return client; 89 | } 90 | 91 | BusType::BusType() 92 | : _nbrRestarts1(0), 93 | _nbrRestarts2(0), 94 | _nbrArbitrations(0), 95 | _nbrLost1(0), 96 | _nbrLost2(0), 97 | _nbrWon1(0), 98 | _nbrWon2(0), 99 | _nbrErrors(0), 100 | _nbrLate(0), 101 | _client(0) {} 102 | 103 | BusType::~BusType() { end(); } 104 | 105 | #if USE_ASYNCHRONOUS 106 | void IRAM_ATTR BusType::receiveHandler() { 107 | BaseType_t xHigherPriorityTaskWoken = pdFALSE; 108 | vTaskNotifyGiveFromISR(Bus._serialEventTask, &xHigherPriorityTaskWoken); 109 | portEND_SWITCHING_ISR(xHigherPriorityTaskWoken); 110 | } 111 | 112 | void BusType::readDataFromSoftwareSerial(void* args) { 113 | for (;;) { 114 | BaseType_t r = ulTaskNotifyTake(pdFALSE, portMAX_DELAY); 115 | { 116 | // For SoftwareSerial; 117 | // The method "available" always evaluates all the interrupts received 118 | // The method "read" only evaluates the interrupts received if there is no 119 | // byte available 120 | int avail = mySerial.available(); 121 | if (!avail && r == 1) { 122 | // avoid this busy wait: delayMicroseconds(1+ MAX_FRAMEBITS * 1000000 / 123 | // BAUD_RATE); 124 | 125 | // Need to wait for 1000000 / BAUD_RATE, rounded to the next upper 126 | // digit. delayMicroseconds is a busy wait, which blocks the CPU to do 127 | // other things and could be the reason that the Wifi connection is 128 | // blocked. Instead of a busy wait, do the majority of the waiting with 129 | // vTaskDelay. Because vTaskDelay is switching at Tick cycle, doing 130 | // vTaskDelay(1) can wait anywhere between 0 Tick and 1 Ticks. On esp32 131 | // Arduino 1 Tick is 1 MilliSecond, although it depends on 132 | // configuration. 133 | 134 | // Validate 1 Tick is 1 MilliSecond with a compile time assert 135 | static_assert(pdMS_TO_TICKS(1) == 1); 136 | // static_assert(sizeof(uint32_t) == sizeof(unsigned long)); 137 | 138 | // We need to poll mySerial for availability of a byte. Testing has 139 | // shown that from 1 millisecond onward we need to check for incoming 140 | // data every 500 micros. We have to wait using vTaskDelay to allow the 141 | // processor to do other things, however that only allows millisecond 142 | // resolution. To work around, split the polling in two sections: 1) 143 | // Wait for 500 micros using busy wait with delayMicroseconds 2) Wait 144 | // the rest of the timeslice, which will be about 500 micros, using 145 | // vTaskDelay 146 | uint32_t begin = micros(); 147 | vTaskDelay(pdMS_TO_TICKS(1)); 148 | avail = mySerial.available(); 149 | 150 | // How was the delay until now? 151 | uint32_t delayed = micros() - begin; 152 | 153 | // Loop till the maximum duration of 1 byte (4167 micros from begin) 154 | // and check every 500 micros, using combination of 155 | // delayMicroseconds(500) and vTaskDelay(pdMS_TO_TICKS(1)) . The 156 | // vTaskDelay will wait till the end of the current timeslice, which is 157 | // typically about 500 micros away, because the previous vTaskDelay 158 | // makes sure the code is already synced to this tick Assumption: time 159 | // needed for mySerial.available() is less than 500 micros. 160 | while (delayed < 4167 && !avail) { 161 | if (4167 - delayed > 1000) { // Need to wait more than 1000 micros? 162 | delayMicroseconds(500); 163 | avail = mySerial.available(); 164 | if (!avail) { 165 | vTaskDelay(pdMS_TO_TICKS(1)); 166 | } 167 | } else { // Otherwise spend the remaining wait with delayMicroseconds 168 | uint32_t delay = 4167 - delayed < 500 ? 4167 - delayed : 500; 169 | delayMicroseconds(delay); 170 | } 171 | avail = mySerial.available(); 172 | delayed = micros() - begin; 173 | } 174 | } 175 | if (avail) { 176 | int symbol = mySerial.read(); 177 | Bus.receive(symbol, mySerial.readStartBitTimeStamp()); 178 | } 179 | } 180 | } 181 | vTaskDelete(NULL); 182 | } 183 | #endif 184 | 185 | void BusType::begin() { 186 | #if USE_SOFTWARE_SERIAL 187 | BusSer.begin(2400, SERIAL_8N1, -1, UART_TX); // used for writing 188 | mySerial.enableStartBitTimeStampRecording(true); 189 | mySerial.enableTx(false); 190 | mySerial.enableIntTx(false); 191 | mySerial.begin(2400, SWSERIAL_8N1, UART_RX, -1, false, 192 | RXBUFFERSIZE); // used for reading 193 | #else 194 | BusSer.setRxBufferSize(RXBUFFERSIZE); 195 | BusSer.begin(2400, SERIAL_8N1, UART_RX, UART_TX); // used for writing 196 | BusSer.setRxFIFOFull(1); 197 | #endif 198 | 199 | #if USE_ASYNCHRONOUS 200 | _queue = xQueueCreate(QUEUE_SIZE, sizeof(data)); 201 | xTaskCreateUniversal(BusType::readDataFromSoftwareSerial, "_serialEventQueue", 202 | SERIAL_EVENT_TASK_STACK_SIZE, this, 203 | SERIAL_EVENT_TASK_PRIORITY, &_serialEventTask, 204 | SERIAL_EVENT_TASK_RUNNING_CORE); 205 | mySerial.onReceive(BusType::receiveHandler); 206 | #endif 207 | } 208 | 209 | void BusType::end() { 210 | BusSer.end(); 211 | #if USE_SOFTWARE_SERIAL 212 | mySerial.end(); 213 | #endif 214 | 215 | #if USE_ASYNCHRONOUS 216 | vQueueDelete(_queue); 217 | _queue = 0; 218 | 219 | vTaskDelete(_serialEventTask); 220 | _serialEventTask = 0; 221 | #endif 222 | } 223 | 224 | int BusType::availableForWrite() { return BusSer.availableForWrite(); } 225 | 226 | size_t BusType::write(uint8_t symbol) { return BusSer.write(symbol); } 227 | 228 | bool BusType::read(data& d) { 229 | #if USE_ASYNCHRONOUS 230 | return xQueueReceive(_queue, &d, 0) == pdTRUE; 231 | #else 232 | #if USE_SOFTWARE_SERIAL 233 | if (mySerial.available()) { 234 | uint8_t symbol = mySerial.read(); 235 | receive(symbol, mySerial.readStartBitTimeStamp()); 236 | } 237 | #else 238 | if (BusSer.available()) { 239 | uint8_t symbol = BusSer.read(); 240 | receive(symbol, micros()); 241 | } 242 | #endif 243 | if (_queue.size() > 0) { 244 | d = _queue.front(); 245 | _queue.pop(); 246 | return true; 247 | } 248 | return false; 249 | #endif 250 | } 251 | 252 | int BusType::available() { 253 | #if USE_SOFTWARE_SERIAL 254 | return mySerial.available(); 255 | #else 256 | return BusSer.available(); 257 | #endif 258 | } 259 | 260 | void BusType::push(const data& d) { 261 | #if USE_ASYNCHRONOUS 262 | xQueueSendToBack(_queue, &d, 0); 263 | #else 264 | _queue.push(d); 265 | #endif 266 | } 267 | 268 | void BusType::receive(uint8_t symbol, uint32_t startBitTime) { 269 | _busState.data(symbol); 270 | Arbitration::state state = _arbitration.data(_busState, symbol, startBitTime); 271 | switch (state) { 272 | case Arbitration::restart1: 273 | _nbrRestarts1++; 274 | goto NONE; 275 | case Arbitration::restart2: 276 | _nbrRestarts2++; 277 | goto NONE; 278 | case Arbitration::none: 279 | NONE: 280 | uint8_t address; 281 | _client = arbitrationRequested(address); 282 | if (_client) { 283 | switch (_arbitration.start(_busState, address, startBitTime)) { 284 | case Arbitration::started: 285 | _nbrArbitrations++; 286 | DEBUG_LOG("BUS START SUCC 0x%02x %lu us\n", symbol, 287 | _busState.microsSinceLastSyn()); 288 | break; 289 | case Arbitration::late: 290 | _nbrLate++; 291 | case Arbitration::not_started: 292 | DEBUG_LOG("BUS START WAIT 0x%02x %lu us\n", symbol, 293 | _busState.microsSinceLastSyn()); 294 | } 295 | } 296 | // send to everybody. ebusd needs the SYN to get in the right mood 297 | push({false, RECEIVED, symbol, 0, _client}); 298 | break; 299 | case Arbitration::arbitrating: 300 | DEBUG_LOG("BUS ARBITRATIN 0x%02x %lu us\n", symbol, 301 | _busState.microsSinceLastSyn()); 302 | // do not send to arbitration client 303 | push({false, RECEIVED, symbol, _client, _client}); 304 | break; 305 | case Arbitration::won1: 306 | _nbrWon1++; 307 | goto WON; 308 | case Arbitration::won2: 309 | _nbrWon2++; 310 | WON: 311 | arbitrationDone(); 312 | DEBUG_LOG("BUS SEND WON 0x%02x %lu us\n", _busState._master, 313 | _busState.microsSinceLastSyn()); 314 | // send only to the arbitrating client 315 | push({true, STARTED, _busState._master, _client, _client}); 316 | // do not send to arbitrating client 317 | push({false, RECEIVED, symbol, _client, _client}); 318 | _client = 0; 319 | break; 320 | case Arbitration::lost1: 321 | _nbrLost1++; 322 | goto LOST; 323 | case Arbitration::lost2: 324 | _nbrLost2++; 325 | LOST: 326 | arbitrationDone(); 327 | DEBUG_LOG("BUS SEND LOST 0x%02x 0x%02x %lu us\n", _busState._master, 328 | _busState._symbol, _busState.microsSinceLastSyn()); 329 | // send only to the arbitrating client 330 | push({true, FAILED, _busState._master, _client, _client}); 331 | // send to everybody 332 | push({false, RECEIVED, symbol, 0, _client}); 333 | _client = 0; 334 | break; 335 | case Arbitration::error: 336 | _nbrErrors++; 337 | arbitrationDone(); 338 | // send only to the arbitrating client 339 | push({true, ERROR_EBUS, ERR_FRAMING, _client, _client}); 340 | // send to everybody 341 | push({false, RECEIVED, symbol, 0, _client}); 342 | _client = 0; 343 | break; 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/mqtt.cpp: -------------------------------------------------------------------------------- 1 | #if defined(EBUS_INTERNAL) 2 | #include "mqtt.hpp" 3 | 4 | #include 5 | 6 | #include "main.hpp" 7 | #include "mqttha.hpp" 8 | 9 | Mqtt mqtt; 10 | 11 | Mqtt::Mqtt() { 12 | client.onConnect(onConnect); 13 | client.onDisconnect(onDisconnect); 14 | client.onSubscribe(onSubscribe); 15 | client.onUnsubscribe(onUnsubscribe); 16 | client.onMessage(onMessage); 17 | client.onPublish(onPublish); 18 | } 19 | 20 | void Mqtt::setUniqueId(const char* id) { 21 | uniqueId = id; 22 | rootTopic = "ebus/" + uniqueId + "/"; 23 | willTopic = mqtt.rootTopic + "state/available"; 24 | client.setWill(willTopic.c_str(), 0, true, "offline"); 25 | } 26 | 27 | const std::string& Mqtt::getUniqueId() const { return uniqueId; } 28 | 29 | const std::string& Mqtt::getRootTopic() const { return rootTopic; } 30 | 31 | const std::string& Mqtt::getWillTopic() const { return willTopic; } 32 | 33 | void Mqtt::setServer(const char* host, uint16_t port) { 34 | client.setServer(host, port); 35 | } 36 | 37 | void Mqtt::setCredentials(const char* username, const char* password) { 38 | client.setCredentials(username, password); 39 | } 40 | 41 | void Mqtt::setEnabled(const bool enable) { enabled = enable; } 42 | 43 | const bool Mqtt::isEnabled() const { return enabled; } 44 | 45 | void Mqtt::connect() { client.connect(); } 46 | 47 | const bool Mqtt::connected() const { return client.connected(); } 48 | 49 | void Mqtt::disconnect() { client.disconnect(); } 50 | 51 | uint16_t Mqtt::publish(const char* topic, uint8_t qos, bool retain, 52 | const char* payload, bool prefix) { 53 | if (!enabled) return 0; 54 | 55 | std::string mqttTopic = prefix ? rootTopic + topic : topic; 56 | return client.publish(mqttTopic.c_str(), qos, retain, payload); 57 | } 58 | 59 | void Mqtt::enqueueOutgoing(const OutgoingAction& action) { 60 | if (!mqtt.enabled) return; 61 | 62 | mqtt.outgoingQueue.push(action); 63 | } 64 | 65 | void Mqtt::publishData(const std::string& id, 66 | const std::vector& master, 67 | const std::vector& slave) { 68 | if (!mqtt.enabled) return; 69 | 70 | std::string payload; 71 | JsonDocument doc; 72 | doc["id"] = id; 73 | doc["master"] = ebus::to_string(master); 74 | doc["slave"] = ebus::to_string(slave); 75 | doc.shrinkToFit(); 76 | serializeJson(doc, payload); 77 | mqtt.publish("response", 0, false, payload.c_str()); 78 | } 79 | 80 | void Mqtt::publishValue(const Command* command, const JsonDocument& doc) { 81 | if (!mqtt.enabled) return; 82 | 83 | std::string payload; 84 | serializeJson(doc, payload); 85 | 86 | std::string name = command->name; 87 | std::transform(name.begin(), name.end(), name.begin(), 88 | [](unsigned char c) { return std::tolower(c); }); 89 | 90 | std::string topic = "values/" + name; 91 | 92 | mqtt.publish(topic.c_str(), 0, false, payload.c_str()); 93 | } 94 | 95 | void Mqtt::doLoop() { 96 | checkIncomingQueue(); 97 | checkOutgoingQueue(); 98 | } 99 | 100 | uint16_t Mqtt::subscribe(const char* topic, uint8_t qos) { 101 | return client.subscribe(topic, qos); 102 | } 103 | 104 | void Mqtt::onConnect(bool sessionPresent) { 105 | std::string topicRequest = mqtt.rootTopic + "request"; 106 | mqtt.subscribe(topicRequest.c_str(), 0); 107 | 108 | mqtt.publish(mqtt.willTopic.c_str(), 0, true, "online", false); 109 | 110 | if (mqttha.isEnabled()) mqttha.publishDeviceInfo(); 111 | } 112 | 113 | void Mqtt::onMessage(const char* topic, const char* payload, 114 | AsyncMqttClientMessageProperties properties, size_t len, 115 | size_t index, size_t total) { 116 | JsonDocument doc; 117 | DeserializationError error = deserializeJson(doc, payload); 118 | 119 | if (error) { 120 | std::string errorPayload; 121 | JsonDocument errorDoc; 122 | errorDoc["error"] = error.c_str(); 123 | errorDoc.shrinkToFit(); 124 | serializeJson(errorDoc, errorPayload); 125 | mqtt.publish("response", 0, false, errorPayload.c_str()); 126 | return; 127 | } 128 | 129 | std::string id = doc["id"].as(); 130 | auto it = mqtt.commandHandlers.find(id); 131 | if (it != mqtt.commandHandlers.end()) { 132 | it->second(doc); // Call the handler 133 | } else { 134 | // Unknown command error handling 135 | std::string errorPayload; 136 | JsonDocument errorDoc; 137 | errorDoc["error"] = "command '" + id + "' not found"; 138 | errorDoc.shrinkToFit(); 139 | serializeJson(errorDoc, errorPayload); 140 | mqtt.publish("response", 0, false, errorPayload.c_str()); 141 | } 142 | } 143 | 144 | void Mqtt::handleRestart(const JsonDocument& doc) { restart(); } 145 | 146 | void Mqtt::handleInsert(const JsonDocument& doc) { 147 | JsonArrayConst commands = doc["commands"].as(); 148 | if (!commands.isNull()) { 149 | for (JsonVariantConst command : commands) { 150 | std::string evalError = store.evaluateCommand(command); 151 | if (evalError.empty()) { 152 | incomingQueue.push(IncomingAction(store.createCommand(command))); 153 | } else { 154 | std::string errorPayload; 155 | JsonDocument errorDoc; 156 | errorDoc["error"] = evalError; 157 | errorDoc.shrinkToFit(); 158 | serializeJson(errorDoc, errorPayload); 159 | mqtt.publish("response", 0, false, errorPayload.c_str()); 160 | } 161 | } 162 | } 163 | } 164 | 165 | void Mqtt::handleRemove(const JsonDocument& doc) { 166 | JsonArrayConst keys = doc["keys"].as(); 167 | if (!keys.isNull()) { 168 | for (JsonVariantConst key : keys) 169 | incomingQueue.push(IncomingAction(key.as())); 170 | } else { 171 | for (const Command* command : store.getCommands()) 172 | incomingQueue.push(IncomingAction(command->key)); 173 | } 174 | } 175 | 176 | void Mqtt::handlePublish(const JsonDocument& doc) { 177 | for (const Command* command : store.getCommands()) 178 | enqueueOutgoing(OutgoingAction(command)); 179 | } 180 | 181 | void Mqtt::handleLoad(const JsonDocument& doc) { 182 | int64_t bytes = store.loadCommands(); 183 | if (bytes > 0) 184 | mqtt.publishResponse("load", "successful", bytes); 185 | else if (bytes < 0) 186 | mqtt.publishResponse("load", "failed"); 187 | else 188 | mqtt.publishResponse("load", "no data"); 189 | 190 | if (mqttha.isEnabled()) mqttha.publishComponents(); 191 | } 192 | 193 | void Mqtt::handleSave(const JsonDocument& doc) { 194 | int64_t bytes = store.saveCommands(); 195 | if (bytes > 0) 196 | mqtt.publishResponse("save", "successful", bytes); 197 | else if (bytes < 0) 198 | mqtt.publishResponse("save", "failed"); 199 | else 200 | mqtt.publishResponse("save", "no data"); 201 | } 202 | 203 | void Mqtt::handleWipe(const JsonDocument& doc) { 204 | int64_t bytes = store.wipeCommands(); 205 | if (bytes > 0) 206 | mqtt.publishResponse("wipe", "successful", bytes); 207 | else if (bytes < 0) 208 | mqtt.publishResponse("wipe", "failed"); 209 | else 210 | mqtt.publishResponse("wipe", "no data"); 211 | } 212 | 213 | void Mqtt::handleScan(const JsonDocument& doc) { 214 | boolean full = doc["full"].as(); 215 | boolean vendor = doc["vendor"].as(); 216 | JsonArrayConst addresses = doc["addresses"].as(); 217 | 218 | if (full) 219 | schedule.handleScanFull(); 220 | else if (vendor) 221 | schedule.handleScanVendor(); 222 | else if (addresses.isNull() || addresses.size() == 0) 223 | schedule.handleScan(); 224 | else 225 | schedule.handleScanAddresses(addresses); 226 | 227 | mqtt.publishResponse("scan", "initiated"); 228 | } 229 | 230 | void Mqtt::handleParticipants(const JsonDocument& doc) { 231 | for (const Participant* participant : schedule.getParticipants()) 232 | enqueueOutgoing(OutgoingAction(participant)); 233 | } 234 | 235 | void Mqtt::handleSend(const JsonDocument& doc) { 236 | JsonArrayConst commands = doc["commands"].as(); 237 | if (commands.isNull() || commands.size() == 0) 238 | mqtt.publishResponse("send", "commands array invalid"); 239 | else 240 | schedule.handleSend(commands); 241 | } 242 | 243 | void Mqtt::handleForward(const JsonDocument& doc) { 244 | JsonArrayConst filters = doc["filters"].as(); 245 | if (!filters.isNull()) schedule.handleForwardFilter(filters); 246 | boolean enable = doc["enable"].as(); 247 | schedule.toggleForward(enable); 248 | } 249 | 250 | void Mqtt::handleReset(const JsonDocument& doc) { 251 | schedule.resetCounter(); 252 | schedule.resetTiming(); 253 | } 254 | 255 | void Mqtt::handleRead(const JsonDocument& doc) { 256 | std::string key = doc["key"].as(); 257 | const Command* command = store.findCommand(key); 258 | if (command != nullptr) { 259 | String s = "{\"id\":\"read\","; 260 | s += store.getValueFullJson(command).substr(1).c_str(); // skip opening { 261 | publish("response", 0, false, s.c_str()); 262 | } else { 263 | mqtt.publishResponse("read", "key '" + key + "' not found"); 264 | } 265 | } 266 | 267 | void Mqtt::handleWrite(const JsonDocument& doc) { 268 | std::string key = doc["key"].as(); 269 | Command* command = store.findCommand(key); 270 | if (command != nullptr) { 271 | std::vector valueBytes; 272 | if (command->numeric) { 273 | double value = doc["value"].as(); 274 | valueBytes = getVectorFromDouble(command, value); 275 | } else { 276 | std::string value = doc["value"].as(); 277 | valueBytes = getVectorFromString(command, value); 278 | } 279 | if (valueBytes.size() > 0) { 280 | std::vector writeCmd = command->write_cmd; 281 | writeCmd.insert(writeCmd.end(), valueBytes.begin(), valueBytes.end()); 282 | schedule.handleWrite(writeCmd); 283 | command->last = 0; // force immediate update 284 | } else { 285 | mqtt.publishResponse("write", "invalid value for key '" + key + "'"); 286 | } 287 | } else { 288 | mqtt.publishResponse("write", "key '" + key + "' not found"); 289 | } 290 | } 291 | 292 | void Mqtt::checkIncomingQueue() { 293 | if (!incomingQueue.empty() && millis() > lastIncoming + incomingInterval) { 294 | lastIncoming = millis(); 295 | IncomingAction action = incomingQueue.front(); 296 | incomingQueue.pop(); 297 | 298 | switch (action.type) { 299 | case IncomingActionType::Insert: 300 | store.insertCommand(action.command); 301 | if (mqttha.isEnabled()) mqttha.publishComponent(&action.command, false); 302 | publishResponse("insert", "key '" + action.command.key + "' inserted"); 303 | break; 304 | case IncomingActionType::Remove: 305 | const Command* cmd = store.findCommand(action.key); 306 | if (cmd) { 307 | if (mqttha.isEnabled()) mqttha.publishComponent(cmd, true); 308 | store.removeCommand(action.key); 309 | publishResponse("remove", "key '" + action.key + "' removed"); 310 | } else { 311 | publishResponse("remove", "key '" + action.key + "' not found"); 312 | } 313 | break; 314 | } 315 | } 316 | } 317 | 318 | void Mqtt::checkOutgoingQueue() { 319 | if (!outgoingQueue.empty() && millis() > lastOutgoing + outgoingInterval) { 320 | lastOutgoing = millis(); 321 | OutgoingAction action = outgoingQueue.front(); 322 | outgoingQueue.pop(); 323 | 324 | switch (action.type) { 325 | case OutgoingActionType::Command: 326 | publishCommand(action.command); 327 | break; 328 | case OutgoingActionType::Participant: 329 | publishParticipant(action.participant); 330 | break; 331 | case OutgoingActionType::Component: 332 | mqttha.publishComponent(action.command, action.haRemove); 333 | break; 334 | } 335 | } 336 | } 337 | 338 | void Mqtt::publishResponse(const std::string& id, const std::string& status, 339 | const size_t& bytes) { 340 | std::string payload; 341 | JsonDocument doc; 342 | doc["id"] = id; 343 | doc["status"] = status; 344 | if (bytes > 0) doc["bytes"] = bytes; 345 | doc.shrinkToFit(); 346 | serializeJson(doc, payload); 347 | publish("response", 0, false, payload.c_str()); 348 | } 349 | 350 | void Mqtt::publishCommand(const Command* command) { 351 | std::string topic = "commands/" + command->key; 352 | std::string payload; 353 | serializeJson(store.getCommandJson(command), payload); 354 | publish(topic.c_str(), 0, false, payload.c_str()); 355 | } 356 | 357 | void Mqtt::publishParticipant(const Participant* participant) { 358 | std::string topic = "participants/" + ebus::to_string(participant->slave); 359 | std::string payload; 360 | serializeJson(schedule.getParticipantJson(participant), payload); 361 | publish(topic.c_str(), 0, false, payload.c_str()); 362 | } 363 | 364 | #endif 365 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esp8266-arduino-ebus 2 | 3 | **Warning: Do not power your adapter from a power supply on eBus terminals - you will burn the transmit circuit (receive may still work)!** 4 | 5 | ## Quickstart 6 | - connect adapter to ebus 7 | - you should see at least one LED on the adapter shining (HW v3.0+) - if not, switch eBus wires 8 | - LED D1 blinking indicates activity on the bus. The adapter comes pre-adjusted but if D1 is still on or off, you need to re-adjust it: 9 | - by a configuration in web interface for v6.0 and newer: 10 | - open http://esp-ebus.local/config to adjust PWM value 11 | - the default value is 130, max is 255, min is 1 12 | - when D1 is still on, you need to lower the value 13 | - when D1 is still off, you need to raise the value 14 | - using trimer RV1 for v5.x and older: 15 | - Note: the following directions are reversed (clockwise/counterclockwise) for adapters purchased via elecrow) 16 | - turn the trimmer counterclockwise until you find the position between D1 blinking and still on 17 | - turn the trimmer clockwise until you find the position between D1 blinking and still off 18 | - count the turns between these positions and set the trimmer in the middle position with D1 blinking 19 | - if you have adjusted the trimmer, disconnect and connect the adapter to bus again 20 | - search for WiFi networks, you should see network with name "esp-eBus" 21 | - connect to the network - a configuration page should open automatically, default password is: ebusebus 22 | - configure your WiFi network settings (SSID, password) 23 | - after reboot, you should be able to run `ping esp-ebus.local` successfully from a computer in your network (if your network is correctly configured for mDNS) 24 | - to verify there are bytes being received by the adapter, you can connect to `esp-ebus.local` port `3334` using telnet - you should see unreadable binary data 25 | - you can use [ebusd](https://github.com/john30/ebusd) to decode bus messages, see ports section for device option configuration 26 | - the adapter listens on following TCP ports (latest SW): 27 | - 3333 - raw - on this port you can both read and write to the eBus - ebusd config: `-d esp-ebus.local:3333` 28 | - 3334 - listen only port - everything sent to this port will be discarded and not sent to bus 29 | - 3335 - [enhanced protocol](https://github.com/john30/ebusd/blob/b5d6a49/docs/enhanced_proto.md) - ebusd config: `-d enh:esp-ebus.local:3335` 30 | - 5555 - status server - you can telnet to this port (or http://esp-ebus.local:5555) to see some basic status info 31 | 32 | ## Hardware revisions 33 | 34 | This section lists adapter hardware revisions together with specifics for each one. Each revision lists only change from the previous one. 35 | 36 | ### v1.0 37 | - MCU: ESP8266 38 | - step-down: MP2307 39 | - SMD trimmer 40 | 41 | ### v2.0 42 | - firmware file: firmware-HW_v3.x.bin 43 | - step-down: XL7015 44 | - RESET_PIN: MCU GPIO5 45 | - replaced SMD to multiturn timmer 46 | - added TX-disable - GPIO2 - function not working 47 | 48 | ### v3.0 49 | - firmware file: firmware-HW_v3.x.bin 50 | - step-down: ME3116 51 | - RESET_PIN: MCU GPIO5 52 | - fixed TX-disable - GPIO2 - blue LED on module shines when TX enabled 53 | - added programming header 54 | - added LEDS for TX, RX, Power 55 | - added tp2 - you can apply 24V external power supply between tp2 (+) and BUS GND (-). If you remove D4, you can use an adapter with voltage 5-24V 56 | 57 | ### v4.0 58 | - firmware file: firmware-HW_v4.x.bin 59 | - RESET_PIN: TX-DISABLE (GPIO5) 60 | - moved TX-DISABLE to GPIO5 61 | - LEDs position changed 62 | - added tp3, jp1 - you can apply 24V external power supply between tp2 (+) and tp3 (-). If you cut jp1, you can use any adapter with voltage 5-24V 63 | 64 | ### v4.1 65 | - firmware file: firmware-HW_v4.x.bin 66 | - RESET_PIN: TX-DISABLE (GPIO5) 67 | - added debug pin to programming header 68 | 69 | ### v5.0 70 | - firmware file: firmware-HW_v5.x.bin 71 | - MCU changed to ESP32-C3 72 | - RESET_PIN: TO-EBUS (GPIO20) 73 | - removed TX-DISABLE - MCU doesn't transmit any messages during startup on UART pins 74 | - added USB-C connector for power and/or programming 75 | - USB power works only with USB-A - USB-C cables (bug) 76 | - added VCC selector jumper - you can choose from: 77 | - power from EBUS: jumper in position VCC-VBUS 78 | - power from 5V USB-C connector: jumper in position VCC-VUSB 79 | - power from any adapter 5-24V: remove jumper and connect adapter to VCC and GND pins 80 | - SOD-123 diodes 81 | - added LED D8 for MCU status 82 | 83 | ### v5.1 84 | - firmware file: firmware-HW_v5.x.bin 85 | - RESET_PIN: TO-EBUS (GPIO20) 86 | - fixed reference voltage resistor value 87 | 88 | ### v5.2 89 | - firmware file: firmware-HW_v5.x.bin 90 | - RESET_PIN: TO-EBUS (GPIO20) 91 | - USB power works with USB-C - USB-C cables 92 | - replaced VCC selector jumper with 2.0mm pitch: 93 | - power from EBUS: jumper in position VCC-VBUS 94 | - power from 5V USB-C connector: jumper removed 95 | - power from any adapter 5-24V: remove jumper and connect adapter to VCC and GND pins 96 | 97 | ### v6.0 98 | - firmware file: firmware-HW_v5.x.bin 99 | - RESET_PIN: TO-EBUS (GPIO20) 100 | - trimmer is replaced by PWM setting in web interface 101 | 102 | ### v6.1 and newer 103 | - firmware file: firmware-HW_v5.x.bin 104 | - RESET_PIN: TO-EBUS (GPIO20) 105 | - added missing via in v6.0 106 | 107 | ## Troubleshooting 108 | #### The adapter seems dead, no LED shines or blinks. 109 | The ebus wires are reversed, try switching the wires. Don't worry, it has a protection against reversing. 110 | 111 | #### The ebus logs shows `[bus error] device status: unexpected enhanced byte 2` 112 | Please read quickstart section to find correct device configuration options 113 | 114 | #### I often see messages like: `ERR: arbitration lost`, `signal lost`, `signal acquired` 115 | It's possible that the adapter has a poor WiFi reception so it has to resend messages and the latency increases. Try to improve the signal by moving the adapter closer to the WiFi AP. You can also try to increase the ebusd latency parameter to e.g. `--latency=200000`. 116 | 117 | #### The adapter is loosing connectivity, breaking other ebus components communication, D7 is blinking, WiFi is disappearing after connecting, devices on the bus show error status or other intermittent failures. 118 | It's possible that ebus doesn't have enough power to supply the adapter together with all other connected devices. From version v3.0 there are options for supplying external power, see the hardware revisions section for details. 119 | 120 | #### Nothing helps. I need support. 121 | Run ebusd with `--lograwdata=data --latency=2000 --log=all:debug` options. Then save the log, open an issue here, describe the problem and attach the log. I'll try to help you. 122 | 123 | ## Config reset 124 | - check which RESET_PIN is used in your adapter (see hardware revisions) 125 | - note: RESET_PIN has been changing in different software versions. The defined value refers to latest software revision. If the value doesn't work, you may try also pin ESP-RX/FROM-EBUS 126 | - disconnect device from bus 127 | - connect RESET_PIN and GND pins using a wire 128 | - connect device to bus 129 | - wait 5 seconds 130 | - disconnect the wire 131 | 132 | ## Upgrading 133 | There are following options: 134 | - over the network (OTA) 135 | - [using web interface](#web-interface) 136 | - easiest 137 | - [using platform.io](#platformio) 138 | - heavier option - it will compile the firmware from source code and upload using internall tooling 139 | - [using espota.py](#espotapy) 140 | - lightweight - just needs OTA script and precompiled firmware file 141 | - physically using a USB-TTL adaptor or device USB port (HW v5.0+) 142 | 143 | ### web interface 144 | - open web interface of the device by IP or on: http://esp-ebus.local 145 | - find the update link 146 | - upload correct firmware file (see hardware revisions) - use version WITHOUT `fullflash` keyword 147 | - click `Update` button 148 | - wait for restart, reconnect to adapter and configure WiFi if not connected automatically 149 | - in case you cannot open web interface, [reset device](#config-reset) to access it 150 | 151 | ### platform.io 152 | - clone this repository using git 153 | - `pip3 install platformio` 154 | - check for correct firmware file (see hardware revisions) 155 | - inside the project folder run: 156 | - for firmware-HW_v3.x.bin: `pio run -e esp12e-v3.0-ota -t upload` 157 | - for firmware-HW_v4.x.bin: `pio run -e esp12e-ota -t upload` 158 | - for firmware-HW_v5.x.bin: `pio run -e esp32-c3-ota -t upload` 159 | 160 | ### espota.py 161 | - you need python installed in your computer 162 | - download [espota.py script](https://github.com/esp8266/Arduino/blob/master/tools/espota.py) 163 | - for Windows, you can download espota.exe from [esp32-xx.zip](https://github.com/espressif/arduino-esp32/releases) - it is located in `tools` folder 164 | - download firmware according to your hardware version from https://github.com/danielkucera/esp8266-arduino-ebus/releases 165 | - use port number: 166 | - 8266 - for esp8266 (HW up to v4.1) 167 | - 3232 - for esp32-c3 (HW from v5.0 up) 168 | - to upgrade, run: 169 | ``` 170 | $ python3 espota.py -i esp-ebus.local -f -p -r -d 171 | 16:33:23 [DEBUG]: Options: {'esp_ip': 'esp-ebus.local', 'host_ip': '0.0.0.0', 'esp_port': 8266, 'host_port': 47056, 'auth': '', 'image': 'firmware.bin', 'spiffs': False, 'debug': True, 'progress': True} 172 | 16:33:23 [INFO]: Starting on 0.0.0.0:47056 173 | 16:33:23 [INFO]: Upload size: 380320 174 | 16:33:23 [INFO]: Sending invitation to: esp-ebus.local 175 | 16:33:23 [INFO]: Waiting for device... 176 | Uploading: [============================================================] 100% Done... 177 | 178 | 16:33:30 [INFO]: Waiting for result... 179 | 16:33:31 [INFO]: Result: OK 180 | ``` 181 | 182 | ### upgrading over USB (HW v5.0+) 183 | - this version has built-in USB serial interface 184 | - download `firmware-fullflash-*` firmware from https://github.com/danielkucera/esp8266-arduino-ebus/releases 185 | - connect `PROG` and `GND` 186 | - connect adapter to a PC using USB-A - USB-C cable 187 | - you should see a new serial port 188 | - flash the firmware to address 0x0 using either one of tools: 189 | - Web based: https://adafruit.github.io/Adafruit_WebSerial_ESPTool/ 190 | - Windows: using Flash Download Tools from https://www.espressif.com/en/support/download/other-tools 191 | - Linux esptool.py: `esptool.py write_flash 0x0 firmware-fullflash-*` 192 | 193 | 194 | ### upgrading over USB-TTL adaptor (before HW v5.0) 195 | You will need an USB-TTL adaptor (dongle) which suports 3V3 voltage levels and has 3V3 voltage output pin 196 | - download firmware bin file from https://github.com/danielkucera/esp8266-arduino-ebus/releases 197 | - download NodeMCU PyFlasher from https://github.com/marcelstoer/nodemcu-pyflasher/releases 198 | - using a wire connect pins `PROG` and `TP3` 199 | - connect your adaptor in a following way (dongle - module): 200 | - 3V3 <-> 3V3 201 | - TX <-> ESP-RX 202 | - RX <-> ESP-TX 203 | - GND <-> GND 204 | - now connect the dongle to your PC - you should see two red LEDs on, blue should flash briefly and stay off (v4.0+) 205 | - open NodeMCU PyFlasher and select your firmware file and serial port 206 | - click Flash NodeMCU and watch the progress in Console 207 | - if that doesn't work, connect also TP1 to 3V3 and try again (see Issue #27) 208 | 209 | 210 | ## INTERNAL Firmware Features 211 | 212 | The **INTERNAL firmware** enables devices to function independently as eBUS devices, eliminating the need for external control software like ebusd. Powered by the ebus library, which can be found at the [C++ library for eBUS communication](https://github.com/yuhu-/ebus), this firmware offers several key features: 213 | 214 | - **Port Access**: Supports read and write operations through ports **3333**, **3334**, and **3335**, utilizing the ebusd enhanced protocol. 215 | - **Status Queries**: Conduct status checks via port **5555**. 216 | - **Internal Command Store**: Facilitates both active and passive command operations. 217 | - **Command Installation**: Commands can be installed through **MQTT** or **HTTP** uploads. 218 | - **Persistent Storage**: Commands are stored in **NVS memory** and automatically loaded after a device restart. 219 | - **Message Evaluation**: Processes received or sent messages, with results published to **MQTT**. 220 | - **Non-Installed Command Support**: Allows sending of non-installed commands via **MQTT**. 221 | - **eBUS Device Scanning**: Supports scanning for eBUS devices. 222 | - **Startup Scanning**: Automatically detects eBUS devices during startup. 223 | - **Message Forwarding**: Recognizes messages through patterns and forwards them via **MQTT**. 224 | - **Value Reading/Writing**: Supports reading from stored commands via **MQTT** and writing values using these commands. 225 | - **Home Assistant Auto Discovery**: Available for specific device types. 226 | 227 | For more detailed information, visit the [INTERNAL Firmware Documentation](https://github.com/danielkucera/esp-arduino-ebus/wiki/6.-Firmware-INTERNAL). 228 | -------------------------------------------------------------------------------- /src/mqttha.cpp: -------------------------------------------------------------------------------- 1 | #if defined(EBUS_INTERNAL) 2 | #include 3 | #include 4 | 5 | MqttHA mqttha; 6 | 7 | void MqttHA::setUniqueId(const std::string& id) { 8 | uniqueId = id; 9 | deviceIdentifiers = "ebus" + uniqueId; 10 | } 11 | 12 | void MqttHA::setRootTopic(const std::string& topic) { 13 | rootTopic = topic; 14 | commandTopic = rootTopic + "request"; 15 | } 16 | 17 | void MqttHA::setWillTopic(const std::string& topic) { willTopic = topic; } 18 | 19 | void MqttHA::setEnabled(const bool enable) { enabled = enable; } 20 | 21 | const bool MqttHA::isEnabled() const { return enabled; } 22 | 23 | void MqttHA::setThingName(const std::string& name) { thingName = name; } 24 | 25 | void MqttHA::setThingModel(const std::string& model) { thingModel = model; } 26 | 27 | void MqttHA::setThingModelId(const std::string& modelId) { 28 | thingModelId = modelId; 29 | } 30 | 31 | void MqttHA::setThingManufacturer(const std::string& manufacturer) { 32 | thingManufacturer = manufacturer; 33 | } 34 | 35 | void MqttHA::setThingSwVersion(const std::string& swVersion) { 36 | thingSwVersion = swVersion; 37 | } 38 | 39 | void MqttHA::setThingHwVersion(const std::string& hwVersion) { 40 | thingHwVersion = hwVersion; 41 | } 42 | 43 | void MqttHA::setThingConfigurationUrl(const std::string& configurationUrl) { 44 | thingConfigurationUrl = configurationUrl; 45 | } 46 | 47 | void MqttHA::publishDeviceInfo() const { 48 | mqttha.publishComponent(createButtonRestart(), !enabled); 49 | 50 | mqttha.publishComponent(createDiagnosticUptime(), !enabled); 51 | mqttha.publishComponent(createDiagnosticFreeHeap(), !enabled); 52 | mqttha.publishComponent(createDiagnosticLoopDuration(), !enabled); 53 | } 54 | 55 | void MqttHA::publishComponents() const { 56 | for (const Command* command : store.getCommands()) { 57 | if (command->ha) mqtt.enqueueOutgoing(OutgoingAction(command, !enabled)); 58 | } 59 | } 60 | 61 | void MqttHA::publishComponent(const Command* command, const bool remove) const { 62 | if (command->ha_component == "binary_sensor") { 63 | publishComponent(createBinarySensor(command), remove); 64 | } else if (command->ha_component == "sensor") { 65 | publishComponent(createSensor(command), remove); 66 | } else if (command->ha_component == "number") { 67 | publishComponent(createNumber(command), remove); 68 | } else if (command->ha_component == "select") { 69 | publishComponent(createSelect(command), remove); 70 | } else if (command->ha_component == "switch") { 71 | publishComponent(createSwitch(command), remove); 72 | } 73 | } 74 | 75 | void MqttHA::publishComponent(const Component& c, const bool remove) const { 76 | std::string topic = "homeassistant/" + c.component + '/' + 77 | c.deviceIdentifiers + '/' + c.objectId + "/config"; 78 | 79 | // Only publish config if HA support is enabled and not removing 80 | if (!remove && enabled) { 81 | std::string payload = getComponentJson(c); 82 | mqtt.publish(topic.c_str(), 0, true, payload.c_str(), false); 83 | } 84 | // Only publish empty payload if removing (to remove entity in HA) 85 | else if (remove) { 86 | mqtt.publish(topic.c_str(), 0, true, "", false); 87 | } 88 | // Otherwise, do nothing 89 | } 90 | 91 | const std::string MqttHA::getComponentJson(const Component& c) const { 92 | JsonDocument doc; 93 | doc["unique_id"] = c.uniqueId; 94 | doc["name"] = c.name; 95 | 96 | // fields 97 | for (const auto& kv : c.fields) doc[kv.first] = kv.second; 98 | 99 | // options 100 | if (!c.options.empty()) { 101 | JsonArray options = doc["options"].to(); 102 | for (const auto& opt : c.options) options.add(opt); 103 | } 104 | 105 | // device 106 | JsonObject device = doc["device"].to(); 107 | device["identifiers"] = c.deviceIdentifiers; 108 | for (const auto& kv : c.device) device[kv.first] = kv.second; 109 | 110 | std::string payload; 111 | doc.shrinkToFit(); 112 | serializeJson(doc, payload); 113 | 114 | return payload; 115 | } 116 | 117 | std::string MqttHA::createStateTopic(const std::string& prefix, 118 | const std::string& topic) const { 119 | std::string stateTopic = topic; 120 | std::transform(stateTopic.begin(), stateTopic.end(), stateTopic.begin(), 121 | [](unsigned char c) { return std::tolower(c); }); 122 | return rootTopic + prefix + (prefix.empty() ? "" : "/") + stateTopic; 123 | } 124 | 125 | MqttHA::Component MqttHA::createComponent(const std::string& component, 126 | const std::string& uniqueIdKey, 127 | const std::string& name) const { 128 | std::string objectId = name; 129 | std::transform(objectId.begin(), objectId.end(), objectId.begin(), 130 | [](unsigned char c) { return std::tolower(c); }); 131 | std::replace(objectId.begin(), objectId.end(), '/', '_'); 132 | std::replace(objectId.begin(), objectId.end(), ' ', '_'); 133 | 134 | std::string key = uniqueIdKey; 135 | std::transform(key.begin(), key.end(), key.begin(), 136 | [](unsigned char c) { return std::tolower(c); }); 137 | std::replace(key.begin(), key.end(), '/', '_'); 138 | std::replace(key.begin(), key.end(), ' ', '_'); 139 | 140 | std::string prettyName = name; 141 | std::replace(prettyName.begin(), prettyName.end(), '/', ' '); 142 | std::replace(prettyName.begin(), prettyName.end(), '_', ' '); 143 | 144 | Component c; 145 | c.component = component; 146 | c.objectId = objectId; 147 | c.uniqueId = deviceIdentifiers + '_' + key; 148 | c.name = prettyName; 149 | c.deviceIdentifiers = deviceIdentifiers; 150 | c.fields["availability_topic"] = willTopic; 151 | return c; 152 | } 153 | 154 | MqttHA::KeyValueMapping MqttHA::createOptions( 155 | const std::map& ha_key_value_map, 156 | const int& ha_default_key) { 157 | // Create a vector of options names and a vector of pairs 158 | std::vector> optionsVec; 159 | std::vector options; 160 | 161 | // Populate optionsVec and options from the map 162 | for (const auto& kv : ha_key_value_map) { 163 | optionsVec.emplace_back(kv.second, kv.first); 164 | options.push_back(kv.second); 165 | } 166 | 167 | // Determine default option name and value 168 | const auto defaultIt = 169 | std::find_if(optionsVec.begin(), optionsVec.end(), 170 | [&](const std::pair& opt) { 171 | return opt.second == ha_default_key; 172 | }); 173 | 174 | std::string defaultOptionName = optionsVec.empty() ? "" : optionsVec[0].first; 175 | int defaultOptionValue = optionsVec.empty() ? 0 : optionsVec[0].second; 176 | if (defaultIt != optionsVec.end()) { 177 | defaultOptionName = defaultIt->first; 178 | defaultOptionValue = defaultIt->second; 179 | } 180 | 181 | // Build value_template for displaying option name from value 182 | std::string valueMap = "{% set values = {"; 183 | for (size_t i = 0; i < optionsVec.size(); ++i) { 184 | valueMap += 185 | std::to_string(optionsVec[i].second) + ":'" + optionsVec[i].first + "'"; 186 | if (i < optionsVec.size() - 1) valueMap += ","; 187 | } 188 | valueMap += 189 | "} %}{{ values[value_json.value] if value_json.value in values.keys() " 190 | "else '" + 191 | defaultOptionName + "' }}"; 192 | 193 | // Build command_template for sending value from option name 194 | std::string cmdMap = "{% set values = {"; 195 | for (size_t i = 0; i < optionsVec.size(); ++i) { 196 | cmdMap += 197 | "'" + optionsVec[i].first + "':" + std::to_string(optionsVec[i].second); 198 | if (i < optionsVec.size() - 1) cmdMap += ","; 199 | } 200 | cmdMap += "} %}{{ values[value] if value in values.keys() else " + 201 | std::to_string(defaultOptionValue) + " }}"; 202 | 203 | return KeyValueMapping{options, valueMap, cmdMap}; 204 | } 205 | 206 | MqttHA::Component MqttHA::createBinarySensor(const Command* command) const { 207 | Component c = createComponent("binary_sensor", command->key, command->name); 208 | c.fields["state_topic"] = createStateTopic("values", command->name); 209 | if (!command->ha_device_class.empty()) 210 | c.fields["device_class"] = command->ha_device_class; 211 | if (!command->ha_entity_category.empty()) 212 | c.fields["entity_category"] = command->ha_entity_category; 213 | c.fields["payload_on"] = std::to_string(command->ha_payload_on); 214 | c.fields["payload_off"] = std::to_string(command->ha_payload_off); 215 | c.fields["value_template"] = "{{value_json.value}}"; 216 | return c; 217 | } 218 | 219 | MqttHA::Component MqttHA::createSensor(const Command* command) const { 220 | Component c = createComponent("sensor", command->key, command->name); 221 | c.fields["state_topic"] = createStateTopic("values", command->name); 222 | if (!command->ha_device_class.empty()) 223 | c.fields["device_class"] = command->ha_device_class; 224 | if (!command->ha_entity_category.empty()) 225 | c.fields["entity_category"] = command->ha_entity_category; 226 | if (!command->ha_state_class.empty()) 227 | c.fields["state_class"] = command->ha_state_class; 228 | if (!command->unit.empty()) c.fields["unit_of_measurement"] = command->unit; 229 | 230 | if (!command->ha_key_value_map.empty()) { 231 | KeyValueMapping optionsResult = 232 | createOptions(command->ha_key_value_map, command->ha_default_key); 233 | 234 | c.fields["value_template"] = optionsResult.valueMap; 235 | } else { 236 | c.fields["value_template"] = "{{value_json.value}}"; 237 | } 238 | return c; 239 | } 240 | 241 | MqttHA::Component MqttHA::createNumber(const Command* command) const { 242 | Component c = createComponent("number", command->key, command->name); 243 | c.fields["state_topic"] = createStateTopic("values", command->name); 244 | if (!command->ha_device_class.empty()) 245 | c.fields["device_class"] = command->ha_device_class; 246 | if (!command->ha_entity_category.empty()) 247 | c.fields["entity_category"] = command->ha_entity_category; 248 | if (!command->unit.empty()) c.fields["unit_of_measurement"] = command->unit; 249 | c.fields["value_template"] = "{{value_json.value}}"; 250 | c.fields["command_topic"] = commandTopic; 251 | c.fields["command_template"] = 252 | "{\"id\":\"write\",\"key\":\"" + command->key + "\",\"value\":{{value}}}"; 253 | c.fields["min"] = std::to_string(command->min); 254 | c.fields["max"] = std::to_string(command->max); 255 | c.fields["step"] = std::to_string(command->ha_step); 256 | c.fields["mode"] = command->ha_mode; 257 | return c; 258 | } 259 | 260 | MqttHA::Component MqttHA::createSelect(const Command* command) const { 261 | Component c = createComponent("select", command->key, command->name); 262 | c.fields["state_topic"] = createStateTopic("values", command->name); 263 | if (!command->ha_device_class.empty()) 264 | c.fields["device_class"] = command->ha_device_class; 265 | if (!command->ha_entity_category.empty()) 266 | c.fields["entity_category"] = command->ha_entity_category; 267 | c.fields["command_topic"] = commandTopic; 268 | 269 | KeyValueMapping optionsResult = 270 | createOptions(command->ha_key_value_map, command->ha_default_key); 271 | 272 | c.options = optionsResult.options; 273 | c.fields["value_template"] = optionsResult.valueMap; 274 | c.fields["command_template"] = "{\"id\":\"write\",\"key\":\"" + command->key + 275 | "\",\"value\":" + optionsResult.cmdMap + "}"; 276 | 277 | return c; 278 | } 279 | 280 | MqttHA::Component MqttHA::createSwitch(const Command* command) const { 281 | Component c = createComponent("switch", command->key, command->name); 282 | c.fields["state_topic"] = createStateTopic("values", command->name); 283 | if (!command->ha_device_class.empty()) 284 | c.fields["device_class"] = command->ha_device_class; 285 | if (!command->ha_entity_category.empty()) 286 | c.fields["entity_category"] = command->ha_entity_category; 287 | c.fields["payload_on"] = std::to_string(command->ha_payload_on); 288 | c.fields["payload_off"] = std::to_string(command->ha_payload_off); 289 | c.fields["value_template"] = "{{value_json.value}}"; 290 | c.fields["command_topic"] = commandTopic; 291 | c.fields["command_template"] = 292 | "{\"id\":\"write\",\"key\":\"" + command->key + "\",\"value\":{{value}}}"; 293 | return c; 294 | } 295 | 296 | MqttHA::Component MqttHA::createButtonRestart() const { 297 | Component c = createComponent("button", "restart", "Restart"); 298 | c.fields["command_topic"] = commandTopic; 299 | c.fields["payload_press"] = "{\"id\":\"restart\",\"value\":true}"; 300 | c.fields["entity_category"] = "config"; 301 | return c; 302 | } 303 | 304 | MqttHA::Component MqttHA::createDiagnostic(const std::string& component, 305 | const std::string& uniqueIdKey, 306 | const std::string& name) const { 307 | Component c = createComponent(component, uniqueIdKey, name); 308 | c.fields["entity_category"] = "diagnostic"; 309 | return c; 310 | } 311 | 312 | MqttHA::Component MqttHA::createDiagnosticUptime() const { 313 | Component c = createDiagnostic("sensor", "uptime", "Uptime"); 314 | c.fields["state_topic"] = createStateTopic("state", "uptime"); 315 | c.fields["value_template"] = 316 | "{{timedelta(seconds=((value|float)/1000)|int)}}"; 317 | 318 | c.device["name"] = thingName; 319 | c.device["manufacturer"] = thingManufacturer; 320 | c.device["model"] = thingModel; 321 | c.device["model_id"] = thingModelId; 322 | c.device["serial_number"] = uniqueId; 323 | c.device["hw_version"] = thingHwVersion; 324 | c.device["sw_version"] = thingSwVersion; 325 | c.device["configuration_url"] = thingConfigurationUrl; 326 | return c; 327 | } 328 | 329 | MqttHA::Component MqttHA::createDiagnosticFreeHeap() const { 330 | Component c = createDiagnostic("sensor", "free_heap", "Free Heap"); 331 | c.fields["state_topic"] = createStateTopic("state", "free_heap"); 332 | c.fields["unit_of_measurement"] = "B"; 333 | c.fields["value_template"] = "{{value|int}}"; 334 | return c; 335 | } 336 | 337 | MqttHA::Component MqttHA::createDiagnosticLoopDuration() const { 338 | Component c = createDiagnostic("sensor", "loop_duration", "Loop Duration"); 339 | c.fields["state_topic"] = createStateTopic("state", "loop_duration"); 340 | c.fields["unit_of_measurement"] = "µs"; 341 | c.fields["value_template"] = "{{value|int}}"; 342 | return c; 343 | } 344 | 345 | #endif -------------------------------------------------------------------------------- /src/client.cpp: -------------------------------------------------------------------------------- 1 | #include "client.hpp" 2 | 3 | #include "bus.hpp" 4 | #include "main.hpp" 5 | 6 | #define M1 0b11000000 7 | #define M2 0b10000000 8 | 9 | enum requests { CMD_INIT = 0, CMD_SEND, CMD_START, CMD_INFO }; 10 | 11 | bool handleNewClient(WiFiServer* server, WiFiClient clients[]) { 12 | if (!server->hasClient()) return false; 13 | 14 | // Find free/disconnected slot 15 | int i; 16 | for (i = 0; i < MAX_WIFI_CLIENTS; i++) { 17 | if (!clients[i]) { // equivalent to !clients[i].connected() 18 | clients[i] = server->accept(); 19 | clients[i].setNoDelay(true); 20 | break; 21 | } 22 | } 23 | 24 | // No free/disconnected slot so reject 25 | if (i == MAX_WIFI_CLIENTS) { 26 | server->accept().println("busy"); 27 | // hints: server.available() is a WiFiClient with short-term scope 28 | // when out of scope, a WiFiClient will 29 | // - flush() - all data will be sent 30 | // - stop() - automatically too 31 | } 32 | 33 | return true; 34 | } 35 | 36 | void handleClient(WiFiClient* client) { 37 | while (client->available() && Bus.availableForWrite() > 0) { 38 | // working char by char is not very efficient 39 | Bus.write(client->read()); 40 | } 41 | } 42 | 43 | int pushClient(WiFiClient* client, uint8_t byte) { 44 | if (client->availableForWrite() >= AVAILABLE_THRESHOLD) { 45 | client->write(byte); 46 | return 1; 47 | } 48 | return 0; 49 | } 50 | 51 | void decode(int b1, int b2, uint8_t (&data)[2]) { 52 | data[0] = (b1 >> 2) & 0b1111; 53 | data[1] = ((b1 & 0b11) << 6) | (b2 & 0b00111111); 54 | } 55 | 56 | void encode(uint8_t c, uint8_t d, uint8_t (&data)[2]) { 57 | data[0] = M1 | c << 2 | d >> 6; 58 | data[1] = M2 | (d & 0b00111111); 59 | } 60 | 61 | void send_res(WiFiClient* client, uint8_t c, uint8_t d) { 62 | uint8_t data[2]; 63 | encode(c, d, data); 64 | client->write(data, 2); 65 | } 66 | 67 | void process_cmd(WiFiClient* client, uint8_t c, uint8_t d) { 68 | if (c == CMD_INIT) { 69 | send_res(client, RESETTED, 0x0); 70 | return; 71 | } 72 | if (c == CMD_START) { 73 | if (d == SYN) { 74 | clearArbitrationClient(); 75 | DEBUG_LOG("CMD_START SYN\n"); 76 | return; 77 | } else { 78 | // start arbitration 79 | WiFiClient* cl = client; 80 | uint8_t ad = d; 81 | if (!setArbitrationClient(client, d)) { 82 | if (cl != client) { 83 | // only one client can be in arbitration 84 | DEBUG_LOG("CMD_START ONGOING 0x%02 0x%02x\n", ad, d); 85 | send_res(client, ERROR_HOST, ERR_FRAMING); 86 | return; 87 | } else { 88 | DEBUG_LOG("CMD_START REPEAT 0x%02x\n", d); 89 | } 90 | } else { 91 | DEBUG_LOG("CMD_START 0x%02x\n", d); 92 | } 93 | setArbitrationClient(client, d); 94 | return; 95 | } 96 | } 97 | if (c == CMD_SEND) { 98 | DEBUG_LOG("SEND 0x%02x\n", d); 99 | Bus.write(d); 100 | return; 101 | } 102 | if (c == CMD_INFO) { 103 | // if needed, set bit 0 as reply to INIT command 104 | return; 105 | } 106 | } 107 | 108 | bool read_cmd(WiFiClient* client, uint8_t (&data)[2]) { 109 | int b, b2; 110 | 111 | b = client->read(); 112 | 113 | if (b < 0) { 114 | // available and read -1 ??? 115 | return false; 116 | } 117 | 118 | if (b < 0b10000000) { 119 | data[0] = CMD_SEND; 120 | data[1] = b; 121 | return true; 122 | } 123 | 124 | if (b < 0b11000000) { 125 | DEBUG_LOG("first command signature error\n"); 126 | client->write("first command signature error"); 127 | // first command signature error 128 | client->stop(); 129 | return false; 130 | } 131 | 132 | b2 = client->read(); 133 | 134 | if (b2 < 0) { 135 | // second command missing 136 | DEBUG_LOG("second command missing\n"); 137 | client->write("second command missing"); 138 | client->stop(); 139 | return false; 140 | } 141 | 142 | if ((b2 & 0b11000000) != 0b10000000) { 143 | // second command signature error 144 | DEBUG_LOG("second command signature error\n"); 145 | client->write("second command signature error"); 146 | client->stop(); 147 | return false; 148 | } 149 | 150 | decode(b, b2, data); 151 | return true; 152 | } 153 | 154 | void handleClientEnhanced(WiFiClient* client) { 155 | while (client->available()) { 156 | uint8_t data[2]; 157 | if (read_cmd(client, data)) { 158 | process_cmd(client, data[0], data[1]); 159 | } 160 | } 161 | } 162 | 163 | int pushClientEnhanced(WiFiClient* client, uint8_t c, uint8_t d, bool log) { 164 | if (log) { 165 | DEBUG_LOG("DATA 0x%02x 0x%02x\n", c, d); 166 | } 167 | if (client->availableForWrite() >= AVAILABLE_THRESHOLD) { 168 | send_res(client, c, d); 169 | return 1; 170 | } 171 | return 0; 172 | } 173 | 174 | #if defined(EBUS_INTERNAL) 175 | #include 176 | 177 | AbstractClient::AbstractClient(WiFiClient* client, ebus::Request* request, 178 | bool write) 179 | : client(client), request(request), write(write) {} 180 | 181 | bool AbstractClient::isWriteCapable() const { return write; } 182 | 183 | bool AbstractClient::isConnected() const { 184 | return client && client->connected(); 185 | } 186 | 187 | void AbstractClient::stop() { 188 | if (client) client->stop(); 189 | } 190 | 191 | ReadOnlyClient::ReadOnlyClient(WiFiClient* client, ebus::Request* request) 192 | : AbstractClient(client, request, false) {} 193 | 194 | bool ReadOnlyClient::available() const { return false; } 195 | 196 | bool ReadOnlyClient::readByte(uint8_t& byte) { return false; } 197 | 198 | bool ReadOnlyClient::writeBytes(const std::vector& bytes) { 199 | if (!isConnected() || bytes.empty()) return false; 200 | 201 | client->write(bytes.data(), bytes.size()); 202 | return true; 203 | } 204 | 205 | bool ReadOnlyClient::handleBusData(const uint8_t& byte) { return false; } 206 | 207 | RegularClient::RegularClient(WiFiClient* client, ebus::Request* request) 208 | : AbstractClient(client, request, true) {} 209 | 210 | bool RegularClient::available() const { return client && client->available(); } 211 | 212 | bool RegularClient::readByte(uint8_t& byte) { 213 | if (available()) { 214 | byte = client->read(); 215 | return true; 216 | } 217 | return false; 218 | } 219 | 220 | bool RegularClient::writeBytes(const std::vector& bytes) { 221 | if (!isConnected() || bytes.empty()) return false; 222 | 223 | client->write(bytes.data(), bytes.size()); 224 | return true; 225 | } 226 | 227 | bool RegularClient::handleBusData(const uint8_t& byte) { 228 | // Handle bus response according to last command 229 | switch (request->getResult()) { 230 | case ebus::RequestResult::observeSyn: 231 | case ebus::RequestResult::firstLost: 232 | case ebus::RequestResult::firstError: 233 | case ebus::RequestResult::retryError: 234 | case ebus::RequestResult::secondLost: 235 | case ebus::RequestResult::secondError: 236 | return false; 237 | case ebus::RequestResult::observeData: 238 | case ebus::RequestResult::firstSyn: 239 | case ebus::RequestResult::firstRetry: 240 | case ebus::RequestResult::retrySyn: 241 | case ebus::RequestResult::firstWon: 242 | case ebus::RequestResult::secondWon: 243 | writeBytes({byte}); 244 | return true; 245 | default: 246 | break; 247 | } 248 | return false; 249 | } 250 | 251 | EnhancedClient::EnhancedClient(WiFiClient* client, ebus::Request* request) 252 | : AbstractClient(client, request, true) {} 253 | 254 | bool EnhancedClient::available() const { return client && client->available(); } 255 | 256 | bool EnhancedClient::readByte(uint8_t& byte) { 257 | int b1 = client->peek(); 258 | if (b1 < 0) return false; 259 | 260 | if (b1 < 0x80) { 261 | // Short form: just a data byte, no prefix 262 | byte = client->read(); 263 | return true; 264 | } 265 | 266 | // Full enhanced protocol: need two bytes 267 | if (client->available() < 2) return false; 268 | b1 = client->read(); 269 | int b2 = client->read(); 270 | 271 | // Check signatures 272 | if ((b1 & 0xc0) != 0xc0 || (b2 & 0xc0) != 0x80) { 273 | // Invalid signature, protocol error 274 | writeBytes({ERROR_HOST, ERR_FRAMING}); 275 | client->stop(); 276 | return false; 277 | } 278 | 279 | // Decode command and data according to enhanced protocol 280 | uint8_t cmd = (b1 >> 2) & 0x0f; 281 | uint8_t data = ((b1 & 0x03) << 6) | (b2 & 0x3f); 282 | 283 | // Handle commands 284 | switch (cmd) { 285 | case CMD_INIT: 286 | writeBytes({RESETTED, 0x0}); 287 | return false; 288 | case CMD_SEND: 289 | byte = data; 290 | return true; 291 | case CMD_START: 292 | if (data == SYN) return false; 293 | byte = data; 294 | return true; 295 | case CMD_INFO: 296 | return false; 297 | default: 298 | break; 299 | } 300 | 301 | return false; 302 | } 303 | 304 | bool EnhancedClient::writeBytes(const std::vector& bytes) { 305 | if (!isConnected() || bytes.empty()) return false; 306 | 307 | uint8_t cmd = RECEIVED; 308 | uint8_t data = bytes[0]; 309 | 310 | if (bytes.size() == 2) { 311 | cmd = bytes[0]; 312 | data = bytes[1]; 313 | } 314 | 315 | // Short form for data < 0x80 316 | if (bytes.size() == 1 && data < 0x80) { 317 | client->write(data); 318 | } else { 319 | uint8_t out[2]; 320 | out[0] = 0xc0 | (cmd << 2) | (data >> 6); 321 | out[1] = 0x80 | (data & 0x3f); 322 | client->write(out, 2); 323 | } 324 | return true; 325 | } 326 | 327 | bool EnhancedClient::handleBusData(const uint8_t& byte) { 328 | // Handle bus response according to last command 329 | switch (request->getResult()) { 330 | case ebus::RequestResult::observeSyn: 331 | case ebus::RequestResult::firstLost: 332 | case ebus::RequestResult::secondLost: 333 | writeBytes({FAILED, byte}); 334 | return false; 335 | case ebus::RequestResult::firstError: 336 | case ebus::RequestResult::retryError: 337 | case ebus::RequestResult::secondError: 338 | writeBytes({ERROR_EBUS, ERR_FRAMING}); 339 | return false; 340 | case ebus::RequestResult::observeData: 341 | writeBytes({RECEIVED, byte}); 342 | return true; 343 | case ebus::RequestResult::firstSyn: 344 | case ebus::RequestResult::firstRetry: 345 | case ebus::RequestResult::retrySyn: 346 | // Waiting for arbitration, do nothing 347 | return true; 348 | case ebus::RequestResult::firstWon: 349 | case ebus::RequestResult::secondWon: 350 | writeBytes({STARTED, byte}); 351 | return true; 352 | default: 353 | break; 354 | } 355 | return false; 356 | } 357 | 358 | ClientManager clientManager; 359 | 360 | ClientManager::ClientManager() 361 | : readonlyServer(3334), regularServer(3333), enhancedServer(3335) {} 362 | 363 | void ClientManager::start(ebus::Bus* bus, ebus::Request* request, 364 | ebus::ServiceRunnerFreeRtos* serviceRunner) { 365 | readonlyServer.begin(); 366 | regularServer.begin(); 367 | enhancedServer.begin(); 368 | 369 | this->request = request; 370 | this->serviceRunner = serviceRunner; 371 | 372 | clientByteQueue = new ebus::Queue(); 373 | 374 | request->setExternalBusRequestedCallback([this]() { busRequested = true; }); 375 | 376 | serviceRunner->addByteListener( 377 | [this](const uint8_t& byte) { clientByteQueue->try_push(byte); }); 378 | 379 | // Start the clientManagerRunner task 380 | xTaskCreate(&ClientManager::taskFunc, "clientManagerRunner", 4096, this, 3, 381 | &clientManagerTaskHandle); 382 | } 383 | 384 | void ClientManager::stop() { stopRunner = true; } 385 | 386 | void ClientManager::taskFunc(void* arg) { 387 | ClientManager* self = static_cast(arg); 388 | AbstractClient* activeClient = nullptr; 389 | BusState busState = BusState::Idle; 390 | uint8_t receiveByte = 0; 391 | 392 | for (;;) { 393 | if (self->stopRunner) vTaskDelete(NULL); 394 | 395 | // Check for new clients 396 | self->acceptClients(); 397 | 398 | // Clean up disconnected active client 399 | if (activeClient && !activeClient->isConnected()) { 400 | activeClient->stop(); 401 | activeClient = nullptr; 402 | busState = BusState::Idle; 403 | self->busRequested = false; 404 | ebus::request->reset(); 405 | } 406 | 407 | // Select new active client if idle 408 | if (!activeClient && busState == BusState::Idle) { 409 | for (size_t i = 0; i < self->clients.size(); ++i) { 410 | AbstractClient* client = self->clients[i].get(); 411 | if (client->isConnected() && client->isWriteCapable() && 412 | client->available()) { 413 | activeClient = client; 414 | busState = BusState::Request; 415 | self->busRequested = false; 416 | break; 417 | } 418 | } 419 | } 420 | 421 | // Request bus access 422 | if (activeClient && busState == BusState::Request) { 423 | if (ebus::request->busAvailable()) { 424 | uint8_t firstByte = 0; 425 | if (activeClient->readByte(firstByte)) { 426 | ebus::request->requestBus(firstByte, true); 427 | busState = BusState::Response; 428 | } else { 429 | // Client initialized or error 430 | activeClient = nullptr; 431 | busState = BusState::Idle; 432 | self->busRequested = false; 433 | ebus::request->reset(); 434 | } 435 | } 436 | } 437 | 438 | // Transmit to bus if needed 439 | if (activeClient && busState == BusState::Transmit) { 440 | uint8_t sendByte = 0; 441 | if (activeClient->readByte(sendByte)) { 442 | ebus::bus->writeByte(sendByte); 443 | busState = BusState::Response; 444 | } 445 | } 446 | 447 | // Process received bytes from bus 448 | while (self->clientByteQueue->try_pop(receiveByte)) { 449 | updateLastComms(); 450 | 451 | if (activeClient) { 452 | if ((busState == BusState::Response || 453 | busState == BusState::Transmit) && 454 | self->busRequested) { 455 | if (activeClient->handleBusData(receiveByte)) { 456 | // Continue transmitting if needed 457 | busState = BusState::Transmit; 458 | } else { 459 | // Transaction done or error 460 | activeClient = nullptr; 461 | busState = BusState::Idle; 462 | self->busRequested = false; 463 | ebus::request->reset(); 464 | } 465 | } 466 | } 467 | 468 | // Forward to all other clients 469 | for (size_t i = 0; i < self->clients.size(); ++i) { 470 | AbstractClient* client = self->clients[i].get(); 471 | if (client != activeClient && client->isConnected()) { 472 | client->writeBytes({receiveByte}); 473 | } 474 | } 475 | } 476 | 477 | vTaskDelay(pdMS_TO_TICKS(1)); 478 | } 479 | } 480 | 481 | void ClientManager::acceptClients() { 482 | // Accept read-only clients 483 | while (readonlyServer.hasClient()) { 484 | WiFiClient* client = new WiFiClient(readonlyServer.accept()); 485 | client->setNoDelay(true); 486 | clients.push_back(make_unique(client, request)); 487 | } 488 | 489 | // Accept regular clients 490 | while (regularServer.hasClient()) { 491 | WiFiClient* client = new WiFiClient(regularServer.accept()); 492 | client->setNoDelay(true); 493 | clients.push_back(make_unique(client, request)); 494 | } 495 | 496 | // Accept enhanced clients 497 | while (enhancedServer.hasClient()) { 498 | WiFiClient* client = new WiFiClient(enhancedServer.accept()); 499 | client->setNoDelay(true); 500 | clients.push_back(make_unique(client, request)); 501 | } 502 | 503 | // Clean up disconnected clients 504 | clients.erase( 505 | std::remove_if(clients.begin(), clients.end(), 506 | [](const std::unique_ptr& client) { 507 | if (!client->isConnected()) { 508 | client->stop(); // <-- ensure socket is closed 509 | return true; 510 | } 511 | return false; 512 | }), 513 | clients.end()); 514 | } 515 | 516 | #endif -------------------------------------------------------------------------------- /src/store.cpp: -------------------------------------------------------------------------------- 1 | #if defined(EBUS_INTERNAL) 2 | #include "store.hpp" 3 | 4 | #include 5 | 6 | #include 7 | 8 | const double getDoubleFromVector(const Command* command) { 9 | double value = 0; 10 | 11 | switch (command->datatype) { 12 | case ebus::DataType::BCD: 13 | value = ebus::byte_2_bcd(command->data); 14 | break; 15 | case ebus::DataType::UINT8: 16 | value = ebus::byte_2_uint8(command->data); 17 | break; 18 | case ebus::DataType::INT8: 19 | value = ebus::byte_2_int8(command->data); 20 | break; 21 | case ebus::DataType::UINT16: 22 | value = ebus::byte_2_uint16(command->data); 23 | break; 24 | case ebus::DataType::INT16: 25 | value = ebus::byte_2_int16(command->data); 26 | break; 27 | case ebus::DataType::UINT32: 28 | value = ebus::byte_2_uint32(command->data); 29 | break; 30 | case ebus::DataType::INT32: 31 | value = ebus::byte_2_int32(command->data); 32 | break; 33 | case ebus::DataType::DATA1B: 34 | value = ebus::byte_2_data1b(command->data); 35 | break; 36 | case ebus::DataType::DATA1C: 37 | value = ebus::byte_2_data1c(command->data); 38 | break; 39 | case ebus::DataType::DATA2B: 40 | value = ebus::byte_2_data2b(command->data); 41 | break; 42 | case ebus::DataType::DATA2C: 43 | value = ebus::byte_2_data2c(command->data); 44 | break; 45 | case ebus::DataType::FLOAT: 46 | value = ebus::byte_2_float(command->data); 47 | break; 48 | default: 49 | break; 50 | } 51 | 52 | value = value / command->divider; 53 | value = ebus::round_digits(value, command->digits); 54 | 55 | return value; 56 | } 57 | 58 | const std::vector getVectorFromDouble(const Command* command, 59 | double value) { 60 | std::vector result; 61 | if (!command) return result; 62 | 63 | value = value * command->divider; 64 | value = ebus::round_digits(value, command->digits); 65 | 66 | switch (command->datatype) { 67 | case ebus::DataType::BCD: 68 | result = ebus::bcd_2_byte(value); 69 | break; 70 | case ebus::DataType::UINT8: 71 | result = ebus::uint8_2_byte(value); 72 | break; 73 | case ebus::DataType::INT8: 74 | result = ebus::int8_2_byte(value); 75 | break; 76 | case ebus::DataType::UINT16: 77 | result = ebus::uint16_2_byte(value); 78 | break; 79 | case ebus::DataType::INT16: 80 | result = ebus::int16_2_byte(value); 81 | break; 82 | case ebus::DataType::UINT32: 83 | result = ebus::uint32_2_byte(value); 84 | break; 85 | case ebus::DataType::INT32: 86 | result = ebus::int32_2_byte(value); 87 | break; 88 | case ebus::DataType::DATA1B: 89 | result = ebus::data1b_2_byte(value); 90 | break; 91 | case ebus::DataType::DATA1C: 92 | result = ebus::data1c_2_byte(value); 93 | break; 94 | case ebus::DataType::DATA2B: 95 | result = ebus::data2b_2_byte(value); 96 | break; 97 | case ebus::DataType::DATA2C: 98 | result = ebus::data2c_2_byte(value); 99 | break; 100 | case ebus::DataType::FLOAT: 101 | result = ebus::float_2_byte(value); 102 | break; 103 | default: 104 | break; 105 | } 106 | 107 | return result; 108 | } 109 | 110 | const std::string getStringFromVector(const Command* command) { 111 | std::string value; 112 | 113 | switch (command->datatype) { 114 | case ebus::DataType::CHAR1: 115 | case ebus::DataType::CHAR2: 116 | case ebus::DataType::CHAR3: 117 | case ebus::DataType::CHAR4: 118 | case ebus::DataType::CHAR5: 119 | case ebus::DataType::CHAR6: 120 | case ebus::DataType::CHAR7: 121 | case ebus::DataType::CHAR8: 122 | value = ebus::byte_2_char(command->data); 123 | break; 124 | case ebus::DataType::HEX1: 125 | case ebus::DataType::HEX2: 126 | case ebus::DataType::HEX3: 127 | case ebus::DataType::HEX4: 128 | case ebus::DataType::HEX5: 129 | case ebus::DataType::HEX6: 130 | case ebus::DataType::HEX7: 131 | case ebus::DataType::HEX8: 132 | value = ebus::byte_2_hex(command->data); 133 | break; 134 | default: 135 | break; 136 | } 137 | 138 | return value; 139 | } 140 | 141 | const std::vector getVectorFromString(const Command* command, 142 | const std::string& value) { 143 | std::vector result; 144 | if (!command) return result; 145 | 146 | switch (command->datatype) { 147 | case ebus::DataType::CHAR1: 148 | case ebus::DataType::CHAR2: 149 | case ebus::DataType::CHAR3: 150 | case ebus::DataType::CHAR4: 151 | case ebus::DataType::CHAR5: 152 | case ebus::DataType::CHAR6: 153 | case ebus::DataType::CHAR7: 154 | case ebus::DataType::CHAR8: 155 | result = ebus::char_2_byte(value.substr(0, command->length)); 156 | break; 157 | case ebus::DataType::HEX1: 158 | case ebus::DataType::HEX2: 159 | case ebus::DataType::HEX3: 160 | case ebus::DataType::HEX4: 161 | case ebus::DataType::HEX5: 162 | case ebus::DataType::HEX6: 163 | case ebus::DataType::HEX7: 164 | case ebus::DataType::HEX8: 165 | result = ebus::hex_2_byte(value.substr(0, command->length)); 166 | break; 167 | default: 168 | break; 169 | } 170 | return result; 171 | } 172 | 173 | Store store; 174 | 175 | struct FieldEvaluation { 176 | const char* name; 177 | bool required; 178 | FieldType type; 179 | }; 180 | 181 | const std::string Store::evaluateCommand(const JsonDocument& doc) { 182 | // Define the fields to evaluate 183 | const FieldEvaluation fields[] = {// Command Fields 184 | {"key", true, FT_String}, 185 | {"name", true, FT_String}, 186 | {"read_cmd", true, FT_HexString}, 187 | {"write_cmd", false, FT_HexString}, 188 | {"active", true, FT_Bool}, 189 | {"interval", false, FT_Uint32T}, 190 | // Data Fields 191 | {"master", true, FT_Bool}, 192 | {"position", true, FT_SizeT}, 193 | {"datatype", true, FT_DataType}, 194 | {"divider", false, FT_Float}, 195 | {"min", false, FT_Float}, 196 | {"max", false, FT_Float}, 197 | {"digits", false, FT_Uint8T}, 198 | {"unit", false, FT_String}, 199 | // Home Assistant 200 | {"ha", false, FT_Bool}, 201 | {"ha_component", false, FT_String}, 202 | {"ha_device_class", false, FT_String}, 203 | {"ha_entity_category", false, FT_String}, 204 | {"ha_mode", false, FT_String}, 205 | {"ha_key_value_map", false, FT_KeyValueMap}, 206 | {"ha_default_key", false, FT_Int}, 207 | {"ha_payload_on", false, FT_Uint8T}, 208 | {"ha_payload_off", false, FT_Uint8T}, 209 | {"ha_state_class", false, FT_String}, 210 | {"ha_step", false, FT_Float}}; 211 | 212 | // Evaluate each field in a loop 213 | for (const auto& field : fields) { 214 | std::string error = 215 | isFieldValid(doc, field.name, field.required, field.type); 216 | if (!error.empty()) return error; // Return the first error found 217 | } 218 | 219 | return ""; // No errors found 220 | } 221 | 222 | Command Store::createCommand(const JsonDocument& doc) { 223 | Command command; 224 | 225 | // Command Fields 226 | command.key = doc["key"].as(); 227 | command.name = doc["name"].as(); 228 | command.read_cmd = ebus::to_vector(doc["read_cmd"].as()); 229 | if (!doc["write_cmd"].isNull()) 230 | command.write_cmd = ebus::to_vector(doc["write_cmd"].as()); 231 | command.active = doc["active"].as(); 232 | if (!doc["interval"].isNull()) 233 | command.interval = doc["interval"].as(); 234 | command.last = 0; 235 | command.data = std::vector(); 236 | 237 | // Data Fields 238 | command.master = doc["master"].as(); 239 | command.position = doc["position"].as(); 240 | command.datatype = ebus::string_2_datatype(doc["datatype"].as()); 241 | command.length = ebus::sizeof_datatype(command.datatype); 242 | command.numeric = ebus::typeof_datatype(command.datatype); 243 | if (!doc["divider"].isNull() && doc["divider"].as() > 0) 244 | command.divider = doc["divider"].as(); 245 | if (!doc["min"].isNull()) command.min = doc["min"].as(); 246 | if (!doc["max"].isNull()) command.max = doc["max"].as(); 247 | if (!doc["digits"].isNull()) command.digits = doc["digits"].as(); 248 | if (!doc["unit"].isNull()) command.unit = doc["unit"].as(); 249 | 250 | // Home Assistant 251 | if (!doc["ha"].isNull()) command.ha = doc["ha"].as(); 252 | 253 | if (command.ha) { 254 | if (!doc["ha_component"].isNull()) 255 | command.ha_component = doc["ha_component"].as(); 256 | if (!doc["ha_device_class"].isNull()) 257 | command.ha_device_class = doc["ha_device_class"].as(); 258 | if (!doc["ha_entity_category"].isNull()) 259 | command.ha_entity_category = doc["ha_entity_category"].as(); 260 | if (!doc["ha_mode"].isNull()) 261 | command.ha_mode = doc["ha_mode"].as(); 262 | 263 | if (!doc["ha_key_value_map"].isNull()) { 264 | JsonObjectConst ha_key_value_map = doc["ha_key_value_map"]; 265 | for (JsonPairConst kv : ha_key_value_map) { 266 | command.ha_key_value_map[std::stoi(kv.key().c_str())] = 267 | kv.value().as(); 268 | } 269 | } 270 | 271 | if (!doc["ha_default_key"].isNull()) 272 | command.ha_default_key = doc["ha_default_key"].as(); 273 | if (!doc["ha_payload_on"].isNull()) 274 | command.ha_payload_on = doc["ha_payload_on"].as(); 275 | if (!doc["ha_payload_off"].isNull()) 276 | command.ha_payload_off = doc["ha_payload_off"].as(); 277 | if (!doc["ha_state_class"].isNull()) 278 | command.ha_state_class = doc["ha_state_class"].as(); 279 | if (!doc["ha_step"].isNull() && doc["ha_step"].as() > 0) 280 | command.ha_step = doc["ha_step"].as(); 281 | } 282 | 283 | return command; 284 | } 285 | 286 | void Store::insertCommand(const Command& command) { 287 | // Insert or update in allCommandsByKey 288 | auto it = allCommandsByKey.find(command.key); 289 | if (it != allCommandsByKey.end()) 290 | it->second = command; 291 | else 292 | allCommandsByKey.insert(std::make_pair(command.key, command)); 293 | 294 | // Remove from previous index if exists 295 | for (auto itp = passiveCommands.begin(); itp != passiveCommands.end(); 296 | ++itp) { 297 | itp->second.erase(std::remove_if(itp->second.begin(), itp->second.end(), 298 | [&](const Command* cmd) { 299 | return cmd->key == command.key; 300 | }), 301 | itp->second.end()); 302 | } 303 | 304 | activeCommands.erase( 305 | std::remove_if( 306 | activeCommands.begin(), activeCommands.end(), 307 | [&](const Command* cmd) { return cmd->key == command.key; }), 308 | activeCommands.end()); 309 | 310 | // Add to passive or active index 311 | Command* cmdPtr = &allCommandsByKey[command.key]; 312 | if (command.active) 313 | activeCommands.push_back(cmdPtr); 314 | else 315 | passiveCommands[command.read_cmd].push_back(cmdPtr); 316 | } 317 | 318 | void Store::removeCommand(const std::string& key) { 319 | auto it = allCommandsByKey.find(key); 320 | if (it != allCommandsByKey.end()) { 321 | // Remove from passiveCommands vectors (only matching key) 322 | for (auto& kv : passiveCommands) { 323 | kv.second.erase( 324 | std::remove_if(kv.second.begin(), kv.second.end(), 325 | [&](const Command* cmd) { return cmd->key == key; }), 326 | kv.second.end()); 327 | } 328 | 329 | // Remove from activeCommands 330 | activeCommands.erase( 331 | std::remove_if(activeCommands.begin(), activeCommands.end(), 332 | [&](const Command* cmd) { return cmd->key == key; }), 333 | activeCommands.end()); 334 | 335 | // Remove from allCommandsByKey 336 | allCommandsByKey.erase(it); 337 | } 338 | } 339 | 340 | Command* Store::findCommand(const std::string& key) { 341 | auto it = allCommandsByKey.find(key); 342 | if (it != allCommandsByKey.end()) 343 | return &(it->second); 344 | else 345 | return nullptr; 346 | } 347 | 348 | int64_t Store::loadCommands() { 349 | Preferences preferences; 350 | preferences.begin("commands", true); 351 | 352 | int64_t bytes = preferences.getBytesLength("ebus"); 353 | if (bytes > 2) { // 2 = empty json array "[]" 354 | std::vector buffer(bytes); 355 | bytes = preferences.getBytes("ebus", buffer.data(), bytes); 356 | if (bytes > 0) { 357 | std::string payload(buffer.begin(), buffer.end()); 358 | deserializeCommands(payload.c_str()); 359 | } else { 360 | bytes = -1; 361 | } 362 | } else { 363 | bytes = 0; 364 | } 365 | 366 | preferences.end(); 367 | return bytes; 368 | } 369 | 370 | int64_t Store::saveCommands() const { 371 | Preferences preferences; 372 | preferences.begin("commands", false); 373 | 374 | std::string payload = serializeCommands(); 375 | int64_t bytes = payload.size(); 376 | if (bytes > 2) { // 2 = empty json array "[]" 377 | bytes = preferences.putBytes("ebus", payload.data(), bytes); 378 | if (bytes == 0) bytes = -1; 379 | } else { 380 | bytes = 0; 381 | } 382 | 383 | preferences.end(); 384 | return bytes; 385 | } 386 | 387 | int64_t Store::wipeCommands() { 388 | Preferences preferences; 389 | preferences.begin("commands", false); 390 | 391 | int64_t bytes = preferences.getBytesLength("ebus"); 392 | if (bytes > 0) { 393 | if (!preferences.remove("ebus")) bytes = -1; 394 | } 395 | 396 | preferences.end(); 397 | return bytes; 398 | } 399 | 400 | JsonDocument Store::getCommandJson(const Command* command) { 401 | JsonDocument doc; 402 | 403 | // Command Fields 404 | doc["key"] = command->key; 405 | doc["name"] = command->name; 406 | doc["read_cmd"] = ebus::to_string(command->read_cmd); 407 | doc["write_cmd"] = ebus::to_string(command->write_cmd); 408 | doc["active"] = command->active; 409 | doc["interval"] = command->interval; 410 | 411 | // Data Fields 412 | doc["master"] = command->master; 413 | doc["position"] = command->position; 414 | doc["datatype"] = ebus::datatype_2_string(command->datatype); 415 | doc["divider"] = command->divider; 416 | doc["min"] = command->min; 417 | doc["max"] = command->max; 418 | doc["digits"] = command->digits; 419 | doc["unit"] = command->unit; 420 | 421 | // Home Assistant 422 | doc["ha"] = command->ha; 423 | doc["ha_component"] = command->ha_component; 424 | doc["ha_device_class"] = command->ha_device_class; 425 | doc["ha_entity_category"] = command->ha_entity_category; 426 | doc["ha_mode"] = command->ha_mode; 427 | 428 | JsonObject ha_key_value_map = doc["ha_key_value_map"].to(); 429 | for (const auto& kv : command->ha_key_value_map) 430 | ha_key_value_map[std::to_string(kv.first)] = kv.second; 431 | 432 | doc["ha_default_key"] = command->ha_default_key; 433 | doc["ha_payload_on"] = command->ha_payload_on; 434 | doc["ha_payload_off"] = command->ha_payload_off; 435 | doc["ha_state_class"] = command->ha_state_class; 436 | doc["ha_step"] = command->ha_step; 437 | 438 | doc.shrinkToFit(); 439 | return doc; 440 | } 441 | 442 | const JsonDocument Store::getCommandsJsonDocument() const { 443 | JsonDocument doc; 444 | 445 | if (!allCommandsByKey.empty()) 446 | for (const auto kv : allCommandsByKey) doc.add(getCommandJson(&kv.second)); 447 | 448 | if (doc.isNull()) doc.to(); 449 | 450 | doc.shrinkToFit(); 451 | return doc; 452 | } 453 | 454 | const std::string Store::getCommandsJson() const { 455 | std::string payload; 456 | JsonDocument doc = getCommandsJsonDocument(); 457 | serializeJson(doc, payload); 458 | return payload; 459 | } 460 | 461 | const std::vector Store::getCommands() { 462 | std::vector commands; 463 | for (auto& kv : allCommandsByKey) commands.push_back(&(kv.second)); 464 | return commands; 465 | } 466 | 467 | const size_t Store::getActiveCommands() const { return activeCommands.size(); } 468 | 469 | const size_t Store::getPassiveCommands() const { 470 | return passiveCommands.size(); 471 | } 472 | 473 | const bool Store::active() const { return !activeCommands.empty(); } 474 | 475 | Command* Store::nextActiveCommand() { 476 | Command* next = nullptr; 477 | bool init = false; 478 | for (Command* cmd : activeCommands) { 479 | if (cmd->last == 0) { 480 | next = cmd; 481 | init = true; 482 | break; 483 | } 484 | if (next == nullptr || 485 | (cmd->last + cmd->interval * 1000 < next->last + next->interval * 1000)) 486 | next = cmd; 487 | } 488 | 489 | if (!init && next && millis() < next->last + next->interval * 1000) 490 | next = nullptr; 491 | 492 | return next; 493 | } 494 | 495 | std::vector Store::findPassiveCommands( 496 | const std::vector& master) { 497 | std::vector commands; 498 | // Fast lookup by command pattern 499 | auto it = passiveCommands.find(master); 500 | if (it != passiveCommands.end()) { 501 | commands = it->second; 502 | } else { 503 | // fallback: scan for all that match (if needed) 504 | for (const auto& kv : passiveCommands) { 505 | if (ebus::contains(master, kv.first)) 506 | commands.insert(commands.end(), kv.second.begin(), kv.second.end()); 507 | } 508 | } 509 | return commands; 510 | } 511 | 512 | std::vector Store::updateData(Command* command, 513 | const std::vector& master, 514 | const std::vector& slave) { 515 | if (command) { 516 | command->last = millis(); 517 | if (command->master) 518 | command->data = 519 | ebus::range(master, 4 + command->position, command->length); 520 | else 521 | command->data = ebus::range(slave, command->position, command->length); 522 | // Return a vector with just this command, but avoid heap allocation 523 | return {command}; 524 | } 525 | 526 | // Passive: potentially multiple matches 527 | std::vector commands = findPassiveCommands(master); 528 | for (Command* cmd : commands) { 529 | cmd->last = millis(); 530 | if (cmd->master) 531 | cmd->data = ebus::range(master, 4 + cmd->position, cmd->length); 532 | else 533 | cmd->data = ebus::range(slave, cmd->position, cmd->length); 534 | } 535 | return commands; 536 | } 537 | 538 | JsonDocument Store::getValueJson(const Command* command) { 539 | JsonDocument doc; 540 | 541 | if (command->numeric) 542 | doc["value"] = getDoubleFromVector(command); 543 | else 544 | doc["value"] = getStringFromVector(command); 545 | 546 | doc.shrinkToFit(); 547 | return doc; 548 | } 549 | 550 | const std::string Store::getValueFullJson(const Command* command) { 551 | std::string payload; 552 | JsonDocument doc; 553 | 554 | doc["key"] = command->key; 555 | if (command->numeric) 556 | doc["value"] = getDoubleFromVector(command); 557 | else 558 | doc["value"] = getStringFromVector(command); 559 | doc["unit"] = command->unit; 560 | doc["name"] = command->name; 561 | doc["age"] = static_cast((millis() - command->last) / 1000); 562 | doc.shrinkToFit(); 563 | serializeJson(doc, payload); 564 | 565 | return payload; 566 | } 567 | 568 | const std::string Store::getValuesJson() const { 569 | std::string payload; 570 | JsonDocument doc; 571 | 572 | JsonArray results = doc["results"].to(); 573 | 574 | if (!allCommandsByKey.empty()) { 575 | size_t index = 0; 576 | uint32_t now = millis(); 577 | 578 | for (const auto& kv : allCommandsByKey) { 579 | const Command& command = kv.second; 580 | JsonArray array = results[index][command.key].to(); 581 | if (command.numeric) 582 | array.add(getDoubleFromVector(&command)); 583 | else 584 | array.add(getStringFromVector(&command)); 585 | 586 | array.add(command.unit); 587 | array.add(command.name); 588 | array.add(static_cast((now - command.last) / 1000)); 589 | index++; 590 | } 591 | } 592 | 593 | if (doc.isNull()) doc.to(); 594 | 595 | doc.shrinkToFit(); 596 | serializeJson(doc, payload); 597 | 598 | return payload; 599 | } 600 | 601 | const std::string Store::isFieldValid(const JsonDocument& doc, 602 | const std::string& field, bool required, 603 | FieldType type) { 604 | JsonObjectConst root = doc.as(); 605 | 606 | // Check if the required field exists 607 | if (required && !root[field.c_str()].is()) 608 | return "Missing required field: " + field; 609 | 610 | // Skip type checking if the field is not present and not required 611 | if (!root[field.c_str()].is() || 612 | root[field.c_str()].isNull()) 613 | return ""; // not present and not required => ok 614 | 615 | JsonVariantConst v = root[field.c_str()]; 616 | 617 | switch (type) { 618 | case FT_String: { 619 | if (!v.is()) return "Invalid type for field: " + field; 620 | } break; 621 | case FT_HexString: { 622 | if (!v.is()) return "Invalid type for field: " + field; 623 | const std::string hexStr = v.as(); 624 | if (!hexStr.empty()) { 625 | std::regex hexRegex(R"(^([0-9A-Fa-f]{2})+$)"); 626 | if (!std::regex_match(hexStr, hexRegex)) 627 | return "Invalid hex string for field: " + field; 628 | } 629 | } break; 630 | case FT_Bool: { 631 | if (!v.is()) return "Invalid type for field: " + field; 632 | } break; 633 | case FT_Int: { 634 | if (!v.is()) return "Invalid type for field: " + field; 635 | } break; 636 | case FT_Float: { 637 | if (!v.is() && !v.is() && !v.is()) 638 | return "Invalid type for field: " + field; 639 | } break; 640 | case FT_Uint8T: { 641 | if (!v.is()) return "Invalid type for field: " + field; 642 | long val = v.as(); 643 | if (val < 0 || val > 0xFF) return "Out of range for field: " + field; 644 | } break; 645 | case FT_Uint32T: { 646 | if (!v.is()) return "Invalid type for field: " + field; 647 | long val = v.as(); 648 | if (val < 0 || val > UINT32_MAX) 649 | return "Out of range for field: " + field; 650 | } break; 651 | case FT_SizeT: { 652 | if (!v.is()) return "Invalid type for field: " + field; 653 | long val = v.as(); 654 | if (val < 0 || val > SIZE_MAX) return "Out of range for field: " + field; 655 | } break; 656 | case FT_DataType: { 657 | if (!v.is() || 658 | ebus::string_2_datatype(v.as()) == ebus::DataType::ERROR) 659 | return "Invalid datatype for field: " + field; 660 | } break; 661 | case FT_KeyValueMap: { 662 | if (!v.is()) return "Invalid type for field: " + field; 663 | return isKeyValueMapValid(v.as()); 664 | } break; 665 | } 666 | 667 | return ""; 668 | } 669 | 670 | const std::string Store::isKeyValueMapValid( 671 | const JsonObjectConst ha_key_value_map) { 672 | for (JsonPairConst kv : ha_key_value_map) { 673 | // Check if the key can be converted to an integer 674 | try { 675 | std::stoi(kv.key().c_str()); 676 | } catch (const std::invalid_argument& e) { 677 | return "Invalid key: " + std::string(kv.key().c_str()); 678 | } catch (const std::out_of_range& e) { 679 | return "Key out of range: " + std::string(kv.key().c_str()); 680 | } 681 | if (!kv.value().is()) return "Invalid value type in map"; 682 | } 683 | return ""; // Passed key-value map evaluation checks 684 | } 685 | 686 | const std::string Store::serializeCommands() const { 687 | std::string payload; 688 | JsonDocument doc; 689 | 690 | // Define field names (order matters) 691 | std::vector fields = { 692 | // Command Fields 693 | "key", "name", "read_cmd", "write_cmd", "active", "interval", 694 | 695 | // Data Fields 696 | "master", "position", "datatype", "divider", "min", "max", "digits", 697 | "unit", 698 | 699 | // Home Assistant 700 | "ha", "ha_component", "ha_device_class", "ha_entity_category", "ha_mode", 701 | "ha_key_value_map", "ha_default_key", "ha_payload_on", "ha_payload_off", 702 | "ha_state_class", "ha_step"}; 703 | 704 | // Add header as first entry 705 | JsonArray header = doc.add(); 706 | for (const auto& field : fields) header.add(field); 707 | 708 | // Add each command as an array of values in the same order as header 709 | for (const auto& cmd : allCommandsByKey) { 710 | const Command& command = cmd.second; 711 | JsonArray array = doc.add(); 712 | 713 | // Command Fields 714 | array.add(command.key); 715 | array.add(command.name); 716 | array.add(ebus::to_string(command.read_cmd)); 717 | array.add(ebus::to_string(command.write_cmd)); 718 | array.add(command.active); 719 | array.add(command.interval); 720 | 721 | // Data Fields 722 | array.add(command.master); 723 | array.add(command.position); 724 | array.add(ebus::datatype_2_string(command.datatype)); 725 | array.add(command.divider); 726 | array.add(command.min); 727 | array.add(command.max); 728 | array.add(command.digits); 729 | array.add(command.unit); 730 | 731 | // Home Assistant 732 | array.add(command.ha); 733 | array.add(command.ha_component); 734 | array.add(command.ha_device_class); 735 | array.add(command.ha_entity_category); 736 | array.add(command.ha_mode); 737 | 738 | JsonObject ha_key_value_map = array.add(); 739 | for (const auto& kv : command.ha_key_value_map) 740 | ha_key_value_map[std::to_string(kv.first)] = kv.second; 741 | 742 | array.add(command.ha_default_key); 743 | array.add(command.ha_payload_on); 744 | array.add(command.ha_payload_off); 745 | array.add(command.ha_state_class); 746 | array.add(command.ha_step); 747 | } 748 | 749 | doc.shrinkToFit(); 750 | serializeJson(doc, payload); 751 | return payload; 752 | } 753 | 754 | void Store::deserializeCommands(const char* payload) { 755 | JsonDocument doc; 756 | DeserializationError error = deserializeJson(doc, payload); 757 | 758 | if (!error) { 759 | JsonArray array = doc.as(); 760 | if (array.size() < 2) return; // Need at least header + one command 761 | 762 | // Read header 763 | JsonArray header = array[0]; 764 | std::vector fields; 765 | for (JsonVariant v : header) fields.push_back(v.as()); 766 | 767 | // Read each command 768 | for (size_t i = 1; i < array.size(); ++i) { 769 | JsonArray values = array[i]; 770 | JsonDocument tmpDoc; 771 | for (size_t j = 0; j < fields.size() && j < values.size(); ++j) { 772 | // Special handling for 'ha_key_value_map' 773 | if (fields[j] == "ha_key_value_map") { 774 | JsonObjectConst kvObject = values[j].as(); 775 | JsonObject ha_key_value_map = 776 | tmpDoc["ha_key_value_map"].to(); 777 | 778 | for (JsonPairConst kv : kvObject) 779 | ha_key_value_map[kv.key()] = kv.value(); 780 | } else { 781 | tmpDoc[fields[j]] = values[j]; 782 | } 783 | } 784 | std::string evalError = store.evaluateCommand(tmpDoc); 785 | if (evalError.empty()) insertCommand(createCommand(tmpDoc)); 786 | } 787 | } 788 | } 789 | 790 | #endif 791 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "main.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #if defined(EBUS_INTERNAL) 9 | #include 10 | 11 | #include "log.hpp" 12 | #include "mqtt.hpp" 13 | #include "mqttha.hpp" 14 | #include "schedule.hpp" 15 | #include "track.hpp" 16 | #else 17 | #include "bus.hpp" 18 | #endif 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #include "client.hpp" 25 | #include "esp32c3/rom/rtc.h" 26 | #include "esp_sntp.h" 27 | #include "http.hpp" 28 | 29 | HTTPUpdateServer httpUpdater; 30 | 31 | #if !defined(EBUS_INTERNAL) 32 | TaskHandle_t Task1; 33 | #endif 34 | 35 | Preferences preferences; 36 | 37 | // minimum time of reset pin 38 | #define RESET_MS 1000 39 | 40 | // PWM 41 | #define PWM_CHANNEL 0 42 | #define PWM_FREQ 10000 43 | #define PWM_RESOLUTION 8 44 | 45 | // mDNS 46 | #define HOSTNAME "esp-eBus" 47 | 48 | // IotWebConf 49 | // adjust this if the iotwebconf structure has changed 50 | #define CONFIG_VERSION "eeb" 51 | 52 | #define STRING_LEN 64 53 | #define DNS_LEN 255 54 | #define NUMBER_LEN 8 55 | 56 | #define DEFAULT_APMODE_PASS "ebusebus" 57 | #define DEFAULT_AP "ebus-test" 58 | #define DEFAULT_PASS "lectronz" 59 | 60 | #define DUMMY_STATIC_IP "192.168.1.180" 61 | #define DUMMY_GATEWAY "192.168.1.1" 62 | #define DUMMY_NETMASK "255.255.255.0" 63 | 64 | #define DUMMY_SNTP_SERVER "pool.ntp.org" 65 | #define DUMMY_SNTP_TIMEZONE "UTC0" 66 | 67 | #define DUMMY_MQTT_SERVER DUMMY_GATEWAY 68 | #define DUMMY_MQTT_USER "roger" 69 | #define DUMMY_MQTT_PASS "password" 70 | 71 | char unique_id[7]{}; 72 | 73 | DNSServer dnsServer; 74 | 75 | char staticIPValue[STRING_LEN]; 76 | char ipAddressValue[STRING_LEN]; 77 | char gatewayValue[STRING_LEN]; 78 | char netmaskValue[STRING_LEN]; 79 | 80 | char sntpEnabled[STRING_LEN]; 81 | char sntpServer[DNS_LEN]; 82 | char sntpTimezone[STRING_LEN]; 83 | 84 | uint32_t pwm; 85 | char pwm_value[NUMBER_LEN]; 86 | 87 | #if defined(EBUS_INTERNAL) 88 | char ebus_address[NUMBER_LEN]; 89 | static char ebus_address_values[][NUMBER_LEN] = { 90 | "00", "10", "30", "70", "f0", "01", "11", "31", "71", 91 | "f1", "03", "13", "33", "73", "f3", "07", "17", "37", 92 | "77", "f7", "0f", "1f", "3f", "7f", "ff"}; 93 | char busisr_window[NUMBER_LEN]; 94 | char busisr_offset[NUMBER_LEN]; 95 | 96 | char inquiryOfExistenceValue[STRING_LEN]; 97 | char scanOnStartupValue[STRING_LEN]; 98 | char command_distance[NUMBER_LEN]; 99 | 100 | char mqtt_enabled[STRING_LEN]; 101 | char mqtt_server[STRING_LEN]; 102 | char mqtt_user[STRING_LEN]; 103 | char mqtt_pass[STRING_LEN]; 104 | 105 | char mqttPublishCounterValue[STRING_LEN]; 106 | char mqttPublishTimingValue[STRING_LEN]; 107 | 108 | char haEnabledValue[STRING_LEN]; 109 | #endif 110 | 111 | IotWebConf iotWebConf(HOSTNAME, &dnsServer, &configServer, DEFAULT_APMODE_PASS, 112 | CONFIG_VERSION); 113 | 114 | iotwebconf::ParameterGroup connGroup = 115 | iotwebconf::ParameterGroup("conn", "Connection parameters"); 116 | iotwebconf::CheckboxParameter staticIPParam = iotwebconf::CheckboxParameter( 117 | "Static IP", "staticIPParam", staticIPValue, STRING_LEN); 118 | iotwebconf::TextParameter ipAddressParam = iotwebconf::TextParameter( 119 | "IP address", "ipAddress", ipAddressValue, STRING_LEN, "", DUMMY_STATIC_IP); 120 | iotwebconf::TextParameter gatewayParam = iotwebconf::TextParameter( 121 | "Gateway", "gateway", gatewayValue, STRING_LEN, "", DUMMY_GATEWAY); 122 | iotwebconf::TextParameter netmaskParam = iotwebconf::TextParameter( 123 | "Subnet mask", "netmask", netmaskValue, STRING_LEN, "", DUMMY_NETMASK); 124 | 125 | #if defined(EBUS_INTERNAL) 126 | iotwebconf::ParameterGroup sntpGroup = 127 | iotwebconf::ParameterGroup("sntp", "SNTP configuration"); 128 | iotwebconf::CheckboxParameter sntpEnabledParam = iotwebconf::CheckboxParameter( 129 | "SNTP enabled", "sntpEnabled", sntpEnabled, STRING_LEN); 130 | iotwebconf::TextParameter sntpServerParam = iotwebconf::TextParameter( 131 | "SNTP server", "sntpServer", sntpServer, DNS_LEN, "", DUMMY_SNTP_SERVER); 132 | iotwebconf::TextParameter sntpTimezoneParam = 133 | iotwebconf::TextParameter("SNTP timezone", "sntpTimezone", sntpTimezone, 134 | STRING_LEN, "", DUMMY_SNTP_TIMEZONE); 135 | #endif 136 | 137 | iotwebconf::ParameterGroup ebusGroup = 138 | iotwebconf::ParameterGroup("ebus", "eBUS configuration"); 139 | iotwebconf::NumberParameter pwmParam = 140 | iotwebconf::NumberParameter("PWM value", "pwm_value", pwm_value, NUMBER_LEN, 141 | "130", "1..255", "min='1' max='255' step='1'"); 142 | #if defined(EBUS_INTERNAL) 143 | iotwebconf::SelectParameter ebusAddressParam = iotwebconf::SelectParameter( 144 | "eBUS address", "ebus_address", ebus_address, NUMBER_LEN, 145 | reinterpret_cast(ebus_address_values), 146 | reinterpret_cast(ebus_address_values), 147 | sizeof(ebus_address_values) / NUMBER_LEN, NUMBER_LEN, "ff"); 148 | iotwebconf::NumberParameter busIsrWindowParam = iotwebconf::NumberParameter( 149 | "Bus ISR window (micro seconds)", "busisr_window", busisr_window, 150 | NUMBER_LEN, "4300", "4250..4500", "min='4250' max='4500' step='1'"); 151 | iotwebconf::NumberParameter busIsrOffsetParam = iotwebconf::NumberParameter( 152 | "Bus ISR offset (micro seconds)", "busisr_offset", busisr_offset, 153 | NUMBER_LEN, "80", "0..200", "min='0' max='200' step='1'"); 154 | 155 | iotwebconf::ParameterGroup scheduleGroup = 156 | iotwebconf::ParameterGroup("schedule", "Schedule configuration"); 157 | iotwebconf::CheckboxParameter inquiryOfExistenceParam = 158 | iotwebconf::CheckboxParameter("Send an inquiry of existence command", 159 | "inquiryOfExistenceParam", 160 | inquiryOfExistenceValue, STRING_LEN); 161 | iotwebconf::CheckboxParameter scanOnStartupParam = 162 | iotwebconf::CheckboxParameter("Scan for eBUS participants on startup", 163 | "scanOnStartupParam", scanOnStartupValue, 164 | STRING_LEN); 165 | iotwebconf::NumberParameter commandDistanceParam = iotwebconf::NumberParameter( 166 | "Command distance (seconds)", "command_distance", command_distance, 167 | NUMBER_LEN, "1", "1..60", "min='1' max='60' step='1'"); 168 | 169 | iotwebconf::ParameterGroup mqttGroup = 170 | iotwebconf::ParameterGroup("mqtt", "MQTT configuration"); 171 | iotwebconf::CheckboxParameter mqttEnabledParam = iotwebconf::CheckboxParameter( 172 | "MQTT enabled", "mqttEnabledParam", mqtt_enabled, STRING_LEN); 173 | iotwebconf::TextParameter mqttServerParam = 174 | iotwebconf::TextParameter("MQTT server", "mqtt_server", mqtt_server, 175 | STRING_LEN, "", DUMMY_MQTT_SERVER); 176 | iotwebconf::TextParameter mqttUserParam = iotwebconf::TextParameter( 177 | "MQTT user", "mqtt_user", mqtt_user, STRING_LEN, "", DUMMY_MQTT_USER); 178 | iotwebconf::PasswordParameter mqttPasswordParam = iotwebconf::PasswordParameter( 179 | "MQTT password", "mqtt_pass", mqtt_pass, STRING_LEN, "", DUMMY_MQTT_PASS); 180 | 181 | iotwebconf::CheckboxParameter mqttPublishCounterParam = 182 | iotwebconf::CheckboxParameter("Publish Counter to MQTT", 183 | "mqttPublishCounterParam", 184 | mqttPublishCounterValue, STRING_LEN); 185 | iotwebconf::CheckboxParameter mqttPublishTimingParam = 186 | iotwebconf::CheckboxParameter("Publish Timing to MQTT", 187 | "mqttPublishTimingParam", 188 | mqttPublishTimingValue, STRING_LEN); 189 | 190 | iotwebconf::ParameterGroup haGroup = 191 | iotwebconf::ParameterGroup("ha", "Home Assistant configuration"); 192 | iotwebconf::CheckboxParameter haEnabledParam = iotwebconf::CheckboxParameter( 193 | "Home Assistant enabled", "haEnabledParam", haEnabledValue, STRING_LEN); 194 | #endif 195 | 196 | IPAddress ipAddress; 197 | IPAddress gateway; 198 | IPAddress netmask; 199 | 200 | #if !defined(EBUS_INTERNAL) 201 | WiFiServer wifiServer(3333); 202 | WiFiClient wifiClients[MAX_WIFI_CLIENTS]; 203 | 204 | WiFiServer wifiServerEnhanced(3335); 205 | WiFiClient wifiClientsEnhanced[MAX_WIFI_CLIENTS]; 206 | 207 | WiFiServer wifiServerReadOnly(3334); 208 | WiFiClient wifiClientsReadOnly[MAX_WIFI_CLIENTS]; 209 | #endif 210 | 211 | WiFiServer statusServer(5555); 212 | 213 | volatile uint32_t last_comms = 0; 214 | 215 | // status 216 | uint32_t reset_code = 0; 217 | #if defined(EBUS_INTERNAL) 218 | Track uptime("state/uptime", 10); 219 | Track free_heap("state/free_heap", 10); 220 | Track loopDuration("state/loop_duration", 10); 221 | #else 222 | uint32_t uptime = 0; 223 | uint32_t free_heap = 0; 224 | uint32_t loopDuration = 0; 225 | #endif 226 | uint32_t maxLoopDuration; 227 | 228 | // wifi 229 | uint32_t last_connect = 0; 230 | int reconnect_count = 0; 231 | 232 | #if defined(EBUS_INTERNAL) 233 | // mqtt 234 | bool needMqttConnect = false; 235 | int mqtt_reconnect_count = 0; 236 | uint32_t lastMqttConnectionAttempt = 0; 237 | uint32_t lastMqttUpdate = 0; 238 | 239 | bool connectMqtt() { 240 | if (mqtt.connected()) return true; 241 | 242 | if (1000 > millis() - lastMqttConnectionAttempt) return false; 243 | 244 | mqtt.connect(); 245 | 246 | if (!mqtt.connected()) { 247 | lastMqttConnectionAttempt = millis(); 248 | return false; 249 | } 250 | 251 | return true; 252 | } 253 | #endif 254 | 255 | void wifiConnected() { 256 | last_connect = millis(); 257 | ++reconnect_count; 258 | #if defined(EBUS_INTERNAL) 259 | needMqttConnect = true; 260 | #endif 261 | } 262 | 263 | void wdt_start() { esp_task_wdt_init(6, true); } 264 | 265 | void wdt_feed() { esp_task_wdt_reset(); } 266 | 267 | inline void disableTX() { 268 | #if defined(TX_DISABLE_PIN) 269 | pinMode(TX_DISABLE_PIN, OUTPUT); 270 | digitalWrite(TX_DISABLE_PIN, HIGH); 271 | #endif 272 | } 273 | 274 | inline void enableTX() { 275 | #if defined(TX_DISABLE_PIN) 276 | digitalWrite(TX_DISABLE_PIN, LOW); 277 | #endif 278 | } 279 | 280 | void set_pwm(uint8_t value) { 281 | #if defined(PWM_PIN) 282 | ledcWrite(PWM_CHANNEL, value); 283 | #if defined(EBUS_INTERNAL) 284 | schedule.resetCounter(); 285 | schedule.resetTiming(); 286 | #endif 287 | #endif 288 | } 289 | 290 | uint32_t get_pwm() { 291 | #if defined(PWM_PIN) 292 | return ledcRead(PWM_CHANNEL); 293 | #else 294 | return 0; 295 | #endif 296 | } 297 | 298 | void calcUniqueId() { 299 | uint32_t id = 0; 300 | for (int i = 0; i < 6; ++i) { 301 | id |= ((ESP.getEfuseMac() >> (8 * (5 - i))) & 0xff) << (8 * i); 302 | } 303 | char tmp[9]{}; 304 | snprintf(tmp, sizeof(tmp), "%08x", id); 305 | strncpy(unique_id, &tmp[2], 6); 306 | } 307 | 308 | void restart() { 309 | disableTX(); 310 | ESP.restart(); 311 | } 312 | 313 | void check_reset() { 314 | // check if RESET_PIN being hold low and reset 315 | pinMode(RESET_PIN, INPUT_PULLUP); 316 | uint32_t resetStart = millis(); 317 | while (digitalRead(RESET_PIN) == 0) { 318 | if (millis() > resetStart + RESET_MS) { 319 | preferences.clear(); 320 | restart(); 321 | } 322 | } 323 | } 324 | 325 | void updateLastComms() { last_comms = millis(); } 326 | 327 | void loop_duration() { 328 | static uint32_t lastTime = 0; 329 | uint32_t now = micros(); 330 | uint32_t delta = now - lastTime; 331 | float alpha = 0.3; 332 | 333 | lastTime = now; 334 | 335 | #if defined(EBUS_INTERNAL) 336 | loopDuration = ((1 - alpha) * loopDuration.value() + (alpha * delta)); 337 | #else 338 | loopDuration = ((1 - alpha) * loopDuration + (alpha * delta)); 339 | #endif 340 | 341 | if (delta > maxLoopDuration) { 342 | maxLoopDuration = delta; 343 | } 344 | } 345 | 346 | #if !defined(EBUS_INTERNAL) 347 | void data_process() { 348 | loop_duration(); 349 | 350 | // check clients for data 351 | for (int i = 0; i < MAX_WIFI_CLIENTS; i++) { 352 | handleClient(&wifiClients[i]); 353 | handleClientEnhanced(&wifiClientsEnhanced[i]); 354 | } 355 | 356 | // check queue for data 357 | BusType::data d; 358 | if (Bus.read(d)) { 359 | for (int i = 0; i < MAX_WIFI_CLIENTS; i++) { 360 | if (d._enhanced) { 361 | if (d._client == &wifiClientsEnhanced[i]) { 362 | if (pushClientEnhanced(&wifiClientsEnhanced[i], d._c, d._d, true)) { 363 | updateLastComms(); 364 | } 365 | } 366 | } else { 367 | if (pushClient(&wifiClients[i], d._d)) { 368 | updateLastComms(); 369 | } 370 | if (pushClient(&wifiClientsReadOnly[i], d._d)) { 371 | updateLastComms(); 372 | } 373 | if (d._client != &wifiClientsEnhanced[i]) { 374 | if (pushClientEnhanced(&wifiClientsEnhanced[i], d._c, d._d, 375 | d._logtoclient == &wifiClientsEnhanced[i])) { 376 | updateLastComms(); 377 | } 378 | } 379 | } 380 | } 381 | } 382 | } 383 | 384 | void data_loop(void* pvParameters) { 385 | while (1) { 386 | data_process(); 387 | } 388 | } 389 | #endif 390 | 391 | #if defined(EBUS_INTERNAL) 392 | void time_sync_notification_cb(struct timeval* tv) { 393 | addLog("SNTP synchronized to " + String(sntpServer)); 394 | } 395 | 396 | void initSNTP(const char* server) { 397 | sntp_set_sync_interval(1 * 60 * 60 * 1000UL); // 1 hour 398 | 399 | esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); 400 | esp_sntp_setservername(0, server); 401 | 402 | sntp_set_time_sync_notification_cb(time_sync_notification_cb); 403 | esp_sntp_init(); 404 | } 405 | 406 | void setTimezone(const char* timezone) { 407 | if (strlen(timezone) > 0) { 408 | addLog("Timezone set to " + String(timezone)); 409 | setenv("TZ", timezone, 1); 410 | tzset(); 411 | } 412 | } 413 | #endif 414 | 415 | bool formValidator(iotwebconf::WebRequestWrapper* webRequestWrapper) { 416 | bool valid = true; 417 | 418 | if (webRequestWrapper->arg(staticIPParam.getId()).equals("selected")) { 419 | if (!ipAddress.fromString(webRequestWrapper->arg(ipAddressParam.getId()))) { 420 | ipAddressParam.errorMessage = "Please provide a valid IP address!"; 421 | valid = false; 422 | } 423 | if (!netmask.fromString(webRequestWrapper->arg(netmaskParam.getId()))) { 424 | netmaskParam.errorMessage = "Please provide a valid netmask!"; 425 | valid = false; 426 | } 427 | if (!gateway.fromString(webRequestWrapper->arg(gatewayParam.getId()))) { 428 | gatewayParam.errorMessage = "Please provide a valid gateway address!"; 429 | valid = false; 430 | } 431 | } 432 | #if defined(EBUS_INTERNAL) 433 | if (webRequestWrapper->arg(sntpServerParam.getId()).length() > DNS_LEN - 1) { 434 | String tmp = "max. "; 435 | tmp += String(DNS_LEN); 436 | tmp += " characters allowed"; 437 | sntpServerParam.errorMessage = tmp.c_str(); 438 | valid = false; 439 | } 440 | 441 | if (webRequestWrapper->arg(sntpTimezoneParam.getId()).length() > 442 | STRING_LEN - 1) { 443 | String tmp = "max. "; 444 | tmp += String(STRING_LEN); 445 | tmp += " characters allowed"; 446 | sntpTimezoneParam.errorMessage = tmp.c_str(); 447 | valid = false; 448 | } 449 | 450 | if (webRequestWrapper->arg(mqttServerParam.getId()).length() > 451 | STRING_LEN - 1) { 452 | String tmp = "max. "; 453 | tmp += String(STRING_LEN); 454 | tmp += " characters allowed"; 455 | mqttServerParam.errorMessage = tmp.c_str(); 456 | valid = false; 457 | } 458 | #endif 459 | 460 | return valid; 461 | } 462 | 463 | void saveParamsCallback() { 464 | set_pwm(atoi(pwm_value)); 465 | pwm = get_pwm(); 466 | 467 | #if defined(EBUS_INTERNAL) 468 | ebus::handler->setSourceAddress( 469 | uint8_t(std::strtoul(ebus_address, nullptr, 16))); 470 | ebus::setBusIsrWindow(atoi(busisr_window)); 471 | ebus::setBusIsrOffset(atoi(busisr_offset)); 472 | 473 | if (sntpEnabledParam.isChecked()) { 474 | esp_sntp_stop(); 475 | initSNTP(sntpServer); 476 | setTimezone(sntpTimezone); 477 | } else { 478 | esp_sntp_stop(); 479 | } 480 | 481 | schedule.setSendInquiryOfExistence(inquiryOfExistenceParam.isChecked()); 482 | schedule.setScanOnStartup(scanOnStartupParam.isChecked()); 483 | schedule.setDistance(atoi(command_distance)); 484 | 485 | mqtt.setEnabled(mqttEnabledParam.isChecked()); 486 | if (!mqtt.isEnabled() && mqtt.connected()) mqtt.disconnect(); 487 | mqtt.setServer(mqtt_server, 1883); 488 | mqtt.setCredentials(mqtt_user, mqtt_pass); 489 | 490 | schedule.setPublishCounter(mqttPublishCounterParam.isChecked()); 491 | schedule.setPublishTiming(mqttPublishTimingParam.isChecked()); 492 | 493 | mqttha.setEnabled(haEnabledParam.isChecked()); 494 | mqttha.publishDeviceInfo(); 495 | mqttha.publishComponents(); 496 | #endif 497 | } 498 | 499 | void connectWifi(const char* ssid, const char* password) { 500 | if (staticIPParam.isChecked()) { 501 | bool valid = true; 502 | valid = valid && ipAddress.fromString(String(ipAddressValue)); 503 | valid = valid && netmask.fromString(String(netmaskValue)); 504 | valid = valid && gateway.fromString(String(gatewayValue)); 505 | 506 | if (valid) WiFi.config(ipAddress, gateway, netmask); 507 | } 508 | 509 | WiFi.begin(ssid, password); 510 | } 511 | 512 | char* status_string() { 513 | const size_t bufferSize = 1024; 514 | static char status[bufferSize]; 515 | 516 | size_t pos = 0; 517 | 518 | #if !defined(EBUS_INTERNAL) 519 | pos += snprintf(status + pos, bufferSize - pos, "async_mode: %s\n", 520 | USE_ASYNCHRONOUS ? "true" : "false"); 521 | pos += snprintf(status + pos, bufferSize - pos, "software_serial_mode: %s\n", 522 | USE_SOFTWARE_SERIAL ? "true" : "false"); 523 | #endif 524 | pos += snprintf(status + pos, bufferSize - pos, "unique_id: %s\n", unique_id); 525 | pos += snprintf(status + pos, bufferSize - pos, "clock_speed: %u Mhz\n", 526 | getCpuFrequencyMhz()); 527 | pos += snprintf(status + pos, bufferSize - pos, "apb_speed: %u Hz\n", 528 | getApbFrequency()); 529 | pos += snprintf(status + pos, bufferSize - pos, "uptime: %ld ms\n", millis()); 530 | pos += snprintf(status + pos, bufferSize - pos, "last_connect_time: %u ms\n", 531 | last_connect); 532 | pos += snprintf(status + pos, bufferSize - pos, "reconnect_count: %d \n", 533 | reconnect_count); 534 | pos += 535 | snprintf(status + pos, bufferSize - pos, "rssi: %d dBm\n", WiFi.RSSI()); 536 | #if defined(EBUS_INTERNAL) 537 | pos += snprintf(status + pos, bufferSize - pos, "free_heap: %u B\n", 538 | free_heap.value()); 539 | #else 540 | pos += 541 | snprintf(status + pos, bufferSize - pos, "free_heap: %u B\n", free_heap); 542 | #endif 543 | pos += 544 | snprintf(status + pos, bufferSize - pos, "reset_code: %u\n", reset_code); 545 | 546 | #if defined(EBUS_INTERNAL) 547 | pos += snprintf(status + pos, bufferSize - pos, "loop_duration: %u us\r\n", 548 | loopDuration.value()); 549 | #else 550 | pos += snprintf(status + pos, bufferSize - pos, "loop_duration: %u us\r\n", 551 | loopDuration); 552 | #endif 553 | pos += snprintf(status + pos, bufferSize - pos, 554 | "max_loop_duration: %u us\r\n", maxLoopDuration); 555 | pos += 556 | snprintf(status + pos, bufferSize - pos, "version: %s\r\n", AUTO_VERSION); 557 | 558 | #if defined(EBUS_INTERNAL) 559 | pos += snprintf(status + pos, bufferSize - pos, "sntpEnabled: %s\r\n", 560 | sntpEnabledParam.isChecked() ? "true" : "false"); 561 | pos += snprintf(status + pos, bufferSize - pos, "sntpServer: %s\r\n", 562 | sntpServer); 563 | pos += snprintf(status + pos, bufferSize - pos, "sntpTimezone: %s\r\n", 564 | sntpTimezone); 565 | #endif 566 | 567 | pos += 568 | snprintf(status + pos, bufferSize - pos, "pwm_value: %u\r\n", get_pwm()); 569 | 570 | #if defined(EBUS_INTERNAL) 571 | pos += snprintf(status + pos, bufferSize - pos, "ebus_address: %s\r\n", 572 | ebus_address); 573 | pos += snprintf(status + pos, bufferSize - pos, "busisr_window: %i us\r\n", 574 | atoi(busisr_window)); 575 | pos += snprintf(status + pos, bufferSize - pos, "busisr_offset: %i us\r\n", 576 | atoi(busisr_offset)); 577 | 578 | pos += 579 | snprintf(status + pos, bufferSize - pos, "inquiry_of_existence: %s\r\n", 580 | inquiryOfExistenceParam.isChecked() ? "true" : "false"); 581 | pos += snprintf(status + pos, bufferSize - pos, "scan_on_startup: %s\r\n", 582 | scanOnStartupParam.isChecked() ? "true" : "false"); 583 | pos += snprintf(status + pos, bufferSize - pos, "command_distance: %i\r\n", 584 | atoi(command_distance)); 585 | pos += snprintf(status + pos, bufferSize - pos, "active_commands: %zu\r\n", 586 | store.getActiveCommands()); 587 | pos += snprintf(status + pos, bufferSize - pos, "passive_commands: %zu\r\n", 588 | store.getPassiveCommands()); 589 | 590 | pos += snprintf(status + pos, bufferSize - pos, "mqtt_enabled: %s\r\n", 591 | mqttEnabledParam.isChecked() ? "true" : "false"); 592 | pos += snprintf(status + pos, bufferSize - pos, "mqtt_connected: %s\r\n", 593 | mqtt.connected() ? "true" : "false"); 594 | pos += snprintf(status + pos, bufferSize - pos, "mqtt_reconnect_count: %d \n", 595 | mqtt_reconnect_count); 596 | pos += snprintf(status + pos, bufferSize - pos, "mqtt_server: %s\r\n", 597 | mqtt_server); 598 | pos += 599 | snprintf(status + pos, bufferSize - pos, "mqtt_user: %s\r\n", mqtt_user); 600 | pos += 601 | snprintf(status + pos, bufferSize - pos, "mqtt_publish_counter: %s\r\n", 602 | mqttPublishCounterParam.isChecked() ? "true" : "false"); 603 | pos += snprintf(status + pos, bufferSize - pos, "mqtt_publish_timing: %s\r\n", 604 | mqttPublishTimingParam.isChecked() ? "true" : "false"); 605 | 606 | pos += snprintf(status + pos, bufferSize - pos, "ha_enabled: %s\r\n", 607 | haEnabledParam.isChecked() ? "true" : "false"); 608 | #endif 609 | 610 | if (pos >= bufferSize) status[bufferSize - 1] = '\0'; 611 | 612 | return status; 613 | } 614 | 615 | const std::string getStatusJson() { 616 | std::string payload; 617 | JsonDocument doc; 618 | 619 | JsonObject Status = doc["Status"].to(); 620 | Status["Reset_Code"] = reset_code; 621 | #if defined(EBUS_INTERNAL) 622 | Status["Uptime"] = uptime.value(); 623 | Status["Free_Heap"] = free_heap.value(); 624 | Status["Loop_Duration"] = loopDuration.value(); 625 | #else 626 | Status["Uptime"] = uptime; 627 | Status["Free_Heap"] = free_heap; 628 | Status["Loop_Duration"] = loopDuration; 629 | #endif 630 | Status["Loop_Duration_Max"] = maxLoopDuration; 631 | 632 | #if !defined(EBUS_INTERNAL) 633 | // Arbitration 634 | JsonObject Arbitration = doc["Arbitration"].to(); 635 | Arbitration["Total"] = static_cast(Bus._nbrArbitrations); 636 | Arbitration["Restarts1"] = static_cast(Bus._nbrRestarts1); 637 | Arbitration["Restarts2"] = static_cast(Bus._nbrRestarts2); 638 | Arbitration["Won1"] = static_cast(Bus._nbrWon1); 639 | Arbitration["Won2"] = static_cast(Bus._nbrWon2); 640 | Arbitration["Lost1"] = static_cast(Bus._nbrLost1); 641 | Arbitration["Lost2"] = static_cast(Bus._nbrLost2); 642 | Arbitration["Late"] = static_cast(Bus._nbrLate); 643 | Arbitration["Errors"] = static_cast(Bus._nbrErrors); 644 | #endif 645 | 646 | // Firmware 647 | JsonObject Firmware = doc["Firmware"].to(); 648 | Firmware["Version"] = AUTO_VERSION; 649 | Firmware["SDK"] = ESP.getSdkVersion(); 650 | #if !defined(EBUS_INTERNAL) 651 | Firmware["Async"] = USE_ASYNCHRONOUS ? true : false; 652 | Firmware["Software_Serial"] = USE_SOFTWARE_SERIAL ? true : false; 653 | #endif 654 | Firmware["Unique_ID"] = unique_id; 655 | Firmware["Clock_Speed"] = getCpuFrequencyMhz(); 656 | Firmware["Apb_Speed"] = getApbFrequency(); 657 | 658 | // WIFI 659 | JsonObject WIFI = doc["WIFI"].to(); 660 | WIFI["Last_Connect"] = last_connect; 661 | WIFI["Reconnect_Count"] = reconnect_count; 662 | WIFI["RSSI"] = WiFi.RSSI(); 663 | 664 | if (staticIPParam.isChecked()) { 665 | WIFI["Static_IP"] = true; 666 | WIFI["IP_Address"] = ipAddress.toString(); 667 | WIFI["Gateway"] = gateway.toString(); 668 | WIFI["Netmask"] = netmask.toString(); 669 | } else { 670 | WIFI["Static_IP"] = false; 671 | WIFI["IP_Address"] = WiFi.localIP().toString(); 672 | WIFI["Gateway"] = WiFi.gatewayIP().toString(); 673 | WIFI["Netmask"] = WiFi.subnetMask().toString(); 674 | } 675 | WIFI["SSID"] = WiFi.SSID(); 676 | WIFI["BSSID"] = WiFi.BSSIDstr(); 677 | WIFI["Channel"] = WiFi.channel(); 678 | WIFI["Hostname"] = WiFi.getHostname(); 679 | WIFI["MAC_Address"] = WiFi.macAddress(); 680 | 681 | // SNTP 682 | #if defined(EBUS_INTERNAL) 683 | JsonObject SNTP = doc["SNTP"].to(); 684 | SNTP["Enabled"] = sntpEnabledParam.isChecked(); 685 | SNTP["Server"] = sntpServer; 686 | SNTP["Timezone"] = sntpTimezone; 687 | #endif 688 | 689 | // eBUS 690 | JsonObject eBUS = doc["eBUS"].to(); 691 | eBUS["PWM"] = get_pwm(); 692 | #if defined(EBUS_INTERNAL) 693 | eBUS["Ebus_Address"] = ebus_address; 694 | eBUS["BusIsr_Window"] = atoi(busisr_window); 695 | eBUS["BusIsr_Offset"] = atoi(busisr_offset); 696 | 697 | // Schedule 698 | JsonObject Schedule = doc["Schedule"].to(); 699 | Schedule["Inquiry_Of_Existence"] = inquiryOfExistenceParam.isChecked(); 700 | Schedule["Scan_On_Startup"] = scanOnStartupParam.isChecked(); 701 | Schedule["Command_Distance"] = atoi(command_distance); 702 | Schedule["Active_Commands"] = store.getActiveCommands(); 703 | Schedule["Passive_Commands"] = store.getPassiveCommands(); 704 | 705 | // MQTT 706 | JsonObject MQTT = doc["MQTT"].to(); 707 | MQTT["Enabled"] = mqttEnabledParam.isChecked(); 708 | MQTT["Server"] = mqtt_server; 709 | MQTT["User"] = mqtt_user; 710 | MQTT["Connected"] = mqtt.connected(); 711 | MQTT["Reconnect_Count"] = mqtt_reconnect_count; 712 | MQTT["Publish_Counter"] = mqttPublishCounterParam.isChecked(); 713 | MQTT["Publish_Timing"] = mqttPublishTimingParam.isChecked(); 714 | 715 | // HomeAssistant 716 | JsonObject HomeAssistant = doc["Home_Assistant"].to(); 717 | HomeAssistant["Enabled"] = haEnabledParam.isChecked(); 718 | #endif 719 | 720 | doc.shrinkToFit(); 721 | serializeJson(doc, payload); 722 | 723 | return payload; 724 | } 725 | 726 | bool handleStatusServerRequests() { 727 | if (!statusServer.hasClient()) return false; 728 | 729 | WiFiClient client = statusServer.accept(); 730 | 731 | if (client.availableForWrite() >= AVAILABLE_THRESHOLD) { 732 | client.print(status_string()); 733 | client.flush(); 734 | client.stop(); 735 | } 736 | return true; 737 | } 738 | 739 | void setup() { 740 | DebugSer.begin(115200); 741 | DebugSer.setDebugOutput(true); 742 | 743 | preferences.begin("esp-ebus", false); 744 | 745 | check_reset(); 746 | 747 | reset_code = rtc_get_reset_reason(0); 748 | 749 | calcUniqueId(); 750 | 751 | #if defined(EBUS_INTERNAL) 752 | ebus::setupBusIsr(UART_NUM_1, UART_RX, UART_TX, 1, 0); 753 | #else 754 | Bus.begin(); 755 | #endif 756 | 757 | disableTX(); 758 | 759 | #if defined(PWM_PIN) 760 | ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION); 761 | ledcAttachPin(PWM_PIN, PWM_CHANNEL); 762 | #endif 763 | 764 | // IotWebConf 765 | connGroup.addItem(&staticIPParam); 766 | connGroup.addItem(&ipAddressParam); 767 | connGroup.addItem(&gatewayParam); 768 | connGroup.addItem(&netmaskParam); 769 | 770 | #if defined(EBUS_INTERNAL) 771 | sntpGroup.addItem(&sntpEnabledParam); 772 | sntpGroup.addItem(&sntpServerParam); 773 | sntpGroup.addItem(&sntpTimezoneParam); 774 | #endif 775 | 776 | ebusGroup.addItem(&pwmParam); 777 | 778 | #if defined(EBUS_INTERNAL) 779 | ebusGroup.addItem(&ebusAddressParam); 780 | ebusGroup.addItem(&busIsrWindowParam); 781 | ebusGroup.addItem(&busIsrOffsetParam); 782 | 783 | scheduleGroup.addItem(&inquiryOfExistenceParam); 784 | scheduleGroup.addItem(&scanOnStartupParam); 785 | scheduleGroup.addItem(&commandDistanceParam); 786 | 787 | mqttGroup.addItem(&mqttEnabledParam); 788 | mqttGroup.addItem(&mqttServerParam); 789 | mqttGroup.addItem(&mqttUserParam); 790 | mqttGroup.addItem(&mqttPasswordParam); 791 | 792 | mqttGroup.addItem(&mqttPublishCounterParam); 793 | mqttGroup.addItem(&mqttPublishTimingParam); 794 | 795 | haGroup.addItem(&haEnabledParam); 796 | #endif 797 | 798 | iotWebConf.addParameterGroup(&connGroup); 799 | #if defined(EBUS_INTERNAL) 800 | iotWebConf.addParameterGroup(&sntpGroup); 801 | #endif 802 | iotWebConf.addParameterGroup(&ebusGroup); 803 | #if defined(EBUS_INTERNAL) 804 | iotWebConf.addParameterGroup(&scheduleGroup); 805 | iotWebConf.addParameterGroup(&mqttGroup); 806 | iotWebConf.addParameterGroup(&haGroup); 807 | #endif 808 | iotWebConf.setFormValidator(&formValidator); 809 | iotWebConf.setConfigSavedCallback(&saveParamsCallback); 810 | iotWebConf.getApTimeoutParameter()->visible = true; 811 | iotWebConf.setWifiConnectionTimeoutMs(7000); 812 | iotWebConf.setWifiConnectionHandler(&connectWifi); 813 | iotWebConf.setWifiConnectionCallback(&wifiConnected); 814 | 815 | #if defined(STATUS_LED_PIN) 816 | iotWebConf.setStatusPin(STATUS_LED_PIN); 817 | #endif 818 | 819 | if (preferences.getBool("firstboot", true)) { 820 | preferences.putBool("firstboot", false); 821 | 822 | iotWebConf.init(); 823 | strncpy(iotWebConf.getApPasswordParameter()->valueBuffer, 824 | DEFAULT_APMODE_PASS, IOTWEBCONF_WORD_LEN); 825 | strncpy(iotWebConf.getWifiSsidParameter()->valueBuffer, DEFAULT_AP, 826 | IOTWEBCONF_WORD_LEN); 827 | strncpy(iotWebConf.getWifiPasswordParameter()->valueBuffer, DEFAULT_PASS, 828 | IOTWEBCONF_WORD_LEN); 829 | iotWebConf.saveConfig(); 830 | } else { 831 | iotWebConf.skipApStartup(); 832 | // -- Initializing the configuration. 833 | iotWebConf.init(); 834 | } 835 | 836 | SetupHttpHandlers(); 837 | 838 | iotWebConf.setupUpdateServer( 839 | [](const char* updatePath) { 840 | httpUpdater.setup(&configServer, updatePath); 841 | }, 842 | [](const char* userName, char* password) { 843 | httpUpdater.updateCredentials(userName, password); 844 | }); 845 | 846 | set_pwm(atoi(pwm_value)); 847 | 848 | while (iotWebConf.getState() != iotwebconf::NetworkState::OnLine) { 849 | iotWebConf.doLoop(); 850 | } 851 | 852 | #if defined(EBUS_INTERNAL) 853 | if (sntpEnabledParam.isChecked()) { 854 | initSNTP(sntpServer); 855 | setTimezone(sntpTimezone); 856 | } 857 | 858 | mqtt.setEnabled(mqttEnabledParam.isChecked()); 859 | mqtt.setUniqueId(unique_id); 860 | mqtt.setServer(mqtt_server, 1883); 861 | mqtt.setCredentials(mqtt_user, mqtt_pass); 862 | 863 | mqttha.setUniqueId(mqtt.getUniqueId()); 864 | mqttha.setRootTopic(mqtt.getRootTopic()); 865 | mqttha.setWillTopic(mqtt.getWillTopic()); 866 | mqttha.setEnabled(haEnabledParam.isChecked()); 867 | 868 | mqttha.setThingName(iotWebConf.getThingName()); 869 | mqttha.setThingModel(ESP.getChipModel()); 870 | mqttha.setThingModelId("Revision: " + std::to_string(ESP.getChipRevision())); 871 | mqttha.setThingManufacturer("danman.eu"); 872 | mqttha.setThingSwVersion(AUTO_VERSION); 873 | // mqttha.setThingHwVersion(""); // how to determine hardware version? 874 | mqttha.setThingConfigurationUrl( 875 | "http://" + std::string(WiFi.localIP().toString().c_str()) + "/"); 876 | #endif 877 | 878 | #if !defined(EBUS_INTERNAL) 879 | wifiServer.begin(); 880 | wifiServerEnhanced.begin(); 881 | wifiServerReadOnly.begin(); 882 | #endif 883 | 884 | statusServer.begin(); 885 | 886 | ArduinoOTA.begin(); 887 | MDNS.begin(HOSTNAME); 888 | wdt_start(); 889 | 890 | last_comms = millis(); 891 | enableTX(); 892 | 893 | #if defined(EBUS_INTERNAL) 894 | ebus::handler->setSourceAddress( 895 | uint8_t(std::strtoul(ebus_address, nullptr, 16))); 896 | 897 | schedule.setSendInquiryOfExistence(inquiryOfExistenceParam.isChecked()); 898 | schedule.setScanOnStartup(scanOnStartupParam.isChecked()); 899 | schedule.setDistance(atoi(command_distance)); 900 | schedule.setPublishCounter(mqttPublishCounterParam.isChecked()); 901 | schedule.setPublishTiming(mqttPublishTimingParam.isChecked()); 902 | schedule.start(ebus::request, ebus::handler); 903 | 904 | ebus::setBusIsrWindow(atoi(busisr_window)); 905 | ebus::setBusIsrOffset(atoi(busisr_offset)); 906 | 907 | ebus::serviceRunner->start(); 908 | 909 | clientManager.start(ebus::bus, ebus::request, ebus::serviceRunner); 910 | 911 | ArduinoOTA.onStart([]() { 912 | ebus::serviceRunner->stop(); 913 | schedule.stop(); 914 | clientManager.stop(); 915 | }); 916 | 917 | store.loadCommands(); // install saved commands 918 | mqttha.publishComponents(); 919 | #else 920 | xTaskCreate(data_loop, "data_loop", 10000, NULL, 1, &Task1); 921 | ArduinoOTA.onStart([]() { vTaskDelete(Task1); }); 922 | #endif 923 | } 924 | 925 | void loop() { 926 | ArduinoOTA.handle(); 927 | 928 | wdt_feed(); 929 | 930 | iotWebConf.doLoop(); 931 | 932 | #if defined(EBUS_INTERNAL) 933 | if (mqtt.isEnabled()) { 934 | if (needMqttConnect) { 935 | if (connectMqtt()) { 936 | needMqttConnect = false; 937 | ++mqtt_reconnect_count; 938 | } 939 | 940 | } else if ((iotWebConf.getState() == iotwebconf::OnLine) && 941 | (!mqtt.connected())) { 942 | needMqttConnect = true; 943 | } 944 | 945 | if (mqtt.connected()) { 946 | uint32_t currentMillis = millis(); 947 | if (currentMillis > lastMqttUpdate + 5 * 1000) { 948 | lastMqttUpdate = currentMillis; 949 | 950 | schedule.fetchCounter(); 951 | schedule.fetchTiming(); 952 | } 953 | mqtt.doLoop(); 954 | } 955 | } else { 956 | if (mqtt.connected()) { 957 | mqtt.disconnect(); 958 | } 959 | } 960 | #endif 961 | 962 | uptime = millis(); 963 | free_heap = ESP.getFreeHeap(); 964 | 965 | if (millis() > last_comms + 200 * 1000) { 966 | restart(); 967 | } 968 | 969 | // Check if new client on the status server 970 | if (handleStatusServerRequests()) { 971 | #if !defined(EBUS_INTERNAL) 972 | // exclude handleStatusServerRequests from maxLoopDuration calculation 973 | // as it skews the typical loop duration and set maxLoopDuration to 0 974 | loop_duration(); 975 | maxLoopDuration = 0; 976 | #endif 977 | } 978 | 979 | // Check if there are any new clients on the eBUS servers 980 | #if defined(EBUS_INTERNAL) 981 | loop_duration(); 982 | #else 983 | handleNewClient(&wifiServer, wifiClients); 984 | handleNewClient(&wifiServerEnhanced, wifiClientsEnhanced); 985 | handleNewClient(&wifiServerReadOnly, wifiClientsReadOnly); 986 | #endif 987 | } 988 | --------------------------------------------------------------------------------