From 5a373262b7503e919c04fec81621ba887a2f95c0 Mon Sep 17 00:00:00 2001 From: ivmarkov Date: Fri, 26 Apr 2024 14:19:18 +0000 Subject: [PATCH] First commit --- .cargo/config.toml | 30 ++ .github/configs/sdkconfig.defaults | 35 ++ .github/workflows/ci.yml | 96 ++++ .github/workflows/publish-dry-run.yml | 22 + .github/workflows/publish.yml | 43 ++ .gitignore | 4 + CHANGELOG.md | 8 + Cargo.toml | 29 ++ LICENSE-APACHE | 201 ++++++++ LICENSE-MIT | 25 + README.md | 46 ++ build.rs | 4 + clippy.toml | 1 + espflash.toml | 1 + partitions.csv | 5 + src/ble.rs | 640 ++++++++++++++++++++++++++ src/lib.rs | 3 + src/nvs.rs | 85 ++++ 18 files changed, 1278 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .github/configs/sdkconfig.defaults create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish-dry-run.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 build.rs create mode 100644 clippy.toml create mode 100644 espflash.toml create mode 100644 partitions.csv create mode 100644 src/ble.rs create mode 100644 src/lib.rs create mode 100644 src/nvs.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..a8e4671 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,30 @@ +[build] +#target = "riscv32imc-esp-espidf" +target = "xtensa-esp32-espidf" + +[target.xtensa-esp32-espidf] +linker = "ldproxy" +rustflags = ["--cfg", "espidf_time64"] + +[target.xtensa-esp32s2-espidf] +linker = "ldproxy" +rustflags = ["--cfg", "espidf_time64"] + +[target.xtensa-esp32s3-espidf] +linker = "ldproxy" +rustflags = ["--cfg", "espidf_time64"] + +[target.riscv32imc-esp-espidf] +linker = "ldproxy" +rustflags = ["--cfg", "espidf_time64"] + +[target.riscv32imac-esp-espidf] +linker = "ldproxy" +rustflags = ["--cfg", "espidf_time64"] + +[env] +ESP_IDF_SDKCONFIG_DEFAULTS = ".github/configs/sdkconfig.defaults" +ESP_IDF_VERSION = "v5.1.2" + +[unstable] +build-std = ["std", "panic_abort"] diff --git a/.github/configs/sdkconfig.defaults b/.github/configs/sdkconfig.defaults new file mode 100644 index 0000000..a4cd52a --- /dev/null +++ b/.github/configs/sdkconfig.defaults @@ -0,0 +1,35 @@ +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y + +# Examples often require a larger than the default stack size for the main thread and for the sysloop event task. +CONFIG_ESP_MAIN_TASK_STACK_SIZE=20000 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=8192 +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=8192 + +# Go figure... +CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=4096 + +# Enable WS support +CONFIG_HTTPD_WS_SUPPORT=y + +# SPI Ethernet demo +CONFIG_ETH_SPI_ETHERNET_DM9051=y +CONFIG_ETH_SPI_ETHERNET_W5500=y +CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y + +## Uncomment to enable Classic BT +CONFIG_BT_ENABLED=y +CONFIG_BT_BLUEDROID_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=y +CONFIG_BTDM_CTRL_MODE_BLE_ONLY=n +CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y +CONFIG_BTDM_CTRL_MODE_BTDM=n +CONFIG_BT_A2DP_ENABLE=y +CONFIG_BT_HFP_ENABLE=y +CONFIG_BT_HFP_CLIENT_ENABLE=y +CONFIG_BT_HFP_AUDIO_DATA_PATH_HCI=y +CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y +CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y + +# Support for TLS with a pre-shared key. +#CONFIG_ESP_TLS_PSK_VERIFICATION=y diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..776602a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + schedule: + - cron: "50 6 * * *" + workflow_dispatch: + +env: + rust_toolchain: nightly + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + compile: + name: Compile + runs-on: ubuntu-latest + strategy: + matrix: + target: + - riscv32imc-esp-espidf + - xtensa-esp32-espidf + - xtensa-esp32s2-espidf + - xtensa-esp32s3-espidf + idf-version: + - v4.4.6 + - v5.1.2 + - v5.2 + steps: + - name: Setup | Checkout + uses: actions/checkout@v3 + + - name: Setup | Rust + if: matrix.target == 'riscv32imc-esp-espidf' + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.rust_toolchain }} + components: rustfmt, clippy, rust-src + + - name: Install Rust for Xtensa + if: matrix.target != 'riscv32imc-esp-espidf' + uses: esp-rs/xtensa-toolchain@v1.5.1 + with: + default: true + + - name: Build | Fmt Check + run: cargo fmt -- --check + + - name: Build | Clippy + env: + ESP_IDF_VERSION: ${{ matrix.idf-version }} + ESP_IDF_SDKCONFIG_DEFAULTS: "${{ github.workspace }}/.github/configs/sdkconfig.defaults" + RUSTFLAGS: "${{ startsWith(matrix.idf-version, 'v5') && '--cfg espidf_time64' || '' }}" + run: cargo clippy --features nightly,experimental --no-deps --target ${{ matrix.target }} -Zbuild-std=std,panic_abort -Zbuild-std-features=panic_immediate_abort -- -Dwarnings + + - name: Build | Compile + env: + ESP_IDF_VERSION: ${{ matrix.idf-version }} + ESP_IDF_SDKCONFIG_DEFAULTS: "${{ github.workspace }}/.github/configs/sdkconfig.defaults" + RUSTFLAGS: "${{ startsWith(matrix.idf-version, 'v5') && '--cfg espidf_time64' || '' }}" + run: cargo build --target ${{ matrix.target }} -Zbuild-std=std,panic_abort -Zbuild-std-features=panic_immediate_abort + + - name: Build | Compile, experimental, nightly, no_std + env: + ESP_IDF_VERSION: ${{ matrix.idf-version }} + ESP_IDF_SDKCONFIG_DEFAULTS: "${{ github.workspace }}/.github/configs/sdkconfig.defaults" + RUSTFLAGS: "${{ startsWith(matrix.idf-version, 'v5') && '--cfg espidf_time64' || '' }}" + run: cargo build --no-default-features --features nightly,experimental --target ${{ matrix.target }} -Zbuild-std=std,panic_abort -Zbuild-std-features=panic_immediate_abort + + - name: Build | Compile, experimental, nightly, alloc + env: + ESP_IDF_VERSION: ${{ matrix.idf-version }} + ESP_IDF_SDKCONFIG_DEFAULTS: "${{ github.workspace }}/.github/configs/sdkconfig.defaults" + RUSTFLAGS: "${{ startsWith(matrix.idf-version, 'v5') && '--cfg espidf_time64' || '' }}" + run: cargo build --no-default-features --features nightly,experimental,alloc --target ${{ matrix.target }} -Zbuild-std=std,panic_abort -Zbuild-std-features=panic_immediate_abort + + - name: Setup | ldproxy + if: matrix.target == 'riscv32imc-esp-espidf' + run: | + curl -L https://github.com/esp-rs/embuild/releases/latest/download/ldproxy-x86_64-unknown-linux-gnu.zip -o $HOME/.cargo/bin/ldproxy.zip + unzip "$HOME/.cargo/bin/ldproxy.zip" -d "$HOME/.cargo/bin/" + chmod a+x $HOME/.cargo/bin/ldproxy + + - name: Build | Examples + env: + ESP_IDF_VERSION: ${{ matrix.idf-version }} + ESP_IDF_SDKCONFIG_DEFAULTS: "${{ github.workspace }}/.github/configs/sdkconfig.defaults" + RUSTFLAGS: "${{ startsWith(matrix.idf-version, 'v5') && '--cfg espidf_time64' || '' }} ${{ '-C default-linker-libraries' }}" + WIFI_SSID: "ssid" + WIFI_PASS: "pass" + ESP_DEVICE_IP: "192.168.1.250" + GATEWAY_IP: "192.168.1.1" + GATEWAY_NETMASK: "24" + run: cargo build --examples --target ${{ matrix.target }} -Zbuild-std=std,panic_abort -Zbuild-std-features=panic_immediate_abort diff --git a/.github/workflows/publish-dry-run.yml b/.github/workflows/publish-dry-run.yml new file mode 100644 index 0000000..04083dd --- /dev/null +++ b/.github/workflows/publish-dry-run.yml @@ -0,0 +1,22 @@ +name: PublishDryRun + +on: workflow_dispatch + +env: + rust_toolchain: nightly + +jobs: + publishdryrun: + name: Publish Dry Run + runs-on: ubuntu-latest + steps: + - name: Setup | Checkout + uses: actions/checkout@v3 + + - name: Setup | Rust + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.rust_toolchain }} + components: rust-src + - name: Build | Publish Dry Run + run: export ESP_IDF_TOOLS_INSTALL_DIR=out; export ESP_IDF_SDKCONFIG_DEFAULTS=$(pwd)/.github/configs/sdkconfig.defaults; cargo publish --dry-run --target riscv32imc-esp-espidf -Zbuild-std=std,panic_abort -Zbuild-std-features=panic_immediate_abort diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..8fba1cb --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,43 @@ +name: Publish + +on: workflow_dispatch + +env: + rust_toolchain: nightly + CRATE_NAME: esp-idf-svc + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + steps: + - name: Setup | Checkout + uses: actions/checkout@v3 + - name: Setup | Rust + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.rust_toolchain }} + components: rust-src + - name: Login + run: cargo login ${{ secrets.crates_io_token }} + - name: Build | Publish + run: export ESP_IDF_TOOLS_INSTALL_DIR=out; export ESP_IDF_SDKCONFIG_DEFAULTS=$(pwd)/.github/configs/sdkconfig.defaults; cargo publish --target riscv32imc-esp-espidf -Zbuild-std=std,panic_abort -Zbuild-std-features=panic_immediate_abort + - name: Build Documentation + run: cargo doc --target riscv32imc-esp-espidf -Zbuild-std=std,panic_abort -Zbuild-std-features=panic_immediate_abort; echo "" > target/riscv32imc-esp-espidf/doc/index.html; mv target/riscv32imc-esp-espidf/doc ./docs + - name: Deploy Documentation + if: ${{ github.ref == 'refs/heads/master' }} + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + force_orphan: true + publish_dir: ./docs + - name: Get the crate version from cargo + run: | + version=$(cargo metadata --format-version=1 --no-deps | jq -r ".packages[] | select(.name == \"${{env.CRATE_NAME}}\") | .version") + echo "crate_version=$version" >> $GITHUB_ENV + echo "${{env.CRATE_NAME}} version: $version" + - name: Tag the new release + uses: rickstaa/action-create-tag@v1 + with: + tag: v${{env.crate_version}} + message: "Release v${{env.crate_version}}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73a638b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.vscode +/.embuild +/target +/Cargo.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ceb2dd4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [?.??.?] - ????-??-?? diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c5c43b6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "esp-idf-matter" +version = "0.1.0" +authors = ["ivmarkov "] +edition = "2021" +resolver = "2" +categories = ["embedded", "hardware-support"] +keywords = ["embedded", "svc", "idf", "esp-idf", "esp32"] +description = "Implementation of the embedded-svc traits for ESP-IDF (Espressif's IoT Development Framework)" +repository = "https://github.com/ivmarkov/esp-idf-matter" +license = "MIT OR Apache-2.0" +readme = "README.md" +build = "build.rs" +#documentation = "https://docs.esp-rs.org/esp-idf-svc/" +rust-version = "1.77" + +[patch.crates-io] +esp-idf-svc = { path = "../esp-idf-svc" } +rs-matter = { path = "../rs-matter" } + +[features] + +[dependencies] +log = { version = "0.4", default-features = false } +esp-idf-svc = { version = "0.48", default-features = false, fatures = ["experimental"] } +rs-matter = { version = "0.1", default-features = false } + +[build-dependencies] +embuild = "0.31.3" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..f8e5e5e --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..b03fd3c --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright 2019-2020 Contributors to xtensa-lx6-rt + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e2fae8 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Run [rs-matter](https://github.com/project-chip/rs-matter) on top of the [Rust ESP IDF SDK wrappers](https://github.com/esp-rs/esp-idf-svc) + +[![CI](https://github.com/ivmarkov/esp-idf-matter/actions/workflows/ci.yml/badge.svg)](https://github.com/ivmarkov/esp-idf-matter/actions/workflows/ci.yml) +[![crates.io](https://img.shields.io/crates/v/esp-idf-matter.svg)](https://crates.io/crates/esp-idf-matter) +[![Documentation](https://img.shields.io/badge/docs-esp--rs-brightgreen)](https://ivmarkov.github.io/esp-idf-matter/esp_idf_matter/index.html) +[![Matrix](https://img.shields.io/matrix/ivmarkov:matrix.org?label=join%20matrix&color=BEC5C9&logo=matrix)](https://matrix.to/#/#esp-rs:matrix.org) +[![Wokwi](https://img.shields.io/endpoint?url=https%3A%2F%2Fwokwi.com%2Fbadge%2Fclick-to-simulate.json)](https://wokwi.com/projects/332188235906155092) + +## Highlights + +This boring crate provides the necessary glue to operate the [`rs-matter` Rust Matter stack]() on Espressif chips with the ESP IDF SDK. + +In particular: +* [Bluetooth commissioning support]() with the ESP IDF Bluedroid stack +* WiFi provisioning support via an [ESP IDF specific Matter Network Commissioning Cluster implementation]() +* [Non-volatile storage for Matter data (fabrics and ACLs)]() on top of the ESP IDF NVS flash API +* mDNS: + * Optional [Matter mDNS responder implementation]() based on the ESP IDF mDNS responder (use if you need to register other services besides Matter in mDNS) + * [UDP-multicast workarounds]() for `rs-matter`'s built-in mDNS responder, specifc to the Rust wrappers of ESP IDF + +For enabling UDP and TCP networking in `rs-matter`, just use the [`async-io`]() crate which has support for ESP IDF out of the box. + +## Build Prerequisites + +Follow the [Prerequisites](https://github.com/esp-rs/esp-idf-template#prerequisites) section in the `esp-idf-template` crate. + +## Examples + +The examples could be built and flashed conveniently with [`cargo-espflash`](https://github.com/esp-rs/espflash/). To run e.g. `wifi` on an e.g. ESP32-C3: +(Swap the Rust target and example name with the target corresponding for your ESP32 MCU and with the example you would like to build) + +with `cargo-espflash`: +```sh +$ MCU=esp32c3 cargo espflash flash --target riscv32imc-esp-espidf --example wifi --monitor +``` + +| MCU | "--target" | +| --- | ------ | +| esp32c2 | riscv32imc-esp-espidf | +| esp32c3| riscv32imc-esp-espidf | +| esp32c6| riscv32imac-esp-espidf | +| esp32h2 | riscv32imac-esp-espidf | +| esp32p4 | riscv32imafc-esp-espidf | +| esp32 | xtensa-esp32-espidf | +| esp32s2 | xtensa-esp32s2-espidf | +| esp32s3 | xtensa-esp32s3-espidf | diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f0f0a39 --- /dev/null +++ b/build.rs @@ -0,0 +1,4 @@ +fn main() { + embuild::espidf::sysenv::relay(); + embuild::espidf::sysenv::output(); // Only necessary for building the examples +} diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..c495986 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +future-size-threshold = 250 diff --git a/espflash.toml b/espflash.toml new file mode 100644 index 0000000..d22b92e --- /dev/null +++ b/espflash.toml @@ -0,0 +1 @@ +partition_table = "partitions.csv" diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..924878b --- /dev/null +++ b/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, , 0x6000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 3M, \ No newline at end of file diff --git a/src/ble.rs b/src/ble.rs new file mode 100644 index 0000000..8dbde01 --- /dev/null +++ b/src/ble.rs @@ -0,0 +1,640 @@ +use core::borrow::Borrow; +use core::cell::RefCell; + +use alloc::sync::Arc; + +use embassy_sync::blocking_mutex::Mutex; + +use enumset::enum_set; + +use esp_idf_svc::bt::ble::gap::{BleGapEvent, EspBleGap}; +use esp_idf_svc::bt::ble::gatt::server::{ConnectionId, EspGatts, GattsEvent, TransferId}; +use esp_idf_svc::bt::ble::gatt::{ + AutoResponse, GattCharacteristic, GattDescriptor, GattId, GattInterface, GattResponse, + GattServiceId, GattStatus, Handle, Permission, Property, +}; +use esp_idf_svc::bt::{BdAddr, BleEnabled, BtDriver, BtStatus, BtUuid}; +use esp_idf_svc::sys::{EspError, ESP_FAIL}; + +use log::{info, trace, warn}; + +use rs_matter::error::{Error, ErrorCode}; +use rs_matter::transport::network::btp::{ + AdvData, GattPeripheral, GattPeripheralEvent, C1_CHARACTERISTIC_UUID, C1_MAX_LEN, + C2_CHARACTERISTIC_UUID, C2_MAX_LEN, MATTER_BLE_SERVICE_UUID16, MAX_BTP_SESSIONS, +}; +use rs_matter::transport::network::BtAddr; +use rs_matter::utils::std_mutex::StdRawMutex; + +const MAX_CONNECTIONS: usize = MAX_BTP_SESSIONS; + +struct ConnState { + peer: BdAddr, + conn_id: Handle, + subscribed: bool, + mtu: Option, +} + +struct GattsState { + gatt_if: Option, + service_handle: Option, + c1_handle: Option, + c2_handle: Option, + c2_cccd_handle: Option, + connections: heapless::Vec, +} + +/// Implements the `GattPeripheral` trait using the ESP-IDF Bluedroid GATT stack. +struct PeripheralState<'d, M, T> +where + T: Borrow>, + M: BleEnabled, +{ + gap: EspBleGap<'d, M, T>, + gatts: EspGatts<'d, M, T>, + state: Mutex>, +} + +impl<'d, M, T> PeripheralState<'d, M, T> +where + T: Borrow> + Clone, + M: BleEnabled, +{ + fn new(driver: T) -> Result { + Ok(Self { + gap: EspBleGap::new(driver.clone())?, + gatts: EspGatts::new(driver)?, + state: Mutex::new(RefCell::new(GattsState { + gatt_if: None, + service_handle: None, + c1_handle: None, + c2_handle: None, + c2_cccd_handle: None, + connections: heapless::Vec::new(), + })), + }) + } + + fn indicate(&self, data: &[u8], address: BtAddr) -> Result { + let conn = self.state.lock(|state| { + let state = state.borrow(); + + let Some(gatts_if) = state.gatt_if else { + return None; + }; + + let Some(c2_handle) = state.c2_handle else { + return None; + }; + + let Some(conn) = state + .connections + .iter() + .find(|conn| conn.peer.addr() == address.0 && conn.subscribed) + else { + return None; + }; + + Some((gatts_if, conn.conn_id, c2_handle)) + }); + + if let Some((gatts_if, conn_id, attr_handle)) = conn { + self.gatts.indicate(gatts_if, conn_id, attr_handle, data)?; + + Ok(true) + } else { + Ok(false) + } + } + + fn on_gap_event(&self, event: BleGapEvent) -> Result<(), EspError> { + match event { + BleGapEvent::RawAdvertisingConfigured(status) => { + self.check_bt_status(status)?; + self.gap.start_advertising()?; + } + _ => (), + } + + Ok(()) + } + + fn on_gatts_event( + &self, + service_name: &str, + service_adv_data: &AdvData, + gatt_if: GattInterface, + event: GattsEvent, + mut callback: F, + ) -> Result<(), EspError> + where + F: FnMut(GattPeripheralEvent), + { + match event { + GattsEvent::ServiceRegistered { status, .. } => { + self.check_gatt_status(status)?; + self.create_service(gatt_if, service_name, service_adv_data)?; + } + GattsEvent::ServiceCreated { + status, + service_handle, + .. + } => { + self.check_gatt_status(status)?; + self.configure_and_start_service(service_handle)?; + } + GattsEvent::CharacteristicAdded { + status, + attr_handle, + service_handle, + char_uuid, + } => { + self.check_gatt_status(status)?; + self.register_characteristic(service_handle, attr_handle, char_uuid)?; + } + GattsEvent::DescriptorAdded { + status, + attr_handle, + service_handle, + descr_uuid, + } => { + self.check_gatt_status(status)?; + self.register_cccd_descriptor(service_handle, attr_handle, descr_uuid)?; + } + GattsEvent::ServiceDeleted { + status, + service_handle, + } => { + self.check_gatt_status(status)?; + self.delete_service(service_handle)?; + } + GattsEvent::ServiceUnregistered { + status, + service_handle, + .. + } => { + self.check_gatt_status(status)?; + self.unregister_service(service_handle)?; + } + GattsEvent::Mtu { conn_id, mtu } => { + self.register_conn_mtu(conn_id, mtu)?; + } + GattsEvent::PeerConnected { conn_id, addr, .. } => { + self.create_conn(conn_id, addr, &mut callback)?; + } + GattsEvent::PeerDisconnected { addr, .. } => { + self.delete_conn(addr, &mut callback)?; + } + GattsEvent::Write { + conn_id, + trans_id, + addr, + handle, + offset, + need_rsp, + is_prep, + value, + } => { + self.write( + gatt_if, + conn_id, + trans_id, + addr, + handle, + offset, + need_rsp, + is_prep, + value, + &mut callback, + )?; + } + _ => (), + } + + Ok(()) + } + + fn check_esp_status(&self, status: Result<(), EspError>) { + if let Err(e) = status { + warn!("Got status: {:?}", e); + } + } + + fn check_bt_status(&self, status: BtStatus) -> Result<(), EspError> { + if !matches!(status, BtStatus::Success) { + warn!("Got status: {:?}", status); + Err(EspError::from_infallible::()) + } else { + Ok(()) + } + } + + fn check_gatt_status(&self, status: GattStatus) -> Result<(), EspError> { + if !matches!(status, GattStatus::Ok) { + warn!("Got status: {:?}", status); + Err(EspError::from_infallible::()) + } else { + Ok(()) + } + } + + fn create_service( + &self, + gatt_if: GattInterface, + service_name: &str, + service_adv_data: &AdvData, + ) -> Result<(), EspError> { + self.state.lock(|state| { + state.borrow_mut().gatt_if = Some(gatt_if); + }); + + self.gap.set_device_name(service_name)?; + self.gap.set_raw_adv_conf( + &service_adv_data + .service_payload_iter() + .collect::>(), + )?; + self.gatts.create_service( + gatt_if, + &GattServiceId { + id: GattId { + uuid: BtUuid::uuid16(MATTER_BLE_SERVICE_UUID16), + inst_id: 0, + }, + is_primary: true, + }, + 8, + )?; + + Ok(()) + } + + fn delete_service(&self, service_handle: Handle) -> Result<(), EspError> { + self.state.lock(|state| { + if state.borrow().service_handle == Some(service_handle) { + state.borrow_mut().c1_handle = None; + state.borrow_mut().c2_handle = None; + state.borrow_mut().c2_cccd_handle = None; + } + }); + + Ok(()) + } + + fn unregister_service(&self, service_handle: Handle) -> Result<(), EspError> { + self.state.lock(|state| { + if state.borrow().service_handle == Some(service_handle) { + state.borrow_mut().gatt_if = None; + state.borrow_mut().service_handle = None; + } + }); + + Ok(()) + } + + fn configure_and_start_service(&self, service_handle: Handle) -> Result<(), EspError> { + self.state.lock(|state| { + state.borrow_mut().service_handle = Some(service_handle); + }); + + self.gatts.start_service(service_handle)?; + self.add_characteristics(service_handle)?; + + Ok(()) + } + + fn add_characteristics(&self, service_handle: Handle) -> Result<(), EspError> { + self.gatts.add_characteristic( + service_handle, + &GattCharacteristic { + uuid: BtUuid::uuid128(C1_CHARACTERISTIC_UUID), + permissions: enum_set!(Permission::Write), + properties: enum_set!(Property::Write), + max_len: C1_MAX_LEN, + auto_rsp: AutoResponse::ByGatt, + }, + &[], + )?; + + self.gatts.add_characteristic( + service_handle, + &GattCharacteristic { + uuid: BtUuid::uuid128(C2_CHARACTERISTIC_UUID), + permissions: enum_set!(Permission::Write | Permission::Read), + properties: enum_set!(Property::Indicate), + max_len: C2_MAX_LEN, + auto_rsp: AutoResponse::ByGatt, + }, + &[], + )?; + + Ok(()) + } + + fn register_characteristic( + &self, + service_handle: Handle, + attr_handle: Handle, + char_uuid: BtUuid, + ) -> Result<(), EspError> { + let c2 = self.state.lock(|state| { + if state.borrow().service_handle != Some(service_handle) { + return false; + } + + if char_uuid == BtUuid::uuid128(C1_CHARACTERISTIC_UUID) { + state.borrow_mut().c1_handle = Some(attr_handle); + + false + } else if char_uuid == BtUuid::uuid128(C2_CHARACTERISTIC_UUID) { + state.borrow_mut().c2_handle = Some(attr_handle); + + true + } else { + false + } + }); + + if c2 { + self.gatts.add_descriptor( + attr_handle, + &GattDescriptor { + uuid: BtUuid::uuid16(0x2902), // CCCD + permissions: enum_set!(Permission::Read | Permission::Write), + }, + )?; + } + + Ok(()) + } + + fn register_cccd_descriptor( + &self, + service_handle: Handle, + attr_handle: Handle, + descr_uuid: BtUuid, + ) -> Result<(), EspError> { + self.state.lock(|state| { + if descr_uuid == BtUuid::uuid16(0x2902) && state.borrow().c2_handle == Some(attr_handle) + { + state.borrow_mut().c2_cccd_handle = Some(service_handle); + } + }); + + Ok(()) + } + + fn register_conn_mtu(&self, conn_id: ConnectionId, mtu: u16) -> Result<(), EspError> { + self.state.lock(|state| { + let mut state = state.borrow_mut(); + if let Some(conn) = state + .connections + .iter_mut() + .find(|conn| conn.conn_id == conn_id) + { + conn.mtu = Some(mtu); + } + }); + + Ok(()) + } + + fn create_conn( + &self, + conn_id: ConnectionId, + addr: BdAddr, + callback: &mut F, + ) -> Result<(), EspError> + where + F: FnMut(GattPeripheralEvent), + { + let added = self.state.lock(|state| { + let mut state = state.borrow_mut(); + if state.connections.len() < MAX_CONNECTIONS { + state + .connections + .push(ConnState { + peer: addr, + conn_id, + subscribed: false, + mtu: None, + }) + .map_err(|_| ()) + .unwrap(); + + true + } else { + false + } + }); + + if added { + self.gap.set_conn_params_conf(addr, 10, 20, 0, 400)?; + + callback(GattPeripheralEvent::NotifySubscribed(BtAddr(addr.into()))); + } + + Ok(()) + } + + fn delete_conn(&self, addr: BdAddr, callback: &mut F) -> Result<(), EspError> + where + F: FnMut(GattPeripheralEvent), + { + self.state.lock(|state| { + let mut state = state.borrow_mut(); + if let Some(index) = state + .connections + .iter() + .position(|ConnState { peer, .. }| *peer == addr) + { + state.connections.swap_remove(index); + } + }); + + callback(GattPeripheralEvent::NotifyUnsubscribed(BtAddr(addr.into()))); + + Ok(()) + } + + fn write( + &self, + gatt_if: GattInterface, + conn_id: ConnectionId, + trans_id: TransferId, + addr: BdAddr, + handle: Handle, + offset: u16, + need_rsp: bool, + is_prep: bool, + value: &[u8], + callback: &mut F, + ) -> Result<(), EspError> + where + F: FnMut(GattPeripheralEvent), + { + let event = self.state.lock(|state| { + let mut state = state.borrow_mut(); + let c2_handle = state.c2_handle; + let c2_cccd_handle = state.c2_cccd_handle; + + let Some(conn) = state + .connections + .iter_mut() + .find(|conn| conn.conn_id == conn_id) + else { + return None; + }; + + if c2_cccd_handle == Some(handle) { + // TODO: What if this needs a response? + + if !is_prep && offset == 0 && value.len() == 2 { + let value = u16::from_le_bytes([value[0], value[1]]); + if value == 0x02 { + if !conn.subscribed { + conn.subscribed = true; + return Some(GattPeripheralEvent::NotifySubscribed(BtAddr( + addr.into(), + ))); + } + } else { + if conn.subscribed { + conn.subscribed = false; + return Some(GattPeripheralEvent::NotifyUnsubscribed(BtAddr( + addr.into(), + ))); + } + } + } + } else if c2_handle == Some(handle) { + if offset == 0 { + // TODO: Is it safe to report the write before it was confirmed? + return Some(GattPeripheralEvent::Write { + address: BtAddr(addr.into()), + data: value, + }); + } + } + + None + }); + + if let Some(event) = event { + if matches!(event, GattPeripheralEvent::Write { .. }) { + if need_rsp { + let response = if is_prep { + // TODO: Do not allocate on-stack + let mut response = GattResponse::new(); + + response + .attr_handle(handle) + .auth_req(0) + .offset(0) + .value(value)?; + + Some(response) + } else { + None + }; + + self.gatts.send_response( + gatt_if, + conn_id, + trans_id, + GattStatus::Ok, + response.as_ref(), + )?; + } + } + + callback(event); + } + + Ok(()) + } +} + +pub struct BluedroidGattPeripheral(Arc>) +where + T: Borrow>, + M: BleEnabled; + +impl BluedroidGattPeripheral +where + T: Borrow> + Clone, + M: BleEnabled, +{ + /// Create a new instance. + pub fn new(driver: T) -> Result { + Ok(Self(Arc::new(PeripheralState::new(driver)?))) + } + + pub fn run( + &self, + service_name: &str, + service_adv_data: &AdvData, + mut callback: F, + ) -> Result<(), EspError> + where + F: FnMut(GattPeripheralEvent) + Send + Clone + 'static, + T: Send + 'static, + M: BleEnabled + 'static, + { + let _pin = service_adv_data.pin(); + + let gap_state = self.0.clone(); + let gatts_state = self.0.clone(); + + self.0.gap.subscribe(move |event| { + gap_state.check_esp_status(gap_state.on_gap_event(event)); + })?; + + let adv_data = service_adv_data.clone(); + let service_name = service_name.to_owned(); + + self.0.gatts.subscribe(move |(gatt_if, event)| { + gatts_state.check_esp_status(gatts_state.on_gatts_event( + &service_name, + &adv_data, + gatt_if, + event, + &mut callback, + )) + })?; + + Ok(()) + } + + /// Indicate new data on characteristic `C2` to a remote peer. + pub fn indicate(&self, data: &[u8], address: BtAddr) -> Result { + self.0.indicate(data, address) + } +} + +impl GattPeripheral for BluedroidGattPeripheral +where + T: Borrow> + Clone + Send + 'static, + M: BleEnabled + 'static, +{ + async fn run(&self, service_name: &str, adv_data: &AdvData, callback: F) -> Result<(), Error> + where + F: FnMut(GattPeripheralEvent) + Send + Clone + 'static, + { + BluedroidGattPeripheral::run(self, service_name, adv_data, callback) + .map_err(|_| ErrorCode::BtpError)?; + + core::future::pending().await + } + + async fn indicate(&self, data: &[u8], address: BtAddr) -> Result<(), Error> { + // TODO: Is indicate blocking? + if BluedroidGattPeripheral::indicate(self, data, address) + .map_err(|_| ErrorCode::BtpError)? + { + Ok(()) + } else { + Err(ErrorCode::NoNetworkInterface.into()) + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b9b47d6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ + +pub mod ble; +pub mod nvs; diff --git a/src/nvs.rs b/src/nvs.rs new file mode 100644 index 0000000..45b1f13 --- /dev/null +++ b/src/nvs.rs @@ -0,0 +1,85 @@ +use esp_idf_svc::{nvs::{EspNvs, NvsPartitionId}, sys::EspError}; + +use log::info; + +use rs_matter::Matter; + +pub struct Psm<'a, T> +where + T: NvsPartitionId, +{ + matter: &'a Matter<'a>, + nvs: EspNvs, +} + +impl<'a, T> Psm<'a, T> +where + T: NvsPartitionId, +{ + #[inline(always)] + pub fn new(matter: &'a Matter<'a>, nvs: EspNvs, buf: &mut [u8]) -> Result { + Ok(Self { + matter, + nvs, + }) + } + + pub async fn run(&mut self) -> Result<(), EspError> { + loop { + self.matter.wait_changed().await; + + if self.matter.is_changed() { + if let Some(data) = self.matter.store_acls(&mut self.buf)? { + Self::store(&self.dir, "acls", data)?; + } + + if let Some(data) = self.matter.store_fabrics(&mut self.buf)? { + Self::store(&self.dir, "fabrics", data)?; + } + } + } + } + + fn load<'b>(dir: &Path, key: &str, buf: &'b mut [u8]) -> Result, EspError> { + let path = dir.join(key); + + match fs::File::open(path) { + Ok(mut file) => { + let mut offset = 0; + + loop { + if offset == buf.len() { + Err(ErrorCode::NoSpace)?; + } + + let len = file.read(&mut buf[offset..])?; + + if len == 0 { + break; + } + + offset += len; + } + + let data = &buf[..offset]; + + info!("Key {}: loaded {} bytes {:?}", key, data.len(), data); + + Ok(Some(data)) + } + Err(_) => Ok(None), + } + } + + fn store(dir: &Path, key: &str, data: &[u8]) -> Result<(), EspError> { + let path = dir.join(key); + + let mut file = fs::File::create(path)?; + + file.write_all(data)?; + + info!("Key {}: stored {} bytes {:?}", key, data.len(), data); + + Ok(()) + } +}