diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5643d67..9b32f11 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -33,16 +33,24 @@ jobs: run: ./d download-all - name: Run build run: cargo run --release --bin stm32-bindings-gen + - name: Check wba-linklayer + run: cargo check --manifest-path wba-linklayer/Cargo.toml - name: Run package build run: | - cd build/stm32-bindings export CARGO_BUILD_TARGET=thumbv8m.main-none-eabihf - cargo fix --lib -p stm32-bindings --allow-no-vcs - cargo build - cargo package - ls target/package + for crate_dir in linklayer-bindings; do + echo "::group::Packaging ${crate_dir}" + ( + cd build/${crate_dir} + cargo fix --lib --allow-no-vcs --target ${CARGO_BUILD_TARGET} + cargo build --target ${CARGO_BUILD_TARGET} + cargo package + ls target/package + ) + echo "::endgroup::" + done - name: Upload package build uses: actions/upload-artifact@v4 with: name: crate - path: build/stm32-bindings/target/package/*.crate \ No newline at end of file + path: build/*-bindings/target/package/*.crate diff --git a/Cargo.toml b/Cargo.toml index 3dd07b0..a16fc8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,8 @@ members = [ "stm32-bindings-gen", ] exclude = [ - "build" + "build", + "wba-linklayer", ] # Optimize for dev experience: shortest "build+run" time after making a small change. diff --git a/stm32-bindings-gen/res/ble/Cargo.toml b/stm32-bindings-gen/res/ble/Cargo.toml new file mode 100644 index 0000000..df6e0be --- /dev/null +++ b/stm32-bindings-gen/res/ble/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ble-bindings" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +description = "Raw STM32 WBA BLE stack bindings generated from the ST middleware." +build = "build.rs" + +[lib] +path = "src/lib.rs" + +[dependencies] +linklayer-bindings = { path = "../linklayer-bindings" } + +[features] +default = [] +lib_stm32wba_ble_stack_po = [] +lib_stm32wba_ble_stack_llo = [] +lib_stm32wba_ble_stack_full = [] +lib_stm32wba_ble_stack_basic = [] +lib_stm32wba_ble_stack_basic_plus = [] +lib_lc3 = [] +lib_codec_mngr = [] +lib_ble_audio = [] diff --git a/stm32-bindings-gen/res/ble/README.md b/stm32-bindings-gen/res/ble/README.md new file mode 100644 index 0000000..05e1c1e --- /dev/null +++ b/stm32-bindings-gen/res/ble/README.md @@ -0,0 +1,28 @@ +# ble-bindings + +Raw bindings for the STM32 WBA Bluetooth Low Energy stack. This crate is generated automatically by `stm32-bindings-gen` and is meant to be consumed by higher-level wrappers that expose safe BLE abstractions. + +## Overview + +The generator ingests the ST middleware headers and prebuilt static libraries, then uses `bindgen` to emit Rust FFI items. Everything in this crate mirrors the original C API one-to-one; you should treat every item as `unsafe` and build ergonomic wrappers in a separate crate. + +## Layout + +- `src/bindings/`: Modules produced by `bindgen`, containing raw FFI definitions. +- `src/lib/`: Static libraries copied from the STM32CubeWBA distribution. Selecting the proper feature toggles which archives get linked. +- `build.rs`: Registers the static libraries with Cargo based on the enabled features. + +## Usage + +1. Run `cargo run -p stm32-bindings-gen` so this crate is regenerated under `build/ble-bindings`. +2. Add a path dependency in your Cargo manifest pointing to that directory. +3. Enable the feature corresponding to the static library variant required by your project (e.g. `lib_stm32wba_ble_stack_full`). +4. Wrap the raw FFI functions in a higher-level API before exposing them to the rest of your system. + +## Feature Flags + +Each `lib_*` feature matches a static archive provided by ST. Enable exactly the variants you need and the build script will emit the proper `cargo:rustc-link-lib` entries. + +## Regeneration + +If the underlying middleware or binding configuration changes, rerun `cargo run -p stm32-bindings-gen`. The generator will overwrite this crate with the latest bindings, libraries, and metadata. \ No newline at end of file diff --git a/stm32-bindings-gen/res/ble/build.rs b/stm32-bindings-gen/res/ble/build.rs new file mode 100644 index 0000000..fb840f7 --- /dev/null +++ b/stm32-bindings-gen/res/ble/build.rs @@ -0,0 +1,41 @@ +use std::path::{Path, PathBuf}; +use std::{env, fs, io}; + +fn add_dir(dir: &Path) -> io::Result<()> { + if !dir.exists() { + return Ok(()); + } + + println!("cargo:rustc-link-search=native={}", dir.display()); + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + add_dir(&path)?; + } + } + + Ok(()) +} + +fn main() { + let crate_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let lib_dir = crate_dir.join("src").join("lib"); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default(); + let is_embedded = target_os == "none" || target_family == "embedded"; + + if !is_embedded { + return; + } + + add_dir(&lib_dir).expect("failed to add link search paths"); + + env::vars() + .filter_map(|(key, _)| { + key.strip_prefix("CARGO_FEATURE_LIB_") + .map(|suffix| suffix.to_ascii_lowercase()) + }) + .for_each(|lib| println!("cargo:rustc-link-lib=static={lib}")); +} diff --git a/stm32-bindings-gen/res/ble/src/lib.rs b/stm32-bindings-gen/res/ble/src/lib.rs new file mode 100644 index 0000000..b623619 --- /dev/null +++ b/stm32-bindings-gen/res/ble/src/lib.rs @@ -0,0 +1,14 @@ +#![no_std] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(non_upper_case_globals)] +#![doc(html_no_source)] + +/// Raw bindings generated by `stm32-bindings-gen`. +/// +/// The `bindings` module is produced by `bindgen` and exposes a one-to-one FFI +/// surface to the STM32 WBA BLE middleware. All items should be treated as +/// `unsafe` and wrapped by higher-level crates before use. +pub mod bindings; + +pub use bindings::*; diff --git a/stm32-bindings-gen/res/linklayer/Cargo.toml b/stm32-bindings-gen/res/linklayer/Cargo.toml new file mode 100644 index 0000000..3cc9f73 --- /dev/null +++ b/stm32-bindings-gen/res/linklayer/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "linklayer-bindings" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +description = "Raw STM32 WBA link-layer bindings generated from the ST middleware." +build = "build.rs" + +[lib] +path = "src/lib.rs" + +[dependencies] + +[features] +default = [] +lib_wba5_linklayer_ble_basic_20_links_lib = [] +lib_wba5_linklayer_ble_basic_lib = [] +lib_wba5_linklayer_ble_basic_plus_20_links_lib = [] +lib_wba5_linklayer_ble_basic_plus_lib = [] +lib_wba5_linklayer_ble_full_lib = [] +lib_wba5_linklayer_ble_mac_lib = [] +lib_wba5_linklayer_ble_peripheral_only_lib = [] +lib_wba5_linklayer_ble_thread_lib = [] +lib_wba5_linklayer_rawmac_lib = [] +lib_wba5_linklayer_thread_lib = [] +lib_wba5_linklayer15_4 = [] +lib_wba6_linklayer_ble_basic_20_links_lib = [] +lib_wba6_linklayer_ble_basic_lib = [] +lib_wba6_linklayer_ble_basic_plus_20_links_lib = [] +lib_wba6_linklayer_ble_basic_plus_lib = [] +lib_wba6_linklayer_ble_full_lib = [] +lib_wba6_linklayer_ble_mac_lib = [] +lib_wba6_linklayer_ble_peripheral_only_lib = [] +lib_wba6_linklayer_ble_thread_lib = [] +lib_wba6_linklayer_rawmac_lib = [] +lib_wba6_linklayer_thread_lib = [] +lib_wba6_linklayer15_4 = [] +lib_wba_mac_lib = [] +lib_stm32wba_ble_stack_basic = [] +lib_stm32wba_ble_stack_basic_plus = [] +lib_stm32wba_ble_stack_full = [] +lib_stm32wba_ble_stack_llo = [] +lib_stm32wba_ble_stack_po = [] +lib_lc3 = [] +lib_codec_mngr = [] +lib_ble_audio = [] diff --git a/stm32-bindings-gen/res/linklayer/README.md b/stm32-bindings-gen/res/linklayer/README.md new file mode 100644 index 0000000..5abad47 --- /dev/null +++ b/stm32-bindings-gen/res/linklayer/README.md @@ -0,0 +1,28 @@ +# linklayer-bindings + +Raw bindings for the STM32 WBA link-layer middleware. This crate is generated automatically by `stm32-bindings-gen` and is intended to be consumed by higher-level wrappers that provide safe abstractions for BLE or IEEE 802.15.4 stacks. + +## Overview + +The generator pulls in STM-provided headers and static libraries, then runs `bindgen` to emit Rust FFI shims. No additional logic lives here—consumers should treat every item as `unsafe` and wrap it before use. + +## Layout + +- `src/bindings/`: Raw Rust modules produced by `bindgen`. +- `src/lib/`: Static libraries copied from the STM32CubeWBA middleware tree, gated behind Cargo features. +- `build.rs`: Emits the appropriate `cargo:rustc-link-*` directives based on the selected features. + +## Usage + +1. Ensure `stm32-bindings-gen` has been run so this crate exists in `build/linklayer-bindings`. +2. Add a path dependency in your Cargo manifest pointing at that directory. +3. Opt into the desired static library variant by enabling the matching `lib_*` feature exposed by this crate. +4. Call the generated functions through `unsafe` code and wrap them in a higher-level API before exposing them to the rest of your application. + +## Feature Flags + +Each feature named `lib_` selects one of the prebuilt static archives shipped by ST. Enable exactly the libraries required by your firmware configuration; the build script will link them automatically. + +## Regeneration + +If the upstream ST middleware or the bindings configuration changes, rerun `cargo run -p stm32-bindings-gen`. The generator will overwrite this crate with the latest bindings, static libraries, and metadata. \ No newline at end of file diff --git a/stm32-bindings-gen/res/linklayer/build.rs b/stm32-bindings-gen/res/linklayer/build.rs new file mode 100644 index 0000000..7be97fe --- /dev/null +++ b/stm32-bindings-gen/res/linklayer/build.rs @@ -0,0 +1,40 @@ +use std::path::{Path, PathBuf}; +use std::{env, fs, io}; + +fn add_dir(dir: &Path) -> io::Result<()> { + if !dir.exists() { + return Ok(()); + } + + println!("cargo:rustc-link-search=native={}", dir.display()); + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + add_dir(&path)?; + } + } + Ok(()) +} + +fn main() { + let crate_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let lib_dir = crate_dir.join("src").join("lib"); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default(); + let is_embedded = target_os == "none" || target_family == "embedded"; + + if !is_embedded { + return; + } + + add_dir(&lib_dir).expect("failed to add link search paths"); + + env::vars() + .filter_map(|(key, _)| { + key.strip_prefix("CARGO_FEATURE_LIB_") + .map(|s| s.to_ascii_lowercase()) + }) + .for_each(|lib| println!("cargo:rustc-link-lib=static={lib}")); +} diff --git a/stm32-bindings-gen/res/linklayer/src/lib.rs b/stm32-bindings-gen/res/linklayer/src/lib.rs new file mode 100644 index 0000000..aa91df8 --- /dev/null +++ b/stm32-bindings-gen/res/linklayer/src/lib.rs @@ -0,0 +1,14 @@ +#![no_std] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(non_upper_case_globals)] +#![doc(html_no_source)] + +/// Raw bindings generated by `stm32-bindings-gen`. +/// +/// The `bindings` module is produced by `bindgen` and exposes a one-to-one FFI +/// surface to the STM32 WBA link-layer middleware. All items should be treated +/// as `unsafe` and wrapped by higher-level crates before use. +pub mod bindings; + +pub use bindings::*; diff --git a/stm32-bindings-gen/res/mac/Cargo.toml b/stm32-bindings-gen/res/mac/Cargo.toml new file mode 100644 index 0000000..2eafdb0 --- /dev/null +++ b/stm32-bindings-gen/res/mac/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mac-bindings" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +description = "Raw STM32 WBA IEEE 802.15.4 MAC bindings generated from the ST middleware." +build = "build.rs" + +[lib] +path = "src/lib.rs" + +[dependencies] +linklayer-bindings = { path = "../linklayer-bindings" } + +[features] +default = [] +lib_wba_mac_lib = [] diff --git a/stm32-bindings-gen/res/mac/README.md b/stm32-bindings-gen/res/mac/README.md new file mode 100644 index 0000000..097676f --- /dev/null +++ b/stm32-bindings-gen/res/mac/README.md @@ -0,0 +1,28 @@ +# mac-bindings + +Raw bindings for the STM32 WBA IEEE 802.15.4 MAC middleware. This crate is generated automatically by `stm32-bindings-gen` and is intended to be consumed by higher-level wrappers that provide safe abstractions for IEEE 802.15.4 stacks. + +## Overview + +The generator pulls in STM-provided headers and static libraries, then runs `bindgen` to emit Rust FFI shims. No additional logic lives here—consumers should treat every item as `unsafe` and wrap it before use. + +## Layout + +- `src/bindings/`: Raw Rust modules produced by `bindgen`. +- `src/lib/`: Static libraries copied from the STM32CubeWBA middleware tree, gated behind Cargo features. +- `build.rs`: Emits the appropriate `cargo:rustc-link-*` directives based on the selected features. + +## Usage + +1. Ensure `stm32-bindings-gen` has been run so this crate exists in `build/mac-bindings`. +2. Add a path dependency in your Cargo manifest pointing at that directory. +3. Opt into the desired static library variant by enabling the matching `lib_*` feature exposed by this crate. +4. Call the generated functions through `unsafe` code and wrap them in a higher-level API before exposing them to the rest of your application. + +## Feature Flags + +Each feature named `lib_` selects one of the prebuilt static archives shipped by ST. Enable exactly the libraries required by your firmware configuration; the build script will link them automatically. + +## Regeneration + +If the upstream ST middleware or the bindings configuration changes, rerun `cargo run -p stm32-bindings-gen`. The generator will overwrite this crate with the latest bindings, static libraries, and metadata. \ No newline at end of file diff --git a/stm32-bindings-gen/res/mac/build.rs b/stm32-bindings-gen/res/mac/build.rs new file mode 100644 index 0000000..fb840f7 --- /dev/null +++ b/stm32-bindings-gen/res/mac/build.rs @@ -0,0 +1,41 @@ +use std::path::{Path, PathBuf}; +use std::{env, fs, io}; + +fn add_dir(dir: &Path) -> io::Result<()> { + if !dir.exists() { + return Ok(()); + } + + println!("cargo:rustc-link-search=native={}", dir.display()); + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + add_dir(&path)?; + } + } + + Ok(()) +} + +fn main() { + let crate_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let lib_dir = crate_dir.join("src").join("lib"); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or_default(); + let is_embedded = target_os == "none" || target_family == "embedded"; + + if !is_embedded { + return; + } + + add_dir(&lib_dir).expect("failed to add link search paths"); + + env::vars() + .filter_map(|(key, _)| { + key.strip_prefix("CARGO_FEATURE_LIB_") + .map(|suffix| suffix.to_ascii_lowercase()) + }) + .for_each(|lib| println!("cargo:rustc-link-lib=static={lib}")); +} diff --git a/stm32-bindings-gen/res/mac/src/lib.rs b/stm32-bindings-gen/res/mac/src/lib.rs new file mode 100644 index 0000000..f79700c --- /dev/null +++ b/stm32-bindings-gen/res/mac/src/lib.rs @@ -0,0 +1,14 @@ +#![no_std] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(non_upper_case_globals)] +#![doc(html_no_source)] + +/// Raw bindings generated by `stm32-bindings-gen`. +/// +/// The `bindings` module is produced by `bindgen` and exposes a one-to-one FFI +/// surface to the STM32 WBA MAC middleware. All items should be treated as +/// `unsafe` and wrapped by higher-level crates before use. +pub mod bindings; + +pub use bindings::*; diff --git a/stm32-bindings-gen/src/lib.rs b/stm32-bindings-gen/src/lib.rs index 42a2f6f..7b9063e 100644 --- a/stm32-bindings-gen/src/lib.rs +++ b/stm32-bindings-gen/src/lib.rs @@ -21,12 +21,10 @@ const NEWLIB_SHARED_OPAQUES: &[&str] = &["_reent", "__sFILE", "__sFILE64"]; #[derive(Debug, Clone, Copy)] struct BindingSpec { module: &'static str, - feature: Option<&'static str>, header: &'static str, include_dirs: &'static [&'static str], clang_args: &'static [&'static str], allowlist: &'static [&'static str], - aliases: &'static [&'static str], library_artifacts: &'static [LibraryArtifact], } @@ -36,126 +34,133 @@ struct LibraryArtifact { destination: &'static str, } -const BINDING_SPECS: &[BindingSpec] = &[ - BindingSpec { - module: "wba_link_layer", - feature: Some("wba_wpan"), - header: "stm32-bindings-gen/inc/link_layer.h", - include_dirs: &[ - "Middlewares/ST/STM32_WPAN", - "Middlewares/ST/STM32_WPAN/mac_802_15_4/core/inc", - "Middlewares/ST/STM32_WPAN/mac_802_15_4/mac_utilities/inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_sys/inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/_40nm_reg_files", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/ot_inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config/ieee_15_4_basic", - "Drivers/CMSIS/Core/Include", - ], - clang_args: &[ - "-DSUPPORT_MAC=1", - "-DSUPPORT_BLE=1", - "-DMAC=1", - "-DBLE=1", - "-DBLE_LL=1", - "-DMAC_LAYER=1", - "-DSUPPORT_MAC=1", - "-DSUPPORT_CONFIG_LIB=1", - "-DSUPPORT_OPENTHREAD_1_2=1", - "-DSUPPORT_ANT_DIV=1", - "-DEXT_ADDRESS_LENGTH=8", - ], - allowlist: &[], - aliases: &[], - library_artifacts: &[LibraryArtifact { - source: "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/lib", - destination: "src/lib/link_layer", - }], - }, - BindingSpec { - module: "wba_wpan_mac", - feature: Some("wba_wpan_mac"), - header: "stm32-bindings-gen/inc/wba_wpan_mac.h", - include_dirs: &[ - "Middlewares/ST/STM32_WPAN", - "Middlewares/ST/STM32_WPAN/mac_802_15_4/core/inc", - "Middlewares/ST/STM32_WPAN/mac_802_15_4/mac_utilities/inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_sys/inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/_40nm_reg_files", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/ot_inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config/ieee_15_4_basic", - "Drivers/CMSIS/Core/Include", - ], - clang_args: &["-DSUPPORT_MAC=1", "-DMAC=1", "-DMAC_LAYER=1"], - allowlist: &[], - aliases: &["mac", "mac_802_15_4", "wpan_wba"], - library_artifacts: &[ - LibraryArtifact { - source: "Middlewares/ST/STM32_WPAN/mac_802_15_4/lib", - destination: "src/lib/wba_wpan_mac", - }, - LibraryArtifact { - source: "Middlewares/ST/STM32_WPAN/mac_802_15_4/lib/wba_mac_lib.a", - destination: "src/lib/wba_mac_lib.a", - }, - ], - }, - BindingSpec { - module: "wba_ble_stack", - feature: Some("wba_wpan_ble"), - header: "stm32-bindings-gen/inc/wba_ble.h", - include_dirs: &[ - "Middlewares/ST/STM32_WPAN", - "Middlewares/ST/STM32_WPAN/ble/stack/include", - "Middlewares/ST/STM32_WPAN/ble/stack/include/auto", - "Middlewares/ST/STM32_WPAN/link_layer/ll_sys/inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/_40nm_reg_files", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/ot_inc", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config", - "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config/ble_basic_plus", - "Middlewares/ST/STM32_WPAN/ble/audio/Inc", - "Middlewares/ST/STM32_WPAN/ble/codec/codec_manager/Inc", - "Middlewares/ST/STM32_WPAN/ble/codec/lc3/Inc", - "Drivers/CMSIS/Core/Include", - ], - clang_args: &[ - "-DBLE=1", - "-DBLE_LL=1", - "-DSUPPORT_BLE=1", - "-DMAC=1", - "-DMAC_LAYER=1", - "-DSUPPORT_MAC=1", - "-DSUPPORT_CONFIG_LIB=1", - "-DSUPPORT_OPENTHREAD_1_2=1", - "-DSUPPORT_ANT_DIV=1", - "-DEXT_ADDRESS_LENGTH=8", - ], - allowlist: &[], - aliases: &["ble", "ble_wba"], - library_artifacts: &[ - LibraryArtifact { - source: "Middlewares/ST/STM32_WPAN/ble/stack/lib", - destination: "src/lib/ble/stack", - }, - LibraryArtifact { - source: "Middlewares/ST/STM32_WPAN/ble/audio/lib", - destination: "src/lib/ble/audio", - }, - LibraryArtifact { - source: "Middlewares/ST/STM32_WPAN/ble/codec/codec_manager/Lib", - destination: "src/lib/ble/codec_manager", - }, - LibraryArtifact { - source: "Middlewares/ST/STM32_WPAN/ble/codec/lc3/Lib", - destination: "src/lib/ble/lc3", - }, - ], - }, -]; +#[derive(Debug, Clone, Copy)] +struct CrateSpec { + name: &'static str, + template: &'static str, + bindings: &'static [&'static [BindingSpec]], +} + +const LINKLAYER_BINDINGS: &[BindingSpec] = &[BindingSpec { + module: "wba_link_layer", + header: "stm32-bindings-gen/inc/link_layer.h", + include_dirs: &[ + "Middlewares/ST/STM32_WPAN", + "Middlewares/ST/STM32_WPAN/mac_802_15_4/core/inc", + "Middlewares/ST/STM32_WPAN/mac_802_15_4/mac_utilities/inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_sys/inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/_40nm_reg_files", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/ot_inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config/ieee_15_4_basic", + "Drivers/CMSIS/Core/Include", + ], + clang_args: &[ + "-DSUPPORT_MAC=1", + "-DSUPPORT_BLE=1", + "-DMAC=1", + "-DBLE=1", + "-DBLE_LL=1", + "-DMAC_LAYER=1", + "-DSUPPORT_MAC=1", + "-DSUPPORT_CONFIG_LIB=1", + "-DSUPPORT_OPENTHREAD_1_2=1", + "-DSUPPORT_ANT_DIV=1", + "-DEXT_ADDRESS_LENGTH=8", + ], + allowlist: &[], + library_artifacts: &[LibraryArtifact { + source: "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/lib", + destination: "src/lib/link_layer", + }], +}]; + +const BLE_BINDINGS: &[BindingSpec] = &[BindingSpec { + module: "wba_ble_stack", + header: "stm32-bindings-gen/inc/wba_ble.h", + include_dirs: &[ + "Middlewares/ST/STM32_WPAN", + "Middlewares/ST/STM32_WPAN/ble/stack/include", + "Middlewares/ST/STM32_WPAN/ble/stack/include/auto", + "Middlewares/ST/STM32_WPAN/link_layer/ll_sys/inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/_40nm_reg_files", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/ot_inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config/ble_basic_plus", + "Middlewares/ST/STM32_WPAN/ble/audio/Inc", + "Middlewares/ST/STM32_WPAN/ble/codec/codec_manager/Inc", + "Middlewares/ST/STM32_WPAN/ble/codec/lc3/Inc", + "Drivers/CMSIS/Core/Include", + ], + clang_args: &[ + "-DBLE=1", + "-DBLE_LL=1", + "-DSUPPORT_BLE=1", + "-DMAC=1", + "-DMAC_LAYER=1", + "-DSUPPORT_MAC=1", + "-DSUPPORT_CONFIG_LIB=1", + "-DSUPPORT_OPENTHREAD_1_2=1", + "-DSUPPORT_ANT_DIV=1", + "-DEXT_ADDRESS_LENGTH=8", + ], + allowlist: &[], + library_artifacts: &[ + LibraryArtifact { + source: "Middlewares/ST/STM32_WPAN/ble/stack/lib", + destination: "src/lib/ble/stack", + }, + LibraryArtifact { + source: "Middlewares/ST/STM32_WPAN/ble/audio/lib", + destination: "src/lib/ble/audio", + }, + LibraryArtifact { + source: "Middlewares/ST/STM32_WPAN/ble/codec/codec_manager/Lib", + destination: "src/lib/ble/codec_manager", + }, + LibraryArtifact { + source: "Middlewares/ST/STM32_WPAN/ble/codec/lc3/Lib", + destination: "src/lib/ble/lc3", + }, + ], +}]; + +const MAC_BINDINGS: &[BindingSpec] = &[BindingSpec { + module: "wba_wpan_mac", + header: "stm32-bindings-gen/inc/wba_wpan_mac.h", + include_dirs: &[ + "Middlewares/ST/STM32_WPAN", + "Middlewares/ST/STM32_WPAN/mac_802_15_4/core/inc", + "Middlewares/ST/STM32_WPAN/mac_802_15_4/mac_utilities/inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_sys/inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/_40nm_reg_files", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/inc/ot_inc", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config", + "Middlewares/ST/STM32_WPAN/link_layer/ll_cmd_lib/config/ieee_15_4_basic", + "Drivers/CMSIS/Core/Include", + ], + clang_args: &["-DSUPPORT_MAC=1", "-DMAC=1", "-DMAC_LAYER=1"], + allowlist: &[], + library_artifacts: &[ + LibraryArtifact { + source: "Middlewares/ST/STM32_WPAN/mac_802_15_4/lib", + destination: "src/lib/mac", + }, + LibraryArtifact { + source: "Middlewares/ST/STM32_WPAN/mac_802_15_4/lib/wba_mac_lib.a", + destination: "src/lib/wba_mac_lib.a", + }, + ], +}]; + +const CRATE_SPECS: &[CrateSpec] = &[CrateSpec { + name: "linklayer-bindings", + template: "linklayer", + bindings: &[LINKLAYER_BINDINGS, BLE_BINDINGS, MAC_BINDINGS], +}]; #[derive(Debug)] struct UppercaseCallbacks; @@ -176,23 +181,6 @@ pub struct Options { pub target_triple: String, } -fn host_isystem_args() -> Vec { - let mut args = Vec::new(); - if cfg!(target_os = "macos") { - if let Ok(output) = Command::new("xcrun").arg("--show-sdk-path").output() { - if output.status.success() { - if let Ok(path) = String::from_utf8(output.stdout) { - let trimmed = path.trim(); - if !trimmed.is_empty() { - args.push(format!("-isystem{}/usr/include", trimmed)); - } - } - } - } - } - args -} - pub struct Gen { opts: Options, } @@ -203,81 +191,85 @@ impl Gen { } pub fn run_gen(&mut self) { + let base_out_dir = self.compute_base_out_dir(); println!( "Generating bindings into {} for target {}", - self.opts.out_dir.display(), + base_out_dir.display(), self.opts.target_triple ); - self.prepare_out_dir(); - self.write_static_files(); - - let mut modules = Vec::new(); - let mut aliases = Vec::new(); - - for spec in BINDING_SPECS { - println!(" -> generating `{}` bindings", spec.module); - self.generate_bindings_for_spec(spec); - self.copy_artifacts_for_spec(spec); + // Remove the legacy single-crate output if it still exists. + let _ = fs::remove_dir_all(&self.opts.out_dir); - modules.push((spec.module.to_owned(), spec.feature.map(str::to_owned))); - for alias in spec.aliases { - aliases.push(( - spec.module.to_owned(), - alias.to_string(), - spec.feature.map(str::to_owned), - )); + for crate_spec in CRATE_SPECS { + println!(" -> emitting `{}` crate", crate_spec.name); + let crate_out_dir = base_out_dir.join(crate_spec.name); + self.prepare_crate_out_dir(crate_spec, &crate_out_dir); + + let mut modules = Vec::new(); + for binding_group in crate_spec.bindings { + for binding in *binding_group { + println!(" - generating `{}` bindings", binding.module); + self.generate_bindings_for_spec(binding, &crate_out_dir); + self.copy_artifacts_for_spec(binding, &crate_out_dir); + modules.push(binding.module); + } } - } - - self.write_bindings_mod(&modules, &aliases); - } - fn prepare_out_dir(&self) { - let _ = fs::remove_dir_all(&self.opts.out_dir); - self.create_dir(self.opts.out_dir.join("src/bindings")); - self.create_dir(self.opts.out_dir.join("src/lib")); + self.write_bindings_mod(&crate_out_dir, &modules); + } } - fn write_static_files(&self) { - self.write_bytes("README.md", include_bytes!("../res/README.md")); - self.write_bytes("Cargo.toml", include_bytes!("../res/Cargo.toml")); - self.write_bytes("build.rs", include_bytes!("../res/build.rs")); - self.write_bytes("src/lib.rs", include_bytes!("../res/src/lib.rs")); + fn compute_base_out_dir(&self) -> PathBuf { + let out_dir = &self.opts.out_dir; + if out_dir + .file_name() + .map(|name| name == "stm32-bindings") + .unwrap_or(false) + { + out_dir + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(|| out_dir.clone()) + } else { + out_dir.clone() + } } - fn write_bindings_mod( - &self, - modules: &[(String, Option)], - aliases: &[(String, String, Option)], - ) { - let mut body = String::new(); - for (module, feature) in modules { - if let Some(feature) = feature { - body.push_str(&format!("#[cfg(feature = \"{feature}\")]\n")); - } - body.push_str("pub mod "); - body.push_str(module); - body.push_str(";\n"); + fn prepare_crate_out_dir(&self, spec: &CrateSpec, dest: &Path) { + if let Some(parent) = dest.parent() { + self.create_dir(parent); } - if !aliases.is_empty() { - body.push('\n'); - for (module, alias, feature) in aliases { - if let Some(feature) = feature { - body.push_str(&format!("#[cfg(feature = \"{feature}\")]\n")); - } - body.push_str("pub use self::"); - body.push_str(module); - body.push_str(" as "); - body.push_str(alias); - body.push_str(";\n"); - } + + let template_root = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("res") + .join(spec.template); + + if !template_root.exists() { + panic!( + "Template directory `{}` is missing for crate `{}`", + template_root.display(), + spec.name + ); } - self.write_string("src/bindings/mod.rs", body); + + let _ = fs::remove_dir_all(dest); + self.copy_dir_recursive(&template_root, dest) + .unwrap_or_else(|err| { + panic!( + "Failed to copy template `{}` into `{}`: {err}", + template_root.display(), + dest.display() + ) + }); + + self.create_dir(dest.join("src/bindings")); + self.create_dir(dest.join("src/lib")); } - fn generate_bindings_for_spec(&self, spec: &BindingSpec) { + fn generate_bindings_for_spec(&self, spec: &BindingSpec, crate_dir: &Path) { let mut builder = bindgen::Builder::default() + .layout_tests(false) .parse_callbacks(Box::new(UppercaseCallbacks)) .header(spec.header) .clang_arg(format!("--target={}", self.opts.target_triple)); @@ -325,26 +317,26 @@ impl Gen { } } - let bindings = builder - .generate() - .unwrap_or_else(|err| panic!("Unable to generate bindings for {}: {err}", spec.module)); + let bindings = builder.generate().unwrap_or_else(|err| { + panic!( + "Unable to generate bindings for module `{}`: {err}", + spec.module + ) + }); - let mut file_contents = bindings.to_string(); - file_contents = Self::normalize_bindings(file_contents); + let mut contents = bindings.to_string(); + contents = Self::normalize_bindings(contents); - let out_path = self - .opts - .out_dir + let out_path = crate_dir .join("src/bindings") .join(format!("{}.rs", spec.module)); - - self.write_string_path(&out_path, file_contents); + self.write_string_path(&out_path, contents); } - fn copy_artifacts_for_spec(&self, spec: &BindingSpec) { + fn copy_artifacts_for_spec(&self, spec: &BindingSpec, crate_dir: &Path) { for artifact in spec.library_artifacts { let src = self.opts.sources_dir.join(artifact.source); - let dst = self.opts.out_dir.join(artifact.destination); + let dst = crate_dir.join(artifact.destination); if src.is_file() { self.copy_lib(&src, &dst) @@ -361,17 +353,39 @@ impl Gen { } } - fn write_bytes(&self, relative: &str, bytes: &[u8]) { - let path = self.opts.out_dir.join(relative); - if let Some(parent) = path.parent() { - self.create_dir(parent); + fn write_bindings_mod(&self, crate_dir: &Path, modules: &[&str]) { + let mut body = String::new(); + for module in modules { + body.push_str("pub mod "); + body.push_str(module); + body.push_str(";\n"); } - fs::write(path, bytes).expect("Unable to write bytes"); + + let mod_path = crate_dir.join("src/bindings/mod.rs"); + self.write_string_path(&mod_path, body); } - fn write_string(&self, relative: &str, contents: String) { - let path = self.opts.out_dir.join(relative); - self.write_string_path(&path, contents); + fn copy_dir_recursive(&self, src: &Path, dst: &Path) -> io::Result<()> { + if !dst.exists() { + fs::create_dir_all(dst)?; + } + + for entry in fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + let target = dst.join(entry.file_name()); + + if path.is_dir() { + self.copy_dir_recursive(&path, &target)?; + } else { + if let Some(parent) = target.parent() { + fs::create_dir_all(parent)?; + } + fs::copy(&path, &target)?; + } + } + + Ok(()) } fn write_string_path(&self, path: &Path, mut contents: String) { @@ -454,6 +468,23 @@ impl Gen { } } +fn host_isystem_args() -> Vec { + let mut args = Vec::new(); + if cfg!(target_os = "macos") { + if let Ok(output) = Command::new("xcrun").arg("--show-sdk-path").output() { + if output.status.success() { + if let Ok(path) = String::from_utf8(output.stdout) { + let trimmed = path.trim(); + if !trimmed.is_empty() { + args.push(format!("-isystem{}/usr/include", trimmed)); + } + } + } + } + } + args +} + fn arm_sysroot_args() -> Vec { let mut args = Vec::new(); let mut system_include_paths = BTreeSet::new(); diff --git a/wba-linklayer/Cargo.toml b/wba-linklayer/Cargo.toml new file mode 100644 index 0000000..e3c1abe --- /dev/null +++ b/wba-linklayer/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "wba-linklayer" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +publish = false +description = "Shared STM32 WBA link layer utilities used by BLE and MAC wrappers." +repository = "https://github.com/embassy-rs/stm32-bindings" + +[dependencies] +bitflags = { version = "2", default-features = false } +linklayer-bindings = { path = "../build/linklayer-bindings" } + +[features] +default = [] +ble = [] +mac = [] + +# Feature passthrough for selecting ST link-layer static libraries. +lib_wba5_linklayer_ble_basic_20_links_lib = ["linklayer-bindings/lib_wba5_linklayer_ble_basic_20_links_lib"] +lib_wba5_linklayer_ble_basic_lib = ["linklayer-bindings/lib_wba5_linklayer_ble_basic_lib"] +lib_wba5_linklayer_ble_basic_plus_20_links_lib = ["linklayer-bindings/lib_wba5_linklayer_ble_basic_plus_20_links_lib"] +lib_wba5_linklayer_ble_basic_plus_lib = ["linklayer-bindings/lib_wba5_linklayer_ble_basic_plus_lib"] +lib_wba5_linklayer_ble_full_lib = ["linklayer-bindings/lib_wba5_linklayer_ble_full_lib"] +lib_wba5_linklayer_ble_mac_lib = ["linklayer-bindings/lib_wba5_linklayer_ble_mac_lib"] +lib_wba5_linklayer_ble_peripheral_only_lib = ["linklayer-bindings/lib_wba5_linklayer_ble_peripheral_only_lib"] +lib_wba5_linklayer_ble_thread_lib = ["linklayer-bindings/lib_wba5_linklayer_ble_thread_lib"] +lib_wba5_linklayer_rawmac_lib = ["linklayer-bindings/lib_wba5_linklayer_rawmac_lib"] +lib_wba5_linklayer_thread_lib = ["linklayer-bindings/lib_wba5_linklayer_thread_lib"] +lib_wba5_linklayer15_4 = ["linklayer-bindings/lib_wba5_linklayer15_4"] +lib_wba6_linklayer_ble_basic_20_links_lib = ["linklayer-bindings/lib_wba6_linklayer_ble_basic_20_links_lib"] +lib_wba6_linklayer_ble_basic_lib = ["linklayer-bindings/lib_wba6_linklayer_ble_basic_lib"] +lib_wba6_linklayer_ble_basic_plus_20_links_lib = ["linklayer-bindings/lib_wba6_linklayer_ble_basic_plus_20_links_lib"] +lib_wba6_linklayer_ble_basic_plus_lib = ["linklayer-bindings/lib_wba6_linklayer_ble_basic_plus_lib"] +lib_wba6_linklayer_ble_full_lib = ["linklayer-bindings/lib_wba6_linklayer_ble_full_lib"] +lib_wba6_linklayer_ble_mac_lib = ["linklayer-bindings/lib_wba6_linklayer_ble_mac_lib"] +lib_wba6_linklayer_ble_peripheral_only_lib = ["linklayer-bindings/lib_wba6_linklayer_ble_peripheral_only_lib"] +lib_wba6_linklayer_ble_thread_lib = ["linklayer-bindings/lib_wba6_linklayer_ble_thread_lib"] +lib_wba6_linklayer_rawmac_lib = ["linklayer-bindings/lib_wba6_linklayer_rawmac_lib"] +lib_wba6_linklayer_thread_lib = ["linklayer-bindings/lib_wba6_linklayer_thread_lib"] +lib_wba6_linklayer15_4 = ["linklayer-bindings/lib_wba6_linklayer15_4"] diff --git a/wba-linklayer/src/ble_sys_if.rs b/wba-linklayer/src/ble_sys_if.rs new file mode 100644 index 0000000..1f79a1a --- /dev/null +++ b/wba-linklayer/src/ble_sys_if.rs @@ -0,0 +1,377 @@ +//! Safe wrappers around a subset of the BLE system interface exposed by the +//! STM32 WBA wireless stack. +//! +//! The goal of this module is to minimise the amount of ad-hoc `unsafe` code +//! required by higher layers while still providing zero-cost access to the +//! underlying C APIs. Only a carefully-curated portion of the enormous BLE +//! surface is exposed here; additional helpers can be added incrementally as +//! the need arises. + +use core::marker::PhantomData; +use core::ptr::{self, NonNull}; +use core::slice; + +use crate::ffi; + +/// Result type returned by helpers that wrap `ble_stat_t`-style status values. +pub type BleResult = Result; + +/// Commonly observed BLE controller status codes. +/// +/// The values are derived from the constants defined in `ble_defs.h`. Any +/// status that is not modelled explicitly will be surfaced through +/// [`BleStatus::Other`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BleStatus { + Success, + Busy, + Pending, + InvalidParameters, + InsufficientResources, + OutOfMemory, + Timeout, + Other(u8), +} + +impl BleStatus { + /// Convert a raw `ble_stat_t` (as returned by the vendor APIs) into the + /// strongly-typed status representation. + #[inline] + pub fn from_raw(value: ffi::ble_stat_t) -> Self { + match (value & 0xFF) as u8 { + 0x00 => Self::Success, + 0x93 => Self::Busy, + 0x95 => Self::Pending, + 0x92 => Self::InvalidParameters, + 0x64 => Self::InsufficientResources, + 0x98 => Self::OutOfMemory, + 0xFF => Self::Timeout, + other => Self::Other(other), + } + } + + /// Returns the raw 8-bit status code. + #[inline] + pub const fn code(self) -> u8 { + match self { + Self::Success => 0x00, + Self::Busy => 0x93, + Self::Pending => 0x95, + Self::InvalidParameters => 0x92, + Self::InsufficientResources => 0x64, + Self::OutOfMemory => 0x98, + Self::Timeout => 0xFF, + Self::Other(code) => code, + } + } + + /// Converts the status into a `Result`, treating [`BleStatus::Success`] as + /// the success case. + #[inline] + pub fn into_result(self) -> BleResult<()> { + match self { + Self::Success => Ok(()), + other => Err(other), + } + } +} + +/// Callback used by the controller to hand HCI buffers back to the host +/// transport. +pub type HostCallback = unsafe extern "C" fn(*mut ffi::ble_buff_hdr_t) -> u8; + +/// Callback invoked when the controller fails to enqueue a buffer because the +/// host queue is full. +pub type HostQueueFullCallback = unsafe extern "C" fn(*mut ffi::ble_buff_hdr_t); + +/// Callback signature used for vendor-specific HCI command handling. +pub type ExternalCustomCallback = unsafe extern "C" fn( + ocf: u16, + packet: *mut u8, + event_packet: *mut u8, + params_length: *mut u8, + return_command_type: *mut ffi::hci_return_command_type, +) -> ffi::ble_stat_t; + +pub type DispatchTable = ffi::hci_dispatch_tbl; + +/// RAII wrapper around the vendor-provided `ble_buff_hdr_t` buffers. +/// +/// Buffers are automatically returned to the controller when dropped. +pub struct BleBuffer { + ptr: NonNull, + _not_send_sync: PhantomData>, +} + +impl BleBuffer { + /// Attempts to allocate a fresh buffer from the controller. + pub fn allocate() -> Option { + let ptr = unsafe { ffi::hci_alloc_msg() }; + NonNull::new(ptr).map(|ptr| Self { + ptr, + _not_send_sync: PhantomData, + }) + } + + /// Wrap an existing raw pointer returned by the vendor middleware. + /// + /// # Safety + /// + /// The caller must ensure that `ptr` is either null (in which case `None` + /// is returned) or a valid buffer obtained from the controller and that it + /// remains valid for the lifetime of the returned wrapper. + pub unsafe fn from_raw(ptr: *mut ffi::ble_buff_hdr_t) -> Option { + NonNull::new(ptr).map(|ptr| Self { + ptr, + _not_send_sync: PhantomData, + }) + } + + #[inline] + fn header(&self) -> &ffi::ble_buff_hdr_t { + unsafe { self.ptr.as_ref() } + } + + #[inline] + fn header_mut(&mut self) -> &mut ffi::ble_buff_hdr_t { + unsafe { self.ptr.as_mut() } + } + + #[inline] + pub fn len(&self) -> usize { + self.header().data_size as usize + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + #[inline] + pub fn capacity(&self) -> usize { + self.header().total_len as usize + } + + #[inline] + pub fn offset(&self) -> usize { + self.header().data_offset as usize + } + + #[inline] + pub fn set_len(&mut self, len: usize) { + debug_assert!(len <= self.capacity()); + debug_assert!(len <= u16::MAX as usize); + self.header_mut().data_size = len as u16; + } + + #[inline] + pub fn set_offset(&mut self, offset: usize) { + debug_assert!(offset <= self.capacity()); + debug_assert!(offset <= u16::MAX as usize); + self.header_mut().data_offset = offset as u16; + } + + fn payload_parts(&self) -> Option<(NonNull, usize)> { + let header = self.header(); + let len = header.data_size as usize; + if len == 0 { + return None; + } + let base = NonNull::new(header.buff_start)?; + let offset = header.data_offset as usize; + let end = offset.checked_add(len)?; + if header.total_len != 0 { + debug_assert!(end <= header.total_len as usize); + } + let ptr = unsafe { base.as_ptr().add(offset) }; + NonNull::new(ptr).map(|ptr| (ptr, len)) + } + + /// Borrow the packet payload as an immutable slice. + #[inline] + pub fn payload(&self) -> Option<&[u8]> { + self.payload_parts() + .map(|(ptr, len)| unsafe { slice::from_raw_parts(ptr.as_ptr() as *const u8, len) }) + } + + /// Borrow the packet payload as a mutable slice. + #[inline] + pub fn payload_mut(&mut self) -> Option<&mut [u8]> { + self.payload_parts() + .map(|(ptr, len)| unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), len) }) + } + + /// Overwrite the payload with `data`, adjusting length/offset as needed. + /// + /// Returns `Err(())` when the payload does not fit in the buffer. + pub fn write_payload(&mut self, data: &[u8]) -> Result<(), ()> { + if data.len() > self.capacity() { + return Err(()); + } + + self.set_offset(0); + self.set_len(data.len()); + + match self.payload_mut() { + Some(slot) => { + slot.copy_from_slice(data); + Ok(()) + } + None => { + if data.is_empty() { + Ok(()) + } else { + Err(()) + } + } + } + } + + /// Returns the raw pointer without relinquishing ownership. + #[inline] + pub fn as_ptr(&self) -> *const ffi::ble_buff_hdr_t { + self.ptr.as_ptr() + } + + /// Returns the raw mutable pointer without relinquishing ownership. + #[inline] + pub fn as_mut_ptr(&mut self) -> *mut ffi::ble_buff_hdr_t { + self.ptr.as_ptr() + } + + /// Consumes the wrapper and returns the raw pointer, preventing the + /// destructor from freeing it. + #[inline] + pub fn into_raw(self) -> *mut ffi::ble_buff_hdr_t { + let ptr = self.ptr.as_ptr(); + core::mem::forget(self); + ptr + } +} + +impl Drop for BleBuffer { + fn drop(&mut self) { + unsafe { ffi::hci_free_msg(self.ptr.as_ptr()) }; + } +} + +/// Initialise the BLE controller glue logic. +/// +/// # Safety +/// +/// The supplied callback must adhere to the constraints expected by the +/// controller (no unwinding across the FFI boundary, ISR safety where +/// applicable, etc.). +pub unsafe fn init_controller(callback: HostCallback) { + unsafe { + ffi::ll_sys_ble_cntrl_init(Some(callback)); + } +} + +/// Initialise the controller <-> host transport layer. +pub fn init_transport(callback: HostCallback) -> BleResult { + let status = unsafe { ffi::ll_hci_init(Some(callback)) }; + BleStatus::from_raw(status).into_result() +} + +pub fn init_with_dispatch_table(table: &DispatchTable) -> BleResult { + let status = unsafe { ffi::ll_intf_init(table as *const _) }; + BleStatus::from_raw(status).into_result() +} + +pub fn init_default_interface() -> BleResult { + match dispatch_table() { + Some(table) => init_with_dispatch_table(table), + None => Err(BleStatus::InsufficientResources), + } +} + +pub fn dispatch_table() -> Option<&'static DispatchTable> { + let mut table: *const DispatchTable = ptr::null(); + unsafe { + ffi::hci_get_dis_tbl(&mut table); + } + if table.is_null() { + None + } else { + Some(unsafe { &*table }) + } +} + +pub fn reset_interface() -> BleResult { + let status = unsafe { ffi::ll_intf_reset() }; + BleStatus::from_raw(status).into_result() +} + +pub fn reset_system() { + unsafe { ffi::ll_sys_reset() }; +} + +/// Register an optional callback that is invoked when the host queue is full. +pub unsafe fn register_queue_full_callback(callback: Option) { + unsafe { + ffi::hci_rgstr_hst_cbk_ll_queue_full(callback); + } +} + +/// Register the primary host callback that receives HCI buffers. +pub unsafe fn register_host_callback(callback: Option) { + unsafe { + ffi::hci_rgstr_hst_cbk(callback); + } +} + +/// Register or clear the vendor-specific HCI command handler. +pub unsafe fn register_external_custom_callback(callback: Option) -> bool { + unsafe { ffi::hci_rgstr_ble_external_custom_cbk(callback) != 0 } +} + +/// Prepare the vendor event queues. This must be called once before invoking +/// any of the queue-related helpers. +pub fn init_event_queues() { + unsafe { ffi::hci_init_events_queues() }; +} + +/// Enqueue a packet for delivery to the host. +pub fn queue_send_packet(buffer: BleBuffer) -> BleResult { + let ptr = buffer.into_raw(); + let status = BleStatus::from_raw(unsafe { ffi::hci_queue_send_pckt(ptr) as ffi::ble_stat_t }); + if status == BleStatus::Success { + Ok(()) + } else { + unsafe { ffi::hci_free_msg(ptr) }; + Err(status) + } +} + +/// Helper that allocates a buffer and enqueues it after copying `payload` into it. +/// +/// Returns [`BleStatus::InsufficientResources`] if a buffer cannot be obtained or +/// [`BleStatus::InvalidParameters`] when the payload exceeds the buffer capacity. +pub fn queue_packet(payload: &[u8]) -> BleResult { + let mut buffer = BleBuffer::allocate().ok_or(BleStatus::InsufficientResources)?; + buffer + .write_payload(payload) + .map_err(|_| BleStatus::InvalidParameters)?; + queue_send_packet(buffer) +} + +/// Update the LE event mask used by the controller. +pub fn set_le_event_mask(mask: &mut [u8; 8]) { + unsafe { ffi::hci_ll_set_le_event_mask(mask.as_mut_ptr()) }; +} + +/// Update the classic/Bluetooth event mask used by the controller. +pub fn set_event_mask(mask: &mut [u8; 8]) { + unsafe { ffi::hci_ll_set_event_mask(mask.as_mut_ptr()) }; +} + +/// Update the page 2 event mask used by the controller. +pub fn set_event_mask_page2(mask: &mut [u8; 8]) { + unsafe { ffi::hci_ll_set_event_mask_page2(mask.as_mut_ptr()) }; +} + +/// Update the custom event mask used by the controller. +pub fn set_custom_event_mask(mask: u8) { + unsafe { ffi::hci_ll_set_custom_event_mask(mask) }; +} diff --git a/wba-linklayer/src/ffi.rs b/wba-linklayer/src/ffi.rs new file mode 100644 index 0000000..bab318d --- /dev/null +++ b/wba-linklayer/src/ffi.rs @@ -0,0 +1,10 @@ +#![allow(missing_docs)] +#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] +#![allow(dead_code)] + +/// Raw FFI items produced by the generated `linklayer-bindings` crate for the shared +/// WBA link layer. +pub use linklayer_bindings::bindings::wba_link_layer::*; + +/// Alias to the entire generated bindings module for convenience. +pub use linklayer_bindings::bindings::wba_link_layer as raw; diff --git a/wba-linklayer/src/lib.rs b/wba-linklayer/src/lib.rs new file mode 100644 index 0000000..04e8d67 --- /dev/null +++ b/wba-linklayer/src/lib.rs @@ -0,0 +1,16 @@ +#![no_std] + +/// High-level helpers around the STM32 WBA link layer primitives. +/// +/// This crate consolidates the link-layer-specific pieces that are common to +/// both the BLE and IEEE 802.15.4 (MAC) stacks. It depends on the generated +/// raw bindings crate for the FFI definitions. +pub mod ffi; + +pub mod linklayer_plat; + +pub mod ll_sys_if; + +pub mod ble_sys_if; + +pub mod mac_sys_if; diff --git a/wba-linklayer/src/linklayer_plat.rs b/wba-linklayer/src/linklayer_plat.rs new file mode 100644 index 0000000..f1e0961 --- /dev/null +++ b/wba-linklayer/src/linklayer_plat.rs @@ -0,0 +1,261 @@ +#![allow(dead_code)] + +use core::fmt; + +use bitflags::bitflags; + +use crate::ffi; + +/// Runtime configuration applied when bootstrapping the link-layer platform. +#[derive(Debug, Clone, Copy)] +pub struct Config { + /// Invoke the vendor clock initialisation routine before touching any other entry point. + pub clock_init: bool, + /// Optional high-priority radio interrupt handler. + pub radio_isr: Option, + /// Optional software low-priority interrupt handler. + pub sw_low_isr: Option, + /// Enable the baseband clock as part of the initialisation sequence. + pub enable_active_clock: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + clock_init: true, + radio_isr: None, + sw_low_isr: None, + enable_active_clock: true, + } + } +} + +/// Wrapper around an `extern "C"` ISR pointer consumed by the vendor firmware. +/// +/// # Safety +/// +/// Callers must guarantee that the wrapped function follows the interrupt-safety +/// requirements imposed by the platform (proper ABI, no unwinding, etc.). +#[derive(Clone, Copy)] +pub struct InterruptHandler { + raw: unsafe extern "C" fn(), +} + +impl InterruptHandler { + /// Create a new wrapper from a raw interrupt handler pointer. + /// + /// # Safety + /// + /// The caller must uphold the invariants documented for [`InterruptHandler`]. + pub const unsafe fn new(raw: unsafe extern "C" fn()) -> Self { + Self { raw } + } + + #[inline(always)] + fn raw(self) -> unsafe extern "C" fn() { + self.raw + } +} + +impl fmt::Debug for InterruptHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("InterruptHandler") + .field(&(self.raw as usize)) + .finish() + } +} + +impl From for InterruptHandler { + fn from(raw: unsafe extern "C" fn()) -> Self { + Self { raw } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum SwLowPriority { + /// Run the ISR with the default (high) priority. + High = 0, + /// Demote the ISR to the lowest radio priority. + Low = 1, +} + +impl Default for SwLowPriority { + fn default() -> Self { + SwLowPriority::High + } +} + +bitflags! { + /// Convenience bit-mask describing which interrupt groups to toggle. + #[derive(Default)] + pub struct InterruptMask: u8 { + const RADIO_HIGH = ffi::LL_HIGH_ISR_ONLY as u8; + const RADIO_LOW = ffi::LL_LOW_ISR_ONLY as u8; + const SYSTEM_LOW = ffi::SYS_LOW_ISR as u8; + } +} + +/// Triple describing scheduler timings expressed in link-layer sleep-timer cycles (31 µs). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SchedulerTiming { + pub drift_time: u32, + pub execution_time: u32, + pub scheduling_time: u32, +} + +impl SchedulerTiming { + pub const fn new(drift_time: u32, execution_time: u32, scheduling_time: u32) -> Self { + Self { + drift_time, + execution_time, + scheduling_time, + } + } + + fn into_raw(self) -> ffi::Evnt_timing_t { + ffi::Evnt_timing_t { + drift_time: self.drift_time, + exec_time: self.execution_time, + schdling_time: self.scheduling_time, + } + } +} + +/// Mirror the vendor start-up sequence using the safe configuration struct. +pub fn init(config: &Config) { + if config.clock_init { + unsafe { ffi::LINKLAYER_PLAT_ClockInit() }; + } + + if let Some(handler) = config.radio_isr { + unsafe { ffi::LINKLAYER_PLAT_SetupRadioIT(Some(handler.raw())) }; + } + + if let Some(handler) = config.sw_low_isr { + unsafe { ffi::LINKLAYER_PLAT_SetupSwLowIT(Some(handler.raw())) }; + } + + control_active_clock(config.enable_active_clock); +} + +/// Busy-wait for `delay` microseconds. +pub fn delay_us(delay: u32) { + unsafe { ffi::LINKLAYER_PLAT_DelayUs(delay) }; +} + +/// Delegate assertion handling to the platform layer. +#[track_caller] +pub fn assert_platform(condition: bool) { + unsafe { ffi::LINKLAYER_PLAT_Assert(condition as u8) }; +} + +/// Enable or disable the baseband (active) radio clock. +pub fn control_active_clock(enable: bool) { + unsafe { ffi::LINKLAYER_PLAT_AclkCtrl(enable as u8) }; +} + +pub fn notify_wfi_enter() { + unsafe { ffi::LINKLAYER_PLAT_NotifyWFIEnter() }; +} + +pub fn notify_wfi_exit() { + unsafe { ffi::LINKLAYER_PLAT_NotifyWFIExit() }; +} + +pub fn wait_for_hclk_ready() { + unsafe { ffi::LINKLAYER_PLAT_WaitHclkRdy() }; +} + +/// Fill `buffer` with random bytes sourced from the on-chip RNG. +pub fn fill_random(buffer: &mut [u8]) { + if buffer.is_empty() { + return; + } + + assert!( + buffer.len() <= u32::MAX as usize, + "buffer length does not fit in u32" + ); + + unsafe { + ffi::LINKLAYER_PLAT_GetRNG(buffer.as_mut_ptr(), buffer.len() as u32); + } +} + +/// Manually trigger the SW-low interrupt with the requested priority. +pub fn trigger_sw_low_interrupt(priority: SwLowPriority) { + unsafe { ffi::LINKLAYER_PLAT_TriggerSwLowIT(priority as u8) }; +} + +pub fn enable_irq() { + unsafe { ffi::LINKLAYER_PLAT_EnableIRQ() }; +} + +pub fn disable_irq() { + unsafe { ffi::LINKLAYER_PLAT_DisableIRQ() }; +} + +pub fn enable_specific_irqs(mask: InterruptMask) { + if mask.is_empty() { + return; + } + unsafe { ffi::LINKLAYER_PLAT_EnableSpecificIRQ(mask.bits()) }; +} + +pub fn disable_specific_irqs(mask: InterruptMask) { + if mask.is_empty() { + return; + } + unsafe { ffi::LINKLAYER_PLAT_DisableSpecificIRQ(mask.bits()) }; +} + +pub fn enable_radio_interrupt() { + unsafe { ffi::LINKLAYER_PLAT_EnableRadioIT() }; +} + +pub fn disable_radio_interrupt() { + unsafe { ffi::LINKLAYER_PLAT_DisableRadioIT() }; +} + +pub fn notify_radio_event_start() { + unsafe { ffi::LINKLAYER_PLAT_StartRadioEvt() }; +} + +pub fn notify_radio_event_stop() { + unsafe { ffi::LINKLAYER_PLAT_StopRadioEvt() }; +} + +pub fn notify_rco_calibration_start() { + unsafe { ffi::LINKLAYER_PLAT_RCOStartClbr() }; +} + +pub fn notify_rco_calibration_stop() { + unsafe { ffi::LINKLAYER_PLAT_RCOStopClbr() }; +} + +pub fn request_temperature_measurement() { + unsafe { ffi::LINKLAYER_PLAT_RequestTemperature() }; +} + +pub fn phy_start_calibration() { + unsafe { ffi::LINKLAYER_PLAT_PhyStartClbr() }; +} + +pub fn phy_stop_calibration() { + unsafe { ffi::LINKLAYER_PLAT_PhyStopClbr() }; +} + +/// Forward the latest scheduler timing tuple to the vendor shim. +pub fn notify_scheduler_timing_update(timing: SchedulerTiming) { + let mut raw = timing.into_raw(); + unsafe { ffi::LINKLAYER_PLAT_SCHLDR_TIMING_UPDATE_NOT(&mut raw as *mut _) }; +} + +pub fn st_company_id() -> u32 { + unsafe { ffi::LINKLAYER_PLAT_GetSTCompanyID() } +} + +pub fn unique_device_number() -> u32 { + unsafe { ffi::LINKLAYER_PLAT_GetUDN() } +} diff --git a/wba-linklayer/src/ll_sys_if.rs b/wba-linklayer/src/ll_sys_if.rs new file mode 100644 index 0000000..3ba1f77 --- /dev/null +++ b/wba-linklayer/src/ll_sys_if.rs @@ -0,0 +1,290 @@ +#![allow(dead_code)] +use bitflags::bitflags; +use core::ffi::c_void; + +use crate::ffi; + +/// Runtime configuration applied when bootstrapping the link-layer system layer. +#[derive(Debug, Clone, Copy)] +pub struct SystemConfig { + /// Invoke `ll_sys_config_params` before any other action. + pub configure_params: bool, + /// Register the background process with the ST scheduler. + pub init_background_task: bool, + /// Enable IRQs once initialisation completes. + pub enable_irq_on_init: bool, +} + +impl Default for SystemConfig { + fn default() -> Self { + Self { + configure_params: true, + init_background_task: true, + enable_irq_on_init: true, + } + } +} + +bitflags! { + /// Convenience bit-mask describing which interrupt groups to toggle. + #[derive(Default)] + pub struct InterruptMask: u8 { + const RADIO_HIGH = ffi::LL_HIGH_ISR_ONLY as u8; + const RADIO_LOW = ffi::LL_LOW_ISR_ONLY as u8; + const SYSTEM_LOW = ffi::SYS_LOW_ISR as u8; + } +} + +/// Triple describing scheduler timings expressed in link-layer sleep-timer cycles (31 µs). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SchedulerTiming { + pub drift_time: u32, + pub execution_time: u32, + pub scheduling_time: u32, +} + +impl SchedulerTiming { + pub const fn new(drift_time: u32, execution_time: u32, scheduling_time: u32) -> Self { + Self { + drift_time, + execution_time, + scheduling_time, + } + } + + fn into_raw(self) -> ffi::Evnt_timing_t { + ffi::Evnt_timing_t { + drift_time: self.drift_time, + exec_time: self.execution_time, + schdling_time: self.scheduling_time, + } + } +} + +/// Status codes returned by several `ll_sys_*` routines. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LlSysStatus { + Ok, + Error, + Busy, +} + +impl LlSysStatus { + #[inline] + fn from_raw(value: ffi::ll_sys_status_t) -> Self { + if value == ffi::LL_SYS_STATUS_T_LL_SYS_OK { + Self::Ok + } else if value == ffi::LL_SYS_STATUS_T_LL_SYS_BUSY { + Self::Busy + } else { + Self::Error + } + } + + #[inline] + fn into_result(self) -> Result<(), Self> { + match self { + Self::Ok => Ok(()), + other => Err(other), + } + } +} + +/// Deep sleep state reported by the vendor system layer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeepSleepState { + Disabled, + Enabled, +} + +/// Initialise the system layer using the provided configuration. +pub fn init(config: &SystemConfig) { + unsafe { + if config.configure_params { + ffi::ll_sys_config_params(); + } + + if config.init_background_task { + ffi::ll_sys_bg_process_init(); + } + + if config.enable_irq_on_init { + ffi::ll_sys_enable_irq(); + } else { + ffi::ll_sys_disable_irq(); + } + } +} + +/// Register the Link Layer background task with the vendor scheduler. +pub fn register_background_task() { + unsafe { ffi::ll_sys_bg_process_init() }; +} + +/// Execute the Link Layer background process once. +pub fn run_background_once() { + unsafe { ffi::ll_sys_bg_process() }; +} + +/// Schedule the Link Layer background process from thread mode. +pub fn schedule_background() { + unsafe { ffi::ll_sys_schedule_bg_process() }; +} + +/// Schedule the Link Layer background process from ISR context. +pub fn schedule_background_from_isr() { + unsafe { ffi::ll_sys_schedule_bg_process_isr() }; +} + +/// Request a background temperature measurement (if enabled in the vendor stack). +pub fn request_temperature() { + unsafe { ffi::ll_sys_request_temperature() }; +} + +/// Invoke the vendor Host stack process hook. +pub fn host_stack_process() { + unsafe { ffi::HostStack_Process() }; +} + +/// Request the PHY calibration routine to start. +pub fn start_phy_calibration() { + unsafe { ffi::ll_sys_phy_start_clbr() }; +} + +/// Request the PHY calibration routine to stop. +pub fn stop_phy_calibration() { + unsafe { ffi::ll_sys_phy_stop_clbr() }; +} + +/// Set BLE scheduler timings; returns the effective execution time computed by the vendor firmware. +pub fn configure_ble_scheduler_timings(drift_time: u8, exec_time: u8) -> u32 { + unsafe { ffi::ll_sys_config_BLE_schldr_timings(drift_time, exec_time) } +} + +/// Forward updated scheduler timings to the vendor shim. +pub fn notify_scheduler_timing_update(timing: SchedulerTiming) { + let mut raw = timing.into_raw(); + unsafe { ffi::ll_sys_schldr_timing_update_not(&mut raw as *mut _) }; +} + +/// Return the current value of the Link Layer sleep timer. +pub fn sleep_timer_value() -> u32 { + unsafe { ffi::ll_intf_cmn_get_slptmr_value() } +} + +/// Return the number of concurrent state machines supported by the vendor firmware. +pub fn concurrent_state_machines() -> u8 { + unsafe { ffi::ll_sys_get_concurrent_state_machines_num() } +} + +/// Retrieve the brief Link Layer firmware version (major/minor/patch packed into a byte). +pub fn brief_firmware_version() -> u8 { + unsafe { ffi::ll_sys_get_brief_fw_version() } +} + +/// Retrieve the system firmware version hash. +pub fn system_firmware_version() -> u32 { + unsafe { ffi::ll_sys_get_system_fw_version() } +} + +/// Retrieve the source firmware version hash. +pub fn source_firmware_version() -> u32 { + unsafe { ffi::ll_sys_get_source_fw_version() } +} + +/// Returns `true` if a pointer refers to a location inside the BLE memory region. +pub fn is_pointer_in_ble_memory(ptr: *mut c_void) -> bool { + unsafe { ffi::ll_intf_is_ptr_in_ble_mem(ptr) != 0 } +} + +/// Host callback signature used by the vendor controller. +pub type HostCallback = unsafe extern "C" fn(*mut ffi::ble_buff_hdr_t) -> u8; + +/// Initialise the BLE controller with a host callback. +pub fn init_ble_controller(callback: HostCallback) { + unsafe { ffi::ll_sys_ble_cntrl_init(Some(callback)) }; +} + +/// Initialise the IEEE 802.15.4 MAC controller glue. +pub fn init_mac_controller() { + unsafe { ffi::ll_sys_mac_cntrl_init() }; +} + +/// Initialise the Thread controller glue. +pub fn init_thread_controller() { + unsafe { ffi::ll_sys_thread_init() }; +} + +/// Initialise the vendor sequencer background process slot. +pub fn init_sequencer_background() { + unsafe { ffi::ll_sys_sequencer_bg_process_init() }; +} + +/// Request the vendor sequencer to schedule a background process. +pub fn sequencer_schedule_background() { + unsafe { ffi::ll_sys_sequencer_schedule_bg_process() }; +} + +/// Trigger the HCI host stack processing routine. +pub fn host_stack_process_once() { + unsafe { ffi::HostStack_Process() }; +} + +/// Enter the vendor deep-sleep initialisation flow. +pub fn deep_sleep_init() -> Result<(), LlSysStatus> { + LlSysStatus::from_raw(unsafe { ffi::ll_sys_dp_slp_init() }).into_result() +} + +/// Request the vendor deep-sleep controller to enter sleep for a given duration (in sleep timer cycles). +pub fn deep_sleep_enter(duration: u32) -> Result<(), LlSysStatus> { + LlSysStatus::from_raw(unsafe { ffi::ll_sys_dp_slp_enter(duration) }).into_result() +} + +/// Exit deep sleep mode. +pub fn deep_sleep_exit() -> Result<(), LlSysStatus> { + LlSysStatus::from_raw(unsafe { ffi::ll_sys_dp_slp_exit() }).into_result() +} + +/// Query the current deep sleep state. +pub fn deep_sleep_state() -> DeepSleepState { + let state = unsafe { ffi::ll_sys_dp_slp_get_state() }; + match state { + s if s == ffi::LL_SYS_DP_SLP_STATE_T_LL_SYS_DP_SLP_ENABLED => DeepSleepState::Enabled, + _ => DeepSleepState::Disabled, + } +} + +/// Manually invoke the vendor wake-up callback hook. +/// +/// # Safety +/// +/// The caller must ensure that `arg` matches the expectations of the underlying vendor firmware. +pub unsafe fn deep_sleep_wakeup_callback(arg: *const c_void) { + ffi::ll_sys_dp_slp_wakeup_evt_clbk(arg); +} + +/// Enable vendor-managed IRQs. +pub fn enable_ll_irq() { + unsafe { ffi::ll_sys_enable_irq() }; +} + +/// Disable vendor-managed IRQs. +pub fn disable_ll_irq() { + unsafe { ffi::ll_sys_disable_irq() }; +} + +/// Enable specific vendor interrupt groups. +pub fn enable_ll_specific_irq(mask: InterruptMask) { + if mask.is_empty() { + return; + } + unsafe { ffi::ll_sys_enable_specific_irq(mask.bits()) }; +} + +/// Disable specific vendor interrupt groups. +pub fn disable_ll_specific_irq(mask: InterruptMask) { + if mask.is_empty() { + return; + } + unsafe { ffi::ll_sys_disable_specific_irq(mask.bits()) }; +} diff --git a/wba-linklayer/src/mac_sys_if.rs b/wba-linklayer/src/mac_sys_if.rs new file mode 100644 index 0000000..96959c7 --- /dev/null +++ b/wba-linklayer/src/mac_sys_if.rs @@ -0,0 +1,297 @@ +use core::mem::MaybeUninit; + +use crate::ffi; + +/// Handle identifying a MAC controller instance within the ST middleware. +/// +/// The underlying value is the 32‑bit handle that the vendor functions expect. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct MacHandle(u32); + +impl MacHandle { + /// Creates a new wrapper around the raw handle value. + #[inline] + pub const fn new(raw: u32) -> Self { + Self(raw) + } + + /// Returns the raw handle value expected by the FFI. + #[inline] + pub const fn raw(self) -> u32 { + self.0 + } +} + +/// Result type used by the safe MAC wrappers. +pub type MacResult = Result; + +/// Error codes surfaced by the MAC middleware. +/// +/// The enumeration focuses on the most frequently observed statuses. Any value +/// that is not explicitly handled is reported via [`MacStatus::Other`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MacStatus { + Success, + Busy, + InvalidParameter, + ChannelAccessFailure, + NoAck, + Unsupported, + InternalError, + Other(u8), +} + +impl MacStatus { + #[inline] + fn from_raw(value: ffi::mac_status_enum_t) -> Self { + match value as u32 { + ffi::MAC_STATUS_ENUM_T_MAC_STATUS_SUCCESS => Self::Success, + ffi::MAC_STATUS_ENUM_T_DENIED => Self::Busy, + ffi::MAC_STATUS_ENUM_T_INVALID_PARAMETER => Self::InvalidParameter, + ffi::MAC_STATUS_ENUM_T_CHANNEL_ACCESS_FAILURE => Self::ChannelAccessFailure, + ffi::MAC_STATUS_ENUM_T_NO_ACK => Self::NoAck, + ffi::MAC_STATUS_ENUM_T_UNSUPPORTED_ATTRIBUTE + | ffi::MAC_STATUS_ENUM_T_UNSUPPORTED_LEGACY + | ffi::MAC_STATUS_ENUM_T_UNSUPPORTED_SECURITY => Self::Unsupported, + ffi::MAC_STATUS_ENUM_T_INTERNAL_ERROR => Self::InternalError, + other => Self::Other(other as u8), + } + } + + #[inline] + fn into_result(self) -> MacResult { + match self { + Self::Success => Ok(()), + err => Err(err), + } + } +} + +/// Describes how often antenna diversity should rotate between antennas. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AntennaIntervalType { + None, + FixedTime, + PacketCount, +} + +impl AntennaIntervalType { + fn to_raw(self) -> ffi::ant_intrv_type_enum_t { + match self { + Self::None => ffi::ANT_INTRV_TYPE_ENUM_NO_TYPE, + Self::FixedTime => ffi::ANT_INTRV_TYPE_ENUM_FIXED_TIME, + Self::PacketCount => ffi::ANT_INTRV_TYPE_ENUM_PACKETS_NUMBER, + } + } + + fn from_raw(raw: ffi::ant_intrv_type_enum_t) -> Self { + match raw { + ffi::ANT_INTRV_TYPE_ENUM_FIXED_TIME => Self::FixedTime, + ffi::ANT_INTRV_TYPE_ENUM_PACKETS_NUMBER => Self::PacketCount, + _ => Self::None, + } + } +} + +/// Parameters passed to the vendor antenna-diversity configuration routines. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AntennaDiversityConfig { + pub interval_type: AntennaIntervalType, + pub interval_value: u32, + pub wanted_coord_short_address: u16, + pub wanted_coord_extended_address: [u8; 8], + pub max_rx_ack_retries: u8, +} + +impl Default for AntennaDiversityConfig { + fn default() -> Self { + Self { + interval_type: AntennaIntervalType::None, + interval_value: 0, + wanted_coord_short_address: 0, + wanted_coord_extended_address: [0; 8], + max_rx_ack_retries: 0, + } + } +} + +impl AntennaDiversityConfig { + fn to_raw(self) -> ffi::antenna_diversity_st { + ffi::antenna_diversity_st { + ant_intrv_type: self.interval_type.to_raw(), + ant_intrv_value: self.interval_value, + wntd_coord_shrt_addr: self.wanted_coord_short_address, + wntd_coord_ext_addr: self.wanted_coord_extended_address, + max_rx_ack_retries: self.max_rx_ack_retries, + } + } + + fn from_raw(raw: ffi::antenna_diversity_st) -> Self { + Self { + interval_type: AntennaIntervalType::from_raw(raw.ant_intrv_type), + interval_value: raw.ant_intrv_value, + wanted_coord_short_address: raw.wntd_coord_shrt_addr, + wanted_coord_extended_address: raw.wntd_coord_ext_addr, + max_rx_ack_retries: raw.max_rx_ack_retries, + } + } +} + +/// Runtime configuration flags stored inside the vendor library. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ConfigLibraryParams { + pub mac_layer_enabled: bool, + pub openthread_1_2_support: bool, + pub ack_all_frames_with_ar_bit: bool, +} + +impl Default for ConfigLibraryParams { + fn default() -> Self { + Self { + mac_layer_enabled: true, + openthread_1_2_support: true, + ack_all_frames_with_ar_bit: false, + } + } +} + +impl ConfigLibraryParams { + fn to_raw(self) -> ffi::config_lib_st { + ffi::config_lib_st { + mac_layer_build: u8::from(self.mac_layer_enabled), + support_openthread_1_2: u8::from(self.openthread_1_2_support), + ack_all_received_frames_with_ar_bit_set: u8::from(self.ack_all_frames_with_ar_bit), + } + } + + fn from_raw(raw: ffi::config_lib_st) -> Self { + Self { + mac_layer_enabled: raw.mac_layer_build != 0, + openthread_1_2_support: raw.support_openthread_1_2 != 0, + ack_all_frames_with_ar_bit: raw.ack_all_received_frames_with_ar_bit_set != 0, + } + } +} + +/// Enable or disable CSMA globally. +#[inline] +pub fn set_csma_enabled(enabled: bool) { + unsafe { ffi::mac_set_csma_en(u8::from(enabled)) }; +} + +/// Enable or disable Clear Channel Assessment globally. +#[inline] +pub fn set_cca_enabled(enabled: bool) { + unsafe { ffi::mac_set_cca_en(u8::from(enabled)) }; +} + +/// Configure the CCA threshold (dBm) for the selected MAC instance. +pub fn set_cca_threshold(handle: MacHandle, threshold_dbm: i8) -> MacResult { + let status = unsafe { ffi::mac_set_cca_threshold(handle.raw(), threshold_dbm) }; + MacStatus::from_raw(status).into_result() +} + +/// Retrieve the current CCA threshold (dBm). +pub fn get_cca_threshold(handle: MacHandle) -> MacResult { + let mut value = 0i8; + let status = unsafe { ffi::mac_get_cca_threshold(handle.raw(), &mut value) }; + match MacStatus::from_raw(status) { + MacStatus::Success => Ok(value), + err => Err(err), + } +} + +/// Request the MAC layer to perform a software reset. +/// +/// When `set_default_pib` is `true`, all PIB attributes are restored to their +/// default values; otherwise only runtime state is cleared. +#[inline] +pub fn mlme_reset(handle: MacHandle, set_default_pib: bool) { + unsafe { ffi::mlme_rst_req(handle.raw(), u8::from(set_default_pib)) }; +} + +/// Apply antenna diversity configuration parameters. +pub fn set_antenna_diversity(handle: MacHandle, params: &AntennaDiversityConfig) -> MacResult { + let mut raw = params.to_raw(); + let status = unsafe { ffi::mac_set_ant_div_params(handle.raw(), &mut raw as *mut _) }; + MacStatus::from_raw(status).into_result() +} + +/// Fetch the current antenna diversity configuration. +pub fn antenna_diversity(handle: MacHandle) -> AntennaDiversityConfig { + let mut raw = MaybeUninit::::uninit(); + unsafe { ffi::mac_get_ant_div_params(handle.raw(), raw.as_mut_ptr()) }; + AntennaDiversityConfig::from_raw(unsafe { raw.assume_init() }) +} + +/// Enable or disable antenna diversity logic. +pub fn set_antenna_diversity_enabled(handle: MacHandle, enabled: bool) -> MacResult { + let status = unsafe { ffi::mac_set_ant_div_enable(handle.raw(), u8::from(enabled)) }; + MacStatus::from_raw(status).into_result() +} + +/// Select the default antenna index used for transmission and reception. +pub fn set_default_antenna(handle: MacHandle, antenna_id: u8) -> MacResult { + let status = unsafe { ffi::mac_set_default_ant_id(handle.raw(), antenna_id) }; + MacStatus::from_raw(status).into_result() +} + +/// Adjust the RSSI threshold used when evaluating antenna diversity. +pub fn set_antenna_diversity_rssi_threshold(handle: MacHandle, threshold_dbm: i8) -> MacResult { + let status = unsafe { ffi::mac_set_ant_div_rssi_threshold(handle.raw(), threshold_dbm) }; + MacStatus::from_raw(status).into_result() +} + +/// Update the per-instance configurable library parameters. +pub fn set_config_library_params(handle: MacHandle, params: &ConfigLibraryParams) -> MacResult { + let mut raw = params.to_raw(); + let status = unsafe { ffi::mac_set_config_lib_params(handle.raw(), &mut raw as *mut _) }; + MacStatus::from_raw(status).into_result() +} + +/// Retrieve the per-instance configurable library parameters. +pub fn config_library_params(handle: MacHandle) -> ConfigLibraryParams { + let mut raw = MaybeUninit::::uninit(); + unsafe { ffi::mac_get_config_lib_params(handle.raw(), raw.as_mut_ptr()) }; + ConfigLibraryParams::from_raw(unsafe { raw.assume_init() }) +} + +/// Read the global configuration shared by all MAC instances. +#[cfg(target_os = "none")] +pub fn shared_config_library_params() -> ConfigLibraryParams { + unsafe { ConfigLibraryParams::from_raw(ffi::G_CONFIG_LIB_PARAMS) } +} + +#[cfg(not(target_os = "none"))] +pub fn shared_config_library_params() -> ConfigLibraryParams { + ConfigLibraryParams::default() +} + +/// Overwrite the global configuration shared by all MAC instances. +/// +/// # Safety +/// +/// The vendor middleware does not provide synchronization. Callers must ensure +/// that concurrent accesses across cores/threads are serialized if required. +#[cfg(target_os = "none")] +pub unsafe fn set_shared_config_library_params(params: &ConfigLibraryParams) { + unsafe { + ffi::G_CONFIG_LIB_PARAMS = params.to_raw(); + } +} + +#[cfg(not(target_os = "none"))] +pub unsafe fn set_shared_config_library_params(_params: &ConfigLibraryParams) {} + +/// Configure the vendor RTL polling period (milliseconds). +#[inline] +pub fn set_rtl_polling_time(handle: MacHandle, period_ms: u8) { + unsafe { ffi::mac_set_rtl_polling_time(handle.raw(), period_ms) }; +} + +/// Query the vendor RTL polling period (milliseconds). +#[inline] +pub fn rtl_polling_time(handle: MacHandle) -> u8 { + unsafe { ffi::mac_get_rtl_polling_time(handle.raw()) } +}