├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── arduino_lint.yml │ ├── build_arduino_ide.yml │ ├── build_platformio.yml │ ├── cppcheck.yml │ ├── cpplint.yml │ └── test_platformio.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── component.mk ├── docs ├── 20C2_Vitodens_200_300_333_Vitotronic_200_Typ_HO1.pdf ├── error_codes.pdf ├── protocol_vs1.md ├── protocol_vs2.md ├── vt200_300gwb_en.pdf ├── vt200ho1a_en.pdf └── vt200ho1abc_en.pdf ├── examples ├── generic-interface │ └── generic-interface.ino ├── linux │ ├── main.cpp │ └── platformio.ini ├── simple-read-GWG │ └── simple-read-GWG.ino ├── simple-read-VS1 │ └── simple-read-VS1.ino ├── simple-read-VS2 │ └── simple-read-VS2.ino ├── simple-write-VS1 │ └── simple-write-VS1.ino ├── simple-write-VS2 │ └── simple-write-VS2.ino └── softwareserial │ └── softwareserial.ino ├── idf_component.yml ├── keywords.txt ├── library.json ├── library.properties ├── platformio.ini ├── src ├── Constants.cpp ├── Constants.h ├── Datapoint │ ├── ConversionHelpers.cpp │ ├── ConversionHelpers.h │ ├── Converter.cpp │ ├── Converter.h │ ├── Datapoint.cpp │ └── Datapoint.h ├── GWG │ ├── GWG.cpp │ ├── GWG.h │ ├── PacketGWG.cpp │ └── PacketGWG.h ├── Helpers.h ├── Interface │ ├── GenericInterface.h │ ├── HardwareSerialInterface.cpp │ ├── HardwareSerialInterface.h │ ├── LinuxSerialInterface.cpp │ ├── LinuxSerialInterface.h │ ├── SerialInterface.h │ ├── SoftwareSerialInterface.cpp │ └── SoftwareSerialInterface.h ├── Logging.h ├── VS1 │ ├── PacketVS1.cpp │ ├── PacketVS1.h │ ├── VS1.cpp │ └── VS1.h ├── VS2 │ ├── PacketVS2.cpp │ ├── PacketVS2.h │ ├── ParserVS2.cpp │ ├── ParserVS2.h │ ├── VS2.cpp │ └── VS2.h └── VitoWiFi.h ├── test ├── test_Datapoint │ └── test_Datapoint.cpp ├── test_PacketVS1 │ └── test_PacketVS1.cpp ├── test_PacketVS2 │ └── test_PacketVS2.cpp └── test_ParserVS2 │ └── test_ParserVS2.cpp └── test_coverage.py /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Installation specifics 2 | * **Heating type:** eg. 20CB - Vitodens 200 with Vitotronic 200 3 | * **Protocol:** KW or P300 4 | * **Board:** ESP8266 or ESP32 (others?), possible brand/make (NodeMCU, Wemos D1 mini...) 5 | * **Hardware:** Which hardware implementation did you use? 6 | 7 | ### Symptom 8 | What happens? (crash, unresponsive, nothing at all...) 9 | When does it happen? (after several hours, immideately...) 10 | What would you expect? 11 | ... 12 | 13 | Be as complete as you can! 14 | 15 | ### Extra info 16 | Please add relevant debug information like debug messages, (decoded) stack traces... 17 | 18 | 19 | *You don't have to provide all the info, but less is not always more!* 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | ### Installation specifics 2 | * **Heating type:** eg. 20CB - Vitodens 200 with Vitotronic 200 3 | * **Protocol:** KW or P300 4 | * **Board:** ESP8266 or ESP32 (others?), possible brand/make (NodeMCU, Wemos D1 mini...) 5 | * **Hardware:** Which hardware implementation did you use? 6 | 7 | ### Symptom 8 | What happens? (crash, unresponsive, nothing at all...) 9 | When does it happen? (after several hours, immideately...) 10 | What would you expect? 11 | ... 12 | 13 | Be as complete as you can! 14 | 15 | ### Extra info 16 | Please add relevant debug information like debug messages, (decoded) stack traces... 17 | 18 | 19 | *You don't have to provide all the info, but less is not always more!* -------------------------------------------------------------------------------- /.github/workflows/arduino_lint.yml: -------------------------------------------------------------------------------- 1 | name: Arduino Lint 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: arduino/arduino-lint-action@v1 -------------------------------------------------------------------------------- /.github/workflows/build_arduino_ide.yml: -------------------------------------------------------------------------------- 1 | name: Build with Arduino IDE 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build-for-esp8266: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | fqbn: 13 | - esp8266:esp8266:generic 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: arduino/compile-sketches@v1 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | fqbn: ${{ matrix.fqbn }} 20 | enable-deltas-report: true 21 | platforms: | 22 | - name: esp8266:esp8266 23 | source-url: https://arduino.esp8266.com/stable/package_esp8266com_index.json 24 | sketch-paths: | 25 | - examples/simple-read-VS1 26 | - examples/simple-read-VS2 27 | - examples/simple-write-VS1 28 | - examples/simple-write-VS2 29 | - examples/simple-read-GWG 30 | - examples/softwareserial 31 | 32 | build-for-esp32: 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | fqbn: 37 | - esp32:esp32:esp32 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: arduino/compile-sketches@v1 41 | with: 42 | github-token: ${{ secrets.GITHUB_TOKEN }} 43 | fqbn: ${{ matrix.fqbn }} 44 | enable-deltas-report: true 45 | platforms: | 46 | - name: esp32:esp32 47 | source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json 48 | sketch-paths: | 49 | - examples/generic-interface 50 | - examples/simple-read-VS1 51 | - examples/simple-read-VS2 52 | - examples/simple-write-VS1 53 | - examples/simple-write-VS2 54 | - examples/simple-read-GWG -------------------------------------------------------------------------------- /.github/workflows/build_platformio.yml: -------------------------------------------------------------------------------- 1 | name: Build with Platformio 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-for-esp8266: 7 | runs-on: ubuntu-latest 8 | container: ghcr.io/bertmelis/pio-test-container 9 | strategy: 10 | matrix: 11 | example: [ 12 | examples/simple-read-VS1/simple-read-VS1.ino, 13 | examples/simple-read-VS2/simple-read-VS2.ino, 14 | examples/simple-write-VS1/simple-write-VS1.ino, 15 | examples/simple-write-VS2/simple-write-VS2.ino, 16 | examples/simple-read-GWG/simple-read-GWG.ino, 17 | examples/softwareserial/softwareserial.ino 18 | ] 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Build PlatformIO examples 22 | run: pio ci --lib="." --board=d1_mini 23 | env: 24 | PLATFORMIO_CI_SRC: ${{ matrix.example }} 25 | 26 | build-for-esp32: 27 | runs-on: ubuntu-latest 28 | container: ghcr.io/bertmelis/pio-test-container 29 | strategy: 30 | matrix: 31 | example: [ 32 | examples/generic-interface/generic-interface.ino, 33 | examples/simple-read-VS1/simple-read-VS1.ino, 34 | examples/simple-read-VS2/simple-read-VS2.ino, 35 | examples/simple-write-VS1/simple-write-VS1.ino, 36 | examples/simple-write-VS2/simple-write-VS2.ino, 37 | examples/simple-read-GWG/simple-read-GWG.ino 38 | ] 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: Build PlatformIO examples 42 | run: pio ci --lib="." --board=lolin32 43 | env: 44 | PLATFORMIO_CI_SRC: ${{ matrix.example }} 45 | 46 | build-for-linux: 47 | runs-on: ubuntu-latest 48 | container: ghcr.io/bertmelis/pio-test-container 49 | strategy: 50 | matrix: 51 | example: [ 52 | examples/linux/main.cpp 53 | ] 54 | steps: 55 | - uses: actions/checkout@v4 56 | - name: Build PlatformIO examples 57 | run: pio ci --lib="." --project-conf="./examples/linux/platformio.ini" 58 | env: 59 | PLATFORMIO_CI_SRC: ${{ matrix.example }} -------------------------------------------------------------------------------- /.github/workflows/cppcheck.yml: -------------------------------------------------------------------------------- 1 | name: Cppcheck 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | container: ghcr.io/bertmelis/pio-test-container 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Cppcheck 12 | run: | 13 | pio check --fail-on-defect=medium --fail-on-defect=high --flags "--inline-suppr --enable=warning --enable=style --enable=performance --suppress=unusedFunction --suppress=preprocessorErrorDirective" --skip-packages -------------------------------------------------------------------------------- /.github/workflows/cpplint.yml: -------------------------------------------------------------------------------- 1 | name: cpplint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: '3.11' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install cpplint 20 | - name: Linting 21 | run: | 22 | cpplint --repository=. --recursive --filter=-whitespace/line_length,-build/include ./src -------------------------------------------------------------------------------- /.github/workflows/test_platformio.yml: -------------------------------------------------------------------------------- 1 | name: Test with Platformio 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | container: ghcr.io/bertmelis/pio-test-container 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Test 14 | run: | 15 | pio test -e native -v -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode 3 | cov 4 | *cov.info -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(COMPONENT_SRCDIRS 2 | "src/Datapoint" "src/GWG" "src/VS1" "src/VS2" "src/Interface" 3 | ) 4 | 5 | set(COMPONENT_ADD_INCLUDEDIRS 6 | "src" "src/Datapoint" "src/GWG" "src/VS1" "src/VS2" "src/Interface" 7 | ) 8 | 9 | set(COMPONENT_REQUIRES 10 | "arduino-esp32" 11 | ) 12 | 13 | register_component() 14 | 15 | target_compile_options(${COMPONENT_TARGET} PRIVATE -fno-rtti) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bert Melis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VitoWiFi 2 | 3 | Library for ESP32, ESP8266 and Linux to communicate with Viessmann systems using a (DIY) serial optolink. 4 | 5 | Based on the fantastic work on [openv](https://github.com/openv/openv/wiki). 6 | 7 | [![PlatformIO Registry](https://badges.registry.platformio.org/packages/bertmelis/library/VitoWiFi.svg)](https://registry.platformio.org/libraries/bertmelis/VitoWiFi) 8 | [![Build with Platformio](https://github.com/bertmelis/VitoWiFi/actions/workflows/build_platformio.yml/badge.svg)](https://github.com/bertmelis/VitoWiFi/actions/workflows/build_platformio.yml) 9 | [![Build with Arduino IDE](https://github.com/bertmelis/VitoWiFi/actions/workflows/build_arduino_ide.yml/badge.svg)](https://github.com/bertmelis/VitoWiFi/actions/workflows/build_arduino_ide.yml) 10 | 11 | ## Features 12 | 13 | - VS1 (KW) and VS2 (P300) support. The older GWG protocol is also supported. 14 | - Non-blocking API calls 15 | - For the Arduino framework and POSIX systems (Linux, tested on a Raspberry Pi 1B) 16 | - Maximum flexibility for communication by supporting standard UART interfaces (HardwareSerial, `SoftwareSerial` on ESP8266) as well as a custom user-created interface. 17 | 18 | ## Contents 19 | 20 | - [Installation](#installation) 21 | - [Hardware](#hardware) 22 | - [Usage](#usage) 23 | - [Datapoints](#datapoints) 24 | - [API reference](#api-reference) 25 | - [Bugs and feature requests](#bugs-and-feature-requests) 26 | - [License](#license) 27 | 28 | ## Installation 29 | 30 | * For Arduino IDE: see [the Arduino Guide](https://www.arduino.cc/en/Guide/Libraries#toc4) 31 | * For Platformio: see the [Platfomio registry page for VitoWifi](https://registry.platformio.org/libraries/bertmelis/VitoWiFi) 32 | 33 | ## Hardware 34 | 35 | The optolink hardware can be really simple. Using the circuit below you can build your own optolink. 36 | Please also check the [openv wiki, in German](https://github.com/openv/openv/wiki/Die-Optolink-Schnittstelle) for more implementations. 37 | 38 | ``` 39 | 3.3V 40 | O 41 | | 42 | +-----+-----+ 43 | | | 44 | --- --- 45 | | | | | 46 | | | 180Ohm | | 10kOhm 47 | | | | | 48 | --- --- 49 | | | 50 | --- | 51 | SFH487-2 \ / -> | 52 | V -> | 53 | --- | 54 | | | 55 | TX O-------+ | 56 | RX O-------------------+ 57 | | 58 | |/ c 59 | -> | SFH309FA 60 | -> |> e 61 | | 62 | ----- 63 | --- 64 | - 65 | ``` 66 | 67 | ## Usage 68 | 69 | The canonical way to use this library is simple and straightforward. A few steps are involved: 70 | 71 | 1. define your VitoWiFi object and specify the protocol and interface 72 | 2. define all needed datapoints 73 | 3. create callback for when data or errors are returned (std::function supported) 74 | 4. in `void setup()` 75 | - attach the callbacks 76 | - start VitoWiFi 77 | 5. in `void loop()`: 78 | - call `loop()` regularly. It keeps VitoWiFi running. Ideally it is called more than once every 10ms. 79 | 80 | A simple program for ESP32 to test and query your devicetype looks like this: 81 | 82 | ```cpp 83 | #include 84 | #include 85 | 86 | VitoWiFi::VitoWiFi myHeater(&Serial1); 87 | VitoWiFi::Datapoint deviceId("device id", 0x00F8, 2, VitoWiFi::noconv); 88 | 89 | void onResponse(const VitoWiFi::PacketVS2& response, const VitoWiFi::Datapoint& request) { 90 | Serial.print("Raw data received:"); 91 | const uint8_t* data = response.data(); 92 | for (uint8_t i = 0; i < response.dataLength(); ++i) { 93 | Serial.printf(" %02x", data[i]); 94 | } 95 | Serial.print("\n"); 96 | } 97 | 98 | void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) { 99 | Serial.printf("Datapoint \"%s\" error: ", request.name()); 100 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 101 | Serial.print("timeout\n"); 102 | } else if (error == VitoWiFi::OptolinkResult::LENGTH) { 103 | Serial.print("length\n"); 104 | } else if (error == VitoWiFi::OptolinkResult::NACK) { 105 | Serial.print("nack\n"); 106 | } else if (error == VitoWiFi::OptolinkResult::CRC) { 107 | Serial.print("crc\n"); 108 | } else if (error == VitoWiFi::OptolinkResult::ERROR) { 109 | Serial.print("error\n"); 110 | } 111 | } 112 | 113 | void setup() { 114 | delay(1000); 115 | Serial.begin(115200); 116 | Serial.print("Setting up vitoWiFi\n"); 117 | myHeater.onResponse(onResponse); 118 | myHeater.onError(onError); 119 | myHeater.begin(); 120 | Serial.print("Setup finished\n"); 121 | } 122 | 123 | void loop() { 124 | static uint32_t lastReadTime = 0; 125 | if (millis() - lastReadTime > 30000) { 126 | lastReadTime = millis(); 127 | if (myHeater.read(deviceId)) { 128 | Serial.printf("reading \"%s\"\n", deviceId.name()); 129 | } else { 130 | Serial.printf("error reading \"%s\"\n", deviceId.name()); 131 | } 132 | } 133 | myHeater.loop(); 134 | } 135 | 136 | ``` 137 | 138 | Most users will have a collection of datapoints thay want to read. A possible technique to query a large number of datapoints is by simply iterating over them: 139 | 140 | ```cpp 141 | // create a collection (array) of datapoints: 142 | VitoWiFi::Datapoint datapoints[] = { 143 | VitoWiFi::Datapoint("outside temp", 0x5525, 2, VitoWiFi::div10), 144 | VitoWiFi::Datapoint("boiler temp", 0x0810, 2, VitoWiFi::div10), 145 | VitoWiFi::Datapoint("pump status", 0x2906, 1, VitoWiFi::noconv) 146 | }; 147 | int numberDatapoints = 3; 148 | int currentIndex = -1; 149 | 150 | // to start reading, set currentIndex to 0 151 | if (currentIndex >= 0) { 152 | // reading will return `true` when successful. 153 | // as long as VitoWiFi is busy it will return `false` 154 | if (myVitoWiFi.read(datapoints[currentIndex])) { 155 | ++currentIndex; 156 | if (currentIndex == numberDatapoints) currentIndex = -1; 157 | } 158 | } 159 | ``` 160 | 161 | ### More examples 162 | 163 | You can find more examples in the `examples` directory in this repo. 164 | 165 | ## Datapoints 166 | 167 | When defining your datapoints, you need to specify the name, address, length and conversion type. Datapoints in C++ looks like this: 168 | 169 | ```cpp 170 | VitoWiFi::Datapoint datapoint1("outside temp", 0x5525, 2, VitoWiFi::div10); 171 | VitoWiFi::Datapoint datapoint2("boiler temp", 0x0810, 2, VitoWiFi::div10); 172 | VitoWiFi::Datapoint datapoint3("pump status", 0x2906, 1, VitoWiFi::noconv); 173 | ``` 174 | 175 | It is not possible for me to give you a list of available datapoints for your device. Please consult the [openv wiki](https://github.com/openv/openv/wiki/Adressen) or the [InsideViessmannVitosoft repo](https://github.com/sarnau/InsideViessmannVitosoft). 176 | 177 | While name, address and length are self-explanatory, conversion type is a bit more complicated. 178 | 179 | ### Conversion types 180 | 181 | Data is stored in binary and often needs a conversion function to transform into a more usable type. This is specified by the conversion type, the last argument in the datapoint definition. 182 | 183 | C++ is a strongly typed programming language so using the right type is important (read: mandatory). Each conversion type corresponds to a certain type. Reading or writing has to be done using this specific type and failure to do so will not work or will lead to undefined results. 184 | 185 | In the table below you can find how to define your datapoints: 186 | 187 | |name|size|converter|return type|remarks| 188 | |---|---|---|---|---| 189 | |Temperature|2|div10|float|| 190 | |Temperature short|1|noconv|uint8_t|Equivalent to Mode| 191 | |Power|1|div2|float|Also used for temperature in GWG| 192 | |Status|1|noconv|bool|This is the same as 'Temperature short' and 'Mode'. The `uint8_t` value will be implicitely converted to bool.| 193 | |Hours|4|div3600|float|This is in fact a `Count` datapoint (seconds) converted to hours.| 194 | |Count|4|noconv|uint32_t|| 195 | |Count short|2|noconv|uint16_t|| 196 | |Mode|1|noconv|uint8_t|Possibly castable to ENUM| 197 | |CoP|1|div10|float|Also used for heating curve slope| 198 | 199 | To use schedules, helper functions are available 200 | 201 | ```cpp 202 | std::size_t encodeSchedule(const char* schedule, std::size_t len, uint8_t* output); 203 | std::size_t encodeSchedule(const char* schedule, uint8_t* output); 204 | ``` 205 | 206 | Mind that the converters are declared within the `VitoWiFi` namespace. 207 | 208 | ## Bugs and feature requests 209 | 210 | Please use Github's facilities to get in touch. While the issue template is not mandatory to use, please use it at least as a starting point to supply the needed info for bughunting. 211 | 212 | ## API reference 213 | 214 | Below is an overview of all commonly used methods. For extra functions you can consult the source code. 215 | 216 | ### `VitoWiFi::Datapoint` 217 | 218 | ##### `VitoWiFi::Datapoint(const char* name, uint16_t address, uint8_t length, const Converter& converter)` 219 | 220 | Constructor for datapoints. 221 | 222 | ##### `const char* name() const` 223 | ##### `uint16_t address() const` 224 | ##### `uint8_t length() const` 225 | 226 | Self-explanatory 227 | 228 | ##### `const Converter& converter() const` 229 | 230 | Returns the associated converter class. Can be used to select code flow in the callbacks. 231 | 232 | ##### `VariantValue decode(const PacketVS2& packet) const` 233 | 234 | Decodes the data in `packet` using the converter class attached. 235 | Returns `VariantValue` which is implicitely castable to the correct datatype. Consult the table above. 236 | 237 | ##### `VariantValue decode(const uint8_t* data, uint8_t length) const` 238 | 239 | Decodes the data in the supplied `data`-buffer using the Converter class attached. 240 | Returns `VariantValue` which is implicitely castable to the correct datatype. Consult the table above. 241 | 242 | ##### `void encode(uint8_t* buf, uint8_t len, const VariantValue& value) const` 243 | 244 | Encodes `value` into the supplied `buf` with maximum size `len`. The size must be at least the length of the datapoint. 245 | 246 | `VariantValue` is a type to implicitely convert datatypes for use in VitoWiFi. Make sure to use the type that matches your Converter type. 247 | 248 | ### `VitoWiFi::PacketVS2` 249 | 250 | Only used in VS2. This type is used in the onResponse callback and contains the returned data. 251 | Most users will only use the following two methods and only if they want to access the raw data. Otherwise, the data can be decoded using the corresponding `Datapoint`. 252 | 253 | ##### `uint8_t dataLength() const` 254 | 255 | Returns the number of bytes in the payload. 256 | 257 | ##### `const uint8_t* data() const` 258 | 259 | Returns a pointer to the payload. 260 | 261 | ### `VitoWiFi::VitoWiFi` 262 | 263 | ##### `VitoWiFi::VitoWiFi(IFACE* interface)` 264 | 265 | Constructor of the VitoWiFi class. `PROTOCOL_VERSION` can be `VitoWiFi::GWG`, `VitoWiFi::VS1` or `VitoWiFi::VS2`. If your Viessmann device is somewhat modern, you should use `VitoWiFi::VS2`. 266 | `interface` can be any of the `HardwareSerial` interfaces (`Serial`, `Serial1`...) on Arduino boards, `SoftwareSerial` (on ESP8266) or if you are on Linux, pass the c-string depicting your device (for example `"/dev/ttyUSB0"`). 267 | 268 | ##### `void onResponse(typename PROTOCOLVERSION::OnResponseCallback callback)` 269 | 270 | Attach an onResponse callback. You can only attach one and will overwrite the previously attached callback. 271 | The callback has the following signature: 272 | 273 | - `VitoWiFi::VS1`: `void (const uint8_t*, uint8_t, const VitoWiFi::Datapoint&)` 274 | - `VitoWiFi::VS2`: `void (const VitoWiFi::PacketVS2&, const VitoWiFi::Datapoint&)` 275 | 276 | ##### `void onError(typename PROTOCOLVERSION::OnErrorCallback callback)` 277 | 278 | Attach an onError callback. You can only attack one and will overwrite the previously attached callback. 279 | The callback has the following signature: 280 | 281 | - `void (VitoWiFi::OptolinkResult, const VitoWiFi::Datapoint&)` 282 | 283 | ##### `bool begin()` 284 | 285 | Start the optolink serial interface. Returns bool on success. 286 | 287 | ##### `void end()` 288 | 289 | Stop the optolink serial interface 290 | 291 | ##### `void loop()` 292 | 293 | Worker function, must be called regularly. This is the method that polls the serial interface. Callbacks are dispatched from this method so they will run in the same thread/task. 294 | 295 | ##### `bool read(Datapoint datapoint)` 296 | 297 | Read `datapoint`. Returns `true` on success. 298 | 299 | ##### `bool write(Datapoint datapoint, T value)` 300 | 301 | Write `value` with type `T` to `datapoint`. Make sure to use the correct type. Consult the table with types in the "Datapoints" section. 302 | 303 | ##### `write(Datapoint datapoint, const uint8_t* data, uint8_t length)` 304 | 305 | Write the raw `data` with `length` to `datapoint`. Returns `true` on success. `length` has to match the length of the datapoint. 306 | 307 | ### Enums 308 | 309 | ##### `VitoWiFi::OptolinkResult` 310 | 311 | Used in the onError callback. Possible returned values are: 312 | - TIMEOUT 313 | - LENGTH 314 | - NACK 315 | - CRC 316 | - ERROR 317 | 318 | ### Compile time configuration 319 | 320 | ##### `VW_START_PAYLOAD_LENGTH` 321 | 322 | This macro sets the initial payload (data) length for incoming packets. VitoWiFi will increased the buffer if needed. If you know the maximum data length you are going to request beforehand, use this set to prevent dynamic memory reallocation. The default is 10 bytes. 323 | 324 | ## Bugs and feature requests 325 | 326 | Please use Githubs facilities, issues and discussions, to get in touch. 327 | When creating a bug report, please use the provided template. In any case, better to include too much info than too little. 328 | 329 | ## License 330 | 331 | Copyright (c) 2017, 2023 Bert Melis 332 | 333 | Permission is hereby granted, free of charge, to any person obtaining a copy 334 | of this software and associated documentation files (the "Software"), to deal 335 | in the Software without restriction, including without limitation the rights 336 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 337 | copies of the Software, and to permit persons to whom the Software is 338 | furnished to do so, subject to the following conditions: 339 | 340 | The above copyright notice and this permission notice shall be included in all 341 | copies or substantial portions of the Software. 342 | 343 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 344 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 345 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 346 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 347 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 348 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 349 | SOFTWARE. 350 | 351 | ### Credits go to 352 | 353 | * Hex print: 2011, robtillaart @ Arduino.cc forum (not used in v3 and above) 354 | * Logger/Blinker: MIT 2015, marvinroger @ Github (not used in v3 and above) 355 | * Serial Protocol @ ~~http://openv.wikispaces.com~~https://github.com/openv/openv/wiki 356 | * [@tolw](https://github.com/tolw) for implementing the writing 357 | * [@Empor-co](https://github.com/Empor-co) for testing the KW-protocol 358 | * and many others for code and inspiration 359 | -------------------------------------------------------------------------------- /component.mk: -------------------------------------------------------------------------------- 1 | COMPONENT_ADD_INCLUDEDIRS := src 2 | COMPONENT_SRCDIRS := src 3 | CXXFLAGS += -fno-rtti -------------------------------------------------------------------------------- /docs/20C2_Vitodens_200_300_333_Vitotronic_200_Typ_HO1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertmelis/VitoWiFi/881a1124290e05e85a4a23056817e605695bd936/docs/20C2_Vitodens_200_300_333_Vitotronic_200_Typ_HO1.pdf -------------------------------------------------------------------------------- /docs/error_codes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertmelis/VitoWiFi/881a1124290e05e85a4a23056817e605695bd936/docs/error_codes.pdf -------------------------------------------------------------------------------- /docs/protocol_vs1.md: -------------------------------------------------------------------------------- 1 | From OpenV Wiki (https://github.com/openv/openv/wiki/Protokoll-300), consulted 10/10/2023 2 | 3 | Dieses Protokoll wird von den Vitotronic Steuerungen der KW Serie unterstützt. 4 | 5 | Es ähnelt dem älteren GWG Protokoll, unterstützt jedoch auch Adressen mit 2 Bytes Länge. 6 | 7 | Soweit bekannt, sind alle Steuerungen, die das Protokoll 300 unterstützen, abwärtskompatibel zum KW Protokoll und können auch über letzteres Protokoll angesprochen werden. 8 | 9 | Serielle Schnittstelle (Optolink): 10 | 11 | 4800 8 E 2, ohne Handshake Protokoll 12 | Kommunikation (alle Werte in Hex) 13 | 14 | Vitotronic sendet periodisch 0x05 15 | Sofort (xx msec) nach dem Empfang von 0x05 kann ein Telegramm gesendet werden und wird sofort beantwortet. 16 | Ehe ein weiteres Telegramm gesendet werden kann, muss erst auf das nächste 0x05 gewartet werden. 17 | Anstatt auf das nächste 0x05 zu warten, kann auch direkt nach dem Empfang einer Antwort das nächste Telegramm geschickt werden. Dabei darf das Telegramm nicht mit 0x01 eingeleitet werden (Somit ist das 0x01 logisch gesehen nicht ein Telegram-Start-Byte sondern ein ACK auf die 0x05). 18 | Telegrammdetails: 19 | 20 | 01 Telegramm-Start-Byte (ACK auf 0x05) 21 | F7 Type [F4=Virtuell_Write (normales Schreiben), F7=Virtuell_Read (normales Lesen), 6B=GFA_Read, 68=GFA_Write, 78=PROZESS_WRITE, 7B=PROZESS_READ] 22 | XX XX Adresse 23 | XX Anzahl der angefragten Bytes 24 | 25 | Beispiel Abfrage Außentemperatur (V200KW2) 26 | 27 | TX: 01 28 | TX: F7 55 25 02 29 | RX: 5B 00 > Außentemperatur 9,1°C 30 | 31 | Beispiel Geräte Kennung abfragen 32 | 33 | TX: 01 34 | TX: F7 00 F8 02 35 | RX: 20 98 36 | 0x2098 = V200KW2 37 | 38 | Beispiel Heizkreis A1 auf Sparbetrieb setzen (V200KW2) 39 | 40 | TX: 01 41 | TX: F4 23 02 01 01 42 | RX: 00 = Bestätigung OK -------------------------------------------------------------------------------- /docs/protocol_vs2.md: -------------------------------------------------------------------------------- 1 | From OpenV Wiki (https://github.com/openv/openv/wiki/Protokoll-300), consulted 10/10/2023 2 | 3 | Dieses Protokoll wird von den neueren Vitotronic Steuerungen unterstützt. 4 | 5 | Gegenüber dem KW Protokoll bietet es einige Vorteile: 6 | 7 | Anfragen können zu jeder Zeit an die Anlage gesendet werden, kein Warten auf 0x05, damit können auch mehrere Abfragen in kurzer Zeit gesendet werden. 8 | Weitgehend fehlerfreie Übertragung dank CRC Checksumme. 9 | Soweit bekannt, sind alle Steuerungen, die das Protokoll 300 unterstützen, abwärtskompatibel zum KW Protokoll und können auch über dieses Protokoll angesprochen werden. 10 | 11 | Serielle Schnittstelle (Optolink): 12 | 13 | 4800 8 E 2, ohne Handshake Protokoll 14 | Initialisierung der Kommunikation (alle Werte in Hex) 15 | 16 | Vitotronic sendet periodisch (ca. 2s) 0x05 17 | Senden von 0x04 (EOT) um einen definierten Ausgangszustand zu erhalten. 18 | Erneutes 0x05 der Vitotronic abwarten 19 | Darauf sofort Antwort geben mit der Synchronisierungssequenz 0x16 0x00 0x00 (diese Synchronisierungssequenz kann vom verbundenen Programm periodisch gesendet werden, um ein Timeout der Kommunikation zu verhindern). 20 | Vitotronic antwortet mit 0x06 21 | Das periodische Senden von 0x05 hört damit auf 22 | Beenden der Kommunikation 23 | 24 | Durch Senden des Telegramm-Ende Steuerzeichens EOT 0x04 wird Vitotronic wieder zurückgesetzt und sendet wieder periodisch 0x05. 25 | Es empfiehlt sich, auch beim Start einer Anwendung zunächst ein 0x04 zu senden um einen definierten Anfangszustand zu erhalten. 26 | Antworten von Vitotronic 27 | 28 | 0x15 NACK/Error 29 | 0x05 ENQ/not init 30 | 0x06 ACK ; Antwort auf 0x16 0x00 0x00 und auf korrekt empfangene Telegramme. Auch das mit einer Vitotronic verbundene Programm antwortet auf Telegramme der Vitotronic mit 0x06 31 | Die Kommunikation scheint sich hier an den ASCII Steuerzeichen zu orientieren. 32 | 33 | Telegrammdetails: 34 | 35 | Wenn Daten gelesen werden: 36 | 37 | 0x41 Telegramm-Start-Byte 38 | 0xXX Länge der Nutzdaten als Anzahl der Bytes zwischen dem Telegramm-Start-Byte (0x41) und der Prüfsumme) 39 | 0x0X MessageIdentifier (untere 4 Bits): 0x00 = Request, 0x01 = Response, 0x02 = UNACKD, 0x03 = Error 40 | 0x0X FunctionCode (untere 5 Bits): 0x01 = Virtual_READ, 0x02 = Virtual_WRITE, 0x07 = Remote_Procedure_Call 41 | Bits 5-7 sind eine Message-Sequenz-Nummer, welche im Reply ebenfalls gesetzt wird. 42 | 0xXX 0xXX 2 byte Adresse der Daten oder Prozedur 43 | 0xXX Anzahl der Bytes, die in der Antwort erwartet werden 44 | 0xXX Prüfsumme = Summe der Werte ab dem 2. Byte (ohne 0x41) 45 | Wenn Daten geschrieben werden sollen: 46 | 47 | 0x41 Telegramm-Start-Byte 48 | 0xXX Länge der Nutzdaten (Anzahl der Bytes zwischen dem Telegramm-Start-Byte (0x41) und der Prüfsumme) 49 | 0x0X 0x00 = Anfrage, 01 = Antwort, 03 = Fehler 50 | 0x0X 0x01 = ReadData, 0x02 = WriteData, 07 = Function Call 51 | Bits 7,6,5 können wahlfrei gesetzt werden, sie werden in der Antwort dann ebenfalls gesetzt 52 | 0xXX 0xXX 2 Byte Adresse der Daten oder Prozedur 53 | 0xXX Anzahl der Bytes, die in der geschrieben werden sollen 54 | 0xXX ... Inhalt der geschrieben werden soll 55 | 0xXX Prüfsumme = Summe der Werte ab dem 2. Byte (ohne 0x41) 56 | Die Heizungssteuerung antwortet auf erfolgreiches Schreiben mit 0x41 LE 0x01 0x02 @@ @@ NM XX mit LE=Länge der Nutzdaten, @@ @@ = 2 Byte Adresse, NM = Anzahl geschriebener Bytes, XX = Prüfsumme 57 | Die im Funktionstyp-Byte der Anfrage / 4. Byte gesendeten Bits 7,6,5 sind dann in der Antwort ebenso enthalten. Wurde z.B. als Funktionstype WriteData 0x02 gesendet, kommt 0x02 in der Antwort; wurde 0xA2gesendet, kommt 0xA2 zurück.1 58 | 59 | Beispiel Aussentemperatur abfragen (Vitotronic 333) 60 | 61 | Senden 0x41 05 00 01 55 25 02 82 62 | Empfangen 0x06 41 07 01 01 55 25 02 07 01 8D 63 | Antwort: 0x0107 = 263 = 26.3° 64 | Beispiel Geräte Kennung abfragen 65 | 66 | Senden 0x41 05 00 01 00 F8 02 00 67 | Empfangen 0x06 41 07 01 01 00 F8 02 20 B8 DB 68 | 0x20B8 = V333MW1 69 | Beispiel Vitodens 333 Betriebsart schreiben: 70 | 71 | Senden 0x41 06 00 02 23 23 01 XY xx 72 | Empfangen 0x06 41 06 01 02 23 23 01 01 xx 73 | XY: 74 | 0 = Abschalten 75 | 1 = nur WW 76 | 2 = Heizen mit WW 77 | 3 = immer Reduziert 78 | 4 = immer Normal -------------------------------------------------------------------------------- /docs/vt200_300gwb_en.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertmelis/VitoWiFi/881a1124290e05e85a4a23056817e605695bd936/docs/vt200_300gwb_en.pdf -------------------------------------------------------------------------------- /docs/vt200ho1a_en.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertmelis/VitoWiFi/881a1124290e05e85a4a23056817e605695bd936/docs/vt200ho1a_en.pdf -------------------------------------------------------------------------------- /docs/vt200ho1abc_en.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bertmelis/VitoWiFi/881a1124290e05e85a4a23056817e605695bd936/docs/vt200ho1abc_en.pdf -------------------------------------------------------------------------------- /examples/generic-interface/generic-interface.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | /* 6 | This example is to show how you could build your own 7 | interface and serves as a compilation test 8 | */ 9 | 10 | // Dummy class that has all the methods for VitoWiFi to work 11 | // but doesn't do anything in this case. 12 | // Use as skeleton for your own implementation 13 | class DummyInterface { 14 | public: 15 | bool begin() { 16 | // prepare the interface 17 | // optolink comm at 4800 baud, 8 bits, even parity and 2 stop bits 18 | // called at VitoWiFi::begin() 19 | return true; 20 | } 21 | void end() { 22 | // stop the interface 23 | // called at VitoWiFi::end() 24 | } 25 | std::size_t write(const uint8_t* data, uint8_t length) { 26 | // tries to write `data` with length `length` to the interface 27 | // returns the actually written data 28 | return 0; 29 | } 30 | uint8_t read() { 31 | // read one byte from the interface 32 | // availability of data is checked first 33 | return 0; 34 | } 35 | size_t available() { 36 | // check if data is available 37 | return 0; 38 | } 39 | }; 40 | 41 | // optolink on Dummy Interface, logging output on UART1 (connected to USB) 42 | DummyInterface dummyInterface; 43 | #define SERIAL1 dummyInterface 44 | #define SERIAL2 Serial 45 | #define SERIALBAUDRATE 115200 46 | 47 | VitoWiFi::VitoWiFi vitoWiFi(&SERIAL1); 48 | bool readValues = false; 49 | uint8_t datapointIndex = 0; 50 | 51 | VitoWiFi::Datapoint datapoints[] = { 52 | VitoWiFi::Datapoint("outsidetemp", 0x5525, 2, VitoWiFi::div10), 53 | VitoWiFi::Datapoint("boilertemp", 0x0810, 2, VitoWiFi::div10), 54 | VitoWiFi::Datapoint("pump", 0x2906, 1, VitoWiFi::noconv) 55 | }; 56 | 57 | void onResponse(const VitoWiFi::PacketVS2& response, const VitoWiFi::Datapoint& request) { 58 | // raw data can be accessed through the 'response' argument 59 | SERIAL2.print("Raw data received:"); 60 | const uint8_t* data = response.data(); 61 | for (uint8_t i = 0; i < response.dataLength(); ++i) { 62 | SERIAL2.printf(" %02x", data[i]); 63 | } 64 | SERIAL2.print("\n"); 65 | 66 | // the raw data can be decoded using the datapoint. Be sure to use the correct type 67 | SERIAL2.printf("%s: ", request.name()); 68 | if (request.converter() == VitoWiFi::div10) { 69 | float value = request.decode(response); 70 | SERIAL2.printf("%.1f\n", value); 71 | } else if (request.converter() == VitoWiFi::noconv) { 72 | bool value = request.decode(response); 73 | // alternatively, we can just cast response.data()[0] to bool 74 | SERIAL2.printf("%s\n", value ? "ON" : "OFF"); 75 | } 76 | } 77 | 78 | void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) { 79 | SERIAL2.printf("Datapoint \"%s\" error: ", request.name()); 80 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 81 | SERIAL2.print("timeout\n"); 82 | } else if (error == VitoWiFi::OptolinkResult::LENGTH) { 83 | SERIAL2.print("length\n"); 84 | } else if (error == VitoWiFi::OptolinkResult::NACK) { 85 | SERIAL2.print("nack\n"); 86 | } else if (error == VitoWiFi::OptolinkResult::CRC) { 87 | SERIAL2.print("crc\n"); 88 | } else if (error == VitoWiFi::OptolinkResult::ERROR) { 89 | SERIAL2.print("error\n"); 90 | } 91 | } 92 | 93 | void setup() { 94 | delay(1000); 95 | SERIAL2.begin(SERIALBAUDRATE); 96 | SERIAL2.print("Setting up vitoWiFi\n"); 97 | 98 | vitoWiFi.onResponse(onResponse); 99 | vitoWiFi.onError(onError); 100 | vitoWiFi.begin(); 101 | 102 | SERIAL2.print("Setup finished\n"); 103 | } 104 | 105 | void loop() { 106 | static uint32_t lastMillis = 0; 107 | if (millis() - lastMillis > 60000UL) { // read all values every 60 seconds 108 | lastMillis = millis(); 109 | readValues = true; 110 | datapointIndex = 0; 111 | } 112 | 113 | if (readValues) { 114 | if (vitoWiFi.read(datapoints[datapointIndex])) { 115 | ++datapointIndex; 116 | } 117 | if (datapointIndex == 3) { 118 | readValues = false; 119 | } 120 | } 121 | 122 | vitoWiFi.loop(); 123 | } 124 | -------------------------------------------------------------------------------- /examples/linux/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include // is already included by VitoWiFi 6 | #include 7 | 8 | VitoWiFi::VitoWiFi vitoWiFi("/dev/ttyUSB0"); 9 | 10 | bool exitProgram = false; 11 | bool readValues = false; 12 | uint8_t datapointIndex = 0; 13 | 14 | VitoWiFi::Datapoint datapoints[] = { 15 | VitoWiFi::Datapoint("outsidetemp", 0x5525, 2, VitoWiFi::div10), 16 | VitoWiFi::Datapoint("boilertemp", 0x0810, 2, VitoWiFi::div10), 17 | VitoWiFi::Datapoint("pump", 0x2906, 1, VitoWiFi::noconv) 18 | }; 19 | 20 | void signalHandler(int signum) { 21 | std::cout << "Caught signal " << signum << std::endl; 22 | exitProgram = true; 23 | } 24 | 25 | void onResponse(const VitoWiFi::PacketVS2& response, const VitoWiFi::Datapoint& request) { 26 | // raw data can be accessed through the 'response' argument 27 | std::cout << "Raw data received: " << std::endl; 28 | const uint8_t* data = response.data(); 29 | for (uint8_t i = 0; i < response.dataLength(); ++i) { 30 | std::cout << std::hex << (int)data[i] << " "; 31 | } 32 | std::cout << std::endl; 33 | 34 | // the raw data can be decoded using the datapoint. Be sure to use the correct type 35 | std::cout << request.name() <<": "; 36 | if (request.converter() == VitoWiFi::div10) { 37 | float value = request.decode(response); 38 | std::cout << std::setprecision(2) << value << std::endl; 39 | } else if (request.converter() == VitoWiFi::noconv) { 40 | bool value = request.decode(response); 41 | if (value) { 42 | std::cout << "ON" << std::endl; 43 | } else { 44 | std::cout << "OFF" << std::endl; 45 | } 46 | // alternatively, we can just cast response.data()[0] to bool 47 | } 48 | } 49 | 50 | void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) { 51 | printf("Datapoint \"%s\" error: ", request.name()); 52 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 53 | std::cout << "timeout" << std::endl; 54 | } else if (error == VitoWiFi::OptolinkResult::LENGTH) { 55 | std::cout << "length" << std::endl; 56 | } else if (error == VitoWiFi::OptolinkResult::NACK) { 57 | std::cout << "nack" << std::endl; 58 | } else if (error == VitoWiFi::OptolinkResult::CRC) { 59 | std::cout << "crc" << std::endl; 60 | } else if (error == VitoWiFi::OptolinkResult::ERROR) { 61 | std::cout << "error" << std::endl; 62 | } 63 | } 64 | 65 | void setup() { 66 | sleep(2); 67 | std::cout << "Setting up VitoWiFi" << std::endl; 68 | 69 | vitoWiFi.onResponse(onResponse); 70 | vitoWiFi.onError(onError); 71 | vitoWiFi.begin(); 72 | 73 | std::cout << "Setup finished" << std::endl; 74 | } 75 | 76 | void loop() { 77 | static uint32_t lastMillis = 0; 78 | if (vw_millis() - lastMillis > 60000UL) { // read all values every 60 seconds 79 | std::cout << "reading datapoints" << std::endl; 80 | lastMillis = vw_millis(); 81 | readValues = true; 82 | datapointIndex = 0; 83 | } 84 | 85 | if (readValues) { 86 | if (vitoWiFi.read(datapoints[datapointIndex])) { 87 | std::cout << "datapoint \"" << datapoints[datapointIndex].name() << "\" requested" << std::endl; 88 | ++datapointIndex; 89 | } 90 | if (datapointIndex == 3) { 91 | readValues = false; 92 | } 93 | } 94 | 95 | vitoWiFi.loop(); 96 | } 97 | 98 | 99 | int main() { 100 | setup(); 101 | while(1) { 102 | loop(); 103 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 104 | if (exitProgram) break; 105 | } 106 | vitoWiFi.end(); 107 | return EXIT_SUCCESS; 108 | } -------------------------------------------------------------------------------- /examples/linux/platformio.ini: -------------------------------------------------------------------------------- 1 | [common] 2 | build_flags = 3 | -std=c++11 4 | -Wall 5 | -Wextra 6 | -Werror 7 | 8 | [env:native] 9 | platform = native 10 | build_flags = 11 | ${common.build_flags} 12 | build_type = debug -------------------------------------------------------------------------------- /examples/simple-read-GWG/simple-read-GWG.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #if defined(ARDUINO_ARCH_ESP8266) 6 | // optolink on full UART, logging output on secondary 7 | #define SERIAL1 Serial 8 | #define SERIAL2 Serial1 9 | #define SERIALBAUDRATE 74880 10 | #elif defined(ARDUINO_ARCH_ESP32) 11 | // optolink on UART2, logging output on UART1 (connected to USB) 12 | #define SERIAL1 Serial1 13 | #define SERIAL2 Serial 14 | #define SERIALBAUDRATE 115200 15 | #endif 16 | 17 | #ifndef SERIALBAUDRATE 18 | #error Target platform not supported 19 | #endif 20 | 21 | VitoWiFi::VitoWiFi vitoWiFi(&SERIAL1); 22 | ssize_t datapointIndex = -1; 23 | constexpr size_t numberDatapoints = 1; 24 | VitoWiFi::Datapoint datapoints[] = { 25 | VitoWiFi::Datapoint("temp", 0x6F, 1, VitoWiFi::div2), 26 | }; 27 | 28 | 29 | void onResponse(const uint8_t* data, uint8_t length, const VitoWiFi::Datapoint& request) { 30 | // raw data can be accessed through the 'response' argument 31 | SERIAL2.print("Raw data received:"); 32 | for (uint8_t i = 0; i < length; ++i) { 33 | SERIAL2.printf(" %02x", data[i]); 34 | } 35 | SERIAL2.print("\n"); 36 | 37 | // the raw data can be decoded using the datapoint. Be sure to use the correct type 38 | SERIAL2.printf("%s: ", request.name()); 39 | if (request.converter() == VitoWiFi::div2) { 40 | float value = request.decode(data, length); 41 | SERIAL2.printf("%.1f\n", value); 42 | } 43 | } 44 | 45 | void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) { 46 | SERIAL2.printf("Datapoint \"%s\" error: ", request.name()); 47 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 48 | SERIAL2.print("timeout\n"); 49 | } 50 | } 51 | 52 | void setup() { 53 | delay(1000); 54 | SERIAL2.begin(SERIALBAUDRATE); 55 | SERIAL2.print("Setting up vitoWiFi\n"); 56 | 57 | vitoWiFi.onResponse(onResponse); 58 | vitoWiFi.onError(onError); 59 | vitoWiFi.begin(); 60 | 61 | SERIAL2.print("Setup finished\n"); 62 | } 63 | 64 | void loop() { 65 | static uint32_t lastMillis = 0; 66 | if (millis() - lastMillis > 60000UL) { // read all values every 60 seconds 67 | lastMillis = millis(); 68 | datapointIndex = 0; 69 | } 70 | 71 | if (datapointIndex >= 0) { 72 | if (vitoWiFi.read(datapoints[datapointIndex])) { 73 | ++datapointIndex; 74 | } 75 | if (datapointIndex == numberDatapoints) { 76 | datapointIndex = -1; 77 | } 78 | } 79 | 80 | vitoWiFi.loop(); 81 | } 82 | -------------------------------------------------------------------------------- /examples/simple-read-VS1/simple-read-VS1.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #if defined(ARDUINO_ARCH_ESP8266) 6 | // optolink on full UART, logging output on secondary 7 | #define SERIAL1 Serial 8 | #define SERIAL2 Serial1 9 | #define SERIALBAUDRATE 74880 10 | #elif defined(ARDUINO_ARCH_ESP32) 11 | // optolink on UART2, logging output on UART1 (connected to USB) 12 | #define SERIAL1 Serial1 13 | #define SERIAL2 Serial 14 | #define SERIALBAUDRATE 115200 15 | #endif 16 | 17 | #ifndef SERIALBAUDRATE 18 | #error Target platform not supported 19 | #endif 20 | 21 | VitoWiFi::VitoWiFi vitoWiFi(&SERIAL1); 22 | bool readValues = false; 23 | uint8_t datapointIndex = 0; 24 | 25 | VitoWiFi::Datapoint datapoints[] = { 26 | VitoWiFi::Datapoint("outsidetemp", 0x5525, 2, VitoWiFi::div10), 27 | VitoWiFi::Datapoint("boilertemp", 0x0810, 2, VitoWiFi::div10), 28 | VitoWiFi::Datapoint("pump", 0x2906, 1, VitoWiFi::noconv) 29 | }; 30 | 31 | void onResponse(const uint8_t* data, uint8_t length, const VitoWiFi::Datapoint& request) { 32 | // raw data can be accessed through the 'response' argument 33 | SERIAL2.print("Raw data received:"); 34 | for (uint8_t i = 0; i < length; ++i) { 35 | SERIAL2.printf(" %02x", data[i]); 36 | } 37 | SERIAL2.print("\n"); 38 | 39 | // the raw data can be decoded using the datapoint. Be sure to use the correct type 40 | SERIAL2.printf("%s: ", request.name()); 41 | if (request.converter() == VitoWiFi::div10) { 42 | float value = request.decode(data, length); 43 | SERIAL2.printf("%.1f\n", value); 44 | } else if (request.converter() == VitoWiFi::noconv) { 45 | // in this example, the response is one byte 46 | SERIAL2.printf("%s\n", (data[0] > 0) ? "ON" : "OFF"); 47 | } 48 | } 49 | 50 | void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) { 51 | SERIAL2.printf("Datapoint \"%s\" error: ", request.name()); 52 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 53 | SERIAL2.print("timeout\n"); 54 | } else if (error == VitoWiFi::OptolinkResult::LENGTH) { 55 | SERIAL2.print("length\n"); 56 | } else if (error == VitoWiFi::OptolinkResult::NACK) { 57 | SERIAL2.print("nack\n"); 58 | } else if (error == VitoWiFi::OptolinkResult::CRC) { 59 | SERIAL2.print("crc\n"); 60 | } else if (error == VitoWiFi::OptolinkResult::ERROR) { 61 | SERIAL2.print("error\n"); 62 | } 63 | } 64 | 65 | void setup() { 66 | delay(1000); 67 | SERIAL2.begin(SERIALBAUDRATE); 68 | SERIAL2.print("Setting up vitoWiFi\n"); 69 | 70 | vitoWiFi.onResponse(onResponse); 71 | vitoWiFi.onError(onError); 72 | vitoWiFi.begin(); 73 | 74 | SERIAL2.print("Setup finished\n"); 75 | } 76 | 77 | void loop() { 78 | static uint32_t lastMillis = 0; 79 | if (millis() - lastMillis > 60000UL) { // read all values every 60 seconds 80 | lastMillis = millis(); 81 | readValues = true; 82 | datapointIndex = 0; 83 | } 84 | 85 | if (readValues) { 86 | if (vitoWiFi.read(datapoints[datapointIndex])) { 87 | ++datapointIndex; 88 | } 89 | if (datapointIndex == 3) { 90 | readValues = false; 91 | } 92 | } 93 | 94 | vitoWiFi.loop(); 95 | } -------------------------------------------------------------------------------- /examples/simple-read-VS2/simple-read-VS2.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #if defined(ARDUINO_ARCH_ESP8266) 6 | // optolink on full UART, logging output on secondary 7 | #define SERIAL1 Serial 8 | #define SERIAL2 Serial1 9 | #define SERIALBAUDRATE 74880 10 | #elif defined(ARDUINO_ARCH_ESP32) 11 | // optolink on UART2, logging output on UART1 (connected to USB) 12 | #define SERIAL1 Serial1 13 | #define SERIAL2 Serial 14 | #define SERIALBAUDRATE 115200 15 | #endif 16 | 17 | #ifndef SERIALBAUDRATE 18 | #error Target platform not supported 19 | #endif 20 | 21 | VitoWiFi::VitoWiFi vitoWiFi(&SERIAL1); 22 | bool readValues = false; 23 | uint8_t datapointIndex = 0; 24 | 25 | VitoWiFi::Datapoint datapoints[] = { 26 | VitoWiFi::Datapoint("outsidetemp", 0x5525, 2, VitoWiFi::div10), 27 | VitoWiFi::Datapoint("boilertemp", 0x0810, 2, VitoWiFi::div10), 28 | VitoWiFi::Datapoint("pump", 0x2906, 1, VitoWiFi::noconv) 29 | }; 30 | 31 | void onResponse(const VitoWiFi::PacketVS2& response, const VitoWiFi::Datapoint& request) { 32 | // raw data can be accessed through the 'response' argument 33 | SERIAL2.print("Raw data received:"); 34 | const uint8_t* data = response.data(); 35 | for (uint8_t i = 0; i < response.dataLength(); ++i) { 36 | SERIAL2.printf(" %02x", data[i]); 37 | } 38 | SERIAL2.print("\n"); 39 | 40 | // the raw data can be decoded using the datapoint. Be sure to use the correct type 41 | SERIAL2.printf("%s: ", request.name()); 42 | if (request.converter() == VitoWiFi::div10) { 43 | float value = request.decode(response); 44 | SERIAL2.printf("%.1f\n", value); 45 | } else if (request.converter() == VitoWiFi::noconv) { 46 | bool value = request.decode(response); 47 | // alternatively, we can just cast response.data()[0] to bool 48 | SERIAL2.printf("%s\n", value ? "ON" : "OFF"); 49 | } 50 | } 51 | 52 | void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) { 53 | SERIAL2.printf("Datapoint \"%s\" error: ", request.name()); 54 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 55 | SERIAL2.print("timeout\n"); 56 | } else if (error == VitoWiFi::OptolinkResult::LENGTH) { 57 | SERIAL2.print("length\n"); 58 | } else if (error == VitoWiFi::OptolinkResult::NACK) { 59 | SERIAL2.print("nack\n"); 60 | } else if (error == VitoWiFi::OptolinkResult::CRC) { 61 | SERIAL2.print("crc\n"); 62 | } else if (error == VitoWiFi::OptolinkResult::ERROR) { 63 | SERIAL2.print("error\n"); 64 | } 65 | } 66 | 67 | void setup() { 68 | delay(1000); 69 | SERIAL2.begin(SERIALBAUDRATE); 70 | SERIAL2.print("Setting up vitoWiFi\n"); 71 | 72 | vitoWiFi.onResponse(onResponse); 73 | vitoWiFi.onError(onError); 74 | vitoWiFi.begin(); 75 | 76 | SERIAL2.print("Setup finished\n"); 77 | } 78 | 79 | void loop() { 80 | static uint32_t lastMillis = 0; 81 | if (millis() - lastMillis > 60000UL) { // read all values every 60 seconds 82 | lastMillis = millis(); 83 | readValues = true; 84 | datapointIndex = 0; 85 | } 86 | 87 | if (readValues) { 88 | if (vitoWiFi.read(datapoints[datapointIndex])) { 89 | ++datapointIndex; 90 | } 91 | if (datapointIndex == 3) { 92 | readValues = false; 93 | } 94 | } 95 | 96 | vitoWiFi.loop(); 97 | } -------------------------------------------------------------------------------- /examples/simple-write-VS1/simple-write-VS1.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #if defined(ARDUINO_ARCH_ESP8266) 6 | // optolink on full UART, logging output on secondary 7 | #define SERIAL1 Serial 8 | #define SERIAL2 Serial1 9 | #define SERIALBAUDRATE 74880 10 | #elif defined(ARDUINO_ARCH_ESP32) 11 | // optolink on UART2, logging output on UART1 (connected to USB) 12 | #define SERIAL1 Serial1 13 | #define SERIAL2 Serial 14 | #define SERIALBAUDRATE 115200 15 | #endif 16 | 17 | #ifndef SERIALBAUDRATE 18 | #error Target platform not supported 19 | #endif 20 | 21 | VitoWiFi::VitoWiFi vitoWiFi(&SERIAL1); 22 | bool readValues = false; 23 | uint8_t datapointIndex = 0; 24 | uint8_t roomTemperature = 20; 25 | bool writeRoomTemp = false; 26 | 27 | VitoWiFi::Datapoint datapoints[] = { 28 | VitoWiFi::Datapoint("roomtemp", 0x2306, 1, VitoWiFi::noconv), 29 | VitoWiFi::Datapoint("boilertemp", 0x0810, 2, VitoWiFi::div10) 30 | }; 31 | 32 | void setRoomTemp(uint8_t value) { 33 | roomTemperature = value; 34 | writeRoomTemp = true; 35 | } 36 | 37 | void onResponse(const uint8_t* data, uint8_t length, const VitoWiFi::Datapoint& request) { 38 | // raw data can be accessed through the 'response' argument 39 | SERIAL2.print("Raw data received:"); 40 | for (uint8_t i = 0; i < length; ++i) { 41 | SERIAL2.printf(" %02x", data[i]); 42 | } 43 | SERIAL2.print("\n"); 44 | 45 | // the raw data can be decoded using the datapoint. Be sure to use the correct type 46 | SERIAL2.printf("%s: ", request.name()); 47 | if (request.converter() == VitoWiFi::div10) { 48 | float value = request.decode(data, length); 49 | SERIAL2.printf("%.1f\n", value); 50 | } else if (request.converter() == VitoWiFi::noconv) { 51 | // in this example, the response is one byte 52 | SERIAL2.printf("%s\n", (data[0] > 0) ? "ON" : "OFF"); 53 | } 54 | } 55 | 56 | void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) { 57 | SERIAL2.printf("Datapoint \"%s\" error: ", request.name()); 58 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 59 | SERIAL2.print("timeout\n"); 60 | } else if (error == VitoWiFi::OptolinkResult::LENGTH) { 61 | SERIAL2.print("length\n"); 62 | } else if (error == VitoWiFi::OptolinkResult::NACK) { 63 | SERIAL2.print("nack\n"); 64 | } else if (error == VitoWiFi::OptolinkResult::CRC) { 65 | SERIAL2.print("crc\n"); 66 | } else if (error == VitoWiFi::OptolinkResult::ERROR) { 67 | SERIAL2.print("error\n"); 68 | } 69 | } 70 | 71 | void setup() { 72 | delay(1000); 73 | SERIAL2.begin(SERIALBAUDRATE); 74 | SERIAL2.print("Setting up vitoWiFi\n"); 75 | 76 | vitoWiFi.onResponse(onResponse); 77 | vitoWiFi.onError(onError); 78 | vitoWiFi.begin(); 79 | 80 | SERIAL2.print("Setup finished\n"); 81 | } 82 | 83 | void loop() { 84 | static uint32_t lastMillis = 0; 85 | if (millis() - lastMillis > 60000UL) { // read all values every 60 seconds 86 | lastMillis = millis(); 87 | readValues = true; 88 | datapointIndex = 0; 89 | } 90 | 91 | if (readValues) { 92 | if (vitoWiFi.read(datapoints[datapointIndex])) { 93 | ++datapointIndex; 94 | } 95 | if (datapointIndex == 3) { 96 | readValues = false; 97 | } 98 | } 99 | 100 | if (writeRoomTemp) { 101 | if (vitoWiFi.write(datapoints[0], roomTemperature)) { 102 | writeRoomTemp = false; 103 | } 104 | } 105 | 106 | vitoWiFi.loop(); 107 | } -------------------------------------------------------------------------------- /examples/simple-write-VS2/simple-write-VS2.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #if defined(ARDUINO_ARCH_ESP8266) 6 | // optolink on full UART, logging output on secondary 7 | #define SERIAL1 Serial 8 | #define SERIAL2 Serial1 9 | #define SERIALBAUDRATE 74880 10 | #elif defined(ARDUINO_ARCH_ESP32) 11 | // optolink on UART2, logging output on UART1 (connected to USB) 12 | #define SERIAL1 Serial1 13 | #define SERIAL2 Serial 14 | #define SERIALBAUDRATE 115200 15 | #endif 16 | 17 | #ifndef SERIALBAUDRATE 18 | #error Target platform not supported 19 | #endif 20 | 21 | VitoWiFi::VitoWiFi vitoWiFi(&SERIAL1); 22 | bool readValues = false; 23 | uint8_t datapointIndex = 0; 24 | uint8_t roomTemperature = 20; 25 | bool writeRoomTemp = false; 26 | 27 | VitoWiFi::Datapoint datapoints[] = { 28 | VitoWiFi::Datapoint("roomtemp", 0x2306, 1, VitoWiFi::noconv), 29 | VitoWiFi::Datapoint("boilertemp", 0x0810, 2, VitoWiFi::div10) 30 | }; 31 | 32 | void setRoomTemp(uint8_t value) { 33 | roomTemperature = value; 34 | writeRoomTemp = true; 35 | } 36 | 37 | void onResponse(const VitoWiFi::PacketVS2& response, const VitoWiFi::Datapoint& request) { 38 | // raw data can be accessed through the 'response' argument 39 | SERIAL2.print("Raw data received:"); 40 | const uint8_t* data = response.data(); 41 | for (uint8_t i = 0; i < response.dataLength(); ++i) { 42 | SERIAL2.printf(" %02x", data[i]); 43 | } 44 | SERIAL2.print("\n"); 45 | 46 | // the raw data can be decoded using the datapoint. Be sure to use the correct type 47 | SERIAL2.printf("%s: ", request.name()); 48 | if (request.converter() == VitoWiFi::div10) { 49 | float value = request.decode(response); 50 | SERIAL2.printf("%.1f\n", value); 51 | } else if (request.converter() == VitoWiFi::noconv) { 52 | bool value = request.decode(response); 53 | // alternatively, we can just cast response.data()[0] to bool 54 | SERIAL2.printf("%s\n", value ? "ON" : "OFF"); 55 | } 56 | } 57 | 58 | void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) { 59 | SERIAL2.printf("Datapoint \"%s\" error: ", request.name()); 60 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 61 | SERIAL2.print("timeout\n"); 62 | } else if (error == VitoWiFi::OptolinkResult::LENGTH) { 63 | SERIAL2.print("length\n"); 64 | } else if (error == VitoWiFi::OptolinkResult::NACK) { 65 | SERIAL2.print("nack\n"); 66 | } else if (error == VitoWiFi::OptolinkResult::CRC) { 67 | SERIAL2.print("crc\n"); 68 | } else if (error == VitoWiFi::OptolinkResult::ERROR) { 69 | SERIAL2.print("error\n"); 70 | } 71 | } 72 | 73 | void setup() { 74 | delay(1000); 75 | SERIAL2.begin(SERIALBAUDRATE); 76 | SERIAL2.print("Setting up vitoWiFi\n"); 77 | 78 | vitoWiFi.onResponse(onResponse); 79 | vitoWiFi.onError(onError); 80 | vitoWiFi.begin(); 81 | 82 | SERIAL2.print("Setup finished\n"); 83 | } 84 | 85 | void loop() { 86 | static uint32_t lastMillis = 0; 87 | if (millis() - lastMillis > 60000UL) { // read all values every 60 seconds 88 | lastMillis = millis(); 89 | readValues = true; 90 | datapointIndex = 0; 91 | } 92 | 93 | if (readValues) { 94 | if (vitoWiFi.read(datapoints[datapointIndex])) { 95 | ++datapointIndex; 96 | } 97 | if (datapointIndex == 3) { 98 | readValues = false; 99 | } 100 | } 101 | 102 | if (writeRoomTemp) { 103 | if (vitoWiFi.write(datapoints[0], roomTemperature)) { 104 | writeRoomTemp = false; 105 | } 106 | } 107 | 108 | vitoWiFi.loop(); 109 | } -------------------------------------------------------------------------------- /examples/softwareserial/softwareserial.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | const int sRX = 4; // software RX on D2(GPIO 4) 6 | const int sTX = 5; // software TX on D1(GPIO 5) 7 | 8 | EspSoftwareSerial::UART swSer(sRX, sTX); 9 | 10 | // optolink on softwareserial, logging output on hardware UART 11 | 12 | VitoWiFi::VitoWiFi vitoWiFi(&swSer); 13 | bool readValues = false; 14 | uint8_t datapointIndex = 0; 15 | 16 | VitoWiFi::Datapoint datapoints[] = { 17 | VitoWiFi::Datapoint("outsidetemp", 0x5525, 2, VitoWiFi::div10), 18 | VitoWiFi::Datapoint("boilertemp", 0x0810, 2, VitoWiFi::div10), 19 | VitoWiFi::Datapoint("pump", 0x2906, 1, VitoWiFi::noconv) 20 | }; 21 | 22 | void onResponse(const uint8_t* data, uint8_t length, const VitoWiFi::Datapoint& request) { 23 | // raw data can be accessed through the 'response' argument 24 | Serial.print("Raw data received:"); 25 | for (uint8_t i = 0; i < length; ++i) { 26 | Serial.printf(" %02x", data[i]); 27 | } 28 | Serial.print("\n"); 29 | 30 | // the raw data can be decoded using the datapoint. Be sure to use the correct type 31 | Serial.printf("%s: ", request.name()); 32 | if (request.converter() == VitoWiFi::div10) { 33 | float value = request.decode(data, length); 34 | Serial.printf("%.1f\n", value); 35 | } else if (request.converter() == VitoWiFi::noconv) { 36 | // in this example, the response is one byte 37 | Serial.printf("%s\n", (data[0] > 0) ? "ON" : "OFF"); 38 | } 39 | } 40 | 41 | void onError(VitoWiFi::OptolinkResult error, const VitoWiFi::Datapoint& request) { 42 | Serial.printf("Datapoint \"%s\" error: ", request.name()); 43 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 44 | Serial.print("timeout\n"); 45 | } else if (error == VitoWiFi::OptolinkResult::LENGTH) { 46 | Serial.print("length\n"); 47 | } else if (error == VitoWiFi::OptolinkResult::NACK) { 48 | Serial.print("nack\n"); 49 | } else if (error == VitoWiFi::OptolinkResult::CRC) { 50 | Serial.print("crc\n"); 51 | } else if (error == VitoWiFi::OptolinkResult::ERROR) { 52 | Serial.print("error\n"); 53 | } 54 | } 55 | 56 | void setup() { 57 | delay(1000); 58 | Serial.begin(74880); 59 | Serial.print("Setting up vitoWiFi\n"); 60 | 61 | vitoWiFi.onResponse(onResponse); 62 | vitoWiFi.onError(onError); 63 | vitoWiFi.begin(); 64 | 65 | Serial.print("Setup finished\n"); 66 | } 67 | 68 | void loop() { 69 | static uint32_t lastMillis = 0; 70 | if (millis() - lastMillis > 60000UL) { // read all values every 60 seconds 71 | lastMillis = millis(); 72 | readValues = true; 73 | datapointIndex = 0; 74 | } 75 | 76 | if (readValues) { 77 | if (vitoWiFi.read(datapoints[datapointIndex])) { 78 | ++datapointIndex; 79 | } 80 | if (datapointIndex == 3) { 81 | readValues = false; 82 | } 83 | } 84 | 85 | vitoWiFi.loop(); 86 | } 87 | -------------------------------------------------------------------------------- /idf_component.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | arduino-esp32: ">=3" 3 | description: Communicate with Viessmann boilers using the optolink for ESP8266 and ESP32 4 | url: https://github.com/bertmelis/VitoWiFi 5 | version: 3.0.0 -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # Datatypes (KEYWORD1) 3 | ####################################### 4 | 5 | VitoWifi KEYWORD1 6 | Datapoint KEYWORD1 7 | PacketVS2 KEYWORD1 8 | 9 | ####################################### 10 | # Methods and Functions (KEYWORD2) 11 | ####################################### 12 | 13 | #VitoWifi 14 | begin KEYWORD2 15 | loop KEYWORD2 16 | onResponse KEYWORD2 17 | onError KEYWORD2 18 | read KEYWORD2 19 | write KEYWORD2 20 | 21 | #Datapoint public methods 22 | name KEYWORD2 23 | address KEYWORD2 24 | length KEYWORD2 25 | decode KEYWORD2 26 | encode KEYWORD2 27 | 28 | #PacketVS2 public methods 29 | createPacket KEYWORD2 30 | length KEYWORD2 31 | packetType KEYWORD2 32 | functionCode KEYWORD2 33 | id KEYWORD2 34 | address KEYWORD2 35 | dataLength KEYWORD2 36 | data KEYWORD2 37 | checksum KEYWORD2 38 | #reset KEYWORD2 39 | 40 | ####################################### 41 | # Constants (LITERAL1) 42 | ####################################### 43 | 44 | #Protocols 45 | GWG LITERAL1 46 | VS1 LITERAL1 47 | VS2 LITERAL1 48 | 49 | #Enums 50 | TIMEOUT LITERAL1 51 | LENGTH LITERAL1 52 | NACK LITERAL1 53 | CRC LITERAL1 54 | ERROR LITERAL1 55 | 56 | #Transformation 57 | div2 LITERAL2 58 | div10 LITERAL2 59 | div3600 LITERAL2 60 | noconv LITERAL2 61 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VitoWiFi", 3 | "version": "3.0.1", 4 | "keywords": "iot, home, automation, Arduino, esp8266, esp32, Viessmann, serial, optolink", 5 | "description": "Communicate with Viessmann boilers using the optolink for ESP8266 and ESP32", 6 | "homepage": "https://github.com/bertmelis/VitoWiFi", 7 | "license": "MIT", 8 | "authors": { 9 | "name": "Bert Melis", 10 | "url": "https://github.com/bertmelis", 11 | "maintainer": true 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/bertmelis/VitoWiFi.git", 16 | "branch": "master" 17 | }, 18 | "frameworks": [ 19 | "arduino", 20 | "*" 21 | ], 22 | "platforms": [ 23 | "espressif8266", 24 | "espressif32", 25 | "native" 26 | ], 27 | "dependencies": [ 28 | { 29 | "owner": "plerup", 30 | "name": "EspSoftwareSerial", 31 | "version": ">=1.2.2", 32 | "platforms": [ 33 | "espressif8266", 34 | "espressif32" 35 | ] 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=VitoWiFi 2 | version=3.0.1 3 | author=Bert Melis 4 | maintainer=Bert Melis 5 | sentence=Communicate with Viessmann boilers using the optolink for ESP8266 and ESP32 6 | paragraph= 7 | category=Communication 8 | url=https://github.com/bertmelis/VitoWiFi 9 | architectures=esp8266,esp32 -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [common] 2 | build_flags = 3 | -D DEBUG_VITOWIFI=1 4 | -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE 5 | -Wall 6 | -Wextra 7 | -Wpedantic 8 | -std=c++11 9 | 10 | [env:native] 11 | platform = native 12 | test_build_src = yes 13 | build_flags = 14 | ${common.build_flags} 15 | -lgcov 16 | --coverage 17 | -D VW_START_PAYLOAD_LENGTH=10 18 | extra_scripts = test_coverage.py 19 | build_type = debug 20 | test_testing_command = 21 | valgrind 22 | --leak-check=full 23 | --show-leak-kinds=all 24 | --track-origins=yes 25 | --error-exitcode=1 26 | ${platformio.build_dir}/${this.__env__}/program -------------------------------------------------------------------------------- /src/Constants.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "Constants.h" 10 | 11 | namespace VitoWiFi { 12 | 13 | const char* errorToString(OptolinkResult error) { 14 | if (error == VitoWiFi::OptolinkResult::TIMEOUT) { 15 | return "timeout"; 16 | } else if (error == VitoWiFi::OptolinkResult::LENGTH) { 17 | return "length"; 18 | } else if (error == VitoWiFi::OptolinkResult::NACK) { 19 | return "nack"; 20 | } else if (error == VitoWiFi::OptolinkResult::CRC) { 21 | return "crc"; 22 | } else if (error == VitoWiFi::OptolinkResult::ERROR) { 23 | return "error"; 24 | } 25 | return "invaled error"; 26 | } 27 | 28 | } // end namespace VitoWiFi 29 | -------------------------------------------------------------------------------- /src/Constants.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | #ifndef VW_START_PAYLOAD_LENGTH 15 | #define VW_START_PAYLOAD_LENGTH 10 16 | #endif 17 | 18 | namespace VitoWiFi { 19 | 20 | constexpr size_t START_PAYLOAD_LENGTH = VW_START_PAYLOAD_LENGTH; 21 | 22 | enum class FunctionCode : uint8_t { 23 | READ = 0x01, 24 | WRITE = 0x02, 25 | RPC = 0x07, 26 | }; 27 | 28 | enum class PacketType : uint8_t { 29 | REQUEST = 0x00, 30 | RESPONSE = 0x01, 31 | UNACKED = 0x02, 32 | ERROR = 0x03, 33 | }; 34 | 35 | constexpr struct { 36 | uint8_t READ = 0xF7; 37 | uint8_t WRITE = 0xF4; 38 | } PacketVS1Type; 39 | 40 | constexpr struct { 41 | uint8_t READ = 0xCB; 42 | uint8_t WRITE = 0xC8; 43 | } PacketGWGType; 44 | 45 | enum class OptolinkResult { 46 | CONTINUE, 47 | PACKET, 48 | TIMEOUT, 49 | LENGTH, 50 | NACK, 51 | CRC, 52 | ERROR 53 | }; 54 | 55 | const char* errorToString(OptolinkResult error); 56 | 57 | } // end namespace VitoWiFi 58 | 59 | namespace VitoWiFiInternals { 60 | 61 | constexpr struct { 62 | uint8_t PACKETSTART = 0x41; 63 | uint8_t ACK = 0x06; 64 | uint8_t ENQ_ACK = 0x01; 65 | uint8_t NACK = 0x15; 66 | uint8_t ENQ = 0x05; 67 | uint8_t EOT = 0x04; 68 | uint8_t SYNC[3] = {0x16, 0x00, 0x00}; 69 | } ProtocolBytes; 70 | 71 | enum class ParserResult { 72 | CONTINUE, 73 | COMPLETE, 74 | CS_ERROR, 75 | ERROR 76 | }; 77 | 78 | } // end namespace VitoWiFiInternals 79 | -------------------------------------------------------------------------------- /src/Datapoint/ConversionHelpers.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "ConversionHelpers.h" 10 | 11 | bool isDigit(const char c) { 12 | return c >= '0' && c <= '9'; 13 | } 14 | 15 | namespace VitoWiFi { 16 | 17 | std::size_t encodeSchedule(const char* schedule, std::size_t len, uint8_t* output) { 18 | enum ScheduleParserStep { 19 | Hours1, 20 | Hours2, 21 | Minutes1, 22 | Minutes2, 23 | Space, 24 | }; 25 | 26 | std::memset(output, 0x00, 8); 27 | 28 | std::size_t i = 0 - 1; // first operation in iteration is ++i 29 | std::size_t scheduleIndex = 0; 30 | ScheduleParserStep step = ScheduleParserStep::Hours1; 31 | unsigned int hour = 0; 32 | unsigned int minutes = 0; 33 | 34 | while (++i < len) { 35 | if (step == ScheduleParserStep::Hours1) { 36 | if (isDigit(schedule[i])) { 37 | hour = schedule[i] - '0'; 38 | step = ScheduleParserStep::Hours2; 39 | continue; 40 | } 41 | } else if (step == ScheduleParserStep::Hours2) { 42 | if (isDigit(schedule[i])) { 43 | hour = hour * 10 + (schedule[i] - '0'); 44 | continue; 45 | } else if (schedule[i] == ':') { 46 | step = ScheduleParserStep::Minutes1; 47 | continue; 48 | } 49 | } else if (step == ScheduleParserStep::Minutes1) { 50 | if (isDigit(schedule[i])) { 51 | minutes = schedule[i] - '0'; 52 | step = ScheduleParserStep::Minutes2; 53 | continue; 54 | } 55 | } else if (step == ScheduleParserStep::Minutes2) { 56 | if (schedule[i] == '0') { 57 | minutes = minutes * 10 + (schedule[i] - '0'); 58 | // parsing is possibly complete 59 | if (hour <= 23 || minutes <= 59) { 60 | output[scheduleIndex] = (0xF8 & hour << 3) | minutes / 10; 61 | ++scheduleIndex; 62 | step = ScheduleParserStep::Space; 63 | continue; 64 | } 65 | } 66 | } else { // step == ScheduleParserStep::Space 67 | if (schedule[i] == ' ') { 68 | step = ScheduleParserStep::Hours1; 69 | continue; 70 | } 71 | } 72 | return 0; 73 | } 74 | if (scheduleIndex % 2 == 0 && step == ScheduleParserStep::Space) { 75 | // TODO(bertmelis): hours have to be ordered 76 | return (scheduleIndex + 1) / 2; 77 | } 78 | return 0; 79 | } 80 | 81 | std::size_t encodeSchedule(const char* schedule, uint8_t* output) { 82 | return encodeSchedule(schedule, strlen(schedule), output); 83 | } 84 | 85 | std::size_t decodeSchedule(const uint8_t* data, std::size_t len, char* output, std::size_t maxLen) { 86 | assert(len == 8); 87 | assert(maxLen >= 48); // 8 times 07:30, 7 spaces and 0-terminator --> 8 * 5 + 7 * 1 + 1 88 | 89 | std::size_t pos = 0; 90 | for (std::size_t i = 0; i < 8; ++i) { 91 | unsigned int hour = data[i] >> 3; 92 | unsigned int minutes = (data[i] & 0x07) * 10; 93 | if (hour > 23 || minutes > 59) { 94 | hour = 0; 95 | minutes = 0; 96 | } 97 | int result = snprintf(&output[pos], maxLen - pos, "%.2u:%.2u", hour, minutes); 98 | if (result < 0) return 0; 99 | pos += result; 100 | if (i < 7) { 101 | output[pos++] = ' '; 102 | } 103 | } 104 | return pos + 1; // include 0-terminator 105 | } 106 | 107 | } // end namespace VitoWiFi 108 | -------------------------------------------------------------------------------- /src/Datapoint/ConversionHelpers.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include // strlen 15 | #include // snprintf 16 | 17 | namespace VitoWiFi { 18 | 19 | /* 20 | Encodes a c-string containing a schedule to an 8-byte schedule sequence. 21 | Returns the number of pairs encoded 22 | Per byte: 23 | - b7-b3 hour 24 | - b2-b1 minute * 10 25 | 26 | Output needs to be at least 8 bytes 27 | 28 | A scedule consists of time pairs with minutes rounded to a multiple of 10min. 29 | Hours can be with leading zero or not. 30 | 31 | Valid: 32 | - 7:30 8:30 15:00 23:50 33 | - 07:30 8:30 15:00 23:50 34 | 35 | Invalid: 36 | - time not specified in pairs 37 | - (hours not ordered earliest to latest) 38 | - other formatting than colons or spaces 39 | - minutes not rounded to multiples of 10 40 | - whitespace not trimmed 41 | */ 42 | std::size_t encodeSchedule(const char* schedule, std::size_t len, uint8_t* output); 43 | std::size_t encodeSchedule(const char* schedule, uint8_t* output); 44 | 45 | /* 46 | Decodes a byte series to a human-readable schedule consisting of time pairs. 47 | Returns the number of characters written including 0-terminator 48 | (will always result in 48 or 0 in case of an error) 49 | 50 | Although passed as variables, the function fails when 51 | - len != 8 52 | - maxLen < 48 53 | */ 54 | std::size_t decodeSchedule(const uint8_t* data, std::size_t len, char* output, std::size_t maxLen); 55 | 56 | }; // end namespace VitoWiFi 57 | -------------------------------------------------------------------------------- /src/Datapoint/Converter.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "Converter.h" 10 | 11 | namespace VitoWiFi { 12 | 13 | VariantValue Div10Convert::decode(const uint8_t* data, uint8_t len) const { 14 | assert(len == 1 || len == 2); 15 | float retVal = 0; 16 | if (len == 1) { 17 | int8_t val = data[0]; 18 | retVal = val / 10.f; 19 | } 20 | if (len == 2) { 21 | int16_t val = data[1] << 8 | data[0]; 22 | retVal = val / 10.f; 23 | } 24 | return VariantValue(retVal); 25 | } 26 | 27 | void Div10Convert::encode(uint8_t* buf, uint8_t len, const VariantValue& val) const { 28 | assert(len == 1 || len == 2); 29 | (void) len; 30 | float srcVal = val; 31 | int16_t tmp = floor((srcVal * 10.f) + 0.5); 32 | if (len == 2) { 33 | buf[1] = tmp >> 8; 34 | } 35 | buf[0] = tmp & 0xFF; 36 | } 37 | 38 | VariantValue Div2Convert::decode(const uint8_t* data, uint8_t len) const { 39 | assert(len == 1); 40 | float retVal = 0; 41 | int8_t val = data[0]; 42 | retVal = val / 2.f; 43 | return VariantValue(retVal); 44 | } 45 | 46 | void Div2Convert::encode(uint8_t* buf, uint8_t len, const VariantValue& val) const { 47 | assert(len == 1); 48 | (void) len; 49 | float srcVal = val; 50 | int8_t tmp = floor((srcVal * 2.f) + 0.5); 51 | buf[0] = tmp; 52 | } 53 | 54 | VariantValue Div3600Convert::decode(const uint8_t* data, uint8_t len) const { 55 | assert(len == 4); 56 | (void) len; 57 | uint32_t val = data[3] << 24 | data[2] << 16 | data[1] << 8 | data[0]; 58 | float retVal = val / 3600.f; 59 | return VariantValue(retVal); 60 | } 61 | 62 | void Div3600Convert::encode(uint8_t* buf, uint8_t len, const VariantValue& val) const { 63 | assert(len == 4); 64 | (void) len; 65 | float srcVal = val; 66 | uint32_t tmp = floor((srcVal * 3600.f) + 0.5); 67 | buf[3] = tmp >> 24; 68 | buf[2] = tmp >> 16; 69 | buf[1] = tmp >> 8; 70 | buf[0] = tmp & 0xFF; 71 | } 72 | 73 | VariantValue NoconvConvert::decode(const uint8_t* data, uint8_t len) const { 74 | // assert(len == 1 || len == 2 || len == 4); 75 | if (len == 1) { 76 | uint8_t retVal = data[0]; 77 | return VariantValue(retVal); 78 | } else if (len == 2) { 79 | uint16_t retVal = data[1] << 8 | data[0]; 80 | return VariantValue(retVal); 81 | } else if (len == 4) { 82 | uint32_t retVal = data[3] << 24 | data[2] << 16 | data[1] << 8 | data[0]; 83 | return VariantValue(retVal); 84 | } else { 85 | // decoding should be done in user code 86 | uint32_t retVal = 0; 87 | return VariantValue(retVal); 88 | } 89 | } 90 | 91 | void NoconvConvert::encode(uint8_t* buf, uint8_t len, const VariantValue& val) const { 92 | // assert(len == 1 || len == 2 || len == 4); 93 | if (len == 1) { 94 | uint8_t srcVal = val; 95 | buf[0] = srcVal; 96 | } else if (len == 2) { 97 | uint16_t srcVal = val; 98 | buf[1] = srcVal >> 8; 99 | buf[0] = srcVal & 0xFF; 100 | } else if (len == 4) { 101 | uint32_t srcVal = val; 102 | buf[3] = srcVal >> 24; 103 | buf[2] = srcVal >> 16; 104 | buf[1] = srcVal >> 8; 105 | buf[0] = srcVal & 0xFF; 106 | } else { 107 | // encoding should be done by user 108 | std::memset(buf, 0, len); 109 | } 110 | } 111 | 112 | Div10Convert div10; 113 | Div2Convert div2; 114 | Div3600Convert div3600; 115 | NoconvConvert noconv; 116 | 117 | } // end namespace VitoWiFi 118 | -------------------------------------------------------------------------------- /src/Datapoint/Converter.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "../Logging.h" 18 | #include "ConversionHelpers.h" 19 | 20 | namespace VitoWiFi { 21 | 22 | class VariantValue { 23 | public: 24 | explicit VariantValue(uint8_t value): _value(value) {} 25 | explicit VariantValue(uint16_t value): _value(value) {} 26 | explicit VariantValue(uint32_t value): _value(value) {} 27 | explicit VariantValue(uint64_t value): _value(value) {} 28 | explicit VariantValue(float value): _value(value) {} 29 | explicit VariantValue(bool value) : _value(value) {} 30 | operator uint8_t() const { return _value._uint8Val; } 31 | operator uint16_t() const { return _value._uint16Val; } 32 | operator uint32_t() const { return _value._uint32Val; } 33 | operator uint64_t() const { return _value._uint64Val; } 34 | operator float() const { return _value._floatVal; } 35 | operator bool() const { return _value._uint8Val; } 36 | 37 | protected: 38 | union _Value { 39 | _Value(uint8_t v): _uint8Val(v) {} 40 | _Value(uint16_t v): _uint16Val(v) {} 41 | _Value(uint32_t v): _uint32Val(v) {} 42 | _Value(uint64_t v): _uint64Val(v) {} 43 | _Value(float v): _floatVal(v) {} 44 | _Value(bool v): _uint8Val(v) {} 45 | uint8_t _uint8Val; 46 | uint16_t _uint16Val; 47 | uint32_t _uint32Val; 48 | uint64_t _uint64Val; 49 | float _floatVal; 50 | } _value; 51 | }; 52 | 53 | class Converter { 54 | public: 55 | virtual VariantValue decode(const uint8_t* data, uint8_t len) const = 0; 56 | virtual void encode(uint8_t* buf, uint8_t len, const VariantValue& val) const = 0; 57 | bool operator==(const Converter& rhs) const { 58 | return (this == &rhs); 59 | } 60 | }; 61 | 62 | class Div10Convert : public Converter { 63 | public: 64 | VariantValue decode(const uint8_t* data, uint8_t len) const override; 65 | void encode(uint8_t* buf, uint8_t len, const VariantValue& val) const override; 66 | }; 67 | 68 | class Div2Convert : public Converter { 69 | public: 70 | VariantValue decode(const uint8_t* data, uint8_t len) const override; 71 | void encode(uint8_t* buf, uint8_t len, const VariantValue& val) const override; 72 | }; 73 | 74 | class Div3600Convert : public Converter { 75 | public: 76 | VariantValue decode(const uint8_t* data, uint8_t len) const override; 77 | void encode(uint8_t* buf, uint8_t len, const VariantValue& val) const override; 78 | }; 79 | 80 | class NoconvConvert : public Converter { 81 | public: 82 | VariantValue decode(const uint8_t* data, uint8_t len) const override; 83 | void encode(uint8_t* buf, uint8_t len, const VariantValue& val) const override; 84 | }; 85 | 86 | extern Div10Convert div10; 87 | extern Div2Convert div2; 88 | extern Div3600Convert div3600; 89 | extern NoconvConvert noconv; 90 | 91 | } // end namespace VitoWiFi 92 | -------------------------------------------------------------------------------- /src/Datapoint/Datapoint.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "Datapoint.h" 10 | 11 | namespace VitoWiFi { 12 | 13 | Datapoint::Datapoint(const char* name, uint16_t address, uint8_t length, const Converter& converter) 14 | : _name(name) 15 | , _address(address) 16 | , _length(length) 17 | , _converter(&converter) { 18 | // empty 19 | } 20 | 21 | Datapoint::operator bool() const { 22 | if (_length == 0) return false; 23 | return true; 24 | } 25 | 26 | const char* Datapoint::name() const { 27 | return _name; 28 | } 29 | 30 | uint16_t Datapoint::address() const { 31 | return _address; 32 | } 33 | 34 | uint8_t Datapoint::length() const { 35 | return _length; 36 | } 37 | 38 | const Converter& Datapoint::converter() const { 39 | return *_converter; 40 | } 41 | 42 | VariantValue Datapoint::decode(const uint8_t* data, uint8_t length) const { 43 | return (*_converter).decode(data, length); 44 | } 45 | 46 | VariantValue Datapoint::decode(const PacketVS2& packet) const { 47 | return (*_converter).decode(packet.data(), packet.dataLength()); 48 | } 49 | 50 | void Datapoint::encode(uint8_t* buf, uint8_t len, const VariantValue& value) const { 51 | return (*_converter).encode(buf, len, value); 52 | } 53 | 54 | } // end namespace VitoWiFi 55 | -------------------------------------------------------------------------------- /src/Datapoint/Datapoint.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include "Converter.h" 12 | #include "../VS2/PacketVS2.h" 13 | 14 | namespace VitoWiFi { 15 | 16 | class Datapoint { 17 | public: 18 | Datapoint(const char* name, uint16_t address, uint8_t length, const Converter& converter); 19 | 20 | explicit operator bool() const; 21 | const char* name() const; 22 | uint16_t address() const; 23 | uint8_t length() const; 24 | const Converter& converter() const; 25 | 26 | VariantValue decode(const uint8_t* data, uint8_t length) const; 27 | VariantValue decode(const PacketVS2& packet) const; 28 | void encode(uint8_t* buf, uint8_t len, const VariantValue& value) const; 29 | 30 | protected: 31 | const char* _name; 32 | uint16_t _address; 33 | uint8_t _length; 34 | const Converter* _converter; 35 | }; 36 | 37 | } // end namespace VitoWiFi 38 | -------------------------------------------------------------------------------- /src/GWG/GWG.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "GWG.h" 10 | 11 | namespace VitoWiFi { 12 | 13 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 14 | GWG::GWG(HardwareSerial* interface) 15 | : _state(State::UNDEFINED) 16 | , _currentMillis(vw_millis()) 17 | , _lastMillis(_currentMillis) 18 | , _requestTime(0) 19 | , _bytesTransferred(0) 20 | , _interface(nullptr) 21 | , _currentDatapoint(Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv)) 22 | , _currentRequest() 23 | , _responseBuffer(nullptr) 24 | , _allocatedLength(0) 25 | , _onResponseCallback(nullptr) 26 | , _onErrorCallback(nullptr) { 27 | assert(interface != nullptr); 28 | _interface = new(std::nothrow) VitoWiFiInternals::HardwareSerialInterface(interface); 29 | if (!_interface) { 30 | vw_log_e("Could not create serial interface"); 31 | vw_abort(); 32 | } 33 | _responseBuffer = reinterpret_cast(malloc(START_PAYLOAD_LENGTH)); 34 | if (!_responseBuffer) { 35 | vw_log_e("Could not create response buffer"); 36 | vw_abort(); 37 | } 38 | } 39 | 40 | #if defined(ARDUINO_ARCH_ESP8266) 41 | GWG::GWG(SoftwareSerial* interface) 42 | : _state(State::UNDEFINED) 43 | , _currentMillis(vw_millis()) 44 | , _lastMillis(_currentMillis) 45 | , _requestTime(0) 46 | , _bytesTransferred(0) 47 | , _interface(nullptr) 48 | , _currentDatapoint(Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv)) 49 | , _currentRequest() 50 | , _responseBuffer(nullptr) 51 | , _allocatedLength(0) 52 | , _onResponseCallback(nullptr) 53 | , _onErrorCallback(nullptr) { 54 | assert(interface != nullptr); 55 | _interface = new(std::nothrow) VitoWiFiInternals::SoftwareSerialInterface(interface); 56 | if (!_interface) { 57 | vw_log_e("Could not create serial interface"); 58 | vw_abort(); 59 | } 60 | _responseBuffer = reinterpret_cast(malloc(START_PAYLOAD_LENGTH)); 61 | if (!_responseBuffer) { 62 | vw_log_e("Could not create response buffer"); 63 | vw_abort(); 64 | } 65 | } 66 | #endif 67 | 68 | #else 69 | GWG::GWG(const char* interface) 70 | : _state(State::UNDEFINED) 71 | , _currentMillis(vw_millis()) 72 | , _lastMillis(_currentMillis) 73 | , _requestTime(0) 74 | , _bytesTransferred(0) 75 | , _interface(nullptr) 76 | , _currentDatapoint(Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv)) 77 | , _currentRequest() 78 | , _responseBuffer(nullptr) 79 | , _allocatedLength(0) 80 | , _onResponseCallback(nullptr) 81 | , _onErrorCallback(nullptr) { 82 | assert(interface != nullptr); 83 | _interface = new(std::nothrow) VitoWiFiInternals::LinuxSerialInterface(interface); 84 | if (!_interface) { 85 | vw_log_e("Could not create serial interface"); 86 | vw_abort(); 87 | } 88 | _responseBuffer = reinterpret_cast(malloc(START_PAYLOAD_LENGTH)); 89 | if (!_responseBuffer) { 90 | vw_log_e("Could not create response buffer"); 91 | vw_abort(); 92 | } 93 | } 94 | #endif 95 | 96 | GWG::~GWG() { 97 | delete _interface; 98 | free(_responseBuffer); 99 | } 100 | 101 | void GWG::onResponse(OnResponseCallback callback) { 102 | _onResponseCallback = callback; 103 | } 104 | void GWG::onError(OnErrorCallback callback) { 105 | _onErrorCallback = callback; 106 | } 107 | 108 | bool GWG::read(const Datapoint& datapoint) { 109 | if (_currentDatapoint) { 110 | return false; 111 | } 112 | if (_currentRequest.createPacket(PacketGWGType.READ, 113 | datapoint.address(), 114 | datapoint.length()) && 115 | _expandResponseBuffer(datapoint.length())) { 116 | _currentDatapoint = datapoint; 117 | _requestTime = _currentMillis; 118 | vw_log_i("reading packet OK"); 119 | return true; 120 | } 121 | vw_log_i("reading not possible, packet creation error"); 122 | return false; 123 | } 124 | 125 | bool GWG::write(const Datapoint& datapoint, const VariantValue& value) { 126 | if (_currentDatapoint) { 127 | return false; 128 | } 129 | uint8_t* payload = reinterpret_cast(malloc(datapoint.length())); 130 | if (!payload) return false; 131 | datapoint.encode(payload, datapoint.length(), value); 132 | bool result = write(datapoint, payload, datapoint.length()); 133 | free(payload); 134 | return result; 135 | } 136 | 137 | bool GWG::write(const Datapoint& datapoint, const uint8_t* data, uint8_t length) { 138 | if (_currentDatapoint) { 139 | return false; 140 | } 141 | if (length != datapoint.length()) { 142 | vw_log_i("writing not possible, length mismatch"); 143 | return false; 144 | } 145 | if (_currentRequest.createPacket(PacketGWGType.WRITE, 146 | datapoint.address(), 147 | datapoint.length(), 148 | data) && 149 | _expandResponseBuffer(datapoint.length())) { 150 | _currentDatapoint = datapoint; 151 | _requestTime = _currentMillis; 152 | vw_log_i("writing packet OK"); 153 | return true; 154 | } 155 | vw_log_i("writing not possible, packet creation error"); 156 | return false; 157 | } 158 | 159 | bool GWG::begin() { 160 | _setState(State::INIT); 161 | return _interface->begin(); 162 | } 163 | 164 | void GWG::loop() { 165 | _currentMillis = vw_millis(); 166 | switch (_state) { 167 | case State::INIT: 168 | _init(); 169 | break; 170 | case State::SEND: 171 | _send(); 172 | break; 173 | case State::RECEIVE: 174 | _receive(); 175 | break; 176 | case State::UNDEFINED: 177 | // begin() not yet called 178 | break; 179 | } 180 | // double timeout to accomodate for connection initialization 181 | if (_currentDatapoint && _currentMillis - _requestTime > 3000UL) { 182 | _setState(State::INIT); 183 | _tryOnError(OptolinkResult::TIMEOUT); 184 | } 185 | } 186 | 187 | void GWG::end() { 188 | _interface->end(); 189 | _setState(State::UNDEFINED); 190 | _currentDatapoint = Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv); 191 | } 192 | 193 | int GWG::getState() const { 194 | return static_cast::type>(_state); 195 | } 196 | 197 | bool GWG::isBusy() const { 198 | if (_currentDatapoint) { 199 | return true; 200 | } 201 | return false; 202 | } 203 | 204 | void GWG::_setState(State state) { 205 | vw_log_i("state %i --> %i", static_cast::type>(_state), static_cast::type>(state)); 206 | _state = state; 207 | } 208 | 209 | void GWG::_init() { 210 | if (_interface->available()) { 211 | if (_interface->read() == VitoWiFiInternals::ProtocolBytes.ENQ && _currentDatapoint) { 212 | _bytesTransferred = 0; 213 | _setState(State::SEND); 214 | } 215 | } 216 | } 217 | 218 | void GWG::_send() { 219 | _bytesTransferred += _interface->write(&_currentRequest[_bytesTransferred], _currentRequest.length() - _bytesTransferred); 220 | if (_bytesTransferred == _currentRequest.length()) { 221 | _bytesTransferred = 0; 222 | _lastMillis = _currentMillis; 223 | _setState(State::RECEIVE); 224 | } 225 | } 226 | 227 | void GWG::_receive() { 228 | while (_interface->available()) { 229 | _responseBuffer[_bytesTransferred] = _interface->read(); 230 | ++_bytesTransferred; 231 | _lastMillis = _currentMillis; 232 | } 233 | if (_bytesTransferred == _currentRequest.length()) { 234 | _setState(State::INIT); 235 | _tryOnResponse(); 236 | } 237 | } 238 | 239 | void GWG::_tryOnResponse() { 240 | if (_onResponseCallback) { 241 | _onResponseCallback(_responseBuffer, _currentRequest.length(), _currentDatapoint); 242 | } 243 | } 244 | 245 | void GWG::_tryOnError(OptolinkResult result) { 246 | if (_onErrorCallback) { 247 | _onErrorCallback(result, _currentDatapoint); 248 | } 249 | _currentDatapoint = Datapoint(nullptr, 0, 0, noconv); 250 | } 251 | 252 | bool GWG::_expandResponseBuffer(uint8_t newSize) { 253 | if (newSize > _allocatedLength) { 254 | uint8_t* newBuffer = reinterpret_cast(realloc(_responseBuffer, newSize)); 255 | if (!newBuffer) { 256 | return false; 257 | } 258 | _responseBuffer = newBuffer; 259 | _allocatedLength = newSize; 260 | } 261 | return true; 262 | } 263 | 264 | } // end namespace VitoWiFi 265 | -------------------------------------------------------------------------------- /src/GWG/GWG.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | 13 | #include "Logging.h" 14 | #include "../Constants.h" 15 | #include "../Helpers.h" 16 | #include "PacketGWG.h" 17 | #include "../Datapoint/Datapoint.h" 18 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 19 | #include "../Interface/HardwareSerialInterface.h" 20 | #if defined(ARDUINO_ARCH_ESP8266) 21 | #include "../Interface/SoftwareSerialInterface.h" 22 | #endif 23 | #elif defined(__linux__) 24 | #include "../Interface/LinuxSerialInterface.h" 25 | #endif 26 | #include "../Interface/GenericInterface.h" 27 | 28 | namespace VitoWiFi { 29 | 30 | class GWG { 31 | public: 32 | typedef std::function OnResponseCallback; 33 | typedef std::function OnErrorCallback; 34 | 35 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 36 | explicit GWG(HardwareSerial* interface); 37 | #if defined(ARDUINO_ARCH_ESP8266) 38 | explicit GWG(SoftwareSerial* interface); 39 | #endif 40 | #else 41 | explicit GWG(const char* interface); 42 | #endif 43 | template 44 | explicit GWG(C* interface) 45 | : _state(State::UNDEFINED) 46 | , _currentMillis(vw_millis()) 47 | , _lastMillis(_currentMillis) 48 | , _requestTime(0) 49 | , _bytesTransferred(0) 50 | , _interface(nullptr) 51 | , _currentDatapoint(Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv)) 52 | , _currentRequest() 53 | , _responseBuffer(nullptr) 54 | , _allocatedLength(0) 55 | , _onResponseCallback(nullptr) 56 | , _onErrorCallback(nullptr) { 57 | assert(interface != nullptr); 58 | _interface = new(std::nothrow) VitoWiFiInternals::GenericInterface(interface); 59 | if (!_interface) { 60 | vw_log_e("Could not create serial interface"); 61 | vw_abort(); 62 | } 63 | _responseBuffer = reinterpret_cast(malloc(START_PAYLOAD_LENGTH)); 64 | if (!_responseBuffer) { 65 | vw_log_e("Could not create response buffer"); 66 | vw_abort(); 67 | } 68 | } 69 | ~GWG(); 70 | GWG(const GWG&) = delete; 71 | GWG & operator=(const GWG&) = delete; 72 | 73 | void onResponse(OnResponseCallback callback); 74 | void onError(OnErrorCallback callback); 75 | 76 | bool read(const Datapoint& datapoint); 77 | bool write(const Datapoint& datapoint, const VariantValue& value); 78 | bool write(const Datapoint& datapoint, const uint8_t* data, uint8_t length); 79 | 80 | bool begin(); 81 | void loop(); 82 | void end(); 83 | 84 | int getState() const; 85 | bool isBusy() const; 86 | 87 | private: 88 | enum class State { 89 | INIT, 90 | SEND, 91 | RECEIVE, 92 | UNDEFINED 93 | } _state; 94 | uint32_t _currentMillis; 95 | uint32_t _lastMillis; 96 | uint32_t _requestTime; 97 | uint8_t _bytesTransferred; 98 | VitoWiFiInternals::SerialInterface* _interface; 99 | Datapoint _currentDatapoint; 100 | PacketGWG _currentRequest; 101 | uint8_t* _responseBuffer; 102 | uint8_t _allocatedLength; 103 | OnResponseCallback _onResponseCallback; 104 | OnErrorCallback _onErrorCallback; 105 | 106 | inline void _setState(State state); 107 | 108 | void _init(); 109 | void _send(); 110 | void _receive(); 111 | 112 | void _tryOnResponse(); 113 | void _tryOnError(OptolinkResult result); 114 | 115 | bool _expandResponseBuffer(uint8_t newSize); 116 | }; 117 | 118 | } // end namespace VitoWiFi 119 | -------------------------------------------------------------------------------- /src/GWG/PacketGWG.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "PacketGWG.h" 10 | 11 | namespace VitoWiFi { 12 | 13 | PacketGWG::PacketGWG() 14 | : _allocatedLength(START_PAYLOAD_LENGTH + 5) 15 | , _buffer(nullptr) { 16 | _buffer = reinterpret_cast(malloc(_allocatedLength)); 17 | if (!_buffer) { 18 | _allocatedLength = 0; 19 | vw_abort(); 20 | } 21 | reset(); 22 | } 23 | 24 | PacketGWG::~PacketGWG() { 25 | free(_buffer); 26 | } 27 | 28 | PacketGWG::operator bool() const { 29 | if (_buffer && _buffer[3] != 0) return true; 30 | return false; 31 | } 32 | 33 | /* 34 | uint8_t PacketGWG::operator[](std::size_t index) const { 35 | return _buffer[index]; 36 | } 37 | */ 38 | 39 | uint8_t& PacketGWG::operator[](std::size_t index) { 40 | return _buffer[index]; 41 | } 42 | 43 | bool PacketGWG::createPacket(uint8_t packetType, uint16_t addr, uint8_t len, const uint8_t* data) { 44 | reset(); 45 | 46 | // check arguments 47 | if (len == 0) { 48 | vw_log_w("Zero length given"); 49 | return false; 50 | } 51 | if (addr > 0xFF) { 52 | vw_log_w("GWG doesn't support addresses > 0xFF"); 53 | return false; 54 | } 55 | if (packetType != PacketGWGType.READ && packetType != PacketGWGType.WRITE) { 56 | vw_log_w("Packet type error: 0x%02x", packetType); 57 | return false; 58 | } 59 | if (packetType == PacketGWGType.WRITE && !data) { 60 | vw_log_w("No data for write packet"); 61 | return false; 62 | } 63 | 64 | // reserve memory 65 | std::size_t toAllocate = (packetType == PacketGWGType.WRITE) ? len + 5 : 5; 66 | if (toAllocate > _allocatedLength) { 67 | uint8_t* newBuffer = reinterpret_cast(realloc(_buffer, toAllocate)); 68 | if (!newBuffer) { 69 | return false; 70 | } 71 | _buffer = newBuffer; 72 | _allocatedLength = toAllocate; 73 | } 74 | 75 | // 2. Serialize into buffer 76 | size_t step = 0; 77 | _buffer[step++] = VitoWiFiInternals::ProtocolBytes.ENQ_ACK; 78 | _buffer[step++] = packetType; 79 | _buffer[step++] = addr & 0xFF; 80 | _buffer[step++] = len; 81 | if (packetType == PacketGWGType.WRITE) { 82 | for (uint8_t i = 0; i < len; ++i) { 83 | _buffer[step++] = data[i]; 84 | } 85 | } 86 | _buffer[step] = VitoWiFiInternals::ProtocolBytes.EOT; 87 | return true; 88 | } 89 | 90 | uint8_t PacketGWG::length() const { 91 | if (_buffer[3] == 0) return 0; 92 | if (_buffer[1] == PacketGWGType.READ) return 5; 93 | if (_buffer[1] == PacketGWGType.WRITE) return _buffer[3] + 5; 94 | return 0; // should not be possible 95 | } 96 | 97 | uint8_t PacketGWG::packetType() const { 98 | return _buffer[1]; 99 | } 100 | 101 | uint16_t PacketGWG::address() const { 102 | return _buffer[2]; 103 | } 104 | 105 | uint8_t PacketGWG::dataLength() const { 106 | return _buffer[3]; 107 | } 108 | 109 | const uint8_t* PacketGWG::data() const { 110 | return &_buffer[4]; 111 | } 112 | 113 | void PacketGWG::reset() { 114 | _buffer[3] = 0x00; 115 | } 116 | 117 | } // end namespace VitoWiFi 118 | -------------------------------------------------------------------------------- /src/GWG/PacketGWG.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "../Constants.h" 17 | #include "../Helpers.h" 18 | #include "../Logging.h" 19 | 20 | namespace VitoWiFi { 21 | 22 | class PacketGWG { 23 | public: 24 | PacketGWG(); 25 | ~PacketGWG(); 26 | PacketGWG (const PacketGWG&) = delete; 27 | PacketGWG& operator =(const PacketGWG&) = delete; 28 | operator bool() const; 29 | /* uint8_t operator[](std::size_t index) const; */ 30 | uint8_t& operator[](std::size_t index); 31 | 32 | public: 33 | bool createPacket(uint8_t packetType, uint16_t addr, uint8_t len, const uint8_t* data = nullptr); 34 | uint8_t length() const; 35 | uint8_t packetType() const; 36 | uint16_t address() const; 37 | uint8_t dataLength() const; 38 | const uint8_t* data() const; 39 | 40 | void reset(); 41 | 42 | protected: 43 | std::size_t _allocatedLength; 44 | uint8_t* _buffer; 45 | }; 46 | 47 | } // end namespace VitoWiFi 48 | -------------------------------------------------------------------------------- /src/Helpers.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #if defined(__linux__) 16 | #include // NOLINT [build/c++11] 17 | #define vw_millis() std::chrono::duration_cast>(std::chrono::system_clock::now().time_since_epoch()).count() 18 | #else 19 | #define vw_millis() millis() 20 | #endif 21 | 22 | #define vw_abort() abort() 23 | -------------------------------------------------------------------------------- /src/Interface/GenericInterface.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | 13 | #include "SerialInterface.h" 14 | 15 | namespace VitoWiFiInternals { 16 | 17 | template 18 | class GenericInterface : public SerialInterface { 19 | public: 20 | explicit GenericInterface(C* interface) 21 | : _interface(interface) { 22 | if (!interface) { 23 | abort(); 24 | } 25 | } 26 | bool begin() override { 27 | return _interface->begin(); 28 | } 29 | void end() override { 30 | _interface->end(); 31 | } 32 | std::size_t write(const uint8_t* data, uint8_t length) override { 33 | return _interface->write(data, length); 34 | } 35 | uint8_t read() override { 36 | return _interface->read(); 37 | } 38 | size_t available() override { 39 | return _interface->available(); 40 | } 41 | 42 | private: 43 | C* _interface; 44 | }; 45 | 46 | } // end namespace VitoWiFiInternals 47 | -------------------------------------------------------------------------------- /src/Interface/HardwareSerialInterface.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 10 | 11 | #include "HardwareSerialInterface.h" 12 | 13 | namespace VitoWiFiInternals { 14 | HardwareSerialInterface::HardwareSerialInterface(HardwareSerial* interface) 15 | : _interface(interface) { 16 | assert(interface); 17 | } 18 | 19 | bool HardwareSerialInterface::begin() { 20 | _interface->begin(4800, SERIAL_8E2); 21 | return (*_interface); 22 | } 23 | 24 | void HardwareSerialInterface::end() { 25 | _interface->end(); 26 | } 27 | 28 | std::size_t HardwareSerialInterface::write(const uint8_t* data, uint8_t length) { 29 | return _interface->write(data, length); 30 | } 31 | 32 | uint8_t HardwareSerialInterface::read() { 33 | uint8_t retVal = 0; 34 | _interface->read(&retVal, 1); 35 | return retVal; 36 | } 37 | 38 | size_t HardwareSerialInterface::available() { 39 | return _interface->available(); 40 | } 41 | 42 | } // end namespace VitoWiFiInternals 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /src/Interface/HardwareSerialInterface.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 12 | 13 | #include 14 | 15 | #include 16 | #include "SerialInterface.h" 17 | 18 | namespace VitoWiFiInternals { 19 | 20 | class HardwareSerialInterface : public SerialInterface { 21 | public: 22 | explicit HardwareSerialInterface(HardwareSerial* interface); 23 | bool begin(); 24 | void end(); 25 | std::size_t write(const uint8_t* data, uint8_t length) override; 26 | uint8_t read() override; 27 | size_t available() override; 28 | 29 | private: 30 | HardwareSerial* _interface; 31 | }; 32 | 33 | } // end namespace VitoWiFiInternals 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /src/Interface/LinuxSerialInterface.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #if defined(__linux__) 10 | 11 | #include "LinuxSerialInterface.h" 12 | 13 | namespace VitoWiFiInternals { 14 | 15 | LinuxSerialInterface::LinuxSerialInterface(const char* interface) 16 | : _interfaceName(interface) 17 | , _fd(0) 18 | , _tty() { 19 | assert(interface); 20 | } 21 | 22 | bool LinuxSerialInterface::begin() { 23 | _fd = open(_interfaceName, O_RDWR); 24 | if (_fd < 0) { 25 | vw_log_e("Error %i from open: %s\n", errno, strerror(errno)); 26 | return false; 27 | } 28 | if (tcgetattr(_fd, &_tty) != 0) { 29 | vw_log_e("Error %i from tcgetattr: %s\n", errno, strerror(errno)); 30 | return false; 31 | } 32 | 33 | _tty.c_cflag |= PARENB; // parity enable 34 | _tty.c_cflag |= CSTOPB; // 2 stop bits 35 | _tty.c_cflag |= CS8; // 8 bits per byte 36 | _tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control 37 | _tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1) 38 | 39 | _tty.c_lflag &= ~ICANON; 40 | _tty.c_lflag &= ~ECHO; // Disable echo 41 | _tty.c_lflag &= ~ECHOE; // Disable erasure 42 | _tty.c_lflag &= ~ECHONL; // Disable new-line echo 43 | _tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP 44 | _tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl 45 | _tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes 46 | 47 | _tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars) 48 | _tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed 49 | // _tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT ON LINUX) 50 | // _tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT ON LINUX) 51 | 52 | _tty.c_cc[VTIME] = 10; // Wait for up to 1s (10 deciseconds), returning as soon as any data is received. 53 | _tty.c_cc[VMIN] = 0; 54 | 55 | // Set in/out baud rate to be 9600 56 | cfsetispeed(&_tty, B4800); 57 | cfsetospeed(&_tty, B4800); 58 | 59 | // Save tty settings, also checking for error 60 | if (tcsetattr(_fd, TCSANOW, &_tty) != 0) { 61 | vw_log_e("Error %i from tcsetattr: %s\n", errno, strerror(errno)); 62 | return false; 63 | } 64 | 65 | return true; 66 | } 67 | 68 | void LinuxSerialInterface::end() { 69 | ::close(_fd); 70 | } 71 | 72 | std::size_t LinuxSerialInterface::write(const uint8_t* data, uint8_t length) { 73 | ssize_t retVal = ::write(_fd, data, length); 74 | if (retVal < 0) { 75 | vw_log_w("Error writing serial port"); 76 | return 0; 77 | } 78 | std::cout << "tx (" << unsigned(length) << "): 0x"; 79 | for (uint8_t i = 0; i < retVal; ++i) { 80 | std::cout << std::setfill('0') << std::setw(2) << std::hex << unsigned(data[i]); 81 | } 82 | std::cout << std::endl; 83 | return retVal; 84 | } 85 | 86 | uint8_t LinuxSerialInterface::read() { 87 | uint8_t buf; 88 | ssize_t retVal = ::read(_fd, &buf, 1); 89 | if (retVal < 0) { 90 | vw_log_e("Error reading serial port"); 91 | return 0; 92 | } 93 | return buf; 94 | } 95 | 96 | size_t LinuxSerialInterface::available() { 97 | int bytesAvailable; 98 | if (ioctl(_fd, FIONREAD, &bytesAvailable) < 0) { 99 | vw_log_e("Error reading serial port"); 100 | return 0; 101 | } 102 | return bytesAvailable; 103 | } 104 | 105 | } // end namespace VitoWiFiInternals 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /src/Interface/LinuxSerialInterface.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | 8 | Serial interface code by @gbmhunter: 9 | https://blog.mbedded.ninja/programming/operating-systems/linux/linux-serial-ports-using-c-cpp/ 10 | https://github.com/gbmhunter/CppLinuxSerial 11 | 12 | (consulted 11/2023) 13 | 14 | */ 15 | 16 | #pragma once 17 | 18 | #if defined(__linux__) 19 | 20 | #include 21 | 22 | // C library headers 23 | #include 24 | #include 25 | 26 | // Linux headers 27 | #include // Contains file controls like O_RDWR 28 | #include // Error integer and strerror() function 29 | #include // Contains POSIX terminal control definitions 30 | #include // write(), read(), close() 31 | #include 32 | 33 | #include "SerialInterface.h" 34 | #include "../Logging.h" 35 | 36 | namespace VitoWiFiInternals { 37 | 38 | class LinuxSerialInterface : public SerialInterface { 39 | public: 40 | explicit LinuxSerialInterface(const char* interface); 41 | bool begin() override; 42 | void end() override; 43 | std::size_t write(const uint8_t* data, uint8_t length) override; 44 | uint8_t read() override; 45 | size_t available() override; 46 | 47 | private: 48 | const char* _interfaceName; 49 | int _fd; 50 | struct termios _tty; 51 | }; 52 | 53 | } // end namespace VitoWiFiInternals 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /src/Interface/SerialInterface.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | namespace VitoWiFiInternals { 15 | 16 | class SerialInterface { 17 | public: 18 | virtual ~SerialInterface() {} 19 | virtual bool begin() = 0; 20 | virtual void end() = 0; 21 | virtual std::size_t write(const uint8_t* data, uint8_t length) = 0; 22 | virtual uint8_t read() = 0; 23 | virtual size_t available() = 0; 24 | }; 25 | 26 | } // end namespace VitoWiFiInternals 27 | -------------------------------------------------------------------------------- /src/Interface/SoftwareSerialInterface.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #if defined(ARDUINO_ARCH_ESP8266) 10 | 11 | #include "SoftwareSerialInterface.h" 12 | 13 | namespace VitoWiFiInternals { 14 | 15 | SoftwareSerialInterface::SoftwareSerialInterface(SoftwareSerial* interface) 16 | : _interface(interface) { 17 | assert(interface); 18 | } 19 | 20 | bool SoftwareSerialInterface::begin() { 21 | _interface->begin(4800, EspSoftwareSerial::SWSERIAL_8E2); 22 | return (*_interface); 23 | } 24 | 25 | void SoftwareSerialInterface::end() { 26 | _interface->end(); 27 | } 28 | 29 | std::size_t SoftwareSerialInterface::write(const uint8_t* data, uint8_t length) { 30 | return _interface->write(data, length); 31 | } 32 | 33 | uint8_t SoftwareSerialInterface::read() { 34 | uint8_t retVal = 0; 35 | _interface->read(&retVal, 1); 36 | return retVal; 37 | } 38 | 39 | size_t SoftwareSerialInterface::available() { 40 | return _interface->available(); 41 | } 42 | 43 | } // end namespace VitoWiFiInternals 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /src/Interface/SoftwareSerialInterface.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #if defined(ARDUINO_ARCH_ESP8266) 12 | 13 | #include 14 | 15 | #include 16 | #include "SerialInterface.h" 17 | 18 | namespace VitoWiFiInternals { 19 | 20 | class SoftwareSerialInterface : public SerialInterface { 21 | public: 22 | explicit SoftwareSerialInterface(SoftwareSerial* interface); 23 | bool begin(); 24 | void end(); 25 | std::size_t write(const uint8_t* data, uint8_t length) override; 26 | uint8_t read() override; 27 | size_t available() override; 28 | 29 | private: 30 | SoftwareSerial* _interface; 31 | }; 32 | 33 | } // end namespace VitoWiFiInternals 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /src/Logging.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #if defined(ARDUINO_ARCH_ESP32) 12 | #include 13 | #include "freertos/FreeRTOS.h" 14 | #include "freertos/task.h" 15 | #if defined(DEBUG_VITOWIFI) 16 | // Logging is en/disabled by Arduino framework macros 17 | #define vw_log_i(...) log_i(__VA_ARGS__) 18 | #define vw_log_e(...) log_e(__VA_ARGS__) 19 | #define vw_log_w(...) log_w(__VA_ARGS__) 20 | #else 21 | // Logging is disabled 22 | #define vw_log_i(...) 23 | #define vw_log_e(...) 24 | #define vw_log_w(...) 25 | #endif 26 | #elif defined(ARDUINO_ARCH_ESP8266) 27 | #if defined(DEBUG_ESP_PORT) && defined(DEBUG_VITOWIFI) 28 | #include 29 | #define vw_log_i(...) DEBUG_ESP_PORT.printf(__VA_ARGS__); DEBUG_ESP_PORT.print("\n") 30 | #define vw_log_e(...) DEBUG_ESP_PORT.printf(__VA_ARGS__); DEBUG_ESP_PORT.print("\n") 31 | #define vw_log_w(...) DEBUG_ESP_PORT.printf(__VA_ARGS__); DEBUG_ESP_PORT.print("\n") 32 | #else 33 | #define vw_log_i(...) 34 | #define vw_log_e(...) 35 | #define vw_log_w(...) 36 | #endif 37 | #else 38 | // when building for PC, always show debug statements as part of testing suite 39 | #include 40 | #include 41 | #define vw_log_i(...) std::cout << "[I] " << __FILE__ ":" << __LINE__ << ": "; printf(__VA_ARGS__); std::cout << std::endl 42 | #define vw_log_e(...) std::cout << "[E] " << __FILE__ ":" << __LINE__ << ": "; printf(__VA_ARGS__); std::cout << std::endl 43 | #define vw_log_w(...) std::cout << "[W] " << __FILE__ ":" << __LINE__ << ": "; printf(__VA_ARGS__); std::cout << std::endl 44 | #endif 45 | -------------------------------------------------------------------------------- /src/VS1/PacketVS1.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "PacketVS1.h" 10 | 11 | namespace VitoWiFi { 12 | 13 | PacketVS1::PacketVS1() 14 | : _allocatedLength(START_PAYLOAD_LENGTH + 4) 15 | , _buffer(nullptr) { 16 | _buffer = reinterpret_cast(malloc(_allocatedLength)); 17 | if (!_buffer) { 18 | _allocatedLength = 0; 19 | vw_abort(); 20 | } 21 | reset(); 22 | } 23 | 24 | PacketVS1::~PacketVS1() { 25 | free(_buffer); 26 | } 27 | 28 | PacketVS1::operator bool() const { 29 | if (_buffer && _buffer[3] != 0) return true; 30 | return false; 31 | } 32 | 33 | /* 34 | uint8_t PacketVS1::operator[](std::size_t index) const { 35 | return _buffer[index]; 36 | } 37 | */ 38 | 39 | uint8_t& PacketVS1::operator[](std::size_t index) { 40 | return _buffer[index]; 41 | } 42 | 43 | bool PacketVS1::createPacket(uint8_t packetType, uint16_t addr, uint8_t len, const uint8_t* data) { 44 | reset(); 45 | 46 | // check arguments 47 | if (len == 0) { 48 | return false; 49 | } 50 | if (packetType != PacketVS1Type.READ && packetType != PacketVS1Type.WRITE) { 51 | return false; 52 | } 53 | if (packetType == PacketVS1Type.WRITE && !data) { 54 | return false; 55 | } 56 | 57 | // reserve memory 58 | std::size_t toAllocate = (packetType == PacketVS1Type.WRITE) ? len + 4 : 4; 59 | if (toAllocate > _allocatedLength) { 60 | uint8_t* newBuffer = reinterpret_cast(realloc(_buffer, toAllocate)); 61 | if (!newBuffer) { 62 | return false; 63 | } 64 | _buffer = newBuffer; 65 | _allocatedLength = toAllocate; 66 | } 67 | 68 | // 2. Serialize into buffer 69 | size_t step = 0; 70 | _buffer[step++] = packetType; 71 | _buffer[step++] = (addr >> 8) & 0xFF; 72 | _buffer[step++] = addr & 0xFF; 73 | _buffer[step++] = len; 74 | if (packetType == PacketVS1Type.WRITE) { 75 | for (uint8_t i = 0; i < len; ++i) { 76 | _buffer[step++] = data[i]; 77 | } 78 | } 79 | return true; 80 | } 81 | 82 | uint8_t PacketVS1::length() const { 83 | if (_buffer[3] == 0) return 0; 84 | if (_buffer[0] == PacketVS1Type.READ) return 4; 85 | if (_buffer[0] == PacketVS1Type.WRITE) return _buffer[3] + 4; 86 | return 0; // should not be possible 87 | } 88 | 89 | uint8_t PacketVS1::packetType() const { 90 | return _buffer[0]; 91 | } 92 | 93 | uint16_t PacketVS1::address() const { 94 | uint16_t retVal = _buffer[1] << 8; 95 | retVal |= _buffer[2]; 96 | return retVal; 97 | } 98 | 99 | uint8_t PacketVS1::dataLength() const { 100 | return _buffer[3]; 101 | } 102 | 103 | const uint8_t* PacketVS1::data() const { 104 | return &_buffer[4]; 105 | } 106 | 107 | void PacketVS1::reset() { 108 | _buffer[3] = 0x00; 109 | } 110 | 111 | } // end namespace VitoWiFi 112 | -------------------------------------------------------------------------------- /src/VS1/PacketVS1.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "../Constants.h" 17 | #include "../Helpers.h" 18 | #include "../Logging.h" 19 | 20 | namespace VitoWiFi { 21 | 22 | class PacketVS1 { 23 | public: 24 | PacketVS1(); 25 | ~PacketVS1(); 26 | PacketVS1 (const PacketVS1&) = delete; 27 | PacketVS1& operator =(const PacketVS1&) = delete; 28 | operator bool() const; 29 | /* uint8_t operator[](std::size_t index) const; */ 30 | uint8_t& operator[](std::size_t index); 31 | 32 | public: 33 | bool createPacket(uint8_t packetType, uint16_t addr, uint8_t len, const uint8_t* data = nullptr); 34 | uint8_t length() const; 35 | uint8_t packetType() const; 36 | uint16_t address() const; 37 | uint8_t dataLength() const; 38 | const uint8_t* data() const; 39 | 40 | void reset(); 41 | 42 | protected: 43 | std::size_t _allocatedLength; 44 | uint8_t* _buffer; 45 | }; 46 | 47 | } // end namespace VitoWiFi 48 | -------------------------------------------------------------------------------- /src/VS1/VS1.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "VS1.h" 10 | 11 | namespace VitoWiFi { 12 | 13 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 14 | VS1::VS1(HardwareSerial* interface) 15 | : _state(State::UNDEFINED) 16 | , _currentMillis(vw_millis()) 17 | , _lastMillis(_currentMillis) 18 | , _requestTime(0) 19 | , _bytesTransferred(0) 20 | , _interface(nullptr) 21 | , _currentDatapoint(Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv)) 22 | , _currentRequest() 23 | , _responseBuffer(nullptr) 24 | , _allocatedLength(0) 25 | , _onResponseCallback(nullptr) 26 | , _onErrorCallback(nullptr) { 27 | assert(interface != nullptr); 28 | _interface = new(std::nothrow) VitoWiFiInternals::HardwareSerialInterface(interface); 29 | if (!_interface) { 30 | vw_log_e("Could not create serial interface"); 31 | vw_abort(); 32 | } 33 | _responseBuffer = reinterpret_cast(malloc(START_PAYLOAD_LENGTH)); 34 | if (!_responseBuffer) { 35 | vw_log_e("Could not create response buffer"); 36 | vw_abort(); 37 | } 38 | } 39 | 40 | #if defined(ARDUINO_ARCH_ESP8266) 41 | VS1::VS1(SoftwareSerial* interface) 42 | : _state(State::UNDEFINED) 43 | , _currentMillis(vw_millis()) 44 | , _lastMillis(_currentMillis) 45 | , _requestTime(0) 46 | , _bytesTransferred(0) 47 | , _interface(nullptr) 48 | , _currentDatapoint(Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv)) 49 | , _currentRequest() 50 | , _responseBuffer(nullptr) 51 | , _allocatedLength(0) 52 | , _onResponseCallback(nullptr) 53 | , _onErrorCallback(nullptr) { 54 | assert(interface != nullptr); 55 | _interface = new(std::nothrow) VitoWiFiInternals::SoftwareSerialInterface(interface); 56 | if (!_interface) { 57 | vw_log_e("Could not create serial interface"); 58 | vw_abort(); 59 | } 60 | _responseBuffer = reinterpret_cast(malloc(START_PAYLOAD_LENGTH)); 61 | if (!_responseBuffer) { 62 | vw_log_e("Could not create response buffer"); 63 | vw_abort(); 64 | } 65 | } 66 | #endif 67 | 68 | #else 69 | VS1::VS1(const char* interface) 70 | : _state(State::UNDEFINED) 71 | , _currentMillis(vw_millis()) 72 | , _lastMillis(_currentMillis) 73 | , _requestTime(0) 74 | , _bytesTransferred(0) 75 | , _interface(nullptr) 76 | , _currentDatapoint(Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv)) 77 | , _currentRequest() 78 | , _responseBuffer(nullptr) 79 | , _allocatedLength(0) 80 | , _onResponseCallback(nullptr) 81 | , _onErrorCallback(nullptr) { 82 | assert(interface != nullptr); 83 | _interface = new(std::nothrow) VitoWiFiInternals::LinuxSerialInterface(interface); 84 | if (!_interface) { 85 | vw_log_e("Could not create serial interface"); 86 | vw_abort(); 87 | } 88 | _responseBuffer = reinterpret_cast(malloc(START_PAYLOAD_LENGTH)); 89 | if (!_responseBuffer) { 90 | vw_log_e("Could not create response buffer"); 91 | vw_abort(); 92 | } 93 | } 94 | #endif 95 | 96 | VS1::~VS1() { 97 | delete _interface; 98 | free(_responseBuffer); 99 | } 100 | 101 | void VS1::onResponse(OnResponseCallback callback) { 102 | _onResponseCallback = callback; 103 | } 104 | void VS1::onError(OnErrorCallback callback) { 105 | _onErrorCallback = callback; 106 | } 107 | 108 | bool VS1::read(const Datapoint& datapoint) { 109 | if (_currentDatapoint) { 110 | return false; 111 | } 112 | if (_currentRequest.createPacket(PacketVS1Type.READ, 113 | datapoint.address(), 114 | datapoint.length()) && 115 | _expandResponseBuffer(datapoint.length())) { 116 | _currentDatapoint = datapoint; 117 | _requestTime = _currentMillis; 118 | vw_log_i("reading packet OK"); 119 | return true; 120 | } 121 | vw_log_i("reading not possible, packet creation error"); 122 | return false; 123 | } 124 | 125 | bool VS1::write(const Datapoint& datapoint, const VariantValue& value) { 126 | if (_currentDatapoint) { 127 | return false; 128 | } 129 | uint8_t* payload = reinterpret_cast(malloc(datapoint.length())); 130 | if (!payload) return false; 131 | datapoint.encode(payload, datapoint.length(), value); 132 | bool result = write(datapoint, payload, datapoint.length()); 133 | free(payload); 134 | return result; 135 | } 136 | 137 | bool VS1::write(const Datapoint& datapoint, const uint8_t* data, uint8_t length) { 138 | if (_currentDatapoint) { 139 | return false; 140 | } 141 | if (length != datapoint.length()) { 142 | vw_log_i("writing not possible, length mismatch"); 143 | return false; 144 | } 145 | if (_currentRequest.createPacket(PacketVS1Type.WRITE, 146 | datapoint.address(), 147 | datapoint.length(), 148 | data) && 149 | _expandResponseBuffer(datapoint.length())) { 150 | _currentDatapoint = datapoint; 151 | _requestTime = _currentMillis; 152 | vw_log_i("writing packet OK"); 153 | return true; 154 | } 155 | vw_log_i("writing not possible, packet creation error"); 156 | return false; 157 | } 158 | 159 | bool VS1::begin() { 160 | if (_interface->begin()) { 161 | while (_interface->available()) { 162 | _interface->read(); // clear rx buffer 163 | } 164 | _setState(State::INIT); 165 | return true; 166 | } 167 | return false; 168 | } 169 | 170 | void VS1::loop() { 171 | _currentMillis = vw_millis(); 172 | switch (_state) { 173 | case State::INIT: 174 | _init(); 175 | break; 176 | case State::SYNC_ENQ: 177 | _syncEnq(); 178 | break; 179 | case State::SYNC_RECV: 180 | _syncRecv(); 181 | break; 182 | case State::SEND: 183 | _send(); 184 | break; 185 | case State::RECEIVE: 186 | _receive(); 187 | break; 188 | case State::UNDEFINED: 189 | // begin() not yet called 190 | break; 191 | } 192 | // double timeout to accomodate for connection initialization 193 | if (_currentDatapoint && _currentMillis - _requestTime > 4000UL) { 194 | _bytesTransferred = 0; 195 | _setState(State::INIT); 196 | _tryOnError(OptolinkResult::TIMEOUT); 197 | } 198 | } 199 | 200 | void VS1::end() { 201 | _interface->end(); 202 | _setState(State::UNDEFINED); 203 | _currentDatapoint = Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv); 204 | } 205 | 206 | int VS1::getState() const { 207 | return static_cast::type>(_state); 208 | } 209 | 210 | bool VS1::isBusy() const { 211 | if (_currentDatapoint) { 212 | return true; 213 | } 214 | return false; 215 | } 216 | 217 | void VS1::_setState(State state) { 218 | vw_log_i("state %i --> %i", static_cast::type>(_state), static_cast::type>(state)); 219 | _state = state; 220 | } 221 | 222 | // wait for ENQ or reset connection if ENQ is not coming 223 | void VS1::_init() { 224 | if (_interface->available()) { 225 | if (_interface->read() == VitoWiFiInternals::ProtocolBytes.ENQ) { 226 | _lastMillis = _currentMillis; 227 | _setState(State::SYNC_ENQ); 228 | } 229 | } else { 230 | if (_currentMillis - _lastMillis > 3000UL) { // reset should Vitotronic be connected with VS2 231 | _lastMillis = _currentMillis; 232 | _interface->write(&VitoWiFiInternals::ProtocolBytes.EOT, 1); 233 | } 234 | } 235 | } 236 | 237 | // if we want to send something within 50msec of receiving the ENQ, send ENQ_ACK and move to SEND 238 | // if > 50msec, return to INIT 239 | void VS1::_syncEnq() { 240 | if (_currentMillis - _lastMillis < 50) { 241 | if (_currentDatapoint && _interface->write(&VitoWiFiInternals::ProtocolBytes.ENQ_ACK, 1) == 1) { 242 | _setState(State::SEND); 243 | _send(); // speed up things 244 | } 245 | } else { 246 | _setState(State::INIT); 247 | } 248 | } 249 | 250 | // if we want to send something within 50msec of previous SEND, send again 251 | // if > 50msec, return to INIT 252 | void VS1::_syncRecv() { 253 | if (_currentMillis - _lastMillis < 50) { 254 | if (_currentDatapoint) { 255 | _setState(State::SEND); 256 | } 257 | } else { 258 | _setState(State::INIT); 259 | } 260 | } 261 | 262 | // send request and move to RECEIVE 263 | void VS1::_send() { 264 | _bytesTransferred += _interface->write(&_currentRequest[_bytesTransferred], _currentRequest.length() - _bytesTransferred); 265 | if (_bytesTransferred == _currentRequest.length()) { 266 | _bytesTransferred = 0; 267 | _lastMillis = _currentMillis; 268 | _setState(State::RECEIVE); 269 | } 270 | } 271 | 272 | // wait for data to receive 273 | // when done, move to SYN_RECV 274 | void VS1::_receive() { 275 | while (_interface->available()) { 276 | _responseBuffer[_bytesTransferred] = _interface->read(); 277 | ++_bytesTransferred; 278 | _lastMillis = _currentMillis; 279 | } 280 | if (_bytesTransferred == _currentDatapoint.length()) { 281 | _bytesTransferred = 0; 282 | _setState(State::SYNC_RECV); 283 | _tryOnResponse(); 284 | } 285 | } 286 | 287 | void VS1::_tryOnResponse() { 288 | if (_onResponseCallback) { 289 | _onResponseCallback(_responseBuffer, _currentDatapoint.length(), _currentDatapoint); 290 | } 291 | _currentDatapoint = Datapoint(nullptr, 0, 0, noconv); 292 | } 293 | 294 | void VS1::_tryOnError(OptolinkResult result) { 295 | if (_onErrorCallback) { 296 | _onErrorCallback(result, _currentDatapoint); 297 | } 298 | _currentDatapoint = Datapoint(nullptr, 0, 0, noconv); 299 | } 300 | 301 | bool VS1::_expandResponseBuffer(uint8_t newSize) { 302 | if (newSize > _allocatedLength) { 303 | uint8_t* newBuffer = reinterpret_cast(realloc(_responseBuffer, newSize)); 304 | if (!newBuffer) { 305 | return false; 306 | } 307 | _responseBuffer = newBuffer; 308 | _allocatedLength = newSize; 309 | } 310 | return true; 311 | } 312 | 313 | } // end namespace VitoWiFi 314 | -------------------------------------------------------------------------------- /src/VS1/VS1.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | 13 | #include "Logging.h" 14 | #include "../Constants.h" 15 | #include "../Helpers.h" 16 | #include "PacketVS1.h" 17 | #include "../Datapoint/Datapoint.h" 18 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 19 | #include "../Interface/HardwareSerialInterface.h" 20 | #if defined(ARDUINO_ARCH_ESP8266) 21 | #include "../Interface/SoftwareSerialInterface.h" 22 | #endif 23 | #elif defined(__linux__) 24 | #include "../Interface/LinuxSerialInterface.h" 25 | #endif 26 | #include "../Interface/GenericInterface.h" 27 | 28 | namespace VitoWiFi { 29 | 30 | class VS1 { 31 | public: 32 | typedef std::function OnResponseCallback; 33 | typedef std::function OnErrorCallback; 34 | 35 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 36 | explicit VS1(HardwareSerial* interface); 37 | #if defined(ARDUINO_ARCH_ESP8266) 38 | explicit VS1(SoftwareSerial* interface); 39 | #endif 40 | #else 41 | explicit VS1(const char* interface); 42 | #endif 43 | template 44 | explicit VS1(C* interface) 45 | : _state(State::UNDEFINED) 46 | , _currentMillis(vw_millis()) 47 | , _lastMillis(_currentMillis) 48 | , _requestTime(0) 49 | , _bytesTransferred(0) 50 | , _interface(nullptr) 51 | , _currentDatapoint(Datapoint(nullptr, 0x0000, 0, VitoWiFi::noconv)) 52 | , _currentRequest() 53 | , _responseBuffer(nullptr) 54 | , _allocatedLength(0) 55 | , _onResponseCallback(nullptr) 56 | , _onErrorCallback(nullptr) { 57 | assert(interface != nullptr); 58 | _interface = new(std::nothrow) VitoWiFiInternals::GenericInterface(interface); 59 | if (!_interface) { 60 | vw_log_e("Could not create serial interface"); 61 | vw_abort(); 62 | } 63 | _responseBuffer = reinterpret_cast(malloc(START_PAYLOAD_LENGTH)); 64 | if (!_responseBuffer) { 65 | vw_log_e("Could not create response buffer"); 66 | vw_abort(); 67 | } 68 | } 69 | ~VS1(); 70 | VS1(const VS1&) = delete; 71 | VS1 & operator=(const VS1&) = delete; 72 | 73 | void onResponse(OnResponseCallback callback); 74 | void onError(OnErrorCallback callback); 75 | 76 | bool read(const Datapoint& datapoint); 77 | bool write(const Datapoint& datapoint, const VariantValue& value); 78 | bool write(const Datapoint& datapoint, const uint8_t* data, uint8_t length); 79 | 80 | bool begin(); 81 | void loop(); 82 | void end(); 83 | 84 | int getState() const; 85 | bool isBusy() const; 86 | 87 | private: 88 | enum class State { 89 | INIT, 90 | SYNC_ENQ, 91 | SYNC_RECV, 92 | SEND, 93 | RECEIVE, 94 | UNDEFINED 95 | } _state; 96 | uint32_t _currentMillis; 97 | uint32_t _lastMillis; 98 | uint32_t _requestTime; 99 | uint8_t _bytesTransferred; 100 | VitoWiFiInternals::SerialInterface* _interface; 101 | Datapoint _currentDatapoint; 102 | PacketVS1 _currentRequest; 103 | uint8_t* _responseBuffer; 104 | uint8_t _allocatedLength; 105 | OnResponseCallback _onResponseCallback; 106 | OnErrorCallback _onErrorCallback; 107 | 108 | inline void _setState(State state); 109 | 110 | void _init(); 111 | void _syncEnq(); 112 | void _syncRecv(); 113 | void _send(); 114 | void _receive(); 115 | 116 | void _tryOnResponse(); 117 | void _tryOnError(OptolinkResult result); 118 | 119 | bool _expandResponseBuffer(uint8_t newSize); 120 | }; 121 | 122 | } // end namespace VitoWiFi 123 | -------------------------------------------------------------------------------- /src/VS2/PacketVS2.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "PacketVS2.h" 10 | 11 | namespace VitoWiFi { 12 | 13 | PacketVS2::PacketVS2() 14 | : _allocatedLength(START_PAYLOAD_LENGTH + 6) 15 | , _buffer(nullptr) { 16 | _buffer = reinterpret_cast(malloc(_allocatedLength)); 17 | if (!_buffer) { 18 | _allocatedLength = 0; 19 | vw_abort(); 20 | } 21 | reset(); 22 | } 23 | 24 | PacketVS2::~PacketVS2() { 25 | free(_buffer); 26 | } 27 | 28 | PacketVS2::operator bool() const { 29 | if (_buffer && _buffer[0] != 0) return true; 30 | return false; 31 | } 32 | 33 | /* 34 | uint8_t PacketVS2::operator[](std::size_t index) const { 35 | return _buffer[index]; 36 | } 37 | */ 38 | 39 | uint8_t& PacketVS2::operator[](std::size_t index) { 40 | return _buffer[index]; 41 | } 42 | 43 | bool PacketVS2::createPacket(PacketType pt, FunctionCode fc, uint8_t id, uint16_t addr, uint8_t len, const uint8_t* data) { 44 | reset(); 45 | 46 | // check arguments 47 | if (len == 0) { 48 | vw_log_w("Length error: %u", len); 49 | return false; 50 | } 51 | if (fc == FunctionCode::WRITE && !data) { 52 | vw_log_w("Function code - data mismatch"); 53 | return false; 54 | } 55 | if (id > 7) { 56 | vw_log_w("Message id overflow: %u > 7", id); 57 | } 58 | 59 | // reserve memory 60 | std::size_t toAllocate = (fc == FunctionCode::WRITE) ? len + 6 : 6; 61 | if (toAllocate > _allocatedLength) { 62 | uint8_t* newBuffer = reinterpret_cast(realloc(_buffer, toAllocate)); 63 | if (!newBuffer) { 64 | vw_log_e("buffer not available"); 65 | return false; 66 | } 67 | _buffer = newBuffer; 68 | _allocatedLength = toAllocate; 69 | } 70 | 71 | // 2. Serialize into buffer 72 | size_t step = 0; 73 | if (fc == FunctionCode::WRITE) { 74 | _buffer[step++] = 0x05 + len; // 0x05 = standard length: mt, fc, addr(2), len + data 75 | } else { 76 | _buffer[step++] = 0x05; 77 | } 78 | _buffer[step++] = static_cast(pt); 79 | _buffer[step++] = static_cast(fc) | id << 5; 80 | _buffer[step++] = (addr >> 8) & 0xFF; 81 | _buffer[step++] = addr & 0xFF; 82 | _buffer[step++] = len; 83 | if (fc == FunctionCode::WRITE || pt == PacketType::RESPONSE) { 84 | for (uint8_t i = 0; i < len; ++i) { 85 | _buffer[step++] = data[i]; 86 | } 87 | } 88 | return true; 89 | } 90 | 91 | bool PacketVS2::setLength(uint8_t length) { 92 | std::size_t toAllocate = length + 1; 93 | if (toAllocate > _allocatedLength) { 94 | uint8_t* newBuffer = reinterpret_cast(realloc(_buffer, toAllocate)); 95 | if (!newBuffer) { 96 | return false; 97 | } 98 | _allocatedLength = toAllocate; 99 | _buffer = newBuffer; 100 | } 101 | _buffer[0] = length; 102 | return true; 103 | } 104 | 105 | uint8_t PacketVS2::length() const { 106 | return _buffer[0] + 1; 107 | } 108 | 109 | PacketType PacketVS2::packetType() const { 110 | return static_cast(_buffer[1]); 111 | } 112 | 113 | FunctionCode PacketVS2::functionCode() const { 114 | return static_cast(_buffer[2] & 0x1F); 115 | } 116 | 117 | uint8_t PacketVS2::id() const { 118 | return _buffer[2] >> 5 & 0x07; 119 | } 120 | 121 | uint16_t PacketVS2::address() const { 122 | uint16_t retVal = _buffer[3] << 8; 123 | retVal |= _buffer[4]; 124 | return retVal; 125 | } 126 | 127 | uint8_t PacketVS2::dataLength() const { 128 | return _buffer[5]; 129 | } 130 | 131 | const uint8_t* PacketVS2::data() const { 132 | if (functionCode() == FunctionCode::WRITE) return nullptr; 133 | return &_buffer[6]; 134 | } 135 | 136 | uint8_t PacketVS2::checksum() const { 137 | uint8_t retVal = 0; 138 | for (std::size_t i = 0; i <= _buffer[0]; ++i) { 139 | retVal += _buffer[i]; 140 | } 141 | return retVal; 142 | } 143 | 144 | void PacketVS2::reset() { 145 | _buffer[0] = 0x00; 146 | } 147 | 148 | } // end namespace VitoWiFi 149 | -------------------------------------------------------------------------------- /src/VS2/PacketVS2.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include // assert 15 | 16 | #include "../Constants.h" 17 | #include "../Helpers.h" 18 | #include "../Logging.h" 19 | 20 | namespace VitoWiFiInternals { 21 | 22 | class ParserVS2; 23 | 24 | } // end namespace VitoWiFiInternals 25 | 26 | namespace VitoWiFi { 27 | 28 | class PacketVS2 { 29 | friend class VitoWiFiInternals::ParserVS2; 30 | 31 | public: 32 | PacketVS2(); 33 | ~PacketVS2(); 34 | PacketVS2 (const PacketVS2&) = delete; 35 | PacketVS2& operator =(const PacketVS2&) = delete; 36 | operator bool() const; 37 | /* uint8_t operator[](std::size_t index) const; */ 38 | uint8_t& operator[](std::size_t index); 39 | 40 | public: 41 | bool createPacket(PacketType pt, FunctionCode fc, uint8_t id, uint16_t addr, uint8_t len, const uint8_t* data = nullptr); 42 | bool setLength(uint8_t length); 43 | uint8_t length() const; 44 | PacketType packetType() const; 45 | FunctionCode functionCode() const; 46 | uint8_t id() const; 47 | uint16_t address() const; 48 | uint8_t dataLength() const; 49 | const uint8_t* data() const; 50 | 51 | uint8_t checksum() const; 52 | 53 | void reset(); 54 | 55 | protected: 56 | std::size_t _allocatedLength; 57 | uint8_t* _buffer; 58 | }; 59 | 60 | } // end namespace VitoWiFi 61 | -------------------------------------------------------------------------------- /src/VS2/ParserVS2.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "ParserVS2.h" 10 | 11 | namespace VitoWiFiInternals { 12 | 13 | ParserVS2::ParserVS2() 14 | : _packet() 15 | , _step(ParserStep::STARTBYTE) 16 | , _payloadLength(0) { 17 | // empty 18 | } 19 | 20 | ParserResult ParserVS2::parse(const uint8_t b) { 21 | switch (_step) { 22 | case ParserStep::STARTBYTE: 23 | if (b != ProtocolBytes.PACKETSTART) { 24 | vw_log_w("Invalid packet start: 0x%02x", b); 25 | break; 26 | } 27 | _packet.reset(); 28 | _step = ParserStep::PACKETLENGTH; 29 | break; 30 | 31 | case ParserStep::PACKETLENGTH: 32 | if (b < 5) { 33 | vw_log_w("Invalid packet length: %u", b); 34 | _step = ParserStep::STARTBYTE; 35 | return ParserResult::ERROR; 36 | } 37 | if (!_packet.setLength(b)) { 38 | vw_log_e("Could not parse packet"); 39 | _step = ParserStep::STARTBYTE; 40 | return ParserResult::ERROR; 41 | } 42 | _step = ParserStep::PACKETTYPE; 43 | break; 44 | 45 | case ParserStep::PACKETTYPE: 46 | if (b > 0x03) { 47 | vw_log_w("Invalid packet type: 0x%02x", b); 48 | _step = ParserStep::STARTBYTE; 49 | return ParserResult::ERROR; 50 | } 51 | _packet[1] = b; 52 | _step = ParserStep::FLAGS; 53 | break; 54 | 55 | case ParserStep::FLAGS: 56 | { 57 | uint8_t fc = b & 0x1F; 58 | if (fc != 0x01 && fc != 0x02 && fc != 0x07) { 59 | vw_log_w("Invalid packet fc: 0x%02x", fc); 60 | _step = ParserStep::STARTBYTE; 61 | return ParserResult::ERROR; 62 | } 63 | } 64 | _packet[2] = b; 65 | _step = ParserStep::ADDRESS1; 66 | break; 67 | 68 | case ParserStep::ADDRESS1: 69 | _packet[3] = b; 70 | _step = ParserStep::ADDRESS2; 71 | break; 72 | 73 | case ParserStep::ADDRESS2: 74 | _packet[4] = b; 75 | _step = ParserStep::PAYLOADLENGTH; 76 | break; 77 | 78 | case ParserStep::PAYLOADLENGTH: 79 | _packet[5] = b; 80 | if ((_packet.functionCode() == VitoWiFi::FunctionCode::READ && 81 | _packet.packetType() == VitoWiFi::PacketType::REQUEST) || 82 | (_packet.functionCode() == VitoWiFi::FunctionCode::WRITE && 83 | _packet.packetType() == VitoWiFi::PacketType::RESPONSE)) { 84 | // read requests and write responses don't have a data payload 85 | _step = ParserStep::CHECKSUM; 86 | } else { 87 | if (b != _packet.length() - 6U) { 88 | vw_log_w("Invalid payload length: %u (expected %u)", b, _packet.length() - 6U); 89 | _step = ParserStep::STARTBYTE; 90 | return ParserResult::ERROR; 91 | } 92 | _payloadLength = b; 93 | _step = ParserStep::PAYLOAD; 94 | } 95 | break; 96 | 97 | case ParserStep::PAYLOAD: 98 | _packet[6 + _packet.dataLength() - _payloadLength--] = b; 99 | if (_payloadLength == 0) { 100 | _step = ParserStep::CHECKSUM; 101 | } 102 | break; 103 | 104 | case ParserStep::CHECKSUM: 105 | if (_packet.checksum() != b) { 106 | vw_log_w("Invalid checksum: 0x%02x (calculated 0x%02x)", b, _packet.checksum()); 107 | return ParserResult::CS_ERROR; 108 | } 109 | _step = ParserStep::STARTBYTE; 110 | return ParserResult::COMPLETE; 111 | } 112 | return ParserResult::CONTINUE; 113 | } 114 | 115 | const VitoWiFi::PacketVS2& ParserVS2::packet() const { 116 | return _packet; 117 | } 118 | 119 | } // end namespace VitoWiFiInternals 120 | -------------------------------------------------------------------------------- /src/VS2/ParserVS2.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | #include "../Constants.h" 15 | #include "../Helpers.h" 16 | #include "../Logging.h" 17 | #include "PacketVS2.h" 18 | 19 | namespace VitoWiFiInternals { 20 | 21 | class ParserVS2 { 22 | public: 23 | ParserVS2(); 24 | ParserResult parse(const uint8_t b); 25 | const VitoWiFi::PacketVS2& packet() const; 26 | void reset(); 27 | 28 | private: 29 | VitoWiFi::PacketVS2 _packet; 30 | enum class ParserStep { 31 | STARTBYTE, 32 | PACKETLENGTH, 33 | PACKETTYPE, 34 | FLAGS, 35 | ADDRESS1, 36 | ADDRESS2, 37 | PAYLOADLENGTH, 38 | PAYLOAD, 39 | CHECKSUM 40 | }_step; 41 | uint8_t _payloadLength; 42 | }; 43 | 44 | } // end namespace VitoWiFiInternals 45 | -------------------------------------------------------------------------------- /src/VS2/VS2.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include "VS2.h" 10 | 11 | namespace VitoWiFi { 12 | 13 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 14 | VS2::VS2(HardwareSerial* interface) 15 | : _state(State::UNDEFINED) 16 | , _currentMillis(vw_millis()) 17 | , _lastMillis(_currentMillis) 18 | , _requestTime(0) 19 | , _bytesTransferred(0) 20 | , _interface(nullptr) 21 | , _parser() 22 | , _currentDatapoint(Datapoint(nullptr, 0, 0, noconv)) 23 | , _currentPacket() 24 | , _onResponseCallback(nullptr) 25 | , _onErrorCallback(nullptr) { 26 | assert(interface != nullptr); 27 | _interface = new(std::nothrow) VitoWiFiInternals::HardwareSerialInterface(interface); 28 | if (!_interface) { 29 | vw_log_e("Could not create serial interface"); 30 | vw_abort(); 31 | } 32 | } 33 | 34 | #if defined(ARDUINO_ARCH_ESP8266) 35 | VS2::VS2(SoftwareSerial* interface) 36 | : _state(State::UNDEFINED) 37 | , _currentMillis(vw_millis()) 38 | , _lastMillis(_currentMillis) 39 | , _requestTime(0) 40 | , _bytesTransferred(0) 41 | , _interface(nullptr) 42 | , _parser() 43 | , _currentDatapoint(Datapoint(nullptr, 0, 0, noconv)) 44 | , _currentPacket() 45 | , _onResponseCallback(nullptr) 46 | , _onErrorCallback(nullptr) { 47 | assert(interface != nullptr); 48 | _interface = new(std::nothrow) VitoWiFiInternals::SoftwareSerialInterface(interface); 49 | if (!_interface) { 50 | vw_log_e("Could not create serial interface"); 51 | vw_abort(); 52 | } 53 | } 54 | #endif 55 | 56 | #else 57 | VS2::VS2(const char* interface) 58 | : _state(State::UNDEFINED) 59 | , _currentMillis(vw_millis()) 60 | , _lastMillis(_currentMillis) 61 | , _requestTime(0) 62 | , _bytesTransferred(0) 63 | , _interface(nullptr) 64 | , _parser() 65 | , _currentDatapoint(Datapoint(nullptr, 0, 0, noconv)) 66 | , _currentPacket() 67 | , _onResponseCallback(nullptr) 68 | , _onErrorCallback(nullptr) { 69 | assert(interface != nullptr); 70 | _interface = new(std::nothrow) VitoWiFiInternals::LinuxSerialInterface(interface); 71 | if (!_interface) { 72 | vw_log_e("Could not create serial interface"); 73 | vw_abort(); 74 | } 75 | } 76 | #endif 77 | 78 | VS2::~VS2() { 79 | delete _interface; 80 | } 81 | 82 | void VS2::onResponse(OnResponseCallback callback) { 83 | _onResponseCallback = callback; 84 | } 85 | void VS2::onError(OnErrorCallback callback) { 86 | _onErrorCallback = callback; 87 | } 88 | 89 | bool VS2::read(const Datapoint& datapoint) { 90 | if (_currentDatapoint) { 91 | return false; 92 | } 93 | if (_currentPacket.createPacket(PacketType::REQUEST, 94 | FunctionCode::READ, 95 | 0, 96 | datapoint.address(), 97 | datapoint.length())) { 98 | _currentDatapoint = datapoint; 99 | _requestTime = _currentMillis; 100 | vw_log_i("reading packet OK"); 101 | return true; 102 | } 103 | vw_log_i("reading not possible, packet creation error"); 104 | return false; 105 | } 106 | 107 | bool VS2::write(const Datapoint& datapoint, const VariantValue& value) { 108 | if (_currentDatapoint) { 109 | return false; 110 | } 111 | uint8_t* payload = reinterpret_cast(malloc(datapoint.length())); 112 | if (!payload) return false; 113 | datapoint.encode(payload, datapoint.length(), value); 114 | bool result = write(datapoint, payload, datapoint.length()); 115 | free(payload); 116 | return result; 117 | } 118 | 119 | bool VS2::write(const Datapoint& datapoint, const uint8_t* data, uint8_t length) { 120 | if (_currentDatapoint) { 121 | return false; 122 | } 123 | if (length != datapoint.length()) { 124 | vw_log_i("writing not possible, length error"); 125 | return false; 126 | } 127 | if (_currentPacket.createPacket(PacketType::REQUEST, 128 | FunctionCode::WRITE, 129 | 0, 130 | datapoint.address(), 131 | datapoint.length(), 132 | data)) { 133 | _currentDatapoint = datapoint; 134 | _requestTime = _currentMillis; 135 | vw_log_i("writing packet OK"); 136 | return true; 137 | } 138 | vw_log_i("writing not possible, packet creation error"); 139 | return false; 140 | } 141 | 142 | bool VS2::begin() { 143 | _setState(State::RESET); 144 | return _interface->begin(); 145 | } 146 | 147 | void VS2::loop() { 148 | _currentMillis = vw_millis(); 149 | switch (_state) { 150 | case State::RESET: 151 | _reset(); 152 | break; 153 | case State::RESET_ACK: 154 | _resetAck(); 155 | break; 156 | case State::INIT: 157 | _init(); 158 | break; 159 | case State::INIT_ACK: 160 | _initAck(); 161 | break; 162 | case State::IDLE: 163 | _idle(); 164 | break; 165 | case State::SENDSTART: 166 | _sendStart(); 167 | break; 168 | case State::SENDPACKET: 169 | _sendPacket(); 170 | break; 171 | case State::SEND_CRC: 172 | _sendCRC(); 173 | break; 174 | case State::SEND_ACK: 175 | _sendAck(); 176 | break; 177 | case State::RECEIVE: 178 | _receive(); 179 | break; 180 | case State::RECEIVE_ACK: 181 | _receiveAck(); 182 | break; 183 | case State::UNDEFINED: 184 | // begin() not yet called 185 | break; 186 | } 187 | if (_currentDatapoint && _currentMillis - _requestTime > 4000UL) { 188 | _setState(State::RESET); 189 | _tryOnError(OptolinkResult::TIMEOUT); 190 | } 191 | } 192 | 193 | void VS2::end() { 194 | _interface->end(); 195 | _setState(State::UNDEFINED); 196 | _currentDatapoint = Datapoint(nullptr, 0, 0, noconv); 197 | } 198 | 199 | int VS2::getState() const { 200 | return static_cast::type>(_state); 201 | } 202 | 203 | bool VS2::isBusy() const { 204 | if (_currentDatapoint) { 205 | return true; 206 | } 207 | return false; 208 | } 209 | 210 | void VS2::_setState(State state) { 211 | vw_log_i("state %i --> %i", static_cast::type>(_state), static_cast::type>(state)); 212 | _state = state; 213 | } 214 | 215 | void VS2::_reset() { 216 | while (_interface->available()) _interface->read(); 217 | if (_interface->write(&VitoWiFiInternals::ProtocolBytes.EOT, 1) == 1) { 218 | _lastMillis = _currentMillis; 219 | _setState(State::RESET_ACK); 220 | } 221 | } 222 | 223 | void VS2::_resetAck() { 224 | if (_interface->available()) { 225 | uint8_t buff = _interface->read(); 226 | if (buff == VitoWiFiInternals::ProtocolBytes.ENQ) { 227 | _lastMillis = _currentMillis; 228 | _setState(State::INIT); 229 | } 230 | } else { 231 | if (_currentMillis - _lastMillis > 3000) { 232 | _setState(State::RESET); 233 | } 234 | } 235 | } 236 | 237 | void VS2::_init() { 238 | _bytesTransferred += _interface->write(&VitoWiFiInternals::ProtocolBytes.SYNC[_bytesTransferred], 239 | sizeof(VitoWiFiInternals::ProtocolBytes.SYNC) - _bytesTransferred); 240 | if (_bytesTransferred == sizeof(VitoWiFiInternals::ProtocolBytes.SYNC)) { 241 | _bytesTransferred = 0; 242 | _lastMillis = _currentMillis; 243 | _setState(State::INIT_ACK); 244 | } 245 | } 246 | 247 | void VS2::_initAck() { 248 | if (_interface->available()) { 249 | uint8_t buff = _interface->read(); 250 | vw_log_i("rcv: 0x%02x", buff); 251 | if (buff == VitoWiFiInternals::ProtocolBytes.ACK) { 252 | _setState(State::IDLE); 253 | } else { 254 | _setState(State::RESET); 255 | } 256 | } else if (_currentMillis - _lastMillis > 3000) { 257 | _setState(State::RESET); 258 | } 259 | } 260 | 261 | void VS2::_idle() { 262 | if (_currentDatapoint) { 263 | _setState(State::SENDSTART); 264 | } 265 | // send INIT every 3 seconds to keep communication alive 266 | if (_currentMillis - _lastMillis > 3000UL) { 267 | _setState(State::INIT); 268 | } 269 | } 270 | 271 | void VS2::_sendStart() { 272 | if (_interface->write(&VitoWiFiInternals::ProtocolBytes.PACKETSTART, 1) == 1) { 273 | _lastMillis = _currentMillis; 274 | _setState(State::SENDPACKET); 275 | } 276 | } 277 | 278 | void VS2::_sendPacket() { 279 | _bytesTransferred += _interface->write(&_currentPacket[_bytesTransferred], _currentPacket.length() - _bytesTransferred); 280 | if (_bytesTransferred == _currentPacket.length()) { 281 | _bytesTransferred = 0; 282 | _lastMillis = _currentMillis; 283 | _setState(State::SEND_CRC); 284 | } 285 | } 286 | 287 | void VS2::_sendCRC() { 288 | uint8_t crc = _currentPacket.checksum(); 289 | if (_interface->write(&crc, 1) == 1) { 290 | _lastMillis = _currentMillis; 291 | _setState(State::SEND_ACK); 292 | } 293 | } 294 | 295 | void VS2::_sendAck() { 296 | if (_interface->available()) { 297 | uint8_t buff = _interface->read(); 298 | vw_log_i("rcv: 0x%02x", buff); 299 | if (buff == VitoWiFiInternals::ProtocolBytes.ACK) { // transmit succesful, moving to next state 300 | _setState(State::RECEIVE); 301 | } else if (buff == VitoWiFiInternals::ProtocolBytes.NACK) { // transmit negatively acknowledged, return to IDLE 302 | _setState(State::IDLE); 303 | _tryOnError(OptolinkResult::NACK); 304 | return; 305 | } 306 | } 307 | } 308 | 309 | void VS2::_receive() { 310 | while (_interface->available()) { 311 | _lastMillis = _currentMillis; 312 | VitoWiFiInternals::ParserResult result = _parser.parse(_interface->read()); 313 | if (result == VitoWiFiInternals::ParserResult::COMPLETE) { 314 | _setState(State::RECEIVE_ACK); 315 | _tryOnResponse(); 316 | return; 317 | } else if (result == VitoWiFiInternals::ParserResult::CS_ERROR) { 318 | _setState(State::RESET); 319 | _tryOnError(OptolinkResult::CRC); 320 | return; 321 | } else if (result == VitoWiFiInternals::ParserResult::ERROR) { 322 | _setState(State::RESET); 323 | _tryOnError(OptolinkResult::ERROR); 324 | return; 325 | } 326 | // else: continue 327 | } 328 | } 329 | 330 | void VS2::_receiveAck() { 331 | if (_interface->write(&VitoWiFiInternals::ProtocolBytes.ACK, 1) == 1) { 332 | _lastMillis = _currentMillis; 333 | _setState(State::IDLE); 334 | } 335 | } 336 | 337 | void VS2::_tryOnResponse() { 338 | if (_onResponseCallback) { 339 | _onResponseCallback(_parser.packet(), _currentDatapoint); 340 | } 341 | _currentDatapoint = Datapoint(nullptr, 0, 0, noconv); 342 | } 343 | 344 | void VS2::_tryOnError(OptolinkResult result) { 345 | if (_onErrorCallback) { 346 | _onErrorCallback(result, _currentDatapoint); 347 | } 348 | _currentDatapoint = Datapoint(nullptr, 0, 0, noconv); 349 | } 350 | 351 | } // end namespace VitoWiFi 352 | -------------------------------------------------------------------------------- /src/VS2/VS2.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | 13 | #include "Logging.h" 14 | #include "../Constants.h" 15 | #include "../Helpers.h" 16 | #include "ParserVS2.h" 17 | #include "../Datapoint/Datapoint.h" 18 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 19 | #include "../Interface/HardwareSerialInterface.h" 20 | #if defined(ARDUINO_ARCH_ESP8266) 21 | #include "../Interface/SoftwareSerialInterface.h" 22 | #endif 23 | #elif defined(__linux__) 24 | #include "../Interface/LinuxSerialInterface.h" 25 | #endif 26 | #include "../Interface/GenericInterface.h" 27 | 28 | namespace VitoWiFi { 29 | 30 | class VS2 { 31 | public: 32 | typedef std::function OnResponseCallback; 33 | typedef std::function OnErrorCallback; 34 | 35 | #if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) 36 | explicit VS2(HardwareSerial* interface); 37 | #if defined(ARDUINO_ARCH_ESP8266) 38 | explicit VS2(SoftwareSerial* interface); 39 | #endif 40 | #else 41 | explicit VS2(const char* interface); 42 | #endif 43 | template 44 | explicit VS2(C* interface) 45 | : _state(State::UNDEFINED) 46 | , _currentMillis(vw_millis()) 47 | , _lastMillis(_currentMillis) 48 | , _requestTime(0) 49 | , _bytesTransferred(0) 50 | , _interface(nullptr) 51 | , _parser() 52 | , _currentDatapoint(Datapoint(nullptr, 0, 0, noconv)) 53 | , _currentPacket() 54 | , _onResponseCallback(nullptr) 55 | , _onErrorCallback(nullptr) { 56 | assert(interface != nullptr); 57 | _interface = new(std::nothrow) VitoWiFiInternals::GenericInterface(interface); 58 | if (!_interface) { 59 | vw_log_e("Could not create serial interface"); 60 | vw_abort(); 61 | } 62 | } 63 | ~VS2(); 64 | VS2(const VS2&) = delete; 65 | VS2 & operator=(const VS2&) = delete; 66 | 67 | void onResponse(OnResponseCallback callback); 68 | void onError(OnErrorCallback callback); 69 | 70 | bool read(const Datapoint& datapoint); 71 | bool write(const Datapoint& datapoint, const VariantValue& value); 72 | bool write(const Datapoint& datapoint, const uint8_t* data, uint8_t length); 73 | 74 | bool begin(); 75 | void loop(); 76 | void end(); 77 | 78 | int getState() const; 79 | bool isBusy() const; 80 | 81 | private: 82 | enum class State { 83 | RESET, 84 | RESET_ACK, 85 | INIT, 86 | INIT_ACK, 87 | IDLE, 88 | SENDSTART, 89 | SENDPACKET, 90 | SEND_CRC, 91 | SEND_ACK, 92 | RECEIVE, 93 | RECEIVE_ACK, 94 | UNDEFINED 95 | } _state; 96 | uint32_t _currentMillis; 97 | uint32_t _lastMillis; 98 | uint32_t _requestTime; 99 | uint8_t _bytesTransferred; 100 | VitoWiFiInternals::SerialInterface* _interface; 101 | VitoWiFiInternals::ParserVS2 _parser; 102 | Datapoint _currentDatapoint; 103 | PacketVS2 _currentPacket; 104 | OnResponseCallback _onResponseCallback; 105 | OnErrorCallback _onErrorCallback; 106 | 107 | inline void _setState(State state); 108 | 109 | void _reset(); 110 | void _resetAck(); 111 | void _init(); 112 | void _initAck(); 113 | void _idle(); 114 | void _sendStart(); 115 | void _sendPacket(); 116 | void _sendCRC(); 117 | void _sendAck(); 118 | void _receive(); 119 | void _receiveAck(); 120 | 121 | void _tryOnResponse(); 122 | void _tryOnError(OptolinkResult result); 123 | }; 124 | 125 | } // end namespace VitoWiFi 126 | -------------------------------------------------------------------------------- /src/VitoWiFi.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #pragma once 10 | 11 | #include "VS2/VS2.h" 12 | #include "VS1/VS1.h" 13 | #include "GWG/GWG.h" 14 | 15 | namespace VitoWiFi { 16 | 17 | template 18 | class VitoWiFi { 19 | public: 20 | template 21 | explicit VitoWiFi(IFACE* interface) 22 | : _optolink(interface) { 23 | // empty 24 | } 25 | 26 | void onResponse(typename PROTOCOLVERSION::OnResponseCallback callback) { 27 | _optolink.onResponse(callback); 28 | } 29 | 30 | void onError(typename PROTOCOLVERSION::OnErrorCallback callback) { 31 | _optolink.onError(callback); 32 | } 33 | 34 | bool begin() { 35 | return _optolink.begin(); 36 | } 37 | 38 | void end() { 39 | return _optolink.end(); 40 | } 41 | 42 | void loop() { 43 | _optolink.loop(); 44 | } 45 | 46 | bool read(Datapoint datapoint) { 47 | return _optolink.read(datapoint); 48 | } 49 | 50 | template 51 | bool write(Datapoint datapoint, T value) { 52 | VariantValue v(value); 53 | return _optolink.write(datapoint, v); 54 | } 55 | 56 | bool write(Datapoint datapoint, const uint8_t* data, uint8_t length) { 57 | return _optolink.write(datapoint, data, length); 58 | } 59 | 60 | bool write(Datapoint datapoint, const uint8_t* data) { 61 | return _optolink.write(datapoint, data, datapoint.length()); 62 | } 63 | 64 | int getState() { 65 | return static_cast(_optolink.getState()); 66 | } 67 | 68 | bool isBusy() { 69 | return _optolink.isBusy(); 70 | } 71 | 72 | private: 73 | PROTOCOLVERSION _optolink; 74 | }; 75 | 76 | } // end namespace VitoWiFi 77 | -------------------------------------------------------------------------------- /test/test_Datapoint/test_Datapoint.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | using VitoWiFi::Datapoint; 15 | using VitoWiFi::PacketVS2; 16 | using VitoWiFi::PacketType; 17 | using VitoWiFi::FunctionCode; 18 | using VitoWiFi::VariantValue; 19 | 20 | void setUp() {} 21 | void tearDown() {} 22 | 23 | void test_Converter() { 24 | VitoWiFi::Converter* myConv = &VitoWiFi::div10; 25 | VitoWiFi::Converter* myOtherConv = &VitoWiFi::div3600; 26 | 27 | TEST_ASSERT_TRUE(*myConv == VitoWiFi::div10); 28 | TEST_ASSERT_FALSE(*myOtherConv == VitoWiFi::div10); 29 | } 30 | 31 | void test_Bool() { 32 | Datapoint dp("temp", 0x0000, 2, VitoWiFi::div10); 33 | Datapoint empty(nullptr, 0x0000, 0, VitoWiFi::noconv); 34 | 35 | TEST_ASSERT_TRUE(dp); 36 | TEST_ASSERT_FALSE(empty); 37 | } 38 | 39 | void test_TempDecode() { 40 | Datapoint dp("temp", 0x0000, 2, VitoWiFi::div10); 41 | const uint8_t data[] = {0x07, 0x01}; 42 | const float expected = 26.3; 43 | PacketVS2 packet; 44 | packet.createPacket(PacketType::RESPONSE, FunctionCode::READ, 0, 0x5525, 2, data); 45 | 46 | float result = dp.decode(packet); 47 | 48 | TEST_ASSERT_EQUAL_FLOAT(expected, result); 49 | } 50 | 51 | void test_TempEncode() { 52 | Datapoint dp("temp", 0x0000, 2, VitoWiFi::div10); 53 | const uint8_t expected[] = {0x07, 0x01}; 54 | const float value = 26.3; 55 | const uint8_t len = 2; 56 | uint8_t buffer[len] = {0}; 57 | 58 | dp.encode(buffer, len, VariantValue(value)); 59 | 60 | TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, buffer, len); 61 | } 62 | 63 | void test_TempShortDecode() { 64 | Datapoint dp("temp", 0x0000, 1, VitoWiFi::noconv); 65 | const uint8_t data[] = {0x10}; 66 | const uint8_t expected = 16; 67 | PacketVS2 packet; 68 | packet.createPacket(PacketType::RESPONSE, FunctionCode::READ, 0, 0x5525, 1, data); 69 | 70 | uint8_t result = dp.decode(packet); 71 | 72 | TEST_ASSERT_EQUAL_UINT8(expected, result); 73 | } 74 | 75 | void test_TempShortEncode() { 76 | Datapoint dp("temp", 0x0000, 1, VitoWiFi::noconv); 77 | const uint8_t expected[] = {0x10}; 78 | const uint8_t value = 16; 79 | const uint8_t len = 1; 80 | uint8_t buffer[len] = {0}; 81 | 82 | dp.encode(buffer, len, VariantValue(value)); 83 | 84 | TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, buffer, len); 85 | } 86 | 87 | void test_StatusDecode() { 88 | Datapoint dp("temp", 0x0000, 1, VitoWiFi::noconv); 89 | const uint8_t data[] = {0x01}; 90 | PacketVS2 packet; 91 | packet.createPacket(PacketType::RESPONSE, FunctionCode::READ, 0, 0x5525, 1, data); 92 | 93 | bool result = dp.decode(packet); 94 | 95 | TEST_ASSERT_TRUE(result); 96 | } 97 | 98 | void test_StatusEncode() { 99 | Datapoint dp("temp", 0x0000, 1, VitoWiFi::noconv); 100 | const uint8_t expected[] = {0x01}; 101 | const bool value = true; 102 | const uint8_t len = 1; 103 | uint8_t buffer[len] = {0}; 104 | 105 | dp.encode(buffer, len, VariantValue(value)); 106 | 107 | TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, buffer, len); 108 | } 109 | 110 | void test_HourDecode() { 111 | Datapoint dp("hour", 0x0000, 4, VitoWiFi::div3600); 112 | const uint8_t data[] = {0x80, 0x8F, 0x21, 0x61}; 113 | const float expected = 452663.72; 114 | PacketVS2 packet; 115 | packet.createPacket(PacketType::RESPONSE, FunctionCode::READ, 0, 0x0000, 4, data); 116 | 117 | float result = dp.decode(packet); 118 | 119 | TEST_ASSERT_EQUAL_FLOAT(expected, result); 120 | } 121 | 122 | void test_HourEncode() { 123 | Datapoint dp("hour", 0x0000, 4, VitoWiFi::div3600); 124 | const uint8_t expected[] = {0x80, 0x8F, 0x21, 0x61}; 125 | const float value = 452663.72; 126 | const uint8_t len = 4; 127 | uint8_t buffer[len] = {0}; 128 | 129 | dp.encode(buffer, len, VariantValue(value)); 130 | 131 | TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, buffer, len); 132 | } 133 | 134 | 135 | void test_CountDecode() { 136 | Datapoint dp("count", 0x0000, 4, VitoWiFi::noconv); 137 | const uint8_t data[] = {0xD8, 0xCA, 0x56, 0x1B}; 138 | const uint32_t expected = 458672856; 139 | PacketVS2 packet; 140 | packet.createPacket(PacketType::RESPONSE, FunctionCode::READ, 0, 0x0000, 4, data); 141 | 142 | uint32_t result = dp.decode(packet); 143 | 144 | TEST_ASSERT_EQUAL_UINT32(expected, result); 145 | } 146 | 147 | void test_CountEncode() { 148 | Datapoint dp("count", 0x0000, 4, VitoWiFi::noconv); 149 | const uint8_t expected[] = {0xD8, 0xCA, 0x56, 0x1B}; 150 | const uint32_t value = 458672856; 151 | const uint8_t len = 4; 152 | uint8_t buffer[len] = {0}; 153 | 154 | dp.encode(buffer, len, VariantValue(value)); 155 | 156 | TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, buffer, len); 157 | } 158 | 159 | void test_CountShortDecode() { 160 | Datapoint dp("count", 0x0000, 2, VitoWiFi::noconv); 161 | const uint8_t data[] = {0x26, 0xB3}; 162 | const uint16_t expected = 45862; 163 | PacketVS2 packet; 164 | packet.createPacket(PacketType::RESPONSE, FunctionCode::READ, 0, 0x0000, 2, data); 165 | 166 | uint16_t result = dp.decode(packet); 167 | 168 | TEST_ASSERT_EQUAL_UINT16(expected, result); 169 | } 170 | 171 | void test_CountShortEncode() { 172 | Datapoint dp("count", 0x0000, 2, VitoWiFi::noconv); 173 | const uint8_t expected[] = {0x26, 0xB3}; 174 | const uint16_t value = 45862; 175 | const uint8_t len = 2; 176 | uint8_t buffer[len] = {0}; 177 | 178 | dp.encode(buffer, len, VariantValue(value)); 179 | 180 | TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, buffer, len); 181 | } 182 | 183 | void test_COPDecode() { 184 | Datapoint dp("count", 0x0000, 1, VitoWiFi::div10); 185 | const uint8_t data[] = {0x1A}; 186 | const float expected = 2.6; 187 | PacketVS2 packet; 188 | packet.createPacket(PacketType::RESPONSE, FunctionCode::READ, 0, 0x0000, 1, data); 189 | 190 | float result = dp.decode(packet); 191 | 192 | TEST_ASSERT_EQUAL_FLOAT(expected, result); 193 | } 194 | 195 | void test_COPEncode() { 196 | Datapoint dp("count", 0x0000, 2, VitoWiFi::div10); 197 | const uint8_t expected[] = {0x1A}; 198 | const float value = 2.6; 199 | const uint8_t len = 1; 200 | uint8_t buffer[len] = {0}; 201 | 202 | dp.encode(buffer, len, VariantValue(value)); 203 | 204 | TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, buffer, len); 205 | } 206 | 207 | void test_ScheduleEncode() { 208 | const char* schedule = "7:30 08:30 16:20 23:10"; 209 | const uint8_t expected[] = {0x3B, 0x43, 0x82, 0xB9, 0x00, 0x00, 0x00, 0x00}; 210 | const size_t numSchedules = 2; 211 | const std::size_t len = 8; 212 | uint8_t buffer[len]; 213 | 214 | std::size_t result = VitoWiFi::encodeSchedule(schedule, buffer); 215 | 216 | TEST_ASSERT_EQUAL(numSchedules, result); 217 | TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, buffer, len); 218 | } 219 | 220 | void test_ScheduleDecode() { 221 | const uint8_t data[] = {0x3B, 0x43, 0x82, 0xB9, 0x00, 0x00, 0x00, 0x00}; 222 | const std::size_t len = 8; 223 | const char* expected = "07:30 08:30 16:20 23:10 00:00 00:00 00:00 00:00"; 224 | const std::size_t bufferLen = 48; 225 | char buffer[bufferLen]; 226 | 227 | std::size_t result = VitoWiFi::decodeSchedule(data, len, buffer, bufferLen); 228 | 229 | TEST_ASSERT_EQUAL(bufferLen, result); 230 | TEST_ASSERT_EQUAL_STRING_LEN(expected, buffer, bufferLen); 231 | } 232 | 233 | int main() { 234 | UNITY_BEGIN(); 235 | RUN_TEST(test_Converter); 236 | RUN_TEST(test_Bool); 237 | RUN_TEST(test_TempDecode); 238 | RUN_TEST(test_TempEncode); 239 | RUN_TEST(test_TempShortDecode); 240 | RUN_TEST(test_TempShortEncode); 241 | RUN_TEST(test_StatusDecode); 242 | RUN_TEST(test_StatusEncode); 243 | RUN_TEST(test_HourDecode); 244 | RUN_TEST(test_HourEncode); 245 | RUN_TEST(test_CountDecode); 246 | RUN_TEST(test_CountEncode); 247 | RUN_TEST(test_CountShortDecode); 248 | RUN_TEST(test_CountShortEncode); 249 | RUN_TEST(test_COPDecode); 250 | RUN_TEST(test_COPEncode); 251 | RUN_TEST(test_ScheduleEncode); 252 | RUN_TEST(test_ScheduleDecode); 253 | return UNITY_END(); 254 | } -------------------------------------------------------------------------------- /test/test_PacketVS1/test_PacketVS1.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include 10 | 11 | #include 12 | 13 | using VitoWiFi::PacketVS1; 14 | using VitoWiFi::PacketVS1Type; 15 | 16 | void setUp() {} 17 | void tearDown() {} 18 | 19 | void test_ok_requestRead() { 20 | const uint8_t data[] = { 21 | 0xF7, // read 22 | 0x55, // address 1 23 | 0x25, // address 2 24 | 0x02 // payload length 25 | }; 26 | const std::size_t length = 4; 27 | 28 | PacketVS1 packet; 29 | packet.createPacket(PacketVS1Type.READ, 30 | 0x5525, 31 | 2); 32 | 33 | TEST_ASSERT_TRUE(packet); 34 | TEST_ASSERT_EQUAL(length, packet.length()); 35 | TEST_ASSERT_EQUAL_UINT8_ARRAY(data, &packet[0], length); 36 | } 37 | 38 | void test_ok_requestWrite() { 39 | const uint8_t data[] = { 40 | 0xF4, // read 41 | 0x23, // address 1 42 | 0x23, // address 2 43 | 0x01, // payload length 44 | 0x01 45 | }; 46 | const std::size_t length = 5; 47 | uint8_t payload[1] = {0x01}; 48 | 49 | PacketVS1 packet; 50 | packet.createPacket(PacketVS1Type.WRITE, 51 | 0x2323, 52 | 1, 53 | payload); 54 | 55 | TEST_ASSERT_TRUE(packet); 56 | TEST_ASSERT_EQUAL(length, packet.length()); 57 | TEST_ASSERT_EQUAL_UINT8_ARRAY(data, &packet[0], length); 58 | } 59 | 60 | void test_packetType() { 61 | PacketVS1 packet; 62 | packet.createPacket(0x05, 63 | 0x5525, 64 | 2); 65 | 66 | TEST_ASSERT_FALSE(packet ? true : false); // contextually convert to bool 67 | } 68 | 69 | void test_payloadData() { 70 | PacketVS1 packet; 71 | packet.createPacket(PacketVS1Type.WRITE, 72 | 0x5525, 73 | 2); 74 | 75 | TEST_ASSERT_FALSE(packet ? true : false); // contextually convert to bool 76 | } 77 | 78 | int main() { 79 | UNITY_BEGIN(); 80 | RUN_TEST(test_ok_requestRead); 81 | RUN_TEST(test_ok_requestWrite); 82 | RUN_TEST(test_packetType); 83 | RUN_TEST(test_payloadData); 84 | return UNITY_END(); 85 | } -------------------------------------------------------------------------------- /test/test_PacketVS2/test_PacketVS2.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include 10 | 11 | #include 12 | 13 | using VitoWiFi::PacketVS2; 14 | using VitoWiFi::PacketType; 15 | using VitoWiFi::FunctionCode; 16 | 17 | void setUp() {} 18 | void tearDown() {} 19 | 20 | void test_ok_requestRead() { 21 | const uint8_t data[] = { 22 | 0x05, // length 23 | 0x00, // packet type (request) 24 | 0x01, // flags: id + function code (0 + read) 25 | 0x55, // address 1 26 | 0x25, // address 2 27 | 0x02 // payload length 28 | }; 29 | const std::size_t length = 6; 30 | const uint8_t checksum = 0x82; 31 | 32 | PacketVS2 packet; 33 | packet.createPacket(PacketType::REQUEST, 34 | FunctionCode::READ, 35 | 0, 36 | 0x5525, 37 | 2); 38 | 39 | TEST_ASSERT_TRUE(packet); 40 | TEST_ASSERT_EQUAL(length, packet.length()); 41 | TEST_ASSERT_EQUAL_UINT8_ARRAY(data, &packet[0], length); 42 | TEST_ASSERT_EQUAL_UINT8(checksum, packet.checksum()); 43 | } 44 | 45 | void test_ok_requestWrite() { 46 | const uint8_t data[] = { 47 | 0x06, // length 48 | 0x00, // packet type (request) 49 | 0x02, // flags: id + function code (0 + read) 50 | 0x23, // address 1 51 | 0x23, // address 2 52 | 0x01, // payload length 53 | 0x01 // payload 54 | }; 55 | const std::size_t length = 7; 56 | const uint8_t checksum = 0x50; 57 | 58 | uint8_t payload[1] = {0x01}; 59 | PacketVS2 packet; 60 | packet.createPacket(PacketType::REQUEST, 61 | FunctionCode::WRITE, 62 | 0, 63 | 0x2323, 64 | 1, 65 | payload); 66 | 67 | TEST_ASSERT_TRUE(packet); 68 | TEST_ASSERT_EQUAL(length, packet.length()); 69 | TEST_ASSERT_EQUAL_UINT8_ARRAY(data, &packet[0], length); 70 | TEST_ASSERT_EQUAL_UINT8(checksum, packet.checksum()); 71 | } 72 | 73 | void test_packetId() { 74 | const uint8_t data[] = { 75 | 0x05, // length 76 | 0x00, // packet type (request) 77 | 0xA1, // flags: id + function code (5 + read) 78 | 0x55, // address 1 79 | 0x25, // address 2 80 | 0x02 // payload length 81 | }; 82 | const uint8_t packetId = 5; 83 | const std::size_t length = 6; 84 | const uint8_t checksum = 0x22; 85 | 86 | PacketVS2 packet; 87 | packet.createPacket(PacketType::REQUEST, 88 | FunctionCode::READ, 89 | 5, 90 | 0x5525, 91 | 2); 92 | 93 | TEST_ASSERT_TRUE(packet); 94 | TEST_ASSERT_EQUAL(length, packet.length()); 95 | TEST_ASSERT_EQUAL(packetId, packet.id()); 96 | TEST_ASSERT_EQUAL_UINT8_ARRAY(data, &packet[0], length); 97 | TEST_ASSERT_EQUAL_UINT8(checksum, packet.checksum()); 98 | } 99 | 100 | void test_payloadLength() { 101 | PacketVS2 packet; 102 | packet.createPacket(PacketType::REQUEST, 103 | FunctionCode::READ, 104 | 5, 105 | 0x5525, 106 | 0); 107 | 108 | TEST_ASSERT_FALSE(packet ? true : false); // contextually convert to bool 109 | } 110 | 111 | void test_payloadData() { 112 | PacketVS2 packet; 113 | packet.createPacket(PacketType::REQUEST, 114 | FunctionCode::WRITE, 115 | 5, 116 | 0x5525, 117 | 2); 118 | 119 | TEST_ASSERT_FALSE(packet ? true : false); // contextually convert to bool 120 | } 121 | 122 | int main() { 123 | UNITY_BEGIN(); 124 | RUN_TEST(test_ok_requestRead); 125 | RUN_TEST(test_ok_requestWrite); 126 | RUN_TEST(test_packetId); 127 | RUN_TEST(test_payloadLength); 128 | RUN_TEST(test_payloadData); 129 | return UNITY_END(); 130 | } -------------------------------------------------------------------------------- /test/test_ParserVS2/test_ParserVS2.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2023 Bert Melis. All rights reserved. 3 | 4 | This work is licensed under the terms of the MIT license. 5 | For a copy, see or 6 | the LICENSE file. 7 | */ 8 | 9 | #include 10 | 11 | #include 12 | 13 | using VitoWiFiInternals::ParserVS2; 14 | using VitoWiFiInternals::ParserResult; 15 | using VitoWiFi::PacketType; 16 | using VitoWiFi::FunctionCode; 17 | 18 | void setUp() {} 19 | void tearDown() {} 20 | 21 | ParserVS2 parser; 22 | 23 | void test_ok_request() { 24 | const uint8_t stream[] = { 25 | 0x41, // start byte 26 | 0x05, // length 27 | 0x00, // packet type (request) 28 | 0x01, // flags: id + function code (0 + read) 29 | 0x55, // address 1 30 | 0x25, // address 2 31 | 0x02, // payload length 32 | 0x82 // cs 33 | }; 34 | const std::size_t length = 8; 35 | const std::size_t packetLength = 6; 36 | 37 | std::size_t bytesRead = 0; 38 | ParserResult result = ParserResult::ERROR; 39 | 40 | while (bytesRead < length) { 41 | result = parser.parse(stream[bytesRead++]); 42 | if (result != ParserResult::CONTINUE) { 43 | break; 44 | } 45 | } 46 | 47 | TEST_ASSERT_EQUAL(ParserResult::COMPLETE, result); 48 | TEST_ASSERT_EQUAL_UINT(length, bytesRead); 49 | TEST_ASSERT_EQUAL_UINT8(packetLength, parser.packet().length()); 50 | TEST_ASSERT_EQUAL_UINT8(PacketType::REQUEST, parser.packet().packetType()); 51 | TEST_ASSERT_EQUAL_UINT8(0x00, parser.packet().id()); 52 | TEST_ASSERT_EQUAL_UINT8(FunctionCode::READ, parser.packet().functionCode()); 53 | TEST_ASSERT_EQUAL_UINT16(0x5525, parser.packet().address()); 54 | TEST_ASSERT_EQUAL_UINT8(0x02, parser.packet().dataLength()); 55 | } 56 | 57 | void test_ok_readresponse() { 58 | const uint8_t stream[] = { 59 | 0x41, // start byte 60 | 0x07, // length 61 | 0x01, // packet type (response) 62 | 0x01, // flags: id + function code (0 + read) 63 | 0x55, // address 1 64 | 0x25, // address 2 65 | 0x02, // payload length 66 | 0x07, // payload 67 | 0x01, 68 | 0x8D // cs 69 | }; 70 | const std::size_t length = 10; 71 | const std::size_t packetLength = 8; 72 | const uint8_t data[2] = {0x07, 0x01}; 73 | 74 | std::size_t bytesRead = 0; 75 | ParserResult result = ParserResult::ERROR; 76 | 77 | while (bytesRead < length) { 78 | result = parser.parse(stream[bytesRead++]); 79 | if (result != ParserResult::CONTINUE) { 80 | break; 81 | } 82 | } 83 | 84 | TEST_ASSERT_EQUAL(ParserResult::COMPLETE, result); 85 | TEST_ASSERT_EQUAL_UINT(length, bytesRead); 86 | TEST_ASSERT_EQUAL_UINT8(packetLength, parser.packet().length()); 87 | TEST_ASSERT_EQUAL_UINT8(PacketType::RESPONSE, parser.packet().packetType()); 88 | TEST_ASSERT_EQUAL_UINT8(0x00, parser.packet().id()); 89 | TEST_ASSERT_EQUAL_UINT8(FunctionCode::READ, parser.packet().functionCode()); 90 | TEST_ASSERT_EQUAL_UINT16(0x5525, parser.packet().address()); 91 | TEST_ASSERT_EQUAL_UINT8(0x02, parser.packet().dataLength()); 92 | TEST_ASSERT_EQUAL_UINT8_ARRAY(data, parser.packet().data(), 2); 93 | } 94 | 95 | void test_ok_writeresponse() { 96 | const uint8_t stream[] = { 97 | 0x41, // start byte 98 | 0x05, // length 99 | 0x01, // packet type (response) 100 | 0x02, // flags: id + function code (0 + write) 101 | 0x23, // address 1 102 | 0x23, // address 2 103 | 0x01, // payload length 104 | 0x4F // cs 105 | }; 106 | const std::size_t length = 8; 107 | const std::size_t packetLength = 6; 108 | 109 | std::size_t bytesRead = 0; 110 | ParserResult result = ParserResult::ERROR; 111 | 112 | while (bytesRead < length) { 113 | result = parser.parse(stream[bytesRead++]); 114 | if (result != ParserResult::CONTINUE) { 115 | break; 116 | } 117 | } 118 | 119 | TEST_ASSERT_EQUAL(ParserResult::COMPLETE, result); 120 | TEST_ASSERT_EQUAL_UINT(length, bytesRead); 121 | TEST_ASSERT_EQUAL_UINT8(packetLength, parser.packet().length()); 122 | TEST_ASSERT_EQUAL_UINT8(PacketType::RESPONSE, parser.packet().packetType()); 123 | TEST_ASSERT_EQUAL_UINT8(0x00, parser.packet().id()); 124 | TEST_ASSERT_EQUAL_UINT8(FunctionCode::WRITE, parser.packet().functionCode()); 125 | TEST_ASSERT_EQUAL_UINT16(0x2323, parser.packet().address()); 126 | TEST_ASSERT_EQUAL_UINT8(0x01, parser.packet().dataLength()); 127 | TEST_ASSERT_NULL(parser.packet().data()); 128 | } 129 | 130 | void test_spuriousbytes() { 131 | const uint8_t stream[] = { 132 | 0x05, 133 | 0x01, 134 | 0x41, // start byte 135 | 0x07, // length 136 | 0x01, // packet type (response) 137 | 0x01, // flags: id + function code (0 + read) 138 | 0x55, // address 1 139 | 0x25, // address 2 140 | 0x02, // payload length 141 | 0x07, // payload 142 | 0x01, 143 | 0x8D // cs 144 | }; 145 | const std::size_t length = 12; 146 | const std::size_t packetLength = 8; 147 | const uint8_t data[2] = {0x07, 0x01}; 148 | 149 | std::size_t bytesRead = 0; 150 | ParserResult result = ParserResult::ERROR; 151 | 152 | while (bytesRead < length) { 153 | result = parser.parse(stream[bytesRead++]); 154 | if (result != ParserResult::CONTINUE) { 155 | break; 156 | } 157 | } 158 | 159 | TEST_ASSERT_EQUAL(ParserResult::COMPLETE, result); 160 | TEST_ASSERT_EQUAL_UINT(length, bytesRead); 161 | TEST_ASSERT_EQUAL_UINT8(packetLength, parser.packet().length()); 162 | TEST_ASSERT_EQUAL_UINT8(PacketType::RESPONSE, parser.packet().packetType()); 163 | TEST_ASSERT_EQUAL_UINT8(0x00, parser.packet().id()); 164 | TEST_ASSERT_EQUAL_UINT8(FunctionCode::READ, parser.packet().functionCode()); 165 | TEST_ASSERT_EQUAL_UINT16(0x5525, parser.packet().address()); 166 | TEST_ASSERT_EQUAL_UINT8(0x02, parser.packet().dataLength()); 167 | TEST_ASSERT_EQUAL_UINT8_ARRAY(data, parser.packet().data(), 2); 168 | } 169 | 170 | void test_invalidLength() { 171 | const uint8_t stream[] = { 172 | 0x41, // start byte 173 | 0x00, // length 174 | 0x01, // packet type (response) 175 | 0x01, // flags: id + function code (0 + read) 176 | 0x55, // address 1 177 | 0x25, // address 2 178 | 0x02, // payload length 179 | 0x07, // payload 180 | 0x01, 181 | 0x8D // cs 182 | }; 183 | const std::size_t length = 12; 184 | 185 | std::size_t bytesRead = 0; 186 | ParserResult result = ParserResult::ERROR; 187 | 188 | while (bytesRead < length) { 189 | result = parser.parse(stream[bytesRead++]); 190 | if (result != ParserResult::CONTINUE) { 191 | break; 192 | } 193 | } 194 | 195 | TEST_ASSERT_EQUAL(ParserResult::ERROR, result); 196 | TEST_ASSERT_EQUAL_UINT(2, bytesRead); 197 | } 198 | 199 | void test_invalidPacketType() { 200 | const uint8_t stream[] = { 201 | 0x41, // start byte 202 | 0x07, // length 203 | 0x06, // packet type (invalid) 204 | 0x01, // flags: id + function code (0 + read) 205 | 0x55, // address 1 206 | 0x25, // address 2 207 | 0x02, // payload length 208 | 0x07, // payload 209 | 0x01, 210 | 0x8D // cs 211 | }; 212 | const std::size_t length = 12; 213 | 214 | std::size_t bytesRead = 0; 215 | ParserResult result = ParserResult::ERROR; 216 | 217 | while (bytesRead < length) { 218 | result = parser.parse(stream[bytesRead++]); 219 | if (result != ParserResult::CONTINUE) { 220 | break; 221 | } 222 | } 223 | 224 | TEST_ASSERT_EQUAL(ParserResult::ERROR, result); 225 | TEST_ASSERT_EQUAL_UINT(3, bytesRead); 226 | } 227 | 228 | void test_invalidFunctionCode() { 229 | const uint8_t stream[] = { 230 | 0x41, // start byte 231 | 0x07, // length 232 | 0x01, // packet type (response) 233 | 0x05, // flags: id + function code (0 + read) 234 | 0x55, // address 1 235 | 0x25, // address 2 236 | 0x02, // payload length 237 | 0x07, // payload 238 | 0x01, 239 | 0x8D // cs 240 | }; 241 | const std::size_t length = 12; 242 | 243 | std::size_t bytesRead = 0; 244 | ParserResult result = ParserResult::ERROR; 245 | 246 | while (bytesRead < length) { 247 | result = parser.parse(stream[bytesRead++]); 248 | if (result != ParserResult::CONTINUE) { 249 | break; 250 | } 251 | } 252 | 253 | TEST_ASSERT_EQUAL(ParserResult::ERROR, result); 254 | TEST_ASSERT_EQUAL_UINT(4, bytesRead); 255 | } 256 | 257 | void test_invalidChecksum() { 258 | const uint8_t stream[] = { 259 | 0x41, // start byte 260 | 0x07, // length 261 | 0x01, // packet type (response) 262 | 0x01, // flags: id + function code (0 + read) 263 | 0x55, // address 1 264 | 0x25, // address 2 265 | 0x02, // payload length 266 | 0x07, // payload 267 | 0x01, 268 | 0x8E // cs 269 | }; 270 | const std::size_t length = 12; 271 | 272 | std::size_t bytesRead = 0; 273 | ParserResult result = ParserResult::ERROR; 274 | 275 | while (bytesRead < length) { 276 | result = parser.parse(stream[bytesRead++]); 277 | if (result != ParserResult::CONTINUE) { 278 | break; 279 | } 280 | } 281 | 282 | TEST_ASSERT_EQUAL(ParserResult::CS_ERROR, result); 283 | TEST_ASSERT_EQUAL_UINT(10, bytesRead); 284 | } 285 | 286 | int main() { 287 | UNITY_BEGIN(); 288 | RUN_TEST(test_ok_request); 289 | RUN_TEST(test_ok_readresponse); 290 | RUN_TEST(test_ok_writeresponse); 291 | RUN_TEST(test_spuriousbytes); 292 | RUN_TEST(test_invalidLength); 293 | RUN_TEST(test_invalidPacketType); 294 | RUN_TEST(test_invalidFunctionCode); 295 | RUN_TEST(test_invalidChecksum); 296 | return UNITY_END(); 297 | } -------------------------------------------------------------------------------- /test_coverage.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | Import("env", "projenv") 4 | 5 | # Dump build environment (for debug purpose) 6 | #print(env.Dump()) 7 | 8 | # access to global build environment 9 | #print(env) 10 | 11 | # access to the project build environment 12 | # (used for source files located in the "src" folder) 13 | #print(projenv) 14 | 15 | def generateCoverageInfo(source, target, env): 16 | for file in os.listdir("test"): 17 | os.system(".pio/build/native/program test/"+file) 18 | os.system("lcov -d .pio/build/native/ -c -o lcov.info") 19 | os.system("lcov --remove lcov.info '*Unity*' '*unity*' '/usr/include/*' '*/test/*' -o filtered_lcov.info") 20 | os.system("genhtml -o cov/ --demangle-cpp filtered_lcov.info") 21 | 22 | env.AddPostAction(".pio/build/native/program", generateCoverageInfo) --------------------------------------------------------------------------------