diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..02c07cc --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,28 @@ +[build] +target = "riscv32imc-esp-espidf" + +[target.xtensa-esp32-espidf] +linker = "ldproxy" + +[target.xtensa-esp32s2-espidf] +linker = "ldproxy" + +[target.xtensa-esp32s3-espidf] +linker = "ldproxy" + +[target.riscv32imc-esp-espidf] +linker = "ldproxy" +rustflags = ["-C", "default-linker-libraries"] + +[env] +ESP_IDF_SDKCONFIG_DEFAULTS = "sdkconfig.defaults" +ESP_IDF_VERSION = { value = "branch:release/v4.4" } + +ESP_IDF_GLOB_CONFIG_FILES_BASE = { value = ".", relative = true } +ESP_IDF_GLOB_CONFIG_FILES_1 = { value = "/partitions.csv" } + +[unstable] +build-std = ["std", "panic_abort"] + +[net] +git-fetch-with-cli = true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d7b781 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target/ +Cargo.lock +.embuild/ +**/.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/bluedroid.iml b/.idea/bluedroid.iml new file mode 100644 index 0000000..7025ac1 --- /dev/null +++ b/.idea/bluedroid.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..59d9a82 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..63c6e02 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..896713f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16.0) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(bluedroid) diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..85250ec --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bluedroid" +version = "0.1.0" +edition = "2021" + +[dependencies] +esp-idf-sys = { version = "0.31.6", features = ["native"] } +log = { version = "0.4.17" } +lazy_static = { version = "1.4.0" } + +[build-dependencies] +embuild = { version = "0.30.1" } +anyhow = { version = "1.0.58" } + +[dev-dependencies] +anyhow = { version = "1.0.58" } +esp-idf-sys = { version = "0.31.4", features = ["native", "binstart"] } +esp-idf-svc = { version = "0.42.1" } + +[[example]] +name = "server" +required-features = ["esp-idf-sys/binstart"] \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..cde35ca --- /dev/null +++ b/build.rs @@ -0,0 +1,4 @@ +fn main() -> anyhow::Result<()> { + embuild::build::CfgArgs::output_propagated("ESP_IDF")?; + embuild::build::LinkArgs::output_propagated("ESP_IDF") +} diff --git a/examples/server.rs b/examples/server.rs new file mode 100644 index 0000000..16a0a0c --- /dev/null +++ b/examples/server.rs @@ -0,0 +1,31 @@ +use bluedroid::{ + gatt_server::{GattServer, Application, Service}, + utilities::ble_uuid::BleUuid, +}; +use esp_idf_svc; +use log::info; +use bluedroid::gatt_server::{Characteristic, Descriptor}; + +fn main() { + esp_idf_sys::link_patches(); + esp_idf_svc::log::EspLogger::initialize_default(); + + info!("Logger initialised."); + + let main_application = Application::new("Main Application", 0x01) + .add_service( + Service::new("Service 1", BleUuid::from_uuid16(0x0001), true) + .add_characteristic( + Characteristic::new("Characteristic 1", BleUuid::from_uuid16(0x0001)) + .add_descriptor( + &mut Descriptor::new("Descriptor 1", BleUuid::from_uuid16(0x0001)) + ) + ) + ); + + let applications = [main_application]; + + let mut s = GattServer::take().unwrap(); + s.add_applications(&applications); + s.start(); +} diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..924878b --- /dev/null +++ b/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, , 0x6000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 3M, \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5fa7a00 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] + +channel = "nightly-2022-07-23" \ No newline at end of file diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..cc8fd36 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,14 @@ +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_BT_ENABLED=y +# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set +CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 +# CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS is not set +# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set +CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM=y +# CONFIG_VFS_SUPPORT_IO is not set +# CONFIG_PARTITION_TABLE_SINGLE_APP is not set +# CONFIG_PARTITION_TABLE_TWO_OTA is not set +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..483bc0c --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,6 @@ +# This file was automatically generated for projects +# without default 'CMakeLists.txt' file. + +FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*) + +idf_component_register(SRCS ${app_sources}) diff --git a/src/gatt_server/application.rs b/src/gatt_server/application.rs new file mode 100644 index 0000000..49b4686 --- /dev/null +++ b/src/gatt_server/application.rs @@ -0,0 +1,59 @@ +use crate::gatt_server::service::Service; +use esp_idf_sys::*; +use log::info; + +#[derive(Debug, Clone)] +pub struct Application { + name: Option, + services: Vec, + identifier: u16, + pub(crate) interface: Option, + handle_counter: u16, +} + +impl Application { + pub fn new(name: &str, identifier: u16) -> Self { + Application { + name: Some(String::from(name)), + services: Vec::new(), + identifier, + interface: None, + handle_counter: 0, + } + } + + pub fn add_service(mut self, service: &Service) -> Self { + self.services.push(service.clone()); + self + } + + pub(crate) fn generate_handle(&mut self) -> u16 { + self.handle_counter += 1; + self.handle_counter + } + + pub(crate) fn register_self(&self) { + info!("Registering {}.", self); + unsafe { esp_nofail!(esp_ble_gatts_app_register(self.identifier)) }; + } + + fn register_services(mut self) { + info!("Registering {}'s services.", &self); + let handle = self.generate_handle(); + self.services.iter_mut().for_each(|service| { + service.register_self(self.interface.unwrap(), handle); + }); + } +} + +impl std::fmt::Display for Application { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let interface_string = if let Some(interface) = self.interface { + format!("{}", interface) + } else { + String::from("None") + }; + + write!(f, "{} (0x{:02x}, interface: {})", self.name.clone().unwrap_or_else(|| "Unnamed application".to_string()), self.identifier, interface_string) + } +} diff --git a/src/gatt_server/characteristic.rs b/src/gatt_server/characteristic.rs new file mode 100644 index 0000000..ac15ff6 --- /dev/null +++ b/src/gatt_server/characteristic.rs @@ -0,0 +1,37 @@ +use std::fmt::Formatter; +use crate::gatt_server::descriptor::Descriptor; +use crate::utilities::ble_uuid::BleUuid; + +#[derive(Debug, Clone)] +pub struct Characteristic { + name: Option, + uuid: BleUuid, + value: Vec, + descriptors: Vec, +} + +impl Characteristic { + pub fn new(name: &str, uuid: BleUuid) -> Characteristic { + Characteristic { + name: Some(String::from(name)), + uuid, + value: Vec::new(), + descriptors: Vec::new(), + } + } + + pub fn add_descriptor(&mut self, descriptor: &mut Descriptor) -> &mut Self { + self.descriptors.push(descriptor.clone()); + self + } + + fn register_self(&mut self) {} + + fn register_descriptors(&self) {} +} + +impl std::fmt::Display for Characteristic { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({})", self.name.clone().unwrap_or_else(|| "Unnamed characteristic".to_string()), self.uuid) + } +} diff --git a/src/gatt_server/descriptor.rs b/src/gatt_server/descriptor.rs new file mode 100644 index 0000000..0077388 --- /dev/null +++ b/src/gatt_server/descriptor.rs @@ -0,0 +1,24 @@ +use crate::utilities::ble_uuid::BleUuid; + +#[derive(Debug, Clone)] +pub struct Descriptor { + name: Option, + uuid: BleUuid, + value: Vec, +} + +impl Descriptor { + pub fn new(name: &str, uuid: BleUuid) -> Descriptor { + Descriptor { + name: Some(String::from(name)), + uuid, + value: Vec::new(), + } + } +} + +impl std::fmt::Display for Descriptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({})", self.name.clone().unwrap_or_else(|| "Unnamed descriptor".to_string()), self.uuid) + } +} \ No newline at end of file diff --git a/src/gatt_server/gap_event_handler.rs b/src/gatt_server/gap_event_handler.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/gatt_server/gap_event_handler.rs @@ -0,0 +1 @@ + diff --git a/src/gatt_server/gatts_event_handler.rs b/src/gatt_server/gatts_event_handler.rs new file mode 100644 index 0000000..051b3c1 --- /dev/null +++ b/src/gatt_server/gatts_event_handler.rs @@ -0,0 +1,16 @@ +use crate::gatt_server::GattServer; +use esp_idf_sys::*; + +impl GattServer { + fn gatts_event_handler( + &mut self, + event: esp_gatts_cb_event_t, + gatts_if: esp_gatt_if_t, + param: *mut esp_ble_gatts_cb_param_t, + ) { + let params = unsafe { (*param).reg }; + if event == esp_gatts_cb_event_t_ESP_GATTS_REG_EVT && params.status == esp_gatt_status_t_ESP_GATT_OK { + // self.applications + } + } +} diff --git a/src/gatt_server/mod.rs b/src/gatt_server/mod.rs new file mode 100644 index 0000000..2ac9352 --- /dev/null +++ b/src/gatt_server/mod.rs @@ -0,0 +1,130 @@ +use std::ptr::replace; +use std::sync::Mutex; + +use esp_idf_sys::*; +use lazy_static::lazy_static; +use log::{info, warn}; + +pub use application::Application; +pub use characteristic::Characteristic; +pub use descriptor::Descriptor; +pub use service::Service; + +use crate::leaky_box_raw; + +// Structs. +mod application; +mod characteristic; +mod descriptor; +mod service; + +// Event handler. +mod gap_event_handler; +mod gatts_event_handler; + +lazy_static! { + static ref GLOBAL_GATT_SERVER: Mutex> = Mutex::new(Some(GattServer { + applications: Vec::new(), + started: false, + })); +} + +pub struct GattServer { + applications: Vec, + started: bool, +} + +impl GattServer { + pub fn take() -> Option { + if let Ok(mut server) = GLOBAL_GATT_SERVER.try_lock() { + let mut server = server.take(); + unsafe { replace(&mut server, None) } + } else { + None + } + } + + pub fn start(&mut self) { + if self.started { + warn!("GATT server already started."); + return; + } + + self.started = true; + self.initialise_ble_stack(); + + // Registration of applications, services, characteristics and descriptors. + self.applications.iter().for_each(|application| { + application.register_self(); + }) + } + + pub fn add_applications(&mut self, applications: &[Application]) { + self.applications.append(&mut applications.to_vec()); + if self.started { + warn!("In order to register the newly added applications, you'll need to restart the GATT server."); + } + } + + fn initialise_ble_stack(&mut self) { + info!("Initialising BLE stack."); + + // NVS initialisation. + unsafe { + let result = nvs_flash_init(); + if result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND { + warn!("NVS initialisation failed. Erasing NVS."); + esp_nofail!(nvs_flash_erase()); + esp_nofail!(nvs_flash_init()); + } + } + + let default_controller_configuration = esp_bt_controller_config_t { + magic: ESP_BT_CTRL_CONFIG_MAGIC_VAL, + version: ESP_BT_CTRL_CONFIG_VERSION, + controller_task_stack_size: ESP_TASK_BT_CONTROLLER_STACK as u16, + controller_task_prio: ESP_TASK_BT_CONTROLLER_PRIO as u8, + controller_task_run_cpu: CONFIG_BT_CTRL_PINNED_TO_CORE as u8, + bluetooth_mode: CONFIG_BT_CTRL_MODE_EFF as u8, + ble_max_act: CONFIG_BT_CTRL_BLE_MAX_ACT_EFF as u8, + sleep_mode: CONFIG_BT_CTRL_SLEEP_MODE_EFF as u8, + sleep_clock: CONFIG_BT_CTRL_SLEEP_CLOCK_EFF as u8, + ble_st_acl_tx_buf_nb: CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB as u8, + ble_hw_cca_check: CONFIG_BT_CTRL_HW_CCA_EFF as u8, + ble_adv_dup_filt_max: CONFIG_BT_CTRL_ADV_DUP_FILT_MAX as u16, + coex_param_en: false, + ce_len_type: CONFIG_BT_CTRL_CE_LENGTH_TYPE_EFF as u8, + coex_use_hooks: false, + hci_tl_type: CONFIG_BT_CTRL_HCI_TL_EFF as u8, + hci_tl_funcs: std::ptr::null_mut(), + txant_dft: CONFIG_BT_CTRL_TX_ANTENNA_INDEX_EFF as u8, + rxant_dft: CONFIG_BT_CTRL_RX_ANTENNA_INDEX_EFF as u8, + txpwr_dft: CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF as u8, + cfg_mask: CFG_NASK, + scan_duplicate_mode: SCAN_DUPLICATE_MODE as u8, + scan_duplicate_type: SCAN_DUPLICATE_TYPE_VALUE as u8, + normal_adv_size: NORMAL_SCAN_DUPLICATE_CACHE_SIZE as u16, + mesh_adv_size: MESH_DUPLICATE_SCAN_CACHE_SIZE as u16, + coex_phy_coded_tx_rx_time_limit: CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_EFF as u8, + hw_target_code: BLE_HW_TARGET_CODE_ESP32C3_CHIP_ECO0, + slave_ce_len_min: SLAVE_CE_LEN_MIN_DEFAULT as u8, + hw_recorrect_en: AGC_RECORRECT_EN as u8, + cca_thresh: CONFIG_BT_CTRL_HW_CCA_VAL as u8, + }; + + // BLE controller initialisation. + unsafe { + esp_nofail!(esp_bt_controller_mem_release( + esp_bt_mode_t_ESP_BT_MODE_CLASSIC_BT + )); + esp_nofail!(esp_bt_controller_init(leaky_box_raw!( + default_controller_configuration + ))); + esp_nofail!(esp_bt_controller_enable(esp_bt_mode_t_ESP_BT_MODE_BLE)); + esp_nofail!(esp_bluedroid_init()); + esp_nofail!(esp_bluedroid_enable()); + // esp_nofail!(esp_ble_gatts_register_callback(Some())); + // esp_nofail!(esp_ble_gap_register_callback(Some())); + } + } +} diff --git a/src/gatt_server/service.rs b/src/gatt_server/service.rs new file mode 100644 index 0000000..c9ed0ac --- /dev/null +++ b/src/gatt_server/service.rs @@ -0,0 +1,61 @@ +use std::fmt::Formatter; +use crate::{gatt_server::characteristic::Characteristic, leaky_box_raw, utilities::ble_uuid::BleUuid}; +use esp_idf_sys::*; +use log::info; + +#[derive(Debug, Clone)] +pub struct Service { + name: Option, + uuid: BleUuid, + characteristics: Vec, + primary: bool, + handle: Option, +} + +impl Service { + pub fn new(name: &str, uuid: BleUuid, primary: bool) -> Service { + Service { + name: Some(String::from(name)), + uuid, + characteristics: Vec::new(), + primary, + handle: None, + } + } + + pub fn add_characteristic(&mut self, characteristic: &mut Characteristic) -> &mut Self { + self.characteristics.push(characteristic.clone()); + self + } + + pub(crate) fn register_self(&mut self, interface: u8, handle: u16) { + info!("Registering {} on interface {} at handle {:04x}.", &self, interface, handle); + let id = esp_gatt_srvc_id_t { + is_primary: true, + id: self.uuid.into(), + }; + self.handle = Some(handle); + unsafe { + esp_nofail!(esp_ble_gatts_create_service(interface, leaky_box_raw!(id), self.handle.unwrap())); + } + } + + // pub(crate) fn register_characteristics(&self) { + // info!("Registering {}'s characteristics.", self); + // self.characteristics.iter().for_each(|mut characteristic| { + // characteristic.register_self(self); + // }); + // } +} + +impl std::fmt::Display for Service { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let handle_string = if let Some(handle) = self.handle { + format!("0x{:04x}", handle) + } else { + String::from("None") + }; + + write!(f, "{} ({}, handle: {})", self.name.clone().unwrap_or_else(|| "Unnamed service".to_string()), self.uuid, handle_string) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7c30079 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod gatt_server; +#[macro_use] +pub mod utilities; diff --git a/src/utilities/ble_uuid.rs b/src/utilities/ble_uuid.rs new file mode 100644 index 0000000..3125b42 --- /dev/null +++ b/src/utilities/ble_uuid.rs @@ -0,0 +1,102 @@ +use esp_idf_sys::esp_gatt_id_t; + +#[derive(Copy, Clone)] +pub enum BleUuid { + Uuid16(u16), + Uuid32(u32), + Uuid128([u8; 16]), +} + +impl BleUuid { + pub fn from_uuid16(uuid: u16) -> Self { + BleUuid::Uuid16(uuid) + } + + pub fn from_uuid32(uuid: u32) -> Self { + BleUuid::Uuid32(uuid) + } + + pub fn from_uuid128(uuid: [u8; 16]) -> Self { + BleUuid::Uuid128(uuid) + } + + pub fn as_uuid128_array(&self) -> [u8; 16] { + let base_ble_uuid = [ + 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]; + + match self { + BleUuid::Uuid16(uuid) => { + let mut uuid128 = base_ble_uuid; + + let uuid_as_bytes: [u8; 2] = uuid.to_be_bytes(); + + uuid128[12..13].copy_from_slice(&uuid_as_bytes[..]); + uuid128 + } + BleUuid::Uuid32(uuid) => { + let mut uuid128 = base_ble_uuid; + + let uuid_as_bytes: [u8; 4] = uuid.to_be_bytes(); + + uuid128[12..15].copy_from_slice(&uuid_as_bytes[..]); + uuid128 + } + BleUuid::Uuid128(uuid) => *uuid, + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for BleUuid { + fn into(self) -> esp_gatt_id_t { + let mut result = esp_gatt_id_t::default(); + match self { + BleUuid::Uuid16(uuid) => { + result.uuid.uuid.uuid16 = uuid; + result.uuid.len = 2; + } + BleUuid::Uuid32(uuid) => { + result.uuid.uuid.uuid32 = uuid; + result.uuid.len = 4; + } + BleUuid::Uuid128(uuid) => { + result.uuid.uuid.uuid128 = uuid; + result.uuid.len = 16; + } + } + + result + } +} + +impl std::fmt::Display for BleUuid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BleUuid::Uuid16(uuid) => write!(f, "0x{:02x}", uuid), + BleUuid::Uuid32(uuid) => write!(f, "0x{:04x}", uuid), + BleUuid::Uuid128(uuid) => { + let mut uuid = *uuid; + uuid.reverse(); + + let mut uuid_str = String::new(); + + for byte in uuid.iter() { + uuid_str.push_str(&format!("{:02x}", byte)); + } + uuid_str.insert(8, '-'); + uuid_str.insert(13, '-'); + uuid_str.insert(18, '-'); + + write!(f, "{}", uuid_str) + } + } + } +} + +impl std::fmt::Debug for BleUuid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} \ No newline at end of file diff --git a/src/utilities/leaky_box.rs b/src/utilities/leaky_box.rs new file mode 100644 index 0000000..bc88108 --- /dev/null +++ b/src/utilities/leaky_box.rs @@ -0,0 +1,20 @@ +#[macro_export] +macro_rules! leaky_box_raw { + ($val:expr) => { + Box::into_raw(Box::new($val)) + }; +} + +#[macro_export] +macro_rules! leaky_box_u8 { + ($val:expr) => { + leaky_box_raw!($val) as *mut u8 + }; +} + +#[macro_export] +macro_rules! leaky_box_be_bytes { + ($val:expr) => { + leaky_box_u8!($val.to_be_bytes()) + }; +} diff --git a/src/utilities/mod.rs b/src/utilities/mod.rs new file mode 100644 index 0000000..4b4b120 --- /dev/null +++ b/src/utilities/mod.rs @@ -0,0 +1,2 @@ +pub mod ble_uuid; +pub mod leaky_box;