├── requirements.txt ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── general_question.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── main.yml ├── main ├── idf_component.yml ├── resolve.h ├── log.h ├── httpd.h ├── httpd_static_files.h.in ├── broadcasters.h ├── eth.h ├── resolve.c ├── ota.h ├── wifi.h ├── gatt.h ├── mqtt.h ├── CMakeLists.txt ├── config.h ├── log.c ├── ble.h ├── ble_utils.h ├── eth.c ├── wifi.c ├── ota.c ├── mqtt.c ├── httpd.c ├── config.c ├── broadcasters.c └── ble_utils.c ├── .gitignore ├── sdkconfig.defaults.esp32c3 ├── sdkconfig.defaults.esp32s3 ├── data └── config.json ├── partitions.csv ├── sdkconfig.defaults ├── www ├── busy.html ├── css │ └── progress.css ├── index.html └── js │ └── ble2mqtt.js ├── LICENSE ├── remote_log.py ├── CMakeLists.txt ├── ota.py ├── get_gatt_assigned_numbers.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ shmuelzon ] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /main/idf_component.yml: -------------------------------------------------------------------------------- 1 | ## IDF Component Manager Manifest File 2 | dependencies: 3 | idf: 4 | version: ">=5.0" 5 | espressif/mdns: "^1.0.7" 6 | -------------------------------------------------------------------------------- /main/resolve.h: -------------------------------------------------------------------------------- 1 | #ifndef RESOLVE_H 2 | #define RESOLVE_H 3 | 4 | const char *resolve_host(const char *host); 5 | 6 | int resolve_initialize(void); 7 | 8 | #endif 9 | 10 | -------------------------------------------------------------------------------- /main/log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H 2 | #define LOG_H 3 | 4 | #include 5 | 6 | int log_start(const char *host, uint16_t port); 7 | int log_stop(void); 8 | 9 | int log_initialize(void); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Outputs 2 | build/ 3 | 4 | # Build depdendencies 5 | dependencies.lock 6 | managed_components 7 | 8 | # Local files 9 | sdkconfig 10 | sdkconfig.old 11 | log.txt 12 | 13 | # General 14 | *.swp 15 | *.swo 16 | tags 17 | -------------------------------------------------------------------------------- /sdkconfig.defaults.esp32c3: -------------------------------------------------------------------------------- 1 | # This file was generated using idf.py save-defconfig. It can be edited manually. 2 | # Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration 3 | # 4 | CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y 5 | -------------------------------------------------------------------------------- /sdkconfig.defaults.esp32s3: -------------------------------------------------------------------------------- 1 | # This file was generated using idf.py save-defconfig. It can be edited manually. 2 | # Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration 3 | # 4 | CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y 5 | -------------------------------------------------------------------------------- /data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": { 3 | "wifi": { 4 | "ssid": "MY_SSID", 5 | "password": "MY_PASSWORD" 6 | } 7 | }, 8 | "mqtt": { 9 | "server": { 10 | "host": "192.168.1.1", 11 | "port": 1883 12 | }, 13 | "publish": { 14 | "retain": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /main/httpd.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTPD_H 2 | #define HTTPD_H 3 | 4 | #include "ota.h" 5 | 6 | /* Event callback types */ 7 | typedef void (*httpd_on_ota_completed_cb_t)(ota_type_t type, ota_err_t err); 8 | 9 | /* Event handlers */ 10 | void httpd_set_on_ota_completed_cb(httpd_on_ota_completed_cb_t cb); 11 | 12 | int httpd_initialize(void); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General question 3 | about: Ask a question regarding this project 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Question** 11 | A clear and concise question. Ex. How do I [...]? 12 | 13 | **Additional context** 14 | Add any other context or screenshots about the question here. 15 | -------------------------------------------------------------------------------- /partitions.csv: -------------------------------------------------------------------------------- 1 | # Name, Type, SubType, Offset, Size 2 | nvs, data, nvs, 0x009000, 0x004000 3 | otadata, data, ota, 0x00d000, 0x002000 4 | phy_init, data, phy, 0x00f000, 0x001000 5 | ota_0, app, ota_0, 0x010000, 0x180000 6 | ota_1, app, ota_1, 0x190000, 0x180000 7 | fs_0, data, spiffs, 0x310000, 0x040000 8 | fs_1, data, spiffs, 0x350000, 0x040000 9 | -------------------------------------------------------------------------------- /main/httpd_static_files.h.in: -------------------------------------------------------------------------------- 1 | #ifndef HTTPD_STATIC_FILES_H 2 | #define HTTPD_STATIC_FILES_H 3 | 4 | #include 5 | #include 6 | 7 | @static_files_declerations@ 8 | typedef struct { 9 | char *path; 10 | const char *start; 11 | const char *end; 12 | } httpd_static_file; 13 | 14 | static httpd_static_file httpd_static_files[] = { 15 | { "/", index_html_start, index_html_end }, 16 | @static_files_list@{ NULL } 17 | }; 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /sdkconfig.defaults: -------------------------------------------------------------------------------- 1 | # This file was generated using idf.py save-defconfig. It can be edited manually. 2 | # Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration 3 | # 4 | CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y 5 | CONFIG_PARTITION_TABLE_CUSTOM=y 6 | CONFIG_COMPILER_OPTIMIZATION_SIZE=y 7 | CONFIG_BT_ENABLED=y 8 | # CONFIG_BT_GATTS_ENABLE is not set 9 | CONFIG_BT_GATTC_MAX_CACHE_CHAR=100 10 | CONFIG_BT_ACL_CONNECTIONS=7 11 | CONFIG_BTDM_CTRL_BLE_MAX_CONN=9 12 | # CONFIG_BTDM_BLE_SCAN_DUPL is not set 13 | CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 14 | -------------------------------------------------------------------------------- /www/busy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Please wait... 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

13 | Restarting. Please wait... 14 |

15 |
16 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /main/broadcasters.h: -------------------------------------------------------------------------------- 1 | #ifndef BROADCASTERS_H 2 | #define BROADCASTERS_H 3 | 4 | #include 5 | #include 6 | 7 | #define MAX_BROADCASTER_NAME 32 8 | 9 | /* Event callback types */ 10 | typedef void (*broadcaster_meta_data_cb_t)(char *name, char *val, void *ctx); 11 | 12 | /* Types */ 13 | typedef struct { 14 | char name[MAX_BROADCASTER_NAME]; 15 | int (*is_broadcaster)(uint8_t *adv_data, size_t adv_data_len); 16 | void (*metadata_get)(uint8_t *adv_data, size_t adv_data_len, int rssi, 17 | broadcaster_meta_data_cb_t cb, void *ctx); 18 | } broadcaster_ops_t; 19 | 20 | broadcaster_ops_t *broadcaster_ops_get(uint8_t *adv_data, size_t adv_data_len); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /main/eth.h: -------------------------------------------------------------------------------- 1 | #ifndef ETH_H 2 | #define ETH_H 3 | 4 | #include 5 | 6 | typedef enum { 7 | PHY_IP101, 8 | PHY_RTL8201, 9 | PHY_LAN8720, 10 | PHY_DP83848, 11 | } eth_phy_t; 12 | 13 | /* Event callback types */ 14 | typedef void (*eth_on_connected_cb_t)(void); 15 | typedef void (*eth_on_disconnected_cb_t)(void); 16 | 17 | eth_phy_t eth_phy_atophy(const char *phy); 18 | uint8_t eth_clk_mode_atoclk_mode(const char *clk_mode); 19 | 20 | /* Event handlers */ 21 | void eth_set_on_connected_cb(eth_on_connected_cb_t cb); 22 | void eth_set_on_disconnected_cb(eth_on_disconnected_cb_t cb); 23 | 24 | int eth_initialize(void); 25 | int eth_connect(eth_phy_t eth_phy, int8_t eth_phy_power_pin); 26 | uint8_t *eth_mac_get(void); 27 | void eth_hostname_set(const char *hostname); 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /main/resolve.c: -------------------------------------------------------------------------------- 1 | #include "resolve.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /* Constants */ 9 | static const char *TAG = "Resolve"; 10 | 11 | const char *resolve_host(const char *hostname) 12 | { 13 | static char buf[64]; 14 | struct hostent *host; 15 | 16 | if (!(host = gethostbyname(hostname)) || host->h_addr_list[0] == NULL) 17 | { 18 | ESP_LOGE(TAG, "Failed resolving %s", hostname); 19 | return hostname; 20 | } 21 | 22 | sprintf(buf, "%s", inet_ntoa(*(struct ip4_addr *)host->h_addr_list[0])); 23 | ESP_LOGD(TAG, "DNS resolved %s to %s", hostname, buf); 24 | return buf; 25 | } 26 | 27 | int resolve_initialize(void) 28 | { 29 | ESP_LOGI(TAG, "Initializing resolver"); 30 | 31 | return 0; 32 | } 33 | -------------------------------------------------------------------------------- /main/ota.h: -------------------------------------------------------------------------------- 1 | #ifndef OTA_H 2 | #define OTA_H 3 | 4 | #include 5 | #include 6 | 7 | /* Types */ 8 | typedef enum { 9 | OTA_TYPE_FIRMWARE, 10 | OTA_TYPE_CONFIG, 11 | } ota_type_t; 12 | 13 | typedef enum { 14 | OTA_ERR_SUCCESS = 0, 15 | OTA_ERR_NO_CHANGE, 16 | OTA_ERR_IN_PROGRESS, 17 | OTA_ERR_FAILED_DOWNLOAD, 18 | OTA_ERR_FAILED_BEGIN, 19 | OTA_ERR_FAILED_WRITE, 20 | OTA_ERR_FAILED_END, 21 | } ota_err_t; 22 | 23 | /* Event callback types */ 24 | typedef void (*ota_on_completed_cb_t)(ota_type_t type, ota_err_t err); 25 | 26 | int ota_download(ota_type_t type, const char *url, ota_on_completed_cb_t cb); 27 | 28 | ota_err_t ota_open(ota_type_t type); 29 | ota_err_t ota_write(uint8_t *data, size_t len); 30 | ota_err_t ota_close(void); 31 | 32 | char *ota_err_to_str(ota_err_t err); 33 | int ota_initialize(void); 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /www/css/progress.css: -------------------------------------------------------------------------------- 1 | .progress-line, .progress-line:before { 2 | height: 0.75rem; 3 | width: 100%; 4 | margin: 0; 5 | } 6 | .progress-line { 7 | background-color: #ddd; 8 | display: -webkit-flex; 9 | display: flex; 10 | } 11 | .progress-line:before { 12 | background-color: #308732; 13 | content: ''; 14 | -webkit-animation: running-progress 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; 15 | animation: running-progress 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; 16 | } 17 | @-webkit-keyframes running-progress { 18 | 0% { margin-left: 0px; margin-right: 100%; } 19 | 50% { margin-left: 25%; margin-right: 0%; } 20 | 100% { margin-left: 100%; margin-right: 0; } 21 | } 22 | @keyframes running-progress { 23 | 0% { margin-left: 0px; margin-right: 100%; } 24 | 50% { margin-left: 25%; margin-right: 0%; } 25 | 100% { margin-left: 100%; margin-right: 0; } 26 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | *Please make sure you're using the ESP-IDF version specified in the [README](https://github.com/shmuelzon/esp32-ble2mqtt/blob/master/README.md) file, any other version is not supported.* 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Configuration and logs** 26 | If applicable, add your configuration file (without passwords) and logs (preferably of DEBUG level) to help understand your problem. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /main/wifi.h: -------------------------------------------------------------------------------- 1 | #ifndef WIFI_H 2 | #define WIFI_H 3 | 4 | #include 5 | 6 | typedef enum { 7 | EAP_NONE, 8 | EAP_TLS, 9 | EAP_PEAP, 10 | EAP_TTLS 11 | } eap_method_t; 12 | 13 | /* Event callback types */ 14 | typedef void (*wifi_on_connected_cb_t)(void); 15 | typedef void (*wifi_on_disconnected_cb_t)(void); 16 | 17 | eap_method_t wifi_eap_atomethod(const char *method); 18 | 19 | /* Event handlers */ 20 | void wifi_set_on_connected_cb(wifi_on_connected_cb_t cb); 21 | void wifi_set_on_disconnected_cb(wifi_on_disconnected_cb_t cb); 22 | 23 | int wifi_initialize(void); 24 | int wifi_start_ap(const char *ssid, const char *password); 25 | int wifi_connect(const char *ssid, const char *password, 26 | eap_method_t eap_method, const char *eap_identity, 27 | const char *eap_username, const char *eap_password, 28 | const char *ca_cert, const char *client_cert, const char *client_key); 29 | int wifi_reconnect(void); 30 | uint8_t *wifi_mac_get(void); 31 | void wifi_hostname_set(const char *hostname); 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | release: 9 | types: [ published ] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | submodules: recursive 20 | 21 | - name: ESP-IDF Build 22 | uses: espressif/esp-idf-ci-action@v1.1.0 23 | with: 24 | esp_idf_version: v5.0 25 | command: idf.py image 26 | 27 | - name: Upload Application Image 28 | uses: actions/upload-artifact@v2.2.4 29 | with: 30 | name: Application 31 | path: build/ble2mqtt.bin 32 | 33 | - name: Upload Filesystem Image 34 | uses: actions/upload-artifact@v2.2.4 35 | with: 36 | name: File System 37 | path: build/fs_0.bin 38 | 39 | - name: Upload Full Flash Image 40 | uses: actions/upload-artifact@v2.2.4 41 | with: 42 | name: Full Flash Image 43 | path: build/ble2mqtt-full.bin 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Assaf Inbal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main/gatt.h: -------------------------------------------------------------------------------- 1 | #ifndef GATT_H 2 | #define GATT_H 3 | 4 | #include "ble_utils.h" 5 | 6 | typedef enum { 7 | CHAR_TYPE_UNKNOWN, 8 | CHAR_TYPE_16BIT, 9 | CHAR_TYPE_24BIT, 10 | CHAR_TYPE_2BIT, 11 | CHAR_TYPE_32BIT, 12 | CHAR_TYPE_4BIT, 13 | CHAR_TYPE_8BIT, 14 | CHAR_TYPE_BOOLEAN, 15 | CHAR_TYPE_FLOAT, 16 | CHAR_TYPE_FLOAT64, 17 | CHAR_TYPE_GATT_UUID, 18 | CHAR_TYPE_NIBBLE, 19 | CHAR_TYPE_REG_CERT_DATA_LIST, 20 | CHAR_TYPE_SFLOAT, 21 | CHAR_TYPE_SINT16, 22 | CHAR_TYPE_SINT24, 23 | CHAR_TYPE_SINT32, 24 | CHAR_TYPE_SINT8, 25 | CHAR_TYPE_UINT12, 26 | CHAR_TYPE_UINT128, 27 | CHAR_TYPE_UINT16, 28 | CHAR_TYPE_UINT24, 29 | CHAR_TYPE_UINT32, 30 | CHAR_TYPE_UINT40, 31 | CHAR_TYPE_UINT48, 32 | CHAR_TYPE_UINT8, 33 | CHAR_TYPE_UTF8S, 34 | CHAR_TYPE_VARIABLE 35 | } characteristic_type_t; 36 | 37 | typedef struct { 38 | ble_uuid_t uuid; 39 | char *name; 40 | characteristic_type_t *types; 41 | } characteristic_desc_t; 42 | 43 | typedef struct { 44 | ble_uuid_t uuid; 45 | char *name; 46 | } service_desc_t; 47 | 48 | extern service_desc_t services[]; 49 | extern characteristic_desc_t characteristics[]; 50 | 51 | #endif -------------------------------------------------------------------------------- /main/mqtt.h: -------------------------------------------------------------------------------- 1 | #ifndef MQTT_H 2 | #define MQTT_H 3 | 4 | #include 5 | #include 6 | 7 | /* Event callback types */ 8 | typedef void (*mqtt_on_connected_cb_t)(void); 9 | typedef void (*mqtt_on_disconnected_cb_t)(void); 10 | typedef void (*mqtt_on_message_received_cb_t)(const char *topic, 11 | const uint8_t *payload, size_t len, void *ctx); 12 | typedef void (*mqtt_free_ctx_cb_t)(void *ctx); 13 | 14 | /* Event handlers */ 15 | void mqtt_set_on_connected_cb(mqtt_on_connected_cb_t cb); 16 | void mqtt_set_on_disconnected_cb(mqtt_on_disconnected_cb_t cb); 17 | 18 | /* Pub/Sub */ 19 | int mqtt_subscribe(const char *topic, int qos, mqtt_on_message_received_cb_t cb, 20 | void *ctx, mqtt_free_ctx_cb_t free_cb); 21 | int mqtt_unsubscribe_topic_prefix(const char *topic_prefix); 22 | int mqtt_unsubscribe(const char *topic); 23 | int mqtt_publish(const char *topic, uint8_t *payload, size_t len, int qos, 24 | uint8_t retained); 25 | 26 | int mqtt_connect(const char *host, uint16_t port, const char *client_id, 27 | const char *username, const char *password, uint8_t ssl, 28 | const char *server_cert, const char *client_cert, const char *client_key, 29 | const char *lwt_topic, const char *lwt_msg, uint8_t lwt_qos, 30 | uint8_t lwt_retain); 31 | int mqtt_disconnect(void); 32 | 33 | uint8_t mqtt_is_connected(void); 34 | 35 | int mqtt_initialize(void); 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register( 2 | SRCS "ble.c" "ble2mqtt.c" "ble_utils.c" "broadcasters.c" "config.c" 3 | "eth.c" "gatt.c" "httpd.c" "log.c" "mqtt.c" "ota.c" "resolve.c" "wifi.c" 4 | INCLUDE_DIRS ".") 5 | 6 | target_compile_definitions(${COMPONENT_TARGET} PRIVATE 7 | "-DBLE2MQTT_VER=\"${PROJECT_VER}\"") 8 | target_include_directories(${COMPONENT_LIB} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) 9 | 10 | # Static files to be served by the web server 11 | file(GLOB_RECURSE www_files RELATIVE ${PROJECT_DIR}/www ${PROJECT_DIR}/www/*) 12 | foreach(file ${www_files}) 13 | string(MAKE_C_IDENTIFIER ${file} c_name) 14 | string(APPEND static_files_list 15 | "{ \"/${file}\", ${c_name}_start, ${c_name}_end },\n ") 16 | string(APPEND static_files_declerations 17 | "extern const char ${c_name}_start[] asm(\"_binary_${c_name}_start\");\n") 18 | string(APPEND static_files_declerations 19 | "extern const char ${c_name}_end[] asm(\"_binary_${c_name}_end\");\n") 20 | get_filename_component(www_file_dir ${build_dir}/www/${file}.gz DIRECTORY) 21 | file(MAKE_DIRECTORY ${www_file_dir}) 22 | add_custom_command(OUTPUT ${build_dir}/www/${file}.gz 23 | COMMAND ${python} -m gzip < ${PROJECT_DIR}/www/${file} > ${build_dir}/www/${file}.gz 24 | DEPENDS ${PROJECT_DIR}/www/${file}) 25 | target_add_binary_data(${COMPONENT_LIB} ${build_dir}/www/${file}.gz 26 | BINARY RENAME_TO ${c_name}) 27 | endforeach() 28 | configure_file(httpd_static_files.h.in httpd_static_files.h) 29 | -------------------------------------------------------------------------------- /remote_log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | from builtins import str 5 | import argparse 6 | import datetime 7 | import ipaddress 8 | import json 9 | import os 10 | import socket 11 | import struct 12 | 13 | dns_cache = dict() 14 | 15 | def get_hostname(addr): 16 | ip = addr[0] 17 | try: 18 | if dns_cache.get(ip) == None: 19 | name, alias, addresslist = socket.gethostbyaddr(ip) 20 | dns_cache[ip] = name 21 | return dns_cache[ip] 22 | except socket.herror: 23 | return ip 24 | 25 | def log_listener(args): 26 | ip = ipaddress.ip_address(str(socket.gethostbyname(args.host))) 27 | 28 | # Set up logging server 29 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 30 | if ip.is_multicast: 31 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 32 | mreq = struct.pack("4sl", socket.inet_aton(str(ip)), socket.INADDR_ANY) 33 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) 34 | sock.bind(('', args.port)) 35 | 36 | print('Listening on port %d' % args.port) 37 | while True: 38 | try: 39 | data, addr = sock.recvfrom(2048) 40 | print('%s (%s): %s' % (datetime.datetime.now(), get_hostname(addr), 41 | data.decode('utf-8', 'ignore')), end='') 42 | except KeyboardInterrupt: 43 | break; 44 | 45 | def main(): 46 | parser = argparse.ArgumentParser(description='Remote logging server') 47 | parser.add_argument('--host', help='Host or IP address to listen on. ' 48 | 'Default take from configuration file') 49 | parser.add_argument('--port', type=int, help='UDP port to listen on. ' 50 | 'Default take from configuration file') 51 | args = parser.parse_args() 52 | 53 | config = json.load(open('data/config.json')) 54 | try: 55 | if args.host is None: 56 | args.host = config['log']['host'] 57 | if args.port is None: 58 | args.port = config['log']['port'] 59 | except KeyError: 60 | print('Logging seems to be disabled in the configuration file') 61 | return 62 | 63 | log_listener(args) 64 | 65 | if __name__ == '__main__': 66 | main() 67 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | execute_process( 4 | COMMAND git describe --always --tags 5 | OUTPUT_VARIABLE PROJECT_VER 6 | OUTPUT_STRIP_TRAILING_WHITESPACE) 7 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 8 | add_compile_definitions(BTA_GATTC_NOTIF_REG_MAX=32) 9 | 10 | project(ble2mqtt) 11 | 12 | spiffs_create_partition_image(fs_0 data FLASH_IN_PROJECT) 13 | spiffs_create_partition_image(fs_1 data FLASH_IN_PROJECT) 14 | add_dependencies(spiffs_fs_0_bin validate-config) 15 | 16 | add_custom_target(check-project-python-requirements 17 | COMMAND ${python} $ENV{IDF_PATH}/tools/check_python_dependencies.py 18 | -r ${PROJECT_DIR}/requirements.txt) 19 | 20 | if(CMAKE_HOST_WIN32) 21 | set(NULDEV NUL) 22 | else() 23 | set(NULDEV /dev/null) 24 | endif() 25 | 26 | add_custom_target(validate-config 27 | COMMAND ${python} -m json.tool ${PROJECT_DIR}/data/config.json >${NULDEV} 28 | || (echo "Error: Invalid JSON in configuration file." && exit 1 )) 29 | 30 | add_custom_target(upload 31 | COMMAND ${python} ${PROJECT_DIR}/ota.py -f ${build_dir}/${PROJECT_BIN} 32 | -v ${PROJECT_VER} -t $$\{OTA_TARGET:-BLE2MQTT\} -n Firmware 33 | DEPENDS check-project-python-requirements app validate-config 34 | USES_TERMINAL 35 | WORKING_DIRECTORY ${PROJECT_DIR}) 36 | 37 | add_custom_target(force-upload 38 | COMMAND ${python} ${PROJECT_DIR}/ota.py -f ${build_dir}/${PROJECT_BIN} 39 | -v \"\" -t $$\{OTA_TARGET:-BLE2MQTT\} -n Firmware 40 | DEPENDS check-project-python-requirements app validate-config 41 | USES_TERMINAL 42 | WORKING_DIRECTORY ${PROJECT_DIR}) 43 | 44 | add_custom_target(upload-config 45 | COMMAND ${python} ${PROJECT_DIR}/ota.py -f ${build_dir}/fs_0.bin 46 | -v $$\(shasum -a 256 ${build_dir}/fs_0.bin | awk '{ print $$1 }'\) 47 | -t $$\{OTA_TARGET:-BLE2MQTT\} -n Config 48 | DEPENDS check-project-python-requirements spiffs_fs_0_bin validate-config 49 | USES_TERMINAL 50 | WORKING_DIRECTORY ${PROJECT_DIR}) 51 | 52 | add_custom_target(force-upload-config 53 | COMMAND ${python} ${PROJECT_DIR}/ota.py -f ${build_dir}/fs_0.bin -v \"\" 54 | -t $$\{OTA_TARGET:-BLE2MQTT\} -n Config 55 | DEPENDS check-project-python-requirements spiffs_fs_0_bin validate-config 56 | USES_TERMINAL 57 | WORKING_DIRECTORY ${PROJECT_DIR}) 58 | 59 | add_custom_target(remote-monitor 60 | COMMAND ${python} -u ${PROJECT_DIR}/remote_log.py 61 | DEPENDS validate-config 62 | USES_TERMINAL 63 | WORKING_DIRECTORY ${PROJECT_DIR}) 64 | 65 | add_custom_target(image 66 | COMMAND esptool.py --chip esp32 merge_bin -o ble2mqtt-full.bin @flash_project_args 67 | DEPENDS bootloader blank_ota_data app spiffs_fs_0_bin spiffs_fs_1_bin) 68 | -------------------------------------------------------------------------------- /main/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #include 5 | #include 6 | 7 | /* Types */ 8 | typedef struct config_update_handle_t config_update_handle_t; 9 | typedef enum config_network_type_t { 10 | NETWORK_TYPE_WIFI, 11 | NETWORK_TYPE_ETH, 12 | } config_network_type_t; 13 | 14 | /* BLE Configuration*/ 15 | const char *config_ble_service_name_get(const char *uuid); 16 | const char *config_ble_characteristic_name_get(const char *uuid); 17 | const char **config_ble_characteristic_types_get(const char *uuid); 18 | uint8_t config_ble_characteristic_should_include(const char *uuid); 19 | uint8_t config_ble_service_should_include(const char *uuid); 20 | uint8_t config_ble_should_connect(const char *mac); 21 | uint32_t config_ble_passkey_get(const char *mac); 22 | const char *config_ble_mikey_get(const char *mac); 23 | 24 | /* Ethernet Configuration */ 25 | const char *config_network_eth_phy_get(void); 26 | int8_t config_network_eth_phy_power_pin_get(void); 27 | 28 | /* MQTT Configuration*/ 29 | const char *config_mqtt_host_get(void); 30 | uint16_t config_mqtt_port_get(void); 31 | uint8_t config_mqtt_ssl_get(void); 32 | const char *config_mqtt_server_cert_get(void); 33 | const char *config_mqtt_client_cert_get(void); 34 | const char *config_mqtt_client_key_get(void); 35 | const char *config_mqtt_client_id_get(void); 36 | const char *config_mqtt_username_get(void); 37 | const char *config_mqtt_password_get(void); 38 | uint8_t config_mqtt_qos_get(void); 39 | uint8_t config_mqtt_retained_get(void); 40 | const char *config_mqtt_prefix_get(void); 41 | const char *config_mqtt_get_suffix_get(void); 42 | const char *config_mqtt_set_suffix_get(void); 43 | 44 | /* Network Configuration */ 45 | config_network_type_t config_network_type_get(void); 46 | 47 | /* WiFi Configuration*/ 48 | const char *config_network_hostname_get(void); 49 | const char *config_network_wifi_ssid_get(void); 50 | const char *config_network_wifi_password_get(void); 51 | const char *config_eap_ca_cert_get(void); 52 | const char *config_eap_client_cert_get(void); 53 | const char *config_eap_client_key_get(void); 54 | const char *config_eap_method_get(void); 55 | const char *config_eap_identity_get(void); 56 | const char *config_eap_username_get(void); 57 | const char *config_eap_password_get(void); 58 | 59 | /* Remote Logging Configuration */ 60 | const char *config_log_host_get(void); 61 | uint16_t config_log_port_get(void); 62 | 63 | /* Configuration Update */ 64 | int config_update_begin(config_update_handle_t **handle); 65 | int config_update_write(config_update_handle_t *handle, uint8_t *data, 66 | size_t len); 67 | int config_update_end(config_update_handle_t *handle); 68 | 69 | char *config_version_get(void); 70 | int config_initialize(void); 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /main/log.c: -------------------------------------------------------------------------------- 1 | #include "log.h" 2 | #include "resolve.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | /* Constants */ 12 | static const char *TAG = "Log"; 13 | 14 | /* Internal state */ 15 | static vprintf_like_t orig_esp_log = NULL; 16 | static int sock = -1; 17 | static struct sockaddr_in dst; 18 | 19 | #define MIN(a, b) ((a) < (b) ? (a) : (b)) 20 | static int log_vprintf(const char *fmt, va_list l) 21 | { 22 | static char buf[2048]; 23 | int ret; 24 | 25 | /* Send remote log */ 26 | ret = vsnprintf(buf,sizeof(buf), fmt, l); 27 | if((ret = sendto(sock, buf, MIN(ret, sizeof(buf) - 1), 0, 28 | (struct sockaddr *)&dst, sizeof(dst))) < 0) 29 | { 30 | printf("Failed sending remote log: %d\n", errno); 31 | } 32 | 33 | /* Also use existing logging mechanism */ 34 | if (orig_esp_log) 35 | orig_esp_log(fmt, l); 36 | 37 | return ret; 38 | } 39 | 40 | int log_start(const char *host, uint16_t port) 41 | { 42 | if (!host || !port) 43 | return -1; 44 | 45 | memset(&dst, 0, sizeof(dst)); 46 | dst.sin_family = AF_INET; 47 | dst.sin_port = htons(port); 48 | if (!inet_pton(AF_INET, resolve_host(host), &dst.sin_addr)) 49 | { 50 | ESP_LOGE(TAG, "Failed parsing IP address"); 51 | goto Error; 52 | } 53 | 54 | if((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) 55 | { 56 | ESP_LOGE(TAG, "Failed creating socket: %d (%m)", errno); 57 | goto Error; 58 | } 59 | 60 | if (fcntl(sock, F_SETFL, O_NONBLOCK)) 61 | { 62 | ESP_LOGE(TAG, "Failed setting socket as non-blocking"); 63 | goto Error; 64 | } 65 | 66 | if (IN_MULTICAST(ntohl(dst.sin_addr.s_addr))) 67 | { 68 | uint8_t ttl = 2; 69 | if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, 70 | sizeof(uint8_t))) 71 | { 72 | ESP_LOGE(TAG, "Failed setting multicast TTL"); 73 | goto Error; 74 | } 75 | } 76 | 77 | ESP_LOGI(TAG, "Enabling remote logging"); 78 | 79 | /* Set our logging function and save the original implementation */ 80 | orig_esp_log = esp_log_set_vprintf(log_vprintf); 81 | 82 | return 0; 83 | 84 | Error: 85 | if (sock >= 0) 86 | { 87 | close(sock); 88 | sock = -1; 89 | } 90 | 91 | return -1; 92 | } 93 | 94 | int log_stop(void) 95 | { 96 | if (sock < 0) 97 | return -1; 98 | 99 | /* Restore original logging implementation */ 100 | esp_log_set_vprintf(orig_esp_log); 101 | 102 | close(sock); 103 | sock = -1; 104 | 105 | ESP_LOGI(TAG, "Disabled remote logging"); 106 | 107 | return 0; 108 | } 109 | 110 | int log_initialize(void) 111 | { 112 | ESP_LOGI(TAG, "Initializing remote logging"); 113 | 114 | return 0; 115 | } 116 | -------------------------------------------------------------------------------- /main/ble.h: -------------------------------------------------------------------------------- 1 | #ifndef BLE_H 2 | #define BLE_H 3 | 4 | #include "broadcasters.h" 5 | #include "ble_utils.h" 6 | #include 7 | #include 8 | 9 | /* Constants */ 10 | #define CHAR_PROP_BROADCAST (1 << 0) 11 | #define CHAR_PROP_READ (1 << 1) 12 | #define CHAR_PROP_WRITE_NR (1 << 2) 13 | #define CHAR_PROP_WRITE (1 << 3) 14 | #define CHAR_PROP_NOTIFY (1 << 4) 15 | #define CHAR_PROP_INDICATE (1 << 5) 16 | #define CHAR_PROP_AUTH (1 << 6) 17 | #define CHAR_PROP_EXT_PROP (1 << 7) 18 | 19 | /* Types */ 20 | typedef struct { 21 | char name[32]; 22 | mac_addr_t mac; 23 | bool connected; 24 | } ble_dev_t; 25 | 26 | /* Event callback types */ 27 | typedef void (*ble_on_broadcaster_discovered_cb_t)(mac_addr_t mac, 28 | uint8_t *adv_data, size_t adv_data_len, int rssi, broadcaster_ops_t *ops); 29 | typedef void (*ble_on_device_discovered_cb_t)(mac_addr_t mac, int rssi); 30 | typedef void (*ble_on_device_connected_cb_t)(mac_addr_t mac); 31 | typedef void (*ble_on_device_disconnected_cb_t)(mac_addr_t mac); 32 | typedef void (*ble_on_device_services_discovered_cb_t)(mac_addr_t mac); 33 | typedef void (*ble_on_device_characteristic_found_cb_t)(mac_addr_t mac, 34 | ble_uuid_t service_uuid, ble_uuid_t characteristic_uuid, uint8_t index, 35 | uint8_t properties); 36 | typedef void (*ble_on_device_characteristic_value_cb_t)(mac_addr_t mac, 37 | ble_uuid_t service, ble_uuid_t characteristic, uint8_t index, 38 | uint8_t *value, size_t value_len); 39 | typedef uint32_t (*ble_on_passkey_requested_cb_t)(mac_addr_t mac); 40 | 41 | /* Event handlers */ 42 | void ble_set_on_broadcaster_discovered_cb( 43 | ble_on_broadcaster_discovered_cb_t cb); 44 | void ble_set_on_device_discovered_cb(ble_on_device_discovered_cb_t cb); 45 | void ble_set_on_device_connected_cb(ble_on_device_connected_cb_t cb); 46 | void ble_set_on_device_disconnected_cb(ble_on_device_disconnected_cb_t cb); 47 | void ble_set_on_device_services_discovered_cb( 48 | ble_on_device_services_discovered_cb_t cb); 49 | void ble_set_on_device_characteristic_value_cb( 50 | ble_on_device_characteristic_value_cb_t cb); 51 | void ble_set_on_passkey_requested_cb(ble_on_passkey_requested_cb_t cb); 52 | 53 | /* BLE Operations */ 54 | void ble_clear_bonding_info(void); 55 | 56 | int ble_scan_start(void); 57 | int ble_scan_stop(void); 58 | 59 | int ble_connect(mac_addr_t mac); 60 | int ble_disconnect(mac_addr_t mac); 61 | int ble_disconnect_all(void); 62 | 63 | int ble_services_scan(mac_addr_t mac); 64 | int ble_foreach_characteristic(mac_addr_t mac, 65 | ble_on_device_characteristic_found_cb_t cb); 66 | 67 | int ble_characteristic_read(mac_addr_t mac, ble_uuid_t service_uuid, 68 | ble_uuid_t characteristic_uuid, uint8_t index); 69 | int ble_characteristic_write(mac_addr_t mac, ble_uuid_t service_uuid, 70 | ble_uuid_t characteristic_uuid, uint8_t index, const uint8_t *value, 71 | size_t value_len); 72 | int ble_characteristic_notify_register(mac_addr_t mac, ble_uuid_t service_uuid, 73 | ble_uuid_t characteristic_uuid, uint8_t index); 74 | int ble_characteristic_notify_unregister(mac_addr_t mac, 75 | ble_uuid_t service_uuid, ble_uuid_t characteristic_uuid, uint8_t index); 76 | 77 | /* Management */ 78 | ble_dev_t *ble_devices_list_get(size_t *number_of_devices); 79 | void ble_devices_list_free(ble_dev_t *devices); 80 | 81 | int ble_initialize(void); 82 | 83 | #endif 84 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | BLE2MQTT 4 | 5 | 6 | 7 | 8 | 9 | 10 |
BLE2MQTT 11 |
12 | Management 13 | 14 | 15 | 17 | 19 | 21 | 23 | 24 |
25 |
26 | 27 |

Edit config file

28 |

29 | 30 |

31 | 32 |
33 |
34 | 35 | 37 | 38 | 39 |
40 |
41 | 42 |

File manager

43 |
45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
NameSize
58 |
59 |
60 |
61 | 62 |
63 |
64 | BLE devices 65 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
NameMAC
78 |
79 | 80 | 81 |
82 | 83 | 84 | -------------------------------------------------------------------------------- /main/ble_utils.h: -------------------------------------------------------------------------------- 1 | #ifndef BLE_UTILS_H 2 | #define BLE_UTILS_H 3 | 4 | #include 5 | #include 6 | 7 | #define MAC_FMT "%02x:%02x:%02x:%02x:%02x:%02x" 8 | #define MAC_PARAM(mac) mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] 9 | 10 | #define UUID_FMT \ 11 | "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x" 12 | #define UUID_PARAM(uuid) \ 13 | uuid[15], uuid[14], uuid[13], uuid[12], \ 14 | uuid[11], uuid[10], \ 15 | uuid[9], uuid[8], \ 16 | uuid[7], uuid[6], \ 17 | uuid[5], uuid[4], uuid[3], uuid[2], uuid[1], uuid[0] 18 | 19 | /* Types */ 20 | typedef uint8_t mac_addr_t[6]; 21 | typedef uint8_t ble_uuid_t[16]; 22 | 23 | typedef struct ble_characteristic_t { 24 | struct ble_characteristic_t *next; 25 | ble_uuid_t uuid; 26 | uint8_t index; 27 | uint16_t handle; 28 | uint8_t properties; 29 | uint16_t client_config_handle; 30 | } ble_characteristic_t; 31 | 32 | typedef struct ble_service_t { 33 | struct ble_service_t *next; 34 | ble_uuid_t uuid; 35 | ble_characteristic_t *characteristics; 36 | } ble_service_t; 37 | 38 | typedef struct ble_device_t { 39 | struct ble_device_t *next; 40 | char *name; 41 | mac_addr_t mac; 42 | esp_ble_addr_type_t addr_type; 43 | uint16_t conn_id; 44 | ble_service_t *services; 45 | uint8_t is_authenticating; 46 | } ble_device_t; 47 | 48 | /* Callback functions */ 49 | typedef int (*ble_on_device_cb_t)(ble_device_t *device); 50 | 51 | /* Enumerations to strings */ 52 | char *gap_event_to_str(esp_gap_ble_cb_event_t event); 53 | char *gattc_event_to_str(esp_gattc_cb_event_t event); 54 | 55 | /* Conversion functions */ 56 | char *mactoa(mac_addr_t mac); 57 | int atomac(const char *str, mac_addr_t mac); 58 | char *uuidtoa(ble_uuid_t uuid); 59 | int atouuid(const char *str, ble_uuid_t uuid); 60 | char *chartoa(ble_uuid_t uuid, const uint8_t *data, size_t len); 61 | uint8_t *atochar(ble_uuid_t uuid, const char *data, size_t len, 62 | size_t *ret_len); 63 | 64 | const char *ble_service_name_get(ble_uuid_t uuid); 65 | const char *ble_characteristic_name_get(ble_uuid_t uuid); 66 | 67 | /* Devices list */ 68 | ble_device_t *ble_device_add(ble_device_t **list, const char *name, 69 | mac_addr_t mac, esp_ble_addr_type_t addr_type, uint16_t conn_id); 70 | void ble_device_update_name(ble_device_t *device, const char *name); 71 | ble_device_t *ble_device_find_by_mac(ble_device_t *list, mac_addr_t mac); 72 | ble_device_t *ble_device_find_by_conn_id(ble_device_t *list, uint16_t conn_id); 73 | void ble_device_foreach(ble_device_t *list, ble_on_device_cb_t cb); 74 | void ble_device_remove_by_mac(ble_device_t **list, mac_addr_t mac); 75 | void ble_device_remove_by_conn_id(ble_device_t **list, uint16_t conn_id); 76 | void ble_device_remove_disconnected(ble_device_t **list); 77 | void ble_device_free(ble_device_t *device); 78 | void ble_devices_free(ble_device_t **list); 79 | 80 | ble_service_t *ble_device_service_add(ble_device_t *device, ble_uuid_t uuid); 81 | ble_service_t *ble_device_service_find(ble_device_t *device, ble_uuid_t uuid); 82 | void ble_device_service_free(ble_service_t *service); 83 | void ble_device_services_free(ble_service_t **list); 84 | 85 | ble_characteristic_t *ble_device_characteristic_add(ble_service_t *service, 86 | ble_uuid_t uuid, uint8_t index, uint16_t handle, uint8_t properties); 87 | ble_characteristic_t *ble_device_characteristic_find_by_uuid( 88 | ble_service_t *service, ble_uuid_t uuid, uint8_t index); 89 | ble_characteristic_t *ble_device_characteristic_find_by_handle( 90 | ble_service_t *service, uint16_t handle); 91 | void ble_device_characteristic_free(ble_characteristic_t *characteristic); 92 | void ble_device_characteristics_free(ble_characteristic_t **list); 93 | 94 | bool ble_uuid_equal(ble_uuid_t uuid1, ble_uuid_t uuid2); 95 | bool ble_mac_equal(mac_addr_t mac1, mac_addr_t mac2); 96 | 97 | int ble_device_info_get_by_conn_id_handle(ble_device_t *list, uint16_t conn_id, 98 | uint16_t handle, ble_device_t **device, ble_service_t **service, 99 | ble_characteristic_t **characteristic); 100 | 101 | #endif 102 | -------------------------------------------------------------------------------- /ota.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from http.server import BaseHTTPRequestHandler, HTTPServer 5 | from socketserver import ThreadingMixIn 6 | except ImportError: 7 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 8 | from SocketServer import ThreadingMixIn 9 | import argparse 10 | import json 11 | import os 12 | import paho.mqtt.client as mqtt 13 | import shutil 14 | from threading import Thread 15 | import time 16 | import socket 17 | import sys 18 | 19 | TIMEOUT = 3 20 | active_connections = 0 21 | last_request_time = time.time() 22 | 23 | class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): 24 | pass 25 | 26 | def OTAServerFactory(args): 27 | class OTAServer(BaseHTTPRequestHandler, object): 28 | def __init__(self, *args, **kwargs): 29 | super(OTAServer, self).__init__(*args, **kwargs) 30 | def log_message(self, format, *args): 31 | return 32 | def do_GET(self): 33 | global active_connections 34 | global last_request_time 35 | 36 | active_connections += 1 37 | last_request_time = time.time() 38 | print('%s - (%s: %s) GET %s ' % (self.log_date_time_string(), 39 | self.address_string(), self.headers.get('User-Agent', ''), self.path)) 40 | 41 | if self.headers.get('If-None-Match', '').replace('"', '') == args.version: 42 | self.send_response(304) 43 | print('%s - (%s: %s) Done: 304 Not Modified' % ( 44 | self.log_date_time_string(), self.address_string(), 45 | self.headers.get('User-Agent', ''))) 46 | active_connections -= 1 47 | return 48 | 49 | self.send_response(200) 50 | self.send_header('Content-Length', os.stat(args.file).st_size) 51 | self.send_header('ETag', args.version) 52 | self.end_headers() 53 | 54 | f = open(args.file, 'rb') 55 | shutil.copyfileobj(f, self.wfile) 56 | f.close() 57 | print('%s - (%s: %s) Done: 200 OK' % ( 58 | self.log_date_time_string(), self.address_string(), 59 | self.headers.get('User-Agent', ''))) 60 | active_connections -= 1 61 | 62 | return OTAServer 63 | 64 | def timeout_thread(httpd): 65 | global active_connections 66 | global last_request_time 67 | 68 | while active_connections > 0 or time.time() < last_request_time + TIMEOUT: 69 | time.sleep(0.1) 70 | httpd.shutdown() 71 | 72 | def get_local_ip(): 73 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 74 | s.connect(("8.8.8.8", 80)) 75 | ip = s.getsockname()[0] 76 | s.close() 77 | 78 | return ip 79 | 80 | def on_mqtt_connect(client, args, flags, rc): 81 | client.publish('%s/OTA/%s' % (args.target, args.name), 82 | 'http://%s:%d/' % (get_local_ip(), args.port)) 83 | client.disconnect() 84 | 85 | def main(): 86 | parser = argparse.ArgumentParser(description='OTA firmware/config upgrade') 87 | parser.add_argument('-f', '--file', required=True, 88 | help='File used for upgrade') 89 | parser.add_argument('-v', '--version', required=True, 90 | help='Version of the upgrade file') 91 | parser.add_argument('-n', '--name', required=True, 92 | help='The name of image type to publish, e.g. "Firmware", "Config", etc.') 93 | parser.add_argument('-t', '--target', default='BLE2MQTT', 94 | help='Host to upgrade. If left empty, will upgrade all hosts') 95 | parser.add_argument('-p', '--port', type=int, default=8000, 96 | help='HTTP server port') 97 | parser.add_argument('--mqtt-broker-server', 98 | help='MQTT broker server for initiating upgrade procedure. ' 99 | 'Default taken from configuration file') 100 | parser.add_argument('--mqtt-broker-port', type=int, 101 | help='MQTT broker port for initiating upgrade procedure. ' 102 | 'Default taken from configuration file') 103 | parser.add_argument('--mqtt-broker-username', 104 | help='MQTT broker username for initiating upgrade procedure. ' 105 | 'Default taken from configuration file') 106 | parser.add_argument('--mqtt-broker-password', 107 | help='MQTT broker password for initiating upgrade procedure. ' 108 | 'Default taken from configuration file') 109 | 110 | args = parser.parse_args() 111 | 112 | config = json.load(open('data/config.json')) 113 | if args.mqtt_broker_server is None: 114 | args.mqtt_broker_server = config['mqtt']['server']['host'] 115 | if args.mqtt_broker_port is None: 116 | args.mqtt_broker_port = config['mqtt']['server']['port'] 117 | if args.mqtt_broker_username is None and 'username' in config['mqtt']['server']: 118 | args.mqtt_broker_username = config['mqtt']['server']['username'] 119 | if args.mqtt_broker_password is None and 'password' in config['mqtt']['server']: 120 | args.mqtt_broker_password = config['mqtt']['server']['password'] 121 | 122 | # Connect to MQTT 123 | mqttc = mqtt.Client(userdata=args) 124 | mqttc.on_connect = on_mqtt_connect 125 | if args.mqtt_broker_username is not None: 126 | mqttc.username_pw_set(args.mqtt_broker_username, args.mqtt_broker_password) 127 | mqttc.connect(args.mqtt_broker_server, args.mqtt_broker_port) 128 | mqttc.loop_start() 129 | 130 | # Set up HTTP server 131 | httpd = ThreadedHTTPServer(("", args.port), OTAServerFactory(args)) 132 | Thread(target=timeout_thread, args=(httpd, )).start() 133 | print('Listening on port %d' % args.port) 134 | httpd.serve_forever() 135 | httpd.server_close() 136 | print('No new connection requests, shutting down') 137 | 138 | if __name__ == '__main__': 139 | main() 140 | -------------------------------------------------------------------------------- /main/eth.c: -------------------------------------------------------------------------------- 1 | #include "eth.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | static const char *TAG = "Ethernet"; 15 | 16 | static eth_on_connected_cb_t on_connected_cb = NULL; 17 | static eth_on_disconnected_cb_t on_disconnected_cb = NULL; 18 | static char *eth_hostname = NULL; 19 | #if CONFIG_ETH_USE_ESP32_EMAC 20 | static esp_netif_t *eth_netif = NULL; 21 | #endif 22 | 23 | void eth_set_on_connected_cb(eth_on_connected_cb_t cb) 24 | { 25 | on_connected_cb = cb; 26 | } 27 | 28 | void eth_set_on_disconnected_cb(eth_on_disconnected_cb_t cb) 29 | { 30 | on_disconnected_cb = cb; 31 | } 32 | 33 | uint8_t *eth_mac_get(void) 34 | { 35 | static uint8_t mac[6] = {}; 36 | 37 | if (!mac[0]) 38 | esp_read_mac(mac, ESP_MAC_ETH); 39 | 40 | return mac; 41 | } 42 | 43 | void eth_hostname_set(const char *hostname) 44 | { 45 | if (eth_hostname) 46 | free(eth_hostname); 47 | eth_hostname = strdup(hostname); 48 | } 49 | 50 | #if CONFIG_ETH_USE_ESP32_EMAC 51 | static void event_handler(void* arg, esp_event_base_t event_base, 52 | int32_t event_id, void* event_data) 53 | { 54 | if (event_base == ETH_EVENT) 55 | { 56 | switch (event_id) { 57 | case ETHERNET_EVENT_START: 58 | if (eth_hostname) 59 | esp_netif_set_hostname(eth_netif, eth_hostname); 60 | break; 61 | case ETHERNET_EVENT_CONNECTED: 62 | ESP_LOGI(TAG, "Connected"); 63 | break; 64 | case ETHERNET_EVENT_DISCONNECTED: 65 | ESP_LOGI(TAG, "Disconnected"); 66 | if (on_disconnected_cb) 67 | on_disconnected_cb(); 68 | break; 69 | default: 70 | ESP_LOGD(TAG, "Unhandled Ethernet event (%" PRId32 ")", event_id); 71 | } 72 | } 73 | else if (event_base == IP_EVENT) 74 | { 75 | switch (event_id) { 76 | case IP_EVENT_ETH_GOT_IP: 77 | { 78 | ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; 79 | 80 | ESP_LOGD(TAG, "Got IP address: %s", 81 | inet_ntoa(event->ip_info.ip)); 82 | if (on_connected_cb) 83 | on_connected_cb(); 84 | break; 85 | } 86 | default: 87 | ESP_LOGD(TAG, "Unhandled IP event (%" PRId32 ")", event_id); 88 | break; 89 | } 90 | } 91 | } 92 | #endif 93 | 94 | eth_phy_t eth_phy_atophy(const char *phy) 95 | { 96 | struct { 97 | const char *name; 98 | int phy; 99 | } *p, phys[] = { 100 | { "IP101", PHY_IP101 }, 101 | { "RTL8201", PHY_RTL8201 }, 102 | { "LAN8720", PHY_LAN8720 }, 103 | { "DP83848", PHY_DP83848 }, 104 | }; 105 | 106 | for (p = phys; p->name; p++) 107 | { 108 | if (!strcmp(p->name, phy)) 109 | break; 110 | } 111 | 112 | return p->phy; 113 | } 114 | 115 | int eth_connect(eth_phy_t eth_phy, int8_t eth_phy_power_pin) 116 | { 117 | #if CONFIG_ETH_USE_ESP32_EMAC 118 | esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); 119 | eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); 120 | eth_esp32_emac_config_t esp32_mac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); 121 | eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); 122 | esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_mac_config, &mac_config); 123 | esp_eth_config_t config = ETH_DEFAULT_CONFIG(mac, NULL); 124 | esp_eth_handle_t eth_handle = NULL; 125 | 126 | eth_netif = esp_netif_new(&cfg); 127 | if (eth_phy_power_pin >= 0) 128 | { 129 | esp_rom_gpio_pad_select_gpio(eth_phy_power_pin); 130 | gpio_set_direction(eth_phy_power_pin, GPIO_MODE_OUTPUT); 131 | gpio_set_level(eth_phy_power_pin, 1); 132 | vTaskDelay(pdMS_TO_TICKS(10)); 133 | } 134 | 135 | switch (eth_phy) 136 | { 137 | case PHY_IP101: 138 | ESP_LOGI(TAG, "PHY config: IP101"); 139 | config.phy = esp_eth_phy_new_ip101(&phy_config); 140 | break; 141 | case PHY_RTL8201: 142 | ESP_LOGI(TAG, "PHY config: RTL8201"); 143 | config.phy = esp_eth_phy_new_rtl8201(&phy_config); 144 | break; 145 | case PHY_LAN8720: 146 | ESP_LOGI(TAG, "PHY config: LAN8720"); 147 | config.phy = esp_eth_phy_new_lan87xx(&phy_config); 148 | break; 149 | case PHY_DP83848: 150 | ESP_LOGI(TAG, "PHY config: DP83848"); 151 | config.phy = esp_eth_phy_new_dp83848(&phy_config); 152 | break; 153 | } 154 | 155 | ESP_ERROR_CHECK(esp_eth_driver_install(&config, ð_handle)); 156 | ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle))); 157 | ESP_ERROR_CHECK(esp_eth_start(eth_handle)); 158 | 159 | return 0; 160 | #else 161 | return -1; 162 | #endif 163 | } 164 | 165 | int eth_initialize(void) 166 | { 167 | #if CONFIG_ETH_USE_ESP32_EMAC 168 | ESP_LOGD(TAG, "Initializing Ethernet"); 169 | ESP_ERROR_CHECK(esp_netif_init()); 170 | ESP_ERROR_CHECK(esp_event_loop_create_default()); 171 | 172 | ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); 173 | ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); 174 | 175 | return 0; 176 | #else 177 | ESP_LOGE(TAG, "Ethernet not supported!"); 178 | return -1; 179 | #endif 180 | } 181 | -------------------------------------------------------------------------------- /get_gatt_assigned_numbers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import json 5 | import re 6 | from multiprocessing.dummy import Pool as ThreadPool 7 | try: 8 | import urllib.request as urlrequest 9 | from http.cookiejar import CookieJar 10 | except ImportError: 11 | import urllib2 as urlrequest 12 | from cookielib import CookieJar 13 | 14 | BT_URL = 'https://www.bluetooth.com' 15 | GATT_URL = BT_URL + '/specifications/gatt' 16 | SERVICES_URL = GATT_URL + '/services'; 17 | CHARACTERISTICS_URL = GATT_URL + '/characteristics' 18 | 19 | services = {} 20 | characteristics = {} 21 | characteristics_types = {} 22 | cj = CookieJar() 23 | 24 | def build_gatt_regex(gatt_type): 25 | # Nasty, nasty regex to parse a service/characterisitc row in the HTML. 26 | return r']*>[^<]*]*>([^<]*)[^<]*[^<]*' \ 27 | ']*>(org.bluetooth.' + gatt_type + \ 28 | '.[^<]*)[^<]*]*>([^<]*)<' 29 | 30 | def build_sig_uuid(uuid): 31 | return '%08x-0000-1000-8000-00805f9b34fb' % (int(uuid, 16)) 32 | 33 | def parse_services(data): 34 | regex = build_gatt_regex('service') 35 | 36 | # Parse all services 37 | for service in re.findall(regex, data): 38 | # service[0] = URL, service[1] = Name, service[2] = Type, service[3] = UUID 39 | uuid = build_sig_uuid(service[3]) 40 | services[uuid] = { 41 | 'name': service[1].replace(' ', ''), 42 | } 43 | 44 | def get_characteristics_types(uuid): 45 | if characteristics[uuid]['url'][0] == '/': 46 | url = BT_URL + characteristics[uuid]['url'] 47 | else: 48 | url = characteristics[uuid]['url'] 49 | opener = urlrequest.build_opener(urlrequest.HTTPCookieProcessor(cj)) 50 | opener.addheaders = [{ 'User-Agent', 'Mozilla/5.0' }] 51 | try: 52 | data = opener.open(url).read().decode('utf-8') 53 | print('Downloaded ' + characteristics[uuid]['org']) 54 | except Exception as e: 55 | print('Failed downloading ' + characteristics[uuid]['org']) 56 | return 57 | 58 | for char_type in re.findall(r'([^<]*)<\/Format>', data): 59 | enum_type = 'CHAR_TYPE_' + char_type.replace('-', '_').upper() 60 | characteristics[uuid]['types'].append(enum_type) 61 | characteristics_types[enum_type] = True 62 | 63 | def parse_characteristics(data): 64 | regex = build_gatt_regex('characteristic') 65 | threads = [] 66 | 67 | # Parse all characteristics 68 | for char in re.findall(regex, data): 69 | # char[0] = URL, char[1] = Name, char[2] = Type, char[3] = UUID 70 | uuid = build_sig_uuid(char[3]) 71 | characteristics[uuid] = { 72 | 'url': char[0], 73 | 'name': char[1].replace(' ', '').replace('–', '-'), 74 | 'org': char[2], 75 | 'types': [] 76 | } 77 | 78 | # Download all characteristic definitions 79 | pool = ThreadPool(10) 80 | pool.map(get_characteristics_types, characteristics.keys()) 81 | pool.close() 82 | pool.join() 83 | 84 | def get_list(url, parser): 85 | opener = urlrequest.build_opener(urlrequest.HTTPCookieProcessor(cj)) 86 | opener.addheaders = [{ 'User-Agent', 'Mozilla/5.0' }] 87 | data = opener.open(url).read().decode('utf-8') 88 | parser(data) 89 | 90 | def write_h(filename): 91 | with open(filename, 'w') as outfile: 92 | outfile.write( 93 | '#ifndef GATT_H\n' \ 94 | '#define GATT_H\n' \ 95 | '\n' \ 96 | '#include "ble_utils.h"\n' \ 97 | '\n' \ 98 | 'typedef enum {\n' \ 99 | ' CHAR_TYPE_UNKNOWN,\n' 100 | ' %s\n' 101 | '} characteristic_type_t;\n' \ 102 | '\n' \ 103 | 'typedef struct {\n' \ 104 | ' ble_uuid_t uuid;\n' \ 105 | ' char *name;\n' \ 106 | ' characteristic_type_t *types;\n' \ 107 | '} characteristic_desc_t;\n' \ 108 | '\n' \ 109 | 'typedef struct {\n' \ 110 | ' ble_uuid_t uuid;\n' \ 111 | ' char *name;\n' \ 112 | '} service_desc_t;\n' \ 113 | '\n' \ 114 | 'extern service_desc_t services[];\n' \ 115 | 'extern characteristic_desc_t characteristics[];\n' \ 116 | '\n' \ 117 | '#endif' % (',\n '.join(sorted(characteristics_types.keys()))) 118 | ) 119 | outfile.close() 120 | 121 | def write_c(filename): 122 | with open(filename, 'w') as outfile: 123 | outfile.write( 124 | '#include "gatt.h"\n' \ 125 | '#include \n' \ 126 | '\n' \ 127 | 'service_desc_t services[] = {\n'); 128 | 129 | # Write services definitions 130 | for uuid, service in sorted(services.items()): 131 | outfile.write( 132 | ' {\n' \ 133 | ' .uuid = { %s },\n' \ 134 | ' .name = "%s",\n' \ 135 | ' },\n' % (', '.join('0x{0}'.format(t) for t in 136 | re.findall('..', uuid.replace('-', ''))[::-1]), service['name'])); 137 | 138 | # Terminate services list 139 | outfile.write( 140 | ' {}\n' \ 141 | '};\n' \ 142 | '\n'); 143 | 144 | # Write characteristic types for each characteristic 145 | for uuid, char in sorted(characteristics.items()): 146 | outfile.write('static characteristic_type_t types_%s[] = { %s-1 };\n' % 147 | (uuid.replace('-', '_'), 148 | ''.join('{0}, '.format(t) for t in char['types']))); 149 | 150 | # Write characteristics definitions 151 | outfile.write('\ncharacteristic_desc_t characteristics[] = {\n' ); 152 | for uuid, char in sorted(characteristics.items()): 153 | outfile.write( 154 | ' {\n' \ 155 | ' .uuid = { %s },\n' \ 156 | ' .name = "%s",\n' \ 157 | ' .types = types_%s,\n' \ 158 | ' },\n' % 159 | (', '.join('0x{0}'.format(t) for t in 160 | re.findall('..', uuid.replace('-', ''))[::-1]), char['name'], 161 | uuid.replace('-', '_'))); 162 | 163 | # Terminate characteristics list 164 | outfile.write( 165 | ' {}\n' \ 166 | '};'); 167 | outfile.close 168 | 169 | def main(): 170 | parser = argparse.ArgumentParser(description='Obtain Bluetooth SIG GATT IDs') 171 | parser.add_argument('-C', metavar='gatt.c', help='Output C file') 172 | parser.add_argument('-H', metavar='gatt.h', help='Output H file') 173 | args = parser.parse_args() 174 | 175 | print('Getting list of services') 176 | get_list(SERVICES_URL, parse_services) 177 | print('Getting list of characteristics') 178 | get_list(CHARACTERISTICS_URL, parse_characteristics) 179 | 180 | print('Generating source code') 181 | write_h(args.H) 182 | write_c(args.C) 183 | 184 | if __name__ == '__main__': 185 | main() 186 | -------------------------------------------------------------------------------- /main/wifi.c: -------------------------------------------------------------------------------- 1 | #include "wifi.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | static const char *TAG = "WiFi"; 12 | 13 | static wifi_on_connected_cb_t on_connected_cb = NULL; 14 | static wifi_on_disconnected_cb_t on_disconnected_cb = NULL; 15 | static char *wifi_hostname = NULL; 16 | static esp_netif_t *wifi_netif = NULL; 17 | 18 | void wifi_set_on_connected_cb(wifi_on_connected_cb_t cb) 19 | { 20 | on_connected_cb = cb; 21 | } 22 | 23 | void wifi_set_on_disconnected_cb(wifi_on_disconnected_cb_t cb) 24 | { 25 | on_disconnected_cb = cb; 26 | } 27 | 28 | uint8_t *wifi_mac_get(void) 29 | { 30 | static uint8_t mac[6] = {}; 31 | 32 | if (!mac[0]) 33 | esp_wifi_get_mac(ESP_IF_WIFI_STA, mac); 34 | 35 | return mac; 36 | } 37 | 38 | void wifi_hostname_set(const char *hostname) 39 | { 40 | if (wifi_hostname) 41 | free(wifi_hostname); 42 | wifi_hostname = strdup(hostname); 43 | } 44 | 45 | static void event_handler(void* arg, esp_event_base_t event_base, 46 | int32_t event_id, void* event_data) 47 | { 48 | if (event_base == WIFI_EVENT) 49 | { 50 | switch(event_id) { 51 | case WIFI_EVENT_STA_START: 52 | if (wifi_hostname) 53 | esp_netif_set_hostname(wifi_netif, wifi_hostname); 54 | esp_wifi_connect(); 55 | break; 56 | case WIFI_EVENT_STA_CONNECTED: 57 | ESP_LOGI(TAG, "Connected"); 58 | break; 59 | case WIFI_EVENT_STA_DISCONNECTED: 60 | ESP_LOGI(TAG, "Disconnected"); 61 | if (on_disconnected_cb) 62 | on_disconnected_cb(); 63 | /* This is a workaround as ESP32 WiFi libs don't currently 64 | * auto-reassociate. */ 65 | esp_wifi_connect(); 66 | break; 67 | default: 68 | ESP_LOGD(TAG, "Unhandled WiFi event (%" PRId32 ")", event_id); 69 | break; 70 | } 71 | } 72 | else if (event_base == IP_EVENT) 73 | { 74 | switch(event_id) { 75 | case IP_EVENT_STA_GOT_IP: 76 | { 77 | ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; 78 | 79 | ESP_LOGD(TAG, "Got IP address: %s", 80 | inet_ntoa(event->ip_info.ip)); 81 | if (on_connected_cb) 82 | on_connected_cb(); 83 | break; 84 | } 85 | case IP_EVENT_STA_LOST_IP: 86 | ESP_LOGD(TAG, "Lost IP address"); 87 | break; 88 | default: 89 | ESP_LOGD(TAG, "Unhandled IP event (%" PRId32 ")", event_id); 90 | break; 91 | } 92 | } 93 | } 94 | 95 | eap_method_t wifi_eap_atomethod(const char *method) 96 | { 97 | if (method == NULL) 98 | return EAP_NONE; 99 | 100 | struct { 101 | const char *name; 102 | int method; 103 | } *p, methods[] = { 104 | { "TLS", EAP_TLS }, 105 | { "PEAP", EAP_PEAP }, 106 | { "TTLS", EAP_TTLS }, 107 | { NULL, EAP_NONE } 108 | }; 109 | 110 | for (p = methods; p->name; p++) 111 | { 112 | if (!strcmp(p->name, method)) 113 | break; 114 | } 115 | 116 | return p->method; 117 | } 118 | 119 | int wifi_start_ap(const char *ssid, const char *password) 120 | { 121 | wifi_config_t wifi_config = { .ap = { .max_connection = 1 } }; 122 | strncpy((char *)wifi_config.ap.ssid, ssid, 32); 123 | if (password) 124 | { 125 | strncpy((char *)wifi_config.sta.password, password, 64); 126 | wifi_config.ap.authmode = WIFI_AUTH_WPA2_PSK; 127 | } 128 | else 129 | wifi_config.ap.authmode = WIFI_AUTH_OPEN; 130 | 131 | wifi_netif = esp_netif_create_default_wifi_ap(); 132 | ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); 133 | ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config)); 134 | ESP_ERROR_CHECK(esp_wifi_start()); 135 | 136 | return 0; 137 | } 138 | 139 | int wifi_connect(const char *ssid, const char *password, 140 | eap_method_t eap_method, const char *eap_identity, 141 | const char *eap_username, const char *eap_password, 142 | const char *ca_cert, const char *client_cert, const char *client_key) 143 | { 144 | wifi_config_t wifi_config = { 145 | .sta = { 146 | .scan_method = WIFI_ALL_CHANNEL_SCAN, 147 | } 148 | }; 149 | strncpy((char *)wifi_config.sta.ssid, ssid, 32); 150 | if (password) 151 | strncpy((char *)wifi_config.sta.password, password, 64); 152 | 153 | ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); 154 | ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); 155 | if (eap_method) 156 | { 157 | if (ca_cert) 158 | { 159 | ESP_ERROR_CHECK(esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *)ca_cert, 160 | strlen(ca_cert))); 161 | } 162 | if (client_cert) 163 | { 164 | ESP_ERROR_CHECK(esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *)client_cert, 165 | strlen(client_cert), (uint8_t *)client_key, 166 | client_key ? strlen(client_key) : 0, NULL, 0)); 167 | } 168 | if (eap_identity) 169 | { 170 | ESP_ERROR_CHECK(esp_wifi_sta_wpa2_ent_set_identity((uint8_t *)eap_identity, 171 | strlen(eap_identity))); 172 | } 173 | if (eap_method == EAP_PEAP || eap_method == EAP_TTLS) 174 | { 175 | if (eap_username || eap_password) 176 | { 177 | ESP_ERROR_CHECK(esp_wifi_sta_wpa2_ent_set_username((uint8_t *)eap_username, 178 | strlen(eap_username))); 179 | ESP_ERROR_CHECK(esp_wifi_sta_wpa2_ent_set_password((uint8_t *)eap_password, 180 | strlen(eap_password))); 181 | } 182 | else 183 | { 184 | ESP_LOGE(TAG, "Username and password are required for " 185 | "Tunneled TLS or Protected EAP"); 186 | } 187 | } 188 | ESP_ERROR_CHECK(esp_wifi_sta_wpa2_ent_enable()); 189 | } 190 | ESP_LOGI(TAG, "Connecting to SSID %s", wifi_config.sta.ssid); 191 | ESP_ERROR_CHECK(esp_wifi_start()); 192 | ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(78)); 193 | 194 | return 0; 195 | } 196 | 197 | int wifi_reconnect(void) 198 | { 199 | return esp_wifi_disconnect(); 200 | } 201 | 202 | int wifi_initialize(void) 203 | { 204 | ESP_LOGD(TAG, "Initializing WiFi station"); 205 | ESP_ERROR_CHECK(esp_netif_init()); 206 | ESP_ERROR_CHECK(esp_event_loop_create_default()); 207 | wifi_netif = esp_netif_create_default_wifi_sta(); 208 | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 209 | ESP_ERROR_CHECK(esp_wifi_init(&cfg)); 210 | ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); 211 | 212 | ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); 213 | ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL)); 214 | 215 | return 0; 216 | } 217 | -------------------------------------------------------------------------------- /main/ota.c: -------------------------------------------------------------------------------- 1 | #include "ota.h" 2 | #include "config.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | /* Constants */ 14 | static const char *TAG = "OTA"; 15 | 16 | /* Types */ 17 | typedef struct { 18 | ota_type_t type; 19 | int (*begin)(void **handle); 20 | int (*write)(void *handle, uint8_t *data, size_t len); 21 | int (*end)(void *handle); 22 | char *(*version_get)(void); 23 | } ota_ops_t; 24 | 25 | typedef struct { 26 | char *url; 27 | ota_on_completed_cb_t on_completed_cb; 28 | } ota_download_ctx; 29 | 30 | /* Internal state */ 31 | struct { 32 | int in_progress; 33 | ota_ops_t *ops; 34 | size_t bytes_written; 35 | void *handle; 36 | } ota_ctx; 37 | 38 | char *ota_err_to_str(ota_err_t err) 39 | { 40 | switch (err) 41 | { 42 | case OTA_ERR_SUCCESS: return "Success"; 43 | case OTA_ERR_NO_CHANGE: return "No change"; 44 | case OTA_ERR_IN_PROGRESS: return "In progress"; 45 | case OTA_ERR_FAILED_DOWNLOAD: return "Failed downloading file"; 46 | case OTA_ERR_FAILED_BEGIN: return "Failed initializing OTA process"; 47 | case OTA_ERR_FAILED_WRITE: return "Failed writing data"; 48 | case OTA_ERR_FAILED_END: return "Failed finalizing OTA process"; 49 | } 50 | 51 | return "Invalid OTA error"; 52 | } 53 | 54 | /* Config OTA Wrappers */ 55 | static int ota_config_begin(void **handle) 56 | { 57 | return config_update_begin((config_update_handle_t **)handle); 58 | } 59 | 60 | static int ota_config_write(void *handle, uint8_t *data, size_t len) 61 | { 62 | return config_update_write((config_update_handle_t *)handle, data, len); 63 | } 64 | 65 | static int ota_config_end(void *handle) 66 | { 67 | return config_update_end((config_update_handle_t *)handle); 68 | } 69 | 70 | static char *ota_config_version_get(void) 71 | { 72 | return config_version_get(); 73 | } 74 | 75 | static ota_ops_t ota_config_ops = { 76 | .type = OTA_TYPE_CONFIG, 77 | .begin = ota_config_begin, 78 | .write = ota_config_write, 79 | .end = ota_config_end, 80 | .version_get = ota_config_version_get, 81 | }; 82 | 83 | /* Firmware OTA Wrappers */ 84 | static int ota_firmware_begin(void **handle) 85 | { 86 | const esp_partition_t *configured = esp_ota_get_boot_partition(); 87 | const esp_partition_t *running = esp_ota_get_running_partition(); 88 | const esp_partition_t *update = NULL; 89 | esp_ota_handle_t update_handle = 0; 90 | esp_err_t err; 91 | 92 | if (configured != running) 93 | { 94 | ESP_LOGW(TAG, "Configured OTA boot partition is different than " 95 | "running partition"); 96 | } 97 | 98 | if (!(update = esp_ota_get_next_update_partition(NULL))) 99 | { 100 | ESP_LOGE(TAG, "Failed getting update partition"); 101 | return OTA_ERR_FAILED_BEGIN; 102 | } 103 | 104 | ESP_LOGI(TAG, "Running partition type 0x%0x subtype 0x%0x (offset 0x%08" 105 | PRIx32 ")", running->type, running->subtype, running->address); 106 | ESP_LOGI(TAG, "Writing partition type 0x%0x subtype 0x%0x (offset 0x%08" 107 | PRIx32 ")", update->type, update->subtype, update->address); 108 | 109 | err = esp_ota_begin(update, OTA_SIZE_UNKNOWN, &update_handle); 110 | if (err != ESP_OK) 111 | { 112 | ESP_LOGE(TAG, "Failed beginning OTA: 0x%x", err); 113 | return OTA_ERR_FAILED_BEGIN; 114 | } 115 | 116 | *handle = (void *)update_handle; 117 | 118 | return 0; 119 | } 120 | 121 | static int ota_firmware_write(void *handle, uint8_t *data, size_t len) 122 | { 123 | esp_ota_handle_t update_handle = (esp_ota_handle_t)handle; 124 | esp_err_t err = esp_ota_write(update_handle, (const void *)data, len); 125 | 126 | if (err != ESP_OK) 127 | { 128 | ESP_LOGE(TAG, "Failed writing OTA: 0x%x", err); 129 | return OTA_ERR_FAILED_WRITE; 130 | } 131 | 132 | return 0; 133 | } 134 | 135 | static int ota_firmware_end(void *handle) 136 | { 137 | esp_ota_handle_t update_handle = (esp_ota_handle_t)handle; 138 | const esp_partition_t *update = esp_ota_get_next_update_partition(NULL); 139 | esp_err_t err = esp_ota_end(update_handle); 140 | 141 | if (!update || err != ESP_OK) 142 | { 143 | ESP_LOGE(TAG, "Failed ending OTA: 0x%x", err); 144 | return OTA_ERR_FAILED_END; 145 | } 146 | 147 | ESP_LOGI(TAG, "Setting boot partition type 0x%0x subtype 0x%0x (offset " 148 | "0x%08" PRIx32 ")", update->type, update->subtype, update->address); 149 | 150 | err = esp_ota_set_boot_partition(update); 151 | if (err != ESP_OK) 152 | { 153 | ESP_LOGE(TAG, "Failed setting boot partition: 0x%x", err); 154 | return OTA_ERR_FAILED_END; 155 | } 156 | 157 | return 0; 158 | } 159 | 160 | static char *ota_firmware_version_get(void) 161 | { 162 | return BLE2MQTT_VER; 163 | } 164 | 165 | static ota_ops_t ota_firmware_ops = { 166 | .type = OTA_TYPE_FIRMWARE, 167 | .begin = ota_firmware_begin, 168 | .write = ota_firmware_write, 169 | .end = ota_firmware_end, 170 | .version_get = ota_firmware_version_get, 171 | }; 172 | 173 | static int http_event_cb(esp_http_client_event_t *event) 174 | { 175 | if (event->event_id != HTTP_EVENT_ON_DATA) 176 | return ESP_OK; 177 | 178 | if (ota_write(event->data, event->data_len)) 179 | return ESP_FAIL; 180 | 181 | return ESP_OK; 182 | } 183 | 184 | static void ota_task(void *pvParameter) 185 | { 186 | ota_download_ctx *ctx = (ota_download_ctx *)pvParameter; 187 | esp_http_client_handle_t handle; 188 | char header[128]; 189 | int http_status = -1; 190 | ota_err_t err; 191 | esp_http_client_config_t config = { 192 | .event_handler = http_event_cb, 193 | .method = HTTP_METHOD_GET, 194 | .url = ctx->url, 195 | .buffer_size = 2048, 196 | }; 197 | 198 | ESP_LOGI(TAG, "Starting OTA from %s", ctx->url); 199 | handle = esp_http_client_init(&config); 200 | 201 | /* Set HTTP headers */ 202 | sprintf(header, "BLE2MQTT/%s", BLE2MQTT_VER); 203 | esp_http_client_set_header(handle, "User-Agent", header); 204 | sprintf(header, "\"%s\"", ota_ctx.ops->version_get()); 205 | esp_http_client_set_header(handle, "If-None-Match", header); 206 | 207 | /* Start HTTP request */ 208 | if (esp_http_client_perform(handle) == ESP_OK) 209 | http_status = esp_http_client_get_status_code(handle); 210 | 211 | ESP_LOGI(TAG, "HTTP request response: %d, read %" PRId64 " (%zu) bytes", 212 | http_status, esp_http_client_get_content_length(handle), 213 | ota_ctx.bytes_written); 214 | 215 | err = ota_close(); 216 | if (http_status != 200 && http_status != 304) 217 | err = OTA_ERR_FAILED_DOWNLOAD; 218 | 219 | if (ctx->on_completed_cb) 220 | ctx->on_completed_cb(ota_ctx.ops->type, err); 221 | 222 | free(ctx->url); 223 | free(ctx); 224 | esp_http_client_cleanup(handle); 225 | vTaskDelete(NULL); 226 | } 227 | 228 | int ota_download(ota_type_t type, const char *url, ota_on_completed_cb_t cb) 229 | { 230 | int ret; 231 | ota_download_ctx *ctx; 232 | 233 | if ((ret = ota_open(type))) 234 | return ret; 235 | 236 | ctx = malloc(sizeof(*ctx)); 237 | ctx->url = strdup(url); 238 | ctx->on_completed_cb = cb; 239 | 240 | xTaskCreatePinnedToCore(ota_task, "ota_task", 8192, ctx, 5, NULL, 1); 241 | 242 | return 0; 243 | } 244 | 245 | ota_err_t ota_open(ota_type_t type) 246 | { 247 | if (ota_ctx.in_progress) 248 | return OTA_ERR_IN_PROGRESS; 249 | 250 | ota_ctx.ops = type == OTA_TYPE_FIRMWARE ? 251 | &ota_firmware_ops : &ota_config_ops; 252 | 253 | ota_ctx.in_progress = 1; 254 | ota_ctx.bytes_written = 0; 255 | 256 | return 0; 257 | } 258 | 259 | ota_err_t ota_write(uint8_t *data, size_t len) 260 | { 261 | if (!ota_ctx.in_progress) 262 | return OTA_ERR_FAILED_WRITE; 263 | 264 | if (!ota_ctx.bytes_written) 265 | { 266 | if (ota_ctx.ops->begin(&ota_ctx.handle)) 267 | return OTA_ERR_FAILED_BEGIN; 268 | } 269 | 270 | if (ota_ctx.ops->write(ota_ctx.handle, data, len)) 271 | { 272 | ESP_LOGE(TAG, "Failed writing data"); 273 | return OTA_ERR_FAILED_WRITE; 274 | } 275 | ota_ctx.bytes_written += len; 276 | ESP_LOGI(TAG, "Wrote %d bytes (total: %d)", len, ota_ctx.bytes_written); 277 | 278 | return 0; 279 | } 280 | 281 | ota_err_t ota_close(void) 282 | { 283 | if (!ota_ctx.in_progress) 284 | return OTA_ERR_FAILED_END; 285 | 286 | ota_ctx.in_progress = 0; 287 | 288 | if (!ota_ctx.bytes_written) 289 | return OTA_ERR_NO_CHANGE; 290 | return ota_ctx.ops->end(ota_ctx.handle) ? 291 | OTA_ERR_FAILED_END : OTA_ERR_SUCCESS; 292 | } 293 | 294 | int ota_initialize(void) 295 | { 296 | ESP_LOGI(TAG, "Initializing OTA"); 297 | return 0; 298 | } 299 | -------------------------------------------------------------------------------- /www/js/ble2mqtt.js: -------------------------------------------------------------------------------- 1 | const RESTART = '/restart'; 2 | const OTA_UPLOAD = '/ota/'; 3 | const BLE_DB = '/ble/bonding_db'; 4 | const BLE_DEVICES = '/ble/devices'; 5 | const CONFIG_FILE_PATH = '/fs/config.json'; 6 | const LATEST_RELEASE = 'https://github.com/shmuelzon/esp32-ble2mqtt/releases'; 7 | const STATUS = '/status'; 8 | 9 | function progress(show = true) { 10 | document.getElementById('progress').style.display = show ? 'flex' : 'none'; 11 | // disable\enable all buttons 12 | [].concat( 13 | Array.from(document.getElementsByTagName('button')), //all buttons 14 | Array.from(document.getElementsByClassName('button')) //all elements same button 15 | ) 16 | .map(item => { 17 | if (show) item.setAttribute('disabled', show) 18 | else item.removeAttribute('disabled') 19 | }); 20 | } 21 | 22 | function toaster(message) { 23 | document.getElementById('message').style.display = 'block'; 24 | document.getElementById('message').innerText = message; 25 | setTimeout(() => document.getElementById('message').style.display = 'none', 3000); 26 | } 27 | 28 | function restart() { 29 | fetch(RESTART, { 30 | method: 'POST', 31 | }) 32 | .then((res) => { 33 | window.open("/busy.html", "_self") 34 | }) 35 | .catch((err) => { 36 | toaster("Can't restart"); 37 | console.error(err) 38 | }) 39 | } 40 | 41 | function getStatus() { 42 | fetch(STATUS) 43 | .then(response => response.json()) 44 | .then(json => { 45 | document.getElementById('software-version').innerHTML = json.version; 46 | }); 47 | } 48 | 49 | function getLatestReleaseInfo() { 50 | fetch('https://api.github.com/repos/shmuelzon/esp32-ble2mqtt/tags') 51 | .then(response => response.json()) 52 | .then(json => { 53 | let release = json[0]; 54 | if (release) document.getElementById('latest-release-id').innerHTML = `(latest ${release.name})`; 55 | }); 56 | } 57 | 58 | function otaStartUpload(type) { 59 | let file = document.getElementById(type + '-file').files[0]; 60 | if (!file) 61 | return; 62 | progress(true); 63 | fetch(OTA_UPLOAD + type, { 64 | method: 'POST', 65 | headers: { 66 | 'Content-Type': 'application/octet-stream' 67 | }, 68 | body: file 69 | }) 70 | .then((res) => { 71 | toaster("Upload complete"); 72 | window.open("/busy.html", "_self") 73 | }) 74 | .catch((err) => { 75 | toaster(`Can't upload ${type}. Please refresh page`); 76 | console.error(err) 77 | }) 78 | } 79 | 80 | function bleClearBonding() { 81 | progress(true); 82 | fetch(BLE_DB, { 83 | method: 'DELETE', 84 | }) 85 | .then((res) => { 86 | progress(false); 87 | toaster("Cleared"); 88 | console.log("cleared") 89 | }) 90 | .catch((err) => { 91 | toaster("Can't clear ble"); 92 | console.error(err) 93 | }) 94 | } 95 | 96 | function downloadConfig() { 97 | fetch(CONFIG_FILE_PATH, { 98 | method: 'GET', 99 | cache: 'no-store' 100 | }) 101 | .then(response => response.json()) 102 | .then(json => { 103 | document.getElementById('config-file').innerHTML = JSON.stringify(json, null, 2); 104 | }) 105 | .catch((err) => { 106 | toaster("Can't download config file"); 107 | console.error(err) 108 | }) 109 | } 110 | 111 | function uploadConfigFile() { 112 | let file = new File([document.getElementById('config-file').value], "config.json", { 113 | type: "text/plain", 114 | }); 115 | if (file.size === 0) { 116 | toaster("File is empty"); 117 | return; 118 | } 119 | progress(true); 120 | fetch(CONFIG_FILE_PATH, { 121 | method: 'POST', 122 | headers: { 123 | 'Content-Type': 'application/octet-stream' 124 | }, 125 | body: file 126 | }) 127 | .then(response => { 128 | restart(); 129 | }) 130 | .catch((err) => { 131 | toaster("Can't upload config file"); 132 | console.error(err) 133 | }) 134 | } 135 | 136 | function bleListUpdate() { 137 | progress(true); 138 | fetch(BLE_DEVICES, { 139 | method: 'GET', 140 | }) 141 | .then(response => { 142 | if (!response.ok) { 143 | throw new Error("HTTP status " + response.status); 144 | } 145 | return response.json(); 146 | }) 147 | .then(json => { 148 | progress(false); 149 | // render BLE table 150 | document.getElementById('ble-list').innerHTML = json.map(item => { 151 | return ` 152 | 153 | ${item.name || "[None]"} 154 | ${item.mac} 155 | `; 156 | }).join('\n'); 157 | }) 158 | .catch((err) => { 159 | progress(false); 160 | toaster("Can't update list of ble"); 161 | console.error(err) 162 | }) 163 | } 164 | 165 | 166 | function loadFileManager(path) { 167 | path ||= document.getElementById('file-manager-path').innerText; 168 | 169 | fetch('/fs' + path, 170 | { 171 | method: "GET", 172 | }) 173 | .then(response => { 174 | return response.json(); 175 | }) 176 | .then(data => { 177 | document.getElementById('file-manager-list').innerHTML = 178 | ` 179 | ..${path} 180 | 181 | 182 | ` + 183 | data.map(entry => { 184 | let name, del, download = ''; 185 | if (entry.type === 'directory') { 186 | name = `📁 ${entry.name}`; 187 | } else { 188 | name = entry.name; 189 | del = ``; 190 | download = ``; 191 | } 192 | return ` 193 | ${name} 194 | ${entry.type === 'file' ? entry.size : ''} 195 | ${del} 196 | ${download} 197 | 198 | ` 199 | }).join('\n') 200 | 201 | }) 202 | .catch(err => { 203 | toaster(`Can't get ${path}`); 204 | console.error(err); 205 | } 206 | ) 207 | } 208 | 209 | function uploadFile() { 210 | let file = document.getElementById('file-manager-file').files[0]; 211 | if (!file) { 212 | toaster("File is empty"); 213 | return; 214 | } 215 | let path = document.getElementById('file-manager-path').innerText; 216 | progress(true); 217 | fetch('/fs' + path + file.name, { 218 | method: 'POST', 219 | headers: { 220 | 'Content-Type': 'application/octet-stream' 221 | }, 222 | body: file 223 | }) 224 | .then(response => { 225 | toaster("OK"); 226 | progress(false); 227 | loadFileManager(path); 228 | }) 229 | .catch((err) => { 230 | toaster("Can't upload file"); 231 | console.error(err) 232 | }) 233 | } 234 | 235 | function deleteFile(fullName) { 236 | if (!fullName) { 237 | return; 238 | } 239 | fetch('/fs' + fullName, { 240 | method: 'DELETE', 241 | headers: { 242 | 'Content-Type': 'application/octet-stream' 243 | }, 244 | }) 245 | .then(response => { 246 | toaster("OK"); 247 | progress(false); 248 | loadFileManager(); 249 | }) 250 | .catch((err) => { 251 | toaster("Can't delete file"); 252 | console.error(err) 253 | }) 254 | } 255 | 256 | document.addEventListener("DOMContentLoaded", event => { 257 | getStatus(); // device status 258 | getLatestReleaseInfo(); // get github version 259 | bleListUpdate() // update ble table 260 | }); 261 | -------------------------------------------------------------------------------- /main/mqtt.c: -------------------------------------------------------------------------------- 1 | #include "mqtt.h" 2 | #include "resolve.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | /* Constants */ 9 | static const char *TAG = "MQTT"; 10 | 11 | /* Types */ 12 | typedef struct mqtt_subscription_t { 13 | struct mqtt_subscription_t *next; 14 | char *topic; 15 | mqtt_on_message_received_cb_t cb; 16 | void *ctx; 17 | mqtt_free_ctx_cb_t free_cb; 18 | } mqtt_subscription_t; 19 | 20 | typedef struct mqtt_publications_t { 21 | struct mqtt_publications_t *next; 22 | char *topic; 23 | uint8_t *payload; 24 | size_t len; 25 | int qos; 26 | uint8_t retained; 27 | } mqtt_publications_t; 28 | 29 | /* Internal state */ 30 | static esp_mqtt_client_handle_t mqtt_handle = NULL; 31 | static mqtt_subscription_t *subscription_list = NULL; 32 | static mqtt_publications_t *publications_list = NULL; 33 | static uint8_t is_connected = 0; 34 | 35 | /* Callback functions */ 36 | static mqtt_on_connected_cb_t on_connected_cb = NULL; 37 | static mqtt_on_disconnected_cb_t on_disconnected_cb = NULL; 38 | 39 | void mqtt_set_on_connected_cb(mqtt_on_connected_cb_t cb) 40 | { 41 | on_connected_cb = cb; 42 | } 43 | 44 | void mqtt_set_on_disconnected_cb(mqtt_on_disconnected_cb_t cb) 45 | { 46 | on_disconnected_cb = cb; 47 | } 48 | 49 | static mqtt_subscription_t *mqtt_subscription_add(mqtt_subscription_t **list, 50 | const char *topic, mqtt_on_message_received_cb_t cb, void *ctx, 51 | mqtt_free_ctx_cb_t free_cb) 52 | { 53 | mqtt_subscription_t *sub, **cur; 54 | 55 | sub = malloc(sizeof(*sub)); 56 | sub->next = NULL; 57 | sub->topic = strdup(topic); 58 | sub->cb = cb; 59 | sub->ctx = ctx; 60 | sub->free_cb = free_cb; 61 | 62 | for (cur = list; *cur; cur = &(*cur)->next); 63 | *cur = sub; 64 | 65 | return sub; 66 | } 67 | 68 | static void mqtt_subscription_free(mqtt_subscription_t *mqtt_subscription) 69 | { 70 | if (mqtt_subscription->ctx && mqtt_subscription->free_cb) 71 | mqtt_subscription->free_cb(mqtt_subscription->ctx); 72 | free(mqtt_subscription->topic); 73 | free(mqtt_subscription); 74 | } 75 | 76 | static void mqtt_subscriptions_free(mqtt_subscription_t **list) 77 | { 78 | mqtt_subscription_t *cur, **head = list; 79 | 80 | while (*list) 81 | { 82 | cur = *list; 83 | *list = cur->next; 84 | mqtt_subscription_free(cur); 85 | } 86 | *head = NULL; 87 | } 88 | 89 | static void mqtt_subscription_remove(mqtt_subscription_t **list, 90 | const char *topic) 91 | { 92 | mqtt_subscription_t **cur, *tmp; 93 | 94 | for (cur = list; *cur; cur = &(*cur)->next) 95 | { 96 | if (!strcmp((*cur)->topic, topic)) 97 | break; 98 | } 99 | 100 | if (!*cur) 101 | return; 102 | 103 | tmp = *cur; 104 | *cur = (*cur)->next; 105 | mqtt_subscription_free(tmp); 106 | } 107 | 108 | static mqtt_publications_t *mqtt_publication_add(mqtt_publications_t **list, 109 | const char *topic, uint8_t *payload, size_t len, int qos, uint8_t retained) 110 | { 111 | mqtt_publications_t *pub = malloc(sizeof(*pub)); 112 | 113 | pub->topic = strdup(topic); 114 | pub->payload = malloc(len); 115 | memcpy(pub->payload, payload, len); 116 | pub->len = len; 117 | pub->qos = qos; 118 | pub->retained = retained; 119 | 120 | pub->next = *list; 121 | *list = pub; 122 | 123 | return pub; 124 | } 125 | 126 | static void mqtt_publication_free(mqtt_publications_t *mqtt_publication) 127 | { 128 | free(mqtt_publication->topic); 129 | free(mqtt_publication->payload); 130 | free(mqtt_publication); 131 | } 132 | 133 | static void mqtt_publications_free(mqtt_publications_t **list) 134 | { 135 | mqtt_publications_t *cur, **head = list; 136 | 137 | while (*list) 138 | { 139 | cur = *list; 140 | *list = cur->next; 141 | mqtt_publication_free(cur); 142 | } 143 | *head = NULL; 144 | } 145 | 146 | static void mqtt_publications_publish(mqtt_publications_t *list) 147 | { 148 | for (; list; list = list->next) 149 | { 150 | ESP_LOGI(TAG, "Publishing from queue: %s = %.*s", list->topic, 151 | list->len, list->payload); 152 | 153 | mqtt_publish(list->topic, list->payload, list->len, list->qos, 154 | list->retained); 155 | } 156 | } 157 | 158 | int mqtt_subscribe(const char *topic, int qos, mqtt_on_message_received_cb_t cb, 159 | void *ctx, mqtt_free_ctx_cb_t free_cb) 160 | { 161 | if (!is_connected) 162 | return -1; 163 | 164 | ESP_LOGD(TAG, "Subscribing to %s", topic); 165 | if (esp_mqtt_client_subscribe(mqtt_handle, topic, qos) < 0) 166 | { 167 | ESP_LOGE(TAG, "Failed subscribing to %s", topic); 168 | return -1; 169 | } 170 | 171 | mqtt_subscription_add(&subscription_list, topic, cb, ctx, free_cb); 172 | return 0; 173 | } 174 | 175 | int mqtt_unsubscribe_topic_prefix(const char *topic_prefix) 176 | { 177 | mqtt_subscription_t *tmp, **cur = &subscription_list; 178 | size_t prefix_len = strlen(topic_prefix); 179 | 180 | ESP_LOGD(TAG, "Unsubscribing topics with %s prefix", topic_prefix); 181 | 182 | while (*cur) 183 | { 184 | tmp = *cur; 185 | if (strncmp(topic_prefix, (*cur)->topic, prefix_len)) 186 | { 187 | cur = &(*cur)->next; 188 | continue; 189 | } 190 | *cur = (*cur)->next; 191 | 192 | ESP_LOGD(TAG, "Unsubscribing from %s", tmp->topic); 193 | if (is_connected) 194 | esp_mqtt_client_unsubscribe(mqtt_handle, tmp->topic); 195 | mqtt_subscription_free(tmp); 196 | } 197 | 198 | return 0; 199 | } 200 | 201 | int mqtt_unsubscribe(const char *topic) 202 | { 203 | ESP_LOGD(TAG, "Unsubscribing from %s", topic); 204 | mqtt_subscription_remove(&subscription_list, topic); 205 | 206 | if (!is_connected) 207 | return 0; 208 | 209 | return esp_mqtt_client_unsubscribe(mqtt_handle, topic); 210 | } 211 | 212 | int mqtt_publish(const char *topic, uint8_t *payload, size_t len, int qos, 213 | uint8_t retained) 214 | { 215 | if (is_connected) 216 | { 217 | return esp_mqtt_client_publish(mqtt_handle, (char *)topic, 218 | (char *)payload, len, qos, retained) < 0; 219 | } 220 | 221 | /* If we're currently not connected, queue publication */ 222 | ESP_LOGD(TAG, "MQTT is disconnected, adding publication to queue..."); 223 | mqtt_publication_add(&publications_list, topic, payload, len, qos, 224 | retained); 225 | 226 | return 0; 227 | } 228 | 229 | static void mqtt_message_cb(const char *topic, size_t topic_len, 230 | uint8_t *payload, size_t len) 231 | { 232 | mqtt_subscription_t *cur; 233 | 234 | ESP_LOGD(TAG, "Received: %.*s => %.*s (%d)\n", topic_len, topic, len, 235 | payload, (int)len); 236 | 237 | for (cur = subscription_list; cur; cur = cur->next) 238 | { 239 | /* TODO: Correctly match MQTT topics (i.e. support wildcards) */ 240 | if (strncmp(cur->topic, topic, topic_len) || 241 | cur->topic[topic_len] != '\0') 242 | { 243 | continue; 244 | } 245 | 246 | cur->cb(cur->topic, payload, len, cur->ctx); 247 | } 248 | } 249 | 250 | static void mqtt_event_cb(void *handler_args, esp_event_base_t base, 251 | int32_t event_id, void *event_data) 252 | { 253 | esp_mqtt_event_handle_t event = event_data; 254 | 255 | switch ((esp_mqtt_event_id_t)event_id) { 256 | case MQTT_EVENT_CONNECTED: 257 | ESP_LOGI(TAG, "MQTT client connected"); 258 | is_connected = 1; 259 | mqtt_publications_publish(publications_list); 260 | mqtt_publications_free(&publications_list); 261 | if (on_connected_cb) 262 | on_connected_cb(); 263 | break; 264 | case MQTT_EVENT_DISCONNECTED: 265 | ESP_LOGI(TAG, "MQTT client disconnected"); 266 | is_connected = 0; 267 | mqtt_subscriptions_free(&subscription_list); 268 | if (on_disconnected_cb) 269 | on_disconnected_cb(); 270 | break; 271 | case MQTT_EVENT_DATA: 272 | mqtt_message_cb(event->topic, event->topic_len, (uint8_t *)event->data, 273 | event->data_len); 274 | break; 275 | default: 276 | break; 277 | } 278 | } 279 | 280 | int mqtt_connect(const char *host, uint16_t port, const char *client_id, 281 | const char *username, const char *password, uint8_t ssl, 282 | const char *server_cert, const char *client_cert, const char *client_key, 283 | const char *lwt_topic, const char *lwt_msg, uint8_t lwt_qos, 284 | uint8_t lwt_retain) 285 | { 286 | esp_mqtt_client_config_t config = { 287 | .broker = { 288 | .address = { 289 | .hostname = resolve_host(host), 290 | .port = port, 291 | .transport = 292 | ssl ? MQTT_TRANSPORT_OVER_SSL : MQTT_TRANSPORT_OVER_TCP, 293 | }, 294 | .verification = { 295 | .certificate = server_cert, 296 | }, 297 | }, 298 | .credentials = { 299 | .client_id = client_id, 300 | .username = username, 301 | .authentication = { 302 | .password = password, 303 | .certificate = client_cert, 304 | .key = client_key, 305 | } 306 | }, 307 | .session = { 308 | .last_will = { 309 | .topic = lwt_topic, 310 | .msg = lwt_msg, 311 | .qos = lwt_qos, 312 | .retain = lwt_retain, 313 | }, 314 | }, 315 | }; 316 | 317 | ESP_LOGI(TAG, "Connecting MQTT client"); 318 | if (mqtt_handle) 319 | esp_mqtt_client_destroy(mqtt_handle); 320 | if (!(mqtt_handle = esp_mqtt_client_init(&config))) 321 | return -1; 322 | esp_mqtt_client_register_event(mqtt_handle, ESP_EVENT_ANY_ID, mqtt_event_cb, 323 | NULL); 324 | esp_mqtt_client_start(mqtt_handle); 325 | return 0; 326 | } 327 | 328 | int mqtt_disconnect(void) 329 | { 330 | ESP_LOGI(TAG, "Disconnecting MQTT client"); 331 | is_connected = 0; 332 | mqtt_subscriptions_free(&subscription_list); 333 | if (mqtt_handle) 334 | esp_mqtt_client_destroy(mqtt_handle); 335 | mqtt_handle = NULL; 336 | 337 | return 0; 338 | } 339 | 340 | uint8_t mqtt_is_connected(void) 341 | { 342 | return is_connected; 343 | } 344 | 345 | int mqtt_initialize(void) 346 | { 347 | ESP_LOGD(TAG, "Initializing MQTT client"); 348 | return 0; 349 | } 350 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32-BLE2MQTT 2 | 3 | This project is a BLE to MQTT bridge, i.e. it exposes BLE GATT characteristics 4 | as MQTT topics for bidirectional communication. It's developed for the ESP32 SoC 5 | and is based on [ESP-IDF](https://github.com/espressif/esp-idf) release v5.0. 6 | Note that using any other ESP-IDF version might not be stable or even compile. 7 | 8 | For example, if a device with a MAC address of `a0:e6:f8:50:72:53` exposes the 9 | [0000180f-0000-1000-8000-00805f9b34fb service](https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.battery_service.xml) 10 | (Battery Service) which includes the 11 | [00002a19-0000-1000-8000-00805f9b34fb characteristic](https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.battery_level.xml) 12 | (Battery Level), the `a0:e6:f8:50:72:53/BatteryService/BatteryLevel` MQTT topic 13 | is published with a value representing the battery level. 14 | 15 | Characteristics supporting notifications will automatically be registered on and 16 | new values will be published once available. It's also possible to proactively 17 | issue a read request by publishing any value to the topic using the above format 18 | suffixed with '/Get'. Note that values are strings representing the 19 | characteristic values based on their definitions grabbed from 20 | http://bluetooth.org. For example, a battery level of 100% (0x64) will be sent 21 | as a string '100'. 22 | 23 | In order to set a GATT value, publish a message to a writable characteristic 24 | using the above format suffixed with `/Set`. Payload should be of the same 25 | format described above and will be converted, when needed, before sending to the 26 | BLE peripheral. 27 | 28 | In addition to the characteristic values, the BLE2MQTT devices also publish 29 | additional topics to help book-keeping: 30 | * `/Connected` - With a payload of `true`/`false` 31 | depicting if the peripheral is currently connected or not. Note that this 32 | topic is monitored by the BLE2MQTT instance currently connected to the 33 | peripheral so that if another instance publishes `false`, the current instance 34 | will re-publish `true` 35 | * `/Owner` - The name of the BLE2MQTT instance currently 36 | connected to the peripheral, e.g. `BLE2MQTT-XXXX`, where `XXXX` are the last 2 37 | octets of the ESP32's WiFi MAC address 38 | * `BLE2MQTT-XXX/Version` - The BLE2MQTT application version currently running 39 | * `BLE2MQTT-XXX/ConfigVersion` - The BLE2MQTT configuration version currently 40 | loaded (MD5 hash of configuration file) 41 | * `BLE2MQTT-XXX/Uptime` - The uptime of the ESP32, in seconds, published every 42 | minute 43 | * `BLE2MQTT-XXX/Status` - `Online` when running, `Offline` when powered off 44 | (the latter is an LWT message) 45 | 46 | ## Broadcasters 47 | 48 | Broadcasters are non-connectable BLE devices that only send advertisements. 49 | This application supports publishing these advertisements over MQTT. 50 | For each broadcaster, at-least two topics are published: 51 | * `BLE2MQTT-XXXX//Type` - The broadcaster type, e.g. 52 | `iBeacon` 53 | * `BLE2MQTT-XXXX//RSSI` - The RSSI value of the 54 | received advertisement 55 | 56 | In addition, depending on the broadcaster type and payload, additional meta-data 57 | is published. 58 | * For iBeacon: `UUID`, `Major`, `Minor` and `Distance` 59 | * For Eddystone: 60 | * `UID` frames: `Namespace`, `Instance` and `Distance` 61 | * `URL` frames: `URL` and `Distance` 62 | * `TLM` frames: `Voltage`, `Temperature`, `Count` and `Uptime` 63 | * For Xiaomi Mijia (MiBeacon) sensors: `MACAddress`, `MessageCounter`, 64 | `Temperature`, `Humidity`, `Moisture`, `Formaldehyde`, `Illuminance`, 65 | `Conductivity`, `Switch`, `Consumable`, `Smoke`, `Light`, `DoorClosed`, `Motion`, 66 | `BatteryLevel` 67 | * For BeeWi Smart Door sensors: `Status` and `Battery` 68 | * For Xiaomi LYWSD03MMC Temperature Sensors running the ATC1441 firmware: 69 | `MACAddress`, `MessageCounter`, `Temperature`, `Humidity`, `BatteryLevel` 70 | and `BatteryVolts` (_See https://github.com/atc1441/ATC_MiThermometer_) 71 | 72 | **Note:** Broadcaster topics are published without the retained flag regardless 73 | of what's defined in the configuration file. 74 | 75 | ## Compiling 76 | 77 | 1. Install `ESP-IDF` 78 | 79 | You will first need to install the 80 | [Espressif IoT Development Framework](https://github.com/espressif/esp-idf). 81 | The [Installation Instructions](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/linux-macos-setup.html) 82 | have all of the details. Make sure to follow ALL the steps, up to and including step 4 where you set up the tools and 83 | the `get_idf` alias. 84 | 85 | 2. Download the repository and its dependencies: 86 | 87 | ```bash 88 | git clone --recursive https://github.com/shmuelzon/esp32-ble2mqtt 89 | ``` 90 | 91 | 3. Modify the config.json and flash 92 | 93 | Modify the [configuration file](#configuration) to fit your environment, build 94 | and flash (make sure to modify the serial device your ESP32 is connected to): 95 | 96 | ```bash 97 | idf.py build flash 98 | ``` 99 | 100 | ## Remote Logging 101 | 102 | If configured, the application can send the logs remotely via UDP to another 103 | host to allow receiving logs from remote devices without a serial connection. 104 | To receive these logs on your host, execute `idf.py remote-monitor`. 105 | 106 | ## Configuration 107 | 108 | The configuration file provided in located at 109 | [data/config.json](data/config.json) in the repository. It contains all of the 110 | different configuration options. 111 | 112 | The `network` section should contain either a `wifi` section or an `eth` 113 | section. If case there are both, the `eth` section has preference over the 114 | `wifi` section. 115 | 116 | Optionally, the network section can contain a `hostname` which, if set, 117 | is used in MQTT subscriptions as well. In such case, relace `BLE2MQTT-XXX` in 118 | this documentation with the hostname you have set. 119 | 120 | The `wifi` section below includes the following entries: 121 | ```json 122 | { 123 | "network": { 124 | "hostname": "MY_HOSTNAME", 125 | "wifi": { 126 | "ssid": "MY_SSID", 127 | "password": "MY_PASSWORD", 128 | "eap": { 129 | "method": null, 130 | "identity": null, 131 | "client_cert": null, 132 | "client_key": null, 133 | "server_cert": null, 134 | "username": null, 135 | "password": null 136 | } 137 | } 138 | } 139 | } 140 | ``` 141 | * `ssid` - The WiFi SSID the ESP32 should connect to 142 | * `password` - The security password for the above network 143 | * `eap` - WPA-Enterprise configuration (for enterprise networks only) 144 | * `method` - `TLS`, `PEAP` or `TTLS` 145 | * `identity` - The EAP identity 146 | * `ca_cert`, `client_cert`, `client_key` - Full path names, including a 147 | leading slash (/), of the certificate/key file (in PEM format) stored under 148 | the data folder 149 | * `username`, `password` - EAP login credentials 150 | 151 | The `eth` section below includes the following entries: 152 | ```json 153 | { 154 | "network": { 155 | "eth": { 156 | "phy": "MY_ETH_PHY", 157 | "phy_power_pin": -1 158 | } 159 | } 160 | } 161 | ``` 162 | * `phy` - The PHY chip connected to ESP32 RMII, one of: 163 | * `IP101` 164 | * `RTL8201` 165 | * `LAN8720` 166 | * `DP83848` 167 | * `phy_power_pin` - Some ESP32 Ethernet modules such as the Olimex ESP32-POE require a GPIO pin to be set high in order to enable the PHY. Omitting this configuration or setting it to -1 will disable this. 168 | 169 | _Note: Defining the `eth` section will disable WiFi_ 170 | 171 | The `mqtt` section below includes the following entries: 172 | ```json 173 | { 174 | "mqtt": { 175 | "server": { 176 | "host": "192.168.1.1", 177 | "port": 1883, 178 | "ssl": false, 179 | "client_cert": null, 180 | "client_key": null, 181 | "server_cert": null, 182 | "username": null, 183 | "password": null, 184 | "client_id": null 185 | }, 186 | "publish": { 187 | "qos": 0, 188 | "retain": true 189 | }, 190 | "topics" :{ 191 | "prefix": "", 192 | "get_suffix": "/Get", 193 | "set_suffix": "/Set" 194 | } 195 | } 196 | } 197 | ``` 198 | * `server` - MQTT connection parameters 199 | * `host` - Host name or IP address of the MQTT broker 200 | * `port` - TCP port of the MQTT broker. If not specificed will default to 201 | 1883 or 8883, depending on SSL configuration 202 | * `client_cert`, `client_key`, `server_cert` - Full path names, including a 203 | leading slash (/), of the certificate/key file (in PEM format) stored under 204 | the data folder. For example, if a certificate file is placed at 205 | `data/certs/my_cert.pem`, the value stored in the configuration should be 206 | `/certs/my_cert.pem` 207 | * `username`, `password` - MQTT login credentials 208 | * `client_id` - The MQTT client ID 209 | * `publish` - Configuration for publishing topics 210 | * `topics` 211 | * `prefix` - Which prefix should be added to all MQTT value topics. OTA 212 | related topics are already prefixed and are not affected by this value 213 | * `get_suffix` - Which suffix should be added to the MQTT value topic in order 214 | to issue a read request from the characteristic 215 | * `set_suffix` - Which suffix should be added to the MQTT value topic in order 216 | to write a new value to the characteristic 217 | 218 | The `ble` section of the configuration file includes the following default 219 | configuration: 220 | ```json 221 | { 222 | "ble": { 223 | "//Optional: 'whitelist' or 'blacklist'": [], 224 | "services": { 225 | "definitions": {}, 226 | "//Optional: 'whitelist' or 'blacklist'": [] 227 | }, 228 | "characteristics": { 229 | "definitions": {}, 230 | "//Optional: 'whitelist' or 'blacklist'": [] 231 | }, 232 | "passkeys": {}, 233 | "mikeys": {} 234 | } 235 | } 236 | ``` 237 | * `whitelist`/`blacklist` - An array of MAC addresses of devices. If `whitelist` 238 | is used, only devices with a MAC address matching one of the entries will be 239 | connected while if `blacklist` is used, only devices that do not match any 240 | entry will be connected. It's possible to use the wildcard character `?` to 241 | denote any value for a nibble. 242 | 243 | ```json 244 | "whitelist": [ 245 | "aa:bb:cc:dd:ee:ff", 246 | "00:11:22:??:??:??" 247 | ] 248 | ``` 249 | * `services` - Add additional services or override a existing definitions to the 250 | ones grabbed automatically during build from http://www.bluetooth.org. Each 251 | service can include a `name` field which will be used in the MQTT topic 252 | instead of its UUID. In addition, it's possible to define a white/black list 253 | for discovered services. The white/black list UUIDs may contain the wildcard 254 | character `?` to denote any value for a nibble. For example: 255 | 256 | ```json 257 | "services": { 258 | "definitions": { 259 | "00002f00-0000-1000-8000-00805f9b34fb": { 260 | "name": "Relay Service" 261 | } 262 | }, 263 | "blacklist": [ 264 | "0000180a-0000-1000-8000-00805f9b34fb", 265 | "0000ffff-????-????-????-????????????" 266 | ] 267 | } 268 | ``` 269 | * `characteristics` - Add additional characteristics or override existing 270 | definitions to the ones grabbed automatically during build from 271 | http://www.bluetooth.org. Each characteristic can include a `name` field which 272 | will be used in the MQTT topic instead of its UUID and a `types` array 273 | defining how to parse the byte array reflecting the characteristic's value. 274 | In addition, it's possible to define a white/black list for discovered 275 | characteristics. The white/black list UUIDs may contain the wildcard character 276 | `?` to denote any value for a nibble. For example: 277 | 278 | ```json 279 | "characteristics": { 280 | "definitions": { 281 | "00002f01-0000-1000-8000-00805f9b34fb": { 282 | "name": "Relay State", 283 | "types": [ 284 | "boolean" 285 | ] 286 | } 287 | }, 288 | "blacklist": [ 289 | "00002a29-0000-1000-8000-00805f9b34fb", 290 | "0000ffff-????-????-????-????????????" 291 | ] 292 | } 293 | ``` 294 | * `passkeys` - An object containing the passkey (number 000000~999999) that 295 | should be used for out-of-band authorization. Each entry is the MAC address of 296 | the BLE device and the value is the passkey to use. It's possible to use the 297 | wildcard character `?` to denote any value for a nibble. 298 | 299 | ```json 300 | "passkeys": { 301 | "aa:bb:cc:dd:ee:ff": 0, 302 | "00:11:22:??:??:??": 123456 303 | } 304 | ``` 305 | * `mikeys` - An object containing "bind keys" for Xiaomi MiBeacon devices. 306 | Each entry is the MAC address of the BLE device and the value is the key to use. 307 | Keys are only required for some devices and can be obtained using 308 | [these methods](https://github.com/custom-components/ble_monitor/blob/master/faq.md#my-sensors-ble-advertisements-are-encrypted-how-can-i-get-the-key). 309 | 310 | ```json 311 | "mikeys": { 312 | "e4:aa:ec:xx:xx:xx": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 313 | "a4:c1:38:xx:xx:xx": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 314 | } 315 | ``` 316 | 317 | The optional `log` section below includes the following entries: 318 | ```json 319 | { 320 | "log": { 321 | "host": "224.0.0.200", 322 | "port": 5000 323 | } 324 | } 325 | ``` 326 | * `host` - The hostname or IP address to send the logs to. In case of an IP 327 | address, this may be a unicast, broadcast or multicast address 328 | * `port` - The destination UDP port 329 | 330 | ## OTA 331 | 332 | It is possible to upgrade both firmware and configuration file over-the-air once 333 | an initial version was flashed via serial interface. To do so, execute: 334 | `idf.py upload` or `idf.py upload-config` accordingly. 335 | The above will upgrade all BLE2MQTT devices connected to the MQTT broker defined 336 | in the configuration file. It is also possible to upgrade a specific device by 337 | adding the `OTA_TARGET` variable to the above command set to the host name of 338 | the requested device, e.g.: 339 | ```bash 340 | OTA_TARGET=BLE2MQTT-470C idf.py upload 341 | ``` 342 | 343 | Note: In order to avoid unneeded upgrades, there is a mechanism in place to 344 | compare the new version with the one that resides on the flash. For the firmware 345 | image it's based on the git tag and for the configuration file it's an MD5 hash 346 | of its contents. In order to force an upgrade regardless of the currently 347 | installed version, run `idf.py force-upload` or `idf.py force-upload-config` 348 | respectively. 349 | 350 | ## Board Compatibility 351 | The `sdkconfig.defaults` included in this project covers common configurations. 352 | 353 | ### Olimex ESP32-POE 354 | A number of minor changes are required to support this board: 355 | * Set the `eth` section as follows: 356 | ```json 357 | { 358 | "network": { 359 | "eth": { 360 | "phy": "LAN8720", 361 | "phy_power_pin": 12 362 | } 363 | } 364 | } 365 | ``` 366 | * Run `idf.py menuconfig` and modify the Ethernet configuration to: 367 | * RMII_CLK_OUTPUT=y 368 | * RMII_CLK_OUT_GPIO=17 369 | -------------------------------------------------------------------------------- /main/httpd.c: -------------------------------------------------------------------------------- 1 | #include "httpd.h" 2 | #include "httpd_static_files.h" 3 | #include "ble.h" 4 | #include "ota.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | static const char *TAG = "HTTPD"; 19 | 20 | /* Internal state */ 21 | httpd_handle_t server = NULL; 22 | 23 | /* Callback functions */ 24 | static httpd_on_ota_completed_cb_t on_ota_completed_cb = NULL; 25 | 26 | void httpd_set_on_ota_completed_cb(httpd_on_ota_completed_cb_t cb) 27 | { 28 | on_ota_completed_cb = cb; 29 | } 30 | 31 | static void delayed_restart_timer_cb(TimerHandle_t xTimer) 32 | { 33 | abort(); 34 | xTimerDelete(xTimer, 0); 35 | } 36 | 37 | static esp_err_t restart_handler(httpd_req_t *req) 38 | { 39 | TimerHandle_t delayed_restart_timer = xTimerCreate("delayed_restart", 40 | pdMS_TO_TICKS(1000), pdFALSE, NULL, delayed_restart_timer_cb); 41 | 42 | httpd_resp_sendstr(req, "OK"); 43 | xTimerStart(delayed_restart_timer, 0); 44 | 45 | return ESP_OK; 46 | } 47 | 48 | static esp_err_t status_handler(httpd_req_t *req) 49 | { 50 | esp_err_t ret; 51 | char *response_str; 52 | cJSON *response = cJSON_CreateObject(); 53 | 54 | cJSON_AddStringToObject(response, "version", BLE2MQTT_VER); 55 | 56 | response_str = cJSON_PrintUnformatted(response); 57 | httpd_resp_set_type(req, "application/json"); 58 | ret = httpd_resp_sendstr(req, response_str); 59 | 60 | cJSON_free(response_str); 61 | cJSON_Delete(response); 62 | return ret; 63 | } 64 | 65 | static int register_management_routes(httpd_handle_t server) 66 | { 67 | httpd_uri_t uri_restart = { 68 | .uri = "/restart", 69 | .method = HTTP_POST, 70 | .handler = restart_handler, 71 | .user_ctx = NULL, 72 | }; 73 | httpd_uri_t uri_status = { 74 | .uri = "/status", 75 | .method = HTTP_GET, 76 | .handler = status_handler, 77 | .user_ctx = NULL, 78 | }; 79 | 80 | httpd_register_uri_handler(server, &uri_restart); 81 | httpd_register_uri_handler(server, &uri_status); 82 | 83 | return 0; 84 | } 85 | 86 | static esp_err_t ble_clear_bonding_db_handler(httpd_req_t *req) 87 | { 88 | ble_clear_bonding_info(); 89 | return httpd_resp_sendstr(req, "OK"); 90 | } 91 | 92 | static esp_err_t ble_get_devices_handler(httpd_req_t *req) 93 | { 94 | esp_err_t ret; 95 | size_t i, number_of_devices; 96 | char *response_str; 97 | cJSON *response = cJSON_CreateArray(); 98 | ble_dev_t *devices = ble_devices_list_get(&number_of_devices); 99 | 100 | for (i = 0; i < number_of_devices; i++) 101 | { 102 | cJSON *object = cJSON_CreateObject(); 103 | if (*devices[i].name) 104 | cJSON_AddStringToObject(object, "name", devices[i].name); 105 | else 106 | cJSON_AddNullToObject(object, "name"); 107 | cJSON_AddStringToObject(object, "mac", mactoa(devices[i].mac)); 108 | cJSON_AddBoolToObject(object, "connected", devices[i].connected); 109 | cJSON_AddItemToArray(response, object); 110 | } 111 | 112 | response_str = cJSON_PrintUnformatted(response); 113 | httpd_resp_set_type(req, "application/json"); 114 | ret = httpd_resp_sendstr(req, response_str); 115 | 116 | cJSON_free(response_str); 117 | cJSON_Delete(response); 118 | ble_devices_list_free(devices); 119 | return ret; 120 | } 121 | 122 | static int register_ble_routes(httpd_handle_t server) 123 | { 124 | httpd_uri_t uri_ble_clear_bonding_db = { 125 | .uri = "/ble/bonding_db", 126 | .method = HTTP_DELETE, 127 | .handler = ble_clear_bonding_db_handler, 128 | .user_ctx = NULL, 129 | }; 130 | httpd_uri_t uri_ble_get_devices = { 131 | .uri = "/ble/devices", 132 | .method = HTTP_GET, 133 | .handler = ble_get_devices_handler, 134 | .user_ctx = NULL, 135 | }; 136 | 137 | httpd_register_uri_handler(server, &uri_ble_clear_bonding_db); 138 | httpd_register_uri_handler(server, &uri_ble_get_devices); 139 | 140 | return 0; 141 | } 142 | 143 | static void ota_delayed_reset_timer_cb(TimerHandle_t xTimer) 144 | { 145 | if (!on_ota_completed_cb) 146 | return; 147 | 148 | on_ota_completed_cb((ota_type_t)pvTimerGetTimerID(xTimer), OTA_ERR_SUCCESS); 149 | xTimerDelete(xTimer, 0); 150 | } 151 | 152 | static esp_err_t ota_handler(httpd_req_t *req) 153 | { 154 | ota_type_t ota_type = (ota_type_t)req->user_ctx; 155 | char buf[2048]; 156 | int ret; 157 | size_t total_received = 0; 158 | TimerHandle_t delayed_reset_timer = NULL; 159 | 160 | ESP_LOGD(TAG, "Handling route for OTA type %d", ota_type); 161 | 162 | if ((ret = ota_open(ota_type))) 163 | { 164 | ESP_LOGE(TAG, "Failed starting OTA: %s", ota_err_to_str(ret)); 165 | return httpd_resp_send_500(req); 166 | } 167 | while (req->content_len - total_received > 0) 168 | { 169 | if ((ret = httpd_req_recv(req, buf, 2048)) <= 0) 170 | { 171 | if (ret == HTTPD_SOCK_ERR_TIMEOUT) 172 | continue; 173 | break; 174 | } 175 | total_received += ret; 176 | if ((ret = ota_write((uint8_t *)buf, ret))) 177 | { 178 | ESP_LOGE(TAG, "Failed writing OTA: %s", ota_err_to_str(ret)); 179 | break; 180 | } 181 | } 182 | if ((ret = ota_close())) 183 | { 184 | ESP_LOGE(TAG, "Failed completing OTA: %s", ota_err_to_str(ret)); 185 | return httpd_resp_send_500(req); 186 | } 187 | httpd_resp_sendstr(req, "OK"); 188 | 189 | delayed_reset_timer = xTimerCreate("delayed_reset", pdMS_TO_TICKS(1000), 190 | pdFALSE, (void *)ota_type, ota_delayed_reset_timer_cb); 191 | xTimerStart(delayed_reset_timer, 0); 192 | 193 | return ESP_OK; 194 | } 195 | 196 | static int register_ota_routes(httpd_handle_t server) 197 | { 198 | httpd_uri_t uri_ota = { 199 | .uri = NULL, 200 | .method = HTTP_POST, 201 | .handler = ota_handler, 202 | .user_ctx = NULL, 203 | }; 204 | 205 | uri_ota.uri = "/ota/firmware"; 206 | uri_ota.user_ctx = (void *)OTA_TYPE_FIRMWARE; 207 | httpd_register_uri_handler(server, &uri_ota); 208 | uri_ota.uri = "/ota/configuration"; 209 | uri_ota.user_ctx = (void *)OTA_TYPE_CONFIG; 210 | httpd_register_uri_handler(server, &uri_ota); 211 | 212 | return 0; 213 | } 214 | 215 | static void fs_add_directory_to_response(cJSON *response, const char *name) 216 | { 217 | int i, size = cJSON_GetArraySize(response); 218 | cJSON *object; 219 | 220 | for (i = 0; i < size; i++) 221 | { 222 | cJSON *entry = cJSON_GetArrayItem(response, i); 223 | if (!strcmp(cJSON_GetObjectItem(entry, "name")->valuestring, name) && 224 | !strcmp(cJSON_GetObjectItem(entry, "type")->valuestring, 225 | "directory")) 226 | { 227 | return; 228 | } 229 | } 230 | 231 | ESP_LOGD(TAG, "Adding directory %s", name); 232 | object = cJSON_CreateObject(); 233 | cJSON_AddStringToObject(object, "type", "directory"); 234 | cJSON_AddStringToObject(object, "name", name); 235 | cJSON_AddItemToArray(response, object); 236 | } 237 | 238 | static void fs_add_file_to_response(cJSON *response, const char *full_name, 239 | const char *name) 240 | { 241 | char full_path[PATH_MAX]; 242 | struct stat st; 243 | cJSON *object; 244 | 245 | snprintf(full_path, sizeof(full_path), "/spiffs/%s", full_name); 246 | if (stat(full_path, &st)) 247 | return; 248 | 249 | ESP_LOGD(TAG, "Adding file %s", name); 250 | object = cJSON_CreateObject(); 251 | cJSON_AddStringToObject(object, "type", "file"); 252 | cJSON_AddStringToObject(object, "name", name); 253 | cJSON_AddNumberToObject(object, "size", st.st_size); 254 | cJSON_AddItemToArray(response, object); 255 | } 256 | 257 | static esp_err_t fs_serve_directory(httpd_req_t *req, const char *path) 258 | { 259 | DIR *dir; 260 | struct dirent *entry; 261 | esp_err_t ret; 262 | cJSON *response; 263 | char *response_str; 264 | size_t path_len = strlen(path); 265 | 266 | ESP_LOGD(TAG, "Serving file list of: %s", path ); 267 | 268 | if (!(dir = opendir("/spiffs"))) 269 | return httpd_resp_send_500(req); 270 | 271 | response = cJSON_CreateArray(); 272 | while((entry = readdir(dir))) 273 | { 274 | char *slash_location, *relative_name; 275 | 276 | ESP_LOGD(TAG, "Found entry %s", entry->d_name); 277 | 278 | /* Check if path is under this directory */ 279 | if (strncmp(path + 1, entry->d_name, path_len - 1)) 280 | continue; 281 | 282 | relative_name = strdup(entry->d_name + path_len - 1); 283 | 284 | if ((slash_location = strchr(relative_name, '/'))) 285 | { 286 | *slash_location = '\0'; 287 | fs_add_directory_to_response(response, relative_name); 288 | } 289 | else 290 | fs_add_file_to_response(response, entry->d_name, relative_name); 291 | 292 | free(relative_name); 293 | } 294 | closedir(dir); 295 | 296 | if (!cJSON_GetArraySize(response)) 297 | { 298 | cJSON_Delete(response); 299 | return httpd_resp_send_404(req); 300 | } 301 | 302 | response_str = cJSON_PrintUnformatted(response); 303 | httpd_resp_set_type(req, "application/json"); 304 | ret = httpd_resp_sendstr(req, response_str); 305 | 306 | cJSON_free(response_str); 307 | cJSON_Delete(response); 308 | return ret; 309 | } 310 | 311 | static esp_err_t fs_serve_file(httpd_req_t *req, const char *path) 312 | { 313 | char full_path[PATH_MAX], buffer[2048]; 314 | struct stat st; 315 | int fd, len; 316 | 317 | snprintf(full_path, sizeof(full_path), "/spiffs%s", path); 318 | ESP_LOGD(TAG, "Serving file %s", full_path); 319 | 320 | if (stat(full_path, &st)) 321 | return httpd_resp_send_404(req); 322 | 323 | if ((fd = open(full_path, O_RDONLY)) < 0) 324 | return httpd_resp_send_500(req); 325 | 326 | httpd_resp_set_type(req, "application/octet-stream"); 327 | httpd_resp_send(req, NULL, st.st_size); 328 | while ((len = read(fd, buffer, sizeof(buffer))) > 0) 329 | httpd_send(req, buffer, len); 330 | 331 | close(fd); 332 | return len < 0 ? ESP_FAIL : ESP_OK; 333 | } 334 | 335 | static esp_err_t fs_get_handler(httpd_req_t *req) 336 | { 337 | const char *path = req->uri + strlen("/fs"); 338 | 339 | ESP_LOGD(TAG, "Handling GET for: '%s'", path); 340 | 341 | if (path[strlen(path) - 1] == '/') 342 | return fs_serve_directory(req, path); 343 | return fs_serve_file(req, path); 344 | } 345 | 346 | static esp_err_t fs_post_handler(httpd_req_t *req) 347 | { 348 | const char *path = req->uri + strlen("/fs"); 349 | char full_path[PATH_MAX], buffer[2048]; 350 | int fd, ret = 0; 351 | size_t total_received = 0; 352 | 353 | ESP_LOGD(TAG, "Handling POST for: '%s'", path); 354 | 355 | if (path[strlen(path) - 1] == '/') 356 | return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, NULL); 357 | 358 | snprintf(full_path, sizeof(full_path), "/spiffs%s", path); 359 | if ((fd = open(full_path, O_WRONLY | O_TRUNC | O_CREAT)) < 0) 360 | { 361 | ESP_LOGE(TAG, "Failed opening file: %d (%s)", errno, strerror(errno)); 362 | return httpd_resp_send_500(req); 363 | } 364 | 365 | while (req->content_len - total_received > 0) 366 | { 367 | if ((ret = httpd_req_recv(req, buffer, 2048)) <= 0) 368 | { 369 | if (ret == HTTPD_SOCK_ERR_TIMEOUT) 370 | continue; 371 | break; 372 | } 373 | total_received += ret; 374 | if ((ret = write(fd, buffer, ret)) < 0) 375 | { 376 | ESP_LOGE(TAG, "Failed writing to file: %d (%s)", errno, 377 | strerror(errno)); 378 | break; 379 | } 380 | ESP_LOGD(TAG, "Wrote %d bytes (%zu/%zu)", ret, total_received, 381 | req->content_len); 382 | } 383 | close(fd); 384 | 385 | if (ret < 0 || total_received != req->content_len) 386 | { 387 | ESP_LOGE(TAG, "Failed downloading file: %d (%s)", errno, 388 | strerror(errno)); 389 | unlink(full_path); 390 | return httpd_resp_send_500(req); 391 | } 392 | 393 | return httpd_resp_sendstr(req, "OK"); 394 | } 395 | 396 | static esp_err_t fs_delete_handler(httpd_req_t *req) 397 | { 398 | const char *path = req->uri + strlen("/fs"); 399 | char full_path[PATH_MAX]; 400 | 401 | ESP_LOGD(TAG, "Handling DELETE for: '%s'", path); 402 | 403 | snprintf(full_path, sizeof(full_path), "/spiffs%s", path); 404 | if (unlink(full_path)) 405 | return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, NULL); 406 | 407 | return httpd_resp_sendstr(req, "OK"); 408 | } 409 | 410 | static int register_fs_routes(httpd_handle_t server) 411 | { 412 | httpd_uri_t uri_fs = { 413 | .uri = "/fs/*", 414 | .method = HTTP_GET, 415 | .handler = NULL, 416 | .user_ctx = NULL, 417 | }; 418 | 419 | uri_fs.method = HTTP_GET; 420 | uri_fs.handler = fs_get_handler; 421 | httpd_register_uri_handler(server, &uri_fs); 422 | 423 | uri_fs.method = HTTP_POST; 424 | uri_fs.handler = fs_post_handler; 425 | httpd_register_uri_handler(server, &uri_fs); 426 | 427 | uri_fs.method = HTTP_DELETE; 428 | uri_fs.handler = fs_delete_handler; 429 | httpd_register_uri_handler(server, &uri_fs); 430 | 431 | return 0; 432 | } 433 | 434 | static esp_err_t static_file_handler(httpd_req_t *req) 435 | { 436 | httpd_static_file *static_file = (httpd_static_file *)req->user_ctx; 437 | 438 | ESP_LOGD(TAG, "Handling route for %s", static_file->path); 439 | httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); 440 | return httpd_resp_send(req, (const char *)static_file->start, 441 | static_file->end - static_file->start); 442 | } 443 | 444 | static int register_static_routes(httpd_handle_t server) 445 | { 446 | httpd_static_file *static_file; 447 | httpd_uri_t uri_static_file = { 448 | .uri = NULL, 449 | .method = HTTP_GET, 450 | .handler = static_file_handler, 451 | .user_ctx = NULL, 452 | }; 453 | 454 | for (static_file = httpd_static_files; static_file->path; static_file++) 455 | { 456 | ESP_LOGD(TAG, "Registerting route %s", static_file->path); 457 | uri_static_file.uri = static_file->path; 458 | uri_static_file.user_ctx = static_file; 459 | httpd_register_uri_handler(server, &uri_static_file); 460 | } 461 | 462 | return 0; 463 | } 464 | 465 | int httpd_initialize(void) 466 | { 467 | ESP_LOGI(TAG, "Initializing HTTP server"); 468 | 469 | httpd_config_t config = HTTPD_DEFAULT_CONFIG(); 470 | config.uri_match_fn = httpd_uri_match_wildcard; 471 | 472 | config.max_uri_handlers = 20; 473 | config.stack_size = 8192; 474 | ESP_ERROR_CHECK(httpd_start(&server, &config)); 475 | 476 | /* Register URI handlers */ 477 | register_management_routes(server); 478 | register_ble_routes(server); 479 | register_ota_routes(server); 480 | register_fs_routes(server); 481 | register_static_routes(server); 482 | 483 | return 0; 484 | } 485 | -------------------------------------------------------------------------------- /main/config.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | /* Types */ 15 | typedef struct config_update_handle_t { 16 | const esp_partition_t *partition; 17 | uint8_t partition_id; 18 | size_t bytes_written; 19 | } config_update_handle_t; 20 | 21 | /* Constants */ 22 | static const char *TAG = "Config"; 23 | static const char *config_file_name = "/spiffs/config.json"; 24 | static const char *nvs_namespace = "ble2mqtt_config"; 25 | static const char *nvs_active_partition = "active_part"; 26 | static cJSON *config; 27 | 28 | /* Internal variables */ 29 | static char config_version[65]; 30 | static nvs_handle nvs; 31 | 32 | /* Common utilities */ 33 | static char *read_file(const char *path) 34 | { 35 | int fd, len; 36 | struct stat st; 37 | char *buf, *p; 38 | 39 | if (stat(path, &st)) 40 | return NULL; 41 | 42 | if ((fd = open(path, O_RDONLY)) < 0) 43 | return NULL; 44 | 45 | if (!(buf = p = malloc(st.st_size + 1))) 46 | return NULL; 47 | 48 | while ((len = read(fd, p, 1024)) > 0) 49 | p += len; 50 | close(fd); 51 | 52 | if (len < 0) 53 | { 54 | free(buf); 55 | return NULL; 56 | } 57 | 58 | *p = '\0'; 59 | return buf; 60 | } 61 | 62 | /* BLE Configuration*/ 63 | static cJSON *config_ble_get_name_by_uuid(uint8_t is_service, 64 | const char *uuid, const char *field_name) 65 | { 66 | cJSON *ble = cJSON_GetObjectItemCaseSensitive(config, "ble"); 67 | cJSON *type = cJSON_GetObjectItemCaseSensitive(ble, 68 | is_service ? "services" : "characteristics"); 69 | cJSON *list = cJSON_GetObjectItemCaseSensitive(type, "definitions"); 70 | 71 | /* Check config.json for override values */ 72 | cJSON *obj = cJSON_GetObjectItem(list, uuid); 73 | cJSON *field = cJSON_GetObjectItemCaseSensitive(obj, field_name); 74 | 75 | return field; 76 | } 77 | 78 | const char *config_ble_service_name_get(const char *uuid) 79 | { 80 | cJSON *name = config_ble_get_name_by_uuid(1, uuid, "name"); 81 | 82 | if (cJSON_IsString(name)) 83 | return name->valuestring; 84 | 85 | return NULL; 86 | } 87 | 88 | const char *config_ble_characteristic_name_get(const char *uuid) 89 | { 90 | cJSON *name = config_ble_get_name_by_uuid(0, uuid, "name"); 91 | 92 | if (cJSON_IsString(name)) 93 | return name->valuestring; 94 | 95 | return NULL; 96 | } 97 | 98 | const char **config_ble_characteristic_types_get(const char *uuid) 99 | { 100 | cJSON *types = config_ble_get_name_by_uuid(0, uuid, "types"); 101 | static char **ret = NULL; 102 | int i, size; 103 | 104 | if (ret) 105 | { 106 | free(ret); 107 | ret = NULL; 108 | } 109 | 110 | if (!cJSON_IsArray(types)) 111 | return NULL; 112 | 113 | size = cJSON_GetArraySize(types); 114 | ret = malloc(sizeof(char *) * (size + 1)); 115 | for (i = 0; i < size; i++) 116 | { 117 | cJSON *type = cJSON_GetArrayItem(types, i); 118 | ret[i] = type->valuestring; 119 | } 120 | ret[size] = NULL; 121 | 122 | return (const char **)ret; 123 | } 124 | 125 | int match_wildcard(const char *fmt, const char *str) 126 | { 127 | while(*fmt && 128 | (tolower((uint8_t)*fmt) == tolower((uint8_t)*str) || *fmt == '?')) 129 | { 130 | fmt++; 131 | str++; 132 | } 133 | 134 | return *fmt == *str || *fmt == '?'; 135 | } 136 | 137 | cJSON *json_find_in_array(cJSON *arr, const char *item) 138 | { 139 | cJSON *cur; 140 | 141 | if (!arr) 142 | return NULL; 143 | 144 | for (cur = arr->child; cur; cur = cur->next) 145 | { 146 | if (cJSON_IsString(cur) && match_wildcard(cur->valuestring, item)) 147 | return cur; 148 | } 149 | 150 | return NULL; 151 | } 152 | 153 | static uint8_t json_is_in_lists(cJSON *base, const char *item) 154 | { 155 | cJSON *whitelist = cJSON_GetObjectItemCaseSensitive(base, "whitelist"); 156 | cJSON *blacklist = cJSON_GetObjectItemCaseSensitive(base, "blacklist"); 157 | uint8_t action = whitelist ? 1 : 0; 158 | cJSON *list = whitelist ? : blacklist; 159 | 160 | /* No list was defined, accept all */ 161 | if (!list) 162 | return 1; 163 | 164 | return json_find_in_array(list, item) ? action : !action; 165 | } 166 | 167 | uint8_t config_ble_characteristic_should_include(const char *uuid) 168 | { 169 | cJSON *ble = cJSON_GetObjectItemCaseSensitive(config, "ble"); 170 | cJSON *characteristics = cJSON_GetObjectItemCaseSensitive(ble, 171 | "characteristics"); 172 | 173 | return json_is_in_lists(characteristics, uuid); 174 | } 175 | 176 | uint8_t config_ble_service_should_include(const char *uuid) 177 | { 178 | cJSON *ble = cJSON_GetObjectItemCaseSensitive(config, "ble"); 179 | cJSON *services = cJSON_GetObjectItemCaseSensitive(ble, "services"); 180 | 181 | return json_is_in_lists(services, uuid); 182 | } 183 | 184 | uint8_t config_ble_should_connect(const char *mac) 185 | { 186 | cJSON *ble = cJSON_GetObjectItemCaseSensitive(config, "ble"); 187 | return json_is_in_lists(ble, mac); 188 | } 189 | 190 | uint32_t config_ble_passkey_get(const char *mac) 191 | { 192 | cJSON *ble = cJSON_GetObjectItemCaseSensitive(config, "ble"); 193 | cJSON *passkeys = cJSON_GetObjectItemCaseSensitive(ble, "passkeys"); 194 | cJSON *key; 195 | 196 | if (!passkeys) 197 | return 0; 198 | 199 | for (key = passkeys->child; key; key = key->next) 200 | { 201 | if (cJSON_IsNumber(key) && match_wildcard(key->string, mac)) 202 | return key->valuedouble; 203 | } 204 | 205 | return 0; 206 | } 207 | 208 | const char *config_ble_mikey_get(const char *mac) 209 | { 210 | cJSON *ble = cJSON_GetObjectItemCaseSensitive(config, "ble"); 211 | cJSON *mikeys = cJSON_GetObjectItemCaseSensitive(ble, "mikeys"); 212 | cJSON *key; 213 | 214 | if (!mikeys) 215 | return NULL; 216 | 217 | for (key = mikeys->child; key; key = key->next) 218 | { 219 | if (cJSON_IsString(key) && match_wildcard(key->string, mac)) 220 | return key->valuestring; 221 | } 222 | 223 | return NULL; 224 | } 225 | 226 | /* Ethernet Configuration */ 227 | const char *config_network_eth_phy_get(void) 228 | { 229 | cJSON *network = cJSON_GetObjectItemCaseSensitive(config, "network"); 230 | cJSON *eth = cJSON_GetObjectItemCaseSensitive(network, "eth"); 231 | cJSON *phy = cJSON_GetObjectItemCaseSensitive(eth, "phy"); 232 | 233 | if (cJSON_IsString(phy)) 234 | return phy->valuestring; 235 | 236 | return NULL; 237 | } 238 | 239 | int8_t config_network_eth_phy_power_pin_get(void) 240 | { 241 | cJSON *network = cJSON_GetObjectItemCaseSensitive(config, "network"); 242 | cJSON *eth = cJSON_GetObjectItemCaseSensitive(network, "eth"); 243 | cJSON *phy_power_pin = cJSON_GetObjectItemCaseSensitive(eth, 244 | "phy_power_pin"); 245 | 246 | if (cJSON_IsNumber(phy_power_pin)) 247 | return phy_power_pin->valuedouble; 248 | 249 | return -1; 250 | } 251 | 252 | /* MQTT Configuration*/ 253 | const char *config_mqtt_server_get(const char *param_name) 254 | { 255 | cJSON *mqtt = cJSON_GetObjectItemCaseSensitive(config, "mqtt"); 256 | cJSON *server = cJSON_GetObjectItemCaseSensitive(mqtt, "server"); 257 | cJSON *param = cJSON_GetObjectItemCaseSensitive(server, param_name); 258 | 259 | if (cJSON_IsString(param)) 260 | return param->valuestring; 261 | 262 | return NULL; 263 | } 264 | 265 | const char *config_mqtt_host_get(void) 266 | { 267 | return config_mqtt_server_get("host"); 268 | } 269 | 270 | uint16_t config_mqtt_port_get(void) 271 | { 272 | cJSON *mqtt = cJSON_GetObjectItemCaseSensitive(config, "mqtt"); 273 | cJSON *server = cJSON_GetObjectItemCaseSensitive(mqtt, "server"); 274 | cJSON *port = cJSON_GetObjectItemCaseSensitive(server, "port"); 275 | 276 | if (cJSON_IsNumber(port)) 277 | return port->valuedouble; 278 | 279 | return 0; 280 | } 281 | 282 | uint8_t config_mqtt_ssl_get(void) 283 | { 284 | cJSON *mqtt = cJSON_GetObjectItemCaseSensitive(config, "mqtt"); 285 | cJSON *server = cJSON_GetObjectItemCaseSensitive(mqtt, "server"); 286 | cJSON *ssl = cJSON_GetObjectItemCaseSensitive(server, "ssl"); 287 | 288 | return cJSON_IsTrue(ssl); 289 | } 290 | 291 | const char *config_mqtt_file_get(const char *field) 292 | { 293 | const char *file = config_mqtt_server_get(field); 294 | char buf[128]; 295 | 296 | if (!file) 297 | return NULL; 298 | 299 | snprintf(buf, sizeof(buf), "/spiffs%s", file); 300 | return read_file(buf); 301 | } 302 | 303 | const char *config_mqtt_server_cert_get(void) 304 | { 305 | static const char *cert; 306 | 307 | if (!cert) 308 | cert = config_mqtt_file_get("server_cert"); 309 | 310 | return cert; 311 | } 312 | 313 | const char *config_mqtt_client_cert_get(void) 314 | { 315 | static const char *cert; 316 | 317 | if (!cert) 318 | cert = config_mqtt_file_get("client_cert"); 319 | 320 | return cert; 321 | } 322 | 323 | const char *config_mqtt_client_key_get(void) 324 | { 325 | static const char *key; 326 | 327 | if (!key) 328 | key = config_mqtt_file_get("client_key"); 329 | 330 | return key; 331 | } 332 | 333 | const char *config_mqtt_client_id_get(void) 334 | { 335 | return config_mqtt_server_get("client_id"); 336 | } 337 | 338 | const char *config_mqtt_username_get(void) 339 | { 340 | return config_mqtt_server_get("username"); 341 | } 342 | 343 | const char *config_mqtt_password_get(void) 344 | { 345 | return config_mqtt_server_get("password"); 346 | } 347 | 348 | uint8_t config_mqtt_qos_get(void) 349 | { 350 | cJSON *mqtt = cJSON_GetObjectItemCaseSensitive(config, "mqtt"); 351 | cJSON *publish = cJSON_GetObjectItemCaseSensitive(mqtt, "publish"); 352 | cJSON *qos = cJSON_GetObjectItemCaseSensitive(publish, "qos"); 353 | 354 | if (cJSON_IsNumber(qos)) 355 | return qos->valuedouble; 356 | 357 | return 0; 358 | } 359 | 360 | uint8_t config_mqtt_retained_get(void) 361 | { 362 | cJSON *mqtt = cJSON_GetObjectItemCaseSensitive(config, "mqtt"); 363 | cJSON *publish = cJSON_GetObjectItemCaseSensitive(mqtt, "publish"); 364 | cJSON *retain = cJSON_GetObjectItemCaseSensitive(publish, "retain"); 365 | 366 | return cJSON_IsTrue(retain); 367 | } 368 | 369 | const char *config_mqtt_topics_get(const char *param_name, const char *def) 370 | { 371 | cJSON *mqtt = cJSON_GetObjectItemCaseSensitive(config, "mqtt"); 372 | cJSON *topics = cJSON_GetObjectItemCaseSensitive(mqtt, "topics"); 373 | cJSON *param = cJSON_GetObjectItemCaseSensitive(topics, param_name); 374 | 375 | if (cJSON_IsString(param)) 376 | return param->valuestring; 377 | 378 | return def; 379 | } 380 | 381 | const char *config_mqtt_prefix_get(void) 382 | { 383 | return config_mqtt_topics_get("prefix", ""); 384 | } 385 | 386 | const char *config_mqtt_get_suffix_get(void) 387 | { 388 | return config_mqtt_topics_get("get_suffix", "/Get"); 389 | } 390 | 391 | const char *config_mqtt_set_suffix_get(void) 392 | { 393 | return config_mqtt_topics_get("set_suffix", "/Set"); 394 | } 395 | 396 | /* Network Configuration */ 397 | config_network_type_t config_network_type_get(void) 398 | { 399 | cJSON *network = cJSON_GetObjectItemCaseSensitive(config, "network"); 400 | cJSON *eth = cJSON_GetObjectItemCaseSensitive(network, "eth"); 401 | 402 | return eth ? NETWORK_TYPE_ETH : NETWORK_TYPE_WIFI; 403 | } 404 | 405 | /* WiFi Configuration */ 406 | const char *config_network_hostname_get(void) 407 | { 408 | cJSON *network = cJSON_GetObjectItemCaseSensitive(config, "network"); 409 | cJSON *ssid = cJSON_GetObjectItemCaseSensitive(network, "hostname"); 410 | 411 | if (cJSON_IsString(ssid)) 412 | return ssid->valuestring; 413 | 414 | return NULL; 415 | } 416 | 417 | const char *config_network_wifi_ssid_get(void) 418 | { 419 | cJSON *network = cJSON_GetObjectItemCaseSensitive(config, "network"); 420 | cJSON *wifi = cJSON_GetObjectItemCaseSensitive(network, "wifi"); 421 | cJSON *ssid = cJSON_GetObjectItemCaseSensitive(wifi, "ssid"); 422 | 423 | if (cJSON_IsString(ssid)) 424 | return ssid->valuestring; 425 | 426 | return "MY_SSID"; 427 | } 428 | 429 | const char *config_network_wifi_password_get(void) 430 | { 431 | cJSON *network = cJSON_GetObjectItemCaseSensitive(config, "network"); 432 | cJSON *wifi = cJSON_GetObjectItemCaseSensitive(network, "wifi"); 433 | cJSON *password = cJSON_GetObjectItemCaseSensitive(wifi, "password"); 434 | 435 | if (cJSON_IsString(password)) 436 | return password->valuestring; 437 | 438 | return NULL; 439 | } 440 | 441 | const char *config_network_wifi_eap_get(const char *param_name) 442 | { 443 | cJSON *network = cJSON_GetObjectItemCaseSensitive(config, "network"); 444 | cJSON *wifi = cJSON_GetObjectItemCaseSensitive(network, "wifi"); 445 | cJSON *eap = cJSON_GetObjectItemCaseSensitive(wifi, "eap"); 446 | cJSON *param = cJSON_GetObjectItemCaseSensitive(eap, param_name); 447 | 448 | if (cJSON_IsString(param)) 449 | return param->valuestring; 450 | 451 | return NULL; 452 | } 453 | 454 | const char *config_eap_file_get(const char *field) 455 | { 456 | const char *file = config_network_wifi_eap_get(field); 457 | char buf[128]; 458 | 459 | if (!file) 460 | return NULL; 461 | 462 | snprintf(buf, sizeof(buf), "/spiffs%s", file); 463 | return read_file(buf); 464 | } 465 | 466 | const char *config_eap_ca_cert_get(void) 467 | { 468 | static const char *cert; 469 | 470 | if (!cert) 471 | cert = config_eap_file_get("ca_cert"); 472 | 473 | return cert; 474 | } 475 | 476 | const char *config_eap_client_cert_get(void) 477 | { 478 | static const char *cert; 479 | 480 | if (!cert) 481 | cert = config_eap_file_get("client_cert"); 482 | 483 | return cert; 484 | } 485 | 486 | const char *config_eap_client_key_get(void) 487 | { 488 | static const char *key; 489 | 490 | if (!key) 491 | key = config_eap_file_get("client_key"); 492 | 493 | return key; 494 | } 495 | 496 | const char *config_eap_method_get(void) 497 | { 498 | return config_network_wifi_eap_get("method"); 499 | } 500 | 501 | const char *config_eap_identity_get(void) 502 | { 503 | return config_network_wifi_eap_get("identity"); 504 | } 505 | 506 | const char *config_eap_username_get(void) 507 | { 508 | return config_network_wifi_eap_get("username"); 509 | } 510 | 511 | const char *config_eap_password_get(void) 512 | { 513 | return config_network_wifi_eap_get("password"); 514 | } 515 | 516 | /* Remote Logging Configuration */ 517 | const char *config_log_host_get(void) 518 | { 519 | cJSON *log = cJSON_GetObjectItemCaseSensitive(config, "log"); 520 | cJSON *ip = cJSON_GetObjectItemCaseSensitive(log, "host"); 521 | 522 | if (cJSON_IsString(ip)) 523 | return ip->valuestring; 524 | 525 | return NULL; 526 | } 527 | 528 | uint16_t config_log_port_get(void) 529 | { 530 | cJSON *log = cJSON_GetObjectItemCaseSensitive(config, "log"); 531 | cJSON *port = cJSON_GetObjectItemCaseSensitive(log, "port"); 532 | 533 | if (cJSON_IsNumber(port)) 534 | return port->valuedouble; 535 | 536 | return 0; 537 | } 538 | 539 | /* Configuration Update */ 540 | static int config_active_partition_get(void) 541 | { 542 | uint8_t partition = 0; 543 | 544 | nvs_get_u8(nvs, nvs_active_partition, &partition); 545 | return partition; 546 | } 547 | 548 | static int config_active_partition_set(uint8_t partition) 549 | { 550 | ESP_LOGD(TAG, "Setting active partition to %u", partition); 551 | 552 | if (nvs_set_u8(nvs, nvs_active_partition, partition) != ESP_OK || 553 | nvs_commit(nvs) != ESP_OK) 554 | { 555 | ESP_LOGE(TAG, "Failed setting active partition to: %u", partition); 556 | return -1; 557 | } 558 | 559 | return 0; 560 | } 561 | 562 | int config_update_begin(config_update_handle_t **handle) 563 | { 564 | const esp_partition_t *partition; 565 | char partition_name[5]; 566 | uint8_t partition_id = !config_active_partition_get(); 567 | 568 | sprintf(partition_name, "fs_%u", partition_id); 569 | ESP_LOGI(TAG, "Writing to partition %s", partition_name); 570 | partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, 571 | ESP_PARTITION_SUBTYPE_DATA_SPIFFS, partition_name); 572 | 573 | if (!partition) 574 | { 575 | ESP_LOGE(TAG, "Failed finding SPIFFS partition"); 576 | return -1; 577 | } 578 | 579 | ESP_LOGI(TAG, "Writing partition type 0x%0x subtype 0x%0x (offset 0x%08" 580 | PRIx32 ")", partition->type, partition->subtype, partition->address); 581 | 582 | /* Erase partition, needed before writing is allowed */ 583 | if (esp_partition_erase_range(partition, 0, partition->size)) 584 | return -1; 585 | 586 | *handle = malloc(sizeof(**handle)); 587 | (*handle)->partition = partition; 588 | (*handle)->partition_id = partition_id; 589 | (*handle)->bytes_written = 0; 590 | 591 | return 0; 592 | } 593 | 594 | int config_update_write(config_update_handle_t *handle, uint8_t *data, 595 | size_t len) 596 | { 597 | if (esp_partition_write(handle->partition, handle->bytes_written, data, 598 | len)) 599 | { 600 | ESP_LOGE(TAG, "Failed writing to SPIFFS partition!"); 601 | free(handle); 602 | return -1; 603 | } 604 | 605 | handle->bytes_written += len; 606 | return 0; 607 | } 608 | 609 | int config_update_end(config_update_handle_t *handle) 610 | { 611 | int ret = -1; 612 | 613 | /* We succeeded only if the entire partition was written */ 614 | if (handle->bytes_written == handle->partition->size) 615 | ret = config_active_partition_set(handle->partition_id); 616 | 617 | free(handle); 618 | return ret; 619 | } 620 | 621 | static cJSON *load_json(const char *path) 622 | { 623 | char *str = read_file(path); 624 | cJSON *json; 625 | 626 | if (!str) 627 | return NULL; 628 | 629 | json = cJSON_Parse(str); 630 | 631 | free(str); 632 | return json; 633 | } 634 | 635 | char *config_version_get(void) 636 | { 637 | return config_version; 638 | } 639 | 640 | int config_load(uint8_t partition_id) 641 | { 642 | char *p, partition_name[] = { 'f', 's', '_', 'x', '\0' }; 643 | esp_vfs_spiffs_conf_t conf = { 644 | .base_path = "/spiffs", 645 | .partition_label = partition_name, 646 | .max_files = 8, 647 | .format_if_mount_failed = true 648 | }; 649 | uint8_t i, sha[32]; 650 | 651 | partition_name[3] = partition_id + '0'; 652 | ESP_LOGD(TAG, "Loading config from partition %s", partition_name); 653 | 654 | ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); 655 | 656 | /* Load config.json from SPIFFS */ 657 | if (!(config = load_json(config_file_name))) 658 | { 659 | esp_vfs_spiffs_unregister(partition_name); 660 | return -1; 661 | } 662 | 663 | /* Calulate hash of active partition */ 664 | esp_partition_get_sha256(esp_partition_find_first(ESP_PARTITION_TYPE_DATA, 665 | ESP_PARTITION_SUBTYPE_DATA_SPIFFS, partition_name), sha); 666 | for (p = config_version, i = 0; i < sizeof(sha); i++) 667 | p += sprintf(p, "%02x", sha[i]); 668 | 669 | return 0; 670 | } 671 | 672 | int config_initialize(void) 673 | { 674 | uint8_t partition; 675 | 676 | ESP_LOGI(TAG, "Initializing configuration"); 677 | ESP_ERROR_CHECK(nvs_open(nvs_namespace, NVS_READWRITE, &nvs)); 678 | 679 | partition = config_active_partition_get(); 680 | 681 | /* Attempt to load configuration from active partition with fall-back */ 682 | if (config_load(partition)) 683 | { 684 | ESP_LOGE(TAG, "Failed loading partition %d, falling back to %d", 685 | partition, !partition); 686 | if (config_load(!partition)) 687 | { 688 | ESP_LOGE(TAG, "Failed loading partition %d as well", !partition); 689 | return -1; 690 | } 691 | /* Fall-back partition is OK, mark it as active */ 692 | config_active_partition_set(!partition); 693 | } 694 | 695 | ESP_LOGI(TAG, "version: %s", config_version_get()); 696 | return 0; 697 | } 698 | -------------------------------------------------------------------------------- /main/broadcasters.c: -------------------------------------------------------------------------------- 1 | #include "broadcasters.h" 2 | #include "ble_utils.h" 3 | #include "config.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | /* Constants */ 15 | static const char *TAG = "Broadcaster"; 16 | 17 | /* Utilities */ 18 | static char *hex2a(char *s, uint8_t *buf, size_t len) 19 | { 20 | int i; 21 | char *p; 22 | 23 | for (i = 0, p = s; i < len; i++) 24 | p += sprintf(p, "%02x", buf[i]); 25 | 26 | return s; 27 | } 28 | 29 | /* UUID's in big-endian (compared to uuidtoa()) */ 30 | static char *_uuidtoa(ble_uuid_t uuid) 31 | { 32 | static char s[37]; 33 | 34 | sprintf(s, 35 | "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", 36 | uuid[0], uuid[1], uuid[2], uuid[3], 37 | uuid[4], uuid[5], 38 | uuid[6], uuid[7], 39 | uuid[8], uuid[9], 40 | uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]); 41 | 42 | return s; 43 | } 44 | 45 | /* MAC addresses in little-endian (compared to mactoa()) */ 46 | char *_mactoa(mac_addr_t mac) 47 | { 48 | static char s[18]; 49 | 50 | sprintf(s, "%02x:%02x:%02x:%02x:%02x:%02x", mac[5], mac[4], mac[3], mac[2], 51 | mac[1], mac[0]); 52 | 53 | return s; 54 | } 55 | 56 | /* iBeacon */ 57 | typedef struct { 58 | uint16_t company_id; 59 | uint16_t beacon_type; 60 | uint8_t proximity_uuid[16]; 61 | uint16_t major; 62 | uint16_t minor; 63 | int8_t measured_power; 64 | } __attribute__((packed)) ibeacon_t; 65 | 66 | static ibeacon_t *ibeacon_data_get(uint8_t *adv_data, uint8_t adv_data_len, 67 | uint8_t *ibeacon_len) 68 | { 69 | uint8_t len; 70 | uint8_t *data = esp_ble_resolve_adv_data(adv_data, 71 | ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, &len); 72 | 73 | if (ibeacon_len) 74 | *ibeacon_len = len; 75 | 76 | return (ibeacon_t *)data; 77 | } 78 | 79 | static int ibeacon_is_broadcaster(uint8_t *adv_data, size_t adv_data_len) 80 | { 81 | uint8_t len; 82 | ibeacon_t *beacon = ibeacon_data_get(adv_data, adv_data_len, &len); 83 | 84 | if (!beacon || len < sizeof(ibeacon_t)) 85 | return 0; 86 | 87 | /* Technically, we should also check the device is BLE only and 88 | * non-connectable, but most iBeacon simulators don't advertise as such */ 89 | return le16toh(beacon->company_id) == 0x004C /* Apple */ && 90 | le16toh(beacon->beacon_type) == 0x1502; 91 | } 92 | 93 | static void ibeacon_metadata_get(uint8_t *adv_data, size_t adv_data_len, 94 | int rssi, broadcaster_meta_data_cb_t cb, void *ctx) 95 | { 96 | char s[6]; 97 | ibeacon_t *beacon = ibeacon_data_get(adv_data, adv_data_len, NULL); 98 | 99 | cb("UUID", _uuidtoa(beacon->proximity_uuid), ctx); 100 | sprintf(s, "%u", be16toh(beacon->major)); 101 | cb("Major", s, ctx); 102 | sprintf(s, "%u", be16toh(beacon->minor)); 103 | cb("Minor", s, ctx); 104 | sprintf(s, "%.2f", pow(10, (beacon->measured_power - rssi) / 20.0)); 105 | cb("Distance", s, ctx); 106 | } 107 | 108 | static broadcaster_ops_t ibeacon_ops = { 109 | .name = "iBeacon", 110 | .is_broadcaster = ibeacon_is_broadcaster, 111 | .metadata_get = ibeacon_metadata_get, 112 | }; 113 | 114 | /* Eddystone */ 115 | #define EDDYSTONE_SERVICE_UUID 0xFEAA 116 | #define EDDYSTONE_FRAME_TYPE_UID 0x00 117 | #define EDDYSTONE_FRAME_TYPE_URL 0x10 118 | #define EDDYSTONE_FRAME_TYPE_TLM 0x20 119 | 120 | typedef struct { 121 | int8_t ranging_data; /* Calibrated Tx power at 0 m */ 122 | uint8_t nid[10]; /* Namespace */ 123 | uint8_t bid[6]; /* Instance */ 124 | uint8_t rfu[2]; /* Reserved for future use */ 125 | } __attribute__((packed)) eddystone_uid_t; 126 | 127 | typedef struct { 128 | int8_t tx_power; /* Calibrated Tx power at 0 m */ 129 | uint8_t url_scheme; /* Encoded Scheme Prefix */ 130 | uint8_t url[0]; /* Length 1-17 */ 131 | } __attribute__((packed)) eddystone_url_t; 132 | 133 | typedef struct { 134 | int8_t version; 135 | uint16_t vbatt; /* Battery voltage, 1mV/bit */ 136 | uint16_t temp; /* Beacon temperature */ 137 | uint32_t adv_cnt; /* Advertising PDU count */ 138 | uint32_t sec_cnt; /* Time since power-on or reboot */ 139 | } __attribute__((packed)) eddystone_tlm_t; 140 | 141 | typedef struct { 142 | uint16_t service_uuid; 143 | uint8_t frame_type; 144 | union { 145 | eddystone_uid_t uid; 146 | eddystone_url_t url; 147 | eddystone_tlm_t tlm; 148 | } u; 149 | } __attribute__((packed)) eddystone_t; 150 | 151 | static eddystone_t *eddystone_data_get(uint8_t *adv_data, uint8_t adv_data_len, 152 | uint8_t *eddystone_len) 153 | { 154 | uint8_t len; 155 | uint8_t *data = esp_ble_resolve_adv_data(adv_data, 156 | ESP_BLE_AD_TYPE_SERVICE_DATA, &len); 157 | 158 | if (eddystone_len) 159 | *eddystone_len = len; 160 | 161 | return (eddystone_t *)data; 162 | } 163 | 164 | static int eddystone_is_broadcaster(uint8_t *adv_data, size_t adv_data_len) 165 | { 166 | uint8_t len; 167 | uint8_t *data = esp_ble_resolve_adv_data(adv_data, 168 | ESP_BLE_AD_TYPE_16SRV_CMPL, &len); 169 | eddystone_t *eddystone; 170 | 171 | if (!data || len != 2 || 172 | le16toh(*(uint16_t *)data) != EDDYSTONE_SERVICE_UUID) 173 | { 174 | return 0; 175 | } 176 | 177 | eddystone = eddystone_data_get(adv_data, adv_data_len, &len); 178 | 179 | /* Make sure we have enough bytes to read UUID and type */ 180 | if (!eddystone || len < offsetof(eddystone_t, u) || 181 | le16toh(eddystone->service_uuid) != EDDYSTONE_SERVICE_UUID) 182 | { 183 | return 0; 184 | } 185 | 186 | /* Validate length */ 187 | if (eddystone->frame_type == EDDYSTONE_FRAME_TYPE_UID) 188 | { 189 | if (len - offsetof(eddystone_t, u) != sizeof(eddystone_uid_t) && 190 | /* RFU is not always available */ 191 | len - offsetof(eddystone_t, u) != offsetof(eddystone_uid_t, rfu)) 192 | { 193 | return 0; 194 | } 195 | } 196 | else if (eddystone->frame_type == EDDYSTONE_FRAME_TYPE_URL) 197 | { 198 | if (len - offsetof(eddystone_t, u) < sizeof(eddystone_url_t)) 199 | return 0; 200 | } 201 | else if (eddystone->frame_type == EDDYSTONE_FRAME_TYPE_TLM) 202 | { 203 | if (len - offsetof(eddystone_t, u) != sizeof(eddystone_tlm_t)) 204 | return 0; 205 | } 206 | else 207 | return 0; /* Unsupported frame type */ 208 | 209 | return 1; 210 | } 211 | 212 | static char *eddystone_url_scheme_get(uint8_t url_scheme) 213 | { 214 | if (url_scheme == 0) return "http://www."; 215 | if (url_scheme == 1) return "https://www."; 216 | if (url_scheme == 2) return "http://"; 217 | if (url_scheme == 3) return "https://"; 218 | 219 | ESP_LOGE(TAG, "Unsupported URL scheme: %d", url_scheme); 220 | return ""; 221 | } 222 | 223 | static char *eddystone_url_get(char url) 224 | { 225 | static char c[2] = {0}; 226 | 227 | if (url == 0) return ".com/"; 228 | if (url == 1) return ".org/"; 229 | if (url == 2) return ".edu/"; 230 | if (url == 3) return ".net/"; 231 | if (url == 4) return ".info/"; 232 | if (url == 5) return ".biz/"; 233 | if (url == 6) return ".gov/"; 234 | if (url == 7) return ".com"; 235 | if (url == 8) return ".org"; 236 | if (url == 9) return ".edu"; 237 | if (url == 10) return ".net"; 238 | if (url == 11) return ".info"; 239 | if (url == 12) return ".biz"; 240 | if (url == 13) return ".gov"; 241 | if (url > 32 && url < 127) 242 | { 243 | *c = url; 244 | return c; 245 | } 246 | 247 | ESP_LOGE(TAG, "Unsupported URL character: 0x%0x", url); 248 | return ""; 249 | } 250 | 251 | static void eddystone_metadata_get(uint8_t *adv_data, size_t adv_data_len, 252 | int rssi, broadcaster_meta_data_cb_t cb, void *ctx) 253 | { 254 | char s[30]; 255 | uint8_t len; 256 | eddystone_t *eddystone = eddystone_data_get(adv_data, adv_data_len, &len); 257 | 258 | if (eddystone->frame_type == EDDYSTONE_FRAME_TYPE_UID) 259 | { 260 | /* Note: 41dBm is the signal loss that occurs over 1 meter */ 261 | sprintf(s, "%.2f", 262 | pow(10, (eddystone->u.uid.ranging_data - rssi - 41) / 20.0)); 263 | cb("Distance", s, ctx); 264 | cb("Namespace", hex2a(s, eddystone->u.uid.nid, 10), ctx); 265 | cb("Instance", hex2a(s, eddystone->u.uid.bid, 6), ctx); 266 | } 267 | else if (eddystone->frame_type == EDDYSTONE_FRAME_TYPE_URL) 268 | { 269 | char *p = s; 270 | int i; 271 | 272 | /* Note: 41dBm is the signal loss that occurs over 1 meter */ 273 | sprintf(s, "%.2f", 274 | pow(10, (eddystone->u.url.tx_power - rssi - 41) / 20.0)); 275 | cb("Distance", s, ctx); 276 | 277 | p += sprintf(p, "%s", 278 | eddystone_url_scheme_get(eddystone->u.url.url_scheme)); 279 | /* Calculate remaining size of URL */ 280 | len -= offsetof(eddystone_t, u) + offsetof(eddystone_url_t, url); 281 | for (i = 0; len > 0; len--, i++) 282 | p += sprintf(p, "%s", eddystone_url_get(eddystone->u.url.url[i])); 283 | cb("URL", s, ctx); 284 | } 285 | else if (eddystone->frame_type == EDDYSTONE_FRAME_TYPE_TLM) 286 | { 287 | if (eddystone->u.tlm.version == 0) 288 | { 289 | uint16_t temp = be16toh(eddystone->u.tlm.temp); 290 | 291 | sprintf(s, "%d", be16toh(eddystone->u.tlm.vbatt)); 292 | cb("Voltage", s, ctx); 293 | sprintf(s, "%d.%02ld", (int8_t)(temp >> 8), 294 | lround((temp & 0xff) * 100 / 256.0)); 295 | cb("Temperature", s, ctx); 296 | sprintf(s, "%" PRIu32, be32toh(eddystone->u.tlm.adv_cnt)); 297 | cb("Count", s, ctx); 298 | sprintf(s, "%" PRIu32, be32toh(eddystone->u.tlm.sec_cnt)); 299 | cb("Uptime", s, ctx); 300 | } 301 | else 302 | { 303 | ESP_LOGE(TAG, "Unsupported TLM verison %d", 304 | eddystone->u.tlm.version); 305 | } 306 | } 307 | } 308 | 309 | static broadcaster_ops_t eddystone_ops = { 310 | .name = "Eddystone", 311 | .is_broadcaster = eddystone_is_broadcaster, 312 | .metadata_get = eddystone_metadata_get, 313 | }; 314 | 315 | /* Xiaomi Mijia Sensors (MiBeacon), for example: 316 | * - LYWSDCGQ - Xiaomi Hygro Thermometer (round, segment LCD) 317 | * - LYWSD02 - Xiaomi Temperature and Humidity sensor (E-Ink with clock) 318 | * - HHCCJCY01 - MiFlora plant sensor 319 | * - GCLS002 - VegTrug Grow Care Garden (very similar to HHCCJCY01) 320 | * - MCCGQ02HL - Xiaomi Mijia Window/Door Sensor 2 321 | * - RTCGQ02LM - Xiaomi Mijia Motion Sensor 2 322 | */ 323 | #define MIJIA_SENSOR_SERVICE_UUID 0xFE95 324 | #define MIJIA_SENSOR_DATA_TYPE_MOTION 0x03 325 | #define MIJIA_SENSOR_DATA_TYPE_TEMP 0x04 326 | #define MIJIA_SENSOR_DATA_TYPE_SWITCH_TEMP 0x05 327 | #define MIJIA_SENSOR_DATA_TYPE_HUM 0x06 328 | #define MIJIA_SENSOR_DATA_TYPE_LUM 0x07 329 | #define MIJIA_SENSOR_DATA_TYPE_MOIST 0x08 330 | #define MIJIA_SENSOR_DATA_TYPE_FDH 0x10 331 | #define MIJIA_SENSOR_DATA_TYPE_COND 0x09 332 | #define MIJIA_SENSOR_DATA_TYPE_BATT 0x0A 333 | #define MIJIA_SENSOR_DATA_TYPE_TEMP_HUM 0x0D 334 | #define MIJIA_SENSOR_DATA_TYPE_MOTION_LIGHT 0x0F 335 | #define MIJIA_SENSOR_DATA_TYPE_SWITCH 0x12 336 | #define MIJIA_SENSOR_DATA_TYPE_CONSUM 0x13 337 | #define MIJIA_SENSOR_DATA_TYPE_MOIST2 0x14 338 | #define MIJIA_SENSOR_DATA_TYPE_SMOKE 0x15 339 | #define MIJIA_SENSOR_DATA_TYPE_LIGHT 0x18 340 | #define MIJIA_SENSOR_DATA_TYPE_DOOR 0x19 341 | 342 | #define MIJIA_DEVICE_TYPE_CGPR1 0x0A83 343 | #define MIJIA_DEVICE_TYPE_MJYD02YL 0x07F6 344 | #define MIJIA_DEVICE_TYPE_RTCGQ02LM 0x0A8D 345 | 346 | typedef struct { 347 | uint8_t data_type; 348 | uint8_t entry_type; 349 | uint8_t data_len; 350 | uint8_t data[0]; 351 | } __attribute__((packed)) mijia_data_entry_t; 352 | 353 | typedef struct { 354 | uint16_t service_uuid; 355 | uint16_t frame_ctrl; 356 | uint16_t device_type; 357 | uint8_t message_counter; 358 | mac_addr_t mac; 359 | } __attribute__((packed)) mijia_header_t; 360 | 361 | static mijia_header_t *mijia_sensor_data_get(uint8_t *adv_data, 362 | uint8_t adv_data_len, uint8_t *mijia_sensor_len) 363 | { 364 | uint8_t len; 365 | uint8_t *data = esp_ble_resolve_adv_data(adv_data, 366 | ESP_BLE_AD_TYPE_SERVICE_DATA, &len); 367 | 368 | if (mijia_sensor_len) 369 | *mijia_sensor_len = len; 370 | 371 | return (mijia_header_t *)data; 372 | } 373 | 374 | static int mijia_sensor_is_broadcaster(uint8_t *adv_data, size_t adv_data_len) 375 | { 376 | uint8_t len; 377 | mijia_header_t *mijia_sensor = mijia_sensor_data_get(adv_data, 378 | adv_data_len, &len); 379 | 380 | if (!mijia_sensor || len < sizeof(mijia_header_t) || 381 | le16toh(mijia_sensor->service_uuid) != MIJIA_SENSOR_SERVICE_UUID) 382 | { 383 | return 0; 384 | } 385 | 386 | return 1; 387 | } 388 | 389 | static void mijia_sensor_metadata_get(uint8_t *adv_data, size_t adv_data_len, 390 | int rssi, broadcaster_meta_data_cb_t cb, void *ctx) 391 | { 392 | char s[9]; 393 | uint8_t len, data_len; 394 | mijia_header_t *mijia_header = mijia_sensor_data_get(adv_data, 395 | adv_data_len, &len); 396 | mijia_data_entry_t *mijia_data_entry; 397 | uint16_t frame_ctrl = le16toh(mijia_header->frame_ctrl); 398 | uint16_t device_type = le16toh(mijia_header->device_type); 399 | uint8_t *payload_start = (uint8_t *)mijia_header + sizeof(mijia_header_t); 400 | uint8_t decrypted[69]; 401 | 402 | cb("MACAddress", _mactoa(mijia_header->mac), ctx); 403 | sprintf(s, "%hhu", mijia_header->message_counter); 404 | cb("MessageCounter", s, ctx); 405 | 406 | /* Check if any data is available */ 407 | if ((frame_ctrl & 0x40) == 0) 408 | return; 409 | 410 | /* Check if there's a capability byte */ 411 | if (frame_ctrl & 0x20) 412 | { 413 | /* Check if there's an IO capability (uses one more byte) */ 414 | if (*payload_start & 0x20) 415 | payload_start++; 416 | payload_start++; 417 | } 418 | 419 | /* Check data is encrypted */ 420 | if (frame_ctrl & 0x08) 421 | { 422 | mbedtls_ccm_context ctx; 423 | uint8_t aad[] = {0x11}; 424 | uint8_t nonce[12]; 425 | uint8_t key[16]; 426 | const char *key_str; 427 | uint8_t version = frame_ctrl >> 12; 428 | size_t crypt_len; 429 | int ret; 430 | 431 | if (version < 4) 432 | { 433 | ESP_LOGW(TAG, "Legacy MiBeacon encryption not supported, " 434 | "skipping %s", _mactoa(mijia_header->mac)); 435 | return; 436 | } 437 | 438 | key_str = config_ble_mikey_get(_mactoa(mijia_header->mac)); 439 | if (!key_str) 440 | { 441 | ESP_LOGE(TAG, "MiBeacon decryption key not found for %s", 442 | _mactoa(mijia_header->mac)); 443 | return; 444 | } 445 | if (strlen(key_str) != sizeof(key) * 2) 446 | { 447 | ESP_LOGE(TAG, "MiBeacon decryption key for %s has the wrong length", 448 | _mactoa(mijia_header->mac)); 449 | return; 450 | } 451 | for (size_t i = 0; i < sizeof(key); i++) 452 | sscanf(key_str + 2 * i, "%02hhx", &key[i]); 453 | 454 | mbedtls_ccm_init(&ctx); 455 | if (mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 128)) 456 | { 457 | ESP_LOGE(TAG, "Could not set MiBeacon decryption key"); 458 | return; 459 | } 460 | 461 | for (uint8_t i = 0; i < 6; i++) 462 | nonce[i] = mijia_header->mac[i]; 463 | for (uint8_t i = 6; i < 9; i++) 464 | nonce[i] = ((uint8_t *)mijia_header)[i - 2]; 465 | for (uint8_t i = 9; i < 12; i++) 466 | nonce[i] = ((uint8_t *)mijia_header)[len - 7 + (i - 9)]; 467 | 468 | crypt_len = len - (payload_start - (uint8_t *)mijia_header) - 7; 469 | if (crypt_len > sizeof(decrypted)) 470 | { 471 | ESP_LOGE(TAG, "MiBeacon encrypted payload too long: %d", crypt_len); 472 | return; 473 | } 474 | 475 | ret = mbedtls_ccm_auth_decrypt(&ctx, crypt_len, nonce, sizeof(nonce), 476 | aad, sizeof(aad), payload_start, decrypted, 477 | (uint8_t *)mijia_header + len - 4, 4); 478 | if (ret) 479 | { 480 | char err[100] = {0}; 481 | mbedtls_strerror(ret, err, 99); 482 | ESP_LOGE(TAG, "Could not decrypt MiBeacon: %s", err); 483 | return; 484 | } 485 | else 486 | { 487 | mijia_data_entry = (mijia_data_entry_t *)decrypted; 488 | data_len = crypt_len; 489 | } 490 | } 491 | else 492 | { 493 | mijia_data_entry = (mijia_data_entry_t *)payload_start; 494 | data_len = len - (payload_start - (uint8_t *)mijia_header); 495 | } 496 | 497 | uint8_t *first_entry = (uint8_t *)mijia_data_entry; 498 | while ((uint8_t *)mijia_data_entry - first_entry < data_len) 499 | { 500 | if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_MOTION) 501 | { 502 | sprintf(s, "%u", *mijia_data_entry->data); 503 | cb("Motion", s, ctx); 504 | } 505 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_TEMP) 506 | { 507 | sprintf(s, "%.1f", 508 | (int16_t)le16toh(*(uint16_t *)mijia_data_entry->data) / 10.0); 509 | cb("Temperature", s, ctx); 510 | } 511 | else if (mijia_data_entry->data_type == 512 | MIJIA_SENSOR_DATA_TYPE_SWITCH_TEMP) 513 | { 514 | sprintf(s, "%u", *mijia_data_entry->data); 515 | cb("Switch", s, ctx); 516 | sprintf(s, "%u", *(mijia_data_entry->data + 1)); 517 | cb("Temperature", s, ctx); 518 | } 519 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_HUM) 520 | { 521 | sprintf(s, "%.1f", 522 | le16toh(*(uint16_t *)mijia_data_entry->data) / 10.0); 523 | cb("Humidity", s, ctx); 524 | } 525 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_MOIST || 526 | mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_MOIST2) 527 | { 528 | sprintf(s, "%u", *mijia_data_entry->data); 529 | cb("Moisture", s, ctx); 530 | } 531 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_FDH) 532 | { 533 | sprintf(s, "%.1f", 534 | le16toh(*(uint16_t *)mijia_data_entry->data) / 100.0); 535 | cb("Formaldehyde", s, ctx); 536 | } 537 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_LUM) 538 | { 539 | uint32_t val = mijia_data_entry->data[0] 540 | | (mijia_data_entry->data[1] << 8) | (mijia_data_entry->data[2] << 16); 541 | sprintf(s, "%" PRIu32, val); 542 | cb("Illuminance", s, ctx); 543 | } 544 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_COND) 545 | { 546 | sprintf(s, "%u", le16toh(*(uint16_t *)mijia_data_entry->data)); 547 | cb("Conductivity", s, ctx); 548 | } 549 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_BATT) 550 | { 551 | sprintf(s, "%u", *mijia_data_entry->data); 552 | cb("BatteryLevel", s, ctx); 553 | } 554 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_TEMP_HUM) 555 | { 556 | sprintf(s, "%.1f", 557 | (int16_t)le16toh(*(uint16_t *)mijia_data_entry->data) / 10.0); 558 | cb("Temperature", s, ctx); 559 | sprintf(s, "%.1f", 560 | le16toh(*(uint16_t *)(mijia_data_entry->data + 2)) / 10.0); 561 | cb("Humidity", s, ctx); 562 | } 563 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_MOTION_LIGHT) 564 | { 565 | uint32_t val = mijia_data_entry->data[0] 566 | | (mijia_data_entry->data[1] << 8) | (mijia_data_entry->data[2] << 16); 567 | if (device_type == MIJIA_DEVICE_TYPE_CGPR1) 568 | { 569 | sprintf(s, "%" PRIu32, val); 570 | cb("Illuminance", s, ctx); 571 | } 572 | else if (device_type == MIJIA_DEVICE_TYPE_MJYD02YL) 573 | cb("Light", val == 100 ? "1" : "0", ctx); 574 | else if (device_type == MIJIA_DEVICE_TYPE_RTCGQ02LM) 575 | cb("Light", val == 256 ? "1" : "0", ctx); 576 | cb("Motion", "1", ctx); 577 | } 578 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_SWITCH) 579 | { 580 | sprintf(s, "%u", *mijia_data_entry->data); 581 | cb("Switch", s, ctx); 582 | } 583 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_CONSUM) 584 | { 585 | sprintf(s, "%u", *mijia_data_entry->data); 586 | cb("Consumable", s, ctx); 587 | } 588 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_SMOKE) 589 | { 590 | sprintf(s, "%u", *mijia_data_entry->data); 591 | cb("Smoke", s, ctx); 592 | } 593 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_LIGHT) 594 | { 595 | sprintf(s, "%u", *mijia_data_entry->data); 596 | cb("Light", s, ctx); 597 | } 598 | else if (mijia_data_entry->data_type == MIJIA_SENSOR_DATA_TYPE_DOOR) 599 | { 600 | sprintf(s, "%u", *mijia_data_entry->data); 601 | cb("DoorClosed", s, ctx); 602 | } 603 | else 604 | { 605 | ESP_LOGW(TAG, "Unknown MiBeacon data type: 0x%x", 606 | mijia_data_entry->data_type); 607 | } 608 | mijia_data_entry = (mijia_data_entry_t *)( 609 | (uint8_t *)mijia_data_entry + 3 + mijia_data_entry->data_len); 610 | } 611 | } 612 | static broadcaster_ops_t mijia_sensor_ops = { 613 | .name = "Xiaomi Mijia", 614 | .is_broadcaster = mijia_sensor_is_broadcaster, 615 | .metadata_get = mijia_sensor_metadata_get, 616 | }; 617 | 618 | /* Beewi Smart Door 619 | * Note that the Beewi Smart Door sensor is also connectable. When connected, it 620 | * provides battery information and door status history (without the current 621 | * state). To overcome this, the sensor should be blacklisted so the app would 622 | * not connect to it and the sensor would brodcast the current state. */ 623 | #define BEEWI_SMART_DOOR_COMPANY_ID 0x000D 624 | #define BEEWI_SMART_DOOR_SERVICE_ID 0x08 625 | #define BEEWI_SMART_DOOR_DATA_TBD1 0x0C 626 | 627 | typedef struct { 628 | uint16_t company_id; 629 | uint8_t service_id; 630 | uint8_t tbd1; 631 | uint8_t status; 632 | uint8_t tbd2; 633 | uint8_t battery; 634 | } __attribute__((packed)) beewi_smart_door_t; 635 | 636 | static beewi_smart_door_t *beewi_smart_door_data_get(uint8_t *adv_data, 637 | uint8_t adv_data_len, uint8_t *beewi_smart_door_len) 638 | { 639 | uint8_t len; 640 | uint8_t *data = esp_ble_resolve_adv_data(adv_data, 641 | ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, &len); 642 | 643 | if (beewi_smart_door_len) 644 | *beewi_smart_door_len = len; 645 | 646 | return (beewi_smart_door_t *)data; 647 | } 648 | 649 | static int beewi_smart_door_is_broadcaster(uint8_t *adv_data, 650 | size_t adv_data_len) 651 | { 652 | uint8_t len; 653 | uint8_t *data = esp_ble_resolve_adv_data(adv_data, 654 | ESP_BLE_AD_TYPE_NAME_CMPL, &len); 655 | 656 | if (len == 16 && strncmp((char *)data, "BeeWi Smart Door", len)) 657 | return 0; 658 | 659 | beewi_smart_door_t *beewi_smart_door = beewi_smart_door_data_get(adv_data, 660 | adv_data_len, &len); 661 | 662 | if (!beewi_smart_door || len != sizeof(beewi_smart_door_t)) 663 | return 0; 664 | 665 | return 1; 666 | } 667 | 668 | static void beewi_smart_door_metadata_get(uint8_t *adv_data, 669 | size_t adv_data_len, int rssi, broadcaster_meta_data_cb_t cb, void *ctx) 670 | { 671 | char s[4]; 672 | beewi_smart_door_t *beewi_smart_door = beewi_smart_door_data_get(adv_data, 673 | adv_data_len, NULL); 674 | 675 | if (beewi_smart_door->tbd1 == BEEWI_SMART_DOOR_DATA_TBD1) 676 | { 677 | sprintf(s,"%hhu",beewi_smart_door->status ); 678 | cb("Status", s, ctx); 679 | sprintf(s,"%hhu",beewi_smart_door->battery); 680 | cb("Battery", s, ctx); 681 | } 682 | } 683 | 684 | static broadcaster_ops_t beewi_smart_door_ops = { 685 | .name = "BeeWi Smart Door", 686 | .is_broadcaster = beewi_smart_door_is_broadcaster, 687 | .metadata_get = beewi_smart_door_metadata_get, 688 | }; 689 | 690 | /* ATC1441 Firmware for the Xiaomi Thermometer LYWSD03MMC Temperature and 691 | * Humidity Sensor, see: https://github.com/atc1441/ATC_MiThermometer */ 692 | 693 | #define ATC1441_TEMP_HUM_SERVICE_UUID 0x181A 694 | 695 | typedef struct { 696 | uint16_t not_used; 697 | uint16_t service_uuid; 698 | mac_addr_t mac; 699 | int16_t temp; 700 | uint8_t humid; 701 | uint8_t battery_percent; 702 | uint16_t battery_mv; 703 | uint8_t message_counter; 704 | } __attribute__((packed)) atc1441_temp_hum_t; 705 | 706 | static atc1441_temp_hum_t *atc1441_temp_hum_data_get(uint8_t *adv_data, 707 | uint8_t adv_data_len, uint8_t *atc1441_temp_hum_len) 708 | { 709 | uint8_t len; 710 | uint8_t *data = esp_ble_resolve_adv_data(adv_data, 711 | ESP_BLE_AD_TYPE_SERVICE_DATA, &len); 712 | 713 | if (atc1441_temp_hum_len) 714 | *atc1441_temp_hum_len = len; 715 | 716 | return (atc1441_temp_hum_t *)data; 717 | } 718 | static int atc1441_temp_hum_is_broadcaster(uint8_t *adv_data, 719 | size_t adv_data_len) 720 | { 721 | uint8_t len; 722 | atc1441_temp_hum_t *atc1441_data = atc1441_temp_hum_data_get(adv_data, 723 | adv_data_len, &len); 724 | 725 | if (len < sizeof(atc1441_temp_hum_t) || 726 | le16toh(atc1441_data->service_uuid) != ATC1441_TEMP_HUM_SERVICE_UUID) 727 | { 728 | return 0; 729 | } 730 | 731 | return 1; 732 | } 733 | 734 | static void atc1441_temp_hum_metadata_get(uint8_t *adv_data, 735 | size_t adv_data_len, int rssi, broadcaster_meta_data_cb_t cb, void *ctx) 736 | { 737 | char s[32]; 738 | uint8_t len; 739 | atc1441_temp_hum_t *atc1441_data = atc1441_temp_hum_data_get(adv_data, 740 | adv_data_len, &len); 741 | 742 | cb("MACAddress", _mactoa(atc1441_data->mac), ctx); 743 | 744 | sprintf(s, "%hhu", atc1441_data->message_counter); 745 | cb("MessageCounter", s, ctx); 746 | 747 | sprintf(s, "%.1f", (int16_t)be16toh(atc1441_data->temp) / 10.0); 748 | cb("Temperature", s, ctx); 749 | 750 | sprintf(s, "%u", atc1441_data->humid); 751 | cb("Humidity", s, ctx); 752 | 753 | sprintf(s, "%u", atc1441_data->battery_percent); 754 | cb("BatteryLevel", s, ctx); 755 | 756 | sprintf(s, "%.3f", be16toh(atc1441_data->battery_mv) / 1000.0 ); 757 | cb("BatteryVolts", s, ctx); 758 | } 759 | 760 | static broadcaster_ops_t atc1441_temp_hum_ops = { 761 | .name = "ATC1441", 762 | .is_broadcaster = atc1441_temp_hum_is_broadcaster, 763 | .metadata_get = atc1441_temp_hum_metadata_get, 764 | }; 765 | 766 | /* Common */ 767 | static broadcaster_ops_t *broadcaster_ops[] = { 768 | &ibeacon_ops, 769 | &eddystone_ops, 770 | &mijia_sensor_ops, 771 | &beewi_smart_door_ops, 772 | &atc1441_temp_hum_ops, 773 | NULL 774 | }; 775 | 776 | broadcaster_ops_t *broadcaster_ops_get(uint8_t *adv_data, size_t adv_data_len) 777 | { 778 | broadcaster_ops_t **ops; 779 | 780 | for (ops = broadcaster_ops; *ops; ops++) 781 | { 782 | if ((*ops)->is_broadcaster(adv_data, adv_data_len)) 783 | return (*ops); 784 | } 785 | 786 | return NULL; 787 | } 788 | -------------------------------------------------------------------------------- /main/ble_utils.c: -------------------------------------------------------------------------------- 1 | #include "ble_utils.h" 2 | #include "config.h" 3 | #include "gatt.h" 4 | #include 5 | #include 6 | #include 7 | 8 | #define CASE_STR(x) case x: return #x 9 | char *gap_event_to_str(esp_gap_ble_cb_event_t event) 10 | { 11 | switch (event) 12 | { 13 | CASE_STR(ESP_GAP_BLE_ADD_WHITELIST_COMPLETE_EVT); 14 | CASE_STR(ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT); 15 | CASE_STR(ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT); 16 | CASE_STR(ESP_GAP_BLE_ADV_START_COMPLETE_EVT); 17 | CASE_STR(ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT); 18 | CASE_STR(ESP_GAP_BLE_AUTH_CMPL_EVT); 19 | CASE_STR(ESP_GAP_BLE_CLEAR_BOND_DEV_COMPLETE_EVT); 20 | CASE_STR(ESP_GAP_BLE_GET_BOND_DEV_COMPLETE_EVT); 21 | CASE_STR(ESP_GAP_BLE_KEY_EVT); 22 | CASE_STR(ESP_GAP_BLE_LOCAL_ER_EVT); 23 | CASE_STR(ESP_GAP_BLE_LOCAL_IR_EVT); 24 | CASE_STR(ESP_GAP_BLE_NC_REQ_EVT); 25 | CASE_STR(ESP_GAP_BLE_OOB_REQ_EVT); 26 | CASE_STR(ESP_GAP_BLE_PASSKEY_NOTIF_EVT); 27 | CASE_STR(ESP_GAP_BLE_PASSKEY_REQ_EVT); 28 | CASE_STR(ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT); 29 | CASE_STR(ESP_GAP_BLE_REMOVE_BOND_DEV_COMPLETE_EVT); 30 | CASE_STR(ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT); 31 | CASE_STR(ESP_GAP_BLE_SCAN_RESULT_EVT); 32 | CASE_STR(ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT); 33 | CASE_STR(ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT); 34 | CASE_STR(ESP_GAP_BLE_SCAN_START_COMPLETE_EVT); 35 | CASE_STR(ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT); 36 | CASE_STR(ESP_GAP_BLE_SEC_REQ_EVT); 37 | CASE_STR(ESP_GAP_BLE_SET_LOCAL_PRIVACY_COMPLETE_EVT); 38 | CASE_STR(ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT); 39 | CASE_STR(ESP_GAP_BLE_SET_STATIC_RAND_ADDR_EVT); 40 | CASE_STR(ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT); 41 | default: return "Invalid GAP event"; 42 | } 43 | } 44 | 45 | char *gattc_event_to_str(esp_gattc_cb_event_t event) 46 | { 47 | switch (event) 48 | { 49 | CASE_STR(ESP_GATTC_REG_EVT); 50 | CASE_STR(ESP_GATTC_UNREG_EVT); 51 | CASE_STR(ESP_GATTC_OPEN_EVT); 52 | CASE_STR(ESP_GATTC_READ_CHAR_EVT); 53 | CASE_STR(ESP_GATTC_WRITE_CHAR_EVT); 54 | CASE_STR(ESP_GATTC_CLOSE_EVT); 55 | CASE_STR(ESP_GATTC_SEARCH_CMPL_EVT); 56 | CASE_STR(ESP_GATTC_SEARCH_RES_EVT); 57 | CASE_STR(ESP_GATTC_READ_DESCR_EVT); 58 | CASE_STR(ESP_GATTC_WRITE_DESCR_EVT); 59 | CASE_STR(ESP_GATTC_NOTIFY_EVT); 60 | CASE_STR(ESP_GATTC_PREP_WRITE_EVT); 61 | CASE_STR(ESP_GATTC_EXEC_EVT); 62 | CASE_STR(ESP_GATTC_ACL_EVT); 63 | CASE_STR(ESP_GATTC_CANCEL_OPEN_EVT); 64 | CASE_STR(ESP_GATTC_SRVC_CHG_EVT); 65 | CASE_STR(ESP_GATTC_ENC_CMPL_CB_EVT); 66 | CASE_STR(ESP_GATTC_CFG_MTU_EVT); 67 | CASE_STR(ESP_GATTC_ADV_DATA_EVT); 68 | CASE_STR(ESP_GATTC_MULT_ADV_ENB_EVT); 69 | CASE_STR(ESP_GATTC_MULT_ADV_UPD_EVT); 70 | CASE_STR(ESP_GATTC_MULT_ADV_DATA_EVT); 71 | CASE_STR(ESP_GATTC_MULT_ADV_DIS_EVT); 72 | CASE_STR(ESP_GATTC_CONGEST_EVT); 73 | CASE_STR(ESP_GATTC_BTH_SCAN_ENB_EVT); 74 | CASE_STR(ESP_GATTC_BTH_SCAN_CFG_EVT); 75 | CASE_STR(ESP_GATTC_BTH_SCAN_RD_EVT); 76 | CASE_STR(ESP_GATTC_BTH_SCAN_THR_EVT); 77 | CASE_STR(ESP_GATTC_BTH_SCAN_PARAM_EVT); 78 | CASE_STR(ESP_GATTC_BTH_SCAN_DIS_EVT); 79 | CASE_STR(ESP_GATTC_SCAN_FLT_CFG_EVT); 80 | CASE_STR(ESP_GATTC_SCAN_FLT_PARAM_EVT); 81 | CASE_STR(ESP_GATTC_SCAN_FLT_STATUS_EVT); 82 | CASE_STR(ESP_GATTC_ADV_VSC_EVT); 83 | CASE_STR(ESP_GATTC_REG_FOR_NOTIFY_EVT); 84 | CASE_STR(ESP_GATTC_UNREG_FOR_NOTIFY_EVT); 85 | CASE_STR(ESP_GATTC_CONNECT_EVT); 86 | CASE_STR(ESP_GATTC_DISCONNECT_EVT); 87 | CASE_STR(ESP_GATTC_READ_MULTIPLE_EVT); 88 | CASE_STR(ESP_GATTC_QUEUE_FULL_EVT); 89 | default: return "Invalid GATTC event"; 90 | } 91 | } 92 | #undef CASE_STR 93 | 94 | char *mactoa(mac_addr_t mac) 95 | { 96 | static char s[18]; 97 | 98 | sprintf(s, MAC_FMT, MAC_PARAM(mac)); 99 | 100 | return s; 101 | } 102 | 103 | int atomac(const char *str, mac_addr_t mac) 104 | { 105 | return sscanf(str, "%2hhx:%2hhx:%2hhx:%2hhx:%2hhx:%2hhx", 106 | &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) != 6; 107 | } 108 | 109 | char *uuidtoa(ble_uuid_t uuid) 110 | { 111 | static char s[37]; 112 | 113 | sprintf(s, UUID_FMT, UUID_PARAM(uuid)); 114 | 115 | return s; 116 | } 117 | 118 | int atouuid(const char *str, ble_uuid_t uuid) 119 | { 120 | return sscanf(str, 121 | "%2hhx%2hhx%2hhx%2hhx-%2hhx%2hhx-%2hhx%2hhx-%2hhx%2hhx-" 122 | "%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx", 123 | &uuid[15], &uuid[14], &uuid[13], &uuid[12], 124 | &uuid[11], &uuid[10], 125 | &uuid[9], &uuid[8], 126 | &uuid[7], &uuid[6], 127 | &uuid[5], &uuid[4], &uuid[3], &uuid[2], &uuid[1], &uuid[0]) != 16; 128 | } 129 | 130 | bool ble_uuid_equal(ble_uuid_t uuid1, ble_uuid_t uuid2){ 131 | return memcmp(uuid1, uuid2, sizeof(ble_uuid_t)) == 0; 132 | } 133 | 134 | bool ble_mac_equal(mac_addr_t mac1, mac_addr_t mac2){ 135 | return memcmp(mac1, mac2, sizeof(mac_addr_t)) == 0; 136 | } 137 | 138 | static service_desc_t *ble_get_sig_service(ble_uuid_t uuid) 139 | { 140 | service_desc_t *p; 141 | 142 | for (p = services; p->name; p++) 143 | { 144 | if (!ble_uuid_equal(p->uuid, uuid)) 145 | continue; 146 | 147 | return p; 148 | } 149 | 150 | return NULL; 151 | } 152 | 153 | static characteristic_desc_t *ble_get_sig_characteristic(ble_uuid_t uuid) 154 | { 155 | characteristic_desc_t *p; 156 | 157 | for (p = characteristics; p->name; p++) 158 | { 159 | if (!ble_uuid_equal(p->uuid, uuid)) 160 | continue; 161 | 162 | return p; 163 | } 164 | 165 | return NULL; 166 | } 167 | 168 | static characteristic_type_t *ble_get_sig_characteristic_types(ble_uuid_t uuid) 169 | { 170 | characteristic_desc_t *c = ble_get_sig_characteristic(uuid); 171 | return c ? c->types : NULL; 172 | } 173 | 174 | static characteristic_type_t ble_atotype(const char *type) 175 | { 176 | struct { 177 | const char *name; 178 | int type; 179 | } *p, types[] = { 180 | { "boolean", CHAR_TYPE_BOOLEAN }, 181 | { "2bit", CHAR_TYPE_2BIT }, 182 | { "4bit", CHAR_TYPE_4BIT }, 183 | { "nibble", CHAR_TYPE_NIBBLE }, 184 | { "8bit", CHAR_TYPE_8BIT }, 185 | { "uint8", CHAR_TYPE_UINT8 }, 186 | { "sint8", CHAR_TYPE_SINT8 }, 187 | { "uint12", CHAR_TYPE_UINT12 }, 188 | { "16bit", CHAR_TYPE_16BIT }, 189 | { "uint16", CHAR_TYPE_UINT16 }, 190 | { "sint16", CHAR_TYPE_SINT16 }, 191 | { "24bit", CHAR_TYPE_24BIT }, 192 | { "uint24", CHAR_TYPE_UINT24 }, 193 | { "sint24", CHAR_TYPE_SINT24 }, 194 | { "32bit", CHAR_TYPE_32BIT }, 195 | { "uint32", CHAR_TYPE_UINT32 }, 196 | { "sint32", CHAR_TYPE_SINT32 }, 197 | { "uint40", CHAR_TYPE_UINT40 }, 198 | { "uint48", CHAR_TYPE_UINT48 }, 199 | { "uint128", CHAR_TYPE_UINT128 }, 200 | { "utf8s", CHAR_TYPE_UTF8S }, 201 | { "float64", CHAR_TYPE_FLOAT64 }, 202 | { "sfloat", CHAR_TYPE_SFLOAT }, 203 | { "float", CHAR_TYPE_FLOAT }, 204 | { "reg-cert-data-list", CHAR_TYPE_REG_CERT_DATA_LIST }, 205 | { "variable", CHAR_TYPE_VARIABLE }, 206 | { "gatt-uuid", CHAR_TYPE_GATT_UUID }, 207 | { NULL, CHAR_TYPE_UNKNOWN }, 208 | }; 209 | 210 | for (p = types; p->name; p++) 211 | { 212 | if (!strcmp(p->name, type)) 213 | break; 214 | } 215 | 216 | return p->type; 217 | } 218 | 219 | static characteristic_type_t *ble_get_characteristic_types(ble_uuid_t uuid) 220 | { 221 | static characteristic_type_t ret[32]; 222 | int i = 0; 223 | const char **iter, **conf_types = 224 | config_ble_characteristic_types_get(uuidtoa(uuid)); 225 | 226 | if (!conf_types) 227 | return ble_get_sig_characteristic_types(uuid); 228 | 229 | for (iter = conf_types; *iter; iter++) 230 | ret[i++] = ble_atotype(*iter); 231 | ret[i] = -1; 232 | 233 | return ret; 234 | } 235 | 236 | static size_t ble_type_size(characteristic_type_t type) 237 | { 238 | switch (type) 239 | { 240 | case CHAR_TYPE_BOOLEAN: 241 | case CHAR_TYPE_2BIT: 242 | case CHAR_TYPE_4BIT: 243 | case CHAR_TYPE_NIBBLE: 244 | case CHAR_TYPE_8BIT: 245 | case CHAR_TYPE_UINT8: 246 | case CHAR_TYPE_SINT8: 247 | return 1; 248 | case CHAR_TYPE_UINT12: 249 | case CHAR_TYPE_16BIT: 250 | case CHAR_TYPE_UINT16: 251 | case CHAR_TYPE_SINT16: 252 | case CHAR_TYPE_SFLOAT: 253 | return 2; 254 | case CHAR_TYPE_24BIT: 255 | case CHAR_TYPE_UINT24: 256 | case CHAR_TYPE_SINT24: 257 | return 3; 258 | case CHAR_TYPE_32BIT: 259 | case CHAR_TYPE_UINT32: 260 | case CHAR_TYPE_SINT32: 261 | case CHAR_TYPE_FLOAT: 262 | return 4; 263 | case CHAR_TYPE_UINT40: 264 | return 5; 265 | case CHAR_TYPE_UINT48: 266 | return 6; 267 | case CHAR_TYPE_FLOAT64: 268 | return 8; 269 | case CHAR_TYPE_UTF8S: 270 | /* String length are whatever is left in the payload, fall-through */ 271 | case CHAR_TYPE_UINT128: 272 | case CHAR_TYPE_REG_CERT_DATA_LIST: 273 | case CHAR_TYPE_VARIABLE: 274 | case CHAR_TYPE_GATT_UUID: 275 | case CHAR_TYPE_UNKNOWN: 276 | return 0; 277 | } 278 | 279 | return 0; 280 | } 281 | 282 | char *chartoa(ble_uuid_t uuid, const uint8_t *data, size_t len) 283 | { 284 | characteristic_type_t *types = ble_get_characteristic_types(uuid); 285 | static char buf[1024]; 286 | char *p = buf; 287 | int i = 0; 288 | 289 | /* A note from the Bluetooth specification: 290 | * If a format is not a whole number of octets, then the data shall be 291 | * contained within the least significant bits of the value, and all other 292 | * bits shall be set to zero on transmission and ignored upon receipt. If 293 | * the Characteristic Value is less than an octet, it occupies an entire 294 | * octet. 295 | */ 296 | for (; types && *types != -1; types++) 297 | { 298 | size_t type_len = ble_type_size(*types); 299 | 300 | if (len - i < type_len) 301 | break; 302 | 303 | switch (*types) 304 | { 305 | case CHAR_TYPE_BOOLEAN: 306 | p += sprintf(p, "%s,", data[i] & 0x01 ? "true" : "false"); 307 | break; 308 | case CHAR_TYPE_2BIT: 309 | { 310 | int j; 311 | 312 | for (; i < len; i++) { 313 | for (j = 0; j < 8; j += 2) 314 | p += sprintf(p, "%" PRIu8 ",", (data[i] >> j) & 0b11); 315 | } 316 | break; 317 | } 318 | case CHAR_TYPE_4BIT: 319 | case CHAR_TYPE_NIBBLE: 320 | p += sprintf(p, "%" PRIu8 ",", data[i] & 0x0F); 321 | break; 322 | case CHAR_TYPE_8BIT: 323 | case CHAR_TYPE_UINT8: 324 | case CHAR_TYPE_SINT8: 325 | if (*types == CHAR_TYPE_SINT8) 326 | p += sprintf(p, "%" PRId8 ",", data[i]); 327 | else 328 | p += sprintf(p, "%" PRIu8 ",", data[i]); 329 | 330 | break; 331 | case CHAR_TYPE_UINT12: 332 | { 333 | uint16_t tmp = (data[i + 1] << 8) | data[i]; 334 | 335 | p += sprintf(p, "%" PRIu16 ",", tmp & 0x0FFF); 336 | break; 337 | } 338 | case CHAR_TYPE_16BIT: 339 | case CHAR_TYPE_UINT16: 340 | case CHAR_TYPE_SINT16: 341 | { 342 | uint16_t tmp = (data[i + 1] << 8) | data[i]; 343 | 344 | if (*types == CHAR_TYPE_SINT16) 345 | p += sprintf(p, "%" PRId16 ",", tmp); 346 | else 347 | p += sprintf(p, "%" PRIu16 ",", tmp); 348 | 349 | break; 350 | } 351 | case CHAR_TYPE_24BIT: 352 | case CHAR_TYPE_UINT24: 353 | case CHAR_TYPE_SINT24: 354 | { 355 | uint32_t tmp = (data[i + 2] << 16) | (data[i + 1] << 8) | data[i]; 356 | 357 | if (*types == CHAR_TYPE_SINT24) 358 | p += sprintf(p, "%" PRId32 ",", (int32_t)tmp << 8 >> 8); 359 | else 360 | p += sprintf(p, "%" PRIu32 ",", tmp); 361 | 362 | break; 363 | } 364 | case CHAR_TYPE_32BIT: 365 | case CHAR_TYPE_UINT32: 366 | case CHAR_TYPE_SINT32: 367 | { 368 | uint32_t tmp = (data[i + 3] << 24) | (data[i + 2] << 16) | 369 | (data[i + 1] << 8) | data[i]; 370 | 371 | if (*types == CHAR_TYPE_SINT32) 372 | p += sprintf(p, "%" PRId32 ",", tmp); 373 | else 374 | p += sprintf(p, "%" PRIu32 ",", tmp); 375 | 376 | break; 377 | } 378 | case CHAR_TYPE_UINT40: 379 | { 380 | uint64_t tmp = ((uint64_t)data[i + 4] << 32) | (data[i + 3] << 24) | 381 | (data[i + 2] << 16) | (data[i + 1] << 8) | data[i]; 382 | 383 | p += sprintf(p, "%" PRIu64 ",", tmp); 384 | 385 | break; 386 | } 387 | case CHAR_TYPE_UINT48: 388 | { 389 | uint64_t tmp = ((uint64_t)data[i + 5] << 40) | 390 | ((uint64_t)data[i + 4] << 32) | (data[i + 3] << 24) | 391 | (data[i + 2] << 16) | (data[i + 1] << 8) | data[i]; 392 | 393 | p += sprintf(p, "%" PRIu64 ",", tmp); 394 | 395 | break; 396 | } 397 | /* String values consume the rest of the buffer */ 398 | case CHAR_TYPE_UTF8S: 399 | { 400 | int c = len - i; 401 | 402 | memcpy(p, &data[i], c); 403 | 404 | p += c; 405 | i += c; 406 | p += sprintf(p, ","); 407 | break; 408 | } 409 | /* IEEE-754 floating point format */ 410 | /* Note, ESP-32 is little endian, as is the characteristic value */ 411 | case CHAR_TYPE_FLOAT64: 412 | { 413 | union { 414 | double d; 415 | uint8_t b[8]; 416 | } tmp; 417 | memcpy(&tmp.b, &data[i], 8); 418 | 419 | p += sprintf(p, "%f,", tmp.d); 420 | break; 421 | } 422 | /* IEEE-11073 floating point format */ 423 | case CHAR_TYPE_SFLOAT: 424 | { 425 | uint16_t tmp = (data[i + 1] << 8) | data[i]; 426 | int16_t mantissa = tmp & 0X0FFF; 427 | int8_t exponent = (tmp >> 12) & 0x0F; 428 | 429 | /* Fix sign */ 430 | if (exponent >= 0x0008) 431 | exponent = -((0x000F + 1) - exponent); 432 | if (mantissa >= 0x0800) 433 | mantissa = -((0x0FFF + 1) - mantissa); 434 | 435 | p += sprintf(p, "%f,", mantissa * pow(10.0f, exponent)); 436 | break; 437 | } 438 | case CHAR_TYPE_FLOAT: 439 | { 440 | int8_t exponent = data[i + 3]; 441 | int32_t mantissa = ((data[i + 2] << 24) | (data[i + 1] << 16) | 442 | data[i] << 8) >> 8; 443 | 444 | /* Fix sign */ 445 | if (mantissa >= 0x800000) 446 | mantissa = -((0xFFFFFF + 1) - mantissa); 447 | 448 | p += sprintf(p, "%f,", mantissa * pow(10.0f, exponent)); 449 | break; 450 | } 451 | case CHAR_TYPE_UINT128: 452 | case CHAR_TYPE_REG_CERT_DATA_LIST: 453 | case CHAR_TYPE_VARIABLE: 454 | case CHAR_TYPE_GATT_UUID: 455 | case CHAR_TYPE_UNKNOWN: 456 | printf(">>>> Unhandled characteristic type %d <<<<\n", *types); 457 | } 458 | 459 | i += type_len; 460 | } 461 | 462 | for (; i < len; i++) 463 | p += sprintf(p, "%" PRIu8 ",", data[i]); 464 | 465 | *(p - 1) = '\0'; 466 | return buf; 467 | } 468 | 469 | uint8_t *atochar(ble_uuid_t uuid, const char *data, size_t len, size_t *ret_len) 470 | { 471 | characteristic_type_t *types = ble_get_characteristic_types(uuid); 472 | static uint8_t buf[512]; 473 | uint8_t *p = buf; 474 | char *str = strndup(data, len); 475 | char *val = strtok(str, ","); 476 | 477 | for (; types && *types != -1 && val; types++) 478 | { 479 | switch (*types) 480 | { 481 | case CHAR_TYPE_BOOLEAN: 482 | *p = !strcmp(val, "true") ? 1 : 0; 483 | p += 1; 484 | break; 485 | case CHAR_TYPE_2BIT: 486 | { 487 | uint8_t crumb_index = 0; 488 | 489 | p--; 490 | while (val != NULL) { 491 | uint8_t crumb = strtoul(val, NULL, 10) & 0b11; 492 | 493 | if ((crumb_index % 4) == 0) 494 | *(++p) = 0; 495 | *p |= (crumb << ((crumb_index % 4) * 2)); 496 | 497 | val = strtok(NULL, ","); 498 | crumb_index++; 499 | } 500 | 501 | p += 1; 502 | break; 503 | } 504 | case CHAR_TYPE_4BIT: 505 | case CHAR_TYPE_NIBBLE: 506 | *p = strtoul(val, NULL, 10) & 0x0F; 507 | p += 1; 508 | break; 509 | case CHAR_TYPE_8BIT: 510 | case CHAR_TYPE_UINT8: 511 | case CHAR_TYPE_SINT8: 512 | if (*types == CHAR_TYPE_SINT8) 513 | *p = strtol(val, NULL, 10); 514 | else 515 | *p = strtoul(val, NULL, 10); 516 | p += 1; 517 | break; 518 | case CHAR_TYPE_UINT12: 519 | { 520 | uint16_t tmp = strtoul(val, NULL, 10) & 0x0FFF; 521 | 522 | *p = tmp & 0xFF; 523 | *(p + 1) = (tmp >> 8) & 0xFF; 524 | 525 | p += 2; 526 | break; 527 | } 528 | case CHAR_TYPE_16BIT: 529 | case CHAR_TYPE_UINT16: 530 | { 531 | uint16_t tmp = strtoul(val, NULL, 10) & 0xFFFF; 532 | 533 | *p = tmp & 0xFF; 534 | *(p + 1) = (tmp >> 8) & 0xFF; 535 | 536 | p += 2; 537 | break; 538 | } 539 | case CHAR_TYPE_SINT16: 540 | { 541 | uint16_t tmp = strtol(val, NULL, 10) & 0xFFFF; 542 | 543 | *p = tmp & 0xFF; 544 | *(p + 1) = (tmp >> 8) & 0xFF; 545 | 546 | p += 2; 547 | break; 548 | } 549 | case CHAR_TYPE_24BIT: 550 | case CHAR_TYPE_UINT24: 551 | { 552 | uint32_t tmp = strtoul(val, NULL, 10) & 0x00FFFFFF; 553 | 554 | *p = tmp & 0xFF; 555 | *(p + 1) = (tmp >> 8) & 0xFF; 556 | *(p + 2) = (tmp >> 16) & 0xFF; 557 | 558 | p += 3; 559 | break; 560 | } 561 | case CHAR_TYPE_SINT24: 562 | { 563 | uint32_t tmp = strtol(val, NULL, 10) & 0x00FFFFFF; 564 | 565 | *p = tmp & 0xFF; 566 | *(p + 1) = (tmp >> 8) & 0xFF; 567 | *(p + 2) = (tmp >> 16) & 0xFF; 568 | 569 | p += 3; 570 | break; 571 | } 572 | case CHAR_TYPE_32BIT: 573 | { 574 | uint32_t tmp = strtol(val, NULL, 10); 575 | 576 | *p = tmp & 0xFF; 577 | *(p + 1) = (tmp >> 8) & 0xFF; 578 | *(p + 2) = (tmp >> 16) & 0xFF; 579 | *(p + 3) = (tmp >> 24) & 0xFF; 580 | 581 | p += 4; 582 | break; 583 | } 584 | case CHAR_TYPE_UINT32: 585 | case CHAR_TYPE_SINT32: 586 | { 587 | uint32_t tmp = strtoul(val, NULL, 10); 588 | 589 | *p = tmp & 0xFF; 590 | *(p + 1) = (tmp >> 8) & 0xFF; 591 | *(p + 2) = (tmp >> 16) & 0xFF; 592 | *(p + 3) = (tmp >> 24) & 0xFF; 593 | 594 | p += 4; 595 | break; 596 | } 597 | case CHAR_TYPE_UINT40: 598 | { 599 | uint64_t tmp = strtoul(val, NULL, 10); 600 | 601 | *p = tmp & 0xFF; 602 | *(p + 1) = (tmp >> 8) & 0xFF; 603 | *(p + 2) = (tmp >> 16) & 0xFF; 604 | *(p + 3) = (tmp >> 24) & 0xFF; 605 | *(p + 4) = (tmp >> 32) & 0xFF; 606 | 607 | p += 5; 608 | break; 609 | } 610 | case CHAR_TYPE_UINT48: 611 | { 612 | uint64_t tmp = strtoul(val, NULL, 10); 613 | 614 | *p = tmp & 0xFF; 615 | *(p + 1) = (tmp >> 8) & 0xFF; 616 | *(p + 2) = (tmp >> 16) & 0xFF; 617 | *(p + 3) = (tmp >> 24) & 0xFF; 618 | *(p + 4) = (tmp >> 32) & 0xFF; 619 | *(p + 5) = (tmp >> 40) & 0xFF; 620 | 621 | p += 6; 622 | break; 623 | } 624 | /* String values consume the rest of the buffer */ 625 | case CHAR_TYPE_UTF8S: 626 | { 627 | size_t len = strlen(val); 628 | 629 | strcpy((char *)p, val); 630 | 631 | p += len + 1; 632 | break; 633 | } 634 | /* IEEE-754 floating point format */ 635 | /* Note, ESP-32 is little endian, as is the characteristic value */ 636 | case CHAR_TYPE_FLOAT64: 637 | { 638 | union { 639 | double d; 640 | uint8_t b[8]; 641 | } tmp; 642 | tmp.d = strtod(val, NULL); 643 | 644 | memcpy(p, tmp.b, 8); 645 | 646 | p += 8; 647 | break; 648 | } 649 | case CHAR_TYPE_UINT128: 650 | /* IEEE-11073 floating point format */ 651 | case CHAR_TYPE_SFLOAT: 652 | case CHAR_TYPE_FLOAT: 653 | case CHAR_TYPE_REG_CERT_DATA_LIST: 654 | case CHAR_TYPE_VARIABLE: 655 | case CHAR_TYPE_GATT_UUID: 656 | case CHAR_TYPE_UNKNOWN: 657 | printf(">>>> Unhandled characteristic type %d <<<<\n", *types); 658 | } 659 | 660 | val = strtok(NULL, ","); 661 | } 662 | 663 | while (val) 664 | { 665 | *p = strtoul(val, NULL, 10); 666 | p += 1; 667 | val = strtok(NULL, ","); 668 | } 669 | 670 | free(str); 671 | 672 | *ret_len = p - buf; 673 | return buf; 674 | } 675 | 676 | static const char *ble_get_sig_service_name(ble_uuid_t uuid) 677 | { 678 | service_desc_t *p = ble_get_sig_service(uuid); 679 | 680 | return p ? p->name : NULL; 681 | } 682 | 683 | const char *ble_service_name_get(ble_uuid_t uuid) 684 | { 685 | const char *name = config_ble_service_name_get(uuidtoa(uuid)); 686 | 687 | if (name) 688 | return name; 689 | 690 | return ble_get_sig_service_name(uuid) ? : uuidtoa(uuid); 691 | } 692 | 693 | static const char *ble_get_sig_characteristic_name(ble_uuid_t uuid) 694 | { 695 | characteristic_desc_t *p = ble_get_sig_characteristic(uuid); 696 | 697 | return p ? p->name : NULL; 698 | } 699 | 700 | const char *ble_characteristic_name_get(ble_uuid_t uuid) 701 | { 702 | const char *name = config_ble_characteristic_name_get(uuidtoa(uuid)); 703 | 704 | if (name) 705 | return name; 706 | 707 | return ble_get_sig_characteristic_name(uuid) ? : uuidtoa(uuid); 708 | } 709 | 710 | ble_device_t *ble_device_add(ble_device_t **list, const char *name, 711 | mac_addr_t mac, esp_ble_addr_type_t addr_type, uint16_t conn_id) 712 | { 713 | ble_device_t *dev, **cur; 714 | 715 | dev = calloc(1, sizeof(*dev)); 716 | dev->name = name ? strdup(name) : NULL; 717 | memcpy(dev->mac, mac, sizeof(mac_addr_t)); 718 | dev->addr_type = addr_type; 719 | dev->conn_id = conn_id; 720 | 721 | for (cur = list; *cur; cur = &(*cur)->next); 722 | *cur = dev; 723 | 724 | return dev; 725 | } 726 | 727 | void ble_device_update_name(ble_device_t *device, const char *name) 728 | { 729 | if (device->name) 730 | free(device->name); 731 | device->name = strdup(name); 732 | } 733 | 734 | ble_device_t *ble_device_find_by_mac(ble_device_t *list, mac_addr_t mac) 735 | { 736 | ble_device_t *cur; 737 | 738 | for (cur = list; cur; cur = cur->next) 739 | { 740 | if (ble_mac_equal(cur->mac, mac)) 741 | break; 742 | } 743 | 744 | return cur; 745 | } 746 | 747 | void ble_device_foreach(ble_device_t *list, ble_on_device_cb_t cb) 748 | { 749 | for (; list; list = list->next) 750 | cb(list); 751 | } 752 | 753 | ble_device_t *ble_device_find_by_conn_id(ble_device_t *list, uint16_t conn_id) 754 | { 755 | ble_device_t *cur; 756 | 757 | for (cur = list; cur; cur = cur->next) 758 | { 759 | if (cur->conn_id == conn_id) 760 | break; 761 | } 762 | 763 | return cur; 764 | } 765 | 766 | void ble_device_remove_by_mac(ble_device_t **list, mac_addr_t mac) 767 | { 768 | ble_device_t **cur, *tmp; 769 | 770 | for (cur = list; *cur; cur = &(*cur)->next) 771 | { 772 | if (ble_mac_equal((*cur)->mac, mac)) 773 | break; 774 | } 775 | 776 | if (!*cur) 777 | return; 778 | 779 | tmp = *cur; 780 | *cur = (*cur)->next; 781 | ble_device_free(tmp); 782 | } 783 | 784 | void ble_device_remove_by_conn_id(ble_device_t **list, uint16_t conn_id) 785 | { 786 | ble_device_t **cur, *tmp; 787 | 788 | for (cur = list; *cur; cur = &(*cur)->next) 789 | { 790 | if ((*cur)->conn_id == conn_id) 791 | break; 792 | } 793 | 794 | if (!*cur) 795 | return; 796 | 797 | tmp = *cur; 798 | *cur = (*cur)->next; 799 | ble_device_free(tmp); 800 | } 801 | 802 | void ble_device_remove_disconnected(ble_device_t **list) 803 | { 804 | ble_device_t *tmp, **cur = list; 805 | 806 | while (*cur) 807 | { 808 | if ((*cur)->conn_id == 0xffff) 809 | { 810 | tmp = *cur; 811 | *cur = (*cur)->next; 812 | ble_device_free(tmp); 813 | } 814 | else 815 | cur = &(*cur)->next; 816 | } 817 | } 818 | 819 | void ble_device_free(ble_device_t *dev) 820 | { 821 | ble_device_services_free(&dev->services); 822 | if (dev->name) 823 | free(dev->name); 824 | free(dev); 825 | } 826 | 827 | void ble_devices_free(ble_device_t **list) 828 | { 829 | ble_device_t *cur, **head = list; 830 | 831 | while (*list) 832 | { 833 | cur = *list; 834 | *list = cur->next; 835 | ble_device_free(cur); 836 | } 837 | *head = NULL; 838 | } 839 | 840 | ble_service_t *ble_device_service_add(ble_device_t *device, ble_uuid_t uuid) 841 | { 842 | ble_service_t *service, **cur; 843 | 844 | service = malloc(sizeof(*service)); 845 | service->next = NULL; 846 | memcpy(service->uuid, uuid, sizeof(ble_uuid_t)); 847 | service->characteristics = NULL; 848 | 849 | for (cur = &device->services; *cur; cur = &(*cur)->next); 850 | *cur = service; 851 | 852 | return service; 853 | } 854 | 855 | ble_service_t *ble_device_service_find(ble_device_t *device, ble_uuid_t uuid) 856 | { 857 | ble_service_t *cur; 858 | 859 | for (cur = device->services; cur; cur = cur->next) 860 | { 861 | if (ble_uuid_equal(cur->uuid, uuid)) 862 | break; 863 | } 864 | 865 | return cur; 866 | } 867 | 868 | void ble_device_service_free(ble_service_t *service) 869 | { 870 | ble_device_characteristics_free(&service->characteristics); 871 | free(service); 872 | } 873 | 874 | void ble_device_services_free(ble_service_t **list) 875 | { 876 | ble_service_t *cur, **head = list; 877 | 878 | while (*list) 879 | { 880 | cur = *list; 881 | *list = cur->next; 882 | ble_device_service_free(cur); 883 | } 884 | *head = NULL; 885 | } 886 | 887 | ble_characteristic_t *ble_device_characteristic_add(ble_service_t *service, 888 | ble_uuid_t uuid, uint8_t index, uint16_t handle, uint8_t properties) 889 | { 890 | ble_characteristic_t *characteristic, **cur; 891 | 892 | characteristic = malloc(sizeof(*characteristic)); 893 | characteristic->next = NULL; 894 | memcpy(characteristic->uuid, uuid, sizeof(ble_uuid_t)); 895 | characteristic->handle = handle; 896 | characteristic->properties = properties; 897 | characteristic->client_config_handle = 0; 898 | characteristic->index = index; 899 | 900 | for (cur = &service->characteristics; *cur; cur = &(*cur)->next); 901 | *cur = characteristic; 902 | 903 | return characteristic; 904 | } 905 | 906 | ble_characteristic_t *ble_device_characteristic_find_by_uuid( 907 | ble_service_t *service, ble_uuid_t uuid, uint8_t index) 908 | { 909 | ble_characteristic_t *cur; 910 | 911 | for (cur = service->characteristics; cur; cur = cur->next) 912 | { 913 | if (ble_uuid_equal(cur->uuid, uuid) && cur->index == index) 914 | break; 915 | } 916 | 917 | return cur; 918 | } 919 | 920 | ble_characteristic_t *ble_device_characteristic_find_by_handle( 921 | ble_service_t *service, uint16_t handle) 922 | { 923 | ble_characteristic_t *cur; 924 | 925 | for (cur = service->characteristics; cur; cur = cur->next) 926 | { 927 | if (cur->handle == handle) 928 | break; 929 | } 930 | 931 | return cur; 932 | } 933 | 934 | void ble_device_characteristic_free(ble_characteristic_t *characteristic) 935 | { 936 | free(characteristic); 937 | } 938 | 939 | void ble_device_characteristics_free(ble_characteristic_t **list) 940 | { 941 | ble_characteristic_t *cur, **head = list; 942 | 943 | while (*list) 944 | { 945 | cur = *list; 946 | *list = cur->next; 947 | ble_device_characteristic_free(cur); 948 | } 949 | *head = NULL; 950 | } 951 | 952 | int ble_device_info_get_by_conn_id_handle(ble_device_t *list, uint16_t conn_id, 953 | uint16_t handle, ble_device_t **device, ble_service_t **service, 954 | ble_characteristic_t **characteristic) 955 | { 956 | if (!(*device = ble_device_find_by_conn_id(list, conn_id))) 957 | return -1; 958 | 959 | for (*service = (*device)->services; *service; *service = (*service)->next) 960 | { 961 | for (*characteristic = (*service)->characteristics; *characteristic; 962 | *characteristic = (*characteristic)->next) 963 | { 964 | if ((*characteristic)->handle == handle) 965 | return 0; 966 | } 967 | } 968 | 969 | return -1; 970 | } 971 | --------------------------------------------------------------------------------