├── .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 | 
2 |
3 | # HomeKey-ESP32 [](https://discord.com/invite/VWpZ5YyUcm) [](https://github.com/rednblkx/HomeKey-ESP32/actions/workflows/esp32.yml)
4 |
5 | ### HomeKey functionality for the rest of us.
6 |
7 | [](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 |
--------------------------------------------------------------------------------
/data/routes/hkinfo.html:
--------------------------------------------------------------------------------
1 | HomeKey Info
2 |
--------------------------------------------------------------------------------
/data/routes/misc.html:
--------------------------------------------------------------------------------
1 | Miscellaneous
2 | Changes in this section will reboot the device
3 |
--------------------------------------------------------------------------------
/data/routes/mqtt.html:
--------------------------------------------------------------------------------
1 | MQTT Configuration
2 | Changes in this section will reboot the device
3 |
--------------------------------------------------------------------------------
/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,,
--------------------------------------------------------------------------------