├── .cargo └── config.toml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── ci.yml │ ├── publish-dry-run.yml.disabled │ ├── publish.yml │ └── security-audit.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── examples ├── gatt_secure_server.rs └── gatt_server.rs ├── partitions.csv ├── rust-toolchain.toml ├── sdkconfig.defaults ├── sdkconfig.esp32 ├── sdkconfig.esp32c3 ├── sdkconfig.esp32s3 └── src ├── advertise.rs ├── gap.rs ├── gatt.rs ├── gatt_client.rs ├── gatt_server.rs ├── lib.rs └── security.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Uncomment the relevant target for your chip here (ESP32, ESP32-S2, ESP32-S3 or ESP32-C3) 3 | target = "xtensa-esp32-espidf" 4 | #target = "xtensa-esp32s2-espidf" 5 | #target = "xtensa-esp32s3-espidf" 6 | #target = "riscv32imc-esp-espidf" 7 | 8 | [target.xtensa-esp32-espidf] 9 | linker = "ldproxy" 10 | 11 | [target.xtensa-esp32s2-espidf] 12 | linker = "ldproxy" 13 | 14 | [target.xtensa-esp32s3-espidf] 15 | linker = "ldproxy" 16 | 17 | [target.riscv32imc-esp-espidf] 18 | linker = "ldproxy" 19 | 20 | # Future - necessary for the experimental "native build" of esp-idf-sys with ESP32C3 21 | # See also https://github.com/ivmarkov/embuild/issues/16 22 | rustflags = ["-C", "default-linker-libraries"] 23 | 24 | [unstable] 25 | 26 | build-std = ["std", "panic_abort"] 27 | #build-std-features = ["panic_immediate_abort"] # Required for older ESP-IDF versions without a realpath implementation 28 | 29 | [env] 30 | # === PIO builder === 31 | # These configurations will pick up your custom "sdkconfig.release", "sdkconfig.debug" or "sdkconfig.defaults[.*]" files 32 | # that you might put in the root of the project 33 | # The easiest way to generate a full "sdkconfig[.release|debug]" configuration (as opposed to manually enabling only the necessary flags via "sdkconfig.defaults[.*]" 34 | # is by running "cargo pio espidf menuconfig" 35 | ESP_IDF_SYS_GLOB_BASE = { value = ".", relative = true } 36 | ESP_IDF_SYS_GLOB_0 = { value = "/sdkconfig.*" } 37 | 38 | # === Native builder === 39 | # Same thing but for the native build. Currently the native and PIO builds disagree how sdkconfig configuration should be passed to the ESP-IDF build 40 | # See https://github.com/esp-rs/esp-idf-sys/issues/10#issuecomment-919022205 41 | ESP_IDF_SDKCONFIG = { value = "./sdkconfig", relative = true } 42 | ESP_IDF_SDKCONFIG_DEFAULTS = { value = "./sdkconfig.defaults", relative = true } 43 | 44 | # Enables the esp-idf-sys "native" build feature to build against ESP-IDF upcoming (v4.4) 45 | ESP_IDF_VERSION = { value = "branch:release/v4.4" } 46 | # Enables the esp-idf-sys "native" build feature to build against ESP-IDF master (v5.0) 47 | #ESP_IDF_VERSION = { value = "master" } 48 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG VARIANT=bullseye 2 | FROM debian:${VARIANT} 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | ENV LC_ALL=C.UTF-8 5 | ENV LANG=C.UTF-8 6 | 7 | # Arguments 8 | ARG CONTAINER_USER=esp 9 | ARG CONTAINER_GROUP=esp 10 | ARG TOOLCHAIN_VERSION=1.64.0.0 11 | ARG ESP_IDF_VERSION=release/v4.4 12 | ARG ESP_BOARD=esp32 13 | ARG INSTALL_RUST_TOOLCHAIN=install-rust-toolchain.sh 14 | 15 | # Install dependencies 16 | RUN apt-get update \ 17 | && apt-get install -y git curl gcc clang ninja-build libudev-dev unzip xz-utils\ 18 | python3 python3-pip python3-venv libusb-1.0-0 libssl-dev pkg-config libtinfo5 libpython2.7 \ 19 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 20 | 21 | # Set users 22 | RUN adduser --disabled-password --gecos "" ${CONTAINER_USER} 23 | USER ${CONTAINER_USER} 24 | WORKDIR /home/${CONTAINER_USER} 25 | 26 | # Install Rust toolchain, extra crates and esp-idf 27 | ENV PATH=${PATH}:/home/${CONTAINER_USER}/.cargo/bin:/home/${CONTAINER_USER}/opt/bin 28 | 29 | ADD --chown=${CONTAINER_USER}:${CONTAINER_GROUP} \ 30 | https://github.com/esp-rs/rust-build/releases/download/v${TOOLCHAIN_VERSION}/${INSTALL_RUST_TOOLCHAIN} \ 31 | /home/${CONTAINER_USER}/${INSTALL_RUST_TOOLCHAIN} 32 | 33 | RUN chmod a+x ${INSTALL_RUST_TOOLCHAIN} \ 34 | && ./${INSTALL_RUST_TOOLCHAIN} \ 35 | --extra-crates "ldproxy cargo-espflash wokwi-server web-flash" \ 36 | --export-file /home/${CONTAINER_USER}/export-esp.sh \ 37 | --esp-idf-version "${ESP_IDF_VERSION}" \ 38 | --minified-esp-idf "YES" \ 39 | --build-target "${ESP_BOARD}" \ 40 | && rustup component add clippy rustfmt 41 | 42 | # Activate ESP environment 43 | RUN echo "source /home/${CONTAINER_USER}/export-esp.sh" >> ~/.bashrc 44 | 45 | CMD [ "/bin/bash" ] -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esp-idf-ble", 3 | // Select between image and build propieties to pull or build the image. 4 | // "image": "docker.io/espressif/idf-rust:esp32_v4.4_1.64.0.0", 5 | // "image": "mcr.microsoft.com/devcontainers/rust:1-bullseye", 6 | "build": { 7 | "dockerfile": "Dockerfile", 8 | "args": { 9 | "CONTAINER_USER": "esp", 10 | "CONTAINER_GROUP": "esp", 11 | "ESP_IDF_VERSION": "release/v4.4", 12 | "ESP_BOARD": "all" 13 | } 14 | }, 15 | "settings": { 16 | "editor.fontFamily": "FiraCode Nerd Font Mono", 17 | "editor.fontSize": 16, 18 | "editor.formatOnPaste": true, 19 | "editor.formatOnSave": true, 20 | "editor.formatOnSaveMode": "modifications", 21 | "editor.formatOnType": true, 22 | "lldb.executable": "/usr/bin/lldb", 23 | "files.watcherExclude": { 24 | "**/target/**": true 25 | }, 26 | "rust-analyzer.checkOnSave.command": "clippy", 27 | "rust-analyzer.checkOnSave.allTargets": false, 28 | "[rust]": { 29 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 30 | }, 31 | "terminal.integrated.fontSize": 16, 32 | "terminal.integrated.fontFamily": "FiraCode Nerd Font Mono" 33 | }, 34 | "extensions": [ 35 | "rust-lang.rust-analyzer", 36 | "tamasfe.even-better-toml", 37 | "serayuzgur.crates", 38 | "mutantdino.resourcemonitor", 39 | "yzhang.markdown-all-in-one", 40 | "webfreak.debug", 41 | "actboy168.tasks", 42 | "ms-azuretools.vscode-docker" 43 | ], 44 | "forwardPorts": [ 45 | 9012, 46 | 9333, 47 | 8000 48 | ], 49 | "workspaceMount": "source=${localWorkspaceFolder},target=/home/esp/esp-idf-ble,type=bind,consistency=cached", 50 | "workspaceFolder": "/home/esp/esp-idf-ble" 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | # build-nightly-xtensa: 10 | # runs-on: ubuntu-latest 11 | # steps: 12 | # - uses: actions/checkout@v2 13 | # - name: Install Rust for Xtensa 14 | # uses: esp-rs/xtensa-toolchain@v1.1 15 | # with: 16 | # default: true 17 | # ldproxy: true 18 | # 19 | # - name: Release build std 20 | # uses: actions-rs/cargo@v1 21 | # with: 22 | # command: build 23 | # args: --release --all-features --examples 24 | 25 | build-nightly-risc: 26 | runs-on: ubuntu-latest 27 | env: 28 | RUSTUP_TOOLCHAIN: nightly 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Nightly with clippy 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: nightly 36 | components: rust-src 37 | 38 | - name: Setup | ldproxy 39 | run: cargo install ldproxy 40 | 41 | - name: Release build std 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: build 45 | args: --release --all-features --examples --target=riscv32imc-esp-espidf 46 | 47 | clippy: 48 | runs-on: ubuntu-latest 49 | env: 50 | RUSTUP_TOOLCHAIN: nightly 51 | steps: 52 | - uses: actions/checkout@v2 53 | - name: Nightly with clippy 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | toolchain: nightly 57 | components: rust-src, clippy 58 | 59 | - name: Annotate commit with clippy warnings std 60 | uses: actions-rs/clippy-check@v1 61 | with: 62 | toolchain: nightly 63 | token: ${{ secrets.GITHUB_TOKEN }} 64 | args: --all-features --examples --target=riscv32imc-esp-espidf 65 | 66 | doc: 67 | runs-on: ubuntu-latest 68 | env: 69 | RUSTUP_TOOLCHAIN: nightly 70 | steps: 71 | - uses: actions/checkout@v2 72 | - name: Nightly 73 | uses: actions-rs/toolchain@v1 74 | with: 75 | toolchain: nightly 76 | components: rust-src 77 | 78 | - name: Documentation build 79 | uses: actions-rs/cargo@v1 80 | with: 81 | command: doc 82 | args: --target=riscv32imc-esp-espidf -------------------------------------------------------------------------------- /.github/workflows/publish-dry-run.yml.disabled: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branch: 4 | - 'main' 5 | workflow_dispatch: 6 | 7 | name: Publish Dry run 8 | 9 | jobs: 10 | publish: 11 | name: Publish dry run 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v2 16 | 17 | - name: Install stable toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | 24 | - run: cargo publish --token ${CRATES_TOKEN} --dry-run 25 | env: 26 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Pattern matched against refs/tags 4 | tags: 5 | - '*' # Push events to every tag not containing / 6 | workflow_dispatch: 7 | 8 | name: Publish 9 | 10 | jobs: 11 | publish: 12 | name: Publish 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout sources 16 | uses: actions/checkout@v2 17 | 18 | - name: Install stable toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: nightly 23 | override: true 24 | 25 | - run: cargo publish --token ${CRATES_TOKEN} 26 | env: 27 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} 28 | RUSTUP_TOOLCHAIN: nightly 29 | -------------------------------------------------------------------------------- /.github/workflows/security-audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | jobs: 6 | security-audit: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Nightly 11 | uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: nightly 14 | env: 15 | RUSTUP_TOOLCHAIN: nightly 16 | - name: Security audit 17 | uses: actions-rs/audit-check@v1 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | env: 21 | RUSTUP_TOOLCHAIN: nightly 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | .embuild/ 3 | target/ 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | # Note: gitpod/workspace-base image references older version of CMake, it's necessary to install newer one 2 | FROM gitpod/workspace-base 3 | ENV LC_ALL=C.UTF-8 4 | ENV LANG=C.UTF-8 5 | 6 | # ARGS 7 | ARG CONTAINER_USER=gitpod 8 | ARG CONTAINER_GROUP=gitpod 9 | ARG TOOLCHAIN_VERSION=1.64.0.0 10 | ARG ESP_IDF_VERSION="release/v4.4" 11 | ARG ESP_BOARD=all 12 | ARG INSTALL_RUST_TOOLCHAIN=install-rust-toolchain.sh 13 | 14 | # Install dependencies 15 | RUN sudo install-packages git curl gcc ninja-build libudev-dev libpython2.7 \ 16 | python3 python3-pip python3-venv libusb-1.0-0 libssl-dev pkg-config libtinfo5 clang 17 | # Set User 18 | USER ${CONTAINER_USER} 19 | WORKDIR /home/${CONTAINER_USER} 20 | 21 | # Install Rust toolchain, extra crates and esp-idf 22 | ENV PATH=${PATH}:/home/${CONTAINER_USER}/.cargo/bin:/home/${CONTAINER_USER}/opt/bin 23 | ADD --chown=${CONTAINER_USER}:${CONTAINER_GROUP} \ 24 | https://github.com/esp-rs/rust-build/releases/download/v${TOOLCHAIN_VERSION}/${INSTALL_RUST_TOOLCHAIN} \ 25 | /home/${CONTAINER_USER}/${INSTALL_RUST_TOOLCHAIN} 26 | RUN chmod a+x ${INSTALL_RUST_TOOLCHAIN} \ 27 | && ./${INSTALL_RUST_TOOLCHAIN} \ 28 | --extra-crates "ldproxy cargo-espflash wokwi-server web-flash" \ 29 | --export-file /home/${CONTAINER_USER}/export-esp.sh \ 30 | --esp-idf-version "${ESP_IDF_VERSION}" \ 31 | --minified-esp-idf "YES" \ 32 | --build-target "${ESP_BOARD}" \ 33 | && rustup component add clippy rustfmt 34 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | tasks: 4 | - name: Setup environment variables for Rust and ESP-IDF 5 | command: | 6 | source /home/gitpod/export-esp.sh 7 | vscode: 8 | extensions: 9 | - matklad.rust-analyzer 10 | - tamasfe.even-better-toml 11 | - anwar.resourcemonitor 12 | - yzhang.markdown-all-in-one 13 | - webfreak.debug 14 | - actboy168.tasks 15 | - serayuzgur.crates 16 | ports: 17 | - port: 9012 18 | visibility: public 19 | - port: 9333 20 | visibility: public 21 | - port: 8000 22 | visibility: public 23 | onOpen: open-browser -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp-idf-ble" 3 | version = "0.0.1" 4 | edition = "2021" 5 | authors = ["Pierre-Yves Aillet "] 6 | resolver = "2" 7 | 8 | [profile.release] 9 | opt-level = "s" 10 | incremental = true 11 | 12 | [profile.dev] 13 | debug = true # Symbols are nice and they don't increase the size on Flash 14 | opt-level = "z" 15 | incremental = true 16 | 17 | [features] 18 | native = ["esp-idf-sys/native"] 19 | default = ["native"] 20 | 21 | [dependencies] 22 | esp-idf-sys = { version = "0.32", features = ["binstart", "std", "native"] } 23 | esp-idf-svc = "0.45" 24 | esp-idf-hal = "0.40" 25 | embedded-svc = "0.24" 26 | embedded-hal = "0.2" 27 | lazy_static = "1.4" 28 | 29 | log = { version = "0.4" } 30 | 31 | [build-dependencies] 32 | embuild = "0.29" 33 | anyhow = "1" 34 | 35 | [package.metadata.espflash] 36 | partition_table = "partitions.csv" 37 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2022 Contributors to esp-idf-ble 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/pyaillet/esp-idf-ble/workflows/Continuous%20integration/badge.svg) 2 | ![MIT/Apache-2.0 licensed](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue) 3 | 4 | ⚠️ Support for BLE in Rust with ESP-IDF seems to now be provided by https://github.com/esp-rs/esp-idf-svc/blob/master/src/bt/ble, make sure to check it. 5 | 6 | # esp-idf-ble 7 | 8 | This project aims at providing a safe Rust wrapper of `esp-idf` to enable BLE on the ESP32 microcontrollers family 9 | 10 | ## What's working ? 11 | 12 | It's using a custom Rust wrapper around the [esp-idf bluedroid BLE API](https://docs.espressif.com/projects/esp-idf/en/v4.4.2/esp32/api-reference/bluetooth/bt_le.html) 13 | As of now, only the `gatt_server` example is partially implemented. IT is a rust port of [this esp-idf gatt_server example](https://github.com/espressif/esp-idf/tree/master/examples/bluetooth/bluedroid/ble/gatt_server). 14 | 15 | The goal is to complete the wrapper library and maybe make it usable elsewhere. 16 | 17 | ## How to use ? 18 | 19 | Refer to [this repo](https://github.com/esp-rs/rust-build) to install the custom Rust ESP toolchain. 20 | You should also install [cargo espflash](https://github.com/esp-rs/espflash) to ease the use of this project. 21 | 22 | Then you can launch the following command to compile one of the example, flash it to your device and monitor the ESP32 serial: 23 | 24 | `cargo espflash --example --monitor --speed 921600 --target ` 25 | 26 | Targets: 27 | 28 | - xtensa-esp32-espidf 29 | - xtensa-esp32s2-espidf 30 | - xtensa-esp32s3-espidf 31 | - riscv32imc-esp-espidf 32 | 33 | ## Examples 34 | 35 | - [ ] gatt_server 36 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Necessary because of this issue: https://github.com/rust-lang/cargo/issues/9641 2 | fn main() -> anyhow::Result<()> { 3 | embuild::build::CfgArgs::output_propagated("ESP_IDF")?; 4 | embuild::build::LinkArgs::output_propagated("ESP_IDF") 5 | } 6 | -------------------------------------------------------------------------------- /examples/gatt_secure_server.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::sync_channel; 2 | use std::sync::Arc; 3 | use std::thread; 4 | use std::time::Duration; 5 | 6 | use esp_idf_ble::{ 7 | AdvertiseData, AttributeValue, AutoResponse, BtUuid, EspBle, GattCharacteristic, 8 | GattDescriptor, GattService, GattServiceEvent, SecurityConfig, AuthenticationRequest, IOCapabilities, KeyMask, 9 | }; 10 | use esp_idf_hal::delay; 11 | // use esp_idf_hal::prelude::*; 12 | use esp_idf_svc::netif::{EspNetif, NetifStack}; 13 | use esp_idf_svc::nvs::{EspDefaultNvs, EspDefaultNvsPartition}; 14 | use esp_idf_sys::*; 15 | use embedded_hal::blocking::delay::DelayUs; 16 | 17 | use log::*; 18 | 19 | fn main() { 20 | esp_idf_sys::link_patches(); 21 | 22 | // Bind the log crate to the ESP Logging facilities 23 | esp_idf_svc::log::EspLogger::initialize_default(); 24 | 25 | 26 | #[allow(unused)] 27 | let netif_stack = Arc::new(EspNetif::new(NetifStack::Eth).expect("Unable to init Netif Stack")); 28 | 29 | #[allow(unused)] 30 | let default_nvs = Arc::new(EspDefaultNvs::new( EspDefaultNvsPartition::take().unwrap(), "ble", true).unwrap()); 31 | 32 | let mut delay = delay::Ets {}; 33 | 34 | delay.delay_us(100_u32); 35 | 36 | let mut ble = EspBle::new("ESP32".into(), default_nvs).unwrap(); 37 | 38 | let security_config = SecurityConfig { 39 | auth_req_mode: AuthenticationRequest::SecureMitmBonding, 40 | io_capabilities: IOCapabilities::DisplayYesNo, 41 | max_key_size: Some(16), 42 | only_accept_specified_auth: false, 43 | enable_oob: false, 44 | responder_key: Some(KeyMask::IdentityResolvingKey | KeyMask::EncryptionKey), 45 | initiator_key: Some(KeyMask::IdentityResolvingKey | KeyMask::EncryptionKey), 46 | static_passkey: Some(123456), 47 | ..Default::default() 48 | }; 49 | 50 | ble.configure_security(security_config).expect("Unable to configure BLE Security"); 51 | 52 | let (s, r) = sync_channel(1); 53 | 54 | ble.register_gatt_service_application(1, move |gatts_if, reg| { 55 | if let GattServiceEvent::Register(reg) = reg { 56 | info!("Service registered with {reg:?}"); 57 | s.send(gatts_if).expect("Unable to send result"); 58 | } 59 | }) 60 | .expect("Unable to register service"); 61 | 62 | let svc_uuid = BtUuid::Uuid16(0x00FF); 63 | 64 | let svc = GattService::new_primary(svc_uuid, 4, 1); 65 | 66 | info!("GattService to be created: {svc:?}"); 67 | 68 | let gatts_if = r.recv().expect("Unable to receive value"); 69 | 70 | ble.register_connect_handler(gatts_if, move |_gatts_if, connect| { 71 | if let GattServiceEvent::Connect(connect) = connect { 72 | info!("Connection from {:?}", connect.remote_bda); 73 | } 74 | }); 75 | 76 | let (s, r) = sync_channel(1); 77 | 78 | ble.create_service(gatts_if, svc, move |gatts_if, create| { 79 | 80 | if let GattServiceEvent::Create(esp_ble_gatts_cb_param_t_gatts_create_evt_param { status, service_handle, .. }) = create { 81 | info!( 82 | "Service created with {{ \tgatts_if: {gatts_if}\tstatus: {status}\n\thandle: {service_handle}\n}}" 83 | ); 84 | s.send(service_handle).expect("Unable to send value"); 85 | } 86 | }) 87 | .expect("Unable to create service"); 88 | 89 | let svc_handle = r.recv().expect("Unable to receive value"); 90 | 91 | ble.start_service(svc_handle, |_, start| { 92 | if let GattServiceEvent::StartComplete(esp_ble_gatts_cb_param_t_gatts_start_evt_param { 93 | service_handle, 94 | .. 95 | }) = start 96 | { 97 | info!("Service started for handle: {service_handle}"); 98 | } 99 | }) 100 | .expect("Unable to start ble service"); 101 | 102 | let attr_value: AttributeValue<12> = AttributeValue::new_with_value(&[ 103 | 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 104 | ]); 105 | let charac = GattCharacteristic::new( 106 | BtUuid::Uuid16(0xff01), 107 | (ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE_ENC_MITM) as _, 108 | (ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE) as _, 109 | attr_value, 110 | AutoResponse::ByApp, 111 | ); 112 | 113 | let (s, r) = sync_channel(1); 114 | 115 | ble.add_characteristic(svc_handle, charac, move |_, add_char| { 116 | if let GattServiceEvent::AddCharacteristicComplete( 117 | esp_ble_gatts_cb_param_t_gatts_add_char_evt_param { attr_handle, .. }, 118 | ) = add_char 119 | { 120 | info!("Attr added with handle: {attr_handle}"); 121 | s.send(attr_handle).expect("Unable to send value"); 122 | } 123 | }) 124 | .expect("Unable to add characteristic"); 125 | 126 | let char_attr_handle = r.recv().expect("Unable to recv attr_handle"); 127 | 128 | let data = ble 129 | .read_attribute_value(char_attr_handle) 130 | .expect("Unable to read characteristic value"); 131 | info!("Characteristic values: {data:?}"); 132 | 133 | let cdesc = GattDescriptor::new( 134 | BtUuid::Uuid16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG as u16), 135 | ESP_GATT_PERM_READ as _, 136 | ); 137 | ble.add_descriptor(svc_handle, cdesc, |_, add_desc| { 138 | if let GattServiceEvent::AddDescriptorComplete( 139 | esp_ble_gatts_cb_param_t_gatts_add_char_descr_evt_param { attr_handle, .. }, 140 | ) = add_desc 141 | { 142 | info!("Descriptor added with handle: {attr_handle}"); 143 | } 144 | }) 145 | .expect("Unable to add characteristic"); 146 | 147 | ble.register_read_handler(char_attr_handle, move |gatts_if, read| { 148 | let val = [0x48, 0x65, 0x6c, 0x6c, 0x6f]; 149 | 150 | if let GattServiceEvent::Read(read) = read { 151 | esp_idf_ble::send( 152 | gatts_if, 153 | char_attr_handle, 154 | read.conn_id, 155 | read.trans_id, 156 | esp_gatt_status_t_ESP_GATT_OK, 157 | &val, 158 | ) 159 | .expect("Unable to send read response"); 160 | } 161 | }); 162 | 163 | ble.register_write_handler(char_attr_handle, move |gatts_if, write| { 164 | if let GattServiceEvent::Write(write) = write { 165 | if write.is_prep { 166 | warn!("Unsupported write"); 167 | } else { 168 | let value = unsafe { std::slice::from_raw_parts(write.value, write.len as usize) }; 169 | info!("Write event received for {char_attr_handle} with: {value:?}"); 170 | 171 | if write.need_rsp { 172 | esp_idf_ble::send( 173 | gatts_if, 174 | char_attr_handle, 175 | write.conn_id, 176 | write.trans_id, 177 | esp_gatt_status_t_ESP_GATT_OK, 178 | &[], 179 | ) 180 | .expect("Unable to send response"); 181 | } 182 | } 183 | } 184 | }); 185 | 186 | let adv_data = AdvertiseData { 187 | appearance: esp_idf_ble::AppearanceCategory::Watch, 188 | include_name: true, 189 | include_txpower: false, 190 | min_interval: 6, 191 | max_interval: 16, 192 | service_uuid: Some(BtUuid::Uuid128([ 193 | 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 194 | 0x00, 0x00, 195 | ])), 196 | flag: (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT) as _, 197 | ..Default::default() 198 | }; 199 | ble.configure_advertising_data(adv_data, |_| { 200 | info!("advertising configured"); 201 | }) 202 | .expect("Failed to configure advertising data"); 203 | 204 | let scan_rsp_data = AdvertiseData { 205 | appearance: esp_idf_ble::AppearanceCategory::Watch, 206 | include_name: false, 207 | include_txpower: true, 208 | set_scan_rsp: true, 209 | service_uuid: Some(BtUuid::Uuid128([ 210 | 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 211 | 0x00, 0x00, 212 | ])), 213 | ..Default::default() 214 | }; 215 | 216 | ble.configure_advertising_data(scan_rsp_data, |_| { 217 | info!("Advertising configured"); 218 | }) 219 | .expect("Failed to configure advertising data"); 220 | 221 | ble.start_advertise(|_| { 222 | info!("advertising started"); 223 | }) 224 | .expect("Failed to start advertising"); 225 | 226 | loop { 227 | thread::sleep(Duration::from_millis(500)); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /examples/gatt_server.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::sync::mpsc::sync_channel; 3 | use std::sync::Arc; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | use esp_idf_ble::{ 8 | AdvertiseData, AttributeValue, AutoResponse, BtUuid, EspBle, GattCharacteristic, 9 | GattDescriptor, GattService, GattServiceEvent, ServiceUuid, 10 | }; 11 | use esp_idf_hal::delay; 12 | // use esp_idf_hal::prelude::*; 13 | use esp_idf_svc::netif::{EspNetif, NetifStack}; 14 | use esp_idf_svc::nvs::{EspDefaultNvs, EspDefaultNvsPartition}; 15 | use esp_idf_sys::*; 16 | 17 | use embedded_hal::blocking::delay::DelayUs; 18 | 19 | use log::*; 20 | 21 | fn main() { 22 | esp_idf_sys::link_patches(); 23 | 24 | // Bind the log crate to the ESP Logging facilities 25 | esp_idf_svc::log::EspLogger::initialize_default(); 26 | 27 | #[allow(unused)] 28 | let netif_stack = Arc::new(EspNetif::new(NetifStack::Eth).expect("Unable to init Netif Stack")); 29 | 30 | #[allow(unused)] 31 | let default_nvs = Arc::new(EspDefaultNvs::new( EspDefaultNvsPartition::take().unwrap(), "ble", true).unwrap()); 32 | 33 | let mut delay = delay::Ets {}; 34 | 35 | delay.delay_us(100_u32); 36 | 37 | let ble = EspBle::new("ESP32".into(), default_nvs); 38 | 39 | let mut ble = match ble { 40 | Ok(ble) => ble, 41 | Err(e) => {error!("{}: {:?}", e.to_string(), e.source()); panic!()}, 42 | }; 43 | 44 | let (s, r) = sync_channel(1); 45 | 46 | ble.register_gatt_service_application(1, move |gatts_if, reg| { 47 | if let GattServiceEvent::Register(reg) = reg { 48 | info!("Service registered with {:?}", reg); 49 | s.send(gatts_if).expect("Unable to send result"); 50 | } else { 51 | warn!("What are you doing here??"); 52 | } 53 | }) 54 | .expect("Unable to register service"); 55 | 56 | let svc_uuid = BtUuid::Uuid16(ServiceUuid::Battery as u16); 57 | 58 | let svc = GattService::new_primary(svc_uuid, 4, 1); 59 | 60 | info!("GattService to be created: {:?}", svc); 61 | 62 | let gatts_if = r.recv().expect("Unable to receive value"); 63 | 64 | let (s, r) = sync_channel(1); 65 | 66 | ble.register_connect_handler(gatts_if, |_gatts_if, connect| { 67 | if let GattServiceEvent::Connect(connect) = connect { 68 | info!("Connect event: {:?}", connect); 69 | } 70 | }); 71 | 72 | ble.create_service(gatts_if, svc, move |gatts_if, create| { 73 | if let GattServiceEvent::Create(create) = create { 74 | info!( 75 | "Service created with {{ \tgatts_if: {}\tstatus: {}\n\thandle: {}\n}}", 76 | gatts_if, create.status, create.service_handle 77 | ); 78 | s.send(create.service_handle).expect("Unable to send value"); 79 | } 80 | }) 81 | .expect("Unable to create service"); 82 | 83 | let svc_handle = r.recv().expect("Unable to receive value"); 84 | 85 | ble.start_service(svc_handle, |_, start| { 86 | if let GattServiceEvent::StartComplete(start) = start { 87 | info!("Service started for handle: {}", start.service_handle); 88 | } 89 | }) 90 | .expect("Unable to start ble service"); 91 | 92 | let attr_value: AttributeValue<12> = AttributeValue::new_with_value(&[ 93 | 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 94 | ]); 95 | let charac = GattCharacteristic::new( 96 | BtUuid::Uuid16(0xff01), 97 | (ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE) as _, 98 | (ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE) as _, 99 | attr_value, 100 | AutoResponse::ByApp, 101 | ); 102 | 103 | let (s, r) = sync_channel(1); 104 | 105 | ble.add_characteristic(svc_handle, charac, move |_, add_char| { 106 | if let GattServiceEvent::AddCharacteristicComplete(add_char) = add_char { 107 | info!("Attr added with handle: {}", add_char.attr_handle); 108 | s.send(add_char.attr_handle).expect("Unable to send value"); 109 | } 110 | }) 111 | .expect("Unable to add characteristic"); 112 | 113 | let char_attr_handle = r.recv().expect("Unable to recv attr_handle"); 114 | 115 | let data = ble 116 | .read_attribute_value(char_attr_handle) 117 | .expect("Unable to read characteristic value"); 118 | info!("Characteristic values: {:?}", data); 119 | 120 | let cdesc = GattDescriptor::new( 121 | BtUuid::Uuid16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG as u16), 122 | ESP_GATT_PERM_READ as _, 123 | ); 124 | ble.add_descriptor(svc_handle, cdesc, |_, add_desc| { 125 | if let GattServiceEvent::AddDescriptorComplete(add_desc) = add_desc { 126 | info!("Descriptor added with handle: {}", add_desc.attr_handle); 127 | } 128 | }) 129 | .expect("Unable to add characteristic"); 130 | 131 | ble.register_read_handler(char_attr_handle, move |gatts_if, read| { 132 | let val = [0x48, 0x65, 0x6c, 0x6c, 0x6f]; 133 | 134 | if let GattServiceEvent::Read(read) = read { 135 | esp_idf_ble::send( 136 | gatts_if, 137 | char_attr_handle, 138 | read.conn_id, 139 | read.trans_id, 140 | esp_gatt_status_t_ESP_GATT_OK, 141 | &val, 142 | ) 143 | .expect("Unable to send read response"); 144 | } 145 | }); 146 | 147 | ble.register_write_handler(char_attr_handle, move |gatts_if, write| { 148 | if let GattServiceEvent::Write(write) = write { 149 | if write.is_prep { 150 | warn!("Unsupported write"); 151 | } else { 152 | let value = unsafe { std::slice::from_raw_parts(write.value, write.len as usize) }; 153 | info!( 154 | "Write event received for {} with: {:?}", 155 | char_attr_handle, value 156 | ); 157 | 158 | if write.need_rsp { 159 | esp_idf_ble::send( 160 | gatts_if, 161 | char_attr_handle, 162 | write.conn_id, 163 | write.trans_id, 164 | esp_gatt_status_t_ESP_GATT_OK, 165 | &[], 166 | ) 167 | .expect("Unable to send response"); 168 | } 169 | } 170 | } 171 | }); 172 | 173 | let adv_data = AdvertiseData { 174 | include_name: true, 175 | include_txpower: false, 176 | min_interval: 6, 177 | max_interval: 16, 178 | service_uuid: Some(BtUuid::Uuid128([ 179 | 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 180 | 0x00, 0x00, 181 | ])), 182 | flag: (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT) as _, 183 | ..Default::default() 184 | }; 185 | ble.configure_advertising_data(adv_data, |_| { 186 | info!("advertising configured"); 187 | }) 188 | .expect("Failed to configure advertising data"); 189 | 190 | let scan_rsp_data = AdvertiseData { 191 | include_name: false, 192 | include_txpower: true, 193 | set_scan_rsp: true, 194 | service_uuid: Some(BtUuid::Uuid128([ 195 | 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 196 | 0x00, 0x00, 197 | ])), 198 | ..Default::default() 199 | }; 200 | 201 | ble.configure_advertising_data(scan_rsp_data, |_| { 202 | info!("Advertising configured"); 203 | }) 204 | .expect("Failed to configure advertising data"); 205 | 206 | ble.start_advertise(|_| { 207 | info!("advertising started"); 208 | }) 209 | .expect("Failed to start advertising"); 210 | 211 | loop { 212 | thread::sleep(Duration::from_millis(5000)); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /partitions.csv: -------------------------------------------------------------------------------- 1 | # ESP-IDF Partition Table 2 | # Name, Type, SubType, Offset, Size, Flags 3 | nvs, data, nvs, 0x9000, 0x6000, 4 | phy_init, data, phy, 0xf000, 0x1000, 5 | factory, app, factory, 0x10000, 3M, 6 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | 3 | channel = "esp" 4 | -------------------------------------------------------------------------------- /sdkconfig.defaults: -------------------------------------------------------------------------------- 1 | # Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) 2 | CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000 3 | CONFIG_BT_BLUEDROID_ENABLED=y 4 | CONFIG_BT_ENABLED=y 5 | CONFIG_BT_BLE_ENABLED=y 6 | CONFIG_BT_GATTS_ENABLE=y 7 | CONFIG_BT_BLE_SMP_ENABLE=y 8 | CONFIG_BT_GATTS_SEND_SERVICE_CHANGE_MODE=y 9 | CONFIG_BT_BTC_TASK_STACK_SIZE=7000 10 | CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y 11 | CONFIG_BT_CTRL_BLE_MAX_ACT=10 12 | CONFIG_BT_CTRL_BLE_MAX_ACT_EFF=10 13 | CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=0 14 | CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y 15 | CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=100 16 | CONFIG_BT_CTRL_BLE_ADV_REPORT_DISCARD_THRSHOLD=20 17 | CONFIG_BT_CTRL_BLE_SCAN_DUPL=y 18 | 19 | # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). 20 | # This allows to use 1 ms granuality for thread sleeps (10 ms by default). 21 | #CONFIG_FREERTOS_HZ=1000 22 | 23 | # Workaround for https://github.com/espressif/esp-idf/issues/7631 24 | #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n 25 | #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n 26 | -------------------------------------------------------------------------------- /sdkconfig.esp32: -------------------------------------------------------------------------------- 1 | # Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) 2 | CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000 3 | CONFIG_BLUEDROID_ENABLED=y 4 | CONFIG_BT_ENABLED=y 5 | CONFIG_BT_BLE_ENABLED=y 6 | CONFIG_BT_GATTS_ENABLE=y 7 | CONFIG_BLE_SMP_ENABLE=y 8 | CONFIG_BT_GATTS_SEND_SERVICE_CHANGE_MODE=y 9 | CONFIG_BT_BTC_TASK_STACK_SIZE=7000 10 | CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y 11 | CONFIG_BT_CTRL_BLE_MAX_ACT=10 12 | CONFIG_BT_CTRL_BLE_MAX_ACT_EFF=10 13 | CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=0 14 | CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y 15 | CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=100 16 | CONFIG_BT_CTRL_BLE_ADV_REPORT_DISCARD_THRSHOLD=20 17 | CONFIG_BT_CTRL_BLE_SCAN_DUPL=y 18 | 19 | # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). 20 | # This allows to use 1 ms granuality for thread sleeps (10 ms by default). 21 | #CONFIG_FREERTOS_HZ=1000 22 | 23 | # Workaround for https://github.com/espressif/esp-idf/issues/7631 24 | #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n 25 | #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n 26 | -------------------------------------------------------------------------------- /sdkconfig.esp32c3: -------------------------------------------------------------------------------- 1 | # Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) 2 | CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000 3 | CONFIG_BT_ENABLED=y 4 | CONFIG_BT_BLE_ENABLED=y 5 | CONFIG_BT_GATTS_ENABLE=y 6 | CONFIG_BT_GATTS_SEND_SERVICE_CHANGE_MODE=y 7 | CONFIG_BT_BTC_TASK_STACK_SIZE=7000 8 | 9 | CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y 10 | CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n 11 | CONFIG_BTDM_CTRL_MODE_BTDM=n 12 | 13 | # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). 14 | # This allows to use 1 ms granuality for thread sleeps (10 ms by default). 15 | #CONFIG_FREERTOS_HZ=1000 16 | 17 | # Workaround for https://github.com/espressif/esp-idf/issues/7631 18 | #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n 19 | #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n 20 | -------------------------------------------------------------------------------- /sdkconfig.esp32s3: -------------------------------------------------------------------------------- 1 | # Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) 2 | CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000 3 | CONFIG_BT_ENABLED=y 4 | CONFIG_BT_BLE_ENABLED=y 5 | CONFIG_BT_GATTS_ENABLE=y 6 | CONFIG_BT_GATTS_SEND_SERVICE_CHANGE_MODE=y 7 | CONFIG_BT_BTC_TASK_STACK_SIZE=7000 8 | 9 | CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y 10 | CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n 11 | CONFIG_BTDM_CTRL_MODE_BTDM=n 12 | 13 | # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). 14 | # This allows to use 1 ms granuality for thread sleeps (10 ms by default). 15 | #CONFIG_FREERTOS_HZ=1000 16 | 17 | # Workaround for https://github.com/espressif/esp-idf/issues/7631 18 | #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n 19 | #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n 20 | -------------------------------------------------------------------------------- /src/advertise.rs: -------------------------------------------------------------------------------- 1 | use crate::BtUuid; 2 | use esp_idf_sys::*; 3 | 4 | #[allow(clippy::upper_case_acronyms)] 5 | #[repr(u16)] 6 | #[derive(Clone, Copy, Debug)] 7 | pub enum AppearanceCategory { 8 | Unknown = 0x00, 9 | Phone, 10 | Computer, 11 | Watch, 12 | Clock, 13 | Display, 14 | RemoteControl, 15 | EyeGlass, 16 | Tag, 17 | Keyring, 18 | MediaPlayer, 19 | BarcodeScanner, 20 | Thermometer, 21 | HeartRateSensor, 22 | BloodPressure, 23 | HumanInterfaceDevice, 24 | GlucoseMeter, 25 | RunningWalkingSensor, 26 | Cycling, 27 | ControlDevice, 28 | NetworkDevice, 29 | Sensor, 30 | LightFixtures, 31 | Fan, 32 | HVAC, 33 | AirConditionning, 34 | Humidifier, 35 | Heating, 36 | AccessControl, 37 | MotorizedDevice, 38 | PowerDevice, 39 | LightSource, 40 | WindowCovering, 41 | AudioSink, 42 | AudioSource, 43 | MotorizedVehicle, 44 | DomesticAppliance, 45 | WearableAudioDevice, 46 | Aircraft, 47 | AVEquipment, 48 | DisplayEquipment, 49 | HearingAid, 50 | Gaming, 51 | Signage, 52 | PulseOximeter = 0x31, 53 | WeightScale, 54 | PersonalMobilityDevice, 55 | ContinuousGlucoseMonitor, 56 | InsulinPump, 57 | MedicationDelivery, 58 | OutdoorSportsActivity = 0x51, 59 | } 60 | 61 | impl From for i32 { 62 | fn from(cat: AppearanceCategory) -> Self { 63 | ((cat as u16) << 6) as _ 64 | } 65 | } 66 | 67 | pub struct RawAdvertiseData { 68 | data: Vec, 69 | pub(crate) set_scan_rsp: bool, 70 | } 71 | 72 | impl RawAdvertiseData { 73 | pub fn new(data: Vec, set_scan_rsp: bool) -> Self { 74 | RawAdvertiseData { data, set_scan_rsp } 75 | } 76 | 77 | pub fn as_raw_data(&self) -> (*mut u8, u32) { 78 | let mut v: Vec = self 79 | .data 80 | .iter() 81 | .flat_map(|v: &AdvertiseType| { 82 | let v: Vec = v.into(); 83 | v 84 | }) 85 | .collect(); 86 | v.shrink_to(31); 87 | let v = v.as_mut_slice(); 88 | log::info!("Adv data({}): {{ {:?} }}", v.len(), &v); 89 | (v.as_mut_ptr(), v.len() as u32) 90 | } 91 | } 92 | 93 | #[derive(Clone, Debug)] 94 | pub enum AdvertiseType { 95 | Flags(u8), 96 | ServicePartial16(Vec), 97 | ServiceComplete16(Vec), 98 | ServicePartial32(Vec), 99 | ServiceComplete32(Vec), 100 | ServicePartial128(Vec<[u8; 16]>), 101 | ServiceComplete128(Vec<[u8; 16]>), 102 | IntervalRange(u16, u16), 103 | DeviceNameShort(String), 104 | DeviceNameComplete(String), 105 | Appearance(AppearanceCategory), 106 | TxPower(u8), 107 | } 108 | 109 | impl From<&AdvertiseType> for Vec { 110 | fn from(adv: &AdvertiseType) -> Self { 111 | match adv { 112 | AdvertiseType::Flags(flag) => vec![0x02, 0x01, *flag], 113 | AdvertiseType::ServicePartial16(svc) => { 114 | let l = svc.len() * 2 + 1; 115 | let mut v = vec![l as u8, 0x02]; 116 | v.append(&mut svc.iter().flat_map(|svc| svc.to_be_bytes()).collect()); 117 | v 118 | } 119 | AdvertiseType::ServiceComplete16(svc) => { 120 | let l = svc.len() * 2 + 1; 121 | let mut v = vec![l as u8, 0x03]; 122 | v.append(&mut svc.iter().flat_map(|svc| svc.to_be_bytes()).collect()); 123 | v 124 | } 125 | AdvertiseType::ServicePartial32(svc) => { 126 | let l = svc.len() * 4 + 1; 127 | let mut v = vec![l as u8, 0x04]; 128 | v.append(&mut svc.iter().flat_map(|svc| svc.to_be_bytes()).collect()); 129 | v 130 | } 131 | AdvertiseType::ServiceComplete32(svc) => { 132 | let l = svc.len() * 4 + 1; 133 | let mut v = vec![l as u8, 0x05]; 134 | v.append(&mut svc.iter().flat_map(|svc| svc.to_be_bytes()).collect()); 135 | v 136 | } 137 | AdvertiseType::ServicePartial128(svc) => { 138 | let l = svc.len() * 16 + 1; 139 | let mut v = vec![l as u8, 0x06]; 140 | v.append(&mut svc.iter().flat_map(|svc| *svc).collect()); 141 | v 142 | } 143 | AdvertiseType::ServiceComplete128(svc) => { 144 | let l = svc.len() * 16 + 1; 145 | let mut v = vec![l as u8, 0x07]; 146 | v.append(&mut svc.iter().flat_map(|svc| *svc).collect()); 147 | v 148 | } 149 | AdvertiseType::IntervalRange(min, max) => { 150 | let mut v = vec![0x06, 0x12]; 151 | v.append(&mut min.to_be_bytes().to_vec()); 152 | v.append(&mut max.to_be_bytes().to_vec()); 153 | v.append(&mut vec![0x00]); 154 | v 155 | } 156 | AdvertiseType::DeviceNameShort(name) => { 157 | let mut v = vec![(name.len() + 1) as u8, 0x08]; 158 | v.append(&mut name.as_bytes().to_vec()); 159 | v 160 | } 161 | AdvertiseType::DeviceNameComplete(name) => { 162 | let mut v = vec![(name.len() + 1) as u8, 0x09]; 163 | v.append(&mut name.as_bytes().to_vec()); 164 | v 165 | } 166 | AdvertiseType::Appearance(cat) => { 167 | let cat: i32 = (*cat).into(); 168 | vec![0x02, 0x19, cat as u8] 169 | } 170 | AdvertiseType::TxPower(_pow) => { 171 | vec![0x03, 0x0a, 0x09] 172 | } 173 | } 174 | } 175 | } 176 | 177 | pub struct AdvertiseData { 178 | pub set_scan_rsp: bool, 179 | pub include_name: bool, 180 | pub include_txpower: bool, 181 | pub min_interval: i32, 182 | pub max_interval: i32, 183 | pub manufacturer: Option, 184 | pub service: Option, 185 | pub service_uuid: Option, 186 | pub appearance: AppearanceCategory, 187 | pub flag: u8, 188 | } 189 | 190 | impl Default for AdvertiseData { 191 | fn default() -> Self { 192 | Self { 193 | set_scan_rsp: false, 194 | include_name: false, 195 | include_txpower: false, 196 | min_interval: 0, 197 | max_interval: 0, 198 | manufacturer: None, 199 | service: None, 200 | service_uuid: None, 201 | appearance: AppearanceCategory::Unknown, 202 | flag: ESP_BLE_ADV_FLAG_NON_LIMIT_DISC as _, 203 | } 204 | } 205 | } 206 | 207 | -------------------------------------------------------------------------------- /src/gap.rs: -------------------------------------------------------------------------------- 1 | use esp_idf_sys::*; 2 | 3 | #[derive(Clone, Copy)] 4 | pub enum GapEvent { 5 | AdvertisingDatasetComplete(esp_ble_gap_cb_param_t_ble_adv_data_cmpl_evt_param), 6 | ScanResponseDatasetComplete(esp_ble_gap_cb_param_t_ble_scan_rsp_data_cmpl_evt_param), 7 | ScanParameterDatasetComplete(esp_ble_gap_cb_param_t_ble_scan_param_cmpl_evt_param), 8 | ScanResult(esp_ble_gap_cb_param_t_ble_scan_result_evt_param), 9 | RawAdvertisingDatasetComplete(esp_ble_gap_cb_param_t_ble_adv_data_raw_cmpl_evt_param), 10 | RawScanResponseDatasetComplete(esp_ble_gap_cb_param_t_ble_scan_rsp_data_raw_cmpl_evt_param), 11 | AdvertisingStartComplete(esp_ble_gap_cb_param_t_ble_adv_start_cmpl_evt_param), 12 | ScanStartComplete(esp_ble_gap_cb_param_t_ble_scan_start_cmpl_evt_param), 13 | AuthenticationComplete(esp_ble_sec_t), 14 | Key(esp_ble_sec_t), 15 | SecurityRequest(esp_ble_sec_t), 16 | PasskeyNotification(esp_ble_sec_t), 17 | PasskeyRequest(esp_ble_sec_t), 18 | OOBRequest, 19 | LocalIR, 20 | LocalER, 21 | NumericComparisonRequest(esp_ble_sec_t), 22 | AdvertisingStopComplete(esp_ble_gap_cb_param_t_ble_adv_stop_cmpl_evt_param), 23 | ScanStopComplete(esp_ble_gap_cb_param_t_ble_scan_stop_cmpl_evt_param), 24 | SetStaticRandomAddressComplete(esp_ble_gap_cb_param_t_ble_set_rand_cmpl_evt_param), 25 | UpdateConnectionParamsComplete(esp_ble_gap_cb_param_t_ble_update_conn_params_evt_param), 26 | SetPacketLengthComplete(esp_ble_gap_cb_param_t_ble_pkt_data_length_cmpl_evt_param), 27 | SetLocalPrivacy(esp_ble_gap_cb_param_t_ble_local_privacy_cmpl_evt_param), 28 | RemoveDeviceBondComplete(esp_ble_gap_cb_param_t_ble_remove_bond_dev_cmpl_evt_param), 29 | ClearDeviceBondComplete(esp_ble_gap_cb_param_t_ble_clear_bond_dev_cmpl_evt_param), 30 | GetDeviceBondComplete(esp_ble_gap_cb_param_t_ble_get_bond_dev_cmpl_evt_param), 31 | ReadRssiComplete(esp_ble_gap_cb_param_t_ble_read_rssi_cmpl_evt_param), 32 | UpdateWhitelistComplete(esp_ble_gap_cb_param_t_ble_update_whitelist_cmpl_evt_param), 33 | UpdateDuplicateListComplete( 34 | esp_ble_gap_cb_param_t_ble_update_duplicate_exceptional_list_cmpl_evt_param, 35 | ), 36 | SetChannelsComplete(esp_ble_gap_cb_param_t_ble_set_channels_evt_param), 37 | /* 38 | #if (BLE_50_FEATURE_SUPPORT == TRUE) 39 | READ_PHY_COMPLETE_EVT, 40 | SET_PREFERED_DEFAULT_PHY_COMPLETE_EVT, 41 | SET_PREFERED_PHY_COMPLETE_EVT, 42 | EXT_ADV_SET_RAND_ADDR_COMPLETE_EVT, 43 | EXT_ADV_SET_PARAMS_COMPLETE_EVT, 44 | EXT_ADV_DATA_SET_COMPLETE_EVT, 45 | EXT_SCAN_RSP_DATA_SET_COMPLETE_EVT, 46 | EXT_ADV_START_COMPLETE_EVT, 47 | EXT_ADV_STOP_COMPLETE_EVT, 48 | EXT_ADV_SET_REMOVE_COMPLETE_EVT, 49 | EXT_ADV_SET_CLEAR_COMPLETE_EVT, 50 | PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT, 51 | PERIODIC_ADV_DATA_SET_COMPLETE_EVT, 52 | PERIODIC_ADV_START_COMPLETE_EVT, 53 | PERIODIC_ADV_STOP_COMPLETE_EVT, 54 | PERIODIC_ADV_CREATE_SYNC_COMPLETE_EVT, 55 | PERIODIC_ADV_SYNC_CANCEL_COMPLETE_EVT, 56 | PERIODIC_ADV_SYNC_TERMINATE_COMPLETE_EVT, 57 | PERIODIC_ADV_ADD_DEV_COMPLETE_EVT, 58 | PERIODIC_ADV_REMOVE_DEV_COMPLETE_EVT, 59 | PERIODIC_ADV_CLEAR_DEV_COMPLETE_EVT, 60 | SET_EXT_SCAN_PARAMS_COMPLETE_EVT, 61 | EXT_SCAN_START_COMPLETE_EVT, 62 | EXT_SCAN_STOP_COMPLETE_EVT, 63 | PREFER_EXT_CONN_PARAMS_SET_COMPLETE_EVT, 64 | PHY_UPDATE_COMPLETE_EVT, 65 | EXT_ADV_REPORT_EVT, 66 | SCAN_TIMEOUT_EVT, 67 | ADV_TERMINATED_EVT, 68 | SCAN_REQ_RECEIVED_EVT, 69 | CHANNEL_SELETE_ALGORITHM_EVT, 70 | PERIODIC_ADV_REPORT_EVT, 71 | PERIODIC_ADV_SYNC_LOST_EVT, 72 | PERIODIC_ADV_SYNC_ESTAB_EVT, 73 | #endif // #if (BLE_50_FEATURE_SUPPORT == TRUE) 74 | EVT_MAX, 75 | */ 76 | } 77 | 78 | impl std::fmt::Debug for GapEvent { 79 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 80 | write!( 81 | f, 82 | "{}", 83 | match self { 84 | GapEvent::AdvertisingDatasetComplete(_) => "AdvertisingDatasetComplete", 85 | GapEvent::ScanResponseDatasetComplete(_) => "ScanResponseDatasetComplete", 86 | GapEvent::ScanParameterDatasetComplete(_) => "ScanParameterDatasetComplete", 87 | GapEvent::ScanResult(_) => "ScanResult", 88 | GapEvent::RawAdvertisingDatasetComplete(_) => "RawAdvertisingDatasetComplete", 89 | GapEvent::RawScanResponseDatasetComplete(_) => "RawScanResponseDatasetComplete", 90 | GapEvent::AdvertisingStartComplete(_) => "AdvertisingStartComplete", 91 | GapEvent::ScanStartComplete(_) => "ScanStartComplete", 92 | GapEvent::AuthenticationComplete(_) => "AuthenticationComplete", 93 | GapEvent::Key(_) => "Key", 94 | GapEvent::SecurityRequest(_) => "SecurityRequest", 95 | GapEvent::PasskeyNotification(_) => "PasskeyNotification", 96 | GapEvent::PasskeyRequest(_) => "PasskeyRequest", 97 | GapEvent::OOBRequest => "OOBRequest", 98 | GapEvent::LocalIR => "LocalIR", 99 | GapEvent::LocalER => "LocalER", 100 | GapEvent::NumericComparisonRequest(_) => "NumericComparisonRequest", 101 | GapEvent::AdvertisingStopComplete(_) => "AdvertisingStopComplete", 102 | GapEvent::ScanStopComplete(_) => "ScanStopComplete", 103 | GapEvent::SetStaticRandomAddressComplete(_) => "SetStaticRandomAddressComplete", 104 | GapEvent::UpdateConnectionParamsComplete(_) => "UpdateConnectionParamsComplete", 105 | GapEvent::SetPacketLengthComplete(_) => "SetPacketLengthComplete", 106 | GapEvent::SetLocalPrivacy(_) => "SetLocalPrivacy", 107 | GapEvent::RemoveDeviceBondComplete(_) => "RemoveDeviceBondComplete", 108 | GapEvent::ClearDeviceBondComplete(_) => "ClearDeviceBondComplete", 109 | GapEvent::GetDeviceBondComplete(_) => "GetDeviceBondComplete", 110 | GapEvent::ReadRssiComplete(_) => "ReadRssiComplete", 111 | GapEvent::UpdateWhitelistComplete(_) => "UpdateWhitelistComplete", 112 | GapEvent::UpdateDuplicateListComplete(_) => "UpdateDuplicateListComplete", 113 | GapEvent::SetChannelsComplete(_) => "SetChannelsComplete", 114 | } 115 | ) 116 | } 117 | } 118 | 119 | impl GapEvent { 120 | #[allow(non_upper_case_globals)] 121 | pub(crate) unsafe fn build( 122 | evt: esp_gap_ble_cb_event_t, 123 | param: *mut esp_ble_gap_cb_param_t, 124 | ) -> Self { 125 | let param = param.as_ref().unwrap(); 126 | match evt { 127 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT => { 128 | GapEvent::AdvertisingDatasetComplete(param.adv_data_cmpl) 129 | } 130 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT => { 131 | GapEvent::ScanResponseDatasetComplete(param.scan_rsp_data_cmpl) 132 | } 133 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT => { 134 | GapEvent::ScanParameterDatasetComplete(param.scan_param_cmpl) 135 | } 136 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_RESULT_EVT => { 137 | GapEvent::ScanResult(param.scan_rst) 138 | } 139 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT => { 140 | GapEvent::RawAdvertisingDatasetComplete(param.adv_data_raw_cmpl) 141 | } 142 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT => { 143 | GapEvent::RawScanResponseDatasetComplete(param.scan_rsp_data_raw_cmpl) 144 | } 145 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_ADV_START_COMPLETE_EVT => { 146 | GapEvent::AdvertisingStartComplete(param.adv_start_cmpl) 147 | } 148 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_START_COMPLETE_EVT => { 149 | GapEvent::ScanStartComplete(param.scan_start_cmpl) 150 | } 151 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_AUTH_CMPL_EVT => { 152 | GapEvent::AuthenticationComplete(param.ble_security) 153 | } 154 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_KEY_EVT => GapEvent::Key(param.ble_security), 155 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SEC_REQ_EVT => { 156 | GapEvent::SecurityRequest(param.ble_security) 157 | } 158 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_PASSKEY_NOTIF_EVT => { 159 | GapEvent::PasskeyNotification(param.ble_security) 160 | } 161 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_PASSKEY_REQ_EVT => { 162 | GapEvent::PasskeyRequest(param.ble_security) 163 | } 164 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_OOB_REQ_EVT => GapEvent::OOBRequest, 165 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_LOCAL_IR_EVT => GapEvent::LocalIR, 166 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_LOCAL_ER_EVT => GapEvent::LocalER, 167 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_NC_REQ_EVT => { 168 | GapEvent::NumericComparisonRequest(param.ble_security) 169 | } 170 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT => { 171 | GapEvent::AdvertisingStopComplete(param.adv_stop_cmpl) 172 | } 173 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT => { 174 | GapEvent::ScanStopComplete(param.scan_stop_cmpl) 175 | } 176 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SET_STATIC_RAND_ADDR_EVT => { 177 | GapEvent::SetStaticRandomAddressComplete(param.set_rand_addr_cmpl) 178 | } 179 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT => { 180 | GapEvent::UpdateConnectionParamsComplete(param.update_conn_params) 181 | } 182 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT => { 183 | GapEvent::SetPacketLengthComplete(param.pkt_data_lenth_cmpl) 184 | } 185 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SET_LOCAL_PRIVACY_COMPLETE_EVT => { 186 | GapEvent::SetLocalPrivacy(param.local_privacy_cmpl) 187 | } 188 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_REMOVE_BOND_DEV_COMPLETE_EVT => { 189 | GapEvent::RemoveDeviceBondComplete(param.remove_bond_dev_cmpl) 190 | } 191 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_CLEAR_BOND_DEV_COMPLETE_EVT => { 192 | GapEvent::ClearDeviceBondComplete(param.clear_bond_dev_cmpl) 193 | } 194 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_GET_BOND_DEV_COMPLETE_EVT => { 195 | GapEvent::GetDeviceBondComplete(param.get_bond_dev_cmpl) 196 | } 197 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT => { 198 | GapEvent::ReadRssiComplete(param.read_rssi_cmpl) 199 | } 200 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_UPDATE_WHITELIST_COMPLETE_EVT => { 201 | GapEvent::UpdateWhitelistComplete(param.update_whitelist_cmpl) 202 | } 203 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_UPDATE_DUPLICATE_EXCEPTIONAL_LIST_COMPLETE_EVT => { 204 | GapEvent::UpdateDuplicateListComplete(param.update_duplicate_exceptional_list_cmpl) 205 | } 206 | esp_gap_ble_cb_event_t_ESP_GAP_BLE_SET_CHANNELS_EVT => { 207 | GapEvent::SetChannelsComplete(param.ble_set_channels) 208 | } 209 | _ => { 210 | log::warn!("Unhandled event {:?}", evt); 211 | panic!("Unhandled event {:?}", evt) 212 | } 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/gatt.rs: -------------------------------------------------------------------------------- 1 | use esp_idf_sys::*; 2 | 3 | #[repr(u16)] 4 | #[derive(Copy, Clone)] 5 | pub enum ServiceUuid { 6 | GenericAccess = 0x1800, 7 | GenericAttribute, 8 | ImmediateAlert, 9 | LinkLoss, 10 | TxPower, 11 | CurrentTime, 12 | ReferenceTimeUpdate, 13 | NextDSTChange = 0x1807, 14 | Glucose, 15 | HealthThermometer, 16 | DeviceInformation, 17 | HeartRate = 0x180D, 18 | PhoneAlertStatus, 19 | Battery, 20 | BloodPressure, 21 | AlertNotification, 22 | HumanInterfaceDevice, 23 | ScanParameters, 24 | RunningSpeedAndCadence, 25 | AutomationIO, 26 | CyclingSpeedAndCadence, 27 | CyclingPower = 0x1818, 28 | LocationAndNavigation, 29 | EnvironmentalSensing, 30 | BodyComposition, 31 | UserData, 32 | WeightScale, 33 | BondManagement, 34 | ContinuousGlucoseMonitoring, 35 | InternetProtocolSupport, 36 | IndoorPositioning, 37 | PulseOximeter, 38 | HTTPProxy, 39 | TransportDiscovery, 40 | ObjectTransfer, 41 | FitnessMachine, 42 | MeshProvisioning, 43 | MeshProxy, 44 | ReconnectionConfiguration, 45 | InsulinDelivery = 0x183A, 46 | BinarySensor, 47 | EmergencyConfiguration, 48 | PhysicalActivityMonitor = 0x183E, 49 | AudioInputControl = 0x1843, 50 | VolumeControl, 51 | VolumeOffsetControl, 52 | CoordinatedSetIdentification, 53 | DeviceTime, 54 | MediaControl, 55 | GenericMediaControl, 56 | ConstantToneExtension, 57 | TelephoneBearer, 58 | GenericTelephoneBearer, 59 | MicrophoneControl, 60 | AudioStreamControl, 61 | BroadcastAudioScan, 62 | PublishedAudioCapabilities, 63 | BasicAudioAnnouncement, 64 | BroadcastAudioAnnouncement, 65 | CommonAudio, 66 | HearingAccess, 67 | TMAS, 68 | PublicBroadcastAnnouncement, 69 | } 70 | 71 | #[derive(Debug, Clone)] 72 | pub enum BtUuid { 73 | Uuid16(u16), 74 | Uuid32(u32), 75 | Uuid128([u8; 16]), 76 | } 77 | 78 | impl From for esp_bt_uuid_t { 79 | fn from(svc: BtUuid) -> Self { 80 | let mut bt_uuid: esp_bt_uuid_t = Default::default(); 81 | match svc { 82 | BtUuid::Uuid16(uuid) => { 83 | bt_uuid.len = 2; 84 | bt_uuid.uuid.uuid16 = uuid; 85 | } 86 | BtUuid::Uuid32(uuid) => { 87 | bt_uuid.len = 4; 88 | bt_uuid.uuid.uuid32 = uuid; 89 | } 90 | BtUuid::Uuid128(uuid) => { 91 | bt_uuid.len = 16; 92 | bt_uuid.uuid.uuid128 = uuid; 93 | } 94 | } 95 | bt_uuid 96 | } 97 | } 98 | 99 | pub struct AttributeValue { 100 | len: usize, 101 | value: [u8; S], 102 | } 103 | 104 | impl From> for esp_attr_value_t { 105 | fn from(mut val: AttributeValue) -> Self { 106 | Self { 107 | attr_max_len: S as _, 108 | attr_len: val.len as _, 109 | attr_value: val.value.as_mut_ptr(), 110 | } 111 | } 112 | } 113 | 114 | impl Default for AttributeValue { 115 | fn default() -> Self { 116 | Self { 117 | len: S, 118 | value: [0; S], 119 | } 120 | } 121 | } 122 | 123 | impl AttributeValue { 124 | pub fn new_with_value(value: &[u8]) -> Self { 125 | let actual_len = std::cmp::min(value.len(), S); 126 | let mut val = Self { 127 | len: S, 128 | value: [0; S], 129 | }; 130 | val.value[0..actual_len].copy_from_slice(&value[0..actual_len]); 131 | val 132 | } 133 | } 134 | 135 | pub enum AutoResponse { 136 | ByApp, 137 | ByGatt, 138 | } 139 | 140 | impl From for esp_attr_control_t { 141 | fn from(auto: AutoResponse) -> Self { 142 | Self { 143 | auto_rsp: match auto { 144 | AutoResponse::ByApp => ESP_GATT_RSP_BY_APP, 145 | AutoResponse::ByGatt => ESP_GATT_AUTO_RSP, 146 | } as _, 147 | } 148 | } 149 | } 150 | 151 | pub struct GattCharacteristic { 152 | pub(crate) uuid: BtUuid, 153 | pub(crate) permissions: esp_gatt_perm_t, 154 | pub(crate) property: esp_gatt_char_prop_t, 155 | pub(crate) value: AttributeValue, 156 | pub(crate) auto_rsp: AutoResponse, 157 | } 158 | 159 | impl GattCharacteristic { 160 | pub fn new( 161 | uuid: BtUuid, 162 | permissions: esp_gatt_perm_t, 163 | property: esp_gatt_char_prop_t, 164 | value: AttributeValue, 165 | auto_rsp: AutoResponse, 166 | ) -> Self { 167 | Self { 168 | uuid, 169 | permissions, 170 | property, 171 | value, 172 | auto_rsp, 173 | } 174 | } 175 | } 176 | 177 | pub struct GattDescriptor { 178 | pub(crate) uuid: BtUuid, 179 | pub(crate) permissions: esp_gatt_perm_t, 180 | } 181 | 182 | impl GattDescriptor { 183 | pub fn new(uuid: BtUuid, permissions: esp_gatt_perm_t) -> Self { 184 | Self { uuid, permissions } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/gatt_client.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyaillet/esp-idf-ble/45d1e98c93c14bf53577ead636e66d7f717a6ba5/src/gatt_client.rs -------------------------------------------------------------------------------- /src/gatt_server.rs: -------------------------------------------------------------------------------- 1 | use esp_idf_sys::*; 2 | 3 | use crate::BtUuid; 4 | 5 | #[derive(Debug)] 6 | pub struct GattService { 7 | pub(crate) is_primary: bool, 8 | pub(crate) id: BtUuid, 9 | pub(crate) instance_id: u8, 10 | pub(crate) handle: u16, 11 | } 12 | 13 | impl GattService { 14 | pub fn new_primary(id: BtUuid, handle: u16, instance_id: u8) -> Self { 15 | Self { 16 | is_primary: true, 17 | id, 18 | handle, 19 | instance_id, 20 | } 21 | } 22 | 23 | pub fn new(id: BtUuid, handle: u16, instance_id: u8) -> Self { 24 | Self { 25 | is_primary: false, 26 | id, 27 | handle, 28 | instance_id, 29 | } 30 | } 31 | } 32 | 33 | #[derive(Copy, Clone)] 34 | pub enum GattServiceEvent { 35 | Register(esp_ble_gatts_cb_param_t_gatts_reg_evt_param), 36 | Read(esp_ble_gatts_cb_param_t_gatts_read_evt_param), 37 | Write(esp_ble_gatts_cb_param_t_gatts_write_evt_param), 38 | ExecWrite(esp_ble_gatts_cb_param_t_gatts_exec_write_evt_param), 39 | Mtu(esp_ble_gatts_cb_param_t_gatts_mtu_evt_param), 40 | Confirm(esp_ble_gatts_cb_param_t_gatts_conf_evt_param), 41 | Unregister(esp_ble_gatts_cb_param_t_gatts_create_evt_param), 42 | Create(esp_ble_gatts_cb_param_t_gatts_create_evt_param), 43 | AddIncludedServiceComplete(esp_ble_gatts_cb_param_t_gatts_add_incl_srvc_evt_param), 44 | AddCharacteristicComplete(esp_ble_gatts_cb_param_t_gatts_add_char_evt_param), 45 | AddDescriptorComplete(esp_ble_gatts_cb_param_t_gatts_add_char_descr_evt_param), 46 | DeleteComplete(esp_ble_gatts_cb_param_t_gatts_delete_evt_param), 47 | StartComplete(esp_ble_gatts_cb_param_t_gatts_start_evt_param), 48 | StopComplete(esp_ble_gatts_cb_param_t_gatts_stop_evt_param), 49 | Connect(esp_ble_gatts_cb_param_t_gatts_connect_evt_param), 50 | Disconnect(esp_ble_gatts_cb_param_t_gatts_disconnect_evt_param), 51 | Open(esp_ble_gatts_cb_param_t_gatts_open_evt_param), 52 | Close(esp_ble_gatts_cb_param_t_gatts_close_evt_param), 53 | Listen(esp_ble_gatts_cb_param_t_gatts_congest_evt_param), 54 | Congest(esp_ble_gatts_cb_param_t_gatts_congest_evt_param), 55 | ResponseComplete(esp_ble_gatts_cb_param_t_gatts_rsp_evt_param), 56 | CreateAttributeTableComplete(esp_ble_gatts_cb_param_t_gatts_add_attr_tab_evt_param), 57 | SetAttributeValueComplete(esp_ble_gatts_cb_param_t_gatts_set_attr_val_evt_param), 58 | SendServiceChangeComplete(esp_ble_gatts_cb_param_t_gatts_send_service_change_evt_param), 59 | } 60 | 61 | impl std::fmt::Debug for GattServiceEvent { 62 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 63 | match self { 64 | GattServiceEvent::Register(reg) => write!( 65 | f, 66 | "Register {{ status: {}, app_id: {} }}", 67 | reg.status, reg.app_id 68 | ), 69 | GattServiceEvent::Read(read) => write!(f, "Read {{ {:?} }}", read), 70 | GattServiceEvent::Write(write) => write!(f, "Write {{ {:?} }}", write), 71 | GattServiceEvent::ExecWrite(_) => write!(f, "ExecWrite"), 72 | GattServiceEvent::Mtu(_) => write!(f, "Mtu"), 73 | GattServiceEvent::Confirm(_) => write!(f, "Confirm"), 74 | GattServiceEvent::Unregister(_) => write!(f, "Unregister"), 75 | GattServiceEvent::Create(_) => write!(f, "Create"), 76 | GattServiceEvent::AddIncludedServiceComplete(_) => { 77 | write!(f, "AddIncludedServiceComplete") 78 | } 79 | GattServiceEvent::AddCharacteristicComplete(_) => { 80 | write!(f, "AddCharacteristicComplete") 81 | } 82 | GattServiceEvent::AddDescriptorComplete(_) => write!(f, "AddDescriptorComplete"), 83 | GattServiceEvent::DeleteComplete(_) => write!(f, "DeleteComplete"), 84 | GattServiceEvent::StartComplete(_) => write!(f, "StartComplete"), 85 | GattServiceEvent::StopComplete(_) => write!(f, "StopComplete"), 86 | GattServiceEvent::Connect(_) => write!(f, "Connect"), 87 | GattServiceEvent::Disconnect(_) => write!(f, "Disconnect"), 88 | GattServiceEvent::Open(_) => write!(f, "Open"), 89 | GattServiceEvent::Close(_) => write!(f, "Close"), 90 | GattServiceEvent::Listen(_) => write!(f, "Listen"), 91 | GattServiceEvent::Congest(_) => write!(f, "Congest"), 92 | GattServiceEvent::ResponseComplete(_) => write!(f, "ResponseComplete"), 93 | GattServiceEvent::CreateAttributeTableComplete(_) => { 94 | write!(f, "CreateAttributeTableComplete") 95 | } 96 | GattServiceEvent::SetAttributeValueComplete(_) => { 97 | write!(f, "SetAttributeValueComplete") 98 | } 99 | GattServiceEvent::SendServiceChangeComplete(_) => { 100 | write!(f, "SendServiceChangeComplete") 101 | } 102 | } 103 | } 104 | } 105 | 106 | impl GattServiceEvent { 107 | pub(crate) unsafe fn build( 108 | event: esp_idf_sys::esp_gatts_cb_event_t, 109 | param: *mut esp_idf_sys::esp_ble_gatts_cb_param_t, 110 | ) -> Self { 111 | let param: &esp_idf_sys::esp_ble_gatts_cb_param_t = param.as_ref().unwrap(); 112 | match event { 113 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_REG_EVT => { 114 | GattServiceEvent::Register(param.reg) 115 | } 116 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_READ_EVT => { 117 | GattServiceEvent::Read(param.read) 118 | } 119 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_WRITE_EVT => { 120 | GattServiceEvent::Write(param.write) 121 | } 122 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_EXEC_WRITE_EVT => { 123 | GattServiceEvent::ExecWrite(param.exec_write) 124 | } 125 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_MTU_EVT => GattServiceEvent::Mtu(param.mtu), 126 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_CONF_EVT => { 127 | GattServiceEvent::Confirm(param.conf) 128 | } 129 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_UNREG_EVT => { 130 | GattServiceEvent::Unregister(param.create) 131 | } 132 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_CREATE_EVT => { 133 | GattServiceEvent::Create(param.create) 134 | } 135 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_ADD_INCL_SRVC_EVT => { 136 | GattServiceEvent::AddIncludedServiceComplete(param.add_incl_srvc) 137 | } 138 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_ADD_CHAR_EVT => { 139 | GattServiceEvent::AddCharacteristicComplete(param.add_char) 140 | } 141 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_ADD_CHAR_DESCR_EVT => { 142 | GattServiceEvent::AddDescriptorComplete(param.add_char_descr) 143 | } 144 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_DELETE_EVT => { 145 | GattServiceEvent::DeleteComplete(param.del) 146 | } 147 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_START_EVT => { 148 | GattServiceEvent::StartComplete(param.start) 149 | } 150 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_STOP_EVT => { 151 | GattServiceEvent::StopComplete(param.stop) 152 | } 153 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_CONNECT_EVT => { 154 | GattServiceEvent::Connect(param.connect) 155 | } 156 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_DISCONNECT_EVT => { 157 | GattServiceEvent::Disconnect(param.disconnect) 158 | } 159 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_OPEN_EVT => { 160 | GattServiceEvent::Open(param.open) 161 | } 162 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_CLOSE_EVT => { 163 | GattServiceEvent::Close(param.close) 164 | } 165 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_LISTEN_EVT => { 166 | GattServiceEvent::Listen(param.congest) 167 | } 168 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_CONGEST_EVT => { 169 | GattServiceEvent::Congest(param.congest) 170 | } 171 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_RESPONSE_EVT => { 172 | GattServiceEvent::ResponseComplete(param.rsp) 173 | } 174 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_CREAT_ATTR_TAB_EVT => { 175 | GattServiceEvent::CreateAttributeTableComplete(param.add_attr_tab) 176 | } 177 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_SET_ATTR_VAL_EVT => { 178 | GattServiceEvent::SetAttributeValueComplete(param.set_attr_val) 179 | } 180 | esp_idf_sys::esp_gatts_cb_event_t_ESP_GATTS_SEND_SERVICE_CHANGE_EVT => { 181 | GattServiceEvent::SendServiceChangeComplete(param.service_change) 182 | } 183 | _ => { 184 | log::warn!("Unhandled event: {:?}", event); 185 | panic!("Unhandled event: {:?}", event) 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod advertise; 2 | mod gap; 3 | mod gatt; 4 | mod gatt_client; 5 | mod gatt_server; 6 | mod security; 7 | 8 | #[macro_use] 9 | extern crate lazy_static; 10 | 11 | use std::ffi::c_void; 12 | use std::{collections::HashMap, ffi::CString, sync::Arc}; 13 | 14 | use ::log::*; 15 | use advertise::RawAdvertiseData; 16 | 17 | use esp_idf_svc::nvs::EspDefaultNvs; 18 | 19 | use esp_idf_sys::*; 20 | 21 | use std::sync::Mutex; 22 | 23 | pub use advertise::*; 24 | pub use gap::*; 25 | pub use gatt::*; 26 | pub use gatt_client::*; 27 | pub use gatt_server::*; 28 | pub use security::*; 29 | 30 | static DEFAULT_TAKEN: Mutex = Mutex::new(false); 31 | 32 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 33 | enum GapCallbacks { 34 | RawAdvertisingDataset, 35 | RawScanResponseDataset, 36 | AdvertisingDataset, 37 | ScanResponseDataset, 38 | AdvertisingStart, 39 | UpdateConnectionParams, 40 | PasskeyNotify, 41 | KeyEvent, 42 | AuthComplete, 43 | NumericComparisonRequest, 44 | SecurityRequest, 45 | } 46 | 47 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 48 | enum GattCallbacks { 49 | Register(u16), // app_id 50 | Create(u8), // gatts_if 51 | Start(u16), // svc_handle 52 | AddCharacteristic(u16), // svc_handle 53 | AddCharacteristicDesc(u16), // svc_handle 54 | Read(u16), // attr_handle 55 | Write(u16), // attr_handle 56 | Connect(u8), // gatts_if 57 | } 58 | lazy_static! { 59 | static ref GAP_CALLBACKS: Mutex>> = 60 | Mutex::new(HashMap::new()); 61 | static ref GATT_CALLBACKS_ONE_TIME: Mutex>> = 62 | Mutex::new(HashMap::new()); 63 | static ref GATT_CALLBACKS_KEPT: Mutex>> = 64 | Mutex::new(HashMap::new()); 65 | } 66 | fn insert_gatt_cb_kept(cb_key: GattCallbacks, cb: impl Fn(u8, GattServiceEvent) + Send + 'static) { 67 | GATT_CALLBACKS_KEPT 68 | .lock() 69 | .as_mut() 70 | .and_then(|m| Ok(m.insert(cb_key, Box::new(cb)))).unwrap(); 71 | } 72 | 73 | fn insert_gatt_cb_onetime( 74 | cb_key: GattCallbacks, 75 | cb: impl Fn(u8, GattServiceEvent) + Send + 'static, 76 | ) { 77 | GATT_CALLBACKS_ONE_TIME 78 | .lock() 79 | .as_mut() 80 | .and_then(|m| Ok(m.insert(cb_key, Box::new(cb)))).unwrap(); 81 | } 82 | 83 | fn insert_gap_cb(cb_key: GapCallbacks, cb: impl Fn(GapEvent) + Send + 'static) { 84 | GAP_CALLBACKS 85 | .lock() 86 | .as_mut() 87 | .and_then(|m| Ok(m.insert(cb_key, Box::new(cb)))).unwrap(); 88 | } 89 | 90 | unsafe extern "C" fn gap_event_handler( 91 | event: esp_gap_ble_cb_event_t, 92 | param: *mut esp_ble_gap_cb_param_t, 93 | ) { 94 | let event = GapEvent::build(event, param); 95 | debug!("Called gap event handler with event {{ {:#?} }}", &event); 96 | 97 | if let Ok(Some(cb)) = GAP_CALLBACKS.lock().as_mut().map(|m| { 98 | (match &event { 99 | GapEvent::RawAdvertisingDatasetComplete(_) => { 100 | Some(&GapCallbacks::RawAdvertisingDataset) 101 | } 102 | GapEvent::RawScanResponseDatasetComplete(_) => { 103 | Some(&GapCallbacks::RawScanResponseDataset) 104 | } 105 | GapEvent::AdvertisingDatasetComplete(_) => Some(&GapCallbacks::AdvertisingDataset), 106 | GapEvent::ScanResponseDatasetComplete(_) => Some(&GapCallbacks::ScanResponseDataset), 107 | GapEvent::AdvertisingStartComplete(_) => Some(&GapCallbacks::AdvertisingStart), 108 | GapEvent::UpdateConnectionParamsComplete(_) => { 109 | Some(&GapCallbacks::UpdateConnectionParams) 110 | } 111 | GapEvent::PasskeyNotification(_) => Some(&GapCallbacks::PasskeyNotify), 112 | GapEvent::Key(_) => Some(&GapCallbacks::KeyEvent), 113 | GapEvent::AuthenticationComplete(_) => Some(&GapCallbacks::AuthComplete), 114 | GapEvent::NumericComparisonRequest(_) => Some(&GapCallbacks::NumericComparisonRequest), 115 | GapEvent::SecurityRequest(_) => Some(&GapCallbacks::SecurityRequest), 116 | _ => { 117 | warn!("Unimplemented {:?}", event); 118 | None 119 | } 120 | }) 121 | .and_then(|cb_key| m.get(cb_key)) 122 | }) { 123 | cb(event); 124 | } else { 125 | warn!("No callbak registered for event: {:?}", event); 126 | } 127 | } 128 | 129 | unsafe extern "C" fn gatts_event_handler( 130 | event: esp_gatts_cb_event_t, 131 | gatts_if: esp_gatt_if_t, 132 | param: *mut esp_ble_gatts_cb_param_t, 133 | ) { 134 | let event = GattServiceEvent::build(event, param); 135 | debug!( 136 | "Called gatt service event handler with gatts_if: {}, event {{ {:#?} }}", 137 | gatts_if, &event 138 | ); 139 | 140 | match &event { 141 | GattServiceEvent::Register(reg) => { 142 | if let Ok(Some(cb)) = GATT_CALLBACKS_ONE_TIME 143 | .lock() 144 | .as_mut() 145 | .and_then(|m| Ok(m.remove(&GattCallbacks::Register(reg.app_id)))) 146 | { 147 | cb(gatts_if, event); 148 | } else { 149 | warn!( 150 | "No callback registered for Register with app_id: {}", 151 | reg.app_id 152 | ); 153 | } 154 | } 155 | GattServiceEvent::Create(_) => { 156 | if let Ok(Some(cb)) = GATT_CALLBACKS_ONE_TIME 157 | .lock() 158 | .as_mut() 159 | .and_then(|m| Ok(m.remove(&GattCallbacks::Create(gatts_if)))) 160 | { 161 | cb(gatts_if, event); 162 | } else { 163 | warn!( 164 | "No callback registered for Create with gatts_if: {}", 165 | gatts_if 166 | ); 167 | } 168 | } 169 | GattServiceEvent::StartComplete(start) => { 170 | if let Ok(Some(cb)) = GATT_CALLBACKS_ONE_TIME 171 | .lock() 172 | .as_mut() 173 | .and_then(|m| Ok(m.remove(&GattCallbacks::Start(start.service_handle)))) 174 | { 175 | cb(gatts_if, event); 176 | } else { 177 | warn!( 178 | "No callback registered for Start with svc_handle: {}", 179 | start.service_handle 180 | ); 181 | } 182 | } 183 | GattServiceEvent::AddCharacteristicComplete(add_char) => { 184 | if let Ok(Some(cb)) = GATT_CALLBACKS_ONE_TIME.lock().as_mut().and_then(|m| { 185 | Ok(m.remove(&GattCallbacks::AddCharacteristic(add_char.service_handle))) 186 | }) { 187 | cb(gatts_if, event); 188 | } else { 189 | warn!( 190 | "No callback registered for AddChar with svc_handle: {}", 191 | add_char.service_handle 192 | ); 193 | } 194 | } 195 | GattServiceEvent::AddDescriptorComplete(add_desc) => { 196 | if let Ok(Some(cb)) = GATT_CALLBACKS_ONE_TIME.lock().as_mut().and_then(|m| { 197 | Ok(m.remove(&GattCallbacks::AddCharacteristicDesc( 198 | add_desc.service_handle, 199 | ))) 200 | }) { 201 | cb(gatts_if, event); 202 | } else { 203 | warn!( 204 | "No callback registered for AddDesc with svc_handle: {}", 205 | add_desc.service_handle 206 | ); 207 | } 208 | } 209 | GattServiceEvent::Connect(conn) => { 210 | let mut conn_params: esp_ble_conn_update_params_t = esp_ble_conn_update_params_t { 211 | bda: conn.remote_bda, 212 | min_int: 0x10, // min_int = 0x10*1.25ms = 20ms 213 | max_int: 0x20, // max_int = 0x20*1.25ms = 40ms 214 | latency: 0, 215 | timeout: 400, // timeout = 400*10ms = 4000ms 216 | }; 217 | // 218 | info!("Connection from: {:?}", conn); 219 | 220 | let _ = esp!(esp_ble_gap_update_conn_params(&mut conn_params)); 221 | if let Ok(Some(cb)) = GATT_CALLBACKS_KEPT 222 | .lock() 223 | .as_mut() 224 | .and_then(|m| Ok(m.get(&GattCallbacks::Connect(gatts_if)))) 225 | { 226 | cb(gatts_if, event); 227 | } 228 | } 229 | GattServiceEvent::Read(read) => { 230 | if let Ok(Some(cb)) = GATT_CALLBACKS_KEPT 231 | .lock() 232 | .as_mut() 233 | .and_then(|m| Ok(m.get(&GattCallbacks::Read(read.handle)))) 234 | { 235 | cb(gatts_if, event); 236 | } else { 237 | warn!( 238 | "No callback registered for Read with handle: {}", 239 | read.handle 240 | ); 241 | } 242 | } 243 | GattServiceEvent::Write(write) => { 244 | if let Ok(Some(cb)) = GATT_CALLBACKS_KEPT 245 | .lock() 246 | .as_mut() 247 | .and_then(|m| Ok(m.get(&GattCallbacks::Write(write.handle)))) 248 | { 249 | cb(gatts_if, event); 250 | } else { 251 | warn!( 252 | "No callback registered for Write with handle: {}", 253 | write.handle 254 | ); 255 | } 256 | } 257 | _ => warn!("Handler for {:?} not implemented", event), 258 | } 259 | } 260 | 261 | #[allow(dead_code)] 262 | pub struct EspBle { 263 | device_name: String, 264 | nvs: Arc, 265 | } 266 | 267 | impl EspBle { 268 | pub fn new(device_name: String, nvs: Arc) -> Result { 269 | if let Ok(mut taken) = DEFAULT_TAKEN.lock() { 270 | if *taken { 271 | esp!(ESP_ERR_INVALID_STATE as i32)?; 272 | } 273 | println!("Test"); 274 | let ble = Self::init(device_name, nvs)?; 275 | 276 | *taken = true; 277 | Ok(ble) 278 | } else { 279 | esp!(ESP_ERR_INVALID_STATE as i32)?; 280 | unreachable!() 281 | } 282 | } 283 | 284 | fn init(device_name: String, nvs: Arc) -> Result { 285 | #[cfg(esp32)] 286 | let mut bt_cfg = esp_bt_controller_config_t { 287 | controller_task_stack_size: ESP_TASK_BT_CONTROLLER_STACK as _, 288 | controller_task_prio: ESP_TASK_BT_CONTROLLER_PRIO as _, 289 | hci_uart_no: BT_HCI_UART_NO_DEFAULT as _, 290 | hci_uart_baudrate: BT_HCI_UART_BAUDRATE_DEFAULT, 291 | scan_duplicate_mode: SCAN_DUPLICATE_MODE as _, 292 | scan_duplicate_type: SCAN_DUPLICATE_TYPE_VALUE as _, 293 | normal_adv_size: NORMAL_SCAN_DUPLICATE_CACHE_SIZE as _, 294 | mesh_adv_size: MESH_DUPLICATE_SCAN_CACHE_SIZE as _, 295 | send_adv_reserved_size: SCAN_SEND_ADV_RESERVED_SIZE as _, 296 | controller_debug_flag: CONTROLLER_ADV_LOST_DEBUG_BIT, 297 | mode: esp_bt_mode_t_ESP_BT_MODE_BLE as _, 298 | ble_max_conn: CONFIG_BTDM_CTRL_BLE_MAX_CONN_EFF as _, 299 | bt_max_acl_conn: CONFIG_BTDM_CTRL_BR_EDR_MAX_ACL_CONN_EFF as _, 300 | bt_sco_datapath: CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_EFF as _, 301 | auto_latency: BTDM_CTRL_AUTO_LATENCY_EFF != 0, 302 | bt_legacy_auth_vs_evt: BTDM_CTRL_LEGACY_AUTH_VENDOR_EVT_EFF != 0, 303 | bt_max_sync_conn: CONFIG_BTDM_CTRL_BR_EDR_MAX_SYNC_CONN_EFF as _, 304 | ble_sca: CONFIG_BTDM_BLE_SLEEP_CLOCK_ACCURACY_INDEX_EFF as _, 305 | pcm_role: CONFIG_BTDM_CTRL_PCM_ROLE_EFF as _, 306 | pcm_polar: CONFIG_BTDM_CTRL_PCM_POLAR_EFF as _, 307 | hli: BTDM_CTRL_HLI != 0, 308 | magic: ESP_BT_CONTROLLER_CONFIG_MAGIC_VAL, 309 | dup_list_refresh_period: SCAN_DUPL_CACHE_REFRESH_PERIOD as _ 310 | }; 311 | 312 | #[cfg(esp32c3)] 313 | let mut bt_cfg = esp_bt_controller_config_t { 314 | magic: esp_idf_sys::ESP_BT_CTRL_CONFIG_MAGIC_VAL, 315 | version: esp_idf_sys::ESP_BT_CTRL_CONFIG_VERSION, 316 | controller_task_stack_size: esp_idf_sys::ESP_TASK_BT_CONTROLLER_STACK as _, 317 | controller_task_prio: esp_idf_sys::ESP_TASK_BT_CONTROLLER_PRIO as _, 318 | controller_task_run_cpu: esp_idf_sys::CONFIG_BT_CTRL_PINNED_TO_CORE as _, 319 | bluetooth_mode: esp_idf_sys::CONFIG_BT_CTRL_MODE_EFF as _, 320 | ble_max_act: esp_idf_sys::CONFIG_BT_CTRL_BLE_MAX_ACT_EFF as _, 321 | sleep_mode: esp_idf_sys::CONFIG_BT_CTRL_SLEEP_MODE_EFF as _, 322 | sleep_clock: esp_idf_sys::CONFIG_BT_CTRL_SLEEP_CLOCK_EFF as _, 323 | ble_st_acl_tx_buf_nb: esp_idf_sys::CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB as _, 324 | ble_hw_cca_check: esp_idf_sys::CONFIG_BT_CTRL_HW_CCA_EFF as _, 325 | ble_adv_dup_filt_max: esp_idf_sys::CONFIG_BT_CTRL_ADV_DUP_FILT_MAX as _, 326 | ce_len_type: esp_idf_sys::CONFIG_BT_CTRL_CE_LENGTH_TYPE_EFF as _, 327 | hci_tl_type: esp_idf_sys::CONFIG_BT_CTRL_HCI_TL_EFF as _, 328 | hci_tl_funcs: std::ptr::null_mut(), 329 | txant_dft: esp_idf_sys::CONFIG_BT_CTRL_TX_ANTENNA_INDEX_EFF as _, 330 | rxant_dft: esp_idf_sys::CONFIG_BT_CTRL_RX_ANTENNA_INDEX_EFF as _, 331 | txpwr_dft: esp_idf_sys::CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF as _, 332 | cfg_mask: esp_idf_sys::CFG_MASK, 333 | scan_duplicate_mode: esp_idf_sys::SCAN_DUPLICATE_MODE as _, 334 | scan_duplicate_type: esp_idf_sys::SCAN_DUPLICATE_TYPE_VALUE as _, 335 | normal_adv_size: esp_idf_sys::NORMAL_SCAN_DUPLICATE_CACHE_SIZE as _, 336 | mesh_adv_size: esp_idf_sys::MESH_DUPLICATE_SCAN_CACHE_SIZE as _, 337 | coex_phy_coded_tx_rx_time_limit: 338 | esp_idf_sys::CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_EFF as _, 339 | hw_target_code: esp_idf_sys::BLE_HW_TARGET_CODE_CHIP_ECO0 as _, 340 | slave_ce_len_min: esp_idf_sys::SLAVE_CE_LEN_MIN_DEFAULT as _, 341 | hw_recorrect_en: esp_idf_sys::AGC_RECORRECT_EN as _, 342 | cca_thresh: esp_idf_sys::CONFIG_BT_CTRL_HW_CCA_VAL as _, 343 | coex_param_en: false, 344 | coex_use_hooks: false, 345 | ble_50_feat_supp: esp_idf_sys::BT_CTRL_50_FEATURE_SUPPORT != 0, 346 | dup_list_refresh_period: esp_idf_sys::DUPL_SCAN_CACHE_REFRESH_PERIOD as _, 347 | scan_backoff_upperlimitmax: esp_idf_sys::BT_CTRL_SCAN_BACKOFF_UPPERLIMITMAX as _ 348 | }; 349 | 350 | #[cfg(esp32s3)] 351 | let mut bt_cfg = esp_bt_controller_config_t { 352 | magic: esp_idf_sys::ESP_BT_CTRL_CONFIG_MAGIC_VAL as _, 353 | version: esp_idf_sys::ESP_BT_CTRL_CONFIG_VERSION as _, 354 | controller_task_stack_size: esp_idf_sys::ESP_TASK_BT_CONTROLLER_STACK as _, 355 | controller_task_prio: esp_idf_sys::ESP_TASK_BT_CONTROLLER_PRIO as _, 356 | controller_task_run_cpu: esp_idf_sys::CONFIG_BT_CTRL_PINNED_TO_CORE as _, 357 | bluetooth_mode: esp_idf_sys::CONFIG_BT_CTRL_MODE_EFF as _, 358 | ble_max_act: esp_idf_sys::CONFIG_BT_CTRL_BLE_MAX_ACT_EFF as _, 359 | sleep_mode: esp_idf_sys::CONFIG_BT_CTRL_SLEEP_MODE_EFF as _, 360 | sleep_clock: esp_idf_sys::CONFIG_BT_CTRL_SLEEP_CLOCK_EFF as _, 361 | ble_st_acl_tx_buf_nb: esp_idf_sys::CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB as _, 362 | ble_hw_cca_check: esp_idf_sys::CONFIG_BT_CTRL_HW_CCA_EFF as _, 363 | ble_adv_dup_filt_max: esp_idf_sys::CONFIG_BT_CTRL_ADV_DUP_FILT_MAX as _, 364 | coex_param_en: false, 365 | ce_len_type: esp_idf_sys::CONFIG_BT_CTRL_CE_LENGTH_TYPE_EFF as _, 366 | coex_use_hooks: false, 367 | hci_tl_type: esp_idf_sys::CONFIG_BT_CTRL_HCI_TL_EFF as _, 368 | hci_tl_funcs: std::ptr::null_mut(), 369 | txant_dft: esp_idf_sys::CONFIG_BT_CTRL_TX_ANTENNA_INDEX_EFF as _, 370 | rxant_dft: esp_idf_sys::CONFIG_BT_CTRL_RX_ANTENNA_INDEX_EFF as _, 371 | txpwr_dft: esp_idf_sys::CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF as _, 372 | cfg_mask: esp_idf_sys::CFG_MASK as _, 373 | scan_duplicate_mode: esp_idf_sys::SCAN_DUPLICATE_MODE as _, 374 | scan_duplicate_type: esp_idf_sys::SCAN_DUPLICATE_TYPE_VALUE as _, 375 | normal_adv_size: esp_idf_sys::NORMAL_SCAN_DUPLICATE_CACHE_SIZE as _, 376 | mesh_adv_size: esp_idf_sys::MESH_DUPLICATE_SCAN_CACHE_SIZE as _, 377 | coex_phy_coded_tx_rx_time_limit: 378 | esp_idf_sys::CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_EFF as _, 379 | hw_target_code: esp_idf_sys::BLE_HW_TARGET_CODE_CHIP_ECO0 as _, 380 | slave_ce_len_min: esp_idf_sys::SLAVE_CE_LEN_MIN_DEFAULT as _, 381 | hw_recorrect_en: esp_idf_sys::AGC_RECORRECT_EN as _, 382 | cca_thresh: esp_idf_sys::CONFIG_BT_CTRL_HW_CCA_VAL as _, 383 | ble_50_feat_supp: esp_idf_sys::BT_CTRL_50_FEATURE_SUPPORT != 0, 384 | dup_list_refresh_period: esp_idf_sys::DUPL_SCAN_CACHE_REFRESH_PERIOD as _, 385 | scan_backoff_upperlimitmax: esp_idf_sys::BT_CTRL_SCAN_BACKOFF_UPPERLIMITMAX as _ 386 | }; 387 | 388 | info!("Init bluetooth controller."); 389 | esp!(unsafe { esp_bt_controller_init(&mut bt_cfg) })?; 390 | 391 | info!("Enable bluetooth controller."); 392 | esp!(unsafe { esp_bt_controller_enable(esp_bt_mode_t_ESP_BT_MODE_BLE) })?; 393 | 394 | info!("Init bluedroid"); 395 | esp!(unsafe { esp_bluedroid_init() })?; 396 | 397 | info!("Enable bluedroid"); 398 | esp!(unsafe { esp_bluedroid_enable() })?; 399 | 400 | esp!(unsafe { esp_ble_gatts_register_callback(Some(gatts_event_handler)) })?; 401 | 402 | esp!(unsafe { esp_ble_gap_register_callback(Some(gap_event_handler)) })?; 403 | 404 | esp!(unsafe { esp_ble_gatt_set_local_mtu(500) })?; 405 | 406 | let device_name_cstr = CString::new(device_name.clone()).unwrap(); 407 | esp!(unsafe { esp_ble_gap_set_device_name(device_name_cstr.as_ptr() as _) })?; 408 | 409 | Ok(EspBle { device_name, nvs }) 410 | } 411 | 412 | pub fn configure_advertising_data_raw( 413 | &self, 414 | data: RawAdvertiseData, 415 | cb: impl Fn(GapEvent) + 'static + Send, 416 | ) -> Result<(), EspError> { 417 | info!("configure_advertising_data_raw enter"); 418 | 419 | let (raw_data, raw_len) = data.as_raw_data(); 420 | 421 | insert_gap_cb( 422 | if data.set_scan_rsp { 423 | GapCallbacks::RawScanResponseDataset 424 | } else { 425 | GapCallbacks::AdvertisingDataset 426 | }, 427 | cb, 428 | ); 429 | if data.set_scan_rsp { 430 | esp!(unsafe { esp_ble_gap_config_scan_rsp_data_raw(raw_data, raw_len) }) 431 | } else { 432 | esp!(unsafe { esp_ble_gap_config_adv_data_raw(raw_data, raw_len) }) 433 | } 434 | } 435 | 436 | pub fn configure_advertising_data( 437 | &self, 438 | data: advertise::AdvertiseData, 439 | cb: impl Fn(GapEvent) + 'static + Send, 440 | ) -> Result<(), EspError> { 441 | info!("configure_advertising enter"); 442 | 443 | let manufacturer_len = data.manufacturer.as_ref().map(|m| m.len()).unwrap_or(0) as u16; 444 | let service_data_len = data.service.as_ref().map(|s| s.len()).unwrap_or(0) as u16; 445 | #[repr(C, align(4))] 446 | struct aligned_uuid { 447 | uuid: [u8; 16], 448 | } 449 | let mut svc_uuid: aligned_uuid = aligned_uuid { uuid: [0; 16] }; 450 | 451 | let svc_uuid_len = data 452 | .service_uuid 453 | .map(|bt_uuid| match bt_uuid { 454 | BtUuid::Uuid16(uuid) => { 455 | svc_uuid.uuid[0..2].copy_from_slice(&uuid.to_le_bytes()); 456 | 2 457 | } 458 | BtUuid::Uuid32(uuid) => { 459 | svc_uuid.uuid[0..4].copy_from_slice(&uuid.to_le_bytes()); 460 | 4 461 | } 462 | BtUuid::Uuid128(uuid) => { 463 | svc_uuid.uuid.copy_from_slice(&uuid); 464 | 16 465 | } 466 | }) 467 | .unwrap_or(0); 468 | 469 | let is_scan_rsp = data.set_scan_rsp; 470 | 471 | info!("svc_uuid: {{ {:?} }}", &svc_uuid.uuid); 472 | let mut adv_data = esp_ble_adv_data_t { 473 | set_scan_rsp: data.set_scan_rsp, 474 | include_name: data.include_name, 475 | include_txpower: data.include_txpower, 476 | min_interval: data.min_interval, 477 | max_interval: data.max_interval, 478 | manufacturer_len, 479 | p_manufacturer_data: data 480 | .manufacturer 481 | .map_or(std::ptr::null_mut(), |mut m| m.as_mut_ptr()), 482 | service_data_len, 483 | p_service_data: data 484 | .service 485 | .map_or(std::ptr::null_mut(), |mut s| s.as_mut_ptr()), 486 | service_uuid_len: svc_uuid_len, 487 | p_service_uuid: if svc_uuid_len == 0 { 488 | std::ptr::null_mut() 489 | } else { 490 | let ptr = svc_uuid.uuid.as_mut_ptr(); 491 | unsafe { 492 | info!("0:{:0x}", *ptr as u8); 493 | info!("1:{:0x}", *ptr.add(1) as u8); 494 | info!("2:{:0x}", *ptr.add(2) as u8); 495 | info!("3:{:0x}", *ptr.add(3) as u8); 496 | } 497 | ptr 498 | }, 499 | appearance: data.appearance.into(), 500 | flag: data.flag, 501 | }; 502 | 503 | if is_scan_rsp { 504 | insert_gap_cb(GapCallbacks::ScanResponseDataset, cb); 505 | } else { 506 | insert_gap_cb(GapCallbacks::AdvertisingDataset, cb); 507 | }; 508 | 509 | info!("Configuring advertising with {{ {:?} }}", &adv_data); 510 | 511 | esp!(unsafe { esp_ble_gap_config_adv_data(&mut adv_data) }) 512 | } 513 | 514 | pub fn start_advertise(&self, cb: impl Fn(GapEvent) + 'static + Send) -> Result<(), EspError> { 515 | info!("start_advertise enter"); 516 | 517 | let mut adv_param: esp_ble_adv_params_t = esp_ble_adv_params_t { 518 | adv_int_min: 0x20, 519 | adv_int_max: 0x40, 520 | adv_type: 0x00, // ADV_TYPE_IND, 521 | own_addr_type: 0x00, // BLE_ADDR_TYPE_PUBLIC, 522 | peer_addr: [0; 6], 523 | peer_addr_type: 0x00, // BLE_ADDR_TYPE_PUBLIC, 524 | channel_map: 0x07, // ADV_CHNL_ALL, 525 | adv_filter_policy: 0x00, // ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, 526 | }; 527 | 528 | insert_gap_cb(GapCallbacks::AdvertisingStart, cb); 529 | esp!(unsafe { esp_ble_gap_start_advertising(&mut adv_param) }) 530 | } 531 | 532 | pub fn register_gatt_service_application( 533 | &mut self, 534 | app_id: u16, 535 | cb: impl Fn(u8, GattServiceEvent) + 'static + Send, 536 | ) -> Result<(), EspError> { 537 | info!( 538 | "register_gatt_service_application enter for app_id: {}", 539 | app_id 540 | ); 541 | insert_gatt_cb_onetime(GattCallbacks::Register(app_id), cb); 542 | esp!(unsafe { esp_ble_gatts_app_register(app_id) }) 543 | } 544 | 545 | pub fn create_service( 546 | &self, 547 | gatt_if: u8, 548 | svc: GattService, 549 | cb: impl Fn(u8, GattServiceEvent) + 'static + Send, 550 | ) -> Result<(), EspError> { 551 | let svc_uuid: esp_bt_uuid_t = svc.id.into(); 552 | 553 | let mut svc_id: esp_gatt_srvc_id_t = esp_gatt_srvc_id_t { 554 | is_primary: svc.is_primary, 555 | id: esp_gatt_id_t { 556 | uuid: svc_uuid, 557 | inst_id: svc.instance_id, 558 | }, 559 | }; 560 | insert_gatt_cb_onetime(GattCallbacks::Create(gatt_if), cb); 561 | 562 | esp!(unsafe { esp_ble_gatts_create_service(gatt_if, &mut svc_id, svc.handle) }) 563 | } 564 | 565 | pub fn start_service( 566 | &self, 567 | svc_handle: u16, 568 | cb: impl Fn(u8, GattServiceEvent) + 'static + Send, 569 | ) -> Result<(), EspError> { 570 | insert_gatt_cb_onetime(GattCallbacks::Start(svc_handle), cb); 571 | 572 | esp!(unsafe { esp_ble_gatts_start_service(svc_handle) }) 573 | } 574 | 575 | pub fn read_attribute_value(&self, attr_handle: u16) -> Result, EspError> { 576 | let mut len: u16 = 0; 577 | let mut data: *const u8 = std::ptr::null_mut(); 578 | 579 | unsafe { 580 | esp!(esp_ble_gatts_get_attr_value( 581 | attr_handle, 582 | &mut len, 583 | &mut data 584 | ))?; 585 | 586 | let data = std::slice::from_raw_parts(data, len as usize); 587 | info!("len: {:?}, data: {:p}", len, data); 588 | Ok(data.to_vec()) 589 | } 590 | } 591 | 592 | pub fn add_characteristic( 593 | &self, 594 | svc_handle: u16, 595 | charac: GattCharacteristic, 596 | cb: impl Fn(u8, GattServiceEvent) + 'static + Send, 597 | ) -> Result<(), EspError> { 598 | insert_gatt_cb_onetime(GattCallbacks::AddCharacteristic(svc_handle), cb); 599 | 600 | let mut uuid = charac.uuid.into(); 601 | 602 | let mut value = charac.value.into(); 603 | let mut auto_rsp = charac.auto_rsp.into(); 604 | 605 | esp!(unsafe { 606 | esp_ble_gatts_add_char( 607 | svc_handle, 608 | &mut uuid, 609 | charac.permissions, 610 | charac.property, 611 | &mut value, 612 | &mut auto_rsp, 613 | ) 614 | }) 615 | } 616 | 617 | pub fn add_descriptor( 618 | &self, 619 | svc_handle: u16, 620 | char_desc: GattDescriptor, 621 | cb: impl Fn(u8, GattServiceEvent) + 'static + Send, 622 | ) -> Result<(), EspError> { 623 | insert_gatt_cb_onetime(GattCallbacks::AddCharacteristicDesc(svc_handle), cb); 624 | 625 | let mut uuid = char_desc.uuid.into(); 626 | 627 | esp!(unsafe { 628 | esp_ble_gatts_add_char_descr( 629 | svc_handle, 630 | &mut uuid, 631 | char_desc.permissions, 632 | std::ptr::null_mut(), 633 | std::ptr::null_mut(), 634 | ) 635 | }) 636 | } 637 | 638 | pub fn register_connect_handler( 639 | &self, 640 | gatts_if: u8, 641 | cb: impl Fn(u8, GattServiceEvent) + 'static + Send, 642 | ) { 643 | insert_gatt_cb_kept(GattCallbacks::Connect(gatts_if), cb); 644 | } 645 | 646 | pub fn register_read_handler( 647 | &self, 648 | attr_handle: u16, 649 | cb: impl Fn(u8, GattServiceEvent) + 'static + Send, 650 | ) { 651 | insert_gatt_cb_kept(GattCallbacks::Read(attr_handle), cb); 652 | } 653 | 654 | pub fn register_write_handler( 655 | &self, 656 | attr_handle: u16, 657 | cb: impl Fn(u8, GattServiceEvent) + 'static + Send, 658 | ) { 659 | insert_gatt_cb_kept(GattCallbacks::Write(attr_handle), cb); 660 | } 661 | 662 | pub fn configure_security(&self, mut config: SecurityConfig) -> Result<(), EspError> { 663 | esp!(unsafe { 664 | esp_ble_gap_set_security_param( 665 | esp_ble_sm_param_t_ESP_BLE_SM_AUTHEN_REQ_MODE, 666 | &mut config.auth_req_mode as *mut _ as *mut c_void, 667 | std::mem::size_of::() as _, 668 | ) 669 | }) 670 | .expect("auth req mode"); 671 | 672 | esp!(unsafe { 673 | esp_ble_gap_set_security_param( 674 | esp_ble_sm_param_t_ESP_BLE_SM_IOCAP_MODE, 675 | &mut config.io_capabilities as *mut _ as *mut c_void, 676 | std::mem::size_of::() as _, 677 | ) 678 | }) 679 | .expect("sm iocap mode"); 680 | 681 | if let Some(mut initiator_key) = config.initiator_key { 682 | esp!(unsafe { 683 | esp_ble_gap_set_security_param( 684 | esp_ble_sm_param_t_ESP_BLE_SM_SET_INIT_KEY, 685 | &mut initiator_key as *mut _ as *mut c_void, 686 | std::mem::size_of::() as _, 687 | ) 688 | }) 689 | .expect("initiator_key"); 690 | } 691 | 692 | if let Some(mut responder_key) = config.responder_key { 693 | esp!(unsafe { 694 | esp_ble_gap_set_security_param( 695 | esp_ble_sm_param_t_ESP_BLE_SM_SET_RSP_KEY, 696 | &mut responder_key as *mut _ as *mut c_void, 697 | std::mem::size_of::() as _, 698 | ) 699 | }) 700 | .expect("responder_key"); 701 | } 702 | if let Some(mut max_key_size) = config.max_key_size { 703 | esp!(unsafe { 704 | esp_ble_gap_set_security_param( 705 | esp_ble_sm_param_t_ESP_BLE_SM_MAX_KEY_SIZE, 706 | &mut max_key_size as *mut _ as *mut c_void, 707 | std::mem::size_of::() as _, 708 | ) 709 | }) 710 | .expect("max key size"); 711 | } 712 | if let Some(mut min_key_size) = config.min_key_size { 713 | esp!(unsafe { 714 | esp_ble_gap_set_security_param( 715 | esp_ble_sm_param_t_ESP_BLE_SM_MIN_KEY_SIZE, 716 | &mut min_key_size as *mut _ as *mut c_void, 717 | std::mem::size_of::() as _, 718 | ) 719 | }) 720 | .expect("min key size"); 721 | } 722 | if let Some(passkey) = config.static_passkey { 723 | let mut passkey = passkey.to_ne_bytes(); 724 | esp!(unsafe { 725 | esp_ble_gap_set_security_param( 726 | esp_ble_sm_param_t_ESP_BLE_SM_SET_STATIC_PASSKEY, 727 | &mut passkey as *mut _ as *mut c_void, 728 | std::mem::size_of::() as _, 729 | ) 730 | }) 731 | .expect("set static passkey"); 732 | } 733 | esp!(unsafe { 734 | let mut only_accept_specified_auth = u8::from(config.only_accept_specified_auth); 735 | esp_ble_gap_set_security_param( 736 | esp_ble_sm_param_t_ESP_BLE_SM_ONLY_ACCEPT_SPECIFIED_SEC_AUTH, 737 | &mut only_accept_specified_auth as *mut _ as *mut c_void, 738 | std::mem::size_of::() as _, 739 | ) 740 | }) 741 | .expect("only accept spec auth"); 742 | esp!(unsafe { 743 | let mut enable_oob = u8::from(config.enable_oob); 744 | esp_ble_gap_set_security_param( 745 | esp_ble_sm_param_t_ESP_BLE_SM_OOB_SUPPORT, 746 | &mut enable_oob as *mut _ as *mut c_void, 747 | std::mem::size_of::() as _, 748 | ) 749 | }) 750 | .expect("oob support"); 751 | 752 | insert_gap_cb(GapCallbacks::SecurityRequest, |sec_req| { 753 | if let GapEvent::SecurityRequest(mut sec_req) = sec_req { 754 | info!("SecurityRequest"); 755 | match esp!(unsafe { 756 | esp_ble_gap_security_rsp(sec_req.ble_req.bd_addr.as_mut_ptr(), true) 757 | }) { 758 | Ok(()) => info!("Security set"), 759 | Err(err) => warn!("Error setting security: {}", err), 760 | } 761 | } 762 | }); 763 | insert_gap_cb(GapCallbacks::PasskeyNotify, |notify| { 764 | if let GapEvent::PasskeyNotification(notify) = notify { 765 | info!("Passkey: {:?}", unsafe { notify.ble_key.key_type }) 766 | } 767 | }); 768 | insert_gap_cb(GapCallbacks::KeyEvent, |key| { 769 | if let GapEvent::Key(key) = key { 770 | info!("Key: {:?}", unsafe { key.ble_key.key_type }); 771 | } 772 | }); 773 | insert_gap_cb(GapCallbacks::AuthComplete, |auth| { 774 | if let GapEvent::AuthenticationComplete(auth) = auth { 775 | info!("Auth: {:?}", unsafe { auth.auth_cmpl.success }); 776 | } 777 | }); 778 | 779 | insert_gap_cb(GapCallbacks::SecurityRequest, |sec_req| { 780 | if let GapEvent::SecurityRequest(sec_req) = sec_req { 781 | let mut ble_sec_req: esp_ble_sec_req_t = unsafe { sec_req.ble_req }; 782 | info!("SecurityRequest: {:?}", ble_sec_req); 783 | unsafe { esp_ble_gap_security_rsp(ble_sec_req.bd_addr.as_mut_ptr(), true) }; 784 | } 785 | }); 786 | 787 | insert_gap_cb(GapCallbacks::NumericComparisonRequest, |ble_sec| { 788 | info!("Numeric comparison request"); 789 | if let GapEvent::NumericComparisonRequest(mut ble_sec) = ble_sec { 790 | esp!(unsafe { esp_ble_confirm_reply(ble_sec.ble_req.bd_addr.as_mut_ptr(), true) }) 791 | .expect("Unable to complete numeric comparison request"); 792 | } 793 | }); 794 | 795 | Ok(()) 796 | } 797 | 798 | pub fn configure_gatt_encryption( 799 | mut remote_bda: [u8; ESP_BD_ADDR_LEN as _], 800 | encryption_config: BleEncryption, 801 | ) { 802 | esp!(unsafe { esp_ble_set_encryption(remote_bda.as_mut_ptr(), encryption_config as u32) }) 803 | .expect("Unable to set security level"); 804 | } 805 | } 806 | 807 | pub fn send( 808 | gatts_if: u8, 809 | handle: u16, 810 | conn_id: u16, 811 | trans_id: u32, 812 | status: u32, 813 | data: &[u8], 814 | ) -> Result<(), EspError> { 815 | let mut rsp: esp_gatt_rsp_t = esp_gatt_rsp_t::default(); 816 | 817 | esp!(unsafe { 818 | rsp.handle = handle; 819 | rsp.attr_value.len = data.len() as u16; 820 | if !data.is_empty() { 821 | rsp.attr_value.value[..data.len()].copy_from_slice(data); 822 | } 823 | 824 | esp_ble_gatts_send_response(gatts_if, conn_id, trans_id, status, &mut rsp) 825 | }) 826 | } 827 | -------------------------------------------------------------------------------- /src/security.rs: -------------------------------------------------------------------------------- 1 | use std::ops::BitOr; 2 | 3 | #[derive(Default)] 4 | #[repr(u8)] 5 | pub enum IOCapabilities { 6 | #[default] 7 | DisplayOnly = 0, 8 | DisplayYesNo = 1, 9 | KeyboardOnly = 2, 10 | NoInputNoOutput = 3, 11 | Keyboard = 4, 12 | } 13 | 14 | #[derive(Default)] 15 | #[repr(u8)] 16 | pub enum AuthenticationRequest { 17 | #[default] 18 | NoBonding = 0b0000_0000, 19 | Bonding = 0b0000_0001, 20 | Mitm = 0b0000_0010, 21 | MitmBonding = 0b0000_0011, 22 | SecureOnly = 0b0000_0100, 23 | SecureBonding = 0b0000_0101, 24 | SecureMitm = 0b0000_0110, 25 | SecureMitmBonding = 0b0000_0111, 26 | } 27 | 28 | #[repr(u8)] 29 | pub enum KeyMask { 30 | EncryptionKey = 0b0000_0001, 31 | IdentityResolvingKey = 0b0000_0010, 32 | ConnectionSignatureResolvingKey = 0b0000_0100, 33 | LinkKey = 0b0000_1000, 34 | Inner0011 = 0b0000_0011, 35 | Inner0101 = 0b0000_0101, 36 | Inner1001 = 0b0000_1001, 37 | Inner1010 = 0b0000_1010, 38 | Inner1100 = 0b0000_1100, 39 | Inner1101 = 0b0000_1101, 40 | Inner1011 = 0b0000_1011, 41 | Inner1111 = 0b0000_1111, 42 | } 43 | 44 | impl BitOr for KeyMask { 45 | type Output = KeyMask; 46 | 47 | fn bitor(self, rhs: Self) -> Self::Output { 48 | (self as u8 | rhs as u8).into() 49 | } 50 | } 51 | 52 | impl From for KeyMask { 53 | fn from(from: u8) -> Self { 54 | match from { 55 | 0b0000_0001 => KeyMask::EncryptionKey, 56 | 0b0000_0010 => KeyMask::IdentityResolvingKey, 57 | 0b0000_0100 => KeyMask::ConnectionSignatureResolvingKey, 58 | 0b0000_1000 => KeyMask::LinkKey, 59 | 0b0000_0011 => KeyMask::Inner0011, 60 | 0b0000_0101 => KeyMask::Inner0101, 61 | 0b0000_1001 => KeyMask::Inner1001, 62 | 0b0000_1010 => KeyMask::Inner1010, 63 | 0b0000_1100 => KeyMask::Inner1100, 64 | 0b0000_1101 => KeyMask::Inner1101, 65 | 0b0000_1011 => KeyMask::Inner1011, 66 | 0b0000_1111 => KeyMask::Inner1111, 67 | _ => unimplemented!("This does not correspond to a valid KeyMask") 68 | } 69 | } 70 | } 71 | 72 | #[repr(u32)] 73 | pub enum BleEncryption { 74 | Encryption = 0x01, 75 | EncryptionNoMitm = 0x02, 76 | EncryptionMitm = 0x03 77 | } 78 | 79 | #[derive(Default)] 80 | pub struct SecurityConfig { 81 | pub auth_req_mode: AuthenticationRequest, 82 | pub io_capabilities: IOCapabilities, 83 | pub initiator_key: Option, 84 | pub responder_key: Option, 85 | pub max_key_size: Option, 86 | pub min_key_size: Option, 87 | pub static_passkey: Option, 88 | pub only_accept_specified_auth: bool, 89 | pub enable_oob: bool, 90 | // app_key_size: u8, 91 | } 92 | --------------------------------------------------------------------------------