diff --git a/.github/workflows/netif_6lowpan_ble__build.yml b/.github/workflows/netif_6lowpan_ble__build.yml new file mode 100644 index 0000000000..709dec9f13 --- /dev/null +++ b/.github/workflows/netif_6lowpan_ble__build.yml @@ -0,0 +1,28 @@ +name: "6lowpan_ble: build-tests" + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened, labeled] + +jobs: + build_6lowpan_ble: + if: contains(github.event.pull_request.labels.*.name, 'component::6lowpan_ble') || github.event_name == 'push' + name: Build + strategy: + matrix: + idf_ver: ["latest", "release-v5.2", "release-v5.3"] + + runs-on: ubuntu-22.04 + container: espressif/idf:${{ matrix.idf_ver }} + steps: + - name: Checkout esp-protocols + uses: actions/checkout@v4 + - name: Build with IDF-${{ matrix.idf_ver }} + shell: bash + run: | + . ${IDF_PATH}/export.sh + pip install idf-component-manager idf-build-apps --upgrade + python ci/build_apps.py components/netif_6lowpan_ble/examples/ -m components/netif_6lowpan_ble/examples/.build-test-rules.yml -c diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19a05a7350..481dd7c75f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,7 +62,7 @@ repos: hooks: - id: commit message scopes name: "commit message must be scoped with: mdns, modem, websocket, asio, mqtt_cxx, console, common, eppp, wifi_remote, tls_cxx" - entry: '\A(?!(feat|fix|ci|bump|test|docs)\((mdns|modem|common|console|websocket|asio|mqtt_cxx|examples|eppp|wifi_remote|tls_cxx)\)\:)' + entry: '\A(?!(feat|fix|ci|bump|test|docs)\((mdns|modem|common|console|websocket|asio|mqtt_cxx|examples|eppp|wifi_remote|tls_cxx|6lowpan_ble)\)\:)' language: pygrep args: [--multiline] stages: [commit-msg] diff --git a/components/netif_6lowpan_ble/CMakeLists.txt b/components/netif_6lowpan_ble/CMakeLists.txt new file mode 100644 index 0000000000..9375094e8c --- /dev/null +++ b/components/netif_6lowpan_ble/CMakeLists.txt @@ -0,0 +1,22 @@ +idf_component_register( + SRCS + src/debug_print_utils.c + src/lowpan6_ble.c + src/lowpan6_ble_netif.c + INCLUDE_DIRS + include + PRIV_INCLUDE_DIRS + src + REQUIRES + bt + esp_netif +) + +idf_component_get_property(lwip lwip COMPONENT_LIB) +target_sources( + ${lwip} + PRIVATE + "$ENV{IDF_PATH}/components/lwip/lwip/src/netif/lowpan6.c" + "$ENV{IDF_PATH}/components/lwip/lwip/src/netif/lowpan6_ble.c" + "$ENV{IDF_PATH}/components/lwip/lwip/src/netif/lowpan6_common.c" +) diff --git a/components/netif_6lowpan_ble/LICENSE b/components/netif_6lowpan_ble/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/components/netif_6lowpan_ble/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/netif_6lowpan_ble/README.md b/components/netif_6lowpan_ble/README.md new file mode 100644 index 0000000000..329676bda6 --- /dev/null +++ b/components/netif_6lowpan_ble/README.md @@ -0,0 +1,93 @@ +# LoWPAN6 BLE Netif + +This component provides a custom ESP-NETIF layer for +[RFC7668](https://datatracker.ietf.org/doc/html/rfc7668), allowing devices to +transport IPv6 over Bluetooth Low Energy links. + +> :warning: This is an experimental library, developed for a pretty narrow +> use-case. In particular, we just needed to connect to a peripheral using +> LoWPAN6 for local communications so have not really tested this component as +> a LoWPAN6 client. This component does not implement any routing capabilities: +> this won't allow you to route packets from your personal area network to the +> public internet, for example. + +## Usage + +This component only supports the NimBLE stack. It also requires support for +L2CAP COC connections. This means your project's `sdkconfig` needs at least: +* NimBLE as the Bluetooth stack (`CONFIG_BT_NIMBLE_ENABLED=y`) +* `CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM` set to a non-zero value + +There is some boilerplate required to set up this netif layer. +```c +// Initialize the lowpan6_ble module +ESP_ERROR_CHECK(lowpan6_ble_init()); + +// Configure a new esp_netif to use the lowpan6_ble netstack +esp_netif_inherent_config_t base_cfg = ESP_NETIF_INHERENT_DEFAULT_LOWPAN6_BLE(); +esp_netif_config_t cfg = { + .base = &base_cfg, + .driver = NULL, + .stack = netstack_default_lowpan6_ble, +}; +esp_netif_t* lowpan6_ble_netif = esp_netif_new(&cfg); + +// Create a lowpan6_ble driver instance. Note that each driver instance only +// supports a single LoWPAN6 BLE channel. +s_l6ble_handle = lowpan6_ble_create(); +if (s_l6ble_handle != NULL) +{ + // Finally, attach the driver instance to the netif you created above. + ESP_ERROR_CHECK(esp_netif_attach(lowpan6_ble_netif, s_l6ble_handle)); +} +``` + +All that is left to bring this netif up is to connect to a BLE peer that +supports LoWPAN6 BLE. You can discover potential peers during, e.g., GAP discovery: + +```c +// Note: no error handling included for brevity's sake +static int on_gap_event(struct ble_gap_event* event, void* arg) +{ + switch (event->type) + { + case BLE_GAP_EVENT_DISC: + // Verify that the discovered peer supports LoWPAN6 BLE + if (lowpan6_ble_connectable(&event->disc)) + { + // Cancel the current discovery process so we can connect + ble_gap_disc_cancel(); + + lowpan6_ble_connect( + s_l6ble_handle, // handle to the driver we created above + &event->disc.addr, // peer's BLE address + BLE_CONNECT_TIMEOUT, // GAP connect timeout in ms + on_lowpan6_ble_event, // user-defined callback + NULL // user-defined callback argument + ); + } + } +} +``` + +Once that netif is up, you should be able to communicate to the BLE peer with +generic network interfaces. For example, a UDP socket listening for messages +from any address using LwIP's BSD sockets API: + +```c +struct sockaddr_in6 server_addr; + +server_addr.sin6_family = AF_INET6; +server_addr.sin6_addr = in6addr_any; +server_addr.sin6_port = htons(PORT); + +int s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); +bind(s, (struct sockaddr*)&server_addr, sizeof(server_addr)); +int rc = recvfrom(s, buf, len, 0, client_addr, addr_len); +``` + +> :warning: If using LwIP's Raw API, be careful to separate message receipt +> from further communications. Raw API callbacks are serviced by the NimBLE +> event handler, which means that trying to send data in a callback can and +> will deadlock as the event handler waits for a BLE unstalled event that will +> never arrive. diff --git a/components/netif_6lowpan_ble/examples/.build-test-rules.yml b/components/netif_6lowpan_ble/examples/.build-test-rules.yml new file mode 100644 index 0000000000..7c555ba5f1 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/.build-test-rules.yml @@ -0,0 +1,5 @@ +components/netif_6lowpan_ble/examples: + disable: + - if: IDF_TARGET in ["esp32p4", "esp32h2"] + temporary: false + reason: No BLE support diff --git a/components/netif_6lowpan_ble/examples/echo/README.md b/components/netif_6lowpan_ble/examples/echo/README.md new file mode 100644 index 0000000000..d1cc7bf4ab --- /dev/null +++ b/components/netif_6lowpan_ble/examples/echo/README.md @@ -0,0 +1,106 @@ +# UDP echo client + +This is a quick-and-dirty example to demonstrate the bare minimum required to +communicate between two ESP-32 devices over LoWPAN6 BLE. + +These examples were developed and tested using ESP-IDF v5.1.2. + +These code samples are based on: +* client + * [bleprph](https://github.com/espressif/esp-idf/tree/master/examples/bluetooth/nimble/bleprph), demonstrating how to do GAP advertisement using NimBLE on ESP-IDF + * [udp_client](https://github.com/espressif/esp-idf/tree/master/examples/protocols/sockets/udp_client), demonstrating how to write a UDP client using esp-lwip's sockets API +* server + * [blecent](https://github.com/espressif/esp-idf/tree/master/examples/bluetooth/nimble/blecent), demonstrating how to do GAP discovery using NimBLE on ESP-IDF + * [udp_server](https://github.com/espressif/esp-idf/tree/master/examples/protocols/sockets/udp_server), demonstrating how to write a UDP server using esp-lwip's sockets API +* both: the boilerplate required to hook up the lowpan6_ble driver provided by this component + +You'll need to run both of these examples at the same time. For example, if you +had two ESP-32 devices connected via a USB-to-serial adapter: + +```console +cd client +idf.py build flash monitor -p /dev/ttyUSB0 +``` + +```console +cd server +idf.py build flash monitor -p /dev/ttyUSB1 +``` + +Replace these with whatever invocations are appropriate for your setup. + +The server will: +* start a UDP echo server on port 1234 +* scan for devices supporting the Internet Protocol Support Service (IPSS, GATT + service UUID 0x1820) +* establish a LoWPAN6 BLE connection to the first such device it finds + +The client will: +* initiate GAP advertisement with support for IPSS +* after being connected to, start sending messages to the server at port 1234 + + +## Example output + +Example client logs: +```console +I (550) main_task: Started on CPU0 +I (560) main_task: Calling app_main() +I (600) BTDM_INIT: BT controller compile version [ec4ac65] +I (610) BTDM_INIT: Bluetooth MAC: 55:44:33:22:11:00 +I (610) phy_init: phy_version 4780,16b31a7,Sep 22 2023,20:42:16 +I (930) main: BLE host task started +I (930) lowpan6_ble: (lowpan6_ble_create) creating lowpan6_ble driver +I (930) main: netif not up, waiting... +I (960) NimBLE: Failed to restore IRKs from store; status=8 + +I (970) NimBLE: GAP procedure initiated: advertise; +I (970) NimBLE: disc_mode=2 +I (970) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=0 adv_itvl_max=0 +I (980) NimBLE: + +I (1240) main: connection established; status=0 +I (1940) main: sending to FE80::0011:22FF:FE33:4455 +I (2020) main: Received 22 bytes: `echo: hello it's me!!!` +I (4020) main: sending to FE80::0011:22FF:FE33:4455 +I (4120) main: Received 22 bytes: `echo: hello it's me!!!` +I (6120) main: sending to FE80::0011:22FF:FE33:4455 +I (6220) main: Received 22 bytes: `echo: hello it's me!!!` + +... and so on, forever ... +``` + +Example server logs: +```console +I (552) main_task: Started on CPU0 +I (562) main_task: Calling app_main() +I (602) BTDM_INIT: BT controller compile version [ec4ac65] +I (602) BTDM_INIT: Bluetooth MAC: 00:11:22:33:44:55 +I (602) phy_init: phy_version 4780,16b31a7,Sep 22 2023,20:42:16 +I (912) main: BLE host task started +I (912) lowpan6_ble: (lowpan6_ble_create) creating lowpan6_ble driver +I (922) main: listening on port 1234 +I (922) main: waiting to receive... +I (952) NimBLE: Failed to restore IRKs from store; status=8 + +I (952) NimBLE: GAP procedure initiated: discovery; +I (952) NimBLE: own_addr_type=0 filter_policy=0 passive=1 limited=0 filter_duplicates=1 +I (962) NimBLE: duration=forever +I (962) NimBLE: + +I (2852) NimBLE: GAP procedure initiated: connect; +I (2852) NimBLE: peer_addr_type=0 peer_addr= +I (2852) NimBLE: 55:44:33:22:11:00 +I (2852) NimBLE: scan_itvl=16 scan_window=16 itvl_min=24 itvl_max=40 latency=0 supervision_timeout=256 min_ce_len=0 max_ce_len=0 own_addr_type=0 +I (2872) NimBLE: + +I (3102) main: BLE GAP connection established +I (3822) main: Received 16 bytes from addr=FE80::5544:33FF:FE22:1100 port=52324: hello it's me!!! +I (3832) main: waiting to receive... +I (5922) main: Received 16 bytes from addr=FE80::5544:33FF:FE22:1100 port=52324: hello it's me!!! +I (5932) main: waiting to receive... +I (8022) main: Received 16 bytes from addr=FE80::5544:33FF:FE22:1100 port=52324: hello it's me!!! +I (8032) main: waiting to receive... + +... and so on, forever ... +``` diff --git a/components/netif_6lowpan_ble/examples/udp_client/CMakeLists.txt b/components/netif_6lowpan_ble/examples/udp_client/CMakeLists.txt new file mode 100644 index 0000000000..6c7f084387 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_client/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +project(example-client + VERSION 1.0 + LANGUAGES C +) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_COLOR_DIAGNOSTICS ON) diff --git a/components/netif_6lowpan_ble/examples/udp_client/main/CMakeLists.txt b/components/netif_6lowpan_ble/examples/udp_client/main/CMakeLists.txt new file mode 100644 index 0000000000..9a2cec70d6 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_client/main/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS + main.c + REQUIRES + netif_6lowpan_ble + nvs_flash +) diff --git a/components/netif_6lowpan_ble/examples/udp_client/main/idf_component.yml b/components/netif_6lowpan_ble/examples/udp_client/main/idf_component.yml new file mode 100644 index 0000000000..42c2440719 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_client/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + espressif/netif_6lowpan_ble: + version: "*" + override_path: "../../.." diff --git a/components/netif_6lowpan_ble/examples/udp_client/main/main.c b/components/netif_6lowpan_ble/examples/udp_client/main/main.c new file mode 100644 index 0000000000..15a6363989 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_client/main/main.c @@ -0,0 +1,311 @@ +/* + * SPDX-FileCopyrightText: 2024 Tenera Care + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "host/ble_hs.h" +#include "host/util/util.h" +#include "lowpan6_ble.h" +#include "lwip/sockets.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "nvs_flash.h" +#include "services/gap/ble_svc_gap.h" + +#include +#include +#include + +static const char *TAG = "main"; + +static uint8_t own_addr_type; +struct sockaddr_in6 dest_addr; + +#define PORT 1234 + +static void do_advertise(); // forward declaration + +static int on_gap_event(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + // A new connection was established or a connection attempt failed. + ESP_LOGI( + TAG, + "connection %s; status=%d", + event->connect.status == 0 ? "established" : "failed", + event->connect.status + ); + + if (event->connect.status != 0) { + // Connection failed; resume advertising. + do_advertise(); + } else { + // We've had a peer connect to us! We'll store their address in `dest_addr` so the + // `udp_task` function below can send messages to the correct destination. + struct ble_gap_conn_desc desc; + int rc = ble_gap_conn_find(event->connect.conn_handle, &desc); + assert(rc == 0); + ip6_addr_t peer_ip_addr; + ble_addr_to_link_local(&desc.peer_id_addr, &peer_ip_addr); + + dest_addr.sin6_addr.un.u32_addr[0] = peer_ip_addr.addr[0]; + dest_addr.sin6_addr.un.u32_addr[1] = peer_ip_addr.addr[1]; + dest_addr.sin6_addr.un.u32_addr[2] = peer_ip_addr.addr[2]; + dest_addr.sin6_addr.un.u32_addr[3] = peer_ip_addr.addr[3]; + } + return 0; + + case BLE_GAP_EVENT_DISCONNECT: + ESP_LOGI(TAG, "disconnect; reason=%d", event->disconnect.reason); + do_advertise(); + return 0; + + case BLE_GAP_EVENT_ADV_COMPLETE: + ESP_LOGI(TAG, "advertise complete; reason=%d", event->adv_complete.reason); + do_advertise(); + return 0; + + default: + return 0; + } + + return 0; +} + +/** Start BLE advertisement. + * + * This is primarily pulled from ESP-IDF's NimBLE bleprph example: + * - + * https://github.com/espressif/esp-idf/blob/master/examples/bluetooth/nimble/bleprph/main/bleprph.h + * + * The only change being we advertise support for IPSS. + */ +static void do_advertise() +{ + struct ble_gap_adv_params adv_params; + struct ble_hs_adv_fields fields; + const char *name; + int rc; + + memset(&fields, 0, sizeof fields); + + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + fields.tx_pwr_lvl_is_present = 1; + fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; + name = ble_svc_gap_device_name(); + fields.name = (uint8_t *)name; + fields.name_len = strlen(name); + fields.name_is_complete = 1; + + // Advertise support for the Internet Protocol Support Services (IPSS). + fields.uuids16 = (ble_uuid16_t[]) { + BLE_UUID16_INIT(LOWPAN6_BLE_SERVICE_UUID_IPSS) + }; + fields.num_uuids16 = 1; + fields.uuids16_is_complete = 1; + + rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + ESP_LOGE(TAG, "Error setting advertisement data; rc=%d\n", rc); + return; + } + + /* Begin advertising. */ + memset(&adv_params, 0, sizeof adv_params); + adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; + adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; + rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params, on_gap_event, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Error enabling advertisement; rc=%d\n", rc); + return; + } +} + +static void on_reset(int reason) +{ + ESP_LOGE(TAG, "BLE state reset; reason=%d", reason); +} + +static void on_sync() +{ + int rc = ble_hs_util_ensure_addr(0); + if (rc != 0) { + ESP_ERROR_CHECK(ESP_FAIL); + } + + rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to determine address type; rc=%d", rc); + return; + } + + do_advertise(); +} + +void nimble_task(void *params) +{ + ESP_LOGI(TAG, "BLE host task started"); + + nimble_port_run(); + nimble_port_freertos_deinit(); +} + +void udp_task(esp_netif_t *lowpan6_netif) +{ + char rx_buffer[128]; + + // Wait for the netif to come up. This is a pretty graceless way to do this -- the better way + // would be to add a `lowpan6_ble` event for "netif up" that we can subscribe to in our + // `lowpan6_ble_create_server` callback. Not currently implemented though so for the sake of + // this example... + while (!esp_netif_is_netif_up(lowpan6_netif)) { + ESP_LOGI(TAG, "netif not up, waiting..."); + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + // Set our destination address family, port, and scope ID. We'll set the `dest_addr.sin6_addr` + // in our GAP event callback: our peer's sin6_addr will be calculated from its BLE address. + // + // Note that it is important to set the `sin6_scope_id` here!!! It will be used by lwIP in + // `sendto` to determine which netif we should use to send this UDP packet. + dest_addr.sin6_family = AF_INET6; + dest_addr.sin6_port = htons(PORT); + dest_addr.sin6_scope_id = esp_netif_get_netif_impl_index(lowpan6_netif); + + int sock = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + ESP_LOGE(TAG, "Failed to create socket; errno=%d", errno); + return; + } + + // We'll set a socket timeout for receives here so we continue trying to send messages even if + // the peer gets disconnected or something. + struct timeval tv; + tv.tv_sec = 2; + tv.tv_usec = 0; + if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { + ESP_LOGE(TAG, "Failed to set socket timeout"); + return; + } + + const char *payload = "hello it's me!!!"; + while (1) { + char addr_str[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &dest_addr.sin6_addr, addr_str, INET6_ADDRSTRLEN); + ESP_LOGI(TAG, "sending to %s", addr_str); + + // Fire a message over to the dest_addr. Note that the `sin6_addr` for this gets set in our + // GAP connect callback. + int err = sendto( + sock, + payload, + strlen(payload), + 0, + (struct sockaddr *)&dest_addr, + sizeof(dest_addr) + ); + if (err < 0) { + ESP_LOGE(TAG, "Failed to send payload; errno=%d", errno); + break; + } + + // Wait for a response from the echo server. + struct sockaddr_in6 recv_addr; + socklen_t recv_addr_len = sizeof(recv_addr); + int len = recvfrom( + sock, + rx_buffer, + sizeof(rx_buffer) - 1, + 0, + (struct sockaddr *)&recv_addr, + &recv_addr_len + ); + if (len < 0) { + if (errno == EAGAIN) { + ESP_LOGD(TAG, "Receive timed out"); + } else { + ESP_LOGE(TAG, "Failed to receive from socket; errno=%d", errno); + break; + } + } else { + rx_buffer[len] = 0; // null terminate whatever we got + ESP_LOGI(TAG, "Received %d bytes: `%s`", len, rx_buffer); + } + + vTaskDelay(pdMS_TO_TICKS(2000)); + } + + if (sock != -1) { + shutdown(sock, 0); + close(sock); + } +} + +void app_main() +{ + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); + + // Initialize `lowpan6_ble` and the ESP-NETIF resources it requires. + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + ESP_ERROR_CHECK(lowpan6_ble_init()); + + // Initialize NimBLE + ESP_ERROR_CHECK(nimble_port_init()); + ble_hs_cfg.reset_cb = on_reset; + ble_hs_cfg.sync_cb = on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + int rc = ble_svc_gap_device_name_set("l6ble-client"); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to set GAP device name; rc=%d", rc); + return; + } + + nimble_port_freertos_init(nimble_task); + + // Add a new lowpan6_ble netif. + esp_netif_inherent_config_t base = ESP_NETIF_INHERENT_DEFAULT_LOWPAN6_BLE(); + + esp_netif_config_t cfg = { + .base = &base, + .driver = NULL, + .stack = netstack_default_lowpan6_ble, + }; + + esp_netif_t *lowpan6_ble_netif = esp_netif_new(&cfg); + + lowpan6_ble_driver_handle lowpan6_ble_driver = lowpan6_ble_create(); + if (lowpan6_ble_driver != NULL) { + ESP_ERROR_CHECK(esp_netif_attach(lowpan6_ble_netif, lowpan6_ble_driver)); + } + + // Finally, register our `lowpan6_ble_driver` as an L2CAP server. This will hook L2CAP events + // into the required driver events. + lowpan6_ble_create_server(lowpan6_ble_driver, NULL, NULL); + + // Use this main thread to run our UDP task forever. + udp_task(lowpan6_ble_netif); +} diff --git a/components/netif_6lowpan_ble/examples/udp_client/sdkconfig.defaults b/components/netif_6lowpan_ble/examples/udp_client/sdkconfig.defaults new file mode 100644 index 0000000000..3fee45b332 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_client/sdkconfig.defaults @@ -0,0 +1,13 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 5.4.0 Project Minimal Configuration +# +CONFIG_APP_RETRIEVE_LEN_ELF_SHA=16 +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM=1 +CONFIG_ESP_WIFI_GMAC_SUPPORT=n +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744 +CONFIG_LWIP_TCP_WND_DEFAULT=5744 +CONFIG_LWIP_PPP_SUPPORT=y +CONFIG_LWIP_PPP_SERVER_SUPPORT=y +CONFIG_MBEDTLS_ECP_RESTARTABLE=y diff --git a/components/netif_6lowpan_ble/examples/udp_server/CMakeLists.txt b/components/netif_6lowpan_ble/examples/udp_server/CMakeLists.txt new file mode 100644 index 0000000000..7854547220 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_server/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +project(example-server + VERSION 1.0 + LANGUAGES C +) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_COLOR_DIAGNOSTICS ON) diff --git a/components/netif_6lowpan_ble/examples/udp_server/main/CMakeLists.txt b/components/netif_6lowpan_ble/examples/udp_server/main/CMakeLists.txt new file mode 100644 index 0000000000..9a2cec70d6 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_server/main/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register( + SRCS + main.c + REQUIRES + netif_6lowpan_ble + nvs_flash +) diff --git a/components/netif_6lowpan_ble/examples/udp_server/main/idf_component.yml b/components/netif_6lowpan_ble/examples/udp_server/main/idf_component.yml new file mode 100644 index 0000000000..42c2440719 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_server/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + espressif/netif_6lowpan_ble: + version: "*" + override_path: "../../.." diff --git a/components/netif_6lowpan_ble/examples/udp_server/main/main.c b/components/netif_6lowpan_ble/examples/udp_server/main/main.c new file mode 100644 index 0000000000..4b31e7c8d2 --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_server/main/main.c @@ -0,0 +1,318 @@ +/* + * SPDX-FileCopyrightText: 2024 Tenera Care + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "host/ble_hs.h" +#include "host/ble_store.h" +#include "host/util/util.h" +#include "lowpan6_ble.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "nvs_flash.h" +#include "services/gap/ble_svc_gap.h" + +#include +#include +#include + +//! How many ms to wait for GAP connection +#define BLE_CONNECT_TIMEOUT 10000 + +//! What port we'll listen on. +#define PORT 1234 + +static const char *TAG = "main"; + +static bool do_scan(); +static int on_gap_event(struct ble_gap_event *event, void *arg); + +static lowpan6_ble_driver_handle s_l6ble_handle; + +static inline int on_gap_connected(struct ble_gap_event *event) +{ + if (event->connect.status == 0) { + ESP_LOGI(TAG, "BLE GAP connection established"); + } else { + ESP_LOGE(TAG, "BLE GAP connection failed; status=%d", event->connect.status); + do_scan(); + } + + return 0; +} + +static inline int on_gap_disconnected(struct ble_gap_event *event) +{ + ESP_LOGI(TAG, "BLE GAP connection disconnected; reason=%d", event->disconnect.reason); + + do_scan(); + + return 0; +} + +static void on_lowpan6_ble_event( + lowpan6_ble_driver_handle handle, + struct lowpan6_ble_event *event, + void *userdata +) +{ + switch (event->type) { + case LOWPAN6_BLE_EVENT_GAP_CONNECTED: + on_gap_connected(event->gap_connected.event); + break; + case LOWPAN6_BLE_EVENT_GAP_DISCONNECTED: + on_gap_disconnected(event->gap_connected.event); + break; + } +} + +//! On discover, connect to _any_ device that advertises IPSS support. +static inline int on_gap_event_discovery(struct ble_gap_event *event) +{ + struct ble_hs_adv_fields fields; + int rc = ble_hs_adv_parse_fields(&fields, event->disc.data, event->disc.length_data); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to parse advertisement fields; rc=%d", rc); + return 0; + } + + if (lowpan6_ble_connectable(&event->disc)) { + // Cancel the scan so we can use the BLE device for connecting + rc = ble_gap_disc_cancel(); + if (rc != 0 && rc != BLE_HS_EALREADY) { + ESP_LOGE(TAG, "Failed to cancel scan; rc=%d", rc); + return rc; + } + + // Kick off a `lowpan6_ble` connection and register `on_lowpan6_ble_event` as a callback for + // lowpan6 ble events. Note that the `lowpan6_ble` driver will replace the GAP event + // callback in NimBLE here! + esp_err_t err = lowpan6_ble_connect( + s_l6ble_handle, + &event->disc.addr, + BLE_CONNECT_TIMEOUT, + on_lowpan6_ble_event, + NULL + ); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to lowpan6_ble_connect; err=%d", err); + } + } + + return 0; +} + +static int on_gap_event(struct ble_gap_event *event, void *arg) +{ + switch (event->type) { + case BLE_GAP_EVENT_DISC: + return on_gap_event_discovery(event); + + default: + ESP_LOGD(TAG, "Ignoring BLE GAP event with type %d", event->type); + break; + } + + return 0; +} + +static bool do_scan() +{ + uint8_t own_addr_type; + int rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to automatically infer address type; rc=%d", rc); + return false; + } + + struct ble_gap_disc_params disc_params; + + disc_params.filter_duplicates = 1; + disc_params.passive = 1; + disc_params.itvl = 0; + disc_params.window = 0; + disc_params.filter_policy = 0; + disc_params.limited = 0; + + // Start discovery and configure `on_gap_event` as our event callback. We'll use this to + // initiate a lowpan6_ble connection once we've found the peer we want to connect to. + rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params, on_gap_event, NULL); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to start GAP discovery; rc=%d", rc); + return false; + } + + return true; +} + +void on_sync() +{ + int rc = ble_hs_util_ensure_addr(0); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to ensure addr"); + return; + } + + do_scan(); +} + +void on_reset(int reason) +{ + ESP_LOGI(TAG, "Resetting state; reason=%d", reason); +} + +void nimble_host_task(void *param) +{ + ESP_LOGI(TAG, "BLE host task started"); + nimble_port_run(); + + nimble_port_freertos_deinit(); +} + +void udp_task() +{ + // Accept incoming connections from any address on port PORT. + struct sockaddr_in6 server_addr; + server_addr.sin6_family = AF_INET6; + server_addr.sin6_addr = in6addr_any; + server_addr.sin6_port = htons(PORT); + + int s = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (s < 0) { + ESP_LOGE(TAG, "failed to create socket; rc=%d", s); + return; + } + + if (bind(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { + ESP_LOGE(TAG, "failed to bind address"); + close(s); + return; + } + + // We'll explicitly set 0 timeout here so we wait forever for incoming messages. + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { + ESP_LOGE(TAG, "failed to clear socket recv timeout"); + return; + } + + ESP_LOGI(TAG, "listening on port %d", PORT); + + // These buffers are hard-code sized to match the buffers in the corresponding client. + uint8_t rx_buffer[128]; + uint8_t tx_buffer[192]; // A bit bigger to fit the extra characters we add to responses. + while (1) { + ESP_LOGI(TAG, "waiting to receive..."); + struct sockaddr_in6 recv_addr; + socklen_t recv_addr_len = sizeof(recv_addr); + + int len = recvfrom( + s, + rx_buffer, + sizeof(rx_buffer), + 0, + (struct sockaddr *)&recv_addr, + &recv_addr_len + ); + if (len < 0) { + ESP_LOGE(TAG, "Failed to receive from socket; errno=%d", errno); + break; + } + + // NULL terminate whatever we received + rx_buffer[len] = 0; + + char straddr[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &recv_addr.sin6_addr, straddr, INET6_ADDRSTRLEN); + ESP_LOGI( + TAG, + "Received %d bytes from addr=%s port=%u: %s", + len, + straddr, + ntohs(recv_addr.sin6_port), + rx_buffer + ); + + // Prepare our response (prepend their message with `echo: ` + snprintf((char *)tx_buffer, 192, "echo: %s", rx_buffer); + + // Respond! + int rc = sendto( + s, + tx_buffer, + strlen((char *)tx_buffer), + 0, + (struct sockaddr *)&recv_addr, + recv_addr_len + ); + if (rc < 0) { + ESP_LOGE(TAG, "Failed to send to socket; errno=%d", errno); + } + } +} + +void app_main() +{ + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); + + // Initialize `lowpan6_ble` and the ESP-NETIF resources it requires. + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + ESP_ERROR_CHECK(lowpan6_ble_init()); + + // Initialize NimBLE + ESP_ERROR_CHECK(nimble_port_init()); + ble_hs_cfg.reset_cb = on_reset; + ble_hs_cfg.sync_cb = on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + int rc = ble_svc_gap_device_name_set("l6ble-server"); + if (rc != 0) { + ESP_LOGE(TAG, "Failed to set GAP device name; rc=%d", rc); + return; + } + + nimble_port_freertos_init(nimble_host_task); + + // Add a new lowpan6_ble netif. + esp_netif_inherent_config_t base_cfg = ESP_NETIF_INHERENT_DEFAULT_LOWPAN6_BLE(); + + esp_netif_config_t cfg = { + .base = &base_cfg, + .driver = NULL, + .stack = netstack_default_lowpan6_ble, + }; + + esp_netif_t *lowpan6_ble_netif = esp_netif_new(&cfg); + + s_l6ble_handle = lowpan6_ble_create(); + if (s_l6ble_handle != NULL) { + ESP_ERROR_CHECK(esp_netif_attach(lowpan6_ble_netif, s_l6ble_handle)); + } + + // Use this main thread to run our UDP task forever. + udp_task(); +} diff --git a/components/netif_6lowpan_ble/examples/udp_server/sdkconfig.defaults b/components/netif_6lowpan_ble/examples/udp_server/sdkconfig.defaults new file mode 100644 index 0000000000..878fd1935c --- /dev/null +++ b/components/netif_6lowpan_ble/examples/udp_server/sdkconfig.defaults @@ -0,0 +1,12 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 5.4.0 Project Minimal Configuration +# +CONFIG_APP_RETRIEVE_LEN_ELF_SHA=16 +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM=1 +CONFIG_ESP_WIFI_GMAC_SUPPORT=n +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5744 +CONFIG_LWIP_TCP_WND_DEFAULT=5744 +CONFIG_LWIP_DEBUG=y +CONFIG_MBEDTLS_ECP_RESTARTABLE=y diff --git a/components/netif_6lowpan_ble/idf_component.yml b/components/netif_6lowpan_ble/idf_component.yml new file mode 100644 index 0000000000..3e878c90a7 --- /dev/null +++ b/components/netif_6lowpan_ble/idf_component.yml @@ -0,0 +1,6 @@ +version: 0.1.0 +url: https://github.com/espressif/esp-protocols/tree/master/components/6lwopan_ble +description: Simple 6lowpan_ble network interface for esp_netif +dependencies: + idf: + version: '>=5.2' diff --git a/components/netif_6lowpan_ble/include/lowpan6_ble.h b/components/netif_6lowpan_ble/include/lowpan6_ble.h new file mode 100644 index 0000000000..9caebe5784 --- /dev/null +++ b/components/netif_6lowpan_ble/include/lowpan6_ble.h @@ -0,0 +1,337 @@ +/* + * SPDX-FileCopyrightText: 2024 Tenera Care + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "esp_compiler.h" +#include "esp_idf_version.h" +#include "host/ble_gap.h" +#include "host/ble_l2cap.h" +#include "lwip/ip6_addr.h" +#include "nimble/ble.h" + +#include + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) +// Add missing includes to esp_netif_types.h +// https://github.com/espressif/esp-idf/commit/822129e234aaedf86e76fe92ab9b49c5f0a612e0 +#include "esp_err.h" +#include "esp_event_base.h" +#include "esp_netif_ip_addr.h" + +#include +#include +#endif + +#include "esp_netif_types.h" + +/** Maximum concurrent IPSP channels */ +#define LOWPAN6_BLE_IPSP_MAX_CHANNELS 1 + +/** Maximum Transmit Unit on an IPSP channel. + * + * This is required by the specification to be 1280 (it's the minimum MTU for + * IPv6). + */ +#define LOWPAN6_BLE_IPSP_MTU 1280 + +/** Maximum data size that can be received. + * + * This value can be modified to be lower than the MTU set on the channel. + */ +#define LOWPAN6_BLE_IPSP_RX_BUFFER_SIZE 1280 + +/** Maximum number of receive buffers. + * + * Each receive buffer is of size LOWPAN6_BLE_IPSP_RX_BUFFER_SIZE. Tweak this + * value to modify the number of Service Data Units (SDUs) that can be received + * while an SDU is being consumed by the application. + */ +#define LOWPAN6_BLE_IPSP_RX_BUFFER_COUNT 4 + +/** The IPSP L2CAP Protocol Service Multiplexer number. + * + * Defined by the Bluetooth Low Energy specification. See: + * https://www.bluetooth.com/specifications/assigned-numbers/ + */ +#define LOWPAN6_BLE_IPSP_PSM 0x0023 + +/** The BLE Service UUID for the Internet Protocol Support Service + * + * Defined by the Bluetooth Low Energy specification. See: + * https://www.bluetooth.com/specifications/assigned-numbers/ + */ +#define LOWPAN6_BLE_SERVICE_UUID_IPSS 0x1820 + +// clang-format off +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +#define ESP_NETIF_INHERENT_DEFAULT_LOWPAN6_BLE() { \ + ESP_COMPILER_DESIGNATED_INIT_AGGREGATE_TYPE_EMPTY(mac) \ + ESP_COMPILER_DESIGNATED_INIT_AGGREGATE_TYPE_EMPTY(ip_info) \ + .get_ip_event = 0, \ + .lost_ip_event = 0, \ + .if_key = "LOWPAN6_BLE_DEF", \ + .if_desc = "lowpan6_ble", \ + .route_prio = 16, \ + .bridge_info = NULL \ +} +#else +#define ESP_NETIF_INHERENT_DEFAULT_LOWPAN6_BLE() { \ + ESP_COMPILER_DESIGNATED_INIT_AGGREGATE_TYPE_EMPTY(mac) \ + ESP_COMPILER_DESIGNATED_INIT_AGGREGATE_TYPE_EMPTY(ip_info) \ + .get_ip_event = 0, \ + .lost_ip_event = 0, \ + .if_key = "LOWPAN6_BLE_DEF", \ + .if_desc = "lowpan6_ble", \ + .route_prio = 16, \ +} +#endif +// clang-format on + +enum lowpan6_ble_event_type { + LOWPAN6_BLE_EVENT_GAP_CONNECTED, + LOWPAN6_BLE_EVENT_GAP_DISCONNECTED +}; + +//! Event struct for LoWPAN6 BLE events. +struct lowpan6_ble_event { + //! Discriminator for the event data included in this event. + enum lowpan6_ble_event_type type; + + union { + //! Data available for type LOWPAN6_BLE_EVENT_GAP_CONNECTED. + struct { + //! The underlying GAP event. + struct ble_gap_event *event; + } gap_connected; + + //! Data available for type LOWPAN6_BLE_EVENT_GAP_DISCONNECTED. + struct { + //! The underlying GAP event. + struct ble_gap_event *event; + } gap_disconnected; + }; +}; + +extern esp_netif_netstack_config_t *netstack_default_lowpan6_ble; + +typedef struct lowpan6_ble_driver *lowpan6_ble_driver_handle; + +/** A LoWPAN6 BLE event handler + * + * @param[in] handle Handle to the lowpan6_ble driver processing this event. + * @param[in] event Pointer to a lowpan6_ble_event struct. + * @param[in] userdata (Optional) Arbitrary data provided by the user during callback registration. + */ +typedef void (*lowpan6_ble_event_handler)( + lowpan6_ble_driver_handle handle, + struct lowpan6_ble_event *event, + void *userdata +); + +/** Initialize the LoWPAN6 BLE module. + * + * This must be called once before creating lowpan6_ble drivers. + * + * @returns ESP_OK on success + */ +esp_err_t lowpan6_ble_init(); + +/** Create a new lowpan6_ble driver instance. + * + * @example + * ```c + * void app_main() + * { + * esp_netif_config_t cfg = { + * .base = ESP_NETIF_INHERENT_DEFAULT_LOWPAN6_BLE(), + * .driver = NULL, + * .stack = netstack_default_lowpan6_ble, + * }; + * + * esp_netif_t* lowpan6_ble_netif = esp_netif_new(&cfg); + * + * lowpan6_ble_driver_handle lowpan6_ble_driver = lowpan6_ble_create(); + * if (lowpan6_ble_driver != NULL) + * { + * ESP_ERROR_CHECK(esp_netif_attach(lowpan6_ble_netif, lowpan6_ble_driver)); + * } + * } + * ``` + * + * @returns A lowpan6_ble_driver_handle on success, NULL otherwise. + */ +lowpan6_ble_driver_handle lowpan6_ble_create(); + +/** Destroy the given lowpan6_ble_driver, freeing its resources. + * + * @param[in] driver The driver to destroy + * + * @returns ESP_OK on success + */ +esp_err_t lowpan6_ble_destroy(lowpan6_ble_driver_handle driver); + +/** Determine if the advertising device can be connected over LoWPAN6 BLE + * + * This verifies that the device: + * - is BLE connectable + * - advertises support for the necessary Internet Protocol Support Service + * + * @example + * ```c + * static int my_gap_event_handler(struct ble_gap_event* event, void* arg) + * { + * switch (event->type) + * { + * case BLE_GAP_EVENT_DISC: + * if (l6ble_io_connectable(event->disc)) + * { + * ESP_LOGD(TAG, "can connect with lowpan6"); + * } + * else + * { + * ESP_LOGD(TAG, "can't connect with lowpan6"); + * } + * default: + * // ignored + * break; + * } + * } + * ``` + * + * @param[in] disc The BLE GAP discovery descriptor. Typically obtained from a BLE_GAP_EVENT_DISC + * callback. + * + * @returns True if the device is connectable, False otherwise. + */ +bool lowpan6_ble_connectable(struct ble_gap_disc_desc *disc); + +/** Establish a LoWPAN6 BLE connection with the given BLE address. + * + * This creates a GAP connection and attaches a callback that will ultimately establish an IPSP + * connection with the device. + * + * @note The user should cancel discover before calling this function. + * + * @note This function will REPLACE any existing GAP event callback (i.e., the callback you set to + * process the BLE_GAP_EVENT_DISC event). + * + * @example + * ``` + * static int my_gap_event_handler(struct ble_gap_event* event, void* arg) + * { + * switch (event->type) + * { + * case BLE_GAP_EVENT_DISC: + * if (lowpan6_ble_connectable(event->disc)) + * { + * rc = ble_gap_disc_cancel(); + * if (rc != 0 && rc != BLE_HS_EALREADY) + * { + * ESP_LOGE(TAG, "Failed to cancel scan; rc=%d", rc); + * return rc; + * } + * + * lowpan6_ble_connect(l6ble_handle, &event->disc.addr, 10000, NULL, NULL); + * } + * default: + * // ignored + * break; + * } + * } + * ``` + * + * @param[in] handle The lowpan6_ble instance to associate with this BLE connection. + * @param[in] addr The BLE address to connect to. + * @param[in] timeout_ms The maximum timeout for connecting, in milliseconds. + * @param[in] cb (Optional) An event handler for lowpan6_ble events. + * @param[in] userdata (Optional) Arbitrary data to pass to `cb` during LoWPAN6 BLE events. + * + * @returns ESP_OK if the connection was initiated successfully. Other codes + * indicate error. Note that success here simply means that the GAP connection + * was initiated. Further connection failures/successes are communicated via + * BLE callback events. + */ +esp_err_t lowpan6_ble_connect( + lowpan6_ble_driver_handle handle, + ble_addr_t *addr, + int32_t timeout_ms, + lowpan6_ble_event_handler cb, + void *userdata +); + +/** Disconnect the given LoWPAN6 BLE connection. + * + * This function does nothing if the instance is not already connected. + * + * @param[in] handle The lowpan6_ble instance to disconnect. + */ +esp_err_t lowpan6_ble_disconnect(lowpan6_ble_driver_handle handle); + +/** A NimBLE ble_l2cap_event_fn used to create an L2CAP server for LoWPAN6 BLE connections. + * + * This should be used to register an L2CAP server on LoWPAN6 BLE nodes (i.e., the devices + * _accepting_ a connection request from the central device). + * + * @example + * ``` + * void app_main() + * { + * esp_netif_config_t cfg = { + * .base = ESP_NETIF_INHERENT_DEFAULT_LOWPAN6_BLE(), + * .driver = NULL, + * .stack = netstack_default_lowpan6_ble, + * }; + * + * esp_netif_t* lowpan6_ble_netif = esp_netif_new(&cfg); + * + * lowpan6_ble_driver_handle lowpan6_ble_driver = lowpan6_ble_create(); + * if (lowpan6_ble_driver != NULL) + * { + * ESP_ERROR_CHECK(esp_netif_attach(lowpan6_ble_netif, lowpan6_ble_driver)); + * } + * + * // this will hook up our driver to our L2CAP channel once the other + * // end initiates the connection + * lowpan6_ble_create_server(lowpan6_ble_driver, NULL, NULL); + * } + * ``` + * + * @param[in] handle The lowpan6_ble instance to associate with this BLE connection. + * @param[in] cb (Optional) An event handler for lowpan6_ble events. + * @param[in] userdata (Optional) Arbitrary data to pass to `cb` during LoWPAN6 BLE events. + * + * @returns ESP_OK if the connection was initiated successfully. Other codes + * indicate error. Note that success here simply means that the GAP connection + * was initiated. Further connection failures/successes are communicated via + * BLE callback events. + */ +esp_err_t lowpan6_ble_create_server( + lowpan6_ble_driver_handle handle, + lowpan6_ble_event_handler cb, + void *userdata +); + +/** Transform the given BLE address into a link-local IPv6 address. + * + * @param[in] ble_addr The BLE address to transform. + * @param[out] ip_addr The output IPv6 address. + * + * @returns ESP_OK on success. + */ +esp_err_t ble_addr_to_link_local(ble_addr_t *ble_addr, ip6_addr_t *ip_addr); diff --git a/components/netif_6lowpan_ble/src/debug_print_utils.c b/components/netif_6lowpan_ble/src/debug_print_utils.c new file mode 100644 index 0000000000..88fdf14faa --- /dev/null +++ b/components/netif_6lowpan_ble/src/debug_print_utils.c @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2024 Tenera Care + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "debug_print_utils.h" + +char *debug_print_ble_addr(ble_addr_t *addr) +{ + // 2 char per byte * 6 bytes + 5 colon + 1 null terminator + // 12 + 5 + 1 == 18 + static char addr_buf[18]; + + if (addr != NULL) { + // NimBLE addresses are stored in reverse order so we print these from index 5 to 0 + snprintf( + addr_buf, + 18, + "%02X:%02X:%02X:%02X:%02X:%02X", + addr->val[5], + addr->val[4], + addr->val[3], + addr->val[2], + addr->val[1], + addr->val[0] + ); + } else { + strcpy(addr_buf, "null"); + } + + return addr_buf; +} diff --git a/components/netif_6lowpan_ble/src/debug_print_utils.h b/components/netif_6lowpan_ble/src/debug_print_utils.h new file mode 100644 index 0000000000..86627944c0 --- /dev/null +++ b/components/netif_6lowpan_ble/src/debug_print_utils.h @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2024 Tenera Care + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "nimble/ble.h" + +/** Produce a BLE addr for printing. + * + * Note that this returns a _static_ buffer and so can only be used once per log line (otherwise + * you'll overwrite it). + * + * @param[in] addr The Bluetooth address to print. + */ +char *debug_print_ble_addr(ble_addr_t *addr); diff --git a/components/netif_6lowpan_ble/src/lowpan6_ble.c b/components/netif_6lowpan_ble/src/lowpan6_ble.c new file mode 100644 index 0000000000..ca323ad35e --- /dev/null +++ b/components/netif_6lowpan_ble/src/lowpan6_ble.c @@ -0,0 +1,533 @@ +/* + * SPDX-FileCopyrightText: 2024 Tenera Care + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "lowpan6_ble.h" + +#include "debug_print_utils.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/portmacro.h" +#include "host/ble_hs.h" +#include "lowpan6_ble_netif.h" +#include "nimble/ble.h" + +/** The chunk of defines below are used to initialize mbufs later in this module. + * + * The MyNewt documentation explains what these are: + * https://mynewt.apache.org/latest/os/core_os/mbuf/mbuf.html + */ +#define MBUF_PKTHDR_OVERHEAD sizeof(struct os_mbuf_pkthdr) +#define MBUF_MEMBLOCK_OVERHEAD sizeof(struct os_mbuf) + MBUF_PKTHDR_OVERHEAD +#define MBUF_NUM_MBUFS (LOWPAN6_BLE_IPSP_MAX_CHANNELS * LOWPAN6_BLE_IPSP_RX_BUFFER_COUNT) +#define MBUF_PAYLOAD_SIZE LOWPAN6_BLE_IPSP_RX_BUFFER_SIZE +#define MBUF_BUF_SIZE OS_ALIGN(MBUF_PAYLOAD_SIZE, 4) +#define MBUF_MEMBLOCK_SIZE (MBUF_BUF_SIZE + MBUF_MEMBLOCK_OVERHEAD) +#define MBUF_MEMPOOL_SIZE OS_MEMPOOL_SIZE(MBUF_NUM_MBUFS, MBUF_MEMBLOCK_SIZE) + +static const char *TAG = "lowpan6_ble"; + +//! Pool of mbufs, shared by all the channels in this module. +static struct os_mbuf_pool s_mbuf_pool; + +//! Memory pool used to initialize our mbuf pool, above. +static struct os_mempool s_mempool; + +//! Memory allocated for our memory pool, above. +static os_membuf_t s_membuf[MBUF_MEMPOOL_SIZE]; + +#define BIT_TX_UNSTALLED (1 << 0) +static StaticEventGroup_t s_lowpan6_event_group_buffer; +static EventGroupHandle_t s_lowpan6_event_group; + +/** LoWPAN6 BLE driver + * + * This struct provides glue logic between esp_netif and the BLE channel used as a transport. + */ +struct lowpan6_ble_driver { + // esp_netif driver base + esp_netif_driver_base_t base; + + // Connection handle for our GAP connection. BLE_HS_CONN_HANDLE_NONE if not connected. + uint16_t conn_handle; + + // Pointer to L2CAP channel used for LoWPAN6-BLE + struct ble_l2cap_chan *chan; + + // (Optional) event handler provided by the user. + lowpan6_ble_event_handler cb; + + // (Optional) event handler data provided by the user. + void *userdata; +}; + +void user_notify(struct lowpan6_ble_driver *driver, struct lowpan6_ble_event *event) +{ + if (driver && driver->cb && event) { + driver->cb(driver, event, driver->userdata); + } +} + +static inline int set_recv_ready(struct ble_l2cap_chan *chan) +{ + struct os_mbuf *next = os_mbuf_get_pkthdr(&s_mbuf_pool, 0); + + int rc = ble_l2cap_recv_ready(chan, next); + return rc; +} + +static int on_l2cap_event(struct ble_l2cap_event *event, void *arg) +{ + ESP_LOGD(TAG, "(%s) event->type=%d", __func__, event->type); + + struct lowpan6_ble_driver *driver = (struct lowpan6_ble_driver *)arg; + + int rc = 0; + switch (event->type) { + case BLE_L2CAP_EVENT_COC_CONNECTED: + driver->conn_handle = event->connect.conn_handle; + driver->chan = event->connect.chan; + struct ble_gap_conn_desc desc; + rc = ble_gap_conn_find(event->connect.conn_handle, &desc); + if (rc != 0) { + ESP_LOGE( + TAG, + "(%s) could not find GAP conn with handle %d", + __func__, + event->connect.conn_handle + ); + return rc; + } + + lowpan6_ble_netif_up(driver->base.netif, &desc.peer_id_addr, &desc.our_id_addr); + break; + + case BLE_L2CAP_EVENT_COC_DISCONNECTED: + driver->conn_handle = BLE_HS_CONN_HANDLE_NONE; + driver->chan = NULL; + lowpan6_ble_netif_down(driver->base.netif); + break; + + case BLE_L2CAP_EVENT_COC_DATA_RECEIVED: + esp_netif_receive( + driver->base.netif, + event->receive.sdu_rx->om_data, + event->receive.sdu_rx->om_len, + event->receive.sdu_rx + ); + + // On data received, we need to provide NimBLE with the next sdu_rx to receive data into. + // We'll do this after the user callback automatically. + os_mbuf_free_chain(event->receive.sdu_rx); + rc = set_recv_ready(event->receive.chan); + if (rc != 0) { + ESP_LOGE(TAG, "(%s) couldn't set up next recv ready; rc=%d", __func__, rc); + } + + break; + + case BLE_L2CAP_EVENT_COC_ACCEPT: + rc = set_recv_ready(event->accept.chan); + if (rc != 0) { + ESP_LOGE(TAG, "(%s) couldn't set up next recv ready; rc=%d", __func__, rc); + } + break; + + case BLE_L2CAP_EVENT_COC_TX_UNSTALLED: + xEventGroupSetBits(s_lowpan6_event_group, BIT_TX_UNSTALLED); + ESP_LOGD( + TAG, + "(%s) tx_unstalled; chan=%p status=%d", + __func__, + event->tx_unstalled.chan, + event->tx_unstalled.status + ); + break; + + default: + ESP_LOGD(TAG, "(%s) ignoring BLE L2CAP event with type %d", __func__, event->type); + break; + } + + return rc; +} + +static inline int +on_gap_event_connect(struct lowpan6_ble_driver *driver, struct ble_gap_event *event) +{ + if (event->connect.status != 0) { + ESP_LOGE(TAG, "(%s) connection failed; status=%d", __func__, event->connect.status); + return ESP_FAIL; + } + + struct ble_gap_conn_desc desc; + int rc = ble_gap_conn_find(event->connect.conn_handle, &desc); + if (rc != 0) { + ESP_LOGE( + TAG, + "(%s) connection not found; conn_handle=%d", + __func__, + event->connect.conn_handle + ); + return ESP_ERR_NOT_FOUND; + } + + ESP_LOGD( + TAG, + "(%s) connection established; conn_handle=%d", + __func__, + event->connect.conn_handle + ); + + struct os_mbuf *sdu_rx = os_mbuf_get_pkthdr(&s_mbuf_pool, 0); + if (sdu_rx == NULL) { + ESP_LOGE(TAG, "(%s) cannot allocate memory for connect mbuf", __func__); + return ESP_ERR_NO_MEM; + } + + rc = ble_l2cap_connect( + event->connect.conn_handle, + LOWPAN6_BLE_IPSP_PSM, + LOWPAN6_BLE_IPSP_MTU, + sdu_rx, + on_l2cap_event, + driver + ); + + if (rc != 0) { + ESP_LOGE(TAG, "(%s) failed to l2cap connect; rc=%d", __func__, rc); + return rc; + } + + return ESP_OK; +} + +static inline int +on_gap_event_disconnect(struct lowpan6_ble_driver *driver, struct ble_gap_event *event) +{ + ESP_LOGD(TAG, "(%s) disconnected; reason=%d", __func__, event->disconnect.reason); + + return 0; +} + +static int on_gap_event(struct ble_gap_event *event, void *arg) +{ + struct lowpan6_ble_driver *driver = (struct lowpan6_ble_driver *)arg; + + ESP_LOGD(TAG, "(%s) GAP event; event->type=%d", __func__, event->type); + + int rc = 0; + struct lowpan6_ble_event out_event; + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + rc = on_gap_event_connect(driver, event); + out_event.type = LOWPAN6_BLE_EVENT_GAP_CONNECTED; + out_event.gap_connected.event = event; + user_notify(driver, &out_event); + return rc; + + case BLE_GAP_EVENT_DISCONNECT: + rc = on_gap_event_disconnect(driver, event); + out_event.type = LOWPAN6_BLE_EVENT_GAP_DISCONNECTED; + out_event.gap_disconnected.event = event; + user_notify(driver, &out_event); + return rc; + + default: + ESP_LOGD(TAG, "(%s) ignoring BLE GAP event with type %d", __func__, event->type); + break; + } + + return rc; +} + +static void lowpan6_ble_free_rx_buffer(void *h, void *buffer) +{ + int rc = os_mbuf_free_chain(buffer); + if (rc != 0) { + ESP_LOGW(TAG, "(%s) failed to free os_mbuf; om=%p rc=%d", __func__, buffer, rc); + } +} + +static esp_err_t lowpan6_ble_transmit(void *h, void *buffer, size_t len) +{ + struct lowpan6_ble_driver *driver = (struct lowpan6_ble_driver *)h; + if (driver == NULL || driver->chan == NULL) { + return ESP_ERR_INVALID_STATE; + } + + struct os_mbuf *sdu_tx = os_mbuf_get_pkthdr(&s_mbuf_pool, 0); + if (sdu_tx == NULL) { + ESP_LOGE(TAG, "(%s) cannot allocate memory for output mbuf", __func__); + return ESP_ERR_NO_MEM; + } + + int rc = os_mbuf_append(sdu_tx, buffer, len); + if (rc != 0) { + ESP_LOGE(TAG, "(%s) could not append data to mbuf; rc=%d", __func__, rc); + os_mbuf_free_chain(sdu_tx); + return ESP_FAIL; + } + + // We'll send data over L2CAP. If the transfer gets stalled (receiving side needs to send us + // credits before we continue), NimBLE will handle sending the rest of our message on receipt of + // those credits (great). This means we _don't_ need to retry ble_l2cap_send on BLE_HS_ESTALLED. + // If, however, there's already a stalled transmission for some OTHER message, we'll get + // BLE_HS_EBUSY. In _this_ case, we'll wait for the unstalled event and try again in case the + // previous operation completed. + do { + ESP_LOGD(TAG, "(%s) sending; sdu_tx=%p", __func__, sdu_tx); + rc = ble_l2cap_send(driver->chan, sdu_tx); + if (rc == BLE_HS_EBUSY) { + ESP_LOGD(TAG, "(%s) waiting for unstall; sdu_tx=%p", __func__, sdu_tx); + xEventGroupWaitBits( + s_lowpan6_event_group, + BIT_TX_UNSTALLED, + pdTRUE, + pdTRUE, + portMAX_DELAY + ); + } else if (rc != 0 && rc != BLE_HS_ESTALLED) { + ESP_LOGW(TAG, "(%s) failed to send data via ipsp; rc=%d", __func__, rc); + os_mbuf_free_chain(sdu_tx); + return ESP_FAIL; + } + } while (rc == BLE_HS_EBUSY); + + return ESP_OK; +} + +static esp_err_t lowpan6_ble_post_attach(esp_netif_t *esp_netif, void *args) +{ + struct lowpan6_ble_driver *driver = (struct lowpan6_ble_driver *)args; + + ESP_LOGD(TAG, "(%s) esp_netif=%p args=%p", __func__, esp_netif, args); + + const esp_netif_driver_ifconfig_t driver_ifconfig = { + .driver_free_rx_buffer = lowpan6_ble_free_rx_buffer, + .transmit = lowpan6_ble_transmit, + .handle = driver + }; + + driver->base.netif = esp_netif; + esp_err_t err = esp_netif_set_driver_config(esp_netif, &driver_ifconfig); + + esp_netif_action_start(driver->base.netif, 0, 0, NULL); + + return err; +} + +esp_err_t lowpan6_ble_init() +{ + int rc = + os_mempool_init(&s_mempool, MBUF_NUM_MBUFS, MBUF_MEMBLOCK_SIZE, s_membuf, "lowpan6_ble"); + if (rc != 0) { + ESP_LOGE(TAG, "(%s) failed to initialize mempool; rc=%d", __func__, rc); + return ESP_FAIL; + } + + rc = os_mbuf_pool_init(&s_mbuf_pool, &s_mempool, MBUF_MEMBLOCK_SIZE, MBUF_NUM_MBUFS); + if (rc != 0) { + ESP_LOGE(TAG, "(%s) failed to initialize mbuf pool; rc=%d", __func__, rc); + os_mempool_clear(&s_mempool); + return ESP_FAIL; + } + + s_lowpan6_event_group = xEventGroupCreateStatic(&s_lowpan6_event_group_buffer); + + // we should _never_ hit this since we're initializing from a static event group. Still, for + // completeness' sake... + if (s_lowpan6_event_group == NULL) { + ESP_LOGE(TAG, "(%s) failed to initialize event group", __func__); + return ESP_FAIL; + } + + return ESP_OK; +} + +lowpan6_ble_driver_handle lowpan6_ble_create() +{ + ESP_LOGI(TAG, "(%s) creating lowpan6_ble driver", __func__); + + struct lowpan6_ble_driver *driver = calloc(1, sizeof(struct lowpan6_ble_driver)); + if (driver == NULL) { + ESP_LOGE(TAG, "(%s) failed to allocate memory for lowpan6_ble driver", __func__); + return NULL; + } + + driver->base.post_attach = lowpan6_ble_post_attach; + + driver->conn_handle = BLE_HS_CONN_HANDLE_NONE; + driver->chan = NULL; + + return driver; +} + +esp_err_t lowpan6_ble_destroy(lowpan6_ble_driver_handle driver) +{ + return ESP_OK; +} + +bool lowpan6_ble_connectable(struct ble_gap_disc_desc *disc) +{ + // Must advertise one of the following Protocol Data Units (PDUs) in order to be + // "connectable". Other PDUs mean it's scannable or not connectable. The Directed + // advertising type (DIR_IND) is maybe a bit suspect -- directed means it'll only accept + // connection requests from a peer device. Theoretically we'd maybe want to confirm that we + // are that peer device instead of just saying "yeah it's connectable". + if (disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_ADV_IND && + disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_DIR_IND) { + return false; + } + + // Must include an advertising field indicating support for the Internet Protocol Support + // Service to be lowpan6_ble compatible. + struct ble_hs_adv_fields fields; + int rc = ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data); + if (rc != 0) { + ESP_LOGE(TAG, "(%s) failed to parse fields; rc=%d", __func__, rc); + return false; + } + + for (int i = 0; i < fields.num_uuids16; ++i) { + if (ble_uuid_u16(&fields.uuids16[i].u) == LOWPAN6_BLE_SERVICE_UUID_IPSS) { + return true; + } + } + + return false; +} + +esp_err_t lowpan6_ble_connect( + lowpan6_ble_driver_handle handle, + ble_addr_t *addr, + int32_t timeout_ms, + lowpan6_ble_event_handler cb, + void *userdata +) +{ + struct lowpan6_ble_driver *driver = (struct lowpan6_ble_driver *)handle; + if (driver == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (cb) { + driver->cb = cb; + driver->userdata = userdata; + } else { + driver->cb = NULL; + driver->userdata = NULL; + } + + uint8_t own_addr_type; + int rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + ESP_LOGE(TAG, "(%s) failed to automatically infer address type; rc=%d", __func__, rc); + return ESP_FAIL; + } + + rc = ble_gap_connect(own_addr_type, addr, timeout_ms, NULL, on_gap_event, handle); + if (rc != 0) { + ESP_LOGE( + TAG, + "(%s) failed to connect to device; addr_type=%d addr=%s rc=%d", + __func__, + own_addr_type, + debug_print_ble_addr(addr), + rc + ); + return ESP_FAIL; + } + + return ESP_OK; +} + +esp_err_t lowpan6_ble_disconnect(lowpan6_ble_driver_handle handle) +{ + struct lowpan6_ble_driver *driver = (struct lowpan6_ble_driver *)handle; + if (driver == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (driver->chan != NULL) { + int rc = ble_l2cap_disconnect(driver->chan); + if (rc != 0 && rc != BLE_HS_EALREADY && rc != BLE_HS_ENOTCONN) { + ESP_LOGW(TAG, "(%s) failed to l2cap disconnect; rc=%d", __func__, rc); + // continue to try GAP disconnect anyways + } else { + driver->chan = NULL; + } + } + + if (driver->conn_handle != BLE_HS_CONN_HANDLE_NONE) { + int rc = ble_gap_terminate(driver->conn_handle, BLE_ERR_REM_USER_CONN_TERM); + if (rc != 0 && rc != BLE_HS_EALREADY && rc != BLE_HS_ENOTCONN) { + ESP_LOGW(TAG, "(%s) failed to gap terminate; rc=%d", __func__, rc); + return ESP_FAIL; + } else { + driver->conn_handle = BLE_HS_CONN_HANDLE_NONE; + } + } + + return ESP_OK; +} + +esp_err_t lowpan6_ble_create_server( + lowpan6_ble_driver_handle handle, + lowpan6_ble_event_handler cb, + void *userdata +) +{ + struct lowpan6_ble_driver *driver = (struct lowpan6_ble_driver *)handle; + if (driver == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (cb) { + driver->cb = cb; + driver->userdata = userdata; + } else { + driver->cb = NULL; + driver->userdata = NULL; + } + + int rc = + ble_l2cap_create_server(LOWPAN6_BLE_IPSP_PSM, LOWPAN6_BLE_IPSP_MTU, on_l2cap_event, driver); + + if (rc != 0) { + ESP_LOGE(TAG, "(%s) failed to create L2CAP server; rc=%d", __func__, rc); + return ESP_FAIL; + } + + return ESP_OK; +} + +esp_err_t ble_addr_to_link_local(ble_addr_t *ble_addr, ip6_addr_t *ip_addr) +{ + if (ble_addr == NULL || ip_addr == NULL) { + return ESP_ERR_INVALID_ARG; + } + + uint8_t eui64_addr[8]; + nimble_addr_to_eui64(ble_addr, eui64_addr); + + ipv6_create_link_local_from_eui64(eui64_addr, ip_addr); + + return ESP_OK; +} diff --git a/components/netif_6lowpan_ble/src/lowpan6_ble_netif.c b/components/netif_6lowpan_ble/src/lowpan6_ble_netif.c new file mode 100644 index 0000000000..d58f84151f --- /dev/null +++ b/components/netif_6lowpan_ble/src/lowpan6_ble_netif.c @@ -0,0 +1,209 @@ +/* + * SPDX-FileCopyrightText: 2024 Tenera Care + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "lowpan6_ble_netif.h" + +#include "debug_print_utils.h" +#include "esp_idf_version.h" +#include "esp_log.h" +#include "esp_netif_net_stack.h" +#include "lwip/err.h" +#include "netif/lowpan6_ble.h" + +static err_t lowpan6_ble_netif_init(struct netif *netif); +static void lowpan6_ble_netif_input(void *h, void *buffer, size_t len, void *eb); + +// The esp_netif_netstack_config_t struct was not publically exposed until after v5 +// If we're running a lower version, we'll define the struct ourselves like in the following +// example: +// https://github.com/david-cermak/eth-ap-nat/blob/2279344e18a0b98b5368999aac9441c59871e6fa/eth-ap-idf4.3/main/ethernet_example_main.c#L90-L96 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + +// clang-format off +#include "lwip/esp_netif_net_stack.h" +const struct esp_netif_netstack_config s_netif_config_lowpan6_ble = { + .lwip = { + .init_fn = lowpan6_ble_netif_init, + .input_fn = lowpan6_ble_netif_input, + } +}; +// clang-format on + +#else + +// clang-format off +struct esp_netif_lwip_vanilla_config { + err_t (*init_fn)(struct netif *); + void (*input_fn)(void *netif, void *buffer, size_t len, void *eb); +}; + +const struct esp_netif_lwip_vanilla_config s_netif_config_lowpan6_ble = { + .init_fn = lowpan6_ble_netif_init, + .input_fn = lowpan6_ble_netif_input, +}; +// clang-format on + +#endif + +const esp_netif_netstack_config_t *netstack_default_lowpan6_ble = + (const esp_netif_netstack_config_t *) &s_netif_config_lowpan6_ble; + +static const char *TAG = "lowpan6_ble_netif"; + +err_t lowpan6_ble_netif_linkoutput(struct netif *netif, struct pbuf *p) +{ + esp_err_t err = esp_netif_transmit(netif->state, p->payload, p->len); + + if (err != ESP_OK) { + return ERR_IF; + } + + return ERR_OK; +} + +static err_t lowpan6_ble_netif_init(struct netif *netif) +{ + rfc7668_if_init(netif); + netif->linkoutput = lowpan6_ble_netif_linkoutput; + + ESP_LOGD(TAG, "(%s) init netif=%p", __func__, netif); + + return ERR_OK; +} + +static void lowpan6_ble_netif_input(void *h, void *buffer, size_t len, void *eb) +{ + struct netif *netif = (struct netif *)h; + struct os_mbuf *sdu_rx = (struct os_mbuf *)eb; + + size_t rx_len = (size_t)OS_MBUF_PKTLEN(sdu_rx); + + struct pbuf *p = pbuf_alloc(PBUF_RAW, rx_len, PBUF_POOL); + if (p == NULL) { + ESP_LOGE(TAG, "(%s) failed to allocate memory for pbuf", __func__); + return; + } + + // TODO: is there a better way here... ideally we wouldn't have to _copy_ this data just to turn + // it into a format LwIP understands (i.e., to go from mbuf to pbuf). Better would be if the + // pbuf just referred to data in the mbuf. Not sure if that's possible though. + int rc = os_mbuf_copydata(sdu_rx, 0, rx_len, p->payload); + if (rc != 0) { + ESP_LOGE(TAG, "(%s) failed to copy mbuf into pbuf", __func__); + pbuf_free(p); + return; + } + + // TODO: determine if we need to do this... + // It's _possible_ that the os_mbuf used for rx is managed by NimBLE for us, but I'm not + // actually sure. Some discussion here but unclear what the expected behaviour is. + // https://github.com/espressif/esp-idf/issues/9044 + // + // esp_netif_t* esp_netif = netif->state; + // esp_netif_free_rx_buffer(esp_netif, eb); + + p->len = rx_len; + + rfc7668_input(p, netif); +} + +void nimble_addr_to_eui64(ble_addr_t const *addr, uint8_t *eui64) +{ + // NimBLE stores addresses in _reverse_ order. We need to reverse these + // before doing the EUI64 conversion, otherwise we get incorrect + // addresses in our IPv6 headers. + uint8_t addr_reversed[6]; + for (int i = 0; i < 6; ++i) { + addr_reversed[i] = addr->val[6 - 1 - i]; + } + + bool is_public_addr = addr->type == BLE_ADDR_PUBLIC || addr->type == BLE_ADDR_PUBLIC_ID; + ble_addr_to_eui64(eui64, addr_reversed, is_public_addr); +} + +void ipv6_create_link_local_from_eui64(uint8_t const *eui64_addr, ip6_addr_t *dst) +{ + IP6_ADDR_PART(dst, 0, 0xFE, 0x80, 0x00, 0x00); + IP6_ADDR_PART(dst, 1, 0x00, 0x00, 0x00, 0x00); + IP6_ADDR_PART(dst, 2, eui64_addr[0] ^ 0x02, eui64_addr[1], eui64_addr[2], eui64_addr[3]); + IP6_ADDR_PART(dst, 3, eui64_addr[4], eui64_addr[5], eui64_addr[6], eui64_addr[7]); +} + +static inline void configure_netif_addresses(struct netif *netif, ble_addr_t *addr, bool is_peer) +{ + ESP_LOGD( + TAG, + "(%s) setting %s address %s", + __func__, + is_peer ? "peer" : "local", + debug_print_ble_addr(addr) + ); + + uint8_t eui64_addr[8]; + nimble_addr_to_eui64(addr, eui64_addr); + + if (is_peer) { + rfc7668_set_peer_addr_eui64(netif, eui64_addr, sizeof(eui64_addr)); + } else { + rfc7668_set_local_addr_eui64(netif, eui64_addr, sizeof(eui64_addr)); + + ip6_addr_t lladdr; + ipv6_create_link_local_from_eui64(eui64_addr, &lladdr); + + ESP_LOGD( + TAG, + "(%s) adding link-local address %s to netif %p", + __func__, + ip6addr_ntoa(&lladdr), + netif + ); + + ip_addr_copy_from_ip6(netif->ip6_addr[0], lladdr); + ip6_addr_assign_zone(ip_2_ip6(&(netif->ip6_addr[0])), IP6_UNICAST, netif); + netif_ip6_addr_set_state(netif, 0, IP6_ADDR_PREFERRED); + } +} + +void lowpan6_ble_netif_up(esp_netif_t *esp_netif, ble_addr_t *peer_addr, ble_addr_t *our_addr) +{ + struct netif *netif = esp_netif_get_netif_impl(esp_netif); + if (netif == NULL || peer_addr == NULL || our_addr == NULL) { + ESP_LOGE(TAG, "(%s) invalid parameters", __func__); + return; + } + + configure_netif_addresses(netif, peer_addr, true); + configure_netif_addresses(netif, our_addr, false); + + netif_set_up(netif); + netif_set_link_up(netif); + + ESP_LOGD(TAG, "(%s) netif up; esp_netif=%p netif=%p", __func__, esp_netif, netif); +} + +void lowpan6_ble_netif_down(esp_netif_t *esp_netif) +{ + struct netif *netif = esp_netif_get_netif_impl(esp_netif); + if (netif == NULL) { + ESP_LOGE(TAG, "(%s) invalid parameters", __func__); + return; + } + + netif_set_down(netif); + netif_set_link_down(netif); +} diff --git a/components/netif_6lowpan_ble/src/lowpan6_ble_netif.h b/components/netif_6lowpan_ble/src/lowpan6_ble_netif.h new file mode 100644 index 0000000000..bf8df72a66 --- /dev/null +++ b/components/netif_6lowpan_ble/src/lowpan6_ble_netif.h @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2024 Tenera Care + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "esp_idf_version.h" +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) +// Add missing includes to esp_netif_types.h +// https://github.com/espressif/esp-idf/commit/822129e234aaedf86e76fe92ab9b49c5f0a612e0 +#include "esp_err.h" +#include "esp_event_base.h" +#include "esp_netif_ip_addr.h" + +#include +#include +#endif + +#include "esp_netif_types.h" +#include "lwip/ip6_addr.h" +#include "nimble/ble.h" + +/** Bring up the netif upon connection. + * + * @param[in] esp_netif The netif to mark up. + * @param[in] peer_addr The BLE address of the peer we've connected to. + * @param[in] our_addr Our BLE address for this connection. + */ +void lowpan6_ble_netif_up(esp_netif_t *esp_netif, ble_addr_t *peer_addr, ble_addr_t *our_addr); + +/** Bring down the netif upon disconnection. + * + * @param[in] esp_netif The netif to mark down. + */ +void lowpan6_ble_netif_down(esp_netif_t *netif); + +/** Convert a NimBLE BLE addr to an EUI64 identifier. + * + * NimBLE stores its BLE addresses in the reverse order that LwIP's `ble_addr_to_eui64` function + * expects. This is a quick wrapper to help flip those bytes before calling `ble_addr_to_eui64`. + * + * @param[in] addr The BLE addr to translate from. + * @param[out] eui64 The resulting EUI64 address. + */ +void nimble_addr_to_eui64(ble_addr_t const *addr, uint8_t *eui64); + +/** Create a link-local address from an EUI64 identifier. + * + * An EUI64 interface identifier can be formed from a 48-bit Bluetooth device address by inserting + * the octets 0xFF and 0xFE in the middle of the Bluetooth device address. + * + * A link-local IPv6 address is then formed by prepending the EUI64 address with the prefix + * FE80::/64. + * + * BLE address (48 bits): 00:11:22:33:44:55 + * EUI identifier (64 bits): 00:11:22:FF:FE:33:44:55 + * IPv6 link-local address: FE80:0000:0000:0000:0011:22FF:FE33:4455 + * + * Use our `nimble_addr_to_eui64` helper to form an EUI64 address from a `ble_addr_t`. + * + * @example + * ```c + * ble_addr_t addr; + * uint8_t eui64_addr[8]; + * nimble_addr_to_eui64(&addr, eui64_addr); + * ipv6_create_link_local_from_eui64(eui64_addr, dst); + * ``` + * + * @param[in] src The source EUI64 address. MUST contain at least 8 bytes. + * @param[out] dst The resulting IPv6 address. + */ +void ipv6_create_link_local_from_eui64(uint8_t const *src, ip6_addr_t *dst);