├── .clangd ├── .envrc ├── .github ├── FUNDING.yml └── workflows │ └── esp32.yml ├── .gitignore ├── .gitmodules ├── .nvim.lua ├── .vscode └── settings.json ├── .zed └── settings.json ├── CMakeLists.txt ├── LICENSE ├── README.md ├── components └── HomeSpan │ └── CMakeLists.txt ├── data ├── assets │ ├── ap-icon.webp │ ├── favicon.webp │ ├── hk-finish-0.webp │ ├── hk-finish-1.webp │ ├── hk-finish-2.webp │ ├── hk-finish-3.webp │ ├── logo-white.webp │ ├── misc.css │ ├── restart-R.webp │ └── trashcan.webp ├── index.html └── routes │ ├── actions.html │ ├── hkinfo.html │ ├── misc.html │ └── mqtt.html ├── main ├── CMakeLists.txt ├── Kconfig.projbuild ├── idf_component.yml ├── include │ ├── NFC_SERV_CHARS.h │ └── config.h └── main.cpp ├── sdkconfig.defaults └── with_ota.csv /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | CompilationDatabase: build 3 | Add: [-ferror-limit=0] 4 | Remove: [-fno-tree-switch-conversion, -fstrict-volatile-bitfields, -march=rv32imc_zicsr_zifencei] -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | . $HOME/esp/v5.3.2/esp-idf/export.sh 2 | export PATH=$HOME/esp-clang/bin:$PATH -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: rednblkx 2 | custom: "https://www.paypal.me/rednblkx" 3 | -------------------------------------------------------------------------------- /.github/workflows/esp32.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | on: 7 | workflow_dispatch: 8 | pull_request: 9 | paths-ignore: 10 | - '.vscode/**' 11 | - '.gitignore' 12 | - 'LICENSE' 13 | - 'README.md' 14 | 15 | jobs: 16 | esp32: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | fetch-tags: true 24 | submodules: 'recursive' 25 | - uses: actions/cache@v4 26 | with: 27 | path: | 28 | ~/.ccache 29 | build 30 | managed_components 31 | sdkconfig 32 | key: ${{ runner.os }}-esp32-build 33 | - name: Cache Docker images. 34 | uses: ScribeMD/docker-cache@0.5.0 35 | with: 36 | key: docker-${{ runner.os }}-espidf 37 | - name: ESP32 Build 38 | uses: espressif/esp-idf-ci-action@v1 39 | with: 40 | esp_idf_version: v5.3.2 41 | target: esp32 42 | path: '.' 43 | extra_docker_args: -v ~/.ccache:/root/.ccache -e CCACHE_DIR=/root/.ccache 44 | command: idf.py --ccache build && idf.py merge-bin 45 | - name: Archive firmware 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: esp32-firmware 49 | path: build/HomeKey-ESP32.bin 50 | - name: Archive merged binary 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: esp32-firmware-merged 54 | path: build/merged-binary.bin 55 | esp32c3: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | with: 61 | fetch-depth: 0 62 | fetch-tags: true 63 | submodules: 'recursive' 64 | - uses: actions/cache@v4 65 | with: 66 | path: | 67 | ~/.ccache 68 | build 69 | managed_components 70 | sdkconfig 71 | key: ${{ runner.os }}-esp32c3-build 72 | - name: Cache Docker images. 73 | uses: ScribeMD/docker-cache@0.5.0 74 | with: 75 | key: docker-${{ runner.os }}-espidf 76 | - name: ESP32C3 Build 77 | uses: espressif/esp-idf-ci-action@v1 78 | with: 79 | esp_idf_version: v5.3.2 80 | target: esp32c3 81 | path: '.' 82 | extra_docker_args: -v ~/.ccache:/root/.ccache -e CCACHE_DIR=/root/.ccache 83 | command: idf.py --ccache build && idf.py merge-bin 84 | - name: Archive firmware 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: esp32c3-firmware 88 | path: build/HomeKey-ESP32.bin 89 | - name: Archive merged binary 90 | uses: actions/upload-artifact@v4 91 | with: 92 | name: esp32c3-firmware-merged 93 | path: build/merged-binary.bin 94 | esp32s3: 95 | runs-on: ubuntu-latest 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@v4 99 | with: 100 | fetch-depth: 0 101 | fetch-tags: true 102 | submodules: 'recursive' 103 | - uses: actions/cache@v4 104 | with: 105 | path: | 106 | ~/.ccache 107 | build 108 | managed_components 109 | sdkconfig 110 | key: ${{ runner.os }}-esp32s3-build 111 | - name: Cache Docker images. 112 | uses: ScribeMD/docker-cache@0.5.0 113 | with: 114 | key: docker-${{ runner.os }}-espidf 115 | - name: ESP32S3 Build 116 | uses: espressif/esp-idf-ci-action@v1 117 | with: 118 | esp_idf_version: v5.3.2 119 | target: esp32s3 120 | path: '.' 121 | extra_docker_args: -v ~/.ccache:/root/.ccache -e CCACHE_DIR=/root/.ccache 122 | command: idf.py --ccache build && idf.py merge-bin 123 | - name: Archive firmware 124 | uses: actions/upload-artifact@v4 125 | with: 126 | name: esp32s3-firmware 127 | path: build/HomeKey-ESP32.bin 128 | - name: Archive merged binary 129 | uses: actions/upload-artifact@v4 130 | with: 131 | name: esp32s3-firmware-merged 132 | path: build/merged-binary.bin 133 | littlefs: 134 | runs-on: ubuntu-latest 135 | steps: 136 | - name: Checkout 137 | uses: actions/checkout@v4 138 | with: 139 | sparse-checkout: 'data' 140 | - uses: actions/setup-python@v5 141 | with: 142 | python-version: '3.11' 143 | - name: Install LittleFS Tool 144 | run: pip install littlefs-python 145 | - name: Create LittleFS Image 146 | run: littlefs-python create $(pwd)/data littlefs.bin -v --fs-size=0x20000 --name-max=64 --block-size=4096 147 | - name: Archive LittleFS image 148 | uses: actions/upload-artifact@v4 149 | with: 150 | name: littlefs-binary 151 | path: littlefs.bin -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | lib 3 | .vscode/.browse.c_cpp.db* 4 | .vscode/c_cpp_properties.json 5 | .vscode/launch.json 6 | .vscode/ipch 7 | *.log 8 | .cache 9 | compile_commands.json 10 | src/config.h 11 | sdkconfig* 12 | dependencies.lock 13 | build 14 | managed_components -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "PN532"] 2 | path = components/PN532 3 | url = https://github.com/rednblkx/PN532.git 4 | branch = esp-idf 5 | [submodule "HK-HomeKit-Lib"] 6 | path = components/HK-HomeKit-Lib 7 | url = https://github.com/rednblkx/HK-HomeKit-Lib.git 8 | branch = esp-idf 9 | [submodule "AsyncTCP"] 10 | path = components/AsyncTCP 11 | url = https://github.com/me-no-dev/AsyncTCP.git 12 | [submodule "ESPAsyncWebServer"] 13 | path = components/ESPAsyncWebServer 14 | url = https://github.com/me-no-dev/ESPAsyncWebServer.git 15 | [submodule "HomeSpan"] 16 | path = components/HomeSpan/upstream 17 | url = https://github.com/HomeSpan/HomeSpan.git 18 | branch = release-2.1.1 19 | -------------------------------------------------------------------------------- /.nvim.lua: -------------------------------------------------------------------------------- 1 | require("lspconfig").clangd.setup({ 2 | capabilities = require("cmp_nvim_lsp").default_capabilities(vim.lsp.protocol.make_client_capabilities()), 3 | cmd = { 4 | os.getenv("HOME") .. "/esp-clang/bin/clangd", 5 | "--query-driver=" 6 | .. os.getenv("HOME") 7 | .. "/.espressif/tools/xtensa-esp-elf/**/xtensa-esp-elf/bin/xtensa-*-elf-*," 8 | .. os.getenv("HOME") 9 | .. "/tools/riscv32-esp-elf/**/riscv32-esp-elf/bin/riscv32-esp-elf-*", 10 | "--background-index", 11 | "--import-insertions", 12 | "--all-scopes-completion", 13 | }, 14 | on_attach = require("cmp_nvim_lsp").on_attach, 15 | lsp_flags = require("cmp_nvim_lsp").lsp_flags, 16 | filetypes = { "c", "cpp", "objc", "objcpp", "cuda", "proto" }, 17 | }) 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.intelliSenseEngine": "default", 3 | "files.associations": { 4 | "*.js": "javascript", 5 | "array": "cpp", 6 | "tuple": "cpp", 7 | "variant": "cpp", 8 | "deque": "cpp", 9 | "string": "cpp", 10 | "vector": "cpp", 11 | "cstdint": "cpp", 12 | "any": "cpp", 13 | "atomic": "cpp", 14 | "bit": "cpp", 15 | "*.tcc": "cpp", 16 | "bitset": "cpp", 17 | "cctype": "cpp", 18 | "charconv": "cpp", 19 | "chrono": "cpp", 20 | "clocale": "cpp", 21 | "cmath": "cpp", 22 | "codecvt": "cpp", 23 | "compare": "cpp", 24 | "complex": "cpp", 25 | "concepts": "cpp", 26 | "condition_variable": "cpp", 27 | "cstdarg": "cpp", 28 | "cstddef": "cpp", 29 | "cstdio": "cpp", 30 | "cstdlib": "cpp", 31 | "cstring": "cpp", 32 | "ctime": "cpp", 33 | "cwchar": "cpp", 34 | "cwctype": "cpp", 35 | "forward_list": "cpp", 36 | "list": "cpp", 37 | "map": "cpp", 38 | "set": "cpp", 39 | "unordered_map": "cpp", 40 | "unordered_set": "cpp", 41 | "exception": "cpp", 42 | "algorithm": "cpp", 43 | "functional": "cpp", 44 | "iterator": "cpp", 45 | "memory": "cpp", 46 | "memory_resource": "cpp", 47 | "netfwd": "cpp", 48 | "numeric": "cpp", 49 | "optional": "cpp", 50 | "random": "cpp", 51 | "ratio": "cpp", 52 | "regex": "cpp", 53 | "string_view": "cpp", 54 | "system_error": "cpp", 55 | "type_traits": "cpp", 56 | "utility": "cpp", 57 | "format": "cpp", 58 | "fstream": "cpp", 59 | "future": "cpp", 60 | "initializer_list": "cpp", 61 | "iomanip": "cpp", 62 | "iosfwd": "cpp", 63 | "iostream": "cpp", 64 | "istream": "cpp", 65 | "limits": "cpp", 66 | "mutex": "cpp", 67 | "new": "cpp", 68 | "numbers": "cpp", 69 | "ostream": "cpp", 70 | "ranges": "cpp", 71 | "semaphore": "cpp", 72 | "shared_mutex": "cpp", 73 | "span": "cpp", 74 | "sstream": "cpp", 75 | "stdexcept": "cpp", 76 | "stop_token": "cpp", 77 | "streambuf": "cpp", 78 | "thread": "cpp", 79 | "cinttypes": "cpp", 80 | "typeinfo": "cpp", 81 | "valarray": "cpp" 82 | }, 83 | "idf.flashType": "UART", 84 | "idf.port": "/dev/ttyACM0", 85 | "idf.openOcdConfigs": [ 86 | "board/esp32s3-builtin.cfg" 87 | ], 88 | "clangd.path": "${userHome}/esp-clang/bin/clangd", 89 | "clangd.arguments": [ 90 | "--query-driver=${userHome}/.espressif/tools/xtensa-esp-elf/**/xtensa-esp-elf/bin/xtensa-*-elf-*,${userHome}/tools/riscv32-esp-elf/**/riscv32-esp-elf/bin/riscv32-esp-elf-*", 91 | "--background-index", 92 | "--import-insertions", 93 | "--all-scopes-completion" 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | // Folder-specific settings 2 | // 3 | // For a full list of overridable settings, and general information on folder-specific settings, 4 | // see the documentation: https://zed.dev/docs/configuring-zed#settings-files 5 | { 6 | "load_direnv": "direct", 7 | "lsp": { 8 | "clangd": { 9 | "binary": { 10 | "arguments": [ 11 | "--query-driver=/home/**/.espressif/tools/xtensa-esp-elf/**/xtensa-esp-elf/bin/xtensa-*-elf-*,/home/**/.espressif/tools/riscv32-esp-elf/**/riscv32-esp-elf/bin/riscv32-esp-elf-*" 12 | ] 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 3 | project(HomeKey-ESP32) 4 | idf_build_get_property(IDF_TARGET IDF_TARGET) 5 | if(NOT IDF_TARGET STREQUAL "esp32" AND CONFIG_ARDUINO_USE_USB_JTAG STREQUAL "y") 6 | idf_build_set_property(COMPILE_OPTIONS "-DARDUINO_USB_CDC_ON_BOOT" APPEND) 7 | idf_build_set_property(COMPILE_OPTIONS "-DARDUINO_USB_MODE" APPEND) 8 | endif() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 rednblkx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![homekey-logo-white](https://github.com/user-attachments/assets/fc93a70a-ef1e-4390-9067-6fafb255e5ac) 2 | 3 | # HomeKey-ESP32 [![Discord](https://badgen.net/discord/members/VWpZ5YyUcm?icon=discord)](https://discord.com/invite/VWpZ5YyUcm) [![CI](https://github.com/rednblkx/HomeKey-ESP32/actions/workflows/esp32.yml/badge.svg?branch=main)](https://github.com/rednblkx/HomeKey-ESP32/actions/workflows/esp32.yml) 4 | 5 | ### HomeKey functionality for the rest of us. 6 | 7 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L3L2UCY8N) 8 | 9 | ## Overview 10 | 11 | This project aims to provide the Apple HomeKey functionality with just an ESP32 and PN532 NFC Module. Sole purpose of the project is to provide the HomeKey functionality and other NFC functionalities such as MIfare Authentication or others are out of scope. 12 | 13 | - It integrates with HomeAssistant's Tags which makes it easier to create automations based on a person(issuer) or device(endpoint). 14 | - The internal state is published and controlled via MQTT through user-defined topics 15 | - Any NFC Target that's not identified as homekey will skip the flow and publish the UID, ATQA and SAK on the same MQTT topic as HomeKey with the `"homekey"` field set to `false` 16 | - Code is not ready for battery-powered applications 17 | - Designed for a board with an ESP32 chip and 4MB Flash size 18 | 19 | Goal of the project is to make it possible to add the homekey functionality to locks that don't support it or to anything for that matter :) 20 | 21 | For more advanced functionality, you might also be interested in [HAP-ESPHome](https://github.com/rednblkx/HAP-ESPHome) which attempts to integrate HomeKit (and HomeKey) into ESPHome for ultimate automations. 22 | 23 | ## Usage 24 | 25 | Visit the [wiki](https://github.com/rednblkx/HomeKey-ESP32/wiki) for documentation on the project 26 | 27 | ## Disclaimer 28 | 29 | Use this at your own risk, i'm not a cryptographic expert, just a hobbyist. Keep in mind that the HomeKey was implemented through reverse-engineering as indicated above so it might be lacking stuff from Apple's specification to which us private individuals do not have access. 30 | 31 | While functional as it is now, the project should still be considered as a **work in progress** so expect breaking changes. 32 | 33 | ## Contributing & Support 34 | 35 | All contributions to the repository are welcomed, if you think you can bring an improvement into the project, feel free to fork the repository and submit your pull requests. 36 | 37 | If you have a suggestion or are in need of assistance, you can open an issue. Additionally, you can join the Discord server at https://discord.com/invite/VWpZ5YyUcm 38 | 39 | If you like the project, please consider giving it a star ⭐ to show the appreciation for it and for others to know this repository is worth something. 40 | 41 | ## Credits 42 | 43 | - [@kormax](https://github.com/kormax) for reverse-engineering the Homekey [NFC Protocol](https://github.com/kormax/apple-home-key) and publishing a [PoC](https://github.com/kormax/apple-home-key-reader) 44 | - [@kupa22](https://github.com/kupa22) for the [research](https://github.com/kupa22/apple-homekey) on the HAP side of things for Homekey 45 | - [HomeSpan](https://github.com/HomeSpan/HomeSpan) which is being used as the framework implementing the HomeKit accessory 46 | -------------------------------------------------------------------------------- /components/HomeSpan/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | FILE(GLOB_RECURSE app_sources ${CMAKE_CURRENT_SOURCE_DIR}/upstream/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/upstream/src/src/extras/*.cpp) 2 | 3 | idf_component_register(SRCS ${app_sources} 4 | INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/upstream/src ${CMAKE_CURRENT_SOURCE_DIR}/upstream/src/src/extras 5 | REQUIRES arduino-esp32 libsodium app_update nvs_flash) 6 | component_compile_options(-Wno-error=format= -Wno-format) -------------------------------------------------------------------------------- /data/assets/ap-icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednblkx/HomeKey-ESP32/72ed26af054a0e91effd581f4d5503574e531090/data/assets/ap-icon.webp -------------------------------------------------------------------------------- /data/assets/favicon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednblkx/HomeKey-ESP32/72ed26af054a0e91effd581f4d5503574e531090/data/assets/favicon.webp -------------------------------------------------------------------------------- /data/assets/hk-finish-0.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednblkx/HomeKey-ESP32/72ed26af054a0e91effd581f4d5503574e531090/data/assets/hk-finish-0.webp -------------------------------------------------------------------------------- /data/assets/hk-finish-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednblkx/HomeKey-ESP32/72ed26af054a0e91effd581f4d5503574e531090/data/assets/hk-finish-1.webp -------------------------------------------------------------------------------- /data/assets/hk-finish-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednblkx/HomeKey-ESP32/72ed26af054a0e91effd581f4d5503574e531090/data/assets/hk-finish-2.webp -------------------------------------------------------------------------------- /data/assets/hk-finish-3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednblkx/HomeKey-ESP32/72ed26af054a0e91effd581f4d5503574e531090/data/assets/hk-finish-3.webp -------------------------------------------------------------------------------- /data/assets/logo-white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednblkx/HomeKey-ESP32/72ed26af054a0e91effd581f4d5503574e531090/data/assets/logo-white.webp -------------------------------------------------------------------------------- /data/assets/misc.css: -------------------------------------------------------------------------------- 1 | :root { 2 | background-color: #353030; 3 | background-image: radial-gradient(rgba(255, 255, 255, 0.2) 0.05rem, transparent 8%); 4 | background-size: 2vh 2.2vh; 5 | } 6 | 7 | fieldset { 8 | border-color: black; 9 | } 10 | 11 | h2 { 12 | background: repeating-linear-gradient( 135deg, transparent, transparent 9px, #8e8271 10px, #8e8271 10px ); 13 | margin: 0!important; 14 | padding: 1rem!important; 15 | margin-bottom: 1rem!important; 16 | margin-top: 1rem!important; 17 | } 18 | 19 | h5 { 20 | color: rgb(156, 147, 134)!important; 21 | } 22 | 23 | p, 24 | h5, 25 | h3, 26 | h4, 27 | h2, 28 | h1, 29 | label, 30 | li, 31 | ul, 32 | legend { 33 | color: white; 34 | } 35 | 36 | * { 37 | border-color: #8e8271; 38 | } 39 | 40 | .destructive-btn { 41 | background-color: hsl(0 62.8% 30.6%/1); 42 | color: white; 43 | } 44 | 45 | .selected-btn { 46 | opacity: .5; 47 | } 48 | 49 | button { 50 | padding: .5rem .8rem .5rem .8rem; 51 | box-sizing: border-box; 52 | border: 0 solid #e5e7eb; 53 | color: hsl(240 5.9% 10%); 54 | background-color: hsl(0 0% 98%); 55 | border-radius: calc(0.5rem - 2px); 56 | white-space: nowrap; 57 | font-weight: 500; 58 | font-size: .875rem; 59 | line-height: 1.25rem; 60 | display: inline-flex; 61 | background-image: none; 62 | text-transform: none; 63 | border-color: hsl(240 3.7% 15.9%) !important; 64 | } 65 | 66 | .tabs-list { 67 | display: flex; 68 | justify-content: space-around; 69 | overflow: auto; 70 | } 71 | 72 | input[type="number"]{ 73 | max-width: 5rem; 74 | } 75 | 76 | .tabs-container { 77 | margin-bottom: .5rem; 78 | } 79 | 80 | div[class$="-selected-body"] { 81 | display: flex; 82 | flex-direction: column; 83 | padding-inline: .5rem; 84 | padding-block: .5rem; 85 | gap: 16px; 86 | } 87 | 88 | [class$="-selected-tab"] { 89 | border-bottom: 2px #8e8271 solid; 90 | background: linear-gradient(180deg, rgba(174,152,118,0.05) 0%, rgba(150,134,110,0.15) 50%, rgba(142, 130, 113, 0.35) 100%); 91 | border-radius: .2rem .2rem 0 0; 92 | box-shadow: 0 0 10px rgba(0,0,0,0.1), 0 0 20px rgba(0,0,0,0.1), 0 0 40px rgba(0,0,0,0.1), 0 0 60px rgba(0,0,0,0.1); 93 | font-weight: bold; 94 | color: white!important; 95 | } 96 | 97 | .tab-btn { 98 | margin: 0; 99 | width: fit-content; 100 | padding: 0.5rem; 101 | cursor: pointer; 102 | color: gray; 103 | } 104 | 105 | div[class$="-hidden-body"] { 106 | display: none; 107 | } 108 | 109 | .flex-col-lg { 110 | display: flex; 111 | flex-direction: column; 112 | gap: 16px; 113 | } 114 | 115 | .flex-row-lg { 116 | display: flex; 117 | flex-direction: row; 118 | gap: 16px; 119 | } 120 | 121 | .input-group { 122 | display: flex; 123 | justify-content: space-between; 124 | } 125 | 126 | @media only screen and (max-width: 600px) { 127 | #top-bar { 128 | flex-direction: column; 129 | align-items: center; 130 | gap: 8px; 131 | } 132 | #top-btns { 133 | flex-wrap: wrap; 134 | justify-content: center; 135 | } 136 | #mqtt-broker, #mqtt-topics { 137 | width: auto; 138 | } 139 | .cards-container { 140 | flex-direction: column; 141 | } 142 | .nfc-triggers-selected-body { 143 | flex-wrap: wrap; 144 | } 145 | #component { 146 | max-width: 90dvw; 147 | } 148 | .flex-sm { 149 | display: flex; 150 | gap: inherit; 151 | flex-direction: inherit; 152 | gap: 16px; 153 | } 154 | .around-sm { 155 | justify-content: space-around; 156 | } 157 | .flex-center-sm { 158 | align-items: center; 159 | } 160 | .flex-end-sm { 161 | align-items: flex-end; 162 | } 163 | } 164 | 165 | @media only screen and (min-width: 800px) { 166 | button:hover { 167 | opacity: .9; 168 | } 169 | 170 | #restart-btn:hover { 171 | opacity: .8; 172 | } 173 | 174 | #restart-btn:active { 175 | background-color: #4040402e!important; 176 | } 177 | .selected-btn:hover { 178 | opacity: .7; 179 | } 180 | 181 | #mqtt-broker-con, #mqtt-topics-container { 182 | min-width: 20rem; 183 | max-width: 25rem; 184 | } 185 | .cards-container { 186 | align-items: flex-start; 187 | } 188 | #component { 189 | max-width: 65rem; 190 | } 191 | } 192 | 193 | @media only screen and (max-width: 1090px) { 194 | .flex-sm { 195 | display: flex; 196 | gap: inherit; 197 | flex-direction: inherit; 198 | gap: 16px; 199 | } 200 | .around-sm { 201 | justify-content: space-around; 202 | } 203 | .flex-center-sm { 204 | align-items: center; 205 | } 206 | .flex-col-sm { 207 | display: flex; 208 | flex-direction: column; 209 | gap: 16px; 210 | } 211 | } 212 | 213 | @media only screen and (min-width: 1090px) { 214 | .flex-center-lg { 215 | align-items: center; 216 | } 217 | } 218 | 219 | .card-content { 220 | display: flex; 221 | flex-direction: column; 222 | border: 1px #8e8271 solid; 223 | border-radius: 8px; 224 | padding: 1rem; 225 | box-shadow: 0px 1px 1px 0px; 226 | background-color: #2d1d1d; 227 | flex: 1; 228 | } 229 | 230 | select { 231 | max-width: fit-content; 232 | text-align: center; 233 | } 234 | 235 | #buttons-group { 236 | display: flex; 237 | justify-content: center; 238 | margin-top: 2rem; 239 | gap: 64px; 240 | } 241 | 242 | 243 | .loader { 244 | width: 100%; 245 | height: 4.8px; 246 | display: inline-block; 247 | position: relative; 248 | overflow: hidden; 249 | } 250 | .loader::after { 251 | content: ''; 252 | width: 96px; 253 | height: 4.8px; 254 | background: #FFF; 255 | position: absolute; 256 | top: 0; 257 | left: 0; 258 | box-sizing: border-box; 259 | animation: animloader 0.6s ease-in-out infinite alternate; 260 | } 261 | 262 | @keyframes animloader { 263 | 0% { 264 | left: 0; 265 | transform: translateX(-1%); 266 | } 267 | 100% { 268 | left: 100%; 269 | transform: translateX(-99%); 270 | } 271 | } -------------------------------------------------------------------------------- /data/assets/restart-R.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednblkx/HomeKey-ESP32/72ed26af054a0e91effd581f4d5503574e531090/data/assets/restart-R.webp -------------------------------------------------------------------------------- /data/assets/trashcan.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednblkx/HomeKey-ESP32/72ed26af054a0e91effd581f4d5503574e531090/data/assets/trashcan.webp -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | HK-ESP32 10 | 324 | 325 | 326 | 327 |
328 |
329 | 330 |
331 |

HomeKey-ESP32

332 |

WiFi RSSI:

333 |

version: %VERSION%

334 |
335 |
336 |
337 | 344 | 351 | 358 | 365 |
366 |
367 |
368 |
369 |
374 | 375 |
376 | 377 | 378 | 379 | -------------------------------------------------------------------------------- /data/routes/actions.html: -------------------------------------------------------------------------------- 1 |
2 |

Hardware Actions

3 |
Assigning 255 to any Pin field will disable the respective option
4 |
5 |
6 |

HomeKey Triggers

7 |
! Executed on successful/failed HomeKey authentications and on NFC Tag detection(treated as a failed event).
8 |
! Both Neopixel and Simple GPIO have a momentary state.
9 |
10 |
11 |

Neopixel

13 |

Simple GPIO

15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 | 35 |
36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 |
50 |
51 |

Auth Success Color

52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 |
65 |
66 |
67 | 68 |
69 |

Auth Failure Color

70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 | 82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | Auth Success 91 |
92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 |
102 |
103 | 104 | 108 |
109 |
110 |
111 |
112 | Auth Failure 113 |
114 |
115 |
116 | 117 | 118 |
119 |
120 | 121 | 122 |
123 |
124 |
125 | 126 | 130 |
131 |
132 |
133 |
134 | 2nd action(on success) 135 |
136 |
137 |
138 | 139 | 140 |
141 |
142 | 143 | 144 |
145 |
146 |
147 | 148 | 152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |

HomeKit Triggers

160 |
! Executed upon interaction in the Home app and optionally on successful HomeKey Authentication(enabled by default)
161 |
162 |
163 |

Simple GPIO

165 |

Dummy

167 |
168 | 169 |
170 |
171 |
172 |
! Simple GPIO follows the "Always Lock/Unlock on HomeKey" option
173 |
! Momentary state applies only if initial state is "LOCKED"
174 |
175 |
176 |
177 | 178 | 179 |
180 |
181 | 182 | 186 |
187 |
188 | 189 | 193 |
194 |
195 | 196 | 200 |
201 |
202 | 203 | 209 |
210 |
211 | 212 | 213 |
214 |
215 |
216 |
217 |
218 |
! Dummy follows the "Always Lock/Unlock on HomeKey" option
219 |
! Momentary state applies only if initial state is "LOCKED"
220 |
221 |
222 |
223 | 224 | 228 |
229 |
230 | 231 | 237 |
238 |
239 | 240 | 241 |
242 |
243 |
244 |
245 |
246 |
247 | 248 | 249 |
250 |
-------------------------------------------------------------------------------- /data/routes/hkinfo.html: -------------------------------------------------------------------------------- 1 |

HomeKey Info

2 | -------------------------------------------------------------------------------- /data/routes/misc.html: -------------------------------------------------------------------------------- 1 |

Miscellaneous

2 |
Changes in this section will reboot the device
3 |
4 |
5 |
6 |

General settings

7 |
8 |
9 |
10 |

HomeKit

11 |

HomeKey

12 |

PN532

13 |

HomeSpan

14 |

Ethernet

15 |
16 | 17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 33 |
34 |
35 | 36 | 40 |
41 |
42 | 43 | 47 |
48 |
49 | 50 | 51 |
52 |
53 |
54 |
55 |
56 | Alt action Initiator Button 57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 |
72 |
73 | HomeKey Card Finish: 74 |
75 |
77 |
78 |
79 |
80 | 81 | 84 |
85 |
86 | 87 | 90 |
91 |
92 | 93 | 96 |
97 |
98 | 99 | 102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | 112 | 114 |
115 |
116 | 117 | 119 |
120 |
121 | 122 | 124 |
125 |
126 | 127 | 129 |
130 |
131 |
132 |
133 | HomeSpan Documentation 134 |
135 | 136 | 138 |
139 |
140 | 141 | 143 |
144 |
145 | 146 | 147 |
148 |
149 |
150 |
151 | 152 | 156 |
157 |
158 | 159 | 162 |
163 |
164 | 165 | 167 |
168 |
169 |
170 | 171 | 173 |
174 |
175 | 176 | 178 |
179 |
180 | 181 | 183 |
184 |
185 | 186 | 188 |
189 |
190 | 191 | 193 |
194 |
195 | 196 | 198 |
199 |
200 | 201 | 203 |
204 |
205 |
206 |
207 | 208 | 210 |
211 |
212 | 213 | 215 |
216 |
217 | 218 | 220 |
221 |
222 | 223 | 225 |
226 |
227 | 228 | 234 |
235 |
236 |
237 |
238 |
239 |
240 |

WebUI

241 |
243 |
244 |
245 |

Authentication

246 |
247 | 248 |
249 |
250 |
251 | 252 | 256 |
257 |
258 | 259 | 261 |
262 |
263 | 264 | 266 |
267 |
268 |
269 |
270 |
271 | 272 |
273 | 276 | 280 |
281 |
-------------------------------------------------------------------------------- /data/routes/mqtt.html: -------------------------------------------------------------------------------- 1 |

MQTT Configuration

2 |
Changes in this section will reboot the device
3 |
4 |
5 |
6 |

Broker Connection

7 |
TCP - Without TLS
8 |
9 |
10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 | 41 | 45 |
46 |
47 |
48 |
49 |

MQTT Topics

50 |
51 |
52 |

Core Topics

53 |

Custom Topics

54 |
55 | 56 |
57 |
58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 | 69 |
70 |
71 | 72 | 73 |
74 |
75 | 76 | 77 |
78 |
79 | 80 | 81 |
82 |
83 | 84 | 85 |
86 |
87 | 88 | 89 |
90 |
91 | 92 | 93 |
94 |
95 |
96 |
97 |
98 |
99 | 100 | 104 |
105 |
106 | 107 | 108 |
109 |
110 | 111 | 112 |
113 |
114 |
115 |
116 | Custom Lock Actions 117 |
119 |
120 | 121 | 122 |
123 |
124 | 125 | 126 |
127 |
128 |
129 |
130 | Custom Lock States 131 |
133 |
134 | 135 | 136 |
137 |
138 | 139 | 140 |
141 |
142 | 143 | 144 |
145 |
146 | 147 | 148 |
149 |
150 | 151 | 152 |
153 |
154 | 155 | 156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | 165 | 167 |
168 |
-------------------------------------------------------------------------------- /main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register(SRCS "main.cpp" 2 | INCLUDE_DIRS "include" 3 | REQUIRES HomeSpan PN532 HK-HomeKit-Lib ESPAsyncWebServer mqtt libsodium) 4 | littlefs_create_partition_image(spiffs ../data FLASH_IN_PROJECT) -------------------------------------------------------------------------------- /main/Kconfig.projbuild: -------------------------------------------------------------------------------- 1 | menu "Arduino Console USB-JTAG" 2 | config ARDUINO_USE_USB_JTAG 3 | depends on !IDF_TARGET_ESP32 4 | bool "Output serial log via the USB/JTAG controller" 5 | default n 6 | endmenu -------------------------------------------------------------------------------- /main/idf_component.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | idf: 3 | version: '>=5.3.0' 4 | espressif/arduino-esp32: ^3.1.0~1 5 | joltwallet/littlefs: ^1.16.1 6 | -------------------------------------------------------------------------------- /main/include/NFC_SERV_CHARS.h: -------------------------------------------------------------------------------- 1 | CUSTOM_CHAR(ConfigurationState, 263, PR+EV, UINT16, 0, 0, 1, true) 2 | CUSTOM_CHAR(HardwareFinish, 26C, PR, TLV_ENC, NULL_TLV, NULL_TLV, NULL_TLV, true) 3 | CUSTOM_CHAR(NFCAccessControlPoint, 264, PR+PW+WR, TLV_ENC, NULL_TLV, NULL_TLV, NULL_TLV, true) 4 | CUSTOM_CHAR(NFCAccessSupportedConfiguration, 265, PR, TLV_ENC, NULL_TLV, NULL_TLV, NULL_TLV, true) 5 | CUSTOM_CHAR(LockControlPoint, 19, PW, TLV_ENC, NULL_TLV, NULL_TLV, NULL_TLV, true) 6 | 7 | namespace Service 8 | { 9 | struct LockManagement : SpanService 10 | { 11 | LockManagement() : SpanService{ "44","LockManagement",true } { 12 | req.push_back(&_CUSTOM_LockControlPoint); 13 | req.push_back(&hapChars.Version); 14 | } 15 | }; 16 | struct NFCAccess : SpanService 17 | { 18 | NFCAccess() : SpanService{ "266","NFCAccess",true } { 19 | req.push_back(&_CUSTOM_ConfigurationState); 20 | req.push_back(&_CUSTOM_NFCAccessControlPoint); 21 | req.push_back(&_CUSTOM_NFCAccessSupportedConfiguration); 22 | } 23 | }; 24 | } -------------------------------------------------------------------------------- /main/include/config.h: -------------------------------------------------------------------------------- 1 | enum HK_COLOR 2 | { 3 | TAN, 4 | GOLD, 5 | SILVER, 6 | BLACK 7 | }; 8 | 9 | enum lockStates 10 | { 11 | UNLOCKED, 12 | LOCKED, 13 | JAMMED, 14 | UNKNOWN, 15 | UNLOCKING, 16 | LOCKING 17 | }; 18 | 19 | enum customLockStates 20 | { 21 | C_LOCKED = 1, 22 | C_UNLOCKING = 2, 23 | C_UNLOCKED = 3, 24 | C_LOCKING = 4, 25 | C_JAMMED = 254, 26 | C_UNKNOWN = 255 27 | }; 28 | // Custom Lock Actions to be used in MQTT_CUSTOM_STATE_TOPIC 29 | enum customLockActions 30 | { 31 | UNLOCK = 1, 32 | LOCK = 2 33 | }; 34 | 35 | enum class gpioMomentaryStateStatus : uint8_t 36 | { 37 | M_DISABLED = 0, 38 | M_HOME = 1 << 0, 39 | M_HK = 1 << 1, 40 | M_HOME_HK = (uint8_t)(M_HOME | M_HK) 41 | }; 42 | 43 | // MQTT Broker Settings 44 | #define MQTT_HOST "" //IP adress of mqtt broker 45 | #define MQTT_PORT 1883 //Port of mqtt broker 46 | #define MQTT_CLIENTID "" //client-id to connect to mqtt broker 47 | #define MQTT_USERNAME "" //username to connect to mqtt broker 48 | #define MQTT_PASSWORD "" //password to connect to mqtt broker 49 | 50 | //MQTT Flags 51 | #define MQTT_CUSTOM_STATE_ENABLED 0 // Flag to enable the use of custom states and relevant MQTT Topics 52 | #define MQTT_DISCOVERY true //Enable or disable discovery for home assistant tags functionality, set to true to enable. 53 | 54 | // MQTT Topics 55 | #define MQTT_LWT_TOPIC "status" 56 | #define MQTT_CUSTOM_STATE_TOPIC "homekit/custom_state" // MQTT Topic for publishing custom lock state 57 | #define MQTT_CUSTOM_STATE_CTRL_TOPIC "homekit/set_custom_state" // MQTT Control Topic with custom lock state 58 | #define MQTT_AUTH_TOPIC "homekey/auth" // MQTT Topic for publishing HomeKey authentication data or RFID UID 59 | #define MQTT_SET_STATE_TOPIC "homekit/set_state" // MQTT Control Topic for the HomeKit lock state (current and target) 60 | #define MQTT_SET_TARGET_STATE_TOPIC "homekit/set_target_state" // MQTT Control Topic for the HomeKit lock target state 61 | #define MQTT_SET_CURRENT_STATE_TOPIC "homekit/set_current_state" // MQTT Control Topic for the HomeKit lock current state 62 | #define MQTT_STATE_TOPIC "homekit/state" // MQTT Topic for publishing the HomeKit lock target state 63 | #define MQTT_PROX_BAT_TOPIC "homekit/set_battery_lvl" // MQTT Topic for publishing the HomeKit lock target state 64 | #define MQTT_HK_ALT_ACTION_TOPIC "alt_action" // MQTT Topic for publishing the Alt Action 65 | 66 | // Miscellaneous 67 | #define HOMEKEY_COLOR TAN 68 | #define SETUP_CODE "46637726" // HomeKit Setup Code (only for reference, has to be changed during WiFi Configuration or from WebUI) 69 | #define OTA_PWD "homespan-ota" //custom password for ota 70 | #define DEVICE_NAME "HK" //Device name 71 | #define HOMEKEY_ALWAYS_UNLOCK 0 // Flag indicating if a successful Homekey authentication should always set and publish the unlock state 72 | #define HOMEKEY_ALWAYS_LOCK 0 // Flag indicating if a successful Homekey authentication should always set and publish the lock state 73 | #define HS_STATUS_LED 255 // HomeSpan Status LED GPIO pin 74 | #define HS_PIN 255 // GPIO Pin for a Configuration Mode button (more info on https://github.com/HomeSpan/HomeSpan/blob/master/docs/UserGuide.md#device-configuration-mode) 75 | 76 | // Actions 77 | #define NFC_NEOPIXEL_PIN 255 // GPIO Pin used for NeoPixel 78 | #define NEOPIXEL_SUCCESS_R 0 // Color value for Red - Success HK Auth 79 | #define NEOPIXEL_SUCCESS_G 255 // Color value for Green - Success HK Auth 80 | #define NEOPIXEL_SUCCESS_B 0 // Color value for Blue - Success HK Auth 81 | #define NEOPIXEL_FAIL_R 255 // Color value for Red - Fail HK Auth 82 | #define NEOPIXEL_FAIL_G 0 // Color value for Green - Fail HK Auth 83 | #define NEOPIXEL_FAIL_B 0 // Color value for Blue - Fail HK Auth 84 | #define NEOPIXEL_SUCCESS_TIME 1000 // GPIO Delay time in ms - Success HK Auth 85 | #define NEOPIXEL_FAIL_TIME 1000 // GPIO Delay time in ms - Success HK Auth 86 | #define NFC_SUCCESS_PIN 255 // GPIO Pin pulled HIGH or LOW (see NFC_SUCCESS_HL) on success HK Auth 87 | #define NFC_SUCCESS_HL HIGH // Flag to define if NFC_SUCCESS_PIN should be held High or Low 88 | #define NFC_SUCCESS_TIME 1000 // How long should NFC_SUCCESS_PIN be held High or Low 89 | #define NFC_FAIL_PIN 255 // GPIO Pin pulled HIGH or LOW (see NFC_SUCCESS_HL) on failed HK Auth 90 | #define NFC_FAIL_HL HIGH // Flag to define if NFC_FAIL_PIN should be held High or Low 91 | #define NFC_FAIL_TIME 1000 // How long should NFC_FAIL_PIN be held High or Low 92 | #define GPIO_ACTION_PIN 255 93 | #define GPIO_ACTION_LOCK_STATE LOW 94 | #define GPIO_ACTION_UNLOCK_STATE HIGH 95 | #define GPIO_ACTION_MOMENTARY_STATE static_cast(gpioMomentaryStateStatus::M_DISABLED) 96 | #define GPIO_ACTION_MOMENTARY_TIMEOUT 5000 97 | #define GPIO_HK_ALT_ACTION_INIT_PIN 255 98 | #define GPIO_HK_ALT_ACTION_INIT_TIMEOUT 5000 99 | #define GPIO_HK_ALT_ACTION_INIT_LED_PIN 255 100 | #define GPIO_HK_ALT_ACTION_PIN 255 101 | #define GPIO_HK_ALT_ACTION_TIMEOUT 5000 102 | #define GPIO_HK_ALT_ACTION_GPIO_STATE HIGH 103 | 104 | // WebUI 105 | #define WEB_AUTH_ENABLED false 106 | #define WEB_AUTH_USERNAME "admin" 107 | #define WEB_AUTH_PASSWORD "password" -------------------------------------------------------------------------------- /main/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #define JSON_NOEXCEPTION 1 4 | #include 5 | #include 6 | #include "HAP.h" 7 | #include "hkAuthContext.h" 8 | #include "HomeKey.h" 9 | #include "array" 10 | #include "logging.h" 11 | #include "HomeSpan.h" 12 | #include "PN532_SPI.h" 13 | #include "PN532.h" 14 | #include "chrono" 15 | #include "ESPAsyncWebServer.h" 16 | #include "LittleFS.h" 17 | #include "HK_HomeKit.h" 18 | #include "config.h" 19 | #include "mqtt_client.h" 20 | #include "esp_app_desc.h" 21 | #include "pins_arduino.h" 22 | #include "NFC_SERV_CHARS.h" 23 | #include 24 | #include 25 | 26 | const char* TAG = "MAIN"; 27 | 28 | AsyncWebServer webServer(80); 29 | PN532_SPI *pn532spi; 30 | PN532 *nfc; 31 | QueueHandle_t gpio_led_handle = nullptr; 32 | QueueHandle_t neopixel_handle = nullptr; 33 | QueueHandle_t gpio_lock_handle = nullptr; 34 | TaskHandle_t gpio_led_task_handle = nullptr; 35 | TaskHandle_t neopixel_task_handle = nullptr; 36 | TaskHandle_t gpio_lock_task_handle = nullptr; 37 | TaskHandle_t alt_action_task_handle = nullptr; 38 | TaskHandle_t nfc_reconnect_task = nullptr; 39 | TaskHandle_t nfc_poll_task = nullptr; 40 | 41 | nvs_handle savedData; 42 | readerData_t readerData; 43 | uint8_t ecpData[18] = { 0x6A, 0x2, 0xCB, 0x2, 0x6, 0x2, 0x11, 0x0 }; 44 | const std::array, 4> hk_color_vals = { {{0x01,0x04,0xce,0xd5,0xda,0x00}, {0x01,0x04,0xaa,0xd6,0xec,0x00}, {0x01,0x04,0xe3,0xe3,0xe3,0x00}, {0x01,0x04,0x00,0x00,0x00,0x00}} }; 45 | const std::array pixelTypeMap = { "RGB", "RBG", "BRG", "BGR", "GBR", "GRB" }; 46 | struct gpioLockAction 47 | { 48 | enum 49 | { 50 | HOMEKIT = 1, 51 | HOMEKEY = 2, 52 | OTHER = 3 53 | }; 54 | uint8_t source; 55 | uint8_t action; 56 | }; 57 | 58 | std::string platform_create_id_string(void) { 59 | uint8_t mac[6]; 60 | char id_string[13]; 61 | esp_read_mac(mac, ESP_MAC_BT); 62 | sprintf(id_string, "ESP32_%02x%02X%02X", mac[3], mac[4], mac[5]); 63 | return std::string(id_string); 64 | } 65 | 66 | struct eth_chip_desc_t { 67 | std::string name; 68 | bool emac; 69 | eth_phy_type_t phy_type; 70 | NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(eth_chip_desc_t, name, emac, phy_type) 71 | }; 72 | 73 | struct eth_board_presets_t { 74 | std::string name; 75 | eth_chip_desc_t ethChip; 76 | #if CONFIG_ETH_USE_ESP32_EMAC 77 | struct rmii_conf_t { 78 | int32_t phy_addr = 1; 79 | uint8_t pin_mcd = 23; 80 | uint8_t pin_mdio = 18; 81 | int8_t pin_power = -1; 82 | eth_clock_mode_t pin_rmii_clock = ETH_CLOCK_GPIO0_IN; 83 | NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(rmii_conf_t, phy_addr, pin_mcd, pin_mdio, pin_power, pin_rmii_clock) 84 | } rmii_conf; 85 | #endif 86 | struct spi_conf_t { 87 | uint8_t spi_freq_mhz = 20; 88 | uint8_t pin_cs = SS; 89 | uint8_t pin_irq = A4; 90 | uint8_t pin_rst = A5; 91 | uint8_t pin_sck = SCK; 92 | uint8_t pin_miso = MISO; 93 | uint8_t pin_mosi = MOSI; 94 | NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(spi_conf_t, spi_freq_mhz, pin_cs, pin_irq, pin_rst, pin_sck, pin_miso, pin_mosi) 95 | } spi_conf; 96 | friend void to_json(nlohmann ::json &nlohmann_json_j, const eth_board_presets_t &nlohmann_json_t) { 97 | nlohmann_json_j["name"] = nlohmann_json_t.name; 98 | nlohmann_json_j["ethChip"] = nlohmann_json_t.ethChip; 99 | if (nlohmann_json_t.ethChip.emac) { 100 | #if CONFIG_ETH_USE_ESP32_EMAC 101 | nlohmann_json_j["rmii_conf"] = nlohmann_json_t.rmii_conf; 102 | #endif 103 | } else { 104 | nlohmann_json_j["spi_conf"] = nlohmann_json_t.spi_conf; 105 | } 106 | } 107 | }; 108 | 109 | namespace eth_config_ns { 110 | std::map supportedChips = { 111 | #if CONFIG_ETH_USE_ESP32_EMAC 112 | {ETH_PHY_LAN8720, eth_chip_desc_t{"LAN8720", true, ETH_PHY_LAN8720}}, 113 | {ETH_PHY_TLK110, eth_chip_desc_t{"TLK110", true, ETH_PHY_TLK110}}, 114 | {ETH_PHY_RTL8201, eth_chip_desc_t{"RTL8201", true, ETH_PHY_RTL8201}}, 115 | {ETH_PHY_DP83848, eth_chip_desc_t{"DP83848", true, ETH_PHY_DP83848}}, 116 | {ETH_PHY_KSZ8041, eth_chip_desc_t{"KSZ8041", true, ETH_PHY_KSZ8041}}, 117 | {ETH_PHY_KSZ8081, eth_chip_desc_t{"KSZ8081", true, ETH_PHY_KSZ8081}}, 118 | #endif 119 | #if CONFIG_ETH_SPI_ETHERNET_DM9051 120 | {ETH_PHY_DM9051, eth_chip_desc_t{"DM9051", false, ETH_PHY_DM9051}}, 121 | #endif 122 | #if CONFIG_ETH_SPI_ETHERNET_W5500 123 | {ETH_PHY_W5500, eth_chip_desc_t{"W5500", false, ETH_PHY_W5500}}, 124 | #endif 125 | #if CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL 126 | {ETH_PHY_KSZ8851, eth_chip_desc_t{"KSZ8851", false, ETH_PHY_KSZ8851}}, 127 | #endif 128 | }; 129 | std::vector boardPresets = { 130 | eth_board_presets_t{.name = "Generic W5500", 131 | .ethChip = supportedChips[ETH_PHY_W5500], 132 | .spi_conf{20, SS, A3, A4, SCK, MISO, MOSI}}, 133 | eth_board_presets_t{.name = "T-ETH-Lite-ESP32S3", 134 | .ethChip = supportedChips[ETH_PHY_W5500], 135 | .spi_conf{20, 9, 13, 14, 10, 11, 12}}, 136 | #if CONFIG_ETH_USE_ESP32_EMAC 137 | eth_board_presets_t{.name = "WT32-ETH01", 138 | .ethChip = supportedChips[ETH_PHY_LAN8720], 139 | .rmii_conf{1, 23, 18, 16, ETH_CLOCK_GPIO0_IN}}, 140 | eth_board_presets_t{.name = "Olimex ESP32-POE", 141 | .ethChip = supportedChips[ETH_PHY_LAN8720], 142 | .rmii_conf{0, 23, 18, 12, ETH_CLOCK_GPIO17_OUT}}, 143 | eth_board_presets_t{.name = "EST-PoE-32", 144 | .ethChip = supportedChips[ETH_PHY_LAN8720], 145 | .rmii_conf{0, 23, 18, 12, ETH_CLOCK_GPIO17_OUT}}, 146 | eth_board_presets_t{.name = "T-ETH-Lite-ESP32", 147 | .ethChip = supportedChips[ETH_PHY_RTL8201], 148 | .rmii_conf{0, 23, 18, 12, ETH_CLOCK_GPIO0_IN}} 149 | #endif 150 | }; 151 | }; 152 | 153 | namespace espConfig 154 | { 155 | struct mqttConfig_t 156 | { 157 | mqttConfig_t() { 158 | std::string id = platform_create_id_string(); 159 | mqttClientId = id; 160 | lwtTopic.append(id).append("/" MQTT_LWT_TOPIC); 161 | hkTopic.append(id).append("/" MQTT_AUTH_TOPIC); 162 | lockStateTopic.append(id).append("/" MQTT_STATE_TOPIC); 163 | lockStateCmd.append(id).append("/" MQTT_SET_STATE_TOPIC); 164 | lockCStateCmd.append(id).append("/" MQTT_SET_CURRENT_STATE_TOPIC); 165 | lockTStateCmd.append(id).append("/" MQTT_SET_TARGET_STATE_TOPIC); 166 | lockCustomStateTopic.append(id).append("/" MQTT_CUSTOM_STATE_TOPIC); 167 | lockCustomStateCmd.append(id).append("/" MQTT_CUSTOM_STATE_CTRL_TOPIC); 168 | btrLvlCmdTopic.append(id).append("/" MQTT_PROX_BAT_TOPIC); 169 | hkAltActionTopic.append(id).append("/" MQTT_HK_ALT_ACTION_TOPIC); 170 | } 171 | /* MQTT Broker */ 172 | std::string mqttBroker = MQTT_HOST; 173 | uint16_t mqttPort = MQTT_PORT; 174 | std::string mqttUsername = MQTT_USERNAME; 175 | std::string mqttPassword = MQTT_PASSWORD; 176 | std::string mqttClientId; 177 | /* MQTT Topics */ 178 | std::string lwtTopic; 179 | std::string hkTopic; 180 | std::string lockStateTopic; 181 | std::string lockStateCmd; 182 | std::string lockCStateCmd; 183 | std::string lockTStateCmd; 184 | std::string btrLvlCmdTopic; 185 | std::string hkAltActionTopic; 186 | /* MQTT Custom State */ 187 | std::string lockCustomStateTopic; 188 | std::string lockCustomStateCmd; 189 | /* Flags */ 190 | bool lockEnableCustomState = MQTT_CUSTOM_STATE_ENABLED; 191 | bool hassMqttDiscoveryEnabled = MQTT_DISCOVERY; 192 | bool nfcTagNoPublish = false; 193 | std::map customLockStates = { {"C_LOCKED", C_LOCKED}, {"C_UNLOCKING", C_UNLOCKING}, {"C_UNLOCKED", C_UNLOCKED}, {"C_LOCKING", C_LOCKING}, {"C_JAMMED", C_JAMMED}, {"C_UNKNOWN", C_UNKNOWN} }; 194 | std::map customLockActions = { {"UNLOCK", UNLOCK}, {"LOCK", LOCK} }; 195 | NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(espConfig::mqttConfig_t, mqttBroker, mqttPort, mqttUsername, mqttPassword, mqttClientId, lwtTopic, hkTopic, lockStateTopic, 196 | lockStateCmd, lockCStateCmd, lockTStateCmd, lockCustomStateTopic, lockCustomStateCmd, lockEnableCustomState, hassMqttDiscoveryEnabled, customLockStates, customLockActions, 197 | nfcTagNoPublish, btrLvlCmdTopic, hkAltActionTopic) 198 | } mqttData; 199 | 200 | struct misc_config_t 201 | { 202 | enum colorMap 203 | { 204 | R, 205 | G, 206 | B 207 | }; 208 | std::string deviceName = DEVICE_NAME; 209 | std::string otaPasswd = OTA_PWD; 210 | uint8_t hk_key_color = HOMEKEY_COLOR; 211 | std::string setupCode = SETUP_CODE; 212 | bool lockAlwaysUnlock = HOMEKEY_ALWAYS_UNLOCK; 213 | bool lockAlwaysLock = HOMEKEY_ALWAYS_LOCK; 214 | uint8_t controlPin = HS_PIN; 215 | uint8_t hsStatusPin = HS_STATUS_LED; 216 | uint8_t nfcNeopixelPin = NFC_NEOPIXEL_PIN; 217 | uint8_t neoPixelType = 5; 218 | std::map neopixelSuccessColor = { {R, NEOPIXEL_SUCCESS_R}, {G, NEOPIXEL_SUCCESS_G}, {B, NEOPIXEL_SUCCESS_B} }; 219 | std::map neopixelFailureColor = { {R, NEOPIXEL_FAIL_R}, {G, NEOPIXEL_FAIL_G}, {B, NEOPIXEL_FAIL_B} }; 220 | uint16_t neopixelSuccessTime = NEOPIXEL_SUCCESS_TIME; 221 | uint16_t neopixelFailTime = NEOPIXEL_FAIL_TIME; 222 | uint8_t nfcSuccessPin = NFC_SUCCESS_PIN; 223 | uint16_t nfcSuccessTime = NFC_SUCCESS_TIME; 224 | bool nfcSuccessHL = NFC_SUCCESS_HL; 225 | uint8_t nfcFailPin = NFC_FAIL_PIN; 226 | uint16_t nfcFailTime = NFC_FAIL_TIME; 227 | bool nfcFailHL = NFC_FAIL_HL; 228 | uint8_t gpioActionPin = GPIO_ACTION_PIN; 229 | bool gpioActionLockState = GPIO_ACTION_LOCK_STATE; 230 | bool gpioActionUnlockState = GPIO_ACTION_UNLOCK_STATE; 231 | uint8_t gpioActionMomentaryEnabled = GPIO_ACTION_MOMENTARY_STATE; 232 | bool hkGpioControlledState = true; 233 | uint16_t gpioActionMomentaryTimeout = GPIO_ACTION_MOMENTARY_TIMEOUT; 234 | bool webAuthEnabled = WEB_AUTH_ENABLED; 235 | std::string webUsername = WEB_AUTH_USERNAME; 236 | std::string webPassword = WEB_AUTH_PASSWORD; 237 | std::array nfcGpioPins{SS, SCK, MISO, MOSI}; 238 | uint8_t btrLowStatusThreshold = 10; 239 | bool proxBatEnabled = false; 240 | bool hkDumbSwitchMode = false; 241 | uint8_t hkAltActionInitPin = GPIO_HK_ALT_ACTION_INIT_PIN; 242 | uint8_t hkAltActionInitLedPin = GPIO_HK_ALT_ACTION_INIT_LED_PIN; 243 | uint16_t hkAltActionInitTimeout = GPIO_HK_ALT_ACTION_INIT_TIMEOUT; 244 | uint8_t hkAltActionPin = GPIO_HK_ALT_ACTION_PIN; 245 | uint16_t hkAltActionTimeout = GPIO_HK_ALT_ACTION_TIMEOUT; 246 | uint8_t hkAltActionGpioState = GPIO_HK_ALT_ACTION_GPIO_STATE; 247 | bool ethernetEnabled = false; 248 | uint8_t ethActivePreset = 255; // 255 for custom pins 249 | uint8_t ethPhyType = 0; 250 | #if CONFIG_ETH_USE_ESP32_EMAC 251 | std::array ethRmiiConfig = {0, -1, -1, -1, 0}; 252 | #endif 253 | std::array ethSpiConfig = {20, -1, -1, -1, -1, -1, -1}; 254 | NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( 255 | misc_config_t, deviceName, otaPasswd, hk_key_color, setupCode, 256 | lockAlwaysUnlock, lockAlwaysLock, controlPin, hsStatusPin, 257 | nfcSuccessPin, nfcSuccessTime, nfcNeopixelPin, neoPixelType, 258 | neopixelSuccessColor, neopixelFailureColor, neopixelSuccessTime, 259 | neopixelFailTime, nfcSuccessHL, nfcFailPin, nfcFailTime, nfcFailHL, 260 | gpioActionPin, gpioActionLockState, gpioActionUnlockState, 261 | gpioActionMomentaryEnabled, gpioActionMomentaryTimeout, webAuthEnabled, 262 | webUsername, webPassword, nfcGpioPins, btrLowStatusThreshold, 263 | proxBatEnabled, hkDumbSwitchMode, hkAltActionInitPin, 264 | hkAltActionInitLedPin, hkAltActionInitTimeout, hkAltActionPin, 265 | hkAltActionTimeout, hkAltActionGpioState, hkGpioControlledState, 266 | ethernetEnabled, ethActivePreset, ethPhyType, 267 | #if CONFIG_ETH_USE_ESP32_EMAC 268 | ethRmiiConfig, 269 | #endif 270 | ethSpiConfig 271 | ) 272 | } miscConfig; 273 | }; // namespace espConfig 274 | 275 | KeyFlow hkFlow = KeyFlow::kFlowFAST; 276 | bool hkAltActionActive = false; 277 | SpanCharacteristic* lockCurrentState; 278 | SpanCharacteristic* lockTargetState; 279 | SpanCharacteristic* statusLowBtr; 280 | SpanCharacteristic* btrLevel; 281 | esp_mqtt_client_handle_t client = nullptr; 282 | 283 | std::shared_ptr pixel; 284 | 285 | bool save_to_nvs() { 286 | std::vector serialized = nlohmann::json::to_msgpack(readerData); 287 | esp_err_t set_nvs = nvs_set_blob(savedData, "READERDATA", serialized.data(), serialized.size()); 288 | esp_err_t commit_nvs = nvs_commit(savedData); 289 | LOG(D, "NVS SET STATUS: %s", esp_err_to_name(set_nvs)); 290 | LOG(D, "NVS COMMIT STATUS: %s", esp_err_to_name(commit_nvs)); 291 | return !set_nvs && !commit_nvs; 292 | } 293 | 294 | struct PhysicalLockBattery : Service::BatteryService 295 | { 296 | PhysicalLockBattery() { 297 | LOG(I, "Configuring PhysicalLockBattery"); 298 | statusLowBtr = new Characteristic::StatusLowBattery(0, true); 299 | btrLevel = new Characteristic::BatteryLevel(100, true); 300 | } 301 | }; 302 | 303 | struct LockManagement : Service::LockManagement 304 | { 305 | SpanCharacteristic* lockControlPoint; 306 | SpanCharacteristic* version; 307 | const char* TAG = "LockManagement"; 308 | 309 | LockManagement() : Service::LockManagement() { 310 | 311 | LOG(I, "Configuring LockManagement"); // initialization message 312 | 313 | lockControlPoint = new Characteristic::LockControlPoint(); 314 | version = new Characteristic::Version(); 315 | 316 | } // end constructor 317 | 318 | }; // end LockManagement 319 | 320 | struct NFCAccessoryInformation : Service::AccessoryInformation 321 | { 322 | const char* TAG = "NFCAccessoryInformation"; 323 | 324 | NFCAccessoryInformation() : Service::AccessoryInformation() { 325 | 326 | LOG(I, "Configuring NFCAccessoryInformation"); // initialization message 327 | 328 | opt.push_back(&_CUSTOM_HardwareFinish); 329 | new Characteristic::Identify(); 330 | new Characteristic::Manufacturer("rednblkx"); 331 | new Characteristic::Model("HomeKey-ESP32"); 332 | new Characteristic::Name(DEVICE_NAME); 333 | const esp_app_desc_t* app_desc = esp_app_get_description(); 334 | std::string app_version = app_desc->version; 335 | uint8_t mac[6]; 336 | esp_read_mac(mac, ESP_MAC_BT); 337 | char macStr[9] = { 0 }; 338 | sprintf(macStr, "%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3]); 339 | std::string serialNumber = "HK-"; 340 | serialNumber.append(macStr); 341 | new Characteristic::SerialNumber(serialNumber.c_str()); 342 | new Characteristic::FirmwareRevision(app_version.c_str()); 343 | std::array decB64 = hk_color_vals[HK_COLOR(espConfig::miscConfig.hk_key_color)]; 344 | TLV8 hwfinish(NULL, 0); 345 | hwfinish.unpack(decB64.data(), decB64.size()); 346 | new Characteristic::HardwareFinish(hwfinish); 347 | 348 | } // end constructor 349 | }; 350 | 351 | // Function to calculate CRC16 352 | void crc16a(unsigned char* data, unsigned int size, unsigned char* result) { 353 | unsigned short w_crc = 0x6363; 354 | 355 | for (unsigned int i = 0; i < size; ++i) { 356 | unsigned char byte = data[i]; 357 | byte = (byte ^ (w_crc & 0x00FF)); 358 | byte = ((byte ^ (byte << 4)) & 0xFF); 359 | w_crc = ((w_crc >> 8) ^ (byte << 8) ^ (byte << 3) ^ (byte >> 4)) & 0xFFFF; 360 | } 361 | 362 | result[0] = static_cast(w_crc & 0xFF); 363 | result[1] = static_cast((w_crc >> 8) & 0xFF); 364 | } 365 | 366 | // Function to append CRC16 to data 367 | void with_crc16(unsigned char* data, unsigned int size, unsigned char* result) { 368 | crc16a(data, size, result); 369 | } 370 | 371 | void alt_action_task(void* arg) { 372 | uint8_t buttonState = 0; 373 | hkAltActionActive = false; 374 | LOG(I, "Starting Alt Action button task"); 375 | while (true) 376 | { 377 | buttonState = digitalRead(espConfig::miscConfig.hkAltActionInitPin); 378 | if (buttonState == HIGH) { 379 | LOG(D, "BUTTON HIGH"); 380 | hkAltActionActive = true; 381 | if(espConfig::miscConfig.hkAltActionInitLedPin != 255) { 382 | digitalWrite(espConfig::miscConfig.hkAltActionInitLedPin, HIGH); 383 | } 384 | vTaskDelay(espConfig::miscConfig.hkAltActionInitTimeout / portTICK_PERIOD_MS); 385 | if (espConfig::miscConfig.hkAltActionInitLedPin != 255) { 386 | digitalWrite(espConfig::miscConfig.hkAltActionInitLedPin, LOW); 387 | } 388 | LOG(D, "TIMEOUT"); 389 | hkAltActionActive = false; 390 | } 391 | vTaskDelay(100 / portTICK_PERIOD_MS); 392 | } 393 | vTaskDelete(NULL); 394 | } 395 | 396 | void gpio_task(void* arg) { 397 | gpioLockAction status; 398 | while (1) { 399 | if (gpio_lock_handle != nullptr) { 400 | status = {}; 401 | if (uxQueueMessagesWaiting(gpio_lock_handle) > 0) { 402 | xQueueReceive(gpio_lock_handle, &status, 0); 403 | LOG(D, "Got something in queue - source = %d action = %d", status.source, status.action); 404 | if (status.action == 0) { 405 | LOG(D, "%d - %d - %d -%d", espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionMomentaryEnabled, espConfig::miscConfig.lockAlwaysUnlock, espConfig::miscConfig.lockAlwaysLock); 406 | if (espConfig::miscConfig.lockAlwaysUnlock && status.source != gpioLockAction::HOMEKIT) { 407 | lockTargetState->setVal(lockStates::UNLOCKED); 408 | if(espConfig::miscConfig.gpioActionPin != 255){ 409 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionUnlockState); 410 | } 411 | lockCurrentState->setVal(lockStates::UNLOCKED); 412 | if (client != nullptr) { 413 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::UNLOCKED).c_str(), 1, 0, false); 414 | } else LOG(W, "MQTT Client not initialized, cannot publish message"); 415 | 416 | if (static_cast(espConfig::miscConfig.gpioActionMomentaryEnabled) & status.source) { 417 | delay(espConfig::miscConfig.gpioActionMomentaryTimeout); 418 | lockTargetState->setVal(lockStates::LOCKED); 419 | if(espConfig::miscConfig.gpioActionPin != 255){ 420 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionLockState); 421 | } 422 | lockCurrentState->setVal(lockStates::LOCKED); 423 | if (client != nullptr) { 424 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::LOCKED).c_str(), 1, 0, false); 425 | } else LOG(W, "MQTT Client not initialized, cannot publish message"); 426 | } 427 | } else if (espConfig::miscConfig.lockAlwaysLock && status.source != gpioLockAction::HOMEKIT) { 428 | lockTargetState->setVal(lockStates::LOCKED); 429 | if(espConfig::miscConfig.gpioActionPin != 255){ 430 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionLockState); 431 | } 432 | lockCurrentState->setVal(lockStates::LOCKED); 433 | if (client != nullptr) { 434 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::LOCKED).c_str(), 1, 0, false); 435 | } else LOG(W, "MQTT Client not initialized, cannot publish message"); 436 | } else { 437 | int currentState = lockCurrentState->getVal(); 438 | if (status.source != gpioLockAction::HOMEKIT) { 439 | lockTargetState->setVal(!currentState); 440 | } 441 | if(espConfig::miscConfig.gpioActionPin != 255){ 442 | digitalWrite(espConfig::miscConfig.gpioActionPin, currentState == lockStates::UNLOCKED ? espConfig::miscConfig.gpioActionLockState : espConfig::miscConfig.gpioActionUnlockState); 443 | } 444 | lockCurrentState->setVal(!currentState); 445 | if (client != nullptr) { 446 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockCurrentState->getNewVal()).c_str(), 1, 0, false); 447 | } else LOG(W, "MQTT Client not initialized, cannot publish message"); 448 | if ((static_cast(espConfig::miscConfig.gpioActionMomentaryEnabled) & status.source) && currentState == lockStates::LOCKED) { 449 | delay(espConfig::miscConfig.gpioActionMomentaryTimeout); 450 | lockTargetState->setVal(currentState); 451 | if(espConfig::miscConfig.gpioActionPin != 255){ 452 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionLockState); 453 | } 454 | lockCurrentState->setVal(currentState); 455 | if (client != nullptr) { 456 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockCurrentState->getNewVal()).c_str(), 1, 0, false); 457 | } else LOG(W, "MQTT Client not initialized, cannot publish message"); 458 | } 459 | } 460 | } else if (status.action == 2) { 461 | vTaskDelete(NULL); 462 | return; 463 | } 464 | } 465 | } 466 | vTaskDelay(100 / portTICK_PERIOD_MS); 467 | } 468 | } 469 | 470 | void neopixel_task(void* arg) { 471 | uint8_t status = 0; 472 | while (1) { 473 | if (neopixel_handle != nullptr) { 474 | status = 0; 475 | if (uxQueueMessagesWaiting(neopixel_handle) > 0) { 476 | xQueueReceive(neopixel_handle, &status, 0); 477 | LOG(D, "Got something in queue %d", status); 478 | switch (status) { 479 | case 0: 480 | if (espConfig::miscConfig.nfcNeopixelPin && espConfig::miscConfig.nfcNeopixelPin != 255) { 481 | LOG(D, "SUCCESS PIXEL %d:%d,%d,%d", espConfig::miscConfig.nfcNeopixelPin, espConfig::miscConfig.neopixelFailureColor[espConfig::misc_config_t::colorMap::R], espConfig::miscConfig.neopixelFailureColor[espConfig::misc_config_t::colorMap::G], espConfig::miscConfig.neopixelFailureColor[espConfig::misc_config_t::colorMap::B]); 482 | pixel->set(pixel->RGB(espConfig::miscConfig.neopixelFailureColor[espConfig::misc_config_t::colorMap::R], espConfig::miscConfig.neopixelFailureColor[espConfig::misc_config_t::colorMap::G], espConfig::miscConfig.neopixelFailureColor[espConfig::misc_config_t::colorMap::B])); 483 | delay(espConfig::miscConfig.neopixelFailTime); 484 | pixel->off(); 485 | } 486 | break; 487 | case 1: 488 | if (espConfig::miscConfig.nfcNeopixelPin && espConfig::miscConfig.nfcNeopixelPin != 255) { 489 | LOG(D, "FAIL PIXEL %d:%d,%d,%d", espConfig::miscConfig.nfcNeopixelPin, espConfig::miscConfig.neopixelSuccessColor[espConfig::misc_config_t::colorMap::R], espConfig::miscConfig.neopixelSuccessColor[espConfig::misc_config_t::colorMap::G], espConfig::miscConfig.neopixelSuccessColor[espConfig::misc_config_t::colorMap::B]); 490 | pixel->set(pixel->RGB(espConfig::miscConfig.neopixelSuccessColor[espConfig::misc_config_t::colorMap::R], espConfig::miscConfig.neopixelSuccessColor[espConfig::misc_config_t::colorMap::G], espConfig::miscConfig.neopixelSuccessColor[espConfig::misc_config_t::colorMap::B])); 491 | delay(espConfig::miscConfig.neopixelSuccessTime); 492 | pixel->off(); 493 | } 494 | break; 495 | default: 496 | vTaskDelete(NULL); 497 | return; 498 | break; 499 | } 500 | } 501 | } 502 | vTaskDelay(100 / portTICK_PERIOD_MS); 503 | } 504 | } 505 | void nfc_gpio_task(void* arg) { 506 | uint8_t status = 0; 507 | while (1) { 508 | if (gpio_led_handle != nullptr) { 509 | status = 0; 510 | if (uxQueueMessagesWaiting(gpio_led_handle) > 0) { 511 | xQueueReceive(gpio_led_handle, &status, 0); 512 | LOG(D, "Got something in queue %d", status); 513 | switch (status) { 514 | case 0: 515 | if (espConfig::miscConfig.nfcFailPin && espConfig::miscConfig.nfcFailPin != 255) { 516 | LOG(D, "FAIL LED %d:%d", espConfig::miscConfig.nfcFailPin, espConfig::miscConfig.nfcFailHL); 517 | digitalWrite(espConfig::miscConfig.nfcFailPin, espConfig::miscConfig.nfcFailHL); 518 | delay(espConfig::miscConfig.nfcFailTime); 519 | digitalWrite(espConfig::miscConfig.nfcFailPin, !espConfig::miscConfig.nfcFailHL); 520 | } 521 | break; 522 | case 1: 523 | if (espConfig::miscConfig.nfcSuccessPin && espConfig::miscConfig.nfcSuccessPin != 255) { 524 | LOG(D, "SUCCESS LED %d:%d", espConfig::miscConfig.nfcSuccessPin, espConfig::miscConfig.nfcSuccessHL); 525 | digitalWrite(espConfig::miscConfig.nfcSuccessPin, espConfig::miscConfig.nfcSuccessHL); 526 | delay(espConfig::miscConfig.nfcSuccessTime); 527 | digitalWrite(espConfig::miscConfig.nfcSuccessPin, !espConfig::miscConfig.nfcSuccessHL); 528 | } 529 | break; 530 | case 2: 531 | if(hkAltActionActive){ 532 | digitalWrite(espConfig::miscConfig.hkAltActionPin, espConfig::miscConfig.hkAltActionGpioState); 533 | delay(espConfig::miscConfig.hkAltActionTimeout); 534 | digitalWrite(espConfig::miscConfig.hkAltActionPin, !espConfig::miscConfig.hkAltActionGpioState); 535 | } 536 | break; 537 | default: 538 | LOG(I, "STOP"); 539 | vTaskDelete(NULL); 540 | return; 541 | break; 542 | } 543 | } 544 | } 545 | vTaskDelay(100 / portTICK_PERIOD_MS); 546 | } 547 | } 548 | 549 | struct LockMechanism : Service::LockMechanism 550 | { 551 | const char* TAG = "LockMechanism"; 552 | 553 | LockMechanism() : Service::LockMechanism() { 554 | LOG(I, "Configuring LockMechanism"); // initialization message 555 | lockCurrentState = new Characteristic::LockCurrentState(1, true); 556 | lockTargetState = new Characteristic::LockTargetState(1, true); 557 | memcpy(ecpData + 8, readerData.reader_gid.data(), readerData.reader_gid.size()); 558 | with_crc16(ecpData, 16, ecpData + 16); 559 | if (espConfig::miscConfig.gpioActionPin != 255) { 560 | if (lockCurrentState->getVal() == lockStates::LOCKED) { 561 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionLockState); 562 | } else if (lockCurrentState->getVal() == lockStates::UNLOCKED) { 563 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionUnlockState); 564 | } 565 | } 566 | } // end constructor 567 | 568 | boolean update() { 569 | int targetState = lockTargetState->getNewVal(); 570 | LOG(I, "New LockState=%d, Current LockState=%d", targetState, lockCurrentState->getVal()); 571 | if (espConfig::miscConfig.gpioActionPin != 255) { 572 | const gpioLockAction gpioAction{ .source = gpioLockAction::HOMEKIT, .action = 0 }; 573 | xQueueSend(gpio_lock_handle, &gpioAction, 0); 574 | } else if (espConfig::miscConfig.hkDumbSwitchMode) { 575 | const gpioLockAction gpioAction{ .source = gpioLockAction::HOMEKIT, .action = 0 }; 576 | xQueueSend(gpio_lock_handle, &gpioAction, 0); 577 | } 578 | int currentState = lockCurrentState->getNewVal(); 579 | if (client != nullptr) { 580 | if (espConfig::miscConfig.gpioActionPin == 255) { 581 | if (targetState != currentState) { 582 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), targetState == lockStates::UNLOCKED ? std::to_string(lockStates::UNLOCKING).c_str() : std::to_string(lockStates::LOCKING).c_str(), 1, 1, true); 583 | } else { 584 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(currentState).c_str(), 1, 1, true); 585 | } 586 | } 587 | if (espConfig::mqttData.lockEnableCustomState) { 588 | if (targetState == lockStates::UNLOCKED) { 589 | esp_mqtt_client_publish(client, espConfig::mqttData.lockCustomStateTopic.c_str(), std::to_string(espConfig::mqttData.customLockActions["UNLOCK"]).c_str(), 0, 0, false); 590 | } else if (targetState == lockStates::LOCKED) { 591 | esp_mqtt_client_publish(client, espConfig::mqttData.lockCustomStateTopic.c_str(), std::to_string(espConfig::mqttData.customLockActions["LOCK"]).c_str(), 0, 0, false); 592 | } 593 | } 594 | } else LOG(W, "MQTT Client not initialized, cannot publish message"); 595 | 596 | return (true); 597 | } 598 | }; 599 | 600 | struct NFCAccess : Service::NFCAccess 601 | { 602 | SpanCharacteristic* configurationState; 603 | SpanCharacteristic* nfcControlPoint; 604 | SpanCharacteristic* nfcSupportedConfiguration; 605 | const char* TAG = "NFCAccess"; 606 | 607 | NFCAccess() : Service::NFCAccess() { 608 | LOG(I, "Configuring NFCAccess"); // initialization message 609 | configurationState = new Characteristic::ConfigurationState(); 610 | nfcControlPoint = new Characteristic::NFCAccessControlPoint(); 611 | TLV8 conf(NULL, 0); 612 | conf.add(0x01, 0x10); 613 | conf.add(0x02, 0x10); 614 | nfcSupportedConfiguration = new Characteristic::NFCAccessSupportedConfiguration(conf); 615 | } 616 | 617 | boolean update() { 618 | LOG(D, "PROVISIONED READER KEY: %s", red_log::bufToHexString(readerData.reader_pk.data(), readerData.reader_pk.size()).c_str()); 619 | LOG(D, "READER GROUP IDENTIFIER: %s", red_log::bufToHexString(readerData.reader_gid.data(), readerData.reader_gid.size()).c_str()); 620 | LOG(D, "READER UNIQUE IDENTIFIER: %s", red_log::bufToHexString(readerData.reader_id.data(), readerData.reader_id.size()).c_str()); 621 | 622 | TLV8 ctrlData(NULL, 0); 623 | nfcControlPoint->getNewTLV(ctrlData); 624 | std::vector tlvData(ctrlData.pack_size()); 625 | ctrlData.pack(tlvData.data()); 626 | if (tlvData.size() == 0) 627 | return false; 628 | LOG(D, "Decoded data: %s", red_log::bufToHexString(tlvData.data(), tlvData.size()).c_str()); 629 | LOG(D, "Decoded data length: %d", tlvData.size()); 630 | HK_HomeKit hkCtx(readerData, savedData, "READERDATA", tlvData); 631 | std::vector result = hkCtx.processResult(); 632 | if (readerData.reader_gid.size() > 0) { 633 | memcpy(ecpData + 8, readerData.reader_gid.data(), readerData.reader_gid.size()); 634 | with_crc16(ecpData, 16, ecpData + 16); 635 | } 636 | TLV8 res(NULL, 0); 637 | res.unpack(result.data(), result.size()); 638 | nfcControlPoint->setTLV(res, false); 639 | return true; 640 | } 641 | 642 | }; 643 | 644 | void deleteReaderData(const char* buf = "") { 645 | esp_err_t erase_nvs = nvs_erase_key(savedData, "READERDATA"); 646 | esp_err_t commit_nvs = nvs_commit(savedData); 647 | readerData.issuers.clear(); 648 | readerData.reader_gid.clear(); 649 | readerData.reader_id.clear(); 650 | readerData.reader_pk.clear(); 651 | readerData.reader_pk_x.clear(); 652 | readerData.reader_sk.clear(); 653 | LOG(D, "*** NVS W STATUS"); 654 | LOG(D, "ERASE: %s", esp_err_to_name(erase_nvs)); 655 | LOG(D, "COMMIT: %s", esp_err_to_name(commit_nvs)); 656 | LOG(D, "*** NVS W STATUS"); 657 | } 658 | 659 | std::vector getHashIdentifier(const uint8_t* key, size_t len) { 660 | const char* TAG = "getHashIdentifier"; 661 | LOG(V, "Key: %s, Length: %d", red_log::bufToHexString(key, len).c_str(), len); 662 | std::vector hashable; 663 | std::string string = "key-identifier"; 664 | hashable.insert(hashable.begin(), string.begin(), string.end()); 665 | hashable.insert(hashable.end(), key, key + len); 666 | LOG(V, "Hashable: %s", red_log::bufToHexString(&hashable.front(), hashable.size()).c_str()); 667 | uint8_t hash[32]; 668 | mbedtls_sha256(&hashable.front(), hashable.size(), hash, 0); 669 | LOG(V, "HashIdentifier: %s", red_log::bufToHexString(hash, 8).c_str()); 670 | return std::vector{hash, hash + 8}; 671 | } 672 | 673 | void pairCallback() { 674 | if (HAPClient::nAdminControllers() == 0) { 675 | deleteReaderData(NULL); 676 | return; 677 | } 678 | for (auto it = homeSpan.controllerListBegin(); it != homeSpan.controllerListEnd(); ++it) { 679 | std::vector id = getHashIdentifier(it->getLTPK(), 32); 680 | LOG(D, "Found allocated controller - Hash: %s", red_log::bufToHexString(id.data(), 8).c_str()); 681 | hkIssuer_t* foundIssuer = nullptr; 682 | for (auto&& issuer : readerData.issuers) { 683 | if (std::equal(issuer.issuer_id.begin(), issuer.issuer_id.end(), id.begin())) { 684 | LOG(D, "Issuer %s already added, skipping", red_log::bufToHexString(issuer.issuer_id.data(), issuer.issuer_id.size()).c_str()); 685 | foundIssuer = &issuer; 686 | break; 687 | } 688 | } 689 | if (foundIssuer == nullptr) { 690 | LOG(D, "Adding new issuer - ID: %s", red_log::bufToHexString(id.data(), 8).c_str()); 691 | hkIssuer_t newIssuer; 692 | newIssuer.issuer_id = std::vector{ id.begin(), id.begin() + 8 }; 693 | newIssuer.issuer_pk.insert(newIssuer.issuer_pk.begin(), it->getLTPK(), it->getLTPK() + 32); 694 | readerData.issuers.emplace_back(newIssuer); 695 | } 696 | } 697 | save_to_nvs(); 698 | } 699 | 700 | void setFlow(const char* buf) { 701 | switch (buf[1]) { 702 | case '0': 703 | hkFlow = KeyFlow::kFlowFAST; 704 | LOG(I, "FAST Flow"); 705 | break; 706 | 707 | case '1': 708 | hkFlow = KeyFlow::kFlowSTANDARD; 709 | LOG(I, "STANDARD Flow"); 710 | break; 711 | case '2': 712 | hkFlow = KeyFlow::kFlowATTESTATION; 713 | LOG(I, "ATTESTATION Flow"); 714 | break; 715 | 716 | default: 717 | LOG(I, "0 = FAST flow, 1 = STANDARD Flow, 2 = ATTESTATION Flow"); 718 | break; 719 | } 720 | } 721 | 722 | void setLogLevel(const char* buf) { 723 | esp_log_level_t level = esp_log_level_get("*"); 724 | if (strncmp(buf + 1, "E", 1) == 0) { 725 | level = ESP_LOG_ERROR; 726 | LOG(I, "ERROR"); 727 | } else if (strncmp(buf + 1, "W", 1) == 0) { 728 | level = ESP_LOG_WARN; 729 | LOG(I, "WARNING"); 730 | } else if (strncmp(buf + 1, "I", 1) == 0) { 731 | level = ESP_LOG_INFO; 732 | LOG(I, "INFO"); 733 | } else if (strncmp(buf + 1, "D", 1) == 0) { 734 | level = ESP_LOG_DEBUG; 735 | LOG(I, "DEBUG"); 736 | } else if (strncmp(buf + 1, "V", 1) == 0) { 737 | level = ESP_LOG_VERBOSE; 738 | LOG(I, "VERBOSE"); 739 | } else if (strncmp(buf + 1, "N", 1) == 0) { 740 | level = ESP_LOG_NONE; 741 | LOG(I, "NONE"); 742 | } 743 | 744 | esp_log_level_set(TAG, level); 745 | esp_log_level_set("HK_HomeKit", level); 746 | esp_log_level_set("HKAuthCtx", level); 747 | esp_log_level_set("HKFastAuth", level); 748 | esp_log_level_set("HKStdAuth", level); 749 | esp_log_level_set("HKAttestAuth", level); 750 | esp_log_level_set("PN532", level); 751 | esp_log_level_set("PN532_SPI", level); 752 | esp_log_level_set("ISO18013_SC", level); 753 | esp_log_level_set("LockMechanism", level); 754 | esp_log_level_set("NFCAccess", level); 755 | esp_log_level_set("actions-config", level); 756 | esp_log_level_set("misc-config", level); 757 | esp_log_level_set("mqttconfig", level); 758 | } 759 | 760 | void print_issuers(const char* buf) { 761 | for (auto&& issuer : readerData.issuers) { 762 | LOG(I, "Issuer ID: %s, Public Key: %s", red_log::bufToHexString(issuer.issuer_id.data(), issuer.issuer_id.size()).c_str(), red_log::bufToHexString(issuer.issuer_pk.data(), issuer.issuer_pk.size()).c_str()); 763 | for (auto&& endpoint : issuer.endpoints) { 764 | LOG(I, "Endpoint ID: %s, Public Key: %s", red_log::bufToHexString(endpoint.endpoint_id.data(), endpoint.endpoint_id.size()).c_str(), red_log::bufToHexString(endpoint.endpoint_pk.data(), endpoint.endpoint_pk.size()).c_str()); 765 | } 766 | } 767 | } 768 | 769 | /** 770 | * The function `set_custom_state_handler` translate the custom states to their HomeKit counterpart 771 | * updating the state of the lock and publishes the new state to the `MQTT_STATE_TOPIC` MQTT topic. 772 | * 773 | * @param client The `client` parameter in the `set_custom_state_handler` function is of type 774 | * `esp_mqtt_client_handle_t`, which is a handle to the MQTT client object for this event. This 775 | * parameter is used to interact with the MQTT client 776 | * @param state The `state` parameter in the `set_custom_state_handler` function represents the 777 | * received custom state value 778 | */ 779 | void set_custom_state_handler(esp_mqtt_client_handle_t client, int state) { 780 | if (espConfig::mqttData.customLockStates["C_UNLOCKING"] == state) { 781 | lockTargetState->setVal(lockStates::UNLOCKED); 782 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::UNLOCKING).c_str(), 0, 1, true); 783 | return; 784 | } else if (espConfig::mqttData.customLockStates["C_LOCKING"] == state) { 785 | lockTargetState->setVal(lockStates::LOCKED); 786 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::LOCKING).c_str(), 0, 1, true); 787 | return; 788 | } else if (espConfig::mqttData.customLockStates["C_UNLOCKED"] == state) { 789 | if (espConfig::miscConfig.gpioActionPin != 255) { 790 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionUnlockState); 791 | } 792 | lockCurrentState->setVal(lockStates::UNLOCKED); 793 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::UNLOCKED).c_str(), 0, 1, true); 794 | return; 795 | } else if (espConfig::mqttData.customLockStates["C_LOCKED"] == state) { 796 | if (espConfig::miscConfig.gpioActionPin != 255) { 797 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionLockState); 798 | } 799 | lockCurrentState->setVal(lockStates::LOCKED); 800 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::LOCKED).c_str(), 0, 1, true); 801 | return; 802 | } else if (espConfig::mqttData.customLockStates["C_JAMMED"] == state) { 803 | lockCurrentState->setVal(lockStates::JAMMED); 804 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::JAMMED).c_str(), 0, 1, true); 805 | return; 806 | } else if (espConfig::mqttData.customLockStates["C_UNKNOWN"] == state) { 807 | lockCurrentState->setVal(lockStates::UNKNOWN); 808 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::UNKNOWN).c_str(), 0, 1, true); 809 | return; 810 | } 811 | LOG(D, "Update state failed! Recv value not valid"); 812 | } 813 | 814 | void set_state_handler(esp_mqtt_client_handle_t client, int state) { 815 | switch (state) { 816 | case lockStates::UNLOCKED: 817 | if (espConfig::miscConfig.gpioActionPin != 255) { 818 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionUnlockState); 819 | } 820 | lockTargetState->setVal(state); 821 | lockCurrentState->setVal(state); 822 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::UNLOCKED).c_str(), 0, 1, true); 823 | if (espConfig::mqttData.lockEnableCustomState) { 824 | esp_mqtt_client_publish(client, espConfig::mqttData.lockCustomStateTopic.c_str(), std::to_string(espConfig::mqttData.customLockActions["UNLOCK"]).c_str(), 0, 0, false); 825 | } 826 | break; 827 | case lockStates::LOCKED: 828 | if (espConfig::miscConfig.gpioActionPin != 255) { 829 | digitalWrite(espConfig::miscConfig.gpioActionPin, espConfig::miscConfig.gpioActionLockState); 830 | } 831 | lockTargetState->setVal(state); 832 | lockCurrentState->setVal(state); 833 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockStates::LOCKED).c_str(), 0, 1, true); 834 | if (espConfig::mqttData.lockEnableCustomState) { 835 | esp_mqtt_client_publish(client, espConfig::mqttData.lockCustomStateTopic.c_str(), std::to_string(espConfig::mqttData.customLockActions["LOCK"]).c_str(), 0, 0, false); 836 | } 837 | break; 838 | case lockStates::JAMMED: 839 | case lockStates::UNKNOWN: 840 | lockCurrentState->setVal(state); 841 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(state).c_str(), 0, 1, true); 842 | break; 843 | default: 844 | LOG(D, "Update state failed! Recv value not valid"); 845 | break; 846 | } 847 | } 848 | 849 | void mqtt_connected_event(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { 850 | esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data; 851 | esp_mqtt_client_handle_t client = event->client; 852 | const esp_app_desc_t* app_desc = esp_app_get_description(); 853 | std::string app_version = app_desc->version; 854 | uint8_t mac[6]; 855 | esp_read_mac(mac, ESP_MAC_BT); 856 | char macStr[18] = { 0 }; 857 | sprintf(macStr, "%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3]); 858 | std::string serialNumber = "HK-"; 859 | serialNumber.append(macStr); 860 | LOG(I, "MQTT connected"); 861 | if (espConfig::mqttData.hassMqttDiscoveryEnabled) { 862 | json payload; 863 | payload["topic"] = espConfig::mqttData.hkTopic.c_str(); 864 | payload["value_template"] = "{{ value_json.uid }}"; 865 | json device; 866 | device["name"] = espConfig::miscConfig.deviceName.c_str(); 867 | char identifier[18]; 868 | sprintf(identifier, "%.2s%.2s%.2s%.2s%.2s%.2s", HAPClient::accessory.ID, HAPClient::accessory.ID + 3, HAPClient::accessory.ID + 6, HAPClient::accessory.ID + 9, HAPClient::accessory.ID + 12, HAPClient::accessory.ID + 15); 869 | std::string id = identifier; 870 | device["identifiers"].push_back(id); 871 | device["identifiers"].push_back(serialNumber); 872 | device["manufacturer"] = "rednblkx"; 873 | device["model"] = "HomeKey-ESP32"; 874 | device["sw_version"] = app_version.c_str(); 875 | device["serial_number"] = serialNumber; 876 | payload["device"] = device; 877 | std::string bufferpub = payload.dump(); 878 | std::string rfidTopic; 879 | rfidTopic.append("homeassistant/tag/").append(espConfig::mqttData.mqttClientId).append("/rfid/config"); 880 | if (!espConfig::mqttData.nfcTagNoPublish) { 881 | esp_mqtt_client_publish(client, rfidTopic.c_str(), bufferpub.c_str(), bufferpub.length(), 1, true); 882 | } 883 | payload = json(); 884 | payload["topic"] = espConfig::mqttData.hkTopic; 885 | payload["value_template"] = "{{ value_json.issuerId }}"; 886 | payload["device"] = device; 887 | bufferpub = payload.dump(); 888 | std::string issuerTopic; 889 | issuerTopic.append("homeassistant/tag/").append(espConfig::mqttData.mqttClientId).append("/hkIssuer/config"); 890 | esp_mqtt_client_publish(client, issuerTopic.c_str(), bufferpub.c_str(), bufferpub.length(), 1, true); 891 | payload = json(); 892 | payload["topic"] = espConfig::mqttData.hkTopic; 893 | payload["value_template"] = "{{ value_json.endpointId }}"; 894 | payload["device"] = device; 895 | bufferpub = payload.dump(); 896 | std::string endpointTopic; 897 | endpointTopic.append("homeassistant/tag/").append(espConfig::mqttData.mqttClientId).append("/hkEndpoint/config"); 898 | esp_mqtt_client_publish(client, endpointTopic.c_str(), bufferpub.c_str(), bufferpub.length(), 1, true); 899 | payload = json(); 900 | payload["name"] = "Lock"; 901 | payload["state_topic"] = espConfig::mqttData.lockStateTopic.c_str(); 902 | payload["command_topic"] = espConfig::mqttData.lockStateCmd.c_str(); 903 | payload["payload_lock"] = "1"; 904 | payload["payload_unlock"] = "0"; 905 | payload["state_locked"] = "1"; 906 | payload["state_unlocked"] = "0"; 907 | payload["state_unlocking"] = "4"; 908 | payload["state_locking"] = "5"; 909 | payload["state_jammed"] = "2"; 910 | payload["availability_topic"] = espConfig::mqttData.lwtTopic.c_str(); 911 | payload["unique_id"] = id; 912 | payload["device"] = device; 913 | payload["retain"] = "false"; 914 | bufferpub = payload.dump(); 915 | std::string lockConfigTopic; 916 | lockConfigTopic.append("homeassistant/lock/").append(espConfig::mqttData.mqttClientId.c_str()).append("/lock/config"); 917 | esp_mqtt_client_publish(client, lockConfigTopic.c_str(), bufferpub.c_str(), bufferpub.length(), 1, true); 918 | LOG(D, "MQTT PUBLISHED DISCOVERY"); 919 | } 920 | esp_mqtt_client_publish(client, espConfig::mqttData.lwtTopic.c_str(), "online", 6, 1, true); 921 | if (espConfig::mqttData.lockEnableCustomState) { 922 | esp_mqtt_client_subscribe(client, espConfig::mqttData.lockCustomStateCmd.c_str(), 0); 923 | } 924 | if (espConfig::miscConfig.proxBatEnabled) { 925 | esp_mqtt_client_subscribe(client, espConfig::mqttData.btrLvlCmdTopic.c_str(), 0); 926 | } 927 | esp_mqtt_client_subscribe(client, espConfig::mqttData.lockStateCmd.c_str(), 0); 928 | esp_mqtt_client_subscribe(client, espConfig::mqttData.lockCStateCmd.c_str(), 0); 929 | esp_mqtt_client_subscribe(client, espConfig::mqttData.lockTStateCmd.c_str(), 0); 930 | } 931 | 932 | void mqtt_data_handler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { 933 | // ESP_LOGD(TAG, "Event dispatched from callback type=%d", event_id); 934 | esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data; 935 | esp_mqtt_client_handle_t client = event->client; 936 | std::string topic(event->topic, event->topic + event->topic_len); 937 | std::string data(event->data, event->data + event->data_len); 938 | LOG(D, "Received message in topic \"%s\": %s", topic.c_str(), data.c_str()); 939 | int state = atoi(data.c_str()); 940 | if (!strcmp(espConfig::mqttData.lockCustomStateCmd.c_str(), topic.c_str())) { 941 | set_custom_state_handler(client, state); 942 | } else if (!strcmp(espConfig::mqttData.lockStateCmd.c_str(), topic.c_str())) { 943 | set_state_handler(client, state); 944 | } else if (!strcmp(espConfig::mqttData.lockTStateCmd.c_str(), topic.c_str())) { 945 | if (state == lockStates::UNLOCKED || state == lockStates::LOCKED) { 946 | lockTargetState->setVal(state); 947 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), state == lockStates::UNLOCKED ? std::to_string(lockStates::UNLOCKING).c_str() : std::to_string(lockStates::LOCKING).c_str(), 0, 1, true); 948 | } 949 | } else if (!strcmp(espConfig::mqttData.lockCStateCmd.c_str(), topic.c_str())) { 950 | if (state == lockStates::UNLOCKED || state == lockStates::LOCKED || state == lockStates::JAMMED || state == lockStates::UNKNOWN) { 951 | lockCurrentState->setVal(state); 952 | esp_mqtt_client_publish(client, espConfig::mqttData.lockStateTopic.c_str(), std::to_string(lockCurrentState->getVal()).c_str(), 0, 1, true); 953 | } 954 | } else if (!strcmp(espConfig::mqttData.btrLvlCmdTopic.c_str(), topic.c_str())) { 955 | btrLevel->setVal(state); 956 | if (state <= espConfig::miscConfig.btrLowStatusThreshold) { 957 | statusLowBtr->setVal(1); 958 | } else { 959 | statusLowBtr->setVal(0); 960 | } 961 | } 962 | } 963 | 964 | /** 965 | * The function `mqtt_app_start` initializes and starts an MQTT client with specified configuration 966 | * parameters. 967 | */ 968 | static void mqtt_app_start(void) { 969 | esp_mqtt_client_config_t mqtt_cfg {}; 970 | mqtt_cfg.broker.address.hostname = espConfig::mqttData.mqttBroker.c_str(); 971 | mqtt_cfg.broker.address.port = espConfig::mqttData.mqttPort; 972 | mqtt_cfg.broker.address.transport = MQTT_TRANSPORT_OVER_TCP; 973 | mqtt_cfg.credentials.client_id = espConfig::mqttData.mqttClientId.c_str(); 974 | mqtt_cfg.credentials.username = espConfig::mqttData.mqttUsername.c_str(); 975 | mqtt_cfg.credentials.authentication.password = espConfig::mqttData.mqttPassword.c_str(); 976 | mqtt_cfg.session.last_will.topic = espConfig::mqttData.lwtTopic.c_str(); 977 | mqtt_cfg.session.last_will.msg = "offline"; 978 | mqtt_cfg.session.last_will.msg_len = 7; 979 | mqtt_cfg.session.last_will.retain = true; 980 | mqtt_cfg.session.last_will.qos = 1; 981 | client = esp_mqtt_client_init(&mqtt_cfg); 982 | esp_mqtt_client_register_event(client, MQTT_EVENT_CONNECTED, mqtt_connected_event, client); 983 | esp_mqtt_client_register_event(client, MQTT_EVENT_DATA, mqtt_data_handler, client); 984 | esp_mqtt_client_start(client); 985 | } 986 | 987 | void notFound(AsyncWebServerRequest* request) { 988 | request->send(404, "text/plain", "Not found"); 989 | } 990 | 991 | void listDir(fs::FS& fs, const char* dirname, uint8_t levels) { 992 | LOG(I, "Listing directory: %s\r\n", dirname); 993 | 994 | File root = fs.open(dirname); 995 | if (!root) { 996 | LOG(I, "- failed to open directory"); 997 | return; 998 | } 999 | if (!root.isDirectory()) { 1000 | LOG(I, " - not a directory"); 1001 | return; 1002 | } 1003 | 1004 | File file = root.openNextFile(); 1005 | while (file) { 1006 | if (file.isDirectory()) { 1007 | Serial.print(" DIR : "); 1008 | LOG(I, "%s", file.name()); 1009 | if (levels) { 1010 | listDir(fs, file.name(), levels - 1); 1011 | } 1012 | } else { 1013 | Serial.print(" FILE: "); 1014 | Serial.print(file.name()); 1015 | Serial.print("\tSIZE: "); 1016 | LOG(I, "%d", file.size()); 1017 | } 1018 | file = root.openNextFile(); 1019 | } 1020 | } 1021 | 1022 | String indexProcess(const String& var) { 1023 | if (var == "VERSION") { 1024 | const esp_app_desc_t* app_desc = esp_app_get_description(); 1025 | std::string app_version = app_desc->version; 1026 | return String(app_version.c_str()); 1027 | } 1028 | return ""; 1029 | } 1030 | 1031 | bool headersFix(AsyncWebServerRequest* request) { request->addInterestingHeader("ANY"); return true; }; 1032 | void setupWeb() { 1033 | auto assetsHandle = new AsyncStaticWebHandler("/assets", LittleFS, "/assets/", NULL); 1034 | assetsHandle->setFilter(headersFix); 1035 | webServer.addHandler(assetsHandle); 1036 | auto routesHandle = new AsyncStaticWebHandler("/fragment", LittleFS, "/routes", NULL); 1037 | routesHandle->setFilter(headersFix); 1038 | webServer.addHandler(routesHandle); 1039 | AsyncCallbackWebHandler* dataProvision = new AsyncCallbackWebHandler(); 1040 | webServer.addHandler(dataProvision); 1041 | dataProvision->setUri("/config"); 1042 | dataProvision->setMethod(HTTP_GET); 1043 | dataProvision->onRequest([](AsyncWebServerRequest* req) { 1044 | if (req->hasParam("type")) { 1045 | json serializedData; 1046 | AsyncWebParameter* data = req->getParam(0); 1047 | std::array pages = {"mqtt", "actions", "misc", "hkinfo"}; 1048 | if (std::equal(data->value().begin(), data->value().end(), pages[0].begin(), pages[0].end())) { 1049 | LOG(D, "MQTT CONFIG REQ"); 1050 | serializedData = espConfig::mqttData; 1051 | } else if (std::equal(data->value().begin(), data->value().end(),pages[1].begin(), pages[1].end()) || std::equal(data->value().begin(), data->value().end(),pages[2].begin(), pages[2].end())) { 1052 | LOG(D, "ACTIONS CONFIG REQ"); 1053 | serializedData = espConfig::miscConfig; 1054 | } else if (std::equal(data->value().begin(), data->value().end(),pages[3].begin(), pages[3].end())) { 1055 | LOG(D, "HK DATA REQ"); 1056 | json inputData = readerData; 1057 | if (inputData.contains("group_identifier")) { 1058 | serializedData["group_identifier"] = red_log::bufToHexString(readerData.reader_gid.data(), readerData.reader_gid.size(), true); 1059 | } 1060 | if (inputData.contains("unique_identifier")) { 1061 | serializedData["unique_identifier"] = red_log::bufToHexString(readerData.reader_id.data(), readerData.reader_id.size(), true); 1062 | } 1063 | if (inputData.contains("issuers")) { 1064 | serializedData["issuers"] = json::array(); 1065 | for (auto it = inputData.at("issuers").begin(); it != inputData.at("issuers").end(); ++it) 1066 | { 1067 | json issuer; 1068 | if (it.value().contains("issuerId")) { 1069 | std::vector id = it.value().at("issuerId").get>(); 1070 | issuer["issuerId"] = red_log::bufToHexString(id.data(), id.size(), true); 1071 | } 1072 | if (it.value().contains("endpoints") && it.value().at("endpoints").size() > 0) { 1073 | issuer["endpoints"] = json::array(); 1074 | for (auto it2 = it.value().at("endpoints").begin(); it2 != it.value().at("endpoints").end(); ++it2) { 1075 | json endpoint; 1076 | if (it2.value().contains("endpointId")) { 1077 | std::vector id = it2.value().at("endpointId").get>(); 1078 | endpoint["endpointId"] = red_log::bufToHexString(id.data(), id.size(), true); 1079 | } 1080 | issuer["endpoints"].push_back(endpoint); 1081 | } 1082 | } 1083 | serializedData["issuers"].push_back(issuer); 1084 | } 1085 | } 1086 | } else { 1087 | req->send(400); 1088 | return; 1089 | } 1090 | if (!serializedData.empty()) { 1091 | req->send(200, "application/json", serializedData.dump().c_str()); 1092 | } else { 1093 | req->send(500); 1094 | } 1095 | } else req->send(500); 1096 | }); 1097 | AsyncCallbackWebHandler* ethSuppportConfig = new AsyncCallbackWebHandler(); 1098 | webServer.addHandler(ethSuppportConfig); 1099 | ethSuppportConfig->setUri("/eth_get_config"); 1100 | ethSuppportConfig->setMethod(HTTP_GET); 1101 | ethSuppportConfig->onRequest([](AsyncWebServerRequest *req) { 1102 | json eth_config; 1103 | eth_config["supportedChips"] = json::array(); 1104 | for (auto &&v : eth_config_ns::supportedChips) { 1105 | eth_config.at("supportedChips").push_back(v.second); 1106 | } 1107 | eth_config["boardPresets"] = eth_config_ns::boardPresets; 1108 | eth_config["ethEnabled"] = espConfig::miscConfig.ethernetEnabled; 1109 | req->send(200, "application/json", eth_config.dump().c_str()); 1110 | }); 1111 | AsyncCallbackWebHandler* dataClear = new AsyncCallbackWebHandler(); 1112 | webServer.addHandler(dataClear); 1113 | dataClear->setUri("/config/clear"); 1114 | dataClear->setMethod(HTTP_POST); 1115 | dataClear->onRequest([](AsyncWebServerRequest* req) { 1116 | if (req->hasParam("type")) { 1117 | AsyncWebParameter* data = req->getParam(0); 1118 | std::array pages = { "mqtt", "actions", "misc" }; 1119 | if (std::equal(data->value().begin(), data->value().end(), pages[0].begin(), pages[0].end())) { 1120 | LOG(D, "MQTT CONFIG SEL"); 1121 | nvs_erase_key(savedData, "MQTTDATA"); 1122 | espConfig::mqttData = {}; 1123 | req->send(200, "text/plain", "200 Success"); 1124 | } else if (std::equal(data->value().begin(), data->value().end(), pages[1].begin(), pages[1].end())) { 1125 | LOG(D, "ACTIONS CONFIG SEL"); 1126 | nvs_erase_key(savedData, "MISCDATA"); 1127 | espConfig::miscConfig = {}; 1128 | req->send(200, "text/plain", "200 Success"); 1129 | } else if (std::equal(data->value().begin(), data->value().end(), pages[2].begin(), pages[2].end())) { 1130 | LOG(D, "MISC CONFIG SEL"); 1131 | nvs_erase_key(savedData, "MISCDATA"); 1132 | espConfig::miscConfig = {}; 1133 | req->send(200, "text/plain", "200 Success"); 1134 | } else { 1135 | req->send(400); 1136 | return; 1137 | } 1138 | } else { 1139 | req->send(400); 1140 | return; 1141 | } 1142 | }); 1143 | AsyncCallbackWebHandler* dataLoad = new AsyncCallbackWebHandler(); 1144 | webServer.addHandler(dataLoad); 1145 | dataLoad->setUri("/config/save"); 1146 | dataLoad->setMethod(HTTP_POST); 1147 | dataLoad->onBody([](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) { 1148 | json *dataJson = new json(json::parse(data, data + len)); 1149 | if (!dataJson->is_discarded()) { 1150 | LOG(I, "%s", dataJson->dump().c_str()); 1151 | request->_tempObject = dataJson; 1152 | } 1153 | }); 1154 | dataLoad->onRequest([=](AsyncWebServerRequest* req) { 1155 | json *serializedData = static_cast(req->_tempObject); 1156 | if (req->hasParam("type") && serializedData) { 1157 | AsyncWebParameter* data = req->getParam(0); 1158 | json configData; 1159 | std::array pages = { "mqtt", "actions", "misc" }; 1160 | uint8_t selConfig; 1161 | if (std::equal(data->value().begin(), data->value().end(), pages[0].begin(), pages[0].end())) { 1162 | LOG(D, "MQTT CONFIG SEL"); 1163 | configData = espConfig::mqttData; 1164 | selConfig = 0; 1165 | } else if (std::equal(data->value().begin(), data->value().end(), pages[1].begin(), pages[1].end())) { 1166 | LOG(D, "ACTIONS CONFIG SEL"); 1167 | configData = espConfig::miscConfig; 1168 | selConfig = 1; 1169 | } else if (std::equal(data->value().begin(), data->value().end(), pages[2].begin(), pages[2].end())) { 1170 | LOG(D, "MISC CONFIG SEL"); 1171 | configData = espConfig::miscConfig; 1172 | selConfig = 2; 1173 | } else { 1174 | req->send(400); 1175 | return; 1176 | } 1177 | uint8_t propertiesProcessed = 0; 1178 | for (auto it = serializedData->begin(); it != serializedData->end(); ++it) { 1179 | if (configData.contains(it.key()) && ((configData.at(it.key()).type() == it.value().type()) || configData.at(it.key()).is_boolean())) { 1180 | if (it.key() == std::string("setupCode")) { 1181 | std::string code = it.value().template get(); 1182 | if (it.value().is_string() && (!code.empty() && std::find_if(code.begin(), code.end(), [](unsigned char c) { return !std::isdigit(c); }) == code.end()) && it.value().template get().length() == 8) { 1183 | if (homeSpan.controllerListBegin() != homeSpan.controllerListEnd() && code.compare(configData.at(it.key()).template get())) { 1184 | LOG(E, "The Setup Code can only be set if no devices are paired, reset if any issues!"); 1185 | req->send(400, "text/plain", "The Setup Code can only be set if no devices are paired, reset if any issues!"); 1186 | break; 1187 | } 1188 | } else { 1189 | LOG(E, "\"%s\" could not validate!", it.key().c_str()); 1190 | std::string msg = "\"\" is not a valid value for \"\""; 1191 | msg.insert(1, it.value().dump().c_str()).insert(msg.length() - 1, it.key()); 1192 | req->send(400, "text/plain", msg.c_str()); 1193 | break; 1194 | } 1195 | } else if (!(std::char_traits::compare(it.key().data() + (it.key().length() - 3), "Pin", 3))) { 1196 | if (it.value().is_number() && it.value() > 0 && it.value() < 256) { 1197 | if (!GPIO_IS_VALID_GPIO(it.value().template get()) && !GPIO_IS_VALID_OUTPUT_GPIO(it.value().template get()) && it.value() != 255) { 1198 | LOG(E, "\"%s\" could not validate!", it.key().c_str()); 1199 | std::string msg = "\"\" is not a valid GPIO Pin for \"\""; 1200 | msg.insert(1, it.value().dump().c_str()).insert(msg.length() - 1, it.key()); 1201 | req->send(400, "text/plain", msg.c_str()); 1202 | break; 1203 | } 1204 | } else { 1205 | LOG(E, "\"%s\" could not validate!", it.key().c_str()); 1206 | std::string msg = "\"\" is not a valid value for \"\""; 1207 | msg.insert(1, it.value().dump().c_str()).insert(msg.length() - 1, it.key()); 1208 | req->send(400, "text/plain", msg.c_str()); 1209 | break; 1210 | } 1211 | } 1212 | if (configData.at(it.key()).is_boolean() && it.value().is_number()) { 1213 | it.value() = static_cast(it.value().template get()); 1214 | } else if(configData.at(it.key()).is_boolean() && !it.value().is_number()) { 1215 | LOG(E, "\"%s\" could not validate!", it.key().c_str()); 1216 | std::string msg = "\"\" is not a valid value for \"\""; 1217 | msg.insert(1, it.value().dump().c_str()).insert(msg.length() - 1, it.key()); 1218 | req->send(400, "text/plain", msg.c_str()); 1219 | break; 1220 | } 1221 | propertiesProcessed++; 1222 | } else { 1223 | LOG(E, "\"%s\" could not validate!", it.key().c_str()); 1224 | std::string msg = "\"\" not of correct type or does not exist in config"; 1225 | msg.insert(1, it.key()); 1226 | req->send(400, "text/plain", msg.c_str()); 1227 | break; 1228 | } 1229 | } 1230 | if (propertiesProcessed != serializedData->size()) { 1231 | LOG(E, "Not all properties could be validated, cannot continue!"); 1232 | if(!req->client()->disconnected() || !req->client()->disconnecting()) { 1233 | req->send(500, "text/plain", "Something went wrong!"); 1234 | } 1235 | return; 1236 | } 1237 | bool rebootNeeded = false; 1238 | std::string rebootMsg; 1239 | for (auto it = serializedData->begin(); it != serializedData->end(); ++it) { 1240 | if (it.key() == std::string("nfcTagNoPublish") && (it.value() != 0)) { 1241 | std::string clientId; 1242 | if (serializedData->contains("mqttClientId")) { 1243 | clientId = serializedData->at("mqttClientId"); 1244 | } 1245 | std::string rfidTopic; 1246 | rfidTopic.append("homeassistant/tag/").append(!clientId.empty() ? clientId : espConfig::mqttData.mqttClientId).append("/rfid/config"); 1247 | esp_mqtt_client_publish(client, rfidTopic.c_str(), "", 0, 0, false); 1248 | } else if (it.key() == std::string("setupCode")) { 1249 | std::string code = it.value().template get(); 1250 | if (espConfig::miscConfig.setupCode.c_str() != it.value() && code.length() == 8) { 1251 | if (homeSpan.controllerListBegin() == homeSpan.controllerListEnd()) { 1252 | homeSpan.setPairingCode(code.c_str()); 1253 | } 1254 | } 1255 | } else if (it.key() == std::string("nfcNeopixelPin")) { 1256 | if (espConfig::miscConfig.nfcNeopixelPin == 255 && it.value() != 255 && neopixel_task_handle == nullptr) { 1257 | xTaskCreate(neopixel_task, "neopixel_task", 4096, NULL, 2, &neopixel_task_handle); 1258 | if (!pixel) { 1259 | pixel = std::make_shared(it.value(), PixelType::GRB); 1260 | } 1261 | } else if (espConfig::miscConfig.nfcNeopixelPin != 255 && it.value() == 255 && neopixel_task_handle != nullptr) { 1262 | uint8_t status = 2; 1263 | xQueueSend(neopixel_handle, &status, 0); 1264 | neopixel_task_handle = nullptr; 1265 | } 1266 | } else if (it.key() == std::string("nfcSuccessPin")) { 1267 | if (espConfig::miscConfig.nfcSuccessPin == 255 && it.value() != 255 && gpio_led_task_handle == nullptr) { 1268 | pinMode(it.value(), OUTPUT); 1269 | xTaskCreate(nfc_gpio_task, "nfc_gpio_task", 4096, NULL, 2, &gpio_led_task_handle); 1270 | } else if (espConfig::miscConfig.nfcSuccessPin != 255 && it.value() == 255 && gpio_led_task_handle != nullptr) { 1271 | if (serializedData->contains("nfcFailPin") && serializedData->at("nfcFailPin") == 255) { 1272 | uint8_t status = 2; 1273 | xQueueSend(gpio_led_handle, &status, 0); 1274 | gpio_led_task_handle = nullptr; 1275 | } 1276 | } else if (it.value() != 255) { 1277 | pinMode(it.value(), OUTPUT); 1278 | } 1279 | } else if (it.key() == std::string("nfcFailPin")) { 1280 | if (espConfig::miscConfig.nfcFailPin == 255 && it.value() != 255 && gpio_led_task_handle == nullptr) { 1281 | pinMode(it.value(), OUTPUT); 1282 | xTaskCreate(nfc_gpio_task, "nfc_gpio_task", 4096, NULL, 2, &gpio_led_task_handle); 1283 | } else if (espConfig::miscConfig.nfcFailPin != 255 && it.value() == 255 && gpio_led_task_handle != nullptr) { 1284 | if (serializedData->contains("nfcSuccessPin") && serializedData->at("nfcSuccessPin") == 255) { 1285 | uint8_t status = 2; 1286 | xQueueSend(gpio_led_handle, &status, 0); 1287 | gpio_led_task_handle = nullptr; 1288 | } 1289 | } else if (it.value() != 255) { 1290 | pinMode(it.value(), OUTPUT); 1291 | } 1292 | } else if (it.key() == std::string("btrLowStatusThreshold")) { 1293 | if (statusLowBtr && btrLevel) { 1294 | if (btrLevel->getVal() <= it.value()) { 1295 | statusLowBtr->setVal(1); 1296 | } else { 1297 | statusLowBtr->setVal(0); 1298 | } 1299 | } 1300 | } else if (it.key() == std::string("neoPixelType")) { 1301 | uint8_t pixelType = it.value().template get(); 1302 | if (pixelType != configData.at(it.key()).template get()) { 1303 | rebootNeeded = true; 1304 | rebootMsg = "Pixel Type was changed, reboot needed! Rebooting..."; 1305 | } 1306 | } else if (it.key() == std::string("gpioActionPin")) { 1307 | if (espConfig::miscConfig.gpioActionPin == 255 && it.value() != 255 ) { 1308 | LOG(D, "ENABLING HomeKit Trigger - Simple GPIO"); 1309 | pinMode(it.value(), OUTPUT); 1310 | if(gpio_lock_task_handle == nullptr){ 1311 | xTaskCreate(gpio_task, "gpio_task", 4096, NULL, 2, &gpio_lock_task_handle); 1312 | } 1313 | if(espConfig::miscConfig.hkDumbSwitchMode){ 1314 | serializedData->at("hkDumbSwitchMode") = false; 1315 | } 1316 | } else if (espConfig::miscConfig.gpioActionPin != 255 && it.value() == 255) { 1317 | LOG(D, "DISABLING HomeKit Trigger - Simple GPIO"); 1318 | if( gpio_lock_task_handle != nullptr){ 1319 | gpioLockAction status{ .source = gpioLockAction::OTHER, .action = 2 }; 1320 | xQueueSend(gpio_lock_handle, &status, 0); 1321 | gpio_lock_task_handle = nullptr; 1322 | } 1323 | gpio_reset_pin(gpio_num_t(espConfig::miscConfig.gpioActionPin)); 1324 | } 1325 | } else if (it.key() == std::string("hkDumbSwitchMode") && gpio_lock_task_handle == nullptr) { 1326 | xTaskCreate(gpio_task, "gpio_task", 4096, NULL, 2, &gpio_lock_task_handle); 1327 | } 1328 | configData.at(it.key()) = it.value(); 1329 | } 1330 | std::vector vectorData = json::to_msgpack(configData); 1331 | esp_err_t set_nvs = nvs_set_blob(savedData, selConfig == 0 ? "MQTTDATA" : "MISCDATA", vectorData.data(), vectorData.size()); 1332 | esp_err_t commit_nvs = nvs_commit(savedData); 1333 | LOG(D, "SET_STATUS: %s", esp_err_to_name(set_nvs)); 1334 | LOG(D, "COMMIT_STATUS: %s", esp_err_to_name(commit_nvs)); 1335 | if (set_nvs == ESP_OK && commit_nvs == ESP_OK) { 1336 | LOG(I, "Config successfully saved to NVS"); 1337 | if (selConfig == 0) { 1338 | configData.get_to(espConfig::mqttData); 1339 | } else { 1340 | configData.get_to(espConfig::miscConfig); 1341 | } 1342 | } else { 1343 | LOG(E, "Something went wrong, could not save to NVS"); 1344 | } 1345 | if (selConfig == 0 || selConfig == 2) { 1346 | req->send(200, "text/plain", "Saved! Restarting..."); 1347 | vTaskDelay(1000 / portTICK_PERIOD_MS); 1348 | ESP.restart(); 1349 | } else { 1350 | if(rebootNeeded){ 1351 | req->send(200, "text/plain", rebootMsg.c_str()); 1352 | } else { 1353 | req->send(200, "text/plain", "Saved and applied!"); 1354 | } 1355 | } 1356 | } 1357 | }); 1358 | auto rebootDeviceHandle = new AsyncCallbackWebHandler(); 1359 | rebootDeviceHandle->setUri("/reboot_device"); 1360 | rebootDeviceHandle->setMethod(HTTP_GET); 1361 | rebootDeviceHandle->onRequest([](AsyncWebServerRequest* request) { 1362 | request->send(200, "text/plain", "Rebooting the device..."); 1363 | delay(1000); 1364 | ESP.restart(); 1365 | }); 1366 | webServer.addHandler(rebootDeviceHandle); 1367 | auto startConfigAP = new AsyncCallbackWebHandler(); 1368 | startConfigAP->setUri("/start_config_ap"); 1369 | startConfigAP->setMethod(HTTP_GET); 1370 | startConfigAP->onRequest([](AsyncWebServerRequest* request) { 1371 | request->send(200, "text/plain", "Starting the AP..."); 1372 | delay(1000); 1373 | webServer.end(); 1374 | homeSpan.processSerialCommand("A"); 1375 | }); 1376 | webServer.addHandler(startConfigAP); 1377 | auto resetHkHandle = new AsyncCallbackWebHandler(); 1378 | resetHkHandle->setUri("/reset_hk_pair"); 1379 | resetHkHandle->setMethod(HTTP_GET); 1380 | resetHkHandle->onRequest([](AsyncWebServerRequest* request) { 1381 | request->send(200, "text/plain", "Erasing HomeKit pairings and restarting..."); 1382 | delay(1000); 1383 | deleteReaderData(); 1384 | homeSpan.processSerialCommand("H"); 1385 | }); 1386 | webServer.addHandler(resetHkHandle); 1387 | auto resetWifiHandle = new AsyncCallbackWebHandler(); 1388 | resetWifiHandle->setUri("/reset_wifi_cred"); 1389 | resetWifiHandle->setMethod(HTTP_GET); 1390 | resetWifiHandle->onRequest([](AsyncWebServerRequest* request) { 1391 | request->send(200, "text/plain", "Erasing WiFi credentials and restarting, AP will start on boot..."); 1392 | delay(1000); 1393 | homeSpan.processSerialCommand("X"); 1394 | }); 1395 | webServer.addHandler(resetWifiHandle); 1396 | auto getWifiRssi = new AsyncCallbackWebHandler(); 1397 | getWifiRssi->setUri("/get_wifi_rssi"); 1398 | getWifiRssi->setMethod(HTTP_GET); 1399 | getWifiRssi->onRequest([](AsyncWebServerRequest* request) { 1400 | std::string rssi_val = std::to_string(WiFi.RSSI()); 1401 | request->send(200, "text/plain", rssi_val.c_str()); 1402 | }); 1403 | webServer.addHandler(getWifiRssi); 1404 | AsyncCallbackWebHandler* rootHandle = new AsyncCallbackWebHandler(); 1405 | webServer.addHandler(rootHandle); 1406 | rootHandle->setUri("/"); 1407 | rootHandle->setMethod(HTTP_GET); 1408 | rootHandle->onRequest([](AsyncWebServerRequest* req) { 1409 | req->send(LittleFS, "/index.html", "text/html", false, indexProcess); 1410 | }); 1411 | AsyncCallbackWebHandler* hashPage = new AsyncCallbackWebHandler(); 1412 | webServer.addHandler(hashPage); 1413 | hashPage->setUri("/#*"); 1414 | hashPage->setMethod(HTTP_GET); 1415 | hashPage->onRequest([](AsyncWebServerRequest* req) { 1416 | req->send(LittleFS, "/index.html", "text/html", false, indexProcess); 1417 | }); 1418 | if (espConfig::miscConfig.webAuthEnabled) { 1419 | LOG(I, "Web Authentication Enabled"); 1420 | routesHandle->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1421 | dataProvision->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1422 | dataLoad->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1423 | dataClear->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1424 | rootHandle->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1425 | hashPage->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1426 | resetHkHandle->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1427 | resetWifiHandle->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1428 | getWifiRssi->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1429 | startConfigAP->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1430 | ethSuppportConfig->setAuthentication(espConfig::miscConfig.webUsername.c_str(), espConfig::miscConfig.webPassword.c_str()); 1431 | } 1432 | webServer.onNotFound(notFound); 1433 | webServer.begin(); 1434 | } 1435 | 1436 | void mqttConfigReset(const char* buf) { 1437 | nvs_erase_key(savedData, "MQTTDATA"); 1438 | nvs_commit(savedData); 1439 | ESP.restart(); 1440 | } 1441 | 1442 | void wifiCallback(int status) { 1443 | if (status == 1) { 1444 | if (espConfig::mqttData.mqttBroker.size() >= 7 && espConfig::mqttData.mqttBroker.size() <= 16 && !std::equal(espConfig::mqttData.mqttBroker.begin(), espConfig::mqttData.mqttBroker.end(), "0.0.0.0")) { 1445 | mqtt_app_start(); 1446 | } 1447 | setupWeb(); 1448 | } 1449 | } 1450 | 1451 | void mqtt_publish(std::string topic, std::string payload, uint8_t qos, bool retain) { 1452 | if (client != nullptr) { 1453 | esp_mqtt_client_publish(client, topic.c_str(), payload.c_str(), payload.length(), 0, retain); 1454 | } else LOG(W, "MQTT Client not initialized, cannot publish message"); 1455 | } 1456 | 1457 | std::string hex_representation(const std::vector& v) { 1458 | std::string hex_tmp; 1459 | for (auto x : v) { 1460 | std::ostringstream oss; 1461 | oss << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << (unsigned)x; 1462 | hex_tmp += oss.str(); 1463 | } 1464 | return hex_tmp; 1465 | } 1466 | 1467 | void nfc_retry(void* arg) { 1468 | ESP_LOGI(TAG, "Starting reconnecting PN532"); 1469 | while (1) { 1470 | nfc->begin(); 1471 | uint32_t versiondata = nfc->getFirmwareVersion(); 1472 | if (!versiondata) { 1473 | ESP_LOGE("NFC_SETUP", "Error establishing PN532 connection"); 1474 | } else { 1475 | unsigned int model = (versiondata >> 24) & 0xFF; 1476 | ESP_LOGI("NFC_SETUP", "Found chip PN5%x", model); 1477 | int maj = (versiondata >> 16) & 0xFF; 1478 | int min = (versiondata >> 8) & 0xFF; 1479 | ESP_LOGI("NFC_SETUP", "Firmware ver. %d.%d", maj, min); 1480 | nfc->SAMConfig(); 1481 | nfc->setRFField(0x02, 0x01); 1482 | nfc->setPassiveActivationRetries(0); 1483 | ESP_LOGI("NFC_SETUP", "Waiting for an ISO14443A card"); 1484 | vTaskResume(nfc_poll_task); 1485 | vTaskDelete(NULL); 1486 | return; 1487 | } 1488 | nfc->stop(); 1489 | vTaskDelay(50 / portTICK_PERIOD_MS); 1490 | } 1491 | } 1492 | 1493 | void nfc_thread_entry(void* arg) { 1494 | uint32_t versiondata = nfc->getFirmwareVersion(); 1495 | if (!versiondata) { 1496 | ESP_LOGE("NFC_SETUP", "Error establishing PN532 connection"); 1497 | nfc->stop(); 1498 | xTaskCreate(nfc_retry, "nfc_reconnect_task", 8192, NULL, 1, &nfc_reconnect_task); 1499 | vTaskSuspend(NULL); 1500 | } else { 1501 | unsigned int model = (versiondata >> 24) & 0xFF; 1502 | ESP_LOGI("NFC_SETUP", "Found chip PN5%x", model); 1503 | int maj = (versiondata >> 16) & 0xFF; 1504 | int min = (versiondata >> 8) & 0xFF; 1505 | ESP_LOGI("NFC_SETUP", "Firmware ver. %d.%d", maj, min); 1506 | nfc->SAMConfig(); 1507 | nfc->setRFField(0x02, 0x01); 1508 | nfc->setPassiveActivationRetries(0); 1509 | ESP_LOGI("NFC_SETUP", "Waiting for an ISO14443A card"); 1510 | } 1511 | memcpy(ecpData + 8, readerData.reader_gid.data(), readerData.reader_gid.size()); 1512 | with_crc16(ecpData, 16, ecpData + 16); 1513 | while (1) { 1514 | uint8_t res[4]; 1515 | uint16_t resLen = 4; 1516 | bool writeStatus = nfc->writeRegister(0x633d, 0, true); 1517 | if (!writeStatus) { 1518 | LOG(W, "writeRegister has failed, abandoning ship !!"); 1519 | nfc->stop(); 1520 | xTaskCreate(nfc_retry, "nfc_reconnect_task", 8192, NULL, 1, &nfc_reconnect_task); 1521 | vTaskSuspend(NULL); 1522 | } 1523 | nfc->inCommunicateThru(ecpData, sizeof(ecpData), res, &resLen, 100, true); 1524 | uint8_t uid[16]; 1525 | uint8_t uidLen = 0; 1526 | uint8_t atqa[2]; 1527 | uint8_t sak[1]; 1528 | bool passiveTarget = nfc->readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLen, atqa, sak, 500, true, true); 1529 | if (passiveTarget) { 1530 | nfc->setPassiveActivationRetries(5); 1531 | LOG(D, "ATQA: %02x", atqa[0]); 1532 | LOG(D, "SAK: %02x", sak[0]); 1533 | ESP_LOG_BUFFER_HEX_LEVEL(TAG, uid, (size_t)uidLen, ESP_LOG_VERBOSE); 1534 | LOG(I, "*** PASSIVE TARGET DETECTED ***"); 1535 | auto startTime = std::chrono::high_resolution_clock::now(); 1536 | uint8_t data[13] = { 0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x08, 0x58, 0x01, 0x01, 0x0 }; 1537 | uint8_t selectCmdRes[9]; 1538 | uint16_t selectCmdResLength = 9; 1539 | LOG(I, "Requesting supported HomeKey versions"); 1540 | LOG(D, "SELECT HomeKey Applet, APDU: "); 1541 | ESP_LOG_BUFFER_HEX_LEVEL(TAG, data, sizeof(data), ESP_LOG_VERBOSE); 1542 | bool status = nfc->inDataExchange(data, sizeof(data), selectCmdRes, &selectCmdResLength); 1543 | LOG(D, "SELECT HomeKey Applet, Response"); 1544 | ESP_LOG_BUFFER_HEX_LEVEL(TAG, selectCmdRes, selectCmdResLength, ESP_LOG_VERBOSE); 1545 | if (status && selectCmdRes[selectCmdResLength - 2] == 0x90 && selectCmdRes[selectCmdResLength - 1] == 0x00) { 1546 | LOG(D, "*** SELECT HOMEKEY APPLET SUCCESSFUL ***"); 1547 | LOG(D, "Reader Private Key: %s", red_log::bufToHexString(readerData.reader_pk.data(), readerData.reader_pk.size()).c_str()); 1548 | HKAuthenticationContext authCtx([](uint8_t* s, uint8_t l, uint8_t* r, uint16_t* rl, bool il) -> bool {return nfc->inDataExchange(s, l, r, rl, il);}, readerData, savedData); 1549 | auto authResult = authCtx.authenticate(hkFlow); 1550 | if (std::get<2>(authResult) != kFlowFailed) { 1551 | bool status = true; 1552 | if (espConfig::miscConfig.nfcSuccessPin != 255) { 1553 | xQueueSend(gpio_led_handle, &status, 0); 1554 | } 1555 | if (espConfig::miscConfig.nfcNeopixelPin != 255) { 1556 | xQueueSend(neopixel_handle, &status, 0); 1557 | } 1558 | if ((espConfig::miscConfig.gpioActionPin != 255 && espConfig::miscConfig.hkGpioControlledState) || espConfig::miscConfig.hkDumbSwitchMode) { 1559 | const gpioLockAction action{ .source = gpioLockAction::HOMEKEY, .action = 0 }; 1560 | xQueueSend(gpio_lock_handle, &action, 0); 1561 | } 1562 | if (espConfig::miscConfig.hkAltActionInitPin != 255 && espConfig::miscConfig.hkAltActionPin != 255) { 1563 | uint8_t status = 2; 1564 | xQueueSend(gpio_led_handle, &status, 0); 1565 | } 1566 | if (hkAltActionActive) { 1567 | mqtt_publish(espConfig::mqttData.hkAltActionTopic, "alt_action", 0, false); 1568 | } 1569 | json payload; 1570 | payload["issuerId"] = hex_representation(std::get<0>(authResult)); 1571 | payload["endpointId"] = hex_representation(std::get<1>(authResult)); 1572 | payload["readerId"] = hex_representation(readerData.reader_id); 1573 | payload["homekey"] = true; 1574 | std::string payloadStr = payload.dump(); 1575 | mqtt_publish(espConfig::mqttData.hkTopic, payloadStr, 0, false); 1576 | if (espConfig::miscConfig.lockAlwaysUnlock) { 1577 | if (espConfig::miscConfig.gpioActionPin == 255 || !espConfig::miscConfig.hkGpioControlledState) { 1578 | lockCurrentState->setVal(lockStates::UNLOCKED); 1579 | lockTargetState->setVal(lockStates::UNLOCKED); 1580 | mqtt_publish(espConfig::mqttData.lockStateTopic, std::to_string(lockStates::UNLOCKED), 1, true); 1581 | } 1582 | if (espConfig::mqttData.lockEnableCustomState) { 1583 | mqtt_publish(espConfig::mqttData.lockCustomStateTopic, std::to_string(espConfig::mqttData.customLockActions["UNLOCK"]), 0, false); 1584 | } 1585 | } else if (espConfig::miscConfig.lockAlwaysLock) { 1586 | if (espConfig::miscConfig.gpioActionPin == 255 || espConfig::miscConfig.hkGpioControlledState) { 1587 | lockCurrentState->setVal(lockStates::LOCKED); 1588 | lockTargetState->setVal(lockStates::LOCKED); 1589 | mqtt_publish(espConfig::mqttData.lockStateTopic, std::to_string(lockStates::LOCKED), 1, true); 1590 | } 1591 | if (espConfig::mqttData.lockEnableCustomState) { 1592 | mqtt_publish(espConfig::mqttData.lockCustomStateTopic, std::to_string(espConfig::mqttData.customLockActions["LOCK"]), 0, false); 1593 | } 1594 | } else { 1595 | int currentState = lockCurrentState->getVal(); 1596 | if (espConfig::mqttData.lockEnableCustomState) { 1597 | if (currentState == lockStates::UNLOCKED) { 1598 | mqtt_publish(espConfig::mqttData.lockCustomStateTopic, std::to_string(espConfig::mqttData.customLockActions["LOCK"]), 0, false); 1599 | } else if (currentState == lockStates::LOCKED) { 1600 | mqtt_publish(espConfig::mqttData.lockCustomStateTopic, std::to_string(espConfig::mqttData.customLockActions["UNLOCK"]), 0, false); 1601 | } 1602 | } 1603 | } 1604 | 1605 | auto stopTime = std::chrono::high_resolution_clock::now(); 1606 | LOG(I, "Total Time (detection->auth->gpio->mqtt): %lli ms", std::chrono::duration_cast(stopTime - startTime).count()); 1607 | } else { 1608 | bool status = false; 1609 | if (espConfig::miscConfig.nfcFailPin != 255) { 1610 | xQueueSend(gpio_led_handle, &status, 0); 1611 | } 1612 | if (espConfig::miscConfig.nfcNeopixelPin != 255) { 1613 | xQueueSend(neopixel_handle, &status, 0); 1614 | } 1615 | LOG(W, "We got status FlowFailed, mqtt untouched!"); 1616 | } 1617 | nfc->setRFField(0x02, 0x01); 1618 | } else if(!espConfig::mqttData.nfcTagNoPublish) { 1619 | LOG(W, "Invalid Response, probably not Homekey, publishing target's UID"); 1620 | bool status = false; 1621 | if (espConfig::miscConfig.nfcFailPin != 255) { 1622 | xQueueSend(gpio_led_handle, &status, 0); 1623 | } 1624 | if (espConfig::miscConfig.nfcNeopixelPin != 255) { 1625 | xQueueSend(neopixel_handle, &status, 0); 1626 | } 1627 | json payload; 1628 | payload["atqa"] = hex_representation(std::vector(atqa, atqa + 2)); 1629 | payload["sak"] = hex_representation(std::vector(sak, sak + 1)); 1630 | payload["uid"] = hex_representation(std::vector(uid, uid + uidLen)); 1631 | payload["homekey"] = false; 1632 | std::string payload_dump = payload.dump(); 1633 | if (client != nullptr) { 1634 | esp_mqtt_client_publish(client, espConfig::mqttData.hkTopic.c_str(), payload_dump.c_str(), 0, 0, false); 1635 | } else LOG(W, "MQTT Client not initialized, cannot publish message"); 1636 | } 1637 | vTaskDelay(50 / portTICK_PERIOD_MS); 1638 | nfc->inRelease(); 1639 | int counter = 50; 1640 | bool deviceStillInField = nfc->readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLen); 1641 | LOG(D, "Target still present: %d", deviceStillInField); 1642 | while (deviceStillInField) { 1643 | if (counter == 0) break; 1644 | vTaskDelay(50 / portTICK_PERIOD_MS); 1645 | nfc->inRelease(); 1646 | deviceStillInField = nfc->readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLen); 1647 | --counter; 1648 | LOG(D, "Target still present: %d Counter=%d", deviceStillInField, counter); 1649 | } 1650 | nfc->inRelease(); 1651 | nfc->setPassiveActivationRetries(0); 1652 | } 1653 | vTaskDelay(50 / portTICK_PERIOD_MS); 1654 | } 1655 | vTaskDelete(NULL); 1656 | return; 1657 | } 1658 | 1659 | void onEvent(arduino_event_id_t event, arduino_event_info_t info) { 1660 | uint8_t mac[6] = { 0, 0, 0, 0, 0, 0 }; 1661 | char macStr[13] = {0}; 1662 | switch (event) { 1663 | case ARDUINO_EVENT_ETH_START: 1664 | LOG(I, "ETH Started"); 1665 | ETH.macAddress(mac); 1666 | sprintf(macStr, "ESP32_%02X%02X%02X", mac[0], mac[1], mac[2]); 1667 | ETH.setHostname(macStr); 1668 | break; 1669 | case ARDUINO_EVENT_ETH_CONNECTED: LOG(I, "ETH Connected"); break; 1670 | case ARDUINO_EVENT_ETH_GOT_IP: LOG(I, "ETH Got IP: '%s'\n", esp_netif_get_desc(info.got_ip.esp_netif)); break; 1671 | case ARDUINO_EVENT_ETH_LOST_IP: 1672 | LOG(I, "ETH Lost IP"); 1673 | break; 1674 | case ARDUINO_EVENT_ETH_DISCONNECTED: 1675 | LOG(I, "ETH Disconnected"); 1676 | break; 1677 | case ARDUINO_EVENT_ETH_STOP: 1678 | LOG(I, "ETH Stopped"); 1679 | break; 1680 | default: break; 1681 | } 1682 | } 1683 | 1684 | void setup() { 1685 | Serial.begin(115200); 1686 | const esp_app_desc_t* app_desc = esp_app_get_description(); 1687 | std::string app_version = app_desc->version; 1688 | gpio_led_handle = xQueueCreate(2, sizeof(uint8_t)); 1689 | neopixel_handle = xQueueCreate(2, sizeof(uint8_t)); 1690 | gpio_lock_handle = xQueueCreate(2, sizeof(gpioLockAction)); 1691 | size_t len; 1692 | const char* TAG = "SETUP"; 1693 | nvs_open("SAVED_DATA", NVS_READWRITE, &savedData); 1694 | if (!nvs_get_blob(savedData, "READERDATA", NULL, &len)) { 1695 | std::vector savedBuf(len); 1696 | nvs_get_blob(savedData, "READERDATA", savedBuf.data(), &len); 1697 | LOG(D, "NVS READERDATA LENGTH: %d", len); 1698 | ESP_LOG_BUFFER_HEX_LEVEL(TAG, savedBuf.data(), savedBuf.size(), ESP_LOG_VERBOSE); 1699 | nlohmann::json data = nlohmann::json::from_msgpack(savedBuf); 1700 | if (!data.is_discarded()) { 1701 | data.get_to(readerData); 1702 | LOG(I, "Reader Data loaded from NVS"); 1703 | } 1704 | } 1705 | if (!nvs_get_blob(savedData, "MQTTDATA", NULL, &len)) { 1706 | std::vector dataBuf(len); 1707 | nvs_get_blob(savedData, "MQTTDATA", dataBuf.data(), &len); 1708 | LOG(D, "NVS MQTTDATA LENGTH: %d", len); 1709 | ESP_LOG_BUFFER_HEX_LEVEL(TAG, dataBuf.data(), dataBuf.size(), ESP_LOG_VERBOSE); 1710 | auto isValidJson = nlohmann::json::accept(dataBuf); 1711 | if (isValidJson) { 1712 | nlohmann::json data = nlohmann::json::parse(dataBuf); 1713 | if (!data.contains("lwtTopic") && data.contains("mqttClientId")) { 1714 | std::string lwt = data["mqttClientId"]; 1715 | lwt.append("/status"); 1716 | data["lwtTopic"] = lwt; 1717 | } 1718 | if (!data.is_discarded()) { 1719 | data.get_to(espConfig::mqttData); 1720 | LOG(I, "MQTT Config loaded from NVS"); 1721 | } 1722 | } else { 1723 | nlohmann::json data = nlohmann::json::from_msgpack(dataBuf); 1724 | if (!data.is_discarded()) { 1725 | data.get_to(espConfig::mqttData); 1726 | LOG(I, "MQTT Config loaded from NVS"); 1727 | } 1728 | } 1729 | } 1730 | if (!nvs_get_blob(savedData, "MISCDATA", NULL, &len)) { 1731 | std::vector dataBuf(len); 1732 | nvs_get_blob(savedData, "MISCDATA", dataBuf.data(), &len); 1733 | std::string str(dataBuf.begin(), dataBuf.end()); 1734 | LOG(D, "NVS MQTTDATA LENGTH: %d", len); 1735 | ESP_LOG_BUFFER_HEX_LEVEL(TAG, dataBuf.data(), dataBuf.size(), ESP_LOG_VERBOSE); 1736 | auto isValidJson = nlohmann::json::accept(dataBuf); 1737 | if (isValidJson) { 1738 | nlohmann::json data = nlohmann::json::parse(str); 1739 | if (!data.is_discarded()) { 1740 | data.get_to(espConfig::miscConfig); 1741 | LOG(I, "Misc Config loaded from NVS"); 1742 | } 1743 | } else { 1744 | nlohmann::json data = nlohmann::json::from_msgpack(dataBuf); 1745 | if (!data.is_discarded()) { 1746 | data.get_to(espConfig::miscConfig); 1747 | LOG(I, "Misc Config loaded from NVS"); 1748 | } 1749 | } 1750 | } 1751 | pn532spi = new PN532_SPI(espConfig::miscConfig.nfcGpioPins[0], espConfig::miscConfig.nfcGpioPins[1], espConfig::miscConfig.nfcGpioPins[2], espConfig::miscConfig.nfcGpioPins[3]); 1752 | nfc = new PN532(*pn532spi); 1753 | nfc->begin(); 1754 | if (espConfig::miscConfig.nfcSuccessPin && espConfig::miscConfig.nfcSuccessPin != 255) { 1755 | pinMode(espConfig::miscConfig.nfcSuccessPin, OUTPUT); 1756 | digitalWrite(espConfig::miscConfig.nfcSuccessPin, !espConfig::miscConfig.nfcSuccessHL); 1757 | } 1758 | if (espConfig::miscConfig.nfcFailPin && espConfig::miscConfig.nfcFailPin != 255) { 1759 | pinMode(espConfig::miscConfig.nfcFailPin, OUTPUT); 1760 | digitalWrite(espConfig::miscConfig.nfcFailPin, !espConfig::miscConfig.nfcFailHL); 1761 | } 1762 | if (espConfig::miscConfig.gpioActionPin && espConfig::miscConfig.gpioActionPin != 255) { 1763 | pinMode(espConfig::miscConfig.gpioActionPin, OUTPUT); 1764 | } 1765 | if (espConfig::miscConfig.hkAltActionInitPin != 255) { 1766 | pinMode(espConfig::miscConfig.hkAltActionInitPin, INPUT); 1767 | if (espConfig::miscConfig.hkAltActionPin != 255) { 1768 | pinMode(espConfig::miscConfig.hkAltActionPin, OUTPUT); 1769 | } 1770 | if (espConfig::miscConfig.hkAltActionInitLedPin != 255) { 1771 | pinMode(espConfig::miscConfig.hkAltActionInitLedPin, OUTPUT); 1772 | } 1773 | } 1774 | if (!LittleFS.begin(true)) { 1775 | LOG(I, "An Error has occurred while mounting LITTLEFS"); 1776 | return; 1777 | } 1778 | listDir(LittleFS, "/", 0); 1779 | LOG(I, "LittleFS used space: %d / %d", LittleFS.usedBytes(), LittleFS.totalBytes()); 1780 | if (espConfig::miscConfig.ethernetEnabled) { 1781 | Network.onEvent(onEvent); 1782 | if (espConfig::miscConfig.ethActivePreset != 255) { 1783 | if (espConfig::miscConfig.ethActivePreset >= eth_config_ns::boardPresets.size()) { 1784 | LOG(E, "Invalid preset index, not initializing ethernet!"); 1785 | } else { 1786 | eth_board_presets_t ethPreset = eth_config_ns::boardPresets[espConfig::miscConfig.ethActivePreset]; 1787 | if (!ethPreset.ethChip.emac) { 1788 | ETH.begin(ethPreset.ethChip.phy_type, 1, ethPreset.spi_conf.pin_cs, ethPreset.spi_conf.pin_irq, ethPreset.spi_conf.pin_rst, SPI2_HOST, ethPreset.spi_conf.pin_sck, ethPreset.spi_conf.pin_miso, ethPreset.spi_conf.pin_mosi, ethPreset.spi_conf.spi_freq_mhz); 1789 | } else { 1790 | #if CONFIG_ETH_USE_ESP32_EMAC 1791 | ETH.begin(ethPreset.ethChip.phy_type, ethPreset.rmii_conf.phy_addr, ethPreset.rmii_conf.pin_mcd, ethPreset.rmii_conf.pin_mdio, ethPreset.rmii_conf.pin_power, ethPreset.rmii_conf.pin_rmii_clock); 1792 | #else 1793 | LOG(E, "Selected a chip without MAC but %s doesn't have a builtin MAC, cannot initialize ethernet!", CONFIG_IDF_TARGET); 1794 | #endif 1795 | } 1796 | } 1797 | } else if (espConfig::miscConfig.ethActivePreset == 255) { 1798 | eth_chip_desc_t chipType = eth_config_ns::supportedChips[eth_phy_type_t(espConfig::miscConfig.ethPhyType)]; 1799 | if (!chipType.emac) { 1800 | ETH.begin(chipType.phy_type, 1, espConfig::miscConfig.ethSpiConfig[1], espConfig::miscConfig.ethSpiConfig[2], espConfig::miscConfig.ethSpiConfig[3], SPI2_HOST, espConfig::miscConfig.ethSpiConfig[4], espConfig::miscConfig.ethSpiConfig[5], espConfig::miscConfig.ethSpiConfig[6], espConfig::miscConfig.ethSpiConfig[0]); 1801 | } else { 1802 | #if CONFIG_ETH_USE_ESP32_EMAC 1803 | ETH.begin(chipType.phy_type, espConfig::miscConfig.ethRmiiConfig[0], espConfig::miscConfig.ethRmiiConfig[1], espConfig::miscConfig.ethRmiiConfig[2], espConfig::miscConfig.ethRmiiConfig[3], eth_clock_mode_t(espConfig::miscConfig.ethRmiiConfig[4])); 1804 | #endif 1805 | } 1806 | } 1807 | } 1808 | if (espConfig::miscConfig.controlPin != 255) { 1809 | homeSpan.setControlPin(espConfig::miscConfig.controlPin); 1810 | } 1811 | if (espConfig::miscConfig.hsStatusPin != 255) { 1812 | homeSpan.setStatusPin(espConfig::miscConfig.hsStatusPin); 1813 | } 1814 | homeSpan.setStatusAutoOff(15); 1815 | homeSpan.setLogLevel(0); 1816 | homeSpan.setSketchVersion(app_version.c_str()); 1817 | 1818 | LOG(I, "READER GROUP ID (%d): %s", readerData.reader_gid.size(), red_log::bufToHexString(readerData.reader_gid.data(), readerData.reader_gid.size()).c_str()); 1819 | LOG(I, "READER UNIQUE ID (%d): %s", readerData.reader_id.size(), red_log::bufToHexString(readerData.reader_id.data(), readerData.reader_id.size()).c_str()); 1820 | 1821 | LOG(I, "HOMEKEY ISSUERS: %d", readerData.issuers.size()); 1822 | for (auto&& issuer : readerData.issuers) { 1823 | LOG(D, "Issuer ID: %s, Public Key: %s", red_log::bufToHexString(issuer.issuer_id.data(), issuer.issuer_id.size()).c_str(), red_log::bufToHexString(issuer.issuer_pk.data(), issuer.issuer_pk.size()).c_str()); 1824 | } 1825 | homeSpan.enableAutoStartAP(); 1826 | homeSpan.enableOTA(espConfig::miscConfig.otaPasswd.c_str()); 1827 | homeSpan.setPortNum(1201); 1828 | uint8_t mac[6]; 1829 | esp_read_mac(mac, ESP_MAC_BT); 1830 | char macStr[9] = { 0 }; 1831 | sprintf(macStr, "%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3]); 1832 | homeSpan.setHostNameSuffix(macStr); 1833 | homeSpan.begin(Category::Locks, espConfig::miscConfig.deviceName.c_str(), "HK-", "HomeKey-ESP32"); 1834 | 1835 | new SpanUserCommand('D', "Delete Home Key Data", deleteReaderData); 1836 | new SpanUserCommand('L', "Set Log Level", setLogLevel); 1837 | new SpanUserCommand('F', "Set HomeKey Flow", setFlow); 1838 | new SpanUserCommand('P', "Print Issuers", print_issuers); 1839 | new SpanUserCommand('M', "Erase MQTT Config and restart", mqttConfigReset); 1840 | new SpanUserCommand('R', "Remove Endpoints", [](const char*) { 1841 | for (auto&& issuer : readerData.issuers) { 1842 | issuer.endpoints.clear(); 1843 | } 1844 | save_to_nvs(); 1845 | }); 1846 | new SpanUserCommand('N', "Btr status low", [](const char* arg) { 1847 | const char* TAG = "BTR_LOW"; 1848 | if (strncmp(arg + 1, "0", 1) == 0) { 1849 | statusLowBtr->setVal(0); 1850 | LOG(I, "Low status set to NORMAL"); 1851 | } else if (strncmp(arg + 1, "1", 1) == 0) { 1852 | statusLowBtr->setVal(1); 1853 | LOG(I, "Low status set to LOW"); 1854 | } 1855 | }); 1856 | new SpanUserCommand('B', "Btr level", [](const char* arg) { 1857 | uint8_t level = atoi(static_cast(arg + 1)); 1858 | btrLevel->setVal(level); 1859 | }); 1860 | 1861 | new SpanAccessory(); 1862 | new NFCAccessoryInformation(); 1863 | new Service::HAPProtocolInformation(); 1864 | new Characteristic::Version(); 1865 | new LockManagement(); 1866 | new LockMechanism(); 1867 | new NFCAccess(); 1868 | if (espConfig::miscConfig.proxBatEnabled) { 1869 | new PhysicalLockBattery(); 1870 | } 1871 | homeSpan.setControllerCallback(pairCallback); 1872 | homeSpan.setConnectionCallback(wifiCallback); 1873 | if (espConfig::miscConfig.nfcNeopixelPin != 255) { 1874 | pixel = std::make_shared(espConfig::miscConfig.nfcNeopixelPin, pixelTypeMap[espConfig::miscConfig.neoPixelType]); 1875 | xTaskCreate(neopixel_task, "neopixel_task", 4096, NULL, 2, &neopixel_task_handle); 1876 | } 1877 | if (espConfig::miscConfig.nfcSuccessPin != 255 || espConfig::miscConfig.nfcFailPin != 255) { 1878 | xTaskCreate(nfc_gpio_task, "nfc_gpio_task", 4096, NULL, 2, &gpio_led_task_handle); 1879 | } 1880 | if (espConfig::miscConfig.gpioActionPin != 255 || espConfig::miscConfig.hkDumbSwitchMode) { 1881 | xTaskCreate(gpio_task, "gpio_task", 4096, NULL, 2, &gpio_lock_task_handle); 1882 | } 1883 | if (espConfig::miscConfig.hkAltActionInitPin != 255) { 1884 | xTaskCreate(alt_action_task, "alt_action_task", 2048, NULL, 2, &alt_action_task_handle); 1885 | } 1886 | xTaskCreate(nfc_thread_entry, "nfc_task", 8192, NULL, 1, &nfc_poll_task); 1887 | } 1888 | 1889 | ////////////////////////////////////// 1890 | 1891 | void loop() { 1892 | homeSpan.poll(); 1893 | vTaskDelay(5); 1894 | } 1895 | -------------------------------------------------------------------------------- /sdkconfig.defaults: -------------------------------------------------------------------------------- 1 | CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y 2 | CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y 3 | CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y 4 | CONFIG_PARTITION_TABLE_CUSTOM=y 5 | CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="with_ota.csv" 6 | CONFIG_ASYNC_TCP_USE_WDT=n 7 | CONFIG_AUTOSTART_ARDUINO=y 8 | CONFIG_ARDUHAL_LOG_COLORS=y 9 | CONFIG_ARDUINO_SELECTIVE_COMPILATION=y 10 | CONFIG_ARDUINO_SELECTIVE_Wire=n 11 | CONFIG_ARDUINO_SELECTIVE_ESP_SR=n 12 | CONFIG_ARDUINO_SELECTIVE_EEPROM=n 13 | CONFIG_ARDUINO_SELECTIVE_Ticker=n 14 | CONFIG_ARDUINO_SELECTIVE_Zigbee=n 15 | CONFIG_ARDUINO_SELECTIVE_SD=n 16 | CONFIG_ARDUINO_SELECTIVE_SD_MMC=n 17 | CONFIG_ARDUINO_SELECTIVE_SPIFFS=n 18 | CONFIG_ARDUINO_SELECTIVE_FFat=n 19 | CONFIG_ARDUINO_SELECTIVE_PPP=n 20 | CONFIG_ARDUINO_SELECTIVE_HTTPClient=n 21 | CONFIG_ARDUINO_SELECTIVE_Matter=n 22 | CONFIG_ARDUINO_SELECTIVE_NetBIOS=n 23 | CONFIG_ARDUINO_SELECTIVE_WebServer=n 24 | CONFIG_ARDUINO_SELECTIVE_NetworkClientSecure=n 25 | CONFIG_ARDUINO_SELECTIVE_BLE=n 26 | CONFIG_ARDUINO_SELECTIVE_BluetoothSerial=n 27 | CONFIG_ARDUINO_SELECTIVE_RainMaker=n 28 | CONFIG_ARDUINO_SELECTIVE_OpenThread=n 29 | CONFIG_ARDUINO_SELECTIVE_Insights=n 30 | CONFIG_COMPILER_OPTIMIZATION_SIZE=y 31 | CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y 32 | CONFIG_ETH_SPI_ETHERNET_DM9051=y 33 | CONFIG_ETH_SPI_ETHERNET_W5500=y 34 | CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y 35 | CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y 36 | CONFIG_FREERTOS_HZ=1000 37 | CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE=y 38 | CONFIG_LWIP_MAX_SOCKETS=16 39 | CONFIG_MBEDTLS_PSK_MODES=y 40 | CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y 41 | CONFIG_MBEDTLS_HKDF_C=y 42 | CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED=y -------------------------------------------------------------------------------- /with_ota.csv: -------------------------------------------------------------------------------- 1 | # ESP-IDF Partition Table 2 | # Name, Type, SubType, Offset, Size, Flags 3 | nvs,data,nvs,,0x10000,, 4 | otadata,data,ota,,0x2000,, 5 | app0,app,ota_0,,0x1E0000,, 6 | app1,app,ota_1,,0x1E0000,, 7 | spiffs,data,spiffs,,0x20000,, --------------------------------------------------------------------------------