├── .github └── workflows │ └── platformio.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── docs ├── esp32-edge-gateway.svg ├── esp32-integrated.svg └── qrcode.svg ├── json-server ├── README.md ├── routes.json └── test-device.json ├── main ├── CMakeLists.txt ├── Kconfig.projbuild ├── can.c ├── can.h ├── certs │ └── isrgrootx1.pem ├── data_nodes.cpp ├── data_nodes.h ├── emoncms.c ├── emoncms.h ├── isotp_user.c ├── main.c ├── ota.c ├── ota.h ├── provisioning.c ├── provisioning.h ├── stm32bl.c ├── stm32bl.h ├── ts_cbor.c ├── ts_cbor.h ├── ts_client.c ├── ts_client.h ├── ts_mqtt.c ├── ts_mqtt.h ├── ts_serial.c ├── ts_serial.h ├── web_fs.c ├── web_fs.h ├── web_server.c ├── web_server.h ├── wifi.c └── wifi.h ├── partitions.csv ├── platformio.ini ├── sdkconfig.defaults ├── test ├── esp_err.h ├── main.c ├── test_ts_client.c └── tests.h └── webapp ├── .eslintrc.js ├── .gitignore ├── babel.config.js ├── info.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── EspOta.vue │ └── LineChart.vue ├── main.js ├── plugins │ └── vuetify.js ├── router.js ├── store.js └── views │ ├── Config.vue │ ├── ConfigGeneral.vue │ ├── DataLog.vue │ ├── Home.vue │ ├── IO.vue │ ├── Info.vue │ ├── Live.vue │ └── Ota.vue └── vue.config.js /.github/workflows/platformio.yml: -------------------------------------------------------------------------------- 1 | name: PlatformIO CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | submodules: recursive 14 | - name: Set up Python 15 | uses: actions/setup-python@v1 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install wheel 20 | pip install -U platformio 21 | pio platform install native 22 | pio update 23 | - name: Run trailing white space check with git 24 | run: | 25 | git diff --check `git rev-list HEAD | tail -n 1`.. 26 | - name: Run PlatformIO build tests 27 | run: | 28 | platformio run -e esp32-edge 29 | - name: Run PlatformIO unit-tests 30 | run: | 31 | platformio test -e unit_test 32 | - name: Run PlatformIO code checks 33 | run: | 34 | platformio check -e esp32-edge --skip-packages --fail-on-defect high 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # ESP-IDF build files and user configuration 3 | build 4 | sdkconfig* 5 | !sdkconfig.defaults 6 | 7 | # PlatformIO build files 8 | .pio 9 | 10 | # Other files and folders 11 | .vscode 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/isotp"] 2 | path = lib/isotp 3 | url = https://github.com/LibreSolar/isotp-c 4 | [submodule "lib/thingset"] 5 | path = lib/thingset 6 | url = https://github.com/LibreSolar/thingset-device-library.git 7 | [submodule "lib/tinycbor"] 8 | path = lib/tinycbor 9 | url = https://github.com/intel/tinycbor.git 10 | [submodule "lib/cjson"] 11 | path = lib/cjson 12 | url = https://github.com/DaveGamble/cJSON.git 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | cmake_minimum_required(VERSION 3.5) 4 | 5 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 6 | 7 | project(esp32-edge) 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Libre Solar ESP32 Edge Firmware 2 | 3 | Firmware for CAN and UART to WiFi or Bluetooth gateway based on ESP32-IDF v4.0. 4 | 5 | **Warning:** This firmware is at a very early stage. Expect bugs and report them in the issues :) 6 | 7 | ## Concept Overview 8 | 9 | The firmware should support multiple use-cases to connect Libre Solar devices with other networks (incl. the internet). 10 | 11 | ### ESP32 acting as an Edge Gateway to the cloud 12 | 13 | In this application, the ESP32 is integrated in a separate device (like the Libre Solar [Data Manager](https://github.com/LibreSolar/data-manager)) and communicates with the other devices like the charge controller via CAN bus. 14 | 15 | The ThingSet protocol on the CAN bus can translated to MQTT in order to push data to a cloud. For local access, the ESP32 can directly serve a website or provide a Bluetooth Low Energy interface for a mobile phone app. 16 | 17 | ![Edge Gateway Application](docs/esp32-edge-gateway.svg) 18 | 19 | ### ESP32 integrated in device (e.g. charge controller) 20 | 21 | The ESP32 board can also be directly integrated in a charge controller or other devices and communicate with the host device via UART interface (again using the ThingSet protocol). 22 | 23 | The data can be accessed in the same way as described above. 24 | 25 | ![Application with ESP32 integrated in device](docs/esp32-integrated.svg) 26 | 27 | ## Supported devices 28 | 29 | - Libre Solar [Data Manager](https://github.com/LibreSolar/data-manager) 30 | - Sparkfun ESP32thing 31 | - Most other ESP32-based boards 32 | 33 | ## Firmware features 34 | 35 | - Written in C using ESP-IDF and PlatformIO 36 | - Data input from Libre Solar devices via 37 | - [LS.bus](https://libre.solar/hardware/ls-bus.html) (CAN bus) 38 | - [LS.one](https://libre.solar/hardware/ls-one.html) (UART serial) 39 | - Local communication via [ThingSet Protocol](http://libre.solar/thingset/) 40 | - Data access via HTTP JSON API 41 | - Publishing of monitoring data via WiFi to 42 | - Open Energy Monitor [Emoncms](https://emoncms.org/) 43 | - MQTT server (ToDo) 44 | - Data logging on SD card (ToDo) 45 | 46 | ## Usage 47 | 48 | ### Getting the firmware 49 | 50 | This firmware repository contains git submodules, so you need to clone (download) it by calling: 51 | 52 | ``` 53 | git clone --recursive https://github.com/LibreSolar/esp32-edge-firmware 54 | ``` 55 | 56 | Unfortunately, the green GitHub "Clone or download" button does not include submodules. If you cloned the repository already and want to pull the submodules, run `git submodule update --init --recursive`. 57 | 58 | 59 | ### Building the webapp 60 | 61 | To be able to build the esp32-edge-firmware you need to build the webapp first. 62 | To do so, go into the webapp folder 63 | 64 | ``` 65 | cd esp32-edge-firmware/webapp 66 | ``` 67 | and run 68 | 69 | ``` 70 | npm install 71 | npm run build 72 | ``` 73 | 74 | You are now able to build the firmware itself. 75 | 76 | ### ESP-IDF toolchain 77 | 78 | The ESP-IDF is the native toolchain for ESP32 microcontrollers by Espressif. Follow [this guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html#) to install it. 79 | 80 | After installation run the following commands: 81 | 82 | idf.py build # compile the firmware 83 | idf.py flash # flash it to the device 84 | 85 | ### PlatformIO 86 | 87 | You can use PlatformIO for easy building and flashing. However, the PlatformIO packages for ESP-IDF are not updated as frequently as the official repositories. 88 | 89 | ### Configuration 90 | 91 | The firmware is configured using Kconfig integrated into ESP-IDF. 92 | 93 | The most convenient way is to run `idf.py menuconfig` after the ESP-IDF was successfully installed. If ESP-IDF is not available and PlatformIO is used, configuration can be changed manually in the generated `sdkconfig` file. 94 | 95 | ### Wifi Connection 96 | 97 | The firmware supports provisioning via bluetooth. You can download the espressif BLE provisioning app in the [iOS](https://apps.apple.com/in/app/esp-ble-provisioning/id1473590141) or [android](https://play.google.com/store/apps/details?id=com.espressif.provble) app store and scan this QR code: 98 | 99 | ![Provisioning QR Code](docs/qrcode.svg) 100 | 101 | You can also scan for the device manually in the app, the data manager should be found as device `PROV_LS-DM`, the service pin is `LibreSolar`. 102 | -------------------------------------------------------------------------------- /json-server/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## JSON API server 4 | 5 | npm install -g json-server 6 | 7 | json-server --watch test-device.json --routes routes.json 8 | -------------------------------------------------------------------------------- /json-server/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": "/$1" 3 | } -------------------------------------------------------------------------------- /json-server/test-device.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "DeviceID": "abcd1234", 4 | "Manufacturer": "Libre Solar Technologies GmbH", 5 | "DeviceType": "MPPT 1210 HUS", 6 | "HardwareVersion": "0.7.1", 7 | "FirmwareVersion": "19.1", 8 | "FirmwareCommit": "91d4ec44243e08c39c9bc559be5502534652f398" 9 | }, 10 | "conf": { 11 | "BatNom_Ah": 40, 12 | "BatRecharge_V": 13.4, 13 | "BatAbsMin_V": 9.0, 14 | "BatChgMax_A": 40, 15 | "BatTarget_V": 14.4, 16 | "BatCutoff_A": 1.0, 17 | "BatCutoff_s": 3600, 18 | "TrickleEn": true, 19 | "Trickle_V": 13.8, 20 | "TrickleRecharge_s": 3600, 21 | "TempFactor": 3, 22 | "BatInt_Ohm": 0.05, 23 | "BatWire_Ohm": 0, 24 | "BatChgMax_degC": 40, 25 | "BatChgMin_degC": 0, 26 | "BatDisMax_degC": 60, 27 | "BatDisMin_degC": -10, 28 | "EqEn": false, 29 | "Eq_V": 14.8, 30 | "Eq_A": 2.0, 31 | "EqDuration_s": 1800, 32 | "EqInterval_d": 8, 33 | "EqDeepDisTrigger": 10, 34 | "LoadEnDefault": true, 35 | "UsbEnDefault": true, 36 | "LoadDisconnect_V": 10.8, 37 | "LoadReconnect_V": 11.7, 38 | "UsbDisconnect_V": 10.8, 39 | "UsbReconnect_V": 11.7, 40 | "LoadOCRecovery_s": 60, 41 | "LoadUVRecovery_s": 10, 42 | "UsbUVRecovery_s": 10 43 | }, 44 | "input": { 45 | "LoadEn": true, 46 | "DcdcEn": true, 47 | "UsbEn": false 48 | }, 49 | "output": { 50 | "LoadInfo": 1, 51 | "UsbInfo": 1, 52 | "SOC_%": 0.17, 53 | "Bat_V": 12.78, 54 | "Solar_V": 31.4, 55 | "Bat_A": 2.52, 56 | "Load_A": 1.39, 57 | "Bat_degC": 22.3, 58 | "BatTempExt": false, 59 | "Int_degC": 28.1, 60 | "Mosfet_degC": 42.1, 61 | "ChgState": 2, 62 | "DCDCState": 1, 63 | "Solar_A": 0.58, 64 | "BatTarget_V": 14.4, 65 | "BatTarget_A": 2.0, 66 | "Bat_W": 53, 67 | "Solar_W": 94, 68 | "Load_W": 15, 69 | "NumBatteries": 1, 70 | "ErrorFlags": 0 71 | }, 72 | "exec": { 73 | "Reset": null, 74 | "Auth": "secret" 75 | }, 76 | "pub": { 77 | "serial": { 78 | "enable": true, 79 | "interval": 1.0, 80 | "ids": ["Bat_V", "Bat_A"] 81 | }, 82 | "can": { 83 | "enable": false, 84 | "interval": 0.1, 85 | "ids": ["Test"] 86 | } 87 | }, 88 | "log": { 89 | "daily": { 90 | "23094834": {"Bat_kWh":123,"Solar_kWh":456}, 91 | "34209348": {"Bat_kWh":151,"Solar_kWh":531} 92 | }, 93 | "hourly": { 94 | "3093453": {"other":1,"data_objects":2} 95 | } 96 | }, 97 | "nodes": { 98 | "abcd1234": "MPPT 1210 HUS", 99 | "efgh5678": "BMS 8S50 IC" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | set(app_sources 4 | "main.c" 5 | "ts_client.c" 6 | "ts_serial.c" 7 | "ts_mqtt.c" 8 | "can.c" 9 | "emoncms.c" 10 | "stm32bl.c" 11 | "wifi.c" 12 | "web_fs.c" 13 | "web_server.c" 14 | "provisioning.c" 15 | "../lib/isotp/isotp.c" 16 | "isotp_user.c" 17 | "../lib/thingset/src/thingset.cpp" 18 | "../lib/thingset/src/thingset_bin.cpp" 19 | "../lib/thingset/src/thingset_txt.cpp" 20 | "../lib/thingset/src/cbor.c" 21 | "../lib/thingset/src/jsmn.c" 22 | "data_nodes.cpp" 23 | "ts_cbor.c" 24 | "ota.c" 25 | ) 26 | 27 | idf_component_register(SRCS ${app_sources} INCLUDE_DIRS ".") 28 | 29 | # Let's Encrypt ISRG Root X1 certificate for MQTTS, valid until 2035 30 | # Source: https://letsencrypt.org/certificates/ 31 | target_add_binary_data(${COMPONENT_TARGET} "certs/isrgrootx1.pem" TEXT) 32 | 33 | # Create a SPIFFS image from the contents of the 'webapp' directory 34 | # that fits the partition named 'storage'. FLASH_IN_PROJECT indicates that 35 | # the generated image should be flashed when the entire project is flashed to 36 | # the target with 'idf.py -p PORT flash'. 37 | spiffs_create_partition_image(website ../webapp/dist FLASH_IN_PROJECT) 38 | 39 | # Uncomment to upload STM32 firmware binary saved in ../stm_image/firmware.bin (for testing) 40 | #spiffs_create_partition_image(stm_ota ../stm_image FLASH_IN_PROJECT) 41 | 42 | find_package(Git) 43 | if(GIT_FOUND) 44 | execute_process( 45 | COMMAND ${GIT_EXECUTABLE} describe --long --dirty --tags 46 | OUTPUT_VARIABLE FIRMWARE_VERSION_ID 47 | OUTPUT_STRIP_TRAILING_WHITESPACE) 48 | else() 49 | set(FIRMWARE_VERSION_ID "unknown") 50 | endif() 51 | set(PROJECT_VER "${FIRMWARE_VERSION_ID}") 52 | -------------------------------------------------------------------------------- /main/Kconfig.projbuild: -------------------------------------------------------------------------------- 1 | menu "Board Selection" 2 | 3 | config GPIO_LED 4 | int "GPIO pin of LED" 5 | default 18 6 | 7 | config THINGSET_CAN 8 | bool "ThingSet CAN interface (LS.bus)" 9 | default y 10 | 11 | config GPIO_CAN_RX 12 | int "GPIO pin of CAN RX" 13 | default 4 14 | 15 | config GPIO_CAN_TX 16 | int "GPIO pin of CAN TX" 17 | default 5 18 | 19 | config GPIO_CAN_STB 20 | int "GPIO pin of CAN standby" 21 | default 12 22 | 23 | config THINGSET_SERIAL 24 | bool "ThingSet serial interface (LS.one)" 25 | default y 26 | 27 | config GPIO_UART_RX 28 | int "GPIO pin of UART RX" 29 | default 16 30 | 31 | config GPIO_UART_TX 32 | int "GPIO pin of UART TX" 33 | default 17 34 | 35 | endmenu 36 | 37 | menu "User Configuration" 38 | 39 | config WIFI_SSID 40 | string "WiFi SSID" 41 | help 42 | SSID (network name) for the example to connect to. 43 | 44 | config WIFI_PASSWORD 45 | string "WiFi password" 46 | help 47 | WiFi password (WPA or WPA2). 48 | Can be left blank if the network has no security set. 49 | 50 | config DEVICE_HOSTNAME 51 | string "Device host name" 52 | default "esp32-edge" 53 | help 54 | Specify the default host name for this device. It shall be user-configurable in 55 | the future. 56 | 57 | menuconfig THINGSET_MQTT 58 | bool "ThingSet MQTT support" 59 | default n 60 | help 61 | Currently disabled by default to pass CI, as it is not compatible with outdated 62 | ESP-IDF release used by PlatformIO. 63 | 64 | config THINGSET_MQTT_BROKER_URI 65 | string "Hostname of MQTT broker" 66 | default "mqtts://mqtt.eclipseprojects.io:8883" 67 | 68 | config THINGSET_MQTT_TLS 69 | bool "Use secure MQTT connection via TLS (recommended)" 70 | default y 71 | 72 | config THINGSET_MQTT_AUTHENTICATION 73 | bool "Use MQTT broker authentication" 74 | default y 75 | 76 | config THINGSET_MQTT_USER 77 | string "MQTT username" 78 | depends on THINGSET_MQTT_AUTHENTICATION 79 | 80 | config THINGSET_MQTT_PASS 81 | string "MQTT password" 82 | depends on THINGSET_MQTT_AUTHENTICATION 83 | 84 | config THINGSET_MQTT_PUBLISH_INTERVAL 85 | int "MQTT publish interval in seconds" 86 | default 10 87 | 88 | menuconfig EMONCMS 89 | bool "OpenEnergyMonitor Emoncms support" 90 | default n 91 | 92 | config EMONCMS_HOST 93 | string "Hostname of Emoncms server" 94 | default "emoncms.org" 95 | 96 | config EMONCMS_PORT 97 | string "Port of Emoncms server" 98 | default "80" 99 | 100 | config EMONCMS_URL 101 | string "URL of Emoncms post API endpoint" 102 | default "/emoncms/input/post" 103 | 104 | config EMONCMS_APIKEY 105 | string "API key for Emoncms access" 106 | default "" 107 | 108 | config EMONCMS_NODE_SERIAL 109 | string "Node name of ThingSet device connected to serial" 110 | default "serial" 111 | 112 | config EMONCMS_NODE_MPPT 113 | string "Node name of MPPT connected via CAN" 114 | default "mppt" 115 | 116 | config EMONCMS_NODE_BMS 117 | string "Node name of BMS connected via CAN" 118 | default "bms" 119 | 120 | endmenu 121 | -------------------------------------------------------------------------------- /main/can.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include "can.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "freertos/FreeRTOS.h" 16 | #include "freertos/task.h" 17 | #include "freertos/event_groups.h" 18 | #include "freertos/queue.h" 19 | 20 | #include "esp_system.h" 21 | #include "esp_err.h" 22 | #include "esp_log.h" 23 | 24 | #include "driver/twai.h" 25 | #include "driver/gpio.h" 26 | 27 | #include "../lib/isotp/isotp.h" 28 | #include "../lib/isotp/isotp_defines.h" 29 | 30 | #include "ts_client.h" 31 | #include "ts_cbor.h" 32 | #include "cJSON.h" 33 | static const char *TAG = "can"; 34 | 35 | bool update_bms_received = false; 36 | bool update_mppt_received = false; 37 | 38 | xQueueHandle receive_queue; 39 | #define RECV_QUEUE_SIZE 1 40 | #define ISOTP_BUFSIZE 1000 41 | 42 | /* Alloc IsoTpLink statically in RAM */ 43 | static IsoTpLink isotp_link; 44 | 45 | /* Alloc send and receive buffer statically in RAM */ 46 | static uint8_t isotp_recv_buf[ISOTP_BUFSIZE]; 47 | static uint8_t isotp_send_buf[ISOTP_BUFSIZE]; 48 | 49 | uint32_t can_addr_client = 0xF1; // this device 50 | 51 | // buffer for JSON string generated from received data objects via CAN 52 | static char json_buf[500]; 53 | 54 | static const twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); 55 | static const twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); 56 | static const twai_general_config_t g_config = 57 | TWAI_GENERAL_CONFIG_DEFAULT(CONFIG_GPIO_CAN_TX, CONFIG_GPIO_CAN_RX, TWAI_MODE_NORMAL); 58 | 59 | DataObject data_obj_bms[] = { 60 | {0x70, "Bat_V", {0}, 0}, 61 | {0x71, "Bat_A", {0}, 0}, 62 | {0x72, "Bat_degC", {0}, 0}, 63 | {0x76, "IC_degC", {0}, 0}, 64 | {0x77, "MOSFETs_degC", {0}, 0}, 65 | /* 66 | {0x03, "Cell1_V", {0}, 0}, 67 | {0x04, "Cell2_V", {0}, 0}, 68 | {0x05, "Cell3_V", {0}, 0}, 69 | {0x06, "Cell4_V", {0}, 0}, 70 | {0x07, "Cell5_V", {0}, 0}, 71 | {0x0A, "SOC", {0}, 0} 72 | */ 73 | }; 74 | 75 | DataObject data_obj_mppt[] = { 76 | {0x04, "LoadState", {0}, 0}, 77 | {0x0F, "SolarMaxDay_W", {0}, 0}, 78 | {0x10, "LoadMaxDay_W", {0}, 0}, 79 | {0x70, "Bat_V", {0}, 0}, 80 | {0x71, "Solar_V", {0}, 0}, 81 | {0x72, "Bat_A", {0}, 0}, 82 | {0x73, "Load_A", {0}, 0}, 83 | {0x74, "Bat_degC", {0}, 0}, 84 | {0x76, "Int_degC", {0}, 0}, 85 | {0x77, "Mosfet_degC", {0}, 0}, 86 | {0x78, "ChgState", {0}, 0}, 87 | {0x79, "DCDCState", {0}, 0}, 88 | {0x7a, "Solar_A", {0}, 0}, 89 | {0x7d, "Bat_W", {0}, 0}, 90 | {0x7e, "Solar_W", {0}, 0}, 91 | {0x7f, "Load_W", {0}, 0}, 92 | {0xa0, "SolarInDay_Wh", {0}, 0}, 93 | {0xa1, "LoadOutDay_Wh", {0}, 0}, 94 | {0xa2, "BatChgDay_Wh", {0}, 0}, 95 | {0xa3, "BatDisDay_Wh", {0}, 0}, 96 | {0x06, "SOC", {0}, 0}, 97 | {0xA4, "Dis_Ah", {0}, 0} 98 | }; 99 | 100 | static int generate_json_string(char *buf, size_t len, DataObject *objs, size_t num_objs) 101 | { 102 | union float2bytes { float f; char b[4]; } f2b; // for conversion of float to single bytes 103 | int pos = 0; 104 | 105 | for (int i = 0; i < num_objs; i++) { 106 | 107 | if (objs[i].raw_data[0] == 0) { 108 | continue; 109 | } 110 | 111 | // print data object ID 112 | if (pos == 0) { 113 | pos += snprintf(&buf[pos], len - pos, "{\"%s\":", objs[i].name); 114 | } 115 | else { 116 | pos += snprintf(&buf[pos], len - pos, ",\"%s\":", objs[i].name); 117 | } 118 | 119 | float value = 0.0; 120 | uint32_t value_abs; 121 | 122 | // print value 123 | switch (objs[i].raw_data[0]) { 124 | case CAN_TS_T_TRUE: 125 | case CAN_TS_T_FALSE: 126 | pos += snprintf(&buf[pos], len - pos, "%d", 127 | (objs[i].raw_data[0] == CAN_TS_T_TRUE) ? 1 : 0); 128 | break; 129 | case CAN_TS_T_POS_INT32: 130 | value_abs = 131 | (objs[i].raw_data[1] << 24) + 132 | (objs[i].raw_data[2] << 16) + 133 | (objs[i].raw_data[3] << 8) + 134 | (objs[i].raw_data[4]); 135 | pos += snprintf(&buf[pos], len - pos, "%u", value_abs); 136 | break; 137 | case CAN_TS_T_NEG_INT32: 138 | value_abs = 139 | (objs[i].raw_data[1] << 24) + 140 | (objs[i].raw_data[2] << 16) + 141 | (objs[i].raw_data[3] << 8) + 142 | (objs[i].raw_data[4]); 143 | pos += snprintf(&buf[pos], len - pos, "%d", -(int32_t)(value_abs + 1)); 144 | break; 145 | case CAN_TS_T_FLOAT32: 146 | f2b.b[3] = objs[i].raw_data[1]; 147 | f2b.b[2] = objs[i].raw_data[2]; 148 | f2b.b[1] = objs[i].raw_data[3]; 149 | f2b.b[0] = objs[i].raw_data[4]; 150 | pos += snprintf(&buf[pos], len - pos, "%.3f", f2b.f); 151 | break; 152 | case CAN_TS_T_DECFRAC: 153 | value_abs = 154 | (objs[i].raw_data[4] << 24) + 155 | (objs[i].raw_data[5] << 16) + 156 | (objs[i].raw_data[6] << 8) + 157 | (objs[i].raw_data[7]); 158 | 159 | // dirty hack: We know that currently decfrac is only used for mV or mA 160 | if (objs[i].raw_data[3] == 0x1a && 161 | objs[i].raw_data[2] == 0x22) 162 | { 163 | // positive int32 with exp -3 164 | value = (float)value_abs / 1000.0; 165 | } 166 | else if (objs[i].raw_data[3] == 0x3a && 167 | objs[i].raw_data[2] == 0x22) 168 | { 169 | // negative int32 with exp -3 170 | value = -((float)value_abs + 1.0) / 1000.0; 171 | } 172 | else { 173 | pos += snprintf(&buf[pos], len - pos, "err"); 174 | } 175 | pos += snprintf(&buf[pos], len - pos, "%.3f", value); 176 | break; 177 | } 178 | } 179 | 180 | if (pos < len - 1) { 181 | buf[pos++] = '}'; 182 | } 183 | return pos; 184 | } 185 | 186 | char *get_mppt_json_data() 187 | { 188 | generate_json_string(json_buf, sizeof(json_buf), 189 | data_obj_mppt, sizeof(data_obj_mppt)/sizeof(DataObject)); 190 | return json_buf; 191 | } 192 | 193 | char *get_bms_json_data() 194 | { 195 | generate_json_string(json_buf, sizeof(json_buf), 196 | data_obj_bms, sizeof(data_obj_bms)/sizeof(DataObject)); 197 | return json_buf; 198 | } 199 | 200 | void can_setup() 201 | { 202 | 203 | #ifdef GPIO_CAN_STB 204 | // switch CAN transceiver on (STB = low) 205 | gpio_pad_select_gpio(CONFIG_GPIO_CAN_STB); 206 | gpio_set_direction(CONFIG_GPIO_CAN_STB, GPIO_MODE_OUTPUT); 207 | gpio_set_level(CONFIG_GPIO_CAN_STB, 0); 208 | #endif 209 | 210 | if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) { 211 | ESP_LOGE(TAG, "Failed to install CAN driver"); 212 | return; 213 | } 214 | 215 | if (twai_start() != ESP_OK) { 216 | ESP_LOGE(TAG, "Failed to start CAN driver"); 217 | return; 218 | } 219 | 220 | receive_queue = xQueueCreate(RECV_QUEUE_SIZE, sizeof(RecvMsg)); 221 | if (!receive_queue) { 222 | ESP_LOGE(TAG, "Failed to create receiving queue"); 223 | return; 224 | } 225 | } 226 | 227 | void can_receive_task(void *arg) 228 | { 229 | twai_message_t message; 230 | unsigned int device_addr; 231 | unsigned int data_node_id; 232 | 233 | uint8_t payload[1000]; 234 | int ret; 235 | while (1) { 236 | ret = twai_receive(&message, pdMS_TO_TICKS(100)); 237 | if (ret == ESP_OK) { 238 | device_addr = message.identifier & 0x000000FF; 239 | ESP_LOGD(TAG, "Received CAN msg from %.2x", device_addr); 240 | 241 | TSDevice *ts_dev = ts_get_can_device(device_addr); 242 | if (ts_dev == NULL) { 243 | ts_devices_add_can(device_addr); 244 | ESP_LOGI(TAG, "Adding CAN device %x", device_addr); 245 | } 246 | 247 | /* checking for CAN ID used to receive ISO-TP frames */ 248 | if ((message.identifier & 0x1FFFFF00) == (can_addr_client << 8 | 0x1ada << 16)) { 249 | ESP_LOGD(TAG, "ISO TP msg part received"); 250 | isotp_on_can_message(&isotp_link, message.data, message.data_length_code); 251 | 252 | /* process multiple frame transmissions and timeouts */ 253 | isotp_poll(&isotp_link); 254 | 255 | /* extract received data */ 256 | uint16_t out_size = 0; 257 | int ret = isotp_receive(&isotp_link, payload, sizeof(payload) - 1, &out_size); 258 | if (ret == ISOTP_RET_OK) { 259 | RecvMsg msg; 260 | uint8_t *data = malloc(out_size + 1); 261 | memcpy(data, payload, out_size); 262 | msg.data = data; 263 | msg.data[out_size] = '\0'; 264 | msg.len = out_size; 265 | ESP_LOGD(TAG, "Received response: %s", (char *)msg.data); 266 | if (!xQueueSend(receive_queue, &msg, pdMS_TO_TICKS(10))) { 267 | ESP_LOGE(TAG, "Response could not be queued"); 268 | free(data); 269 | } 270 | else if (ret == ISOTP_RET_NO_DATA) { 271 | ESP_LOGE(TAG, "isotp_receive(): No Data Received"); 272 | } 273 | } 274 | } 275 | else { 276 | // ThingSet publication message format: https://libre.solar/thingset/ 277 | data_node_id = (message.identifier & 0x00FFFF00) >> 8; 278 | 279 | if (device_addr == 0) { 280 | for (int i = 0; i < sizeof(data_obj_bms) / sizeof(DataObject); i++) { 281 | if (data_obj_bms[i].id == data_node_id) { 282 | memcpy(data_obj_bms[i].raw_data, message.data, 283 | message.data_length_code); 284 | data_obj_bms[i].len = message.data_length_code; 285 | } 286 | } 287 | update_bms_received = true; 288 | } 289 | else if (device_addr == 10) { 290 | for (int i = 0; i < sizeof(data_obj_mppt) / sizeof(DataObject); i++) { 291 | if (data_obj_mppt[i].id == data_node_id) { 292 | memcpy(data_obj_mppt[i].raw_data, message.data, 293 | message.data_length_code); 294 | data_obj_mppt[i].len = message.data_length_code; 295 | } 296 | } 297 | update_mppt_received = true; 298 | } 299 | ESP_LOGD(TAG, "Received pub-msg on CAN:"); 300 | ESP_LOG_BUFFER_HEX_LEVEL(TAG, message.data, message.data_length_code, ESP_LOG_DEBUG); 301 | } 302 | } 303 | else if (ret == ESP_ERR_TIMEOUT) { 304 | /* transfer consecutive or flow control frames if pending */ 305 | isotp_poll(&isotp_link); 306 | } 307 | else if (ret == ESP_ERR_INVALID_STATE) { 308 | ESP_LOGE(TAG, "Driver in invalid state"); 309 | } 310 | } 311 | } 312 | 313 | char *ts_can_send(uint8_t *req, uint32_t query_size, uint8_t can_address, uint32_t *block_len) 314 | { 315 | RecvMsg msg; 316 | // empty queue before request, don't block if empty and dismiss data if present 317 | if (xQueueReceive(receive_queue, &msg, 50)) { 318 | if (msg.data != NULL) { 319 | free(msg.data); 320 | } 321 | } 322 | 323 | /* Initialize link with the CAN ID we send with */ 324 | isotp_init_link(&isotp_link, can_address << 8 | can_addr_client | 0x1ada << 16, 325 | isotp_send_buf, sizeof(isotp_send_buf), isotp_recv_buf, sizeof(isotp_recv_buf)); 326 | 327 | int ret = isotp_send(&isotp_link, req, query_size); 328 | ESP_LOGI(TAG, "ISOTP Send %s", ret == ESP_OK ? "OK" : "FAILED"); 329 | if (xQueueReceive(receive_queue, &msg, pdMS_TO_TICKS(500))) { 330 | *block_len = msg.len; 331 | return (char *) msg.data; 332 | } 333 | 334 | return NULL; 335 | } 336 | 337 | int ts_can_scan_device_info(TSDevice *device) 338 | { 339 | TSResponse res; 340 | char query[] = "?info"; 341 | 342 | res.block = ts_can_send((uint8_t *)query, strlen(query), device->can_address, &(res.block_len)); 343 | if (res.block == NULL) { 344 | res.block = ""; 345 | } 346 | 347 | if (ts_serial_resp_status(&res) == TS_STATUS_CONTENT) { 348 | // CAN bus is used in TEXT Mode for now so we use the serial methods here 349 | res.data = ts_serial_resp_data(&res); 350 | cJSON *json_data = cJSON_Parse(res.data); 351 | free(res.block); 352 | 353 | if (json_data == NULL) { 354 | ESP_LOGE(TAG, "Error parsing JSON"); 355 | return ESP_FAIL; 356 | } 357 | 358 | int ret = ts_parse_device_info(json_data, device); 359 | free(json_data); 360 | if (ret != ESP_OK) { 361 | ESP_LOGE(TAG, "Error parsing device information"); 362 | return ESP_FAIL; 363 | }; 364 | 365 | device->build_query = ts_build_query_serial; 366 | device->send = ts_can_send; 367 | device->ts_resp_data = ts_serial_resp_data; 368 | device->ts_resp_status = ts_serial_resp_status; 369 | return ESP_OK; 370 | } 371 | else { 372 | ESP_LOGE(TAG, "No valid response"); 373 | return ESP_FAIL; 374 | } 375 | } 376 | 377 | #endif // UNIT_TEST 378 | -------------------------------------------------------------------------------- /main/can.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | #include "ts_client.h" 10 | 11 | typedef struct { 12 | int id; 13 | const char *name; 14 | uint8_t raw_data[8]; 15 | int len; 16 | } DataObject; 17 | 18 | #define CAN_TS_T_TRUE 61 19 | #define CAN_TS_T_FALSE 60 20 | #define CAN_TS_T_POS_INT32 6 21 | #define CAN_TS_T_NEG_INT32 7 22 | #define CAN_TS_T_FLOAT32 30 23 | #define CAN_TS_T_DECFRAC 36 24 | 25 | extern bool update_bms_received; 26 | extern bool update_mppt_received; 27 | 28 | typedef struct { 29 | uint8_t * data; 30 | int len; 31 | } RecvMsg; 32 | 33 | /** 34 | * Sends a query to a given address. If a string is used, the termination bit must be substracted 35 | * from the query length before invoking this method. 36 | * 37 | * @param req A pointer to a buffer with either a string or binary request 38 | * @param query_size The size of the buffer, not including zero termination byte for strings 39 | * @param can_address The target address on the CAN bus 40 | * @param block_len A pointer to a variable where the length of the response block will be stored 41 | * 42 | * \returns A pointer to the response block, to be freed subsequently 43 | */ 44 | char *ts_can_send(uint8_t *req, uint32_t query_size, uint8_t can_address, uint32_t *block_len); 45 | 46 | 47 | int ts_can_scan_device_info(TSDevice *device); 48 | 49 | /** 50 | * Initiate the CAN interface. 51 | */ 52 | void can_setup(); 53 | 54 | /** 55 | * Thread listening to CAN interface, needs to be spawned from main. 56 | */ 57 | void can_receive_task(void *arg); 58 | 59 | /** 60 | * Thread performing regular requests to other devices using ISO-TP 61 | */ 62 | void isotp_task(void *arg); 63 | 64 | /** 65 | * Get data from MPPT connected via CAN bus and convert it to JSON 66 | * 67 | * Caution: This function is currently not thread-safe and could be changed while reading it. 68 | * 69 | * \returns pointer to the buffer 70 | */ 71 | char *get_mppt_json_data(); 72 | 73 | /** 74 | * Get data from BMS connected via CAN bus and convert it to JSON 75 | * 76 | * Caution: This function is currently not thread-safe and could be changed while reading it. 77 | * 78 | * \returns pointer to the buffer 79 | */ 80 | char *get_bms_json_data(); 81 | -------------------------------------------------------------------------------- /main/certs/isrgrootx1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw 3 | TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh 4 | cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 5 | WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu 6 | ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY 7 | MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc 8 | h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 9 | 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U 10 | A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW 11 | T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH 12 | B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC 13 | B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv 14 | KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn 15 | OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn 16 | jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw 17 | qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI 18 | rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV 19 | HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq 20 | hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL 21 | ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 22 | 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK 23 | NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 24 | ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur 25 | TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC 26 | jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc 27 | oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 28 | 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA 29 | mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d 30 | emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /main/data_nodes.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef DATA_NODES_H_ 8 | #define DATA_NODES_H_ 9 | 10 | #ifdef __cplusplus 11 | extern "C" 12 | { 13 | #endif 14 | 15 | #include 16 | #include "esp_err.h" 17 | #include "ts_client.h" 18 | 19 | 20 | #define LIBRE_SOLAR_TYPE_ID 9 21 | 22 | /* 23 | * Partition name 24 | */ 25 | #define PARTITION "config" 26 | #define NAMESPACE "main" 27 | 28 | 29 | /* 30 | * Categories / first layer node IDs 31 | */ 32 | #define ID_ROOT 0x00 33 | #define ID_INFO 0x18 // read-only device information (e.g. manufacturer, device ID) 34 | #define ID_CONF 0x30 // configurable settings 35 | #define ID_CONF_GENERAL 0x31 36 | #define ID_CONF_EMONCMS 0x37 37 | #define ID_CONF_MQTT 0x40 38 | #define ID_INPUT 0x60 // input data (e.g. set-points) 39 | #define ID_OUTPUT 0x70 // output data (e.g. measurement values) 40 | #define ID_REC 0xA0 // recorded data (history-dependent) 41 | #define ID_CAL 0xD0 // calibration 42 | #define ID_EXEC 0xE0 // function call 43 | #define ID_AUTH 0xEA 44 | #define ID_PUB 0xF0 // publication setup 45 | #define ID_SUB 0xF1 // subscription setup 46 | #define ID_LOG 0x100 // access log data 47 | 48 | #define STRING_LEN 128 // size of allocated strings in config 49 | #define DATA_NODE_CONF "conf" 50 | #define DATA_NODE_GENERAL "general" 51 | #define DATA_NODE_EMONCMS "emoncms" 52 | #define DATA_NODE_MQTT "mqtt" 53 | 54 | /* 55 | * Publish/subscribe channels 56 | */ 57 | #define PUB_SER (1U << 0) // UART serial 58 | #define PUB_CAN (1U << 1) // CAN bus 59 | #define PUB_NVM (1U << 2) // data that should be stored in EEPROM 60 | 61 | /** 62 | * Initializes and reads config nodes from nvs 63 | */ 64 | void data_nodes_init(); 65 | 66 | /** 67 | * Process incoming Thingset requests. Has CAN address to match 68 | * send function from TSDevice struct as it is used only internally, 69 | * but could also be used by a UART/CAN task to process ThingSet requests in general 70 | */ 71 | char *process_ts_request(uint8_t *req, uint32_t query_size, uint8_t can_address, uint32_t *block_len); 72 | 73 | void uint64_to_base32(uint64_t in, char *out, size_t size); 74 | 75 | /** 76 | * sets of a timer to reset the device, lets http request return gracefully before restarting 77 | */ 78 | void reset_device(); 79 | 80 | /* 81 | * Wrapper for saving nodes, used as callbacks. Always restarts the device after saving 82 | */ 83 | void save_mqtt(); 84 | 85 | void save_emon(); 86 | 87 | void save_general(); 88 | 89 | /** 90 | * Reads config from Kconfig values 91 | */ 92 | void config_nodes_load_kconfig(); 93 | 94 | /** 95 | * Reads config from NVS values 96 | */ 97 | void config_nodes_load(); 98 | 99 | /** 100 | * Writes config to NVS 101 | */ 102 | void config_nodes_save(const char *node); 103 | 104 | /** 105 | * Wrapper for serial query builder. Eliminates newline termination used for serial communication 106 | */ 107 | char *build_query(uint8_t method, char *node, char *payload); 108 | 109 | /** 110 | * Structs to hold config data 111 | */ 112 | typedef struct { 113 | char wifi_ssid[STRING_LEN]; 114 | char wifi_password[STRING_LEN]; 115 | char mdns_hostname[STRING_LEN]; 116 | bool ts_can_active; 117 | bool ts_serial_active; 118 | } GeneralConfig; 119 | 120 | typedef struct { 121 | bool active; 122 | char emoncms_hostname[STRING_LEN]; 123 | char port[STRING_LEN]; 124 | char url[STRING_LEN]; 125 | char api_key[STRING_LEN]; 126 | char serial_node[STRING_LEN]; 127 | char mppt[STRING_LEN]; 128 | char bms[STRING_LEN]; 129 | } EmoncmsConfig; 130 | 131 | typedef struct { 132 | bool active; 133 | char broker_hostname[STRING_LEN]; 134 | bool use_ssl; 135 | bool use_broker_auth; 136 | char username[STRING_LEN]; 137 | char password[STRING_LEN]; 138 | uint32_t pub_interval; 139 | } MqttConfig; 140 | 141 | #ifdef __cplusplus 142 | } 143 | #endif 144 | 145 | #endif -------------------------------------------------------------------------------- /main/emoncms.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "freertos/FreeRTOS.h" 14 | #include "freertos/task.h" 15 | #include "freertos/event_groups.h" 16 | 17 | #include "esp_system.h" 18 | #include "esp_wifi.h" 19 | #include "esp_event.h" 20 | #include "esp_err.h" 21 | #include "esp_log.h" 22 | #include "driver/gpio.h" 23 | #include "nvs_flash.h" 24 | #include "sdkconfig.h" 25 | 26 | #include "lwip/err.h" 27 | #include "lwip/sockets.h" 28 | #include "lwip/sys.h" 29 | #include "lwip/netdb.h" 30 | #include "lwip/dns.h" 31 | 32 | #include "ts_serial.h" 33 | #include "can.h" 34 | #include "wifi.h" 35 | #include "data_nodes.h" 36 | 37 | static const char* TAG = "emoncms"; 38 | 39 | extern EmoncmsConfig emon_config; 40 | 41 | char http_header[1024]; 42 | 43 | void build_header() 44 | { 45 | snprintf(http_header, sizeof(http_header), 46 | "POST %s HTTP/1.1\r\n" 47 | "Host: %s\r\n" 48 | "Authorization: %s\r\n" 49 | "User-Agent: esp-idf/1.0 esp32\r\n" 50 | "Content-Type: application/x-www-form-urlencoded\r\n" 51 | "Connection: close\r\n", emon_config.url, emon_config.emoncms_hostname, emon_config.api_key); 52 | ESP_LOGD(TAG, "Header (%d bytes): \n%s", strlen(http_header), http_header); 53 | } 54 | 55 | static int send_emoncms(struct addrinfo *res, const char *node_name, const char *json_str) 56 | { 57 | static char buf[500]; 58 | static char http_body[600]; 59 | char recv_buf[64]; 60 | 61 | snprintf(http_body, sizeof(http_body), "node=%s&json=%s", node_name, json_str); 62 | printf("HTTP body for %s: %s\n", node_name, http_body); 63 | 64 | int s = socket(res->ai_family, res->ai_socktype, 0); 65 | if (s < 0) { 66 | ESP_LOGE(TAG, "... Failed to allocate socket."); 67 | return -1; 68 | } 69 | ESP_LOGI(TAG, "... allocated socket\r\n"); 70 | 71 | if (connect(s, res->ai_addr, res->ai_addrlen) != 0) { 72 | ESP_LOGE(TAG, "... socket connect failed errno=%d", errno); 73 | close(s); 74 | return -1; 75 | } 76 | ESP_LOGI(TAG, "... connected"); 77 | 78 | int err = write(s, http_header, strlen(http_header)); 79 | sprintf(buf, "Content-Length: %d\r\n\r\n", (int)strlen(http_body)); 80 | err += write(s, buf, strlen(buf)); 81 | err += write(s, http_body, strlen(http_body)); 82 | err += write(s, "\r\n", 1); 83 | 84 | if (err < 0) { 85 | ESP_LOGE(TAG, "... socket send failed"); 86 | close(s); 87 | return -1; 88 | } 89 | ESP_LOGI(TAG, "... socket send success"); 90 | 91 | // Read HTTP response 92 | int resp; 93 | do { 94 | bzero(recv_buf, sizeof(recv_buf)); 95 | resp = read(s, recv_buf, sizeof(recv_buf)-1); 96 | for(int i = 0; i < resp; i++) { 97 | putchar(recv_buf[i]); 98 | } 99 | } while (resp > 0); 100 | 101 | ESP_LOGI(TAG, "... done reading from socket. Last read return=%d errno=%d\r\n", resp, errno); 102 | close(s); 103 | 104 | return 1; 105 | } 106 | 107 | void emoncms_post_task(void *arg) 108 | { 109 | build_header(); 110 | const struct addrinfo hints = { 111 | .ai_family = AF_INET, 112 | .ai_socktype = SOCK_STREAM, 113 | }; 114 | struct addrinfo *res; 115 | struct in_addr *addr; 116 | 117 | while (1) { 118 | esp_err_t err; 119 | 120 | // attempt to get serial publication message 121 | char *pub_msg = ts_serial_pubmsg(100); 122 | 123 | // wait until we receive an update 124 | while (update_bms_received == false && 125 | update_mppt_received == false && 126 | pub_msg == NULL) 127 | { 128 | // try again as long as a message from 129 | pub_msg = ts_serial_pubmsg(100); 130 | vTaskDelay(100/portTICK_PERIOD_MS); 131 | } 132 | 133 | //esp_netif_ip_info_t ip_info; 134 | //err = esp_netif_get_ip_info(wifi_get_netif, &ip_info); 135 | 136 | err = getaddrinfo(emon_config.emoncms_hostname, emon_config.port, &hints, &res); 137 | 138 | if (err != 0 || res == NULL) { 139 | ESP_LOGE(TAG, "DNS lookup failed err=%d res=%p", err, res); 140 | vTaskDelay(1000 / portTICK_PERIOD_MS); 141 | continue; 142 | } 143 | 144 | // Code to print the resolved IP. 145 | // Note: inet_ntoa is non-reentrant, look at ipaddr_ntoa_r for "real" code 146 | addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr; 147 | ESP_LOGI(TAG, "DNS lookup succeeded. IP=%s", inet_ntoa(*addr)); 148 | 149 | if (update_bms_received) { 150 | gpio_set_level(CONFIG_GPIO_LED, 0); 151 | send_emoncms(res, emon_config.bms, get_bms_json_data()); 152 | update_bms_received = false; 153 | vTaskDelay(100 / portTICK_PERIOD_MS); 154 | } 155 | gpio_set_level(CONFIG_GPIO_LED, 1); 156 | 157 | vTaskDelay(100 / portTICK_PERIOD_MS); 158 | 159 | if (update_mppt_received) { 160 | gpio_set_level(CONFIG_GPIO_LED, 0); 161 | send_emoncms(res, emon_config.mppt, get_mppt_json_data()); 162 | update_mppt_received = false; 163 | vTaskDelay(100 / portTICK_PERIOD_MS); 164 | } 165 | gpio_set_level(CONFIG_GPIO_LED, 1); 166 | 167 | vTaskDelay(100 / portTICK_PERIOD_MS); 168 | 169 | if (pub_msg != NULL && strlen(pub_msg) > 2) { 170 | gpio_set_level(CONFIG_GPIO_LED, 0); 171 | send_emoncms(res, emon_config.serial_node, pub_msg + 2); 172 | ts_serial_pubmsg_clear(); 173 | vTaskDelay(100 / portTICK_PERIOD_MS); 174 | } 175 | gpio_set_level(CONFIG_GPIO_LED, 1); 176 | 177 | // sending interval almost 10s 178 | vTaskDelay(8000 / portTICK_PERIOD_MS); 179 | 180 | freeaddrinfo(res); 181 | } 182 | } 183 | 184 | #endif // UNIT_TEST -------------------------------------------------------------------------------- /main/emoncms.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Sends HTTP post request to specified Emoncms server in 10s interval 9 | */ 10 | void emoncms_post_task(void *arg); 11 | -------------------------------------------------------------------------------- /main/isotp_user.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | 14 | #include "esp_system.h" 15 | #include "esp_err.h" 16 | #include "esp_log.h" 17 | 18 | #include "driver/can.h" 19 | #include "driver/gpio.h" 20 | 21 | #include "../lib/isotp/isotp.h" 22 | 23 | /* 24 | * required, this must send a single CAN message with the given arbitration 25 | * ID (i.e. the CAN message ID) and data. The size will never be more than 8 26 | * bytes. 27 | */ 28 | int isotp_user_send_can(const uint32_t arbitration_id, 29 | const uint8_t* data, const uint8_t size) 30 | { 31 | can_message_t msg; 32 | memcpy(msg.data, data, size); 33 | msg.data_length_code = size; 34 | msg.identifier = arbitration_id; 35 | msg.flags = CAN_MSG_FLAG_EXTD; 36 | return can_transmit(&msg, 0); 37 | } 38 | 39 | /* 40 | * required, return system tick, unit is millisecond 41 | */ 42 | uint32_t isotp_user_get_ms(void) 43 | { 44 | return esp_timer_get_time() / 1000; 45 | } 46 | 47 | /* 48 | * optional, provide to receive debugging log messages 49 | */ 50 | void isotp_user_debug(const char* message, ...) 51 | { 52 | va_list argp; 53 | va_start(argp, message); 54 | printf(message, argp); 55 | va_end(argp); 56 | } 57 | #endif //UNIT_TEST 58 | -------------------------------------------------------------------------------- /main/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | #ifndef UNIT_TEST 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "freertos/FreeRTOS.h" 13 | #include "freertos/task.h" 14 | #include "freertos/event_groups.h" 15 | 16 | #include "esp_system.h" 17 | #include "esp_wifi.h" 18 | #include "esp_event.h" 19 | #include "esp_err.h" 20 | #include "esp_log.h" 21 | #include "esp_netif.h" 22 | #include "driver/gpio.h" 23 | #include "nvs_flash.h" 24 | #include "sdkconfig.h" 25 | 26 | #include "lwip/err.h" 27 | #include "lwip/sockets.h" 28 | #include "lwip/sys.h" 29 | #include "lwip/netdb.h" 30 | #include "lwip/dns.h" 31 | 32 | #include "ts_serial.h" 33 | #include "ts_client.h" 34 | #include "ts_mqtt.h" 35 | #include "can.h" 36 | #include "emoncms.h" 37 | #include "wifi.h" 38 | #include "web_fs.h" 39 | #include "web_server.h" 40 | #include "provisioning.h" 41 | #include "data_nodes.h" 42 | #include "ota.h" 43 | 44 | #define RX_TASK_PRIO 9 // receiving task priority 45 | extern EmoncmsConfig emon_config; 46 | extern MqttConfig mqtt_config; 47 | extern GeneralConfig general_config; 48 | 49 | void app_main(void) 50 | { 51 | esp_ota_check_image(); 52 | 53 | ESP_ERROR_CHECK(nvs_flash_init()); 54 | ESP_ERROR_CHECK(esp_netif_init()); 55 | ESP_ERROR_CHECK(esp_event_loop_create_default()); 56 | 57 | data_nodes_init(); 58 | 59 | // configure the LED pad as GPIO and set direction 60 | gpio_pad_select_gpio(CONFIG_GPIO_LED); 61 | gpio_set_direction(CONFIG_GPIO_LED, GPIO_MODE_OUTPUT); 62 | gpio_set_level(CONFIG_GPIO_LED, 1); 63 | // wait 3s to open serial terminal after flashing finished 64 | vTaskDelay(3000 / portTICK_PERIOD_MS); 65 | printf("Booting Libre Solar ESP32 Edge...\n"); 66 | 67 | init_fs(); 68 | 69 | ts_devices_init(); 70 | 71 | if (general_config.ts_can_active) { 72 | can_setup(); 73 | xTaskCreatePinnedToCore(can_receive_task, "CAN_rx", 4096, 74 | NULL, RX_TASK_PRIO, NULL, 1); 75 | } 76 | 77 | if (general_config.ts_serial_active) { 78 | ts_serial_setup(); 79 | xTaskCreatePinnedToCore(ts_serial_rx_task, "ts_serial_rx", 4096, 80 | NULL, RX_TASK_PRIO, NULL, 1); 81 | ts_devices_scan_serial(); 82 | } 83 | 84 | if (strlen(general_config.wifi_ssid) > 0) { 85 | wifi_connect(); 86 | } 87 | else { 88 | // no hard-coded WiFi credentials --> start provisioning via BLE 89 | provision(); 90 | } 91 | 92 | start_web_server("/www"); 93 | 94 | if (emon_config.active) { 95 | xTaskCreate(&emoncms_post_task, "emoncms_post_task", 4096, NULL, 5, NULL); 96 | } 97 | 98 | if (mqtt_config.active) { 99 | xTaskCreate(&ts_mqtt_pub_task, "mqtt_pub", 4096, NULL, 5, NULL); 100 | } 101 | } 102 | 103 | #endif //unit tests -------------------------------------------------------------------------------- /main/ota.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | #ifndef UNIT_TEST 7 | 8 | #include "esp_http_server.h" 9 | #include "esp_log.h" 10 | #include "esp_err.h" 11 | #include "esp_ota_ops.h" 12 | #include "esp_partition.h" 13 | #include "cJSON.h" 14 | #include "errno.h" 15 | #include "driver/gpio.h" 16 | 17 | static const char *TAG = "esp_ota"; 18 | 19 | #define DIAGNOSTIC_PIN 4 20 | #define BUFFSIZE 1024 21 | static char ota_write_data[BUFFSIZE + 1] = { 0 }; 22 | 23 | static bool diagnostic(void) 24 | { 25 | gpio_config_t io_conf; 26 | io_conf.intr_type = GPIO_INTR_DISABLE; 27 | io_conf.mode = GPIO_MODE_INPUT; 28 | io_conf.pin_bit_mask = (1ULL << DIAGNOSTIC_PIN); 29 | io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; 30 | io_conf.pull_up_en = GPIO_PULLUP_ENABLE; 31 | gpio_config(&io_conf); 32 | 33 | ESP_LOGI(TAG, "Diagnostics (5 sec)..."); 34 | vTaskDelay(5000 / portTICK_PERIOD_MS); 35 | 36 | bool diagnostic_is_ok = gpio_get_level(DIAGNOSTIC_PIN); 37 | 38 | gpio_reset_pin(DIAGNOSTIC_PIN); 39 | return diagnostic_is_ok; 40 | } 41 | 42 | void esp_ota_check_image() 43 | { 44 | const esp_partition_t *running = esp_ota_get_running_partition(); 45 | esp_ota_img_states_t ota_state; 46 | if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) { 47 | if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) { 48 | // run diagnostic function ... 49 | bool diagnostic_is_ok = diagnostic(); 50 | if (diagnostic_is_ok) { 51 | ESP_LOGI(TAG, "Diagnostics completed successfully! Continuing execution ..."); 52 | esp_ota_mark_app_valid_cancel_rollback(); 53 | } 54 | else { 55 | ESP_LOGE(TAG, "Diagnostics failed! Start rollback to the previous version ..."); 56 | esp_ota_mark_app_invalid_rollback_and_reboot(); 57 | } 58 | } 59 | } 60 | } 61 | 62 | esp_err_t esp_ota_handler(httpd_req_t *req, cJSON* res) 63 | { 64 | esp_err_t err; 65 | /* update handle : set by esp_ota_begin(), must be freed via esp_ota_end() */ 66 | esp_ota_handle_t update_handle = 0 ; 67 | const esp_partition_t *update_partition = NULL; 68 | 69 | const esp_partition_t *configured = esp_ota_get_boot_partition(); 70 | const esp_partition_t *running = esp_ota_get_running_partition(); 71 | 72 | if (configured != running) { 73 | ESP_LOGW(TAG, "Configured boot partition at offset 0x%08x, is running from offset 0x%08x", 74 | configured->address, running->address); 75 | ESP_LOGW(TAG, "(This happens if either the ota data or boot image become corrupted.)"); 76 | } 77 | ESP_LOGI(TAG, "Running partition type %d subtype %d (offset 0x%08x)", 78 | running->type, running->subtype, running->address); 79 | 80 | update_partition = esp_ota_get_next_update_partition(NULL); 81 | assert(update_partition != NULL); 82 | ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%x", 83 | update_partition->subtype, update_partition->address); 84 | 85 | int binary_file_length = req->content_len; 86 | int data_read = 0; 87 | int total_received = 0; 88 | 89 | int len_img_head = sizeof(esp_image_header_t); 90 | int len_img_seg_head = sizeof(esp_image_segment_header_t); 91 | /* deal with all receive packets */ 92 | bool image_header_was_checked = false; 93 | while (binary_file_length - total_received > 0) { 94 | data_read = httpd_req_recv(req, ota_write_data, BUFFSIZE); 95 | total_received += data_read; 96 | if (data_read < 0) { 97 | ESP_LOGE(TAG, "Could not receive image"); 98 | cJSON_AddStringToObject(res, "error", "Could not receive image"); 99 | return ESP_FAIL; 100 | } 101 | else if (data_read > 0) { 102 | if (image_header_was_checked == false) { 103 | esp_app_desc_t new_app_info; 104 | int len = len_img_head 105 | + len_img_seg_head 106 | + sizeof(esp_app_desc_t); 107 | if (data_read > len) { 108 | // check current version with downloading 109 | memcpy(&new_app_info, 110 | &ota_write_data[len_img_head + len_img_seg_head], 111 | sizeof(esp_app_desc_t)); 112 | ESP_LOGI(TAG, "New fw version: %s", new_app_info.version); 113 | 114 | esp_app_desc_t running_app_info; 115 | int resp = esp_ota_get_partition_description(running, &running_app_info); 116 | if (resp == ESP_OK) { 117 | ESP_LOGI(TAG, "Running fw version: %s", running_app_info.version); 118 | } 119 | 120 | const esp_partition_t* last_invalid_app = esp_ota_get_last_invalid_partition(); 121 | esp_app_desc_t invalid_app_info; 122 | resp = esp_ota_get_partition_description(last_invalid_app, &invalid_app_info); 123 | if (resp == ESP_OK) { 124 | ESP_LOGI(TAG, "Last invalid fw version: %s", invalid_app_info.version); 125 | } 126 | 127 | // check current version with last invalid partition 128 | if (last_invalid_app != NULL) { 129 | int bytes_coppied = memcmp(invalid_app_info.version, 130 | new_app_info.version, 131 | sizeof(new_app_info.version)); 132 | if (bytes_coppied == 0) { 133 | ESP_LOGW(TAG, "New version is the same as invalid version."); 134 | ESP_LOGW(TAG, "Tried to launch the fw with %s version, but it failed.", 135 | invalid_app_info.version); 136 | ESP_LOGW(TAG, "The fw has been rolled back to the last version."); 137 | cJSON_AddStringToObject(res, 138 | "error", 139 | "New version is invalid, fw has been rolled back to last version"); 140 | return ESP_FAIL; 141 | } 142 | } 143 | image_header_was_checked = true; 144 | err = esp_ota_begin(update_partition, binary_file_length, &update_handle); 145 | if (err != ESP_OK) { 146 | ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err)); 147 | esp_ota_abort(update_handle); 148 | cJSON_AddStringToObject(res, "error", "OTA begin failed"); 149 | return ESP_FAIL; 150 | } 151 | ESP_LOGI(TAG, "esp_ota_begin succeeded"); 152 | } 153 | else { 154 | ESP_LOGE(TAG, "received package does not fit len"); 155 | esp_ota_abort(update_handle); 156 | cJSON_AddStringToObject(res, "error", "received package does not fit len"); 157 | return ESP_FAIL; 158 | } 159 | } 160 | err = esp_ota_write(update_handle, (const void *)ota_write_data, data_read); 161 | if (err != ESP_OK) { 162 | esp_ota_abort(update_handle); 163 | cJSON_AddStringToObject(res, "error", "Unable to write chunk to flash"); 164 | return ESP_FAIL; 165 | } 166 | ESP_LOGD(TAG, "Written image length %d", total_received); 167 | } 168 | else if (data_read == 0) { 169 | if (errno == ECONNRESET || errno == ENOTCONN) { 170 | ESP_LOGE(TAG, "Connection closed, errno = %d", errno); 171 | break; 172 | } 173 | if (total_received == binary_file_length) { 174 | ESP_LOGI(TAG, "Connection closed"); 175 | break; 176 | } 177 | } 178 | } 179 | ESP_LOGI(TAG, "Total image size received: %d bytes", total_received); 180 | if (total_received != binary_file_length) { 181 | ESP_LOGI(TAG, "Error in receiving complete file"); 182 | esp_ota_abort(update_handle); 183 | cJSON_AddStringToObject(res, "error", "Unable to write chunk to flash"); 184 | return ESP_FAIL; 185 | } 186 | err = esp_ota_end(update_handle); 187 | if (err != ESP_OK) { 188 | if (err == ESP_ERR_OTA_VALIDATE_FAILED) { 189 | ESP_LOGE(TAG, "Image validation failed, image is corrupted"); 190 | cJSON_AddStringToObject(res, "error", "Image validation failed, image is corrupted"); 191 | } 192 | else { 193 | ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err)); 194 | cJSON_AddStringToObject(res, "error", "esp_ota_end failed"); 195 | } 196 | return ESP_FAIL; 197 | } 198 | err = esp_ota_set_boot_partition(update_partition); 199 | if (err != ESP_OK) { 200 | ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err)); 201 | cJSON_AddStringToObject(res, "error", "esp_ota_set_boot_partition failed"); 202 | } 203 | 204 | return ESP_OK; 205 | } 206 | 207 | void esp_ota_reset_device() 208 | { 209 | vTaskDelay(pdMS_TO_TICKS(1000)); 210 | ESP_LOGI("reset_task", "Prepare to restart system!"); 211 | esp_restart(); 212 | } 213 | 214 | #endif 215 | -------------------------------------------------------------------------------- /main/ota.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef OTA_H_ 8 | #define OTA_H_ 9 | 10 | #include "esp_err.h" 11 | #include "esp_http_server.h" 12 | #include "cJSON.h" 13 | 14 | esp_err_t esp_ota_handler(httpd_req_t *req, cJSON* res); 15 | void esp_ota_reset_device(); 16 | void esp_ota_check_image(); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /main/provisioning.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include 23 | #include "provisioning.h" 24 | #include "data_nodes.h" 25 | 26 | extern GeneralConfig general_config; 27 | 28 | static const char *TAG = "prov"; 29 | 30 | const int WIFI_CONNECTED_EVENT = BIT0; 31 | static EventGroupHandle_t wifi_event_group; 32 | 33 | char temp_ssid[32]; 34 | char temp_password[64]; 35 | 36 | static void event_handler(void* arg, esp_event_base_t event_base, 37 | int event_id, void* event_data) 38 | { 39 | if(event_base == WIFI_PROV_EVENT) { 40 | switch(event_id) { 41 | case WIFI_PROV_START: { 42 | ESP_LOGI(TAG, "Started provisioning routine"); 43 | break; 44 | } 45 | case WIFI_PROV_CRED_RECV: { 46 | wifi_sta_config_t *wifi_sta_cfg = (wifi_sta_config_t *)event_data; 47 | ESP_LOGI(TAG, "Received Wi-Fi credentials" 48 | "\n\tSSID : %s\n\tPassword : %s", 49 | (const char *) wifi_sta_cfg->ssid, 50 | (const char *) wifi_sta_cfg->password); 51 | memcpy(temp_ssid, wifi_sta_cfg->ssid, sizeof(temp_ssid)); 52 | memcpy(temp_password, wifi_sta_cfg->password, sizeof(temp_password)); 53 | break; 54 | } 55 | case WIFI_PROV_CRED_FAIL: { 56 | wifi_prov_sta_fail_reason_t *reason = (wifi_prov_sta_fail_reason_t *)event_data; 57 | ESP_LOGE(TAG, "Provisioning failed!\n\tReason : %s" 58 | "\n\tPlease reset to factory and retry provisioning", 59 | (*reason == WIFI_PROV_STA_AUTH_ERROR) ? 60 | "Wi-Fi station authentication failed" : "Wi-Fi access-point not found"); 61 | break; 62 | } 63 | case WIFI_PROV_CRED_SUCCESS: 64 | ESP_LOGI(TAG, "Provisioning successful"); 65 | memcpy(general_config.wifi_ssid, temp_ssid, sizeof(temp_ssid)); 66 | memcpy(general_config.wifi_password, temp_password, sizeof(temp_password)); 67 | break; 68 | case WIFI_PROV_END: 69 | /* De-initialize manager once provisioning is finished */ 70 | wifi_prov_mgr_deinit(); 71 | break; 72 | default: 73 | break; 74 | } 75 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { 76 | esp_wifi_connect(); 77 | } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { 78 | ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; 79 | ESP_LOGI(TAG, "Connected with IP Address:" IPSTR, IP2STR(&event->ip_info.ip)); 80 | /* Signal main application to continue execution */ 81 | xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_EVENT); 82 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { 83 | ESP_LOGI(TAG, "Disconnected. Connecting to the AP again..."); 84 | esp_wifi_connect(); 85 | } 86 | } 87 | 88 | static void initialise_mdns(void) 89 | { 90 | char* hostname = general_config.mdns_hostname; 91 | //initialize mDNS 92 | ESP_ERROR_CHECK( mdns_init() ); 93 | //set mDNS hostname (required if you want to advertise services) 94 | ESP_ERROR_CHECK( mdns_hostname_set(hostname) ); 95 | ESP_LOGI(TAG, "mdns hostname set to: [%s]", hostname); 96 | //set default mDNS instance name 97 | ESP_ERROR_CHECK( mdns_instance_name_set(general_config.mdns_hostname) ); 98 | 99 | //structure with TXT records 100 | mdns_txt_item_t serviceTxtData[1] = { 101 | {"board","esp32"} 102 | }; 103 | 104 | //initialize service 105 | ESP_ERROR_CHECK( mdns_service_add("ESP32-WebServer", "_http", "_tcp", 80, serviceTxtData, 1) ); 106 | } 107 | 108 | static void get_device_service_name(char *service_name, size_t max) 109 | { 110 | const char *ssid_prefix = "PROV_"; 111 | snprintf(service_name, max, "%s%s", 112 | ssid_prefix, "LS-DM"); 113 | } 114 | 115 | void provision(void) 116 | { 117 | wifi_event_group = xEventGroupCreate(); 118 | 119 | ESP_ERROR_CHECK(esp_event_handler_register(WIFI_PROV_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); 120 | ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); 121 | ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL)); 122 | 123 | esp_netif_create_default_wifi_sta(); 124 | 125 | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 126 | ESP_ERROR_CHECK(esp_wifi_init(&cfg)); 127 | 128 | /* Configuration for the provisioning manager, we don't need BT or BLE after provisioning */ 129 | wifi_prov_mgr_config_t config = { 130 | .scheme = wifi_prov_scheme_ble, 131 | .scheme_event_handler = WIFI_PROV_SCHEME_BLE_EVENT_HANDLER_FREE_BTDM 132 | }; 133 | ESP_ERROR_CHECK(wifi_prov_mgr_init(config)); 134 | 135 | bool provisioned = false; 136 | /* Let's find out if the device is provisioned */ 137 | ESP_ERROR_CHECK(wifi_prov_mgr_is_provisioned(&provisioned)); 138 | 139 | if (!provisioned) { 140 | ESP_LOGI(TAG, "Starting provisioning"); 141 | 142 | /* What is the Device Service Name that we want 143 | * This translates to device name 144 | */ 145 | char service_name[11]; 146 | get_device_service_name(service_name, sizeof(service_name)); 147 | wifi_prov_security_t security = WIFI_PROV_SECURITY_1; 148 | const char *pop = "LibreSolar"; 149 | const char *service_key = NULL; 150 | 151 | uint8_t custom_service_uuid[] = { 152 | /* LSB <--------------------------------------- 153 | * ---------------------------------------> MSB */ 154 | 0xb4, 0xdf, 0x5a, 0x1c, 0x3f, 0x6b, 0xf4, 0xbf, 155 | 0xea, 0x4a, 0x82, 0x03, 0x04, 0x90, 0x1a, 0x02, 156 | }; 157 | wifi_prov_scheme_ble_set_service_uuid(custom_service_uuid); 158 | 159 | ESP_ERROR_CHECK(wifi_prov_mgr_start_provisioning(security, pop, service_name, service_key)); 160 | } else { 161 | ESP_LOGI(TAG, "Already provisioned, starting Wi-Fi STA"); 162 | 163 | /* We don't need the manager as device is already provisioned, 164 | * so let's release it's resources */ 165 | wifi_prov_mgr_deinit(); 166 | 167 | ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); 168 | ESP_ERROR_CHECK(esp_wifi_start()); 169 | } 170 | // start mdns 171 | initialise_mdns(); 172 | /* Wait for Wi-Fi connection */ 173 | xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_EVENT, false, true, portMAX_DELAY); 174 | return; 175 | } 176 | 177 | #endif // UNIT_TEST 178 | -------------------------------------------------------------------------------- /main/provisioning.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef PROVISION_H_ 8 | #define PROVISION_H_ 9 | 10 | void provision(void); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /main/stm32bl.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include "stm32bl.h" 10 | 11 | #include "freertos/FreeRTOS.h" 12 | #include "freertos/task.h" 13 | #include "freertos/event_groups.h" 14 | #include "string.h" 15 | 16 | #include "esp_system.h" 17 | #include "esp_err.h" 18 | #include "esp_log.h" 19 | 20 | #include "driver/uart.h" 21 | 22 | /* used UART interface */ 23 | static int uart = UART_NUM_2; 24 | 25 | const char *TAG = "stm32bl"; 26 | 27 | #define UART_TIMEOUT_MS 100 28 | 29 | static const uint8_t stm_reset_code[] = { 30 | 0x01, 0x49, // ldr r1, [pc, #4] ; () 31 | 0x02, 0x4A, // ldr r2, [pc, #8] ; () 32 | 0x0A, 0x60, // str r2, [r1, #0] 33 | 0xfe, 0xe7, // endless: b endless 34 | 0x0c, 0xed, 0x00, 0xe0, // .word 0xe000ed0c = NVIC AIRCR register address 35 | 0x04, 0x00, 0xfa, 0x05 // .word 0x05fa0004 = VECTKEY | SYSRESETREQ 36 | }; 37 | 38 | static const stm32_device_t devices[] = { 39 | // STM32G431XX 40 | {0x468, 0x20004000, 0x08000000, 0x1FFF7800}, 41 | // STM32L07XX 42 | {0x447, 0x20002000, 0x08000000, 0x1FF80000} 43 | }; 44 | 45 | static const stm32_device_t *get_device_by_id(uint16_t id) 46 | { 47 | for (int i = 0; i < sizeof(devices); i++) { 48 | if (devices[i].id == id) { 49 | return &(devices[i]); 50 | } 51 | } 52 | return NULL; 53 | } 54 | 55 | static uint8_t calc_checksum(uint8_t *data, uint8_t len) 56 | { 57 | uint8_t ret = data[0]; 58 | for (int i = 1; i < len; i++) { 59 | ret ^= data[i]; 60 | } 61 | return ret; 62 | } 63 | 64 | static void send_buf(uint8_t *buf, size_t len) 65 | { 66 | uint8_t checksum = calc_checksum(buf, len); 67 | uart_write_bytes(uart, (char *)buf, len); 68 | uart_write_bytes(uart, (char *)&checksum, 1); 69 | } 70 | 71 | static inline void send_cmd(uint8_t cmd) 72 | { 73 | uint8_t buf[] = { cmd, ~cmd }; 74 | uart_write_bytes(uart, (char *)buf, 2); 75 | } 76 | 77 | static void send_address(uint32_t addr) 78 | { 79 | if (addr % 4 != 0) { 80 | ESP_LOGE(TAG, "Error: address must be 4 byte aligned"); 81 | return; 82 | } 83 | uint8_t buf[] = { addr >> 24, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF }; 84 | send_buf(buf, sizeof(buf)); 85 | } 86 | 87 | static int wait_resp() 88 | { 89 | uint8_t byte; 90 | int ret = uart_read_bytes(uart, &byte, 1, pdMS_TO_TICKS(1000)); 91 | if (ret > 0) { 92 | return byte; 93 | } 94 | return ESP_FAIL; 95 | } 96 | 97 | int stm32bl_erase_all(uint16_t max_pages) 98 | { 99 | uint8_t buf[4] = {}; 100 | buf[0] = 0; 101 | buf[1] = 0; 102 | for (int i = 0; i < max_pages; i++) { 103 | send_cmd(STM32BL_EEM); 104 | if (wait_resp() != STM32BL_ACK) { 105 | ESP_LOGE(TAG, "Erasing request failed"); 106 | return ESP_FAIL; 107 | } 108 | buf[2] = (i >> 8) & 0xFF; 109 | buf[3] = i & 0xFF; 110 | send_buf(buf, 4); 111 | if (wait_resp() != STM32BL_ACK) { 112 | ESP_LOGE(TAG, "Could not erase page 0x%.2x", i); 113 | return ESP_FAIL; 114 | } 115 | } 116 | 117 | ESP_LOGD(TAG, "Erasing successfully finished"); 118 | return ESP_OK; 119 | } 120 | 121 | int stm32bl_go(uint32_t addr) 122 | { 123 | send_cmd(STM32BL_GO); 124 | if (wait_resp() != STM32BL_ACK) { 125 | ESP_LOGE(TAG,"Go command rejected"); 126 | return ESP_FAIL; 127 | } 128 | 129 | send_address(addr); 130 | return wait_resp(); 131 | } 132 | 133 | static int stm32bl_run_raw_code(uint32_t target_address, const uint8_t *code, uint32_t code_size) 134 | { 135 | uint32_t stack_addr = STM32_RAM_START_ADDR; 136 | uint32_t code_address = target_address + 8 + 1; 137 | uint32_t length = code_size + 8; 138 | uint8_t *mem; 139 | 140 | if (target_address % 4 != 0) { 141 | ESP_LOGE(TAG, "Code address must be 4 byte aligned"); 142 | return ESP_FAIL; 143 | } 144 | 145 | mem = malloc(length); 146 | if (!mem) { 147 | return ESP_FAIL; 148 | } 149 | 150 | memcpy(mem, &stack_addr, sizeof(uint32_t)); 151 | memcpy(mem + 4, &code_address, sizeof(uint32_t)); 152 | memcpy(mem + 8, code, code_size); 153 | 154 | if (stm32bl_write(mem, length, target_address) != STM32BL_ACK) { 155 | ESP_LOGE(TAG, "Could not write raw code!"); 156 | } 157 | 158 | free(mem); 159 | return stm32bl_go(target_address); 160 | } 161 | 162 | int stm32bl_reset_device(uint16_t id) 163 | { 164 | const stm32_device_t *device = get_device_by_id(id); 165 | if (device == NULL) { 166 | ESP_LOGE(TAG, "Cannot reset unkown device"); 167 | return ESP_FAIL; 168 | } 169 | ESP_LOGD(TAG, "Found device: 0x%x", device->id); 170 | 171 | int ret; 172 | switch (device->id) { 173 | case STM32L07XX_ID: 174 | ret = stm32bl_run_raw_code(device->sram_start, stm_reset_code, sizeof(stm_reset_code)); 175 | break; 176 | case STM32G431XX_ID: 177 | ret = stm32bl_reset_optr(device->opt_start); 178 | break; 179 | default: 180 | ESP_LOGE(TAG, "Can't reset unknown device!"); 181 | ret = ESP_FAIL; 182 | } 183 | 184 | // wait for reboot 185 | vTaskDelay(pdMS_TO_TICKS(500)); 186 | if (ret == ESP_FAIL) { 187 | ESP_LOGE(TAG, "Unable reset device!"); 188 | return ESP_FAIL; 189 | }; 190 | 191 | return ESP_OK; 192 | } 193 | 194 | // Use with caution, showed incoherent behavior with different MCUs 195 | int stm32bl_unprotect_write() 196 | { 197 | send_cmd(STM32BL_WUP); 198 | if (wait_resp() != STM32BL_ACK) { 199 | ESP_LOGE(TAG,"Unprotect write rejected"); 200 | return ESP_FAIL; 201 | } 202 | wait_resp(); 203 | 204 | // Unprotect write generates a reset, so we have to reinit the UART connection 205 | vTaskDelay(pdMS_TO_TICKS(500)); 206 | 207 | if (stm32bl_init() != STM32BL_ACK) { 208 | ESP_LOGE(TAG, "Reinit failed"); 209 | return ESP_FAIL; 210 | } 211 | return ESP_OK; 212 | } 213 | 214 | // Use with caution, showed incoherent behavior with different MCUs 215 | int stm32bl_unprotect_read() 216 | { 217 | send_cmd(STM32BL_RUP); 218 | if (wait_resp() != STM32BL_ACK) { 219 | ESP_LOGE(TAG, "Unprotect read rejected"); 220 | return ESP_FAIL; 221 | } 222 | wait_resp(); 223 | 224 | // Unprotect read generates a reset, so we have to reinit the UART connection 225 | vTaskDelay(pdMS_TO_TICKS(500)); 226 | 227 | if (stm32bl_init() != STM32BL_ACK) { 228 | ESP_LOGE(TAG, "Reinit failed"); 229 | return ESP_FAIL; 230 | } 231 | return ESP_OK; 232 | } 233 | 234 | int stm32bl_protect_read() 235 | { 236 | send_cmd(STM32BL_RP); 237 | if (wait_resp() != STM32BL_ACK) { 238 | return ESP_FAIL; 239 | } 240 | return wait_resp(); 241 | } 242 | 243 | int stm32bl_init() 244 | { 245 | uint8_t init_byte = STM32BL_INIT; 246 | uart_write_bytes(uart, (char *)&init_byte, 1); 247 | return wait_resp(); 248 | } 249 | 250 | int stm32bl_read(uint8_t *buf, uint8_t num_bytes, uint32_t addr) 251 | { 252 | send_cmd(STM32BL_RM); 253 | if (wait_resp() != STM32BL_ACK) { 254 | ESP_LOGE(TAG, "Read command rejected"); 255 | return ESP_FAIL; 256 | } 257 | 258 | send_address(addr); 259 | if (wait_resp() != STM32BL_ACK) { 260 | ESP_LOGE(TAG, "Read address rejected"); 261 | return ESP_FAIL; 262 | } 263 | 264 | // One byte with 0 < N < 256 and its complement is the same as 265 | // sending a command 266 | send_cmd(num_bytes); 267 | if (wait_resp() != STM32BL_ACK) { 268 | ESP_LOGE(TAG, "Number of bytes to read rejected"); 269 | return ESP_FAIL; 270 | } 271 | 272 | int ret = uart_read_bytes(uart, buf, num_bytes, pdMS_TO_TICKS(UART_TIMEOUT_MS)); 273 | if (ret > 0) { 274 | return STM32BL_ACK; 275 | } 276 | 277 | return ESP_FAIL; 278 | } 279 | 280 | int stm32bl_write(uint8_t *buf, uint32_t num_bytes, uint32_t start_addr) 281 | { 282 | if (num_bytes % 4 != 0) { 283 | ESP_LOGE(TAG, "Data is not aligned"); 284 | return ESP_FAIL; 285 | } 286 | 287 | send_cmd(STM32BL_WM); 288 | if (wait_resp() == STM32BL_NACK) { 289 | ESP_LOGE(TAG, "Write request failed"); 290 | return ESP_FAIL; 291 | } 292 | 293 | send_address(start_addr); 294 | if (wait_resp() == STM32BL_NACK) { 295 | ESP_LOGE(TAG, "Start address rejected: 0x%.8x", start_addr); 296 | return ESP_FAIL; 297 | } 298 | 299 | uint8_t *total = (uint8_t *)malloc(num_bytes + 1); 300 | total[0] = num_bytes - 1; 301 | xthal_memcpy(total + 1, buf, num_bytes); 302 | 303 | ESP_LOGD(TAG, "Sending out %d bytes", num_bytes); 304 | send_buf(total, num_bytes + 1); 305 | free(total); 306 | return wait_resp(); 307 | } 308 | 309 | int stm32bl_get_version() 310 | { 311 | uint8_t buf[14]; 312 | 313 | send_cmd(STM32BL_GET); 314 | if (wait_resp() != STM32BL_ACK) { 315 | ESP_LOGE(TAG, "Get command rejected"); 316 | return ESP_FAIL; 317 | } 318 | 319 | int ret = uart_read_bytes(uart, buf, 14, pdMS_TO_TICKS(UART_TIMEOUT_MS)); 320 | if (ret > 0) { 321 | return buf[1]; 322 | } 323 | 324 | return ESP_FAIL; 325 | } 326 | 327 | int stm32bl_get_id() 328 | { 329 | uint8_t buf[4]; 330 | 331 | send_cmd(STM32BL_GID); 332 | if (wait_resp() != STM32BL_ACK) { 333 | return ESP_FAIL; 334 | } 335 | 336 | int ret = uart_read_bytes(uart, buf, 4, pdMS_TO_TICKS(UART_TIMEOUT_MS)); 337 | if (ret > 0) { 338 | return (buf[1] << 8) + buf[2]; 339 | } 340 | 341 | return ESP_FAIL; 342 | } 343 | 344 | int stm32bl_reset_optr(uint32_t ob_addr) 345 | { 346 | uint32_t optr[12] = {}; 347 | ESP_LOGD(TAG, "Going to read from 0x%x", ob_addr); 348 | int ret = stm32bl_read((uint8_t *) &optr, sizeof(optr), ob_addr); 349 | if (ret != STM32BL_ACK) { 350 | ESP_LOGE(TAG, "Unable to read option bytes"); 351 | return ESP_FAIL; 352 | } 353 | 354 | ESP_LOGD(TAG, "nSWBOOT: %d", (optr[0] & STM32_nSWBOOT0) >> 26); 355 | ESP_LOGD(TAG, "nBOOT0: %d", (optr[0] & STM32_nBOOT0) >> 27); 356 | ESP_LOGD(TAG, "nBOOT1: %d", (optr[0] & STM32_nBOOT1) >> 23); 357 | 358 | // set software boot = 0, which disables the selection via pin 359 | // should already be set anyway, just making sure here 360 | optr[0] &= ~STM32_nSWBOOT0; 361 | // set BOOT0 = 1 to boot to main flash memory 362 | optr[0] |= STM32_nBOOT0; 363 | // set BOOT1 = 1. If the above values are corrupted, this will make sure 364 | // that the chip does not boot from SRAM but System Memory and could possibly 365 | // be recovered 366 | optr[0] |= STM32_nBOOT1; 367 | // the "right" half of the double word is the negated left half 368 | optr[1] = ~optr[0]; 369 | 370 | if (stm32bl_write((uint8_t *) &optr, sizeof(optr), ob_addr) != STM32BL_ACK) { 371 | // this is bad, we can't reset the option bytes and are stuck in the bootloader 372 | ESP_LOGE(TAG, "Could not write option bytes"); 373 | return ESP_FAIL; 374 | } 375 | 376 | // A system reset should be triggered now 377 | return ESP_OK; 378 | } 379 | 380 | #endif //UNIT_TEST 381 | -------------------------------------------------------------------------------- /main/stm32bl.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef STM32BL_H_ 8 | #define STM32BL_H_ 9 | 10 | #include 11 | 12 | #define STM32BL_INIT 0x7F /* Bootloader init command */ 13 | #define STM32BL_ACK 0x79 /* ACK response */ 14 | #define STM32BL_NACK 0x1F /* NACK response */ 15 | 16 | #define STM32BL_GET 0x00 /* Get command */ 17 | #define STM32BL_GVR 0x01 /* Get Version & Read Protection Status command */ 18 | #define STM32BL_GID 0x02 /* Get ID command */ 19 | #define STM32BL_RM 0x11 /* Read Memory command */ 20 | #define STM32BL_GO 0x21 /* Go command */ 21 | #define STM32BL_WM 0x31 /* Write Memory command */ 22 | #define STM32BL_EM 0x43 /* Erase Memory command */ 23 | #define STM32BL_EEM 0x44 /* Extended Erase Memory command */ 24 | #define STM32BL_WP 0x63 /* Write Protect command */ 25 | #define STM32BL_WUP 0x73 /* Write Unprotect command */ 26 | #define STM32BL_RP 0x82 /* Readout Protect command */ 27 | #define STM32BL_RUP 0x92 /* Readout Unprotect command */ 28 | 29 | /* STM32 code start address (must be changed if custom bootloader is used */ 30 | #define STM32_FLASH_START_ADDR 0x08000000 31 | #define STM32_RAM_START_ADDR 0x20002000 32 | 33 | #define STM32L07XX_ID 0x447 34 | #define STM32G431XX_ID 0x468 35 | 36 | #define STM32_nBOOT0 (1U << 27) 37 | #define STM32_nSWBOOT0 (1U << 26) 38 | #define STM32_nBOOT1 (1U << 23) 39 | 40 | typedef struct { 41 | uint16_t id; 42 | uint32_t sram_start; 43 | uint32_t flash_start; 44 | uint32_t opt_start; 45 | } stm32_device_t; 46 | /** 47 | * Resets the device after flashing 48 | * 49 | * @return ESP_FAIL in case of error 50 | */ 51 | int stm32bl_reset_device(uint16_t id); 52 | 53 | /** 54 | * Get bootloader version 55 | * 56 | * @return Version number or ESP_FAIL in case of error 57 | */ 58 | int stm32bl_get_version(); 59 | 60 | /** 61 | * Get STM32 MCU product ID 62 | * 63 | * @return Product ID or ESP_FAIL in case of error 64 | */ 65 | int stm32bl_get_id(); 66 | 67 | /** 68 | * Send unprotect write memory command 69 | * 70 | * @return 71 | * - ESP_FAIL in case of communication error 72 | * - STM32BL_ACK if protection was successfully unset 73 | * - STM32BL_NACK if protection was not successfully unset 74 | */ 75 | int stm32bl_unprotect_write(); 76 | 77 | /** 78 | * Send unprotect read memory command 79 | * 80 | * @return 81 | * - ESP_FAIL in case of communication error 82 | * - STM32BL_ACK if protection was successfully unset 83 | * - STM32BL_NACK if protection was not successfully unset 84 | */ 85 | int stm32bl_unprotect_read(); 86 | 87 | /** 88 | * Send protect read memory command 89 | * 90 | * @return 91 | * - ESP_FAIL in case of communication error 92 | * - STM32BL_ACK if protection was successfully set 93 | * - STM32BL_NACK if protection was not successfully set 94 | */ 95 | int stm32bl_protect_read(); 96 | 97 | /** 98 | * Global erase of flash 99 | * 100 | * @return 101 | * - ESP_FAIL in case of communication error 102 | * - STM32BL_ACK if erase was successful 103 | * - STM32BL_NACK if erase was not successful 104 | */ 105 | int stm32bl_erase_all(uint16_t max_pages); 106 | 107 | /** 108 | * Read from flash memory 109 | * 110 | * @param buf Buffer to store read data 111 | * @param num_bytes Number of bytes to read (must fit into buffer) 112 | * @param start_addr Address to read from 113 | * 114 | * @return 115 | * - ESP_FAIL in case of communication error 116 | * - STM32BL_ACK if reading was successful 117 | * - STM32BL_NACK if reading was not successful 118 | */ 119 | int stm32bl_read(uint8_t *buf, uint8_t num_bytes, uint32_t start_addr); 120 | 121 | /** 122 | * Write to flash memory 123 | * 124 | * @param buf Buffer containing data to write 125 | * @param num_bytes Number of bytes to write 126 | * @param start_addr Start address to write to 127 | * 128 | * @return 129 | * - ESP_FAIL in case of communication error 130 | * - STM32BL_ACK if reading was successful 131 | * - STM32BL_NACK if writing was not successful 132 | */ 133 | int stm32bl_write(uint8_t *buf, uint32_t num_bytes, uint32_t start_addr); 134 | 135 | /** 136 | * Go to address (to start program) 137 | * 138 | * @param addr Address in MCU RAM 139 | * 140 | * @return 141 | * - ESP_FAIL in case of communication error 142 | * - STM32BL_ACK if reading was successful 143 | * - STM32BL_NACK if reading was not successful 144 | */ 145 | int stm32bl_go(uint32_t addr); 146 | 147 | /** 148 | * Initialize communication with the STM32 bootloader 149 | * 150 | * @return 151 | * - ESP_FAIL in case of communication error 152 | * - STM32BL_ACK if reading was successful 153 | * - STM32BL_NACK if reading was not successful 154 | */ 155 | int stm32bl_init(); 156 | 157 | /** 158 | * Resets the option bytes so that the bootloader is not 159 | * started over and over again. 160 | * 161 | * @param The starting address of the option byte area 162 | * @return 163 | * - ESP_FAIL in case of communication error 164 | * - STM32BL_ACK if reading and writing was successful 165 | * - STM32BL_NACK if reading or writing was not successful 166 | */ 167 | int stm32bl_reset_optr(uint32_t ob_addr); 168 | 169 | #endif /* STM32BL_H_ */ 170 | -------------------------------------------------------------------------------- /main/ts_cbor.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | 8 | #include "ts_cbor.h" 9 | #include "../lib/tinycbor/src/cbor.h" 10 | #include "../lib/tinycbor/src/cborjson.h" 11 | #include "esp_err.h" 12 | #include 13 | #include "cJSON.h" 14 | 15 | #ifndef UNIT_TEST 16 | 17 | #include "esp_log.h" 18 | 19 | #endif 20 | 21 | static const char *TAG = "ts_cbor"; 22 | 23 | uint32_t buffersize = 0; 24 | 25 | int getItemCount(cJSON *json) 26 | { 27 | int count = 0; 28 | for (cJSON *elem = json->child; elem; elem = elem->next) { 29 | count ++; 30 | } 31 | return count; 32 | } 33 | 34 | CborError json2cbor(cJSON *json, CborEncoder *encoder, uint8_t *ts_query) { 35 | CborEncoder sub_encoder; //for the items of the array/object 36 | CborError error = 0; 37 | cJSON *item; 38 | 39 | switch (json->type) { 40 | case cJSON_True: 41 | case cJSON_False: 42 | return cbor_encode_boolean(encoder, json->type == cJSON_True); 43 | 44 | case cJSON_String: 45 | return cbor_encode_text_stringz(encoder, json->valuestring); 46 | 47 | case cJSON_NULL: 48 | return cbor_encode_null(encoder); 49 | 50 | case cJSON_Number: 51 | if ((double)json->valueint == json->valuedouble) { 52 | return cbor_encode_int(encoder, json->valueint); 53 | } 54 | 55 | encode_double: 56 | // the only exception that JSON is larger: floating point numbers 57 | sub_encoder = *encoder; // save the state 58 | error = cbor_encode_double(encoder, json->valuedouble); 59 | if (error == CborErrorOutOfMemory) { 60 | buffersize += 128; 61 | uint8_t *newbuffer = realloc(ts_query, buffersize); 62 | if (newbuffer == NULL) { 63 | return error; 64 | } 65 | *encoder = sub_encoder; // restore state 66 | encoder->data.ptr = newbuffer + (sub_encoder.data.ptr - ts_query); 67 | encoder->end = newbuffer + buffersize; 68 | ts_query = newbuffer; 69 | goto encode_double; 70 | } 71 | return error; 72 | 73 | case cJSON_Array: 74 | // this will init the sub encoder and create an array in the base cbor stream 75 | error = cbor_encoder_create_array(encoder, &sub_encoder, getItemCount(json)); 76 | if (error) { 77 | return error; 78 | } 79 | for (item = json->child; item != NULL; item = item->next) { 80 | // recursive call with the sub encoder 81 | error = json2cbor(item, &sub_encoder, ts_query); 82 | if (error) { 83 | return error; 84 | } 85 | } 86 | return cbor_encoder_close_container(encoder, &sub_encoder); 87 | 88 | case cJSON_Object: 89 | error = cbor_encoder_create_map(encoder, &sub_encoder, getItemCount(json)); 90 | if (error) 91 | return error; 92 | 93 | for (item = json->child ; item; item = item->next) { 94 | error = cbor_encode_text_stringz(&sub_encoder, item->string); 95 | if (error) { 96 | return error; 97 | } 98 | error = json2cbor(item, &sub_encoder, ts_query); 99 | } 100 | if (error){ 101 | return error; 102 | } 103 | return cbor_encoder_close_container_checked(encoder, &sub_encoder); 104 | default: return error; 105 | } 106 | return error; 107 | } 108 | 109 | void *ts_build_query_bin(uint8_t ts_method, TSUriElems *params, uint32_t *query_length) 110 | { 111 | if (params == NULL) { 112 | return NULL; 113 | } 114 | 115 | buffersize = 1; // 1 byte method 116 | if (strlen_null(params->ts_target_node) == 0 && params->ts_list_subnodes == 0) { 117 | buffersize += 2; // cbor-string '/' 118 | } 119 | buffersize += strlen_null(params->ts_target_node) + 1; // strlen + cbor string flag 120 | buffersize += strlen_null(params->ts_payload); //assumption that cbor is always equal or smaller than json string 121 | 122 | uint8_t *ts_query = (uint8_t *) calloc(buffersize, sizeof(uint8_t)); 123 | 124 | if (ts_query == NULL) { 125 | ESP_LOGE(TAG, "Unable to allocate memory for ts_query"); 126 | return NULL; 127 | } 128 | 129 | int pos = 0; 130 | switch (ts_method) { 131 | case TS_GET: 132 | ts_query[pos] = TS_GET; 133 | break; 134 | case TS_POST: 135 | ts_query[pos] = TS_POST; 136 | break; 137 | case TS_PATCH: 138 | ts_query[pos] = TS_PATCH; 139 | break; 140 | case TS_DELETE: 141 | ts_query[pos] = TS_DELETE; 142 | break; 143 | default: 144 | ts_query[pos] = 0; 145 | return ts_query; // nothing else to do here 146 | } 147 | pos++; 148 | 149 | CborEncoder encoder; 150 | cbor_encoder_init(&encoder, ts_query + pos, buffersize - pos, 0); 151 | CborError err = cbor_encode_text_stringz(&encoder, params->ts_target_node); 152 | if (err) { 153 | free(ts_query); 154 | return NULL; 155 | } 156 | if (params->ts_payload != NULL) { 157 | cJSON *payload = cJSON_ParseWithOpts(params->ts_payload, NULL, true); 158 | if (payload == NULL){ 159 | free(ts_query); 160 | return NULL; 161 | } 162 | //char *string = cJSON_Print(payload); 163 | //printf("%s\n", string); 164 | //free(string); 165 | err = json2cbor(payload, &encoder, ts_query); 166 | cJSON_free(payload); 167 | } 168 | *query_length = encoder.data.ptr - ts_query; 169 | return (void *) ts_query; 170 | } 171 | 172 | char *cbor2json(uint8_t *cbor, size_t len) 173 | { 174 | CborParser parser; 175 | CborValue value; 176 | CborError error = cbor_parser_init(cbor, len, 0, &parser, &value); 177 | if (error) { 178 | return NULL; 179 | } 180 | char *json = NULL; 181 | size_t size; 182 | FILE *stream = open_memstream(&json, &size); 183 | error = cbor_value_to_json_advance(stream, &value, 0); 184 | fclose(stream); 185 | if (error) { 186 | return NULL; 187 | } 188 | return json; 189 | } 190 | 191 | // decode cbor and replace binary data with json 192 | // free binary data from receive task and replace pointer 193 | // with new allocated json, will be free'd in web_server.c 194 | char *ts_cbor_resp_data(TSResponse *res) 195 | { 196 | char *json = cbor2json((uint8_t *) res->block + 1, res->block_len); 197 | free(res->block); 198 | res->block = json; 199 | return(json); 200 | } 201 | 202 | uint8_t ts_cbor_resp_status(TSResponse *res) 203 | { 204 | return res->block[0]; 205 | } 206 | -------------------------------------------------------------------------------- /main/ts_cbor.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef TS_CBOR_H_ 8 | #define TS_CBOR_H_ 9 | 10 | #include "ts_client.h" 11 | 12 | void *ts_build_query_bin(uint8_t ts_method, TSUriElems *params, uint32_t *query_length); 13 | 14 | char *cbor2json(uint8_t *cbor, size_t len); 15 | 16 | uint8_t ts_cbor_resp_status(TSResponse *resp); 17 | 18 | char *ts_cbor_resp_data(TSResponse *res); 19 | 20 | #endif // TS_CBOR_H__ 21 | -------------------------------------------------------------------------------- /main/ts_client.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "ts_client.h" 8 | #include 9 | #include 10 | #include "esp_err.h" 11 | 12 | #ifndef UNIT_TEST 13 | 14 | #include "ts_serial.h" 15 | #include "can.h" 16 | #include "esp_http_server.h" 17 | #include "esp_log.h" 18 | #include "cJSON.h" 19 | #include "data_nodes.h" 20 | 21 | static const char *TAG = "ts_client"; 22 | 23 | static TSDevice *devices[10]; 24 | extern char device_id[9]; 25 | extern GeneralConfig general_config; 26 | 27 | void ts_devices_init() 28 | { 29 | // Add self to devices 30 | devices[0] = (TSDevice *) malloc(sizeof(TSDevice)); 31 | devices[0]->ts_device_id = device_id; 32 | devices[0]->ts_name = "self"; 33 | devices[0]->can_address = 0; 34 | devices[0]->send = &process_ts_request; 35 | devices[0]->build_query = &ts_build_query_serial; 36 | devices[0]->ts_resp_data = &ts_serial_resp_data; 37 | devices[0]->ts_resp_status = &ts_serial_resp_status; 38 | } 39 | 40 | void ts_devices_scan_serial() 41 | { 42 | int num = 1; 43 | while (devices[num] != NULL && num < sizeof(devices)) { 44 | num++; 45 | } 46 | 47 | // scan serial connection 48 | if (num < sizeof(devices) && general_config.ts_serial_active) { 49 | devices[num] = (TSDevice *) calloc(1, sizeof(TSDevice)); 50 | int err = ts_serial_scan_device_info(devices[num]); 51 | if (err) { 52 | ts_remove_device(devices[num]); 53 | devices[num] = NULL; 54 | } 55 | } 56 | } 57 | 58 | void ts_devices_add_can(uint8_t can_addr) 59 | { 60 | int num = 1; 61 | while (devices[num] != NULL && num < sizeof(devices)) { 62 | num++; 63 | } 64 | 65 | if (num < sizeof(devices) && general_config.ts_can_active) { 66 | devices[num] = (TSDevice *) calloc(1, sizeof(TSDevice)); 67 | devices[num]->can_address = can_addr; 68 | } 69 | } 70 | 71 | char *ts_get_device_list() 72 | { 73 | cJSON *obj = cJSON_CreateObject(); 74 | int i = 0; 75 | while (devices[i] != NULL) { 76 | if (devices[i]->can_address > 0 && devices[i]->ts_device_id == NULL) { 77 | // device information not yet obtained 78 | int err = ts_can_scan_device_info(devices[i]); 79 | if (!err) { 80 | cJSON *id = cJSON_CreateString(devices[i]->ts_device_id); 81 | cJSON_AddItemToObject(obj, devices[i]->ts_name, id); 82 | } 83 | else { 84 | ts_remove_device(devices[i]); 85 | devices[i] = NULL; 86 | } 87 | } else { 88 | cJSON *id = cJSON_CreateString(devices[i]->ts_device_id); 89 | cJSON_AddItemToObject(obj, devices[i]->ts_name, id); 90 | } 91 | i++; 92 | } 93 | char *names_string = NULL; 94 | if (i > 0) { 95 | names_string = cJSON_Print(obj); 96 | cJSON_Delete(obj); 97 | } else { 98 | // hackky solution so that free() can always be called on names_string 99 | const char msg[] = ""; 100 | names_string = (char *) malloc(sizeof(msg)+1); 101 | strncpy(names_string, msg, sizeof(msg)+1); 102 | } 103 | return names_string; 104 | } 105 | 106 | TSDevice *ts_get_device(char *device_id) 107 | { 108 | if (device_id == NULL) { 109 | return NULL; 110 | } 111 | for (int i = 0; i < sizeof(devices); i++) { 112 | if (devices[i] != NULL && devices[i]->ts_device_id != NULL) { 113 | if (strcmp(devices[i]->ts_device_id, device_id) == 0) { 114 | return devices[i]; 115 | } 116 | } 117 | } 118 | return NULL; 119 | } 120 | 121 | TSDevice *ts_get_can_device(uint8_t can_addr) 122 | { 123 | for (int i = 0; devices[i] != NULL && i < sizeof(devices); i++) { 124 | if (devices[i]->can_address == can_addr) { 125 | return devices[i]; 126 | } 127 | } 128 | return NULL; 129 | } 130 | 131 | int ts_parse_device_info(cJSON *json, TSDevice *device) 132 | { 133 | size_t ts_string_len = strlen(cJSON_GetStringValue(cJSON_GetObjectItem(json, "DeviceType"))); 134 | device->ts_name = (char *) malloc(ts_string_len+1); 135 | if (device->ts_name == NULL) { 136 | ESP_LOGE(TAG, "Unable to allocate memory for device name"); 137 | return -1; 138 | } 139 | strcpy(device->ts_name, cJSON_GetStringValue(cJSON_GetObjectItem(json, "DeviceType"))); 140 | ts_string_len = strlen(cJSON_GetStringValue(cJSON_GetObjectItem(json, "DeviceID"))); 141 | device->ts_device_id = (char *) malloc(ts_string_len+1); 142 | if (device->ts_device_id == NULL) { 143 | ESP_LOGE(TAG, "Unable to allocate memory for device id"); 144 | return -1; 145 | } 146 | strcpy(device->ts_device_id , cJSON_GetStringValue(cJSON_GetObjectItem(json, "DeviceID"))); 147 | ESP_LOGI(TAG, "Got device with ID: %s!", device->ts_device_id); 148 | 149 | return 0; 150 | } 151 | 152 | void ts_remove_device(TSDevice *device) 153 | { 154 | if (device == NULL) { 155 | return; 156 | } 157 | if (device->ts_name != NULL) { 158 | free(device->ts_name); 159 | } 160 | if (device->ts_device_id != NULL) { 161 | free(device->ts_device_id); 162 | } 163 | free(device); 164 | } 165 | 166 | #endif 167 | 168 | // wrapper for strlen() to check for NULL 169 | int strlen_null(char *r) 170 | { 171 | if (r != NULL) { 172 | return(strlen(r)); 173 | } else { 174 | return 0; 175 | } 176 | } 177 | 178 | char *exec_or_create(char *node) 179 | { 180 | if (strstr(node, "auth") != NULL || 181 | strstr(node, "exec") != NULL || 182 | strstr(node, "dfu") != NULL) { 183 | return "!"; 184 | } else { 185 | return "+"; 186 | } 187 | } 188 | 189 | void ts_parse_uri(const char *uri, TSUriElems *params) 190 | { 191 | params->ts_list_subnodes = -1; 192 | params->ts_device_id = NULL; 193 | params->ts_target_node = NULL; 194 | 195 | if (uri == NULL || uri[0] == '\0') { 196 | ESP_LOGE(TAG, "Got invalid uri"); 197 | return; 198 | } 199 | params->ts_list_subnodes = uri[strlen(uri)-1] == '/' ? 0 : 1; 200 | 201 | // copy uri so we can safely modify 202 | char *temp_uri = (char *) malloc(strlen(uri)+1); 203 | if (temp_uri == NULL) { 204 | ESP_LOGE(TAG, "Unable to allocate memory for temp_uri"); 205 | return; 206 | } 207 | strcpy(temp_uri, uri); 208 | // extract device ID 209 | int i = 0; 210 | while (temp_uri[i] != '\0' && temp_uri[i] != '/') { 211 | i++; 212 | } 213 | // replace '/' so we have two null-terminated strings 214 | temp_uri[i] = '\0'; 215 | params->ts_device_id = temp_uri; 216 | // this points either to '\0' aka NULL or the rest of the string 217 | params->ts_target_node = temp_uri + i + 1; 218 | ESP_LOGD(TAG, "Got URI %s", uri); 219 | ESP_LOGD(TAG, "Device_id: %s", params->ts_device_id); 220 | ESP_LOGD(TAG, "Target Node: %s", params->ts_target_node); 221 | ESP_LOGD(TAG, "List the sub nodes: %s", params->ts_list_subnodes == 0 ? "yes" : "no"); 222 | } 223 | 224 | void *ts_build_query_serial(uint8_t ts_method, TSUriElems *params, uint32_t *query_size) 225 | { 226 | if (params == NULL) { 227 | return NULL; 228 | } 229 | // calculate size to allocate buffer 230 | int nbytes = 3; // method + termination + zero termination 231 | if (strlen_null(params->ts_target_node) == 0 && params->ts_list_subnodes == 0) { 232 | nbytes++; // '/' at the end of query 233 | } 234 | nbytes += strlen_null(params->ts_target_node); 235 | if (params->ts_payload != NULL) { 236 | // additional whitespace between uri and array/json 237 | nbytes += strlen_null(params->ts_payload) +1; 238 | } 239 | char *ts_query = (char *) malloc(nbytes); 240 | 241 | // Special case when devices using the CAN Bus with TEXT-Mode, 242 | // they must not send the termination bytes used with UART 243 | if (query_size != NULL) { 244 | *query_size = nbytes - 2; 245 | } 246 | 247 | if (ts_query == NULL) { 248 | ESP_LOGE(TAG, "Unable to allocate memory for ts_query"); 249 | return NULL; 250 | } 251 | int pos = 0; 252 | switch (ts_method){ 253 | case TS_GET: 254 | ts_query[pos] = '?'; 255 | break; 256 | case TS_POST: 257 | ts_query[pos] = *(exec_or_create(params->ts_target_node)); 258 | break; 259 | case TS_PATCH: 260 | ts_query[pos] = '='; 261 | break; 262 | case TS_DELETE: 263 | ts_query[pos] = '-'; 264 | break; 265 | default: 266 | ts_query[pos] = '\0'; 267 | return ts_query; // nothing else to do here 268 | } 269 | pos++; 270 | // corner case for getting device categories 271 | if (strlen(params->ts_target_node) == 0 && params->ts_list_subnodes == 0) { 272 | ts_query[pos] = '/'; 273 | pos++; 274 | } else { 275 | strncpy(ts_query + pos, params->ts_target_node, strlen_null(params->ts_target_node)); 276 | pos += strlen_null(params->ts_target_node); 277 | } 278 | if (params->ts_payload != NULL) { 279 | ts_query[pos] = ' '; 280 | pos++; 281 | strncpy(ts_query + pos, params->ts_payload, strlen_null(params->ts_payload)); 282 | pos += strlen_null(params->ts_payload); 283 | } 284 | //terminate query properly 285 | ts_query[pos] = '\n'; 286 | pos++; 287 | ts_query[pos] = '\0'; 288 | ESP_LOGD(TAG, "Build query String: %s !", ts_query); 289 | return (void *) ts_query; 290 | } 291 | 292 | #ifndef UNIT_TEST 293 | 294 | TSResponse *ts_execute(const char *uri, char *content, int http_method) 295 | { 296 | uint8_t ts_method; 297 | switch (http_method) { 298 | case HTTP_DELETE: 299 | ts_method = TS_DELETE; 300 | break; 301 | case HTTP_GET: 302 | ts_method = TS_GET; 303 | break; 304 | case HTTP_POST: 305 | ts_method = TS_POST; 306 | break; 307 | case HTTP_PATCH: 308 | ts_method = TS_PATCH; 309 | break; 310 | default: 311 | ts_method = TS_GET; 312 | break; 313 | } 314 | TSUriElems params; 315 | params.ts_payload = content; 316 | params.ts_list_subnodes = -1; 317 | ts_parse_uri(uri, ¶ms); 318 | TSDevice *device = ts_get_device(params.ts_device_id); 319 | if (device == NULL) { 320 | ESP_LOGD(TAG, "No Device, freeing query string and device id"); 321 | heap_caps_free(params.ts_device_id); 322 | return NULL; 323 | } 324 | uint32_t query_size; 325 | char *ts_query_string = device->build_query(ts_method, ¶ms, &query_size); 326 | 327 | TSResponse *res = (TSResponse *) malloc(sizeof(TSResponse)); 328 | if (res == NULL) { 329 | ESP_LOGE(TAG, "Unable to allocate memory for ts response"); 330 | heap_caps_free(ts_query_string); 331 | heap_caps_free(params.ts_device_id); 332 | return NULL; 333 | } 334 | // send is already a pointer to the correct function 335 | uint32_t block_len = 0; 336 | res->block = device->send((uint8_t *)ts_query_string, query_size, device->can_address, &block_len); 337 | res->block_len = block_len; 338 | if (res->block == NULL) { 339 | ESP_LOGI(TAG, "No Response, freeing query string and device id"); 340 | heap_caps_free(ts_query_string); 341 | heap_caps_free(params.ts_device_id); 342 | heap_caps_free(res); 343 | return NULL; 344 | } 345 | 346 | //call status code first, data will be overwritten when device is using CAN binary methods 347 | res->ts_status_code = device->ts_resp_status(res); 348 | res->data = device->ts_resp_data(res); 349 | 350 | heap_caps_free(ts_query_string); 351 | heap_caps_free(params.ts_device_id); 352 | 353 | return res; 354 | } 355 | 356 | char *ts_serial_resp_data(TSResponse *res) 357 | { 358 | if (res->block[0] == ':') { 359 | char *pos = strstr(res->block, ". "); 360 | if (pos != NULL) { 361 | return pos + 2; 362 | } 363 | } 364 | return NULL; 365 | } 366 | 367 | uint8_t ts_serial_resp_status(TSResponse *res) 368 | { 369 | unsigned int status_code = -1; 370 | sscanf(res->block, ":%X ", &status_code); 371 | return status_code; 372 | } 373 | 374 | #endif //UNIT_TEST 375 | -------------------------------------------------------------------------------- /main/ts_client.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef TS_CLIENT_H_ 8 | #define TS_CLIENT_H_ 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | #include 15 | #include 16 | #include 17 | #include "cJSON.h" 18 | 19 | /* 20 | * Protocol function codes (same as CoAP) 21 | */ 22 | #define TS_GET 0x01 23 | #define TS_POST 0x02 24 | #define TS_DELETE 0x04 25 | #define TS_FETCH 0x05 26 | #define TS_PATCH 0x07 // it's actually iPATCH 27 | 28 | #define TS_PUBMSG 0x1F 29 | 30 | /* 31 | * Status codes (same as CoAP) 32 | */ 33 | 34 | // success 35 | #define TS_STATUS_CREATED 0x81 36 | #define TS_STATUS_DELETED 0x82 37 | #define TS_STATUS_VALID 0x83 38 | #define TS_STATUS_CHANGED 0x84 39 | #define TS_STATUS_CONTENT 0x85 40 | 41 | // client errors 42 | #define TS_STATUS_BAD_REQUEST 0xA0 43 | #define TS_STATUS_UNAUTHORIZED 0xA1 // need to authenticate 44 | #define TS_STATUS_FORBIDDEN 0xA3 // trying to write read-only value 45 | #define TS_STATUS_NOT_FOUND 0xA4 46 | #define TS_STATUS_METHOD_NOT_ALLOWED 0xA5 47 | #define TS_STATUS_REQUEST_INCOMPLETE 0xA8 48 | #define TS_STATUS_CONFLICT 0xA9 49 | #define TS_STATUS_REQUEST_TOO_LARGE 0xAD 50 | #define TS_STATUS_UNSUPPORTED_FORMAT 0xAF 51 | 52 | // server errors 53 | #define TS_STATUS_INTERNAL_SERVER_ERR 0xC0 54 | #define TS_STATUS_NOT_IMPLEMENTED 0xC1 55 | 56 | // ThingSet specific errors 57 | #define TS_STATUS_RESPONSE_TOO_LARGE 0xE1 58 | 59 | /** 60 | * Struct to hold all information passed by the HTTP request 61 | */ 62 | typedef struct { 63 | char *ts_device_id; 64 | char *ts_target_node; //json pointer 65 | int ts_list_subnodes; // wether to return values or available nodes/options 66 | char *ts_payload; 67 | } TSUriElems; 68 | 69 | /** 70 | * Struct to hold ts response values for the http handlers 71 | */ 72 | typedef struct { 73 | uint8_t ts_status_code; 74 | char *data; 75 | char *block; 76 | uint32_t block_len; 77 | } TSResponse; 78 | 79 | /** 80 | * Struct to hold device information 81 | * when a new device is connected, the function pointer 82 | * should be set accordingly to either the serial or CAN send function 83 | */ 84 | typedef struct { 85 | uint8_t can_address; 86 | char *ts_device_id; 87 | char *ts_name; 88 | //function pointer to send requests to device, abstracting underlying connection 89 | char *(*send)(uint8_t *req, uint32_t query_size, uint8_t can_address, uint32_t *block_len); 90 | void *(*build_query)(uint8_t ts_method, TSUriElems *params, uint32_t* query_size); 91 | char *(*ts_resp_data)(TSResponse *res); 92 | uint8_t (*ts_resp_status)(TSResponse *res); 93 | } TSDevice; 94 | 95 | /** 96 | * Get Handler for basic get requests 97 | * 98 | * \returns a pointer to a response object containing status code and data string 99 | */ 100 | TSResponse *ts_execute(const char *uri, char *content, int http_method); 101 | 102 | /** 103 | * Parses the response for the beginning of the payload. Does not work on binary data! 104 | * \returns A pointer to the first character of the payload 105 | */ 106 | char *ts_serial_resp_data(TSResponse *res); 107 | 108 | /** 109 | * Parses the response for status code. Does not work on binary data! 110 | * \returns The ThingSet status 111 | */ 112 | uint8_t ts_serial_resp_status(TSResponse *res); 113 | 114 | /** 115 | * Initializes the device list by adding this device itself 116 | */ 117 | void ts_devices_init(); 118 | 119 | /** 120 | * Scans the serial interface for a device and adds it if existing 121 | */ 122 | void ts_devices_scan_serial(); 123 | 124 | /** 125 | * Adds an identified CAN device to the devices list (but does not retrieve device information) 126 | */ 127 | void ts_devices_add_can(uint8_t can_addr); 128 | 129 | /** 130 | * Will return a list with the names and IDs of connected devices 131 | * 132 | * \returns A char pointer to a stringified json array 133 | * 134 | * The caller is responsible to call free() on the result after usage 135 | */ 136 | char *ts_get_device_list(); 137 | 138 | /** 139 | * Check if a CAN device is already known 140 | * 141 | * \returns pointer to TSDevice or NULL in case the device is not found 142 | */ 143 | TSDevice *ts_get_can_device(uint8_t can_addr); 144 | 145 | /** 146 | * Generate a ThingSet request header from HTTP URL and mode 147 | * 148 | * \returns Number of characters added to the buffer or 0 in case of error 149 | */ 150 | int ts_req_hdr_from_http(char *buf, size_t buf_size, int method, const char *uri); 151 | 152 | /** 153 | * Parse a given URI into the elems struct. Necessary to map the HTTP endpoint to the 154 | * Thingset Serial/CAN implementation 155 | */ 156 | void ts_parse_uri(const char *uri, TSUriElems *params); 157 | 158 | /** 159 | * Builds the ThingSet query in string format. 160 | * \returns String with the query 161 | * Caller is responsible to free() string 162 | */ 163 | void *ts_build_query_serial(uint8_t ts_method, TSUriElems *params, uint32_t *query_size); 164 | 165 | /** 166 | * Takes device Information as a json and fills it in a TSDevice struct. Allocates the necessary memory. 167 | * \returns a nonzero value in case of failure 168 | */ 169 | int ts_parse_device_info(cJSON *json, TSDevice *device); 170 | 171 | /** 172 | * Frees all fields in the TSDevice struct and the struct itself. 173 | */ 174 | void ts_remove_device(TSDevice *device); 175 | 176 | /** 177 | * Wrapper for strlen where NULL is interpreted as a string with length of zero 178 | */ 179 | int strlen_null(char *r); 180 | 181 | #ifdef __cplusplus 182 | } 183 | #endif 184 | 185 | #endif /* TS_CLIENT_H_ */ 186 | -------------------------------------------------------------------------------- /main/ts_mqtt.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "freertos/FreeRTOS.h" 14 | #include "freertos/task.h" 15 | #include "freertos/event_groups.h" 16 | 17 | #include "esp_system.h" 18 | #include "esp_wifi.h" 19 | #include "esp_netif.h" 20 | #include "esp_event.h" 21 | #include "esp_err.h" 22 | #include "esp_log.h" 23 | #include "driver/gpio.h" 24 | #include "nvs_flash.h" 25 | #include "sdkconfig.h" 26 | 27 | #include "mqtt_client.h" 28 | #include "esp_tls.h" 29 | #include "esp_ota_ops.h" 30 | #include 31 | 32 | #include "ts_serial.h" 33 | #include "ts_client.h" 34 | #include "can.h" 35 | #include "wifi.h" 36 | #include "data_nodes.h" 37 | 38 | MqttConfig mqtt_config; 39 | 40 | static const char* TAG = "ts_mqtt"; 41 | 42 | #if CONFIG_THINGSET_MQTT_TLS 43 | /* the certificate path is linked in via root CMakeLists.txt */ 44 | extern const uint8_t mqtt_root_pem_start[] asm("_binary_isrgrootx1_pem_start"); 45 | //extern const uint8_t mqtt_root_pem_end[] asm("_binary_isrgrootx1_pem_end"); 46 | #endif 47 | 48 | static void send_data(esp_mqtt_client_handle_t client, char *device_id, char *path, char *data) 49 | { 50 | char mqtt_topic[256]; 51 | snprintf(mqtt_topic, sizeof(mqtt_topic), "ts/%s/%s/tx/%s", 52 | mqtt_config.username, device_id, path); 53 | int msg_id = esp_mqtt_client_publish(client, mqtt_topic, data, 0, 0, 0); 54 | ESP_LOGI(TAG, "message sent to %s with msg_id=%d", mqtt_topic, msg_id); 55 | } 56 | 57 | /* 58 | * Event handler called by the MQTT client event loop. Currently only used for debugging 59 | */ 60 | static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, 61 | void *event_data) 62 | { 63 | ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%d", base, event_id); 64 | esp_mqtt_event_handle_t event = event_data; 65 | //esp_mqtt_client_handle_t client = event->client; 66 | //int msg_id; 67 | switch ((esp_mqtt_event_id_t)event_id) { 68 | case MQTT_EVENT_CONNECTED: 69 | ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); 70 | //msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0); 71 | //ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); 72 | //msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1); 73 | //ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); 74 | //msg_id = esp_mqtt_client_unsubscribe(client, "/topic/qos1"); 75 | //ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id); 76 | break; 77 | case MQTT_EVENT_DISCONNECTED: 78 | ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); 79 | break; 80 | case MQTT_EVENT_SUBSCRIBED: 81 | ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); 82 | //msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0); 83 | //ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); 84 | break; 85 | case MQTT_EVENT_UNSUBSCRIBED: 86 | ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); 87 | break; 88 | case MQTT_EVENT_PUBLISHED: 89 | ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); 90 | break; 91 | case MQTT_EVENT_DATA: 92 | ESP_LOGI(TAG, "MQTT_EVENT_DATA"); 93 | //printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); 94 | //printf("DATA=%.*s\r\n", event->data_len, event->data); 95 | //if (strncmp(event->data, "send binary please", event->data_len) == 0) { 96 | // ESP_LOGI(TAG, "Sending the binary"); 97 | // send_binary(client); 98 | //} 99 | break; 100 | case MQTT_EVENT_ERROR: 101 | ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); 102 | if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { 103 | ESP_LOGI(TAG, "Last error code reported from esp-tls: 0x%x", event->error_handle->esp_tls_last_esp_err); 104 | ESP_LOGI(TAG, "Last tls stack error number: 0x%x", event->error_handle->esp_tls_stack_err); 105 | ESP_LOGI(TAG, "Last captured errno : %d (%s)", event->error_handle->esp_transport_sock_errno, 106 | strerror(event->error_handle->esp_transport_sock_errno)); 107 | } else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { 108 | ESP_LOGI(TAG, "Connection refused error: 0x%x", event->error_handle->connect_return_code); 109 | } else { 110 | ESP_LOGW(TAG, "Unknown error type: 0x%x", event->error_handle->error_type); 111 | } 112 | break; 113 | default: 114 | ESP_LOGI(TAG, "Other event id:%d", event->event_id); 115 | break; 116 | } 117 | } 118 | 119 | void ts_mqtt_pub_task(void *arg) 120 | { 121 | TSDevice ts_device; 122 | ts_device.ts_device_id = NULL; 123 | bool device_found = false; 124 | esp_err_t err; 125 | 126 | esp_mqtt_client_config_t mqtt_cfg = { 127 | .uri = mqtt_config.broker_hostname, 128 | #if CONFIG_THINGSET_MQTT_TLS 129 | .cert_pem = (const char *)mqtt_root_pem_start, 130 | #endif 131 | }; 132 | 133 | if (mqtt_config.use_broker_auth) { 134 | mqtt_cfg.username = mqtt_config.username; 135 | mqtt_cfg.password = mqtt_config.password; 136 | } 137 | 138 | // wait 3s for device to boot 139 | vTaskDelay(3000 / portTICK_PERIOD_MS); 140 | 141 | esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); 142 | 143 | // the last argument may be used to pass data to the event handler 144 | esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); 145 | esp_mqtt_client_start(client); 146 | 147 | TickType_t mqtt_pub_ticks = xTaskGetTickCount(); 148 | 149 | while (1) { 150 | if (!device_found) { 151 | err = ts_serial_scan_device_info(&ts_device); 152 | if (err) { 153 | ESP_LOGE(TAG, "No device found, waiting 1 minute"); 154 | vTaskDelay(60 * 1000 / portTICK_PERIOD_MS); 155 | continue; 156 | } 157 | else { 158 | device_found = true; 159 | } 160 | } 161 | 162 | // wait until we get a serial publication message 163 | char *pub_msg = ts_serial_pubmsg(1000); 164 | while (pub_msg == NULL) { 165 | pub_msg = ts_serial_pubmsg(1000); 166 | printf("Waiting for pub msg\n"); 167 | } 168 | 169 | // message format: # 170 | char *delimiter = strchr(pub_msg, ' '); 171 | if (delimiter != NULL && pub_msg[0] == '#') { 172 | gpio_set_level(CONFIG_GPIO_LED, 0); 173 | if (delimiter != pub_msg + 1) { 174 | *delimiter = '\0'; // null-terminate path section 175 | send_data(client, ts_device.ts_device_id, pub_msg + 1, delimiter + 1); 176 | } 177 | else { 178 | // old ThingSet statement format without path 179 | send_data(client, ts_device.ts_device_id, "serial", delimiter + 1); 180 | } 181 | printf("Publishing via MQTT: %s\n", pub_msg); 182 | } 183 | ts_serial_pubmsg_clear(); 184 | 185 | vTaskDelay(100 / portTICK_PERIOD_MS); 186 | gpio_set_level(CONFIG_GPIO_LED, 1); 187 | 188 | vTaskDelayUntil(&mqtt_pub_ticks, 189 | mqtt_config.pub_interval * 1000 / portTICK_PERIOD_MS); 190 | } 191 | } 192 | 193 | #endif /* UNIT_TEST */ 194 | -------------------------------------------------------------------------------- /main/ts_mqtt.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * Sends MQTT pub request to specified server in 10s interval 9 | */ 10 | void ts_mqtt_pub_task(void *arg); 11 | -------------------------------------------------------------------------------- /main/ts_serial.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include "ts_serial.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "freertos/FreeRTOS.h" 16 | #include "freertos/task.h" 17 | #include "freertos/event_groups.h" 18 | 19 | #include "esp_system.h" 20 | #include "esp_log.h" 21 | #include "ts_client.h" 22 | #include "cJSON.h" 23 | #include "driver/uart.h" 24 | 25 | #include "stm32bl.h" 26 | 27 | static const char *TAG = "ts_ser"; 28 | 29 | 30 | #define PUBMSG_BUF_SIZE (1024) 31 | #define RESP_BUF_SIZE (1024) 32 | #define UART_RX_BUF_SIZE (256) 33 | 34 | EventGroupHandle_t events = NULL; 35 | #define FLAG_AWAITING_RESPONSE (1U << 0) 36 | #define FLAG_RESPONSE_RECEIVED (1U << 1) 37 | #define FLAG_PUBMSG_RECEIVED (1U << 2) 38 | 39 | /* stores incoming publication messages */ 40 | static uint8_t pubmsg_buf[PUBMSG_BUF_SIZE]; 41 | SemaphoreHandle_t pubmsg_buf_lock = NULL; 42 | 43 | /* stores incoming response messages */ 44 | static uint8_t resp_buf[RESP_BUF_SIZE]; 45 | SemaphoreHandle_t resp_buf_lock = NULL; 46 | 47 | SemaphoreHandle_t uart_lock = NULL; 48 | 49 | /* used UART interface */ 50 | static const int uart_num = UART_NUM_2; 51 | 52 | void ts_serial_setup(void) 53 | { 54 | const uart_config_t uart_config = { 55 | .baud_rate = 115200, 56 | .data_bits = UART_DATA_8_BITS, 57 | .parity = UART_PARITY_DISABLE, 58 | .stop_bits = UART_STOP_BITS_1, 59 | .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, 60 | }; 61 | ESP_ERROR_CHECK( 62 | uart_param_config(uart_num, &uart_config)); 63 | ESP_ERROR_CHECK( 64 | uart_set_pin(uart_num, CONFIG_GPIO_UART_TX, CONFIG_GPIO_UART_RX, 65 | UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); 66 | ESP_ERROR_CHECK( 67 | uart_driver_install(uart_num, UART_RX_BUF_SIZE, 0, 0, NULL, 0)); 68 | 69 | /* mutex: expected to be taken and given from same task */ 70 | resp_buf_lock = xSemaphoreCreateMutex(); 71 | 72 | /* binary semaphore: uart task for new messages, other tasks for processing */ 73 | pubmsg_buf_lock = xSemaphoreCreateBinary(); 74 | xSemaphoreGive(pubmsg_buf_lock); 75 | 76 | uart_lock = xSemaphoreCreateBinary(); 77 | xSemaphoreGive(uart_lock); 78 | 79 | events = xEventGroupCreate(); 80 | } 81 | 82 | static inline void terminate_buffer(uint8_t *buf, int pos) 83 | { 84 | if (pos > 0 && buf[pos-1] == '\r') { 85 | buf[pos-1] = '\0'; 86 | } 87 | else { 88 | buf[pos] = '\0'; 89 | } 90 | } 91 | 92 | void ts_serial_rx_task(void *arg) 93 | { 94 | // following two flags indicate in which buffer new characters should be stored 95 | static bool receiving_pubmsg = false; 96 | static bool receiving_resp = false; 97 | 98 | int pos = 0; // stores next free position in currently used buffer 99 | 100 | while (true) { 101 | uint8_t byte; 102 | // wait for incoming characters 103 | while (uart_read_bytes(uart_num, &byte, 1, pdMS_TO_TICKS(50)) == 0) { 104 | // this allows other threads to block UART read access in this thread (e.g. for 105 | // firmware upgrade) 106 | xSemaphoreTake(uart_lock, portMAX_DELAY); 107 | xSemaphoreGive(uart_lock); 108 | } 109 | 110 | if (pos == 0 && byte == '#') { 111 | // only store pub msg if nobody is processing previous message 112 | if (xSemaphoreTake(pubmsg_buf_lock, 0) == pdTRUE) { 113 | xEventGroupClearBits(events, FLAG_PUBMSG_RECEIVED); 114 | receiving_pubmsg = true; 115 | } 116 | } 117 | else if (pos == 0 && byte == ':') { 118 | // only store response if someone is actually waiting for it 119 | if (xEventGroupGetBits(events) & FLAG_AWAITING_RESPONSE) { 120 | xEventGroupClearBits(events, FLAG_AWAITING_RESPONSE); 121 | receiving_resp = true; 122 | } 123 | } 124 | 125 | // \r\n and \n are markers for line end, i.e. command end 126 | // we accept this at any time, even if the buffer is 'full', since 127 | // there is always one last character left for the \0 128 | if (byte == '\n') { 129 | if (receiving_pubmsg) { 130 | terminate_buffer(pubmsg_buf, pos); 131 | xEventGroupSetBits(events, FLAG_PUBMSG_RECEIVED); 132 | xSemaphoreGive(pubmsg_buf_lock); 133 | receiving_pubmsg = false; 134 | //ESP_LOGI("serial", "Received pub message with %d bytes: %s\n", pos, pubmsg_buf); 135 | } 136 | else if (receiving_resp) { 137 | terminate_buffer(resp_buf, pos); 138 | xEventGroupSetBits(events, FLAG_RESPONSE_RECEIVED); 139 | receiving_resp = false; 140 | } 141 | pos = 0; 142 | } 143 | // Fill the buffer up to all but 1 character (the last character is reserved for '\0') 144 | // Characters beyond the size of the buffer are dropped. 145 | else if (receiving_pubmsg && pos < (sizeof(pubmsg_buf) - 1)) { 146 | pubmsg_buf[pos++] = byte; 147 | } 148 | else if (receiving_resp && pos < (sizeof(resp_buf) - 1)) { 149 | resp_buf[pos++] = byte; 150 | } 151 | else { 152 | // increase to make sure that pos != 0 for : and # that are not at beginning of line 153 | //printf("%c", (char)byte); 154 | pos++; 155 | } 156 | } 157 | } 158 | 159 | char *ts_serial_pubmsg(int timeout_ms) 160 | { 161 | if (xSemaphoreTake(pubmsg_buf_lock, pdMS_TO_TICKS(timeout_ms)) == pdFALSE) { 162 | ESP_LOGE(TAG, "Could not take semaphore resp_buf_lock"); 163 | return NULL; 164 | } 165 | 166 | if (xEventGroupGetBits(events) & FLAG_PUBMSG_RECEIVED) { 167 | return (char *)pubmsg_buf; 168 | } 169 | else { 170 | xSemaphoreGive(pubmsg_buf_lock); 171 | return NULL; 172 | } 173 | } 174 | 175 | void ts_serial_pubmsg_clear() 176 | { 177 | xEventGroupClearBits(events, FLAG_PUBMSG_RECEIVED); 178 | xSemaphoreGive(pubmsg_buf_lock); 179 | } 180 | 181 | int ts_serial_request(char *req, int timeout_ms) 182 | { 183 | if (xSemaphoreTake(resp_buf_lock, pdMS_TO_TICKS(timeout_ms)) == pdFALSE) { 184 | ESP_LOGE(TAG, "Could not take semaphore resp_buf_lock"); 185 | return ESP_FAIL; 186 | } 187 | 188 | xEventGroupSetBits(events, FLAG_AWAITING_RESPONSE); 189 | uart_write_bytes(uart_num, req, strlen(req)); 190 | 191 | return ESP_OK; 192 | } 193 | 194 | char *ts_serial_response(int timeout_ms) 195 | { 196 | EventBits_t ret = xEventGroupWaitBits(events, FLAG_RESPONSE_RECEIVED, pdTRUE, pdTRUE, 197 | pdMS_TO_TICKS(timeout_ms)); 198 | 199 | if (ret & FLAG_RESPONSE_RECEIVED) { 200 | return (char *)resp_buf; 201 | } 202 | else { 203 | return NULL; 204 | } 205 | } 206 | 207 | void ts_serial_response_clear() 208 | { 209 | xEventGroupClearBits(events, FLAG_RESPONSE_RECEIVED); 210 | xSemaphoreGive(resp_buf_lock); 211 | } 212 | 213 | // can_address and request length is not needed here, but we need the same signature 214 | // as CAN send 215 | char *ts_serial_send(uint8_t *req, uint32_t query_size, uint8_t can_address, uint32_t *block_len) 216 | { 217 | if ((char *) req == NULL) { 218 | ESP_LOGE(TAG, "Got invalid parameter"); 219 | return NULL; 220 | } 221 | 222 | if (ts_serial_request((char *) req, 200) != ESP_OK) { 223 | ESP_LOGE(TAG, "Request failed: %s", (char *) req); 224 | return NULL; 225 | } 226 | 227 | char *buf = ts_serial_response(200); 228 | if (buf == NULL) { 229 | ESP_LOGE(TAG, "Response failed"); 230 | ts_serial_response_clear(); 231 | return NULL; 232 | } 233 | 234 | char *resp = (char *) heap_caps_malloc(strlen(buf)+1, MALLOC_CAP_8BIT); 235 | strcpy(resp, buf); 236 | ts_serial_response_clear(); 237 | return resp; 238 | } 239 | 240 | int ts_serial_scan_device_info(TSDevice *device) 241 | { 242 | char req[7]= "?info\n\0"; 243 | // First request mostly fails, so we request it twice 244 | if (ts_serial_request(req, 500) == ESP_FAIL) { 245 | ESP_LOGE(TAG, "Could not scan for devices on serial adapter"); 246 | } 247 | 248 | char *resp = ts_serial_response(500); 249 | TSResponse res; 250 | res.block = resp != NULL ? resp : ""; 251 | int status = ts_serial_resp_status(&res); 252 | if (status != TS_STATUS_CONTENT) { 253 | ESP_LOGE(TAG, "Could not retrieve device information: Code %d", status); 254 | ts_serial_response_clear(); 255 | } 256 | 257 | ts_serial_response_clear(); 258 | 259 | if (ts_serial_request(req, 500) == ESP_FAIL) { 260 | ESP_LOGE(TAG, "Could not scan for devices on serial adapter"); 261 | return -1; 262 | 263 | } 264 | 265 | resp = ts_serial_response(500); 266 | res.block = resp != NULL ? resp : ""; 267 | status = ts_serial_resp_status(&res); 268 | if (status != TS_STATUS_CONTENT) { 269 | ESP_LOGE(TAG, "Could not retrieve device information: Code %d", status); 270 | ts_serial_response_clear(); 271 | return -1; 272 | } 273 | 274 | // shift pointer to data 275 | char *pos = strstr(resp, ". "); 276 | if (pos != NULL) { 277 | resp = pos + 2; 278 | } 279 | 280 | cJSON *json_data = cJSON_Parse(resp); 281 | ts_serial_response_clear(); 282 | 283 | // link functions 284 | device->send = ts_serial_send; 285 | device->build_query = ts_build_query_serial; 286 | device->ts_resp_data = ts_serial_resp_data; 287 | device->ts_resp_status = ts_serial_resp_status; 288 | device->can_address = UINT8_MAX; 289 | int ret = ts_parse_device_info(json_data, device); 290 | cJSON_Delete(json_data); 291 | 292 | return ret; 293 | } 294 | 295 | esp_err_t ts_serial_ota(int flash_size, int page_size) 296 | { 297 | int ret = ESP_FAIL; 298 | uint16_t pages = flash_size * 1024 / page_size; 299 | 300 | ts_serial_request("!dfu/bootloader-stm\n", 100); 301 | ts_serial_response_clear(); 302 | 303 | // prevent further UART access in RX thread 304 | if (xSemaphoreTake(uart_lock, pdMS_TO_TICKS(OTA_UART_LOCK_TIMEOUT)) != pdTRUE ) { 305 | // this is bad and should not happen, as we already started the bootloader now 306 | ESP_LOGE(TAG, "Could not take semaphore uart_lock"); 307 | return ret; 308 | } 309 | 310 | vTaskDelay(pdMS_TO_TICKS(500)); 311 | uart_flush(uart_num); 312 | uart_set_parity(uart_num, UART_PARITY_EVEN); 313 | uint32_t bytes_read = 0; 314 | uint8_t buf[page_size]; 315 | FILE * f = NULL; 316 | int id = 0; 317 | 318 | if (stm32bl_init() != STM32BL_ACK) { 319 | ESP_LOGE(TAG, "Init failed"); 320 | goto out; 321 | } 322 | id = stm32bl_get_id(); 323 | ESP_LOGD(TAG, "STM32BL version: 0x%x", stm32bl_get_version()); 324 | ESP_LOGD(TAG, "STM32BL pid: 0x%x", id); 325 | 326 | f = fopen("/stm_ota/firmware.bin", "r"); 327 | if (f == NULL) { 328 | ESP_LOGE(TAG, "Failed to open STM image"); 329 | goto out; 330 | } 331 | 332 | ESP_LOGD(TAG, "Going to erase %u pages", pages); 333 | if (stm32bl_erase_all(pages) != ESP_OK) { 334 | ESP_LOGE(TAG, "Mass erase failed"); 335 | goto out; 336 | } 337 | uint32_t address = STM32_FLASH_START_ADDR; 338 | 339 | // The maximum block the stm bootloader can handle is 256 bytes 340 | uint32_t block_size = 128; 341 | while (true) { 342 | bytes_read = fread(buf, 1, block_size, f); 343 | if (bytes_read == EOF || bytes_read == 0) { 344 | ESP_LOGD(TAG, "Reading and sending of firmware file finished. Wrote %d bytes", address - STM32_FLASH_START_ADDR); 345 | ret = ESP_OK; 346 | goto out; 347 | } 348 | if (stm32bl_write(buf, bytes_read, address) != STM32BL_ACK) { 349 | ESP_LOGE(TAG, "Writing failed"); 350 | goto out; 351 | } 352 | address += bytes_read; 353 | }; 354 | 355 | out: 356 | if (f != NULL) { 357 | fclose(f); 358 | } 359 | 360 | // wait a bit to let the stm32 finish writing 361 | vTaskDelay(pdMS_TO_TICKS(1000)); 362 | ret = stm32bl_reset_device(id); 363 | 364 | uart_set_parity(uart_num, UART_PARITY_DISABLE); 365 | xSemaphoreGive(uart_lock); 366 | return ret; 367 | } 368 | 369 | #endif //UINIT_TEST 370 | -------------------------------------------------------------------------------- /main/ts_serial.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef SERIAL_H_ 8 | #define SERIAL_H_ 9 | 10 | #include 11 | #include "esp_err.h" 12 | #include 13 | #include 14 | #include 15 | 16 | #define OTA_UART_LOCK_TIMEOUT 500 17 | 18 | /** 19 | * Initiate the UART interface, event groups and semaphores. 20 | * 21 | * Must be called before starting the task. 22 | */ 23 | void ts_serial_setup(void); 24 | 25 | /** 26 | * Thread listening to UART interface, needs to be spawned from main. 27 | */ 28 | void ts_serial_rx_task(void *arg); 29 | 30 | /** 31 | * Returns latest pub message received on the interface and waits until timeout if receiving is 32 | * currently in progress 33 | * 34 | * \param timeout_ms Timeout in milliseconds 35 | * 36 | * \returns Pointer to received message (terminated string) or NULL if timed out 37 | */ 38 | char *ts_serial_pubmsg(int timeout_ms); 39 | 40 | /** 41 | * Release buffer after processing to receive new messages 42 | * 43 | * This must be called after processing of a successfully received pubmsg is finished. 44 | */ 45 | void ts_serial_pubmsg_clear(void); 46 | 47 | /** 48 | * Send request and lock response buffer 49 | * 50 | * \param req Request buffer (null-terminated) 51 | * \param timeout_ms Maximum timeout for getting buffer mutex 52 | * 53 | * \returns ESP_OK if request was sent, ESP_FAIL otherwise 54 | */ 55 | int ts_serial_request(char *req, int timeout_ms); 56 | 57 | /** 58 | * Receive response from buffer 59 | * 60 | * This function should be called after ts_serial_request 61 | * 62 | * \param timeout_ms Timeout in milliseconds 63 | * 64 | * \returns Pointer to received message (terminated string) or NULL if timed out 65 | */ 66 | char *ts_serial_response(int timeout_ms); 67 | 68 | /** 69 | * Release response buffer for new requests 70 | * 71 | * This must be called after processing of a successfully received response is finished. 72 | */ 73 | void ts_serial_response_clear(void); 74 | 75 | /** 76 | * Scan for device on the serial connection 77 | * 78 | * \param device Pointer to allocated struct of type ts_device 79 | */ 80 | int ts_serial_scan_device_info(TSDevice *device); 81 | 82 | /** 83 | * Start over-the-air firmware update (OTA) 84 | * 85 | * Uses the STM32 bootloader via UART. The binary must be stored in the designated spiffs partition. 86 | * 87 | * \param flash_size The flash size for the target MCU in bytes 88 | * 89 | * \param page_size The size of each page in bytes 90 | */ 91 | esp_err_t ts_serial_ota(int flash_size, int page_size); 92 | 93 | #endif /* SERIAL_H_ */ 94 | -------------------------------------------------------------------------------- /main/web_fs.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include 10 | #include 11 | 12 | #include "sdkconfig.h" 13 | #include "driver/gpio.h" 14 | #include "esp_vfs_fat.h" 15 | #include "esp_spiffs.h" 16 | #include "nvs_flash.h" 17 | 18 | static const char* TAG = "web_fs"; 19 | 20 | static esp_err_t check_response(esp_err_t ret) 21 | { 22 | if (ret != ESP_OK) { 23 | if (ret == ESP_FAIL) { 24 | ESP_LOGE(TAG, "Failed to mount or format filesystem"); 25 | } else if (ret == ESP_ERR_NOT_FOUND) { 26 | ESP_LOGE(TAG, "Failed to find SPIFFS partition"); 27 | } else { 28 | ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); 29 | } 30 | return ESP_FAIL; 31 | } else { 32 | return ESP_OK; 33 | } 34 | } 35 | 36 | esp_err_t init_fs(void) 37 | { 38 | esp_vfs_spiffs_conf_t www_conf = { 39 | .base_path = "/www", 40 | .partition_label = "website", 41 | .max_files = 5, 42 | .format_if_mount_failed = false 43 | }; 44 | if(check_response(esp_vfs_spiffs_register(&www_conf)) != ESP_OK) { 45 | return ESP_FAIL; 46 | }; 47 | 48 | esp_vfs_spiffs_conf_t ota_conf = { 49 | .base_path = "/stm_ota", 50 | .partition_label = "stm_ota", 51 | .max_files = 1, 52 | .format_if_mount_failed = true 53 | }; 54 | if(check_response(esp_vfs_spiffs_register(&ota_conf)) != ESP_OK) { 55 | return ESP_FAIL; 56 | }; 57 | 58 | size_t total = 0, used = 0; 59 | esp_err_t ret = esp_spiffs_info("website", &total, &used); 60 | if (ret != ESP_OK) { 61 | ESP_LOGE(TAG, "Failed to get SPIFFS partition information for website (%s)", esp_err_to_name(ret)); 62 | } else { 63 | ESP_LOGI(TAG, "Partition size for website: total: %d, used: %d", total, used); 64 | } 65 | 66 | ret = esp_spiffs_info("stm_ota", &total, &used); 67 | if (ret != ESP_OK) { 68 | ESP_LOGE(TAG, "Failed to get SPIFFS partition information for ota (%s)", esp_err_to_name(ret)); 69 | } else { 70 | ESP_LOGI(TAG, "Partition size ota: total: %d, used: %d", total, used); 71 | } 72 | 73 | return ESP_OK; 74 | } 75 | 76 | #endif // UNIT_TEST -------------------------------------------------------------------------------- /main/web_fs.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "esp_err.h" 8 | 9 | /** 10 | * Mount SPIFSS containing website data at /www 11 | */ 12 | esp_err_t init_fs(void); 13 | -------------------------------------------------------------------------------- /main/web_server.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "esp_err.h" 8 | 9 | esp_err_t start_web_server(const char *base_path); 10 | -------------------------------------------------------------------------------- /main/wifi.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include "wifi.h" 10 | 11 | #include 12 | #include "sdkconfig.h" 13 | #include "esp_event.h" 14 | #include "esp_wifi.h" 15 | #include "esp_log.h" 16 | #include "driver/gpio.h" 17 | #include "freertos/FreeRTOS.h" 18 | #include "freertos/task.h" 19 | #include "freertos/event_groups.h" 20 | #include "lwip/err.h" 21 | #include "lwip/sys.h" 22 | 23 | #include "data_nodes.h" 24 | 25 | extern GeneralConfig general_config; 26 | 27 | #define GOT_IPV4_BIT BIT(0) 28 | #define GOT_IPV6_BIT BIT(1) 29 | 30 | #ifdef CONFIG_CONNECT_IPV6 31 | #define CONNECTED_BITS (GOT_IPV4_BIT | GOT_IPV6_BIT) 32 | #else 33 | #define CONNECTED_BITS (GOT_IPV4_BIT) 34 | #endif 35 | 36 | static EventGroupHandle_t s_connect_event_group; 37 | static ip4_addr_t s_ip_addr; 38 | static const char *s_connection_name; 39 | 40 | #ifdef CONFIG_CONNECT_IPV6 41 | static ip6_addr_t s_ipv6_addr; 42 | #endif 43 | 44 | static const char *TAG = "wifi"; 45 | 46 | /* set up connection, Wi-Fi or Ethernet */ 47 | static void start(void); 48 | 49 | /* tear down connection, release resources */ 50 | static void stop(void); 51 | 52 | static void on_got_ip(void *arg, esp_event_base_t event_base, 53 | int32_t event_id, void *event_data) 54 | { 55 | ESP_LOGI(TAG, "Got IP event!"); 56 | ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; 57 | memcpy(&s_ip_addr, &event->ip_info.ip, sizeof(s_ip_addr)); 58 | xEventGroupSetBits(s_connect_event_group, GOT_IPV4_BIT); 59 | } 60 | 61 | #ifdef CONFIG_CONNECT_IPV6 62 | 63 | static void on_got_ipv6(void *arg, esp_event_base_t event_base, 64 | int32_t event_id, void *event_data) 65 | { 66 | ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data; 67 | if (event->esp_netif != s_esp_netif) { 68 | ESP_LOGD(TAG, "Got IPv6 from another netif: ignored"); 69 | return; 70 | } 71 | ESP_LOGI(TAG, "Got IPv6 event!"); 72 | memcpy(&s_ipv6_addr, &event->ip6_info.ip, sizeof(s_ipv6_addr)); 73 | xEventGroupSetBits(s_connect_event_group, GOT_IPV6_BIT); 74 | } 75 | 76 | #endif // CONFIG_CONNECT_IPV6 77 | 78 | esp_err_t wifi_connect(void) 79 | { 80 | if (s_connect_event_group != NULL) { 81 | return ESP_ERR_INVALID_STATE; 82 | } 83 | s_connect_event_group = xEventGroupCreate(); 84 | start(); 85 | ESP_ERROR_CHECK(esp_register_shutdown_handler(&stop)); 86 | ESP_LOGI(TAG, "Waiting for IP"); 87 | xEventGroupWaitBits(s_connect_event_group, CONNECTED_BITS, true, true, portMAX_DELAY); 88 | ESP_LOGI(TAG, "Connected to %s", s_connection_name); 89 | ESP_LOGI(TAG, "IPv4 address: " IPSTR, IP2STR(&s_ip_addr)); 90 | #ifdef CONFIG_CONNECT_IPV6 91 | ESP_LOGI(TAG, "IPv6 address: " IPV6STR, IPV62STR(s_ipv6_addr)); 92 | #endif 93 | 94 | return ESP_OK; 95 | } 96 | 97 | esp_err_t wifi_disconnect(void) 98 | { 99 | if (s_connect_event_group == NULL) { 100 | return ESP_ERR_INVALID_STATE; 101 | } 102 | vEventGroupDelete(s_connect_event_group); 103 | s_connect_event_group = NULL; 104 | stop(); 105 | ESP_LOGI(TAG, "Disconnected from %s", s_connection_name); 106 | s_connection_name = NULL; 107 | return ESP_OK; 108 | } 109 | 110 | static void on_wifi_disconnect(void *arg, esp_event_base_t event_base, 111 | int32_t event_id, void *event_data) 112 | { 113 | ESP_LOGI(TAG, "Wi-Fi disconnected, trying to reconnect..."); 114 | esp_err_t err = esp_wifi_connect(); 115 | if (err == ESP_ERR_WIFI_NOT_STARTED) { 116 | return; 117 | } 118 | ESP_ERROR_CHECK(err); 119 | } 120 | 121 | #ifdef CONFIG_CONNECT_IPV6 122 | 123 | static void on_wifi_connect(void *esp_netif, esp_event_base_t event_base, 124 | int32_t event_id, void *event_data) 125 | { 126 | esp_netif_create_ip6_linklocal(esp_netif); 127 | } 128 | 129 | #endif // CONFIG_CONNECT_IPV6 130 | 131 | static void start(void) 132 | { 133 | esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta(); 134 | assert(sta_netif); 135 | 136 | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 137 | ESP_ERROR_CHECK(esp_wifi_init(&cfg)); 138 | 139 | ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &on_wifi_disconnect, NULL)); 140 | ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &on_got_ip, NULL)); 141 | #ifdef CONFIG_EXAMPLE_CONNECT_IPV6 142 | ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &on_wifi_connect, NULL)); 143 | ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &on_got_ipv6, NULL)); 144 | #endif 145 | 146 | ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); 147 | 148 | wifi_config_t wifi_config; 149 | bzero(&wifi_config, sizeof(wifi_config_t)); 150 | memcpy(wifi_config.sta.ssid, general_config.wifi_ssid, sizeof(wifi_config.sta.ssid)); 151 | memcpy(wifi_config.sta.password, general_config.wifi_password, sizeof(wifi_config.sta.password)); 152 | 153 | ESP_LOGI(TAG, "Connecting to %s...", wifi_config.sta.ssid); 154 | ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); 155 | ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); 156 | ESP_ERROR_CHECK(esp_wifi_start()); 157 | 158 | ESP_ERROR_CHECK(esp_netif_set_hostname(sta_netif, general_config.mdns_hostname)); 159 | 160 | ESP_ERROR_CHECK(esp_wifi_connect()); 161 | s_connection_name = general_config.wifi_ssid; 162 | } 163 | 164 | static void stop(void) 165 | { 166 | ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &on_wifi_disconnect)); 167 | ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &on_got_ip)); 168 | #ifdef CONFIG_EXAMPLE_CONNECT_IPV6 169 | ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_GOT_IP6, &on_got_ipv6)); 170 | ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &on_wifi_connect)); 171 | #endif 172 | ESP_ERROR_CHECK(esp_wifi_stop()); 173 | ESP_ERROR_CHECK(esp_wifi_deinit()); 174 | } 175 | 176 | #endif //UNIT_TEST -------------------------------------------------------------------------------- /main/wifi.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #pragma once 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | #include "esp_err.h" 14 | 15 | /** 16 | * @brief Configure Wi-Fi, connect, wait for IP 17 | * 18 | * Simple function to connect to WiFi based on ESP-IDF examples 19 | * 20 | * @return ESP_OK on successful connection 21 | */ 22 | esp_err_t wifi_connect(void); 23 | 24 | /** 25 | * Counterpart to example_connect, de-initializes Wi-Fi 26 | */ 27 | esp_err_t wifi_disconnect(void); 28 | 29 | #ifdef __cplusplus 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /partitions.csv: -------------------------------------------------------------------------------- 1 | # ESP-IDF Partition Table 2 | # Name, Type, SubType, Offset, Size, Flags 3 | nvs, data, nvs, 0x9000, 0x4000, 4 | otadata, data, ota, 0xd000, 0x2000, 5 | phy_init, data, phy, 0xf000, 0x1000, 6 | ota_0, app, ota_0, 0x10000, 1664K, 7 | ota_1, app, ota_1, , 1664K, 8 | website, data, spiffs, , 512K, 9 | stm_ota, data, spiffs, , 128K, 10 | config, data, nvs, , 20K, 11 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | [platformio] 6 | 7 | # Use standard ESP-IDF directory name 8 | src_dir = main 9 | test_dir = test 10 | 11 | # Uncomment to select one of the following boards 12 | default_envs = esp32-edge 13 | # esp32-edge 14 | 15 | [env] 16 | #platform = espressif32 17 | platform = https://github.com/platformio/platform-espressif32.git 18 | framework = espidf 19 | monitor_speed = 115200 20 | monitor_filters = colorize 21 | upload_port = /dev/ttyUSB0 22 | build_flags = 23 | board_build.partitions = partitions.csv 24 | board_build.embed_files = main/certs/isrgrootx1.pem 25 | 26 | check_tool = cppcheck, clangtidy 27 | check_flags = 28 | cppcheck: --enable=warning,style,performance,portability,information,missingInclude -j 7 --inline-suppr 29 | clangtidy: --checks=-*,cert-*,clang-analyzer-*,bugprone-*,misc-*,performance-*,readability-*,-readability-magic-numbers,-cert-err58-cpp 30 | 31 | [env:esp32-edge] 32 | build_flags = ${env.build_flags} 33 | board = esp32thing 34 | 35 | # below libraries are only required for unit tests 36 | lib_ignore = tinycbor, cjson 37 | 38 | [env:unit_test] 39 | platform = native 40 | framework = 41 | #board = esp32thing 42 | 43 | build_flags = 44 | -std=gnu++17 45 | -Wall 46 | -D UNIT_TEST 47 | -D __STDC_FORMAT_MACROS 48 | -D COMMIT_HASH=\"test\" 49 | -I test 50 | # include src directory (otherwise unit-tests will only include lib directory) 51 | test_build_project_src = true 52 | -------------------------------------------------------------------------------- /sdkconfig.defaults: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # Most boards have 40 MHz XTAL frequency, but e.g. ESP32thing has 26 MHz. Auto supports all boards. 6 | CONFIG_ESP32_XTAL_FREQ_AUTO=y 7 | 8 | # Partition table with SPIFFS 9 | CONFIG_PARTITION_TABLE_CUSTOM=y 10 | CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" 11 | 12 | # Increase default flash size 13 | CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y 14 | CONFIG_ESPTOOLPY_FLASHSIZE="4MB" 15 | 16 | # BLE for provisioning 17 | CONFIG_BT_ENABLED=y 18 | CONFIG_BT_NIMBLE_ENABLED=y 19 | 20 | # Provisioning is not used if SSID is set below 21 | CONFIG_WIFI_SSID="" 22 | CONFIG_WIFI_PASSWORD="" 23 | 24 | # OpenEnergyMonitor Emoncms 25 | CONFIG_EMONCMS=y 26 | CONFIG_EMONCMS_HOST="emoncms.org" 27 | CONFIG_EMONCMS_PORT="80" 28 | CONFIG_EMONCMS_URL="/emoncms/input/post" 29 | CONFIG_EMONCMS_APIKEY="your-api-code" 30 | CONFIG_EMONCMS_NODE_MPPT="mppt" 31 | CONFIG_EMONCMS_NODE_BMS="bms" 32 | CONFIG_EMONCMS_NODE_SERIAL="serial" 33 | 34 | # 35 | # HTTP Server 36 | # 37 | CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 38 | # UEXT ESP32 pin definitions 39 | 40 | CONFIG_GPIO_LED=27 41 | 42 | CONFIG_THINGSET_SERIAL=y 43 | CONFIG_GPIO_UART_RX=16 44 | CONFIG_GPIO_UART_TX=17 45 | 46 | CONFIG_THINGSET_CAN=y 47 | CONFIG_GPIO_CAN_RX=32 48 | CONFIG_GPIO_CAN_TX=33 49 | CONFIG_GPIO_CAN_STB=25 50 | 51 | -------------------------------------------------------------------------------- /test/esp_err.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #define ESP_LOGE(fmt, ...) {}; 8 | #define ESP_LOGD(fmt, ...) {}; 9 | -------------------------------------------------------------------------------- /test/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "./tests.h" 8 | #include 9 | 10 | #ifdef __WIN32__ 11 | 12 | // The below lines are added to fix unit test compilation error in PIO. 13 | // The Unit Test Framework uses these functions like a constructor/destructor. 14 | // We can just define two empty functions with these names. 15 | void setUp (void) {} 16 | void tearDown (void) {} 17 | 18 | #endif 19 | 20 | int main() 21 | { 22 | ts_client_tests(); 23 | 24 | #ifdef CUSTOM_TESTS 25 | custom_tests(); 26 | #endif 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /test/test_ts_client.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "tests.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | void parse_uri_no_subnodes(void) 16 | { 17 | const char *uri = "someID/info"; 18 | TSUriElems params; 19 | params.ts_payload = ""; 20 | ts_parse_uri(uri, ¶ms); 21 | TEST_ASSERT_EQUAL_INT(1, params.ts_list_subnodes); 22 | TEST_ASSERT_EQUAL_STRING("info", params.ts_target_node); 23 | TEST_ASSERT_EQUAL_STRING("someID", params.ts_device_id); 24 | TEST_ASSERT_EQUAL_STRING("", params.ts_payload); 25 | 26 | } 27 | 28 | void parse_uri_with_subnodes(void) 29 | { 30 | const char *uri = "someID/info/"; 31 | TSUriElems params; 32 | params.ts_payload = ""; 33 | ts_parse_uri(uri, ¶ms); 34 | TEST_ASSERT_EQUAL_INT(0, params.ts_list_subnodes); 35 | TEST_ASSERT_EQUAL_STRING("info/", params.ts_target_node); 36 | TEST_ASSERT_EQUAL_STRING("someID", params.ts_device_id); 37 | TEST_ASSERT_EQUAL_STRING("", params.ts_payload); 38 | } 39 | 40 | void parse_uri_id_only(void) 41 | { 42 | const char *uri = "someID"; 43 | TSUriElems params; 44 | params.ts_payload = ""; 45 | ts_parse_uri(uri, ¶ms); 46 | TEST_ASSERT_EQUAL_INT(1, params.ts_list_subnodes); 47 | TEST_ASSERT_EQUAL_STRING("", params.ts_target_node); 48 | TEST_ASSERT_EQUAL_STRING("someID", params.ts_device_id); 49 | TEST_ASSERT_EQUAL_STRING("", params.ts_payload); 50 | } 51 | 52 | void parse_uri_empty(void) 53 | { 54 | const char *uri = ""; 55 | TSUriElems params; 56 | params.ts_payload = ""; 57 | ts_parse_uri(uri, ¶ms); 58 | TEST_ASSERT_EQUAL_INT(-1, params.ts_list_subnodes); 59 | TEST_ASSERT_EQUAL_STRING(NULL, params.ts_target_node); 60 | TEST_ASSERT_EQUAL_STRING(NULL, params.ts_device_id); 61 | TEST_ASSERT_EQUAL_STRING("", params.ts_payload); 62 | } 63 | 64 | void parse_uri_null(void) 65 | { 66 | const char *uri = NULL; 67 | TSUriElems params; 68 | params.ts_payload = ""; 69 | ts_parse_uri(uri, ¶ms); 70 | TEST_ASSERT_EQUAL_INT(-1, params.ts_list_subnodes); 71 | TEST_ASSERT_EQUAL_STRING(NULL, params.ts_target_node); 72 | TEST_ASSERT_EQUAL_STRING(NULL, params.ts_device_id); 73 | TEST_ASSERT_EQUAL_STRING("", params.ts_payload); 74 | } 75 | 76 | void parse_uri_with_payload(void) 77 | { 78 | const char *uri = "someID/output"; 79 | TSUriElems params; 80 | params.ts_payload = "{\"Bat_V\"}"; 81 | ts_parse_uri(uri, ¶ms); 82 | TEST_ASSERT_EQUAL_INT(1, params.ts_list_subnodes); 83 | TEST_ASSERT_EQUAL_STRING("output", params.ts_target_node); 84 | TEST_ASSERT_EQUAL_STRING("someID", params.ts_device_id); 85 | TEST_ASSERT_EQUAL_STRING("{\"Bat_V\"}", params.ts_payload); 86 | } 87 | 88 | void ts_build_query_get(void) 89 | { 90 | TSUriElems params; 91 | char *query; 92 | uint32_t length; 93 | params.ts_payload = NULL; 94 | params.ts_target_node = "output"; 95 | params.ts_list_subnodes = 1; 96 | query = ts_build_query_serial(TS_GET, ¶ms, &length); 97 | TEST_ASSERT_EQUAL_STRING("?output\n", query); 98 | free(query); 99 | } 100 | 101 | void ts_build_query_get_subnodes(void) 102 | { 103 | TSUriElems params; 104 | char *query; 105 | uint32_t length; 106 | params.ts_payload = NULL; 107 | params.ts_target_node = "output/"; 108 | params.ts_list_subnodes = 0; 109 | query = ts_build_query_serial(TS_GET, ¶ms, &length); 110 | TEST_ASSERT_EQUAL_STRING("?output/\n", query); 111 | free(query); 112 | } 113 | 114 | void ts_build_query_get_root(void) 115 | { 116 | TSUriElems params; 117 | char *query; 118 | uint32_t length; 119 | params.ts_payload = NULL; 120 | params.ts_target_node = ""; 121 | params.ts_list_subnodes = 0; 122 | query = ts_build_query_serial(TS_GET, ¶ms, &length); 123 | TEST_ASSERT_EQUAL_STRING("?/\n", query); 124 | free(query); 125 | } 126 | 127 | void ts_build_query_exec(void) 128 | { 129 | TSUriElems params; 130 | char *query; 131 | uint32_t length; 132 | params.ts_payload = NULL; 133 | params.ts_target_node = "auth"; 134 | params.ts_list_subnodes = 1; 135 | query = ts_build_query_serial(TS_POST, ¶ms, &length); 136 | TEST_ASSERT_EQUAL_STRING("!auth\n", query); 137 | free(query); 138 | } 139 | 140 | void ts_build_query_post(void) 141 | { 142 | TSUriElems params; 143 | char *query; 144 | uint32_t length; 145 | params.ts_payload = "\"Bat_V\""; 146 | params.ts_target_node = "pub/serial/IDs"; 147 | params.ts_list_subnodes = 1; 148 | query = ts_build_query_serial(TS_POST, ¶ms, &length); 149 | TEST_ASSERT_EQUAL_STRING("+pub/serial/IDs \"Bat_V\"\n", query); 150 | free(query); 151 | } 152 | 153 | void ts_build_query_null(void) 154 | { 155 | TSUriElems *params = NULL; 156 | char *query; 157 | uint32_t length; 158 | query = ts_build_query_serial(TS_GET, params, &length); 159 | TEST_ASSERT_EQUAL_STRING(NULL, query); 160 | } 161 | 162 | void ts_build_query_false_method(void) 163 | { 164 | TSUriElems params; 165 | char *query; 166 | uint32_t length; 167 | params.ts_payload = NULL; 168 | params.ts_target_node = "output"; 169 | params.ts_list_subnodes = 1; 170 | query = ts_build_query_serial(0, ¶ms, &length); 171 | TEST_ASSERT_EQUAL_STRING("", query); 172 | free(query); 173 | } 174 | 175 | void ts_build_query_payload(void) 176 | { 177 | TSUriElems params; 178 | char *query; 179 | uint32_t length; 180 | params.ts_payload = "[\"Bat_V\", \"Bat_A\", \"Bat_W\", \"Bat_degC\", \"BatTempExt\"]"; 181 | params.ts_target_node = "output"; 182 | params.ts_list_subnodes = 1; 183 | query = ts_build_query_serial(TS_GET, ¶ms, &length); 184 | TEST_ASSERT_EQUAL_STRING("?output [\"Bat_V\", \"Bat_A\", \"Bat_W\", \"Bat_degC\", \"BatTempExt\"]\n", query); 185 | free(query); 186 | } 187 | 188 | void ts_build_bin_query_post(void) 189 | { 190 | TSUriElems params; 191 | uint8_t *query; 192 | uint32_t length; 193 | params.ts_payload = "[\"Bat_V\", \"Bat_A\", \"Bat_W\", \"Bat_degC\", \"BatTempExt\"]"; 194 | params.ts_target_node = "pub/serial/IDs"; 195 | params.ts_list_subnodes = 1; 196 | query = ts_build_query_bin(TS_POST, ¶ms, &length); 197 | int offset = 1; 198 | 199 | TEST_ASSERT_EQUAL(TS_POST, query[0]); 200 | uint8_t expected_node[] = {0x6E, 0x70, 0x75, 0x62, 201 | 0x2F, 0x73, 0x65 , 0x72, 202 | 0x69, 0x61, 0x6C, 0x2F, 203 | 0x49, 0x44, 0x73}; 204 | for (int i = 0; i < sizeof(expected_node); i++) { 205 | offset++; 206 | TEST_ASSERT_EQUAL(expected_node[i], query[i+1]); 207 | } 208 | uint8_t expected_payload[] = {0x85, 0x65, 0x42, 0x61, 0x74, 0x5F, 0x56, 0x65, 209 | 0x42, 0x61, 0x74, 0x5F, 0x41, 0x65, 0x42, 0x61, 210 | 0x74, 0x5F, 0x57, 0x68, 0x42, 0x61, 0x74, 0x5F, 211 | 0x64, 0x65, 0x67, 0x43, 0x6A, 0x42, 0x61, 0x74, 212 | 0x54, 0x65, 0x6D, 0x70, 0x45, 0x78, 0x74}; 213 | 214 | for (int i = 0; i < sizeof(expected_payload); i++) { 215 | TEST_ASSERT_EQUAL(expected_payload[i], query[i+offset]); 216 | } 217 | TEST_ASSERT_EQUAL(offset + sizeof(expected_payload), length); 218 | 219 | free(query); 220 | } 221 | 222 | void ts_build_bin_query_with_object(void) 223 | { 224 | TSUriElems params; 225 | uint8_t *query; 226 | uint32_t length; 227 | params.ts_payload = "{\"loadEn\": true}"; 228 | params.ts_target_node = "config"; 229 | params.ts_list_subnodes = 1; 230 | query = ts_build_query_bin(TS_POST, ¶ms, &length); 231 | int offset = 1; 232 | /* 233 | for (int i = 0; i < strlen_null((char *) query); i++) { 234 | printf("%02X", query[i]); 235 | } 236 | printf("\n"); 237 | */ 238 | 239 | TEST_ASSERT_EQUAL(TS_POST, query[0]); 240 | 241 | uint8_t expected_node[] = {0x66, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67}; 242 | for (int i = 0; i < sizeof(expected_node); i++) { 243 | offset++; 244 | TEST_ASSERT_EQUAL(expected_node[i], query[i+1]); 245 | } 246 | uint8_t expected_payload[] = {0xA1, 0x66, 0x6C, 0x6F, 0x61, 0x64, 0x45, 0x6E, 0xF5}; 247 | 248 | for (int i = 0; i < sizeof(expected_payload); i++) { 249 | TEST_ASSERT_EQUAL(expected_payload[i], query[i+offset]); 250 | } 251 | TEST_ASSERT_EQUAL(offset + sizeof(expected_payload), length); 252 | 253 | free(query); 254 | } 255 | 256 | void ts_get_json_from_valid_cbor(void) 257 | { 258 | uint8_t node[] = {0x66, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67}; 259 | char * string = cbor2json(node, sizeof(node)); 260 | TEST_ASSERT_EQUAL_STRING("\"config\"", string); 261 | free(string); 262 | 263 | } 264 | 265 | 266 | void ts_client_tests() 267 | { 268 | UNITY_BEGIN(); 269 | RUN_TEST(parse_uri_no_subnodes); 270 | RUN_TEST(parse_uri_with_subnodes); 271 | RUN_TEST(parse_uri_id_only); 272 | RUN_TEST(parse_uri_empty); 273 | RUN_TEST(parse_uri_null); 274 | RUN_TEST(parse_uri_with_payload); 275 | 276 | RUN_TEST(ts_build_query_get); 277 | RUN_TEST(ts_build_query_get_subnodes); 278 | RUN_TEST(ts_build_query_get_root); 279 | RUN_TEST(ts_build_query_exec); 280 | RUN_TEST(ts_build_query_post); 281 | RUN_TEST(ts_build_query_null); 282 | RUN_TEST(ts_build_query_payload); 283 | RUN_TEST(ts_build_query_false_method); 284 | 285 | RUN_TEST(ts_build_bin_query_post); 286 | RUN_TEST(ts_build_bin_query_with_object); 287 | RUN_TEST(ts_get_json_from_valid_cbor); 288 | UNITY_END(); 289 | } 290 | -------------------------------------------------------------------------------- /test/tests.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | 9 | void ts_client_tests(); 10 | 11 | // activate this via build_flags in platformio.ini or custom.ini 12 | #ifdef CUSTOM_TESTS 13 | void custom_tests(); 14 | #endif 15 | -------------------------------------------------------------------------------- /webapp/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential' 8 | ], 9 | rules: { 10 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 11 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 12 | }, 13 | parserOptions: { 14 | parser: 'babel-eslint' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | .vscode 5 | 6 | node_modules 7 | dist 8 | stats.json 9 | -------------------------------------------------------------------------------- /webapp/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp32-edge-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "chart.js": "^2.9.4", 13 | "core-js": "^3.6.5", 14 | "vue": "^2.6.11", 15 | "vue-chartjs": "^3.5.1", 16 | "vue-router": "^3.4.9", 17 | "vuetify": "^2.3.18", 18 | "vuex": "^3.6.0" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "~4.5.0", 22 | "@vue/cli-plugin-eslint": "~4.5.0", 23 | "@vue/cli-service": "~4.5.0", 24 | "babel-eslint": "^10.1.0", 25 | "compression-webpack-plugin": "^6.0.5", 26 | "eslint": "^6.7.2", 27 | "eslint-plugin-vue": "^6.2.2", 28 | "sass": "^1.19.0", 29 | "sass-loader": "^8.0.0", 30 | "vue-cli-plugin-vuetify": "^2.0.7", 31 | "vue-template-compiler": "^2.6.11", 32 | "vuetify-loader": "^1.3.0" 33 | }, 34 | "eslintConfig": { 35 | "root": true, 36 | "rules": { 37 | "no-console": "off" 38 | }, 39 | "env": { 40 | "node": true 41 | }, 42 | "extends": [ 43 | "plugin:vue/essential", 44 | "eslint:recommended" 45 | ], 46 | "parserOptions": { 47 | "parser": "babel-eslint" 48 | } 49 | }, 50 | "browserslist": [ 51 | "> 1%", 52 | "last 2 versions", 53 | "not dead" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /webapp/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibreSolar/esp32-edge-firmware/f7a7160039ff888db237df6f73454b1874a86918/webapp/public/favicon.ico -------------------------------------------------------------------------------- /webapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Libre Solar ESP32 Edge 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /webapp/src/App.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 104 | -------------------------------------------------------------------------------- /webapp/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibreSolar/esp32-edge-firmware/f7a7160039ff888db237df6f73454b1874a86918/webapp/src/assets/logo.png -------------------------------------------------------------------------------- /webapp/src/components/EspOta.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 145 | -------------------------------------------------------------------------------- /webapp/src/components/LineChart.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /webapp/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import axios from 'axios' 6 | import vuetify from './plugins/vuetify' 7 | 8 | Vue.config.productionTip = false 9 | Vue.prototype.$ajax = axios 10 | 11 | new Vue({ 12 | vuetify, 13 | router, 14 | store, 15 | render: h => h(App) 16 | }).$mount('#app'); 17 | 18 | router.replace('/') 19 | -------------------------------------------------------------------------------- /webapp/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib'; 3 | 4 | Vue.use(Vuetify) 5 | 6 | export default new Vuetify({ 7 | theme: { 8 | dark: false, 9 | themes: { 10 | light: { 11 | primary: '#005e85', 12 | secondary: '#5c9aaf', 13 | accent: '#f5f5f5' 14 | }, 15 | dark: { 16 | primary: '#005e85', 17 | secondary: '#5c9aaf', 18 | accent: '#616161' 19 | } 20 | } 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /webapp/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import store from './store' 4 | import Info from './views/Info.vue' 5 | import Home from './views/Home.vue' 6 | import Live from './views/Live.vue' 7 | import Config from './views/Config.vue' 8 | import IO from './views/IO.vue' 9 | import Ota from './views/Ota.vue' 10 | import DataLog from './views/DataLog.vue' 11 | import ConfigGeneral from './views/ConfigGeneral.vue' 12 | 13 | Vue.use(Router) 14 | 15 | const router = new Router({ 16 | mode: 'abstract', 17 | base: process.env.BASE_URL, 18 | routes: [ 19 | { 20 | path: '/', 21 | name: 'home', 22 | component: Home 23 | }, 24 | { 25 | path: '/info', 26 | name: 'info', 27 | component: Info 28 | }, 29 | { 30 | path: '/live', 31 | name: 'live', 32 | component: Live 33 | }, 34 | { 35 | path: '/config', 36 | name: 'config', 37 | component: Config 38 | }, 39 | { 40 | path: '/io', 41 | name: 'io', 42 | component: IO 43 | }, 44 | { 45 | path: '/data-log', 46 | name: 'data-log', 47 | component: DataLog 48 | }, 49 | { 50 | path: '/ota', 51 | name: 'ota', 52 | component: Ota 53 | }, 54 | { 55 | path: '/esp-config', 56 | name: 'esp-config', 57 | component: ConfigGeneral 58 | } 59 | ] 60 | }) 61 | 62 | export default router 63 | -------------------------------------------------------------------------------- /webapp/src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import axios from 'axios' 4 | import info from "../info.json" 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store({ 9 | state: { 10 | chartData: {}, 11 | chartDataKeys: [], 12 | loading: true, 13 | self: {}, 14 | selfInfo: null, 15 | devices: {}, 16 | activeDevice: "No Devices connected..", 17 | activeDeviceId: "", 18 | info: {}, 19 | thingsetStrings: {}, 20 | isAuthenticated: false, 21 | globAlert: false, 22 | globAlertType: "warning", 23 | globAlertMsg: "" 24 | }, 25 | mutations: { 26 | changeDevice(state, key) { 27 | state.activeDevice = key 28 | state.activeDeviceId = state.devices[key] 29 | if (!state.info[key]) 30 | this.dispatch("getDeviceInfo").then(() => { 31 | this.dispatch("getThingsetStrings") 32 | }) 33 | }, 34 | saveDevices(state, devices) { 35 | state.self = devices['self'] 36 | delete devices['self'] 37 | this.dispatch('getInfoSelf') 38 | if (Object.keys(devices).length > 0) { 39 | state.devices = devices 40 | this.commit('changeDevice', Object.keys(devices)[0]) 41 | state.loading = false 42 | } 43 | }, 44 | saveDeviceInfo(state, deviceInfo) { 45 | state.info[state.activeDeviceId] = deviceInfo 46 | }, 47 | saveThingsetStrings(state, strings) { 48 | state.thingsetStrings[state.activeDeviceId] = strings 49 | }, 50 | initChartData(state, newData) { 51 | const keys = Object.keys(newData) 52 | keys.forEach(key => { 53 | newData[key] = Array(180) 54 | state.chartData = newData 55 | }) 56 | state.chartDataKeys = keys 57 | }, 58 | updateChartData(state, newData) { 59 | state.chartDataKeys.forEach(key => { 60 | state.chartData[key].shift() 61 | state.chartData[key].push(newData[key]) 62 | }); 63 | }, 64 | saveAuthStatus(state, status) { 65 | state.isAuthenticated = status; 66 | }, 67 | saveInfo(state, data) { 68 | state.selfInfo = data 69 | }, 70 | triggerAlert(state, msg) { 71 | state.globAlert = true 72 | state.globAlertMsg = msg 73 | state.globAlertType = "warning" 74 | }, 75 | resetAlert(state) { 76 | state.globAlert = false 77 | } 78 | }, 79 | actions: { 80 | authenticate( { commit }, password){ 81 | return axios.post("ts/" + this.state.activeDeviceId + "/auth", 82 | '"' + password + '"', 83 | {headers: {"Content-Type": "text/plain"}}) 84 | .then(res => { 85 | commit('saveAuthStatus', true); 86 | }) 87 | .catch(error => { 88 | commit('saveAuthStatus', false); 89 | console.log(error); 90 | }) 91 | }, 92 | getDevices( { commit }) { 93 | return axios.get('ts/') 94 | .then(res => { 95 | if (res.data) { 96 | commit('saveDevices', res.data) 97 | } 98 | }).catch(error => { 99 | console.log(error); 100 | }) 101 | }, 102 | getDeviceInfo( { commit }) { 103 | return axios.get("ts/" + this.state.activeDeviceId + "/info") 104 | .then(res => { 105 | commit("saveDeviceInfo", res.data) 106 | }) 107 | .catch(error => { 108 | throw error 109 | }); 110 | }, 111 | getThingsetStrings( { commit }) { 112 | axios.get(this.state.info[this.state.activeDeviceId].DataExtURL) 113 | .then(res => { 114 | commit("saveThingsetStrings", res.data) 115 | }) 116 | .catch(error => { 117 | commit("triggerAlert", 118 | "Unable to fetch extended information for " + this.state.activeDevice + 119 | " device data. Using basic information discovered from device.") 120 | }) 121 | }, 122 | initChartData( { commit }) { 123 | return axios.get("ts/" + this.state.activeDeviceId + "/meas") 124 | .then(res => { 125 | commit("initChartData", res.data); 126 | }) 127 | .catch(error => { 128 | console.log(error); 129 | }) 130 | }, 131 | updateChartData({ commit }) { 132 | return axios.get("ts/" + this.state.activeDeviceId + "/meas") 133 | .then(res => { 134 | commit("updateChartData", res.data); 135 | }) 136 | .catch(error => { 137 | console.log(error); 138 | }); 139 | }, 140 | getInfoSelf({ commit }) { 141 | return axios.get("ts/" + this.state.self + "/info") 142 | .then(res => { 143 | commit("saveInfo", res.data); 144 | }).catch(error => { 145 | console.log(error); 146 | }) 147 | }, 148 | async createPrettyStrings({ commit }, section) { 149 | let id = this.state.activeDeviceId 150 | if (this.state.thingsetStrings[id]) { 151 | return this.state.thingsetStrings[id][section] 152 | } else { 153 | return {} 154 | } 155 | } 156 | } 157 | }) 158 | -------------------------------------------------------------------------------- /webapp/src/views/Config.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 144 | -------------------------------------------------------------------------------- /webapp/src/views/ConfigGeneral.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 180 | -------------------------------------------------------------------------------- /webapp/src/views/DataLog.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 160 | -------------------------------------------------------------------------------- /webapp/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /webapp/src/views/IO.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 102 | -------------------------------------------------------------------------------- /webapp/src/views/Info.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 79 | -------------------------------------------------------------------------------- /webapp/src/views/Live.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 186 | -------------------------------------------------------------------------------- /webapp/src/views/Ota.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 216 | -------------------------------------------------------------------------------- /webapp/vue.config.js: -------------------------------------------------------------------------------- 1 | const CompressionPlugin = require("compression-webpack-plugin"); 2 | 3 | module.exports = { 4 | productionSourceMap: false, 5 | devServer: { 6 | proxy: { 7 | '/ts': { 8 | // add IP address of device below for frontend testing within local network 9 | target: 'http://', 10 | changeOrigin: true, 11 | ws: true 12 | }, 13 | '/ota': { 14 | target: 'http://', 15 | changeOrigin: true, 16 | ws: true 17 | } 18 | } 19 | }, 20 | chainWebpack: (config) => { 21 | config.optimization.delete('splitChunks'), 22 | config.module 23 | .rule('images') 24 | .use('url-loader') 25 | .tap(options => Object.assign({}, options, { name: '[name].[ext]' })); 26 | }, 27 | css: { 28 | extract: { 29 | filename: '[name].css', 30 | chunkFilename: '[name].css', 31 | }, 32 | }, 33 | configureWebpack: { 34 | output: { 35 | filename: '[name].js', 36 | chunkFilename: '[name].js', 37 | }, 38 | plugins: [new CompressionPlugin({ 39 | filename: '[name][ext].gz', 40 | algorithm: "gzip", 41 | deleteOriginalAssets: process.env.NODE_ENV === 'production' ? true : false, 42 | })] 43 | } 44 | } 45 | --------------------------------------------------------------------------------