Commit Diff


commit - /dev/null
commit + 74f9786fa7029f1b31cac153273cb3cb43c27f53
blob - /dev/null
blob + 7faf2d657815c7338a693b5e5adb7e59363992d4 (mode 644)
--- /dev/null
+++ .cargo/config.toml
@@ -0,0 +1,26 @@
+[build]
+target = "riscv32imc-esp-espidf"
+
+[target.riscv32imc-esp-espidf]
+linker = "ldproxy"
+
+# Future - necessary for the experimental "native build" of esp-idf-sys with ESP32C3
+# See also https://github.com/ivmarkov/embuild/issues/16
+rustflags = ["-C", "default-linker-libraries"]
+
+[unstable]
+build-std = ["std", "panic_abort"]
+
+[env]
+# Uncomment this and use the esp-idf-sys "native" build feature (`cargo build --features native`) to build against ESP-IDF 5.0 (master)
+#ESP_IDF_VERSION = { value = "master" }
+
+# Uncomment this and use the esp-idf-sys "native" build feature (`cargo build --features native`) to build against ESP-IDF v4.4
+ESP_IDF_VERSION = { value = "v4.4.3" } 
+
+# These configurations will pick up your custom "sdkconfig.release", "sdkconfig.debug" or "sdkconfig.defaults[.*]" files
+# that you might put in the root of the project
+# The easiest way to generate a full "sdkconfig" configuration (as opposed to manually enabling only the necessary flags via "sdkconfig.defaults[.*]"
+# is by running "cargo pio espidf menuconfig" (that is, if using the pio builder)
+ESP_IDF_SDKCONFIG = { value = "sdkconfig" }
+ESP_IDF_SDKCONFIG_DEFAULTS = { value = "sdkconfig.defaults" }
blob - /dev/null
blob + d96c51339af170c30fd00e154a40176b38b00e91 (mode 644)
--- /dev/null
+++ .gitignore
@@ -0,0 +1,4 @@
+/target
+Cargo.lock
+.embuild/
+.vscode/
\ No newline at end of file
blob - /dev/null
blob + e54ac7c0185a87e4f85c3c9693198e05b7023a0b (mode 644)
--- /dev/null
+++ Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "ble_esp32c3"
+version = "0.1.0"
+authors = ["Thomas Böhler <witcher@wiredspace.de>"]
+edition = "2021"
+
+[dependencies]
+esp-idf-sys = { version = "0.31", features = ["binstart", "native"] }
+esp-idf-svc = "0.42"
+log = "0.4"
+log-panics = "2.0"
+
+[build-dependencies]
+embuild = "0.29"
+anyhow = "1"
+
+[profile.release]
+strip = true
+lto = true
+
+[profile.dev]
+debug = true    # Symbols are nice and they don't increase the size on Flash
+opt-level = "z"
blob - /dev/null
blob + 43e08d2580248b83cd9dde8b77c68e243d67d44c (mode 644)
--- /dev/null
+++ README.md
@@ -0,0 +1,27 @@
+# ble-esp32c3
+
+Primitive Rust crate for interfacing with Bluetooth Low Energy on the ESP32-C3.
+it was developed as part of my
+[bachelor-thesis](https://sr.ht/~witcher/bachelor-thesis) and doesn't allow a
+lot of configuration.
+
+## Compiling
+
+Install the dependencies:
+
+- Rust (nightly, for RISCV on the ESP32-C3)
+
+```sh
+cargo build
+```
+
+## Resources
+
+Send patches and questions to
+[~witcher/bachelor-thesis@lists.sr.ht](https://lists.sr.ht/~witcher/bachelor-thesis).
+
+Instructions for preparing a patch are available at
+[git-send-email.io](https://git-send-email.io/).
+
+Bugs and todo can be found at
+[~witcher/bachelor-thesis](https://todo.sr.ht/~witcher/bachelor-thesis).
blob - /dev/null
blob + 4dd5e1f41a617a45cffc47a1f386b099ab8bec74 (mode 644)
--- /dev/null
+++ build.rs
@@ -0,0 +1,5 @@
+// Necessary because of this issue: https://github.com/rust-lang/cargo/issues/9641
+fn main() -> anyhow::Result<()> {
+    embuild::build::CfgArgs::output_propagated("ESP_IDF")?;
+    embuild::build::LinkArgs::output_propagated("ESP_IDF")
+}
blob - /dev/null
blob + f524b17c05428e88e04155bc26b138bdc0fa1785 (mode 644)
--- /dev/null
+++ examples/simple.rs
@@ -0,0 +1,75 @@
+use ble_esp32c3::gatt::*;
+
+const SERVICE_A_UUID: u16 = 0x00FF;
+const SERVICE_A_CHAR_1_UUID: u16 = 0xFF01;
+const SERVICE_A_CHAR_2_UUID: u16 = 0xFF02;
+const SERVICE_A_CHAR_1_DESC_1_UUID: u16 = 0xABCD as u16;
+
+fn setup(ble: &mut ble_esp32c3::Ble) {
+    let descriptor1 = Descriptor::new(
+        SERVICE_A_CHAR_1_DESC_1_UUID.into(),
+        2,
+        Some(vec![12, 34]),
+        Some(Permissions::Read),
+    );
+
+    let mut characteristic1 = Characteristic::new(
+        SERVICE_A_CHAR_1_UUID.into(),
+        Permissions::ReadWrite,
+        4,
+        Some(vec![0, 0, 0, 0]),
+        vec![
+            descriptor1,
+            Descriptor::client_characteristic_configuration(),
+        ],
+        None,
+    );
+    characteristic1.add_prop_read();
+    characteristic1.add_prop_notify();
+
+    let mut characteristic2 = Characteristic::new(
+        SERVICE_A_CHAR_2_UUID.into(),
+        Permissions::ReadWrite,
+        4,
+        Some(vec![0, 0, 0, 0]),
+        vec![],
+        None,
+    );
+    characteristic2.add_prop_read();
+    characteristic2.add_prop_write();
+
+    let service1 = Service::new(
+        SERVICE_A_UUID.into(),
+        vec![characteristic1, characteristic2],
+    );
+
+    ble.add_service(service1);
+}
+
+fn main() {
+    esp_idf_sys::link_patches();
+
+    // Bind the log crate to the ESP Logging facilities
+    esp_idf_svc::log::EspLogger::initialize_default();
+
+    let mut ble = ble_esp32c3::Ble::default();
+    setup(&mut ble);
+    log::error!("ble: {:?}", ble.init());
+
+    let char1_handle =
+        ble_esp32c3::Ble::find_attribute_by_uuid(SERVICE_A_CHAR_1_UUID.into()).unwrap();
+    let mut frame_cnt = 0u32;
+    loop {
+        if let Some(rssi) = ble_esp32c3::Ble::rssi() {
+            log::info!("RSSI {}", rssi);
+        } else {
+            log::info!("Currently not connected.");
+        }
+
+        frame_cnt = frame_cnt.wrapping_add(1);
+        char1_handle
+            .write_value(&mut frame_cnt.to_be_bytes())
+            .unwrap();
+        std::thread::sleep(std::time::Duration::from_millis(1000));
+    }
+}
blob - /dev/null
blob + 271800cb2f3791b3adc24328e71c9e2550b439db (mode 644)
--- /dev/null
+++ rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
\ No newline at end of file
blob - /dev/null
blob + dd77241baf67be4869a6ab45088c55b8142416e5 (mode 644)
--- /dev/null
+++ sdkconfig.defaults
@@ -0,0 +1,7 @@
+CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000
+
+CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
+
+CONFIG_BT_ENABLED=y
+CONFIG_BT_BLUEDROID_ENABLED=y
+CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
blob - /dev/null
blob + 0f7d620753ebbcb11fa104d2ffa32d19a5250b37 (mode 644)
--- /dev/null
+++ sdkconfig.release
@@ -0,0 +1,6 @@
+CONFIG_LOG_DEFAULT_LEVEL_NONE=y
+CONFIG_LOG_DEFAULT_LEVEL_ERROR=y
+CONFIG_LOG_DEFAULT_LEVEL_WARN=n
+CONFIG_LOG_DEFAULT_LEVEL_INFO=n
+CONFIG_LOG_DEFAULT_LEVEL_DEBUG=n
+CONFIG_LOG_DEFAULT_LEVEL_VERBOSE=n
blob - /dev/null
blob + 8f1a8bb4780212a62fe4f6561b3fcbcfde63b758 (mode 644)
--- /dev/null
+++ src/error.rs
@@ -0,0 +1,25 @@
+use esp_idf_sys::esp_gatt_status_t;
+
+#[derive(Debug)]
+pub enum Error {
+    GATT(esp_gatt_status_t),
+    
+    // TODO
+    ESP()
+}
+
+impl Error {
+    pub fn get_as_u32(&self,) -> u32 {
+        match self {
+            Error::GATT(err_code) => *err_code,
+            Error::ESP() => 0,
+        }
+    }
+
+    pub fn get_as_i32(&self,) -> i32 {
+        match self {
+            Error::GATT(err_code) => *err_code as i32,
+            Error::ESP() => 0,
+        }
+    }
+}
\ No newline at end of file
blob - /dev/null
blob + 331b6953336add9937314b99cecd96d917aa8753 (mode 644)
--- /dev/null
+++ src/gap.rs
@@ -0,0 +1,121 @@
+use esp_idf_sys::*;
+
+use super::STATE;
+
+#[allow(non_snake_case)]
+pub unsafe extern "C" fn ble_gap_event_handler(
+    event: esp_gap_ble_cb_event_t,
+    param: *mut esp_ble_gap_cb_param_t,
+) {
+    log::trace!("GAP Event: {}", event);
+
+    match event {
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_ADV_START_COMPLETE_EVT => {
+            let param = (*param).adv_data_cmpl;
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                log::debug!("Advertising started successfully");
+            } else {
+                log::error!("Advertising start failed");
+            }
+        }
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT => {
+            let param = (*param).adv_data_cmpl;
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                log::debug!("Advertising stopped successfully");
+            } else {
+                log::error!("Advertising stop failed");
+            }
+        }
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT => {
+            let param = (*param).adv_data_cmpl;
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                super::gatt::start_advertising();
+            } else {
+                log::error!("Advertising failed: {:x}", param.status);
+            }
+        }
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT => {
+            let param = (*param).read_rssi_cmpl;
+
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                STATE.set_rssi(param.rssi);
+            }
+        }
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT => {
+            let param = (*param).pkt_data_lenth_cmpl;
+
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                log::debug!(
+                    "ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: rx_len {}, tx_len {}",
+                    param.params.rx_len,
+                    param.params.tx_len
+                );
+            } else {
+                log::error!("Setting packet length failed: {:x}", param.status);
+            }
+        }
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT => {
+            let param = (*param).update_conn_params;
+            log::debug!(
+                "update connection params status = {}, min_int = {}, max_int = {}, conn_int
+                        = {}, latency = {}, timeout = {}",
+                param.status,
+                param.min_int,
+                param.max_int,
+                param.conn_int,
+                param.latency,
+                param.timeout
+            );
+        }
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_READ_PHY_COMPLETE_EVT => {
+            let param = (*param).read_phy;
+
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                log::debug!(
+                    "ESP_GAP_BLE_READ_PHY_COMPLETE_EVT: bda = {:?}, tx_phy = {}, rx_phy = {}",
+                    param.bda,
+                    param.tx_phy,
+                    param.rx_phy
+                );
+            } else {
+                log::error!("PHY read not successful: {:x}", param.status);
+            }
+        }
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_SET_PREFERED_DEFAULT_PHY_COMPLETE_EVT => {
+            let param = (*param).set_perf_def_phy;
+
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                log::debug!("Setting preferred default PHY successful!");
+            } else {
+                log::error!(
+                    "Setting preferred default PHY not successful: {:x}",
+                    param.status
+                );
+            }
+        }
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_SET_PREFERED_PHY_COMPLETE_EVT => {
+            let param = (*param).set_perf_phy;
+
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                log::debug!("Setting preferred PHY successful!");
+            } else {
+                log::error!("Setting preferred PHY not successful: {:x}", param.status);
+            }
+        }
+        esp_gap_ble_cb_event_t_ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT => {
+            let param = (*param).phy_update;
+
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                log::debug!(
+                    "ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: bda = {:?}, tx_phy = {}, rx_phy = {}",
+                    param.bda,
+                    param.tx_phy,
+                    param.rx_phy
+                );
+            } else {
+                log::error!("PHY update was not successful: {:x}", param.status);
+            }
+        }
+        _ => (),
+    };
+}
blob - /dev/null
blob + 373520950cd5211f914a08a0cba438b6833ad166 (mode 644)
--- /dev/null
+++ src/gatt/attribute.rs
@@ -0,0 +1,71 @@
+use esp_idf_sys::{
+    esp_ble_gatts_get_attr_value, esp_ble_gatts_set_attr_value, esp_gatt_perm_t,
+    esp_gatt_status_t_ESP_GATT_ILLEGAL_PARAMETER, esp_gatt_status_t_ESP_GATT_OK,
+    ESP_GATT_PERM_READ, ESP_GATT_PERM_WRITE,
+};
+
+use crate::{Error, Result};
+
+pub trait IsAttribute {
+    fn handle(&self) -> u16;
+    fn value_max_length(&self) -> u16;
+    fn initial_value_length(&self) -> u16;
+    fn initial_value_pointer_mut(&mut self) -> *mut u8;
+    fn permissions(&self) -> esp_gatt_perm_t;
+    fn after_write_value(&self, new_value: &mut [u8]);
+
+    fn can_read(&self) -> bool {
+        (self.permissions() & ESP_GATT_PERM_READ as u16) > 0
+    }
+
+    fn can_write(&self) -> bool {
+        (self.permissions() & ESP_GATT_PERM_WRITE as u16) > 0
+    }
+
+    fn write_value(&self, new_value: &mut [u8]) -> Result<()> {
+        let length = new_value.len() as u16;
+        let max_length = self.value_max_length();
+        let handle = self.handle();
+
+        if length > max_length {
+            debug_assert!(length <= max_length);
+            // TODO: is there a more fitting error code?
+            return Err(Error::GATT(esp_gatt_status_t_ESP_GATT_ILLEGAL_PARAMETER));
+        }
+
+        unsafe {
+            let status_code =
+                esp_ble_gatts_set_attr_value(handle, length, new_value.as_ptr()) as u32;
+            if status_code != esp_gatt_status_t_ESP_GATT_OK {
+                debug_assert!(false, "Failed to update value of a attribute!");
+
+                Err(Error::GATT(status_code))
+            } else {
+                self.after_write_value(new_value);
+
+                Ok(())
+            }
+        }
+    }
+
+    fn read_value(&self) -> Result<Vec<u8>> {
+        let handle = self.handle();
+
+        unsafe {
+            let mut length: u16 = 0;
+            let mut data: *const u8 = std::ptr::null_mut();
+
+            let status_code = esp_ble_gatts_get_attr_value(handle, &mut length, &mut data);
+            if status_code != esp_gatt_status_t_ESP_GATT_OK {
+                debug_assert!(false, "Failed to read value of an attribute!");
+
+                Err(Error::GATT(status_code))
+            } else {
+                let mut result = Vec::with_capacity(length.into());
+                result.extend_from_slice(std::slice::from_raw_parts(data, length.into()));
+
+                Ok(result)
+            }
+        }
+    }
+}
blob - /dev/null
blob + 52e549e5a532985718d93bc008897aeabe4534cd (mode 644)
--- /dev/null
+++ src/gatt/char.rs
@@ -0,0 +1,149 @@
+use esp_idf_sys::{
+    esp_ble_gatts_send_indicate, esp_gatt_char_prop_t, esp_gatt_perm_t,
+    ESP_GATT_CHAR_PROP_BIT_INDICATE, ESP_GATT_CHAR_PROP_BIT_NOTIFY, ESP_GATT_CHAR_PROP_BIT_READ,
+    ESP_GATT_CHAR_PROP_BIT_WRITE,
+};
+
+use super::Descriptor;
+use super::Permissions;
+use super::UUID;
+
+const CHARACTERISTIC_VALUE_LENGTH_MAX: u16 = 0x40;
+
+pub struct Characteristic {
+    pub uuid: UUID,
+    pub props: esp_gatt_char_prop_t,
+    pub attr_perms: esp_gatt_perm_t,
+    pub declaration_handle: u16,
+    pub value_handle: u16,
+    pub descriptors: Vec<Descriptor>,
+    pub max_length: u16,
+    initial_value: Vec<u8>,
+    pub subscribed_connections: Vec<u16>,
+    pub on_change_callback: Option<fn(Vec<u8>)>,
+}
+
+impl Characteristic {
+    pub fn new(
+        uuid: UUID,
+        perms: Permissions,
+        max_length: u16,
+        initial_value: Option<Vec<u8>>,
+        descriptors: Vec<Descriptor>,
+        on_change_callback: Option<fn(Vec<u8>)>,
+    ) -> Self {
+        // debug_assert!(!perms.can_read() || initial_value.is_some());
+
+        let mut value = Vec::with_capacity(max_length as usize);
+        if let Some(mut init) = initial_value {
+            debug_assert!(init.len() <= max_length as usize);
+            value.append(&mut init);
+        }
+
+        Self {
+            uuid,
+            props: 0,
+            attr_perms: perms.get_permissions_u16(),
+            value_handle: 0,
+            declaration_handle: 0,
+            descriptors,
+            max_length,
+            initial_value: value,
+            subscribed_connections: vec![],
+            on_change_callback,
+        }
+    }
+
+    pub fn add_prop_read(&mut self) {
+        self.props |= ESP_GATT_CHAR_PROP_BIT_READ as u8;
+    }
+
+    pub fn remove_prop_read(&mut self) {
+        self.props &= !ESP_GATT_CHAR_PROP_BIT_READ as u8;
+    }
+
+    pub fn has_prop_read(&self) -> bool {
+        (self.props & ESP_GATT_CHAR_PROP_BIT_READ as u8) > 0
+    }
+
+    pub fn add_prop_write(&mut self) {
+        self.props |= ESP_GATT_CHAR_PROP_BIT_WRITE as u8
+    }
+
+    pub fn remove_prop_write(&mut self) {
+        self.props &= !ESP_GATT_CHAR_PROP_BIT_WRITE as u8
+    }
+
+    pub fn has_prop_write(&self) -> bool {
+        (self.props & ESP_GATT_CHAR_PROP_BIT_WRITE as u8) > 0
+    }
+
+    pub fn add_prop_notify(&mut self) {
+        self.props |= ESP_GATT_CHAR_PROP_BIT_NOTIFY as u8
+    }
+
+    pub fn remove_prop_notify(&mut self) {
+        self.props &= !ESP_GATT_CHAR_PROP_BIT_NOTIFY as u8
+    }
+
+    pub fn has_prop_notify(&self) -> bool {
+        (self.props & ESP_GATT_CHAR_PROP_BIT_NOTIFY as u8) > 0
+    }
+
+    pub fn add_prop_indicate(&mut self) {
+        self.props |= ESP_GATT_CHAR_PROP_BIT_INDICATE as u8
+    }
+
+    pub fn remove_prop_indicate(&mut self) {
+        self.props &= !ESP_GATT_CHAR_PROP_BIT_INDICATE as u8
+    }
+
+    pub fn has_prop_indicate(&self) -> bool {
+        (self.props & ESP_GATT_CHAR_PROP_BIT_INDICATE as u8) > 0
+    }
+
+    pub fn add_descriptor(&mut self, desc: Descriptor) {
+        self.descriptors.push(desc);
+    }
+}
+
+impl super::IsAttribute for Characteristic {
+    fn handle(&self) -> u16 {
+        self.value_handle
+    }
+
+    fn initial_value_pointer_mut(&mut self) -> *mut u8 {
+        self.initial_value.as_mut_ptr()
+    }
+
+    fn initial_value_length(&self) -> u16 {
+        self.initial_value.len() as u16
+    }
+
+    fn value_max_length(&self) -> u16 {
+        self.max_length
+    }
+
+    fn permissions(&self) -> esp_gatt_perm_t {
+        self.attr_perms
+    }
+
+    fn after_write_value(&self, new_value: &mut [u8]) {
+        if self.has_prop_notify() || self.has_prop_indicate() {
+            let need_confirm = !self.has_prop_notify();
+            self.subscribed_connections
+                .iter()
+                .for_each(|conn_id| unsafe {
+                    esp_ble_gatts_send_indicate(
+                        crate::MAIN_PROFILE_GATTS_IF,
+                        *conn_id,
+                        self.handle(),
+                        new_value.len() as u16,
+                        new_value.as_mut_ptr(),
+                        need_confirm,
+                    );
+                });
+            // TODO: check if 'indicate' requires additional work to be done here
+        }
+    }
+}
blob - /dev/null
blob + 51de7c633ec1f32b6094228d736b58effb33ce68 (mode 644)
--- /dev/null
+++ src/gatt/descriptor.rs
@@ -0,0 +1,72 @@
+use esp_idf_sys::esp_gatt_perm_t;
+
+use super::Permissions;
+use super::UUID;
+
+pub struct Descriptor {
+    pub uuid: UUID,
+    pub handle: u16,
+    pub max_length: u16,
+    initial_value: Vec<u8>,
+    pub perms: esp_gatt_perm_t,
+}
+
+impl Descriptor {
+    pub fn new(
+        uuid: UUID,
+        max_length: u16,
+        initial_value: Option<Vec<u8>>,
+        permissions: Option<Permissions>,
+    ) -> Self {
+        let mut value = Vec::with_capacity(max_length as usize);
+        if let Some(mut init) = initial_value {
+            debug_assert!(init.len() <= max_length as usize);
+            value.append(&mut init);
+        }
+        Self {
+            uuid,
+            handle: 0,
+            max_length,
+            initial_value: value,
+            perms: permissions
+                .map(|p| p.get_permissions_u16())
+                .unwrap_or_default(),
+        }
+    }
+
+    pub fn client_characteristic_configuration() -> Self {
+        Descriptor {
+            uuid: super::uuid::CLIENT_CHARACTERISTIC_CONFIGURATION,
+            handle: 0,
+            max_length: 2,
+            initial_value: vec![0, 0],
+            perms: Permissions::ReadWrite.get_permissions_u16(),
+        }
+    }
+}
+
+impl super::IsAttribute for Descriptor {
+    fn handle(&self) -> u16 {
+        self.handle
+    }
+
+    fn initial_value_pointer_mut(&mut self) -> *mut u8 {
+        self.initial_value.as_mut_ptr()
+    }
+
+    fn initial_value_length(&self) -> u16 {
+        self.initial_value.len() as u16
+    }
+
+    fn value_max_length(&self) -> u16 {
+        self.max_length
+    }
+
+    fn permissions(&self) -> esp_gatt_perm_t {
+        self.perms
+    }
+
+    fn after_write_value(&self, _: &mut [u8]) {
+        // do nothing
+    }
+}
blob - /dev/null
blob + a5283b82f3b3c9f72deb4d6061bf50043fb0dcd0 (mode 644)
--- /dev/null
+++ src/gatt/event_handler.rs
@@ -0,0 +1,273 @@
+use esp_idf_sys::*;
+
+use super::{enter_handles, start_advertising, IsAttribute, SERVICES};
+use crate::{Error, Result, MAIN_PROFILE_GATTS_IF, STATE, UPDATE_RUNNING};
+
+static mut TEST_TABLE: Option<[esp_gatts_attr_db_t; 4]> = None;
+
+#[allow(clippy::missing_safety_doc)]
+#[allow(non_snake_case)]
+pub unsafe extern "C" fn ble_gatt_event_handler(
+    event: esp_gatts_cb_event_t,
+    gatts_if: esp_gatt_if_t,
+    param: *mut esp_ble_gatts_cb_param_t,
+) {
+    match event {
+        esp_gatts_cb_event_t_ESP_GATTS_MTU_EVT => {
+            let param = (*param).mtu;
+            log::debug!(
+                "ESP_GATTS_MTU_EVT, MTU size {}, conn_id {}",
+                param.mtu,
+                param.conn_id
+            );
+        }
+        esp_gatts_cb_event_t_ESP_GATTS_REG_EVT => {
+            let param = (*param).reg;
+
+            if param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS {
+                debug_assert!(crate::MAIN_PROFILE_GATTS_IF == ESP_GATT_IF_NONE as u8);
+                crate::MAIN_PROFILE_GATTS_IF = gatts_if;
+
+                let attr_table = super::get_attribute_table();
+                debug_assert!(attr_table.len() <= 255);
+                if !attr_table.is_empty() {
+                    let res = esp_ble_gatts_create_attr_tab(
+                        attr_table.as_ptr(),
+                        gatts_if,
+                        attr_table.len() as u8,
+                        0,
+                    );
+                    debug_assert!(res == ESP_OK);
+                }
+            }
+        }
+
+        esp_gatts_cb_event_t_ESP_GATTS_START_EVT => {
+            let param = (*param).start;
+            debug_assert!(param.status == esp_bt_status_t_ESP_BT_STATUS_SUCCESS);
+        }
+
+        esp_gatts_cb_event_t_ESP_GATTS_READ_EVT => {
+            let param = (*param).read;
+
+            if match handle_read(&param) {
+                Err(error_code) => esp_ble_gatts_send_response(
+                    gatts_if,
+                    param.conn_id,
+                    param.trans_id,
+                    error_code.get_as_u32(),
+                    std::ptr::null_mut(),
+                ),
+                Ok(value) => {
+                    let mut val: [u8; 600] = [0; 600];
+
+                    val[..value.len()].copy_from_slice(&value[..]);
+
+                    let rsp_value = esp_gatt_value_t {
+                        handle: param.handle,
+                        value: val,
+                        len: value.len() as u16,
+                        ..Default::default()
+                    };
+                    let mut response = esp_gatt_rsp_t {
+                        attr_value: rsp_value,
+                    };
+
+                    esp_ble_gatts_send_response(
+                        gatts_if,
+                        param.conn_id,
+                        param.trans_id,
+                        esp_gatt_status_t_ESP_GATT_OK,
+                        &mut response,
+                    )
+                }
+            } != ESP_OK
+            {
+                debug_assert!(false, "Failed to respond.");
+            }
+        }
+
+        esp_gatts_cb_event_t_ESP_GATTS_CREAT_ATTR_TAB_EVT => {
+            let param = (*param).add_attr_tab;
+
+            if param.status != esp_gatt_status_t_ESP_GATT_OK {
+                panic!("failure: {}", param.status);
+            }
+
+            enter_handles(std::slice::from_raw_parts(
+                param.handles,
+                param.num_handle as usize,
+            ));
+            SERVICES.iter().for_each(|service| {
+                let retval = esp_ble_gatts_start_service(service.handle);
+                debug_assert!(retval == ESP_OK)
+            });
+        }
+
+        esp_gatts_cb_event_t_ESP_GATTS_WRITE_EVT => {
+            let param = (*param).write;
+
+            if !param.is_prep {
+                // this is a 'normal' write
+                debug_assert_eq!(gatts_if, MAIN_PROFILE_GATTS_IF);
+                let status = handle_write(&param);
+                if param.need_rsp {
+                    esp_ble_gatts_send_response(
+                        gatts_if,
+                        param.conn_id,
+                        param.trans_id,
+                        status,
+                        std::ptr::null_mut(),
+                    );
+                }
+            } else {
+                // TODO: handle long-write ?
+            }
+        }
+
+        esp_gatts_cb_event_t_ESP_GATTS_CONNECT_EVT => {
+            let param = (*param).connect;
+
+            STATE.set_addr(param.remote_bda);
+
+            if UPDATE_RUNNING.is_none() {
+                UPDATE_RUNNING = Some(std::thread::spawn(move || {
+                    while let Some(addr) = STATE.addr() {
+                        esp_ble_gap_read_rssi(addr);
+                        std::thread::sleep(std::time::Duration::from_millis(50));
+                    }
+                }));
+            }
+        }
+        esp_gatts_cb_event_t_ESP_GATTS_DISCONNECT_EVT => {
+            let param = (*param).disconnect;
+
+            STATE.set_disconnected();
+
+            SERVICES.iter_mut().for_each(|service| {
+                service.characteristics.iter_mut().for_each(|char| {
+                    if let Some(index) = char
+                        .subscribed_connections
+                        .iter()
+                        .enumerate()
+                        .find_map(|(i, e)| if *e == param.conn_id { Some(i) } else { None })
+                    {
+                        char.subscribed_connections.remove(index);
+                    }
+                })
+            });
+
+            // TODO: ensure that dropping the handle terminates the thread
+            UPDATE_RUNNING.take();
+
+            start_advertising();
+        }
+
+        _ => {}
+    }
+}
+
+unsafe fn handle_read(param: &esp_ble_gatts_cb_param_t_gatts_read_evt_param) -> Result<Vec<u8>> {
+    for service in SERVICES.iter() {
+        if service.handle == param.handle {
+            // what to do?
+        }
+        for char in service.characteristics.iter() {
+            if char.handle() == param.handle {
+                if !char.has_prop_read() {
+                    // it would make little sense to create a characteritic than the client can write but the attribute cant be written to
+                    debug_assert!(char.can_read());
+
+                    return Err(Error::GATT(esp_gatt_status_t_ESP_GATT_READ_NOT_PERMIT));
+                } else {
+                    return char.read_value();
+                }
+            }
+            for desc in char.descriptors.iter() {
+                if desc.handle() == param.handle {
+                    if !(char.has_prop_write() && desc.can_write()) {
+                        return Err(Error::GATT(esp_gatt_status_t_ESP_GATT_READ_NOT_PERMIT));
+                    } else {
+                        return desc.read_value();
+                    }
+                }
+            }
+        }
+    }
+
+    // TODO: use esp_gatt_status_t_ESP_GATT_INVALID_HANDLE instead?
+    Err(Error::GATT(esp_gatt_status_t_ESP_GATT_NOT_FOUND))
+}
+
+unsafe fn handle_write(
+    param: &esp_ble_gatts_cb_param_t_gatts_write_evt_param,
+) -> esp_gatt_status_t {
+    // TODO: use esp_gatt_status_t_ESP_GATT_INVALID_HANDLE instead?
+    let mut status = esp_gatt_status_t_ESP_GATT_NOT_FOUND;
+    for service in SERVICES.iter_mut() {
+        if service.handle == param.handle {
+            // what to do?
+        }
+        for char in service.characteristics.iter_mut() {
+            if char.handle() == param.handle {
+                if !char.has_prop_write() {
+                    // it would make little sense to create a characteritic that the client can write but the attribute cant be written to
+                    debug_assert!(char.can_write());
+
+                    return esp_gatt_status_t_ESP_GATT_WRITE_NOT_PERMIT;
+                } else {
+                    let was_written = char.write_value(std::slice::from_raw_parts_mut(
+                        param.value,
+                        param.len as usize,
+                    ));
+
+                    if let Some(cb) = char.on_change_callback {
+                        match char.read_value() {
+                            Ok(new_value) => cb(new_value),
+                            Err(error) => println!(
+                                "Failed to read updated value of characteristic \"{}\": {}",
+                                char.uuid,
+                                error.get_as_u32()
+                            ),
+                        }
+                    }
+
+                    status = match was_written {
+                        Err(error) => error.get_as_u32(),
+                        Ok(()) => esp_gatt_status_t_ESP_GATT_OK,
+                    };
+
+                    break;
+                }
+            }
+            for desc in char.descriptors.iter() {
+                if desc.handle() == param.handle {
+                    if !desc.can_write() {
+                        return esp_gatt_status_t_ESP_GATT_WRITE_NOT_PERMIT;
+                    } else {
+                        let was_written = desc.write_value(std::slice::from_raw_parts_mut(
+                            param.value,
+                            param.len as usize,
+                        ));
+
+                        if was_written.is_ok()
+                            && desc.uuid == super::uuid::CLIENT_CHARACTERISTIC_CONFIGURATION
+                            && !char.subscribed_connections.contains(&param.conn_id)
+                        {
+                            char.subscribed_connections.push(param.conn_id);
+                        }
+
+                        status = match was_written {
+                            Ok(()) => esp_gatt_status_t_ESP_GATT_OK,
+                            Err(error) => error.get_as_u32(),
+                        };
+
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    status
+}
blob - /dev/null
blob + 08b4b8e9f0bb431b67603ac729bc1d7cc6f1ede5 (mode 644)
--- /dev/null
+++ src/gatt/mod.rs
@@ -0,0 +1,133 @@
+mod attribute;
+mod char;
+mod descriptor;
+mod event_handler;
+mod permissions;
+mod service;
+mod uuid;
+
+pub use self::char::Characteristic;
+use crate::gatt::uuid::{ATTRIBUTE_TABLE_CHARACTER_DECLARATION, ATTRIBUTE_TABLE_PRIMARY_SERVICE};
+pub use attribute::IsAttribute;
+pub use descriptor::Descriptor;
+pub use event_handler::ble_gatt_event_handler;
+pub use permissions::Permissions;
+pub use service::Service;
+pub use uuid::UUID;
+
+use esp_idf_sys::*;
+
+const MAX_SERVICES: usize = 10;
+pub static mut SERVICES: Vec<Service> = vec![];
+
+pub fn start_advertising() {
+    let mut adv_params = esp_ble_adv_params_t {
+        adv_int_min: 0x0800, // 1.28 seconds, before: 0x20
+        adv_int_max: 0x0800, // 1.28 seconds, before: 0x40
+        adv_type: esp_ble_adv_type_t_ADV_TYPE_IND,
+        own_addr_type: esp_ble_addr_type_t_BLE_ADDR_TYPE_PUBLIC,
+        peer_addr: [0, 0, 0, 0, 0, 0],
+        peer_addr_type: esp_ble_addr_type_t_BLE_ADDR_TYPE_PUBLIC,
+        channel_map: esp_ble_adv_channel_t_ADV_CHNL_ALL,
+        adv_filter_policy: esp_ble_adv_filter_t_ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
+    };
+    unsafe { esp!(esp_ble_gap_start_advertising(&mut adv_params)) }
+        .expect("Could not start advertising: {err:?}");
+}
+
+unsafe fn enter_handles(handles: &[u16]) {
+    let mut it = 0;
+    for service in SERVICES.iter_mut() {
+        debug_assert!(it < handles.len());
+        service.handle = handles[it];
+        it += 1;
+        for char in service.characteristics.iter_mut() {
+            debug_assert!(it + 1 < handles.len());
+            char.declaration_handle = handles[it];
+            char.value_handle = handles[it + 1];
+            it += 2;
+            for desc in char.descriptors.iter_mut() {
+                debug_assert!(it < handles.len());
+                desc.handle = handles[it];
+                it += 1;
+            }
+        }
+    }
+
+    debug_assert_eq!(it, handles.len());
+}
+
+unsafe fn get_attribute_table() -> Vec<esp_gatts_attr_db_t> {
+    get_attribute_table_for_services(&mut SERVICES)
+}
+
+unsafe fn get_attribute_table_for_services(services: &mut [Service]) -> Vec<esp_gatts_attr_db_t> {
+    let mut result = vec![];
+
+    for service in services {
+        result.push(esp_gatts_attr_db_t {
+            att_desc: esp_attr_desc_t {
+                uuid_length: 2,
+                uuid_p: std::ptr::addr_of_mut!(ATTRIBUTE_TABLE_PRIMARY_SERVICE) as *mut u8,
+                perm: ESP_GATT_PERM_READ as u16,
+                max_length: service.uuid.length(),
+                length: service.uuid.length(),
+                value: service.uuid.pointer_mut(),
+            },
+            attr_control: esp_attr_control_t {
+                auto_rsp: ESP_GATT_AUTO_RSP as u8,
+            },
+        });
+
+        for char in service.characteristics.iter_mut() {
+            // char declaration
+            result.push(esp_gatts_attr_db_t {
+                att_desc: esp_attr_desc_t {
+                    uuid_length: 2,
+                    uuid_p: std::ptr::addr_of_mut!(ATTRIBUTE_TABLE_CHARACTER_DECLARATION)
+                        as *mut u8,
+                    perm: ESP_GATT_PERM_READ as u16,
+                    max_length: 1,
+                    length: 1,
+                    value: std::ptr::addr_of_mut!(char.props),
+                },
+                attr_control: esp_attr_control_t {
+                    auto_rsp: ESP_GATT_AUTO_RSP as u8,
+                },
+            });
+
+            // char value
+            result.push(esp_gatts_attr_db_t {
+                att_desc: esp_attr_desc_t {
+                    uuid_length: char.uuid.length(),
+                    uuid_p: char.uuid.pointer_mut(),
+                    perm: (ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE) as u16,
+                    max_length: char.max_length,
+                    length: char.initial_value_length(),
+                    value: char.initial_value_pointer_mut(),
+                },
+                attr_control: esp_attr_control_t {
+                    auto_rsp: ESP_GATT_AUTO_RSP as u8,
+                },
+            });
+
+            for desc in char.descriptors.iter_mut() {
+                result.push(esp_gatts_attr_db_t {
+                    att_desc: esp_attr_desc_t {
+                        uuid_length: desc.uuid.length(),
+                        uuid_p: desc.uuid.pointer_mut(),
+                        perm: (ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE) as u16,
+                        max_length: desc.max_length,
+                        length: desc.initial_value_length(),
+                        value: desc.initial_value_pointer_mut(),
+                    },
+                    attr_control: esp_attr_control_t {
+                        auto_rsp: ESP_GATT_AUTO_RSP as u8,
+                    },
+                });
+            }
+        }
+    }
+
+    result
+}
blob - /dev/null
blob + bfaf48df91fcb68959ec7e165f67fe7e3262dd92 (mode 644)
--- /dev/null
+++ src/gatt/permissions.rs
@@ -0,0 +1,39 @@
+use esp_idf_sys::{ESP_GATT_PERM_READ, ESP_GATT_PERM_WRITE};
+
+pub enum Permissions {
+    None,
+    Read,
+    Write,
+    ReadWrite,
+}
+
+impl Default for Permissions {
+    fn default() -> Self {
+        Permissions::Read
+    }
+}
+
+impl Permissions {
+    pub fn get_permissions_u16(&self) -> u16 {
+        match self {
+            Permissions::None => 0,
+            Permissions::Read => ESP_GATT_PERM_READ as u16,
+            Permissions::Write => ESP_GATT_PERM_WRITE as u16,
+            Permissions::ReadWrite => (ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE) as u16,
+        }
+    }
+
+    pub fn can_read(&self) -> bool {
+        match self {
+            Permissions::Read | Permissions::ReadWrite => true,
+            _ => false,
+        }
+    }
+
+    pub fn can_write(&self) -> bool {
+        match self {
+            Permissions::Write | Permissions::ReadWrite => true,
+            _ => false,
+        }
+    }
+}
blob - /dev/null
blob + a47e5b3c649c50fa4ada1a376372ed8f50edbcf4 (mode 644)
--- /dev/null
+++ src/gatt/service.rs
@@ -0,0 +1,67 @@
+use esp_idf_sys::{
+    esp_ble_gatts_create_service, esp_ble_gatts_start_service, esp_gatt_id_t, esp_gatt_if_t,
+    esp_gatt_srvc_id_t, ESP_OK,
+};
+
+use super::char::Characteristic;
+use super::uuid::UUID;
+
+pub struct Service {
+    pub id: Option<esp_gatt_srvc_id_t>,
+    pub uuid: UUID,
+    pub handle: u16,
+    pub characteristics: Vec<Characteristic>,
+}
+
+impl Service {
+    pub const fn new(uuid: UUID, characteristics: Vec<Characteristic>) -> Self {
+        Self {
+            id: None,
+            uuid,
+            handle: 0,
+            characteristics,
+        }
+    }
+
+    pub unsafe fn create(&mut self, gatts_if: esp_gatt_if_t) {
+        debug_assert!(self.id.is_none(), "The service was already created!");
+
+        let mut service_id = esp_gatt_srvc_id_t {
+            is_primary: true,
+            id: esp_gatt_id_t {
+                inst_id: 0x00,
+                uuid: self.uuid.get(),
+            },
+        };
+
+        // Note: the service itself + all characteristics + all descriptors
+        let num_handles = 1
+            + self.characteristics.len()
+            + self
+                .characteristics
+                .iter()
+                .map(|char| char.descriptors.len())
+                .sum::<usize>();
+        debug_assert!(num_handles < u16::MAX.into());
+
+        if esp_ble_gatts_create_service(gatts_if, &mut service_id, num_handles as u16) != ESP_OK {
+            panic!("Failed to start service \"{}\"!", self.uuid);
+        }
+        self.id = Some(service_id);
+    }
+
+    pub unsafe fn start(&mut self) {
+        debug_assert!(self.handle != 0, "The service was not created yet!");
+        if esp_ble_gatts_start_service(self.handle) != ESP_OK {
+            panic!("Failed to start service \"{}\"", self.uuid);
+        }
+    }
+
+    pub fn add_characteristic(&mut self, char: Characteristic) {
+        debug_assert!(
+            self.handle == 0,
+            "Please add all characteristics before running .create()!"
+        );
+        self.characteristics.push(char);
+    }
+}
blob - /dev/null
blob + 3e0776d7df082d42829fc967df1f8e8c550007ac (mode 644)
--- /dev/null
+++ src/gatt/uuid.rs
@@ -0,0 +1,149 @@
+use esp_idf_sys::{
+    esp_bt_uuid_t, esp_bt_uuid_t__bindgen_ty_1, esp_gatt_srvc_id_t, ESP_GATT_UUID_CHAR_DECLARE,
+    ESP_GATT_UUID_PRI_SERVICE, ESP_UUID_LEN_128, ESP_UUID_LEN_16, ESP_UUID_LEN_32,
+};
+
+pub const CLIENT_CHARACTERISTIC_CONFIGURATION: UUID = UUID::Short(0x2902);
+
+pub static mut ATTRIBUTE_TABLE_PRIMARY_SERVICE: u16 = ESP_GATT_UUID_PRI_SERVICE as u16;
+pub static mut ATTRIBUTE_TABLE_CHARACTER_DECLARATION: u16 = ESP_GATT_UUID_CHAR_DECLARE as u16;
+
+pub enum UUID {
+    Short(u16),
+    Medium(u32),
+    Long([u8; 16]),
+}
+
+impl UUID {
+    pub fn get(&self) -> esp_bt_uuid_t {
+        match self {
+            UUID::Short(s) => uuid_from_u16(*s),
+            UUID::Medium(m) => uuid_from_u32(*m),
+            UUID::Long(l) => uuid_from_u8x16(l.clone()),
+        }
+    }
+
+    pub fn length(&self) -> u16 {
+        match self {
+            UUID::Short(_) => 2,
+            UUID::Medium(_) => 4,
+            UUID::Long(_) => 16,
+        }
+    }
+
+    pub fn pointer_mut(&mut self) -> *mut u8 {
+        match self {
+            UUID::Short(s) => std::ptr::addr_of_mut!(*s) as *mut u8,
+            UUID::Medium(m) => std::ptr::addr_of_mut!(*m) as *mut u8,
+            UUID::Long(l) => std::ptr::addr_of_mut!(*l) as *mut u8,
+        }
+    }
+
+    pub fn to_le(&self) -> Vec<u8> {
+        match self {
+            UUID::Short(s) => s.to_le_bytes().to_vec(),
+            UUID::Medium(m) => m.to_le_bytes().to_vec(),
+            UUID::Long(l) => {
+                let mut l = l.clone();
+                l.reverse();
+                l.to_vec()
+            }
+        }
+    }
+}
+
+impl PartialEq<esp_gatt_srvc_id_t> for UUID {
+    fn eq(&self, rhs: &esp_gatt_srvc_id_t) -> bool {
+        self.eq(&rhs.id.uuid)
+    }
+}
+
+impl PartialEq<esp_bt_uuid_t> for UUID {
+    fn eq(&self, rhs: &esp_bt_uuid_t) -> bool {
+        match self {
+            UUID::Short(lhs) => {
+                if rhs.len == ESP_UUID_LEN_16 as u16 {
+                    return unsafe { rhs.uuid.uuid16 == *lhs };
+                }
+            }
+            UUID::Medium(lhs) => {
+                if rhs.len == ESP_UUID_LEN_32 as u16 {
+                    return unsafe { rhs.uuid.uuid32 == *lhs };
+                }
+            }
+            UUID::Long(lhs) => {
+                if rhs.len == ESP_UUID_LEN_128 as u16 {
+                    return unsafe { rhs.uuid.uuid128 == *lhs };
+                }
+            }
+        }
+
+        false
+    }
+}
+
+impl PartialEq for UUID {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Self::Short(lhs), Self::Short(rhs)) => lhs == rhs,
+            (Self::Medium(lhs), Self::Medium(rhs)) => lhs == rhs,
+            (Self::Long(lhs), Self::Long(rhs)) => lhs == rhs,
+            _ => false,
+        }
+    }
+}
+
+impl std::fmt::Display for UUID {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            UUID::Short(s) => s.fmt(f),
+            UUID::Medium(m) => m.fmt(f),
+            UUID::Long(l) => {
+                for x in l {
+                    f.write_fmt(format_args!("{}", x))?;
+                }
+
+                Ok(())
+            }
+        }
+    }
+}
+
+impl From<u16> for UUID {
+    fn from(uuid: u16) -> Self {
+        UUID::Short(uuid)
+    }
+}
+
+impl From<u32> for UUID {
+    fn from(uuid: u32) -> Self {
+        UUID::Medium(uuid)
+    }
+}
+
+impl From<[u8; 16]> for UUID {
+    fn from(uuid: [u8; 16]) -> Self {
+        UUID::Long(uuid)
+    }
+}
+
+const fn uuid_from_u16(uuid: u16) -> esp_bt_uuid_t {
+    esp_bt_uuid_t {
+        len: ESP_UUID_LEN_16 as u16,
+        uuid: esp_bt_uuid_t__bindgen_ty_1 { uuid16: uuid },
+    }
+}
+
+const fn uuid_from_u32(uuid: u32) -> esp_bt_uuid_t {
+    esp_bt_uuid_t {
+        len: ESP_UUID_LEN_32 as u16,
+        uuid: esp_bt_uuid_t__bindgen_ty_1 { uuid32: uuid },
+    }
+}
+
+const fn uuid_from_u8x16(uuid: [u8; 16]) -> esp_bt_uuid_t {
+    esp_bt_uuid_t {
+        len: ESP_UUID_LEN_128 as u16,
+        uuid: esp_bt_uuid_t__bindgen_ty_1 { uuid128: uuid },
+    }
+}
blob - /dev/null
blob + 6e4795bf46c193e94ee533ef22a6e56d5b1d7e6a (mode 644)
--- /dev/null
+++ src/lib.rs
@@ -0,0 +1,286 @@
+#![allow(non_upper_case_globals)]
+#![allow(dead_code)]
+
+mod error;
+mod gap;
+pub mod gatt;
+
+pub use error::Error;
+use gap::ble_gap_event_handler;
+use gatt::*;
+
+type Result<T> = std::result::Result<T, Error>;
+
+use esp_idf_sys::*;
+use std::ffi::CString;
+
+pub static mut STATE: BleState = BleState::Disconnected;
+static mut UPDATE_RUNNING: Option<std::thread::JoinHandle<()>> = None;
+
+const MAIN_PROFILE_ID: u16 = 0x12;
+static mut MAIN_PROFILE_GATTS_IF: u8 = ESP_GATT_IF_NONE as u8;
+
+pub enum BleState {
+    Disconnected,
+    Connected(esp_bd_addr_t),
+    ConnectedRssi(esp_bd_addr_t, [i8; 8], usize),
+}
+
+impl BleState {
+    fn set_disconnected(&mut self) {
+        *self = Self::Disconnected;
+
+        start_advertising();
+    }
+
+    fn set_addr(&mut self, mut addr: esp_bd_addr_t) {
+        *self = Self::Connected(addr);
+
+        // NOTE: updating to 2M PHY when connecting to prevent `LL_PHY_UPDATE_IND` possibly being
+        // sent by the master before `LL_FEATURE_RSP` arrived. if this is not prevented, the master
+        // will *not* upgrade the connection to 2M because it is not yet aware that the slave
+        // supports this mode.
+        if let Err(e) = esp!(unsafe {
+            esp_ble_gap_set_prefered_phy(
+                addr.as_mut_ptr(),
+                0u8,
+                ESP_BLE_GAP_PHY_2M_PREF_MASK as u8,
+                ESP_BLE_GAP_PHY_2M_PREF_MASK as u8,
+                ESP_BLE_GAP_PHY_OPTIONS_NO_PREF as u16,
+            )
+        }) {
+            log::error!("Unable to set preferred PHY for connection: {}", e);
+        }
+
+        // TODO: tidy up
+        // set maximum LE data packet size to theoretical maximum
+        // BTM_BLE_DATA_SIZE_MAX = 0x00fb = 251
+        log::debug!("Setting BLE DLE to 251");
+        if let Err(e) = esp!(unsafe { esp_ble_gap_set_pkt_data_len(addr.as_mut_ptr(), 251) }) {
+            log::error!("Unable to set BLE DLE to 251: {}", e);
+        }
+
+        //let mut params: esp_ble_conn_update_params_t = esp_ble_conn_update_params_t {
+        //    bda: addr,
+        //    latency: 0,
+        //    min_int: 0x10,
+        //    max_int: 0x20,
+        //    timeout: 400,
+        //};
+        //if let Err(e) = esp!(unsafe { esp_ble_gap_update_conn_params(&mut params) }) {
+        //    log::error!("Unable to update connection parameters: {}", e);
+        //}
+    }
+
+    fn set_rssi(&mut self, rssi: i8) {
+        match self {
+            Self::Disconnected => panic!("Not connected, can not set rssi!"),
+            Self::Connected(addr) => *self = Self::ConnectedRssi(*addr, [rssi; 8], 0),
+            Self::ConnectedRssi(_addr, r, count) => {
+                *count = count.wrapping_add(1);
+                r[*count & 0b111] = rssi;
+            }
+        };
+    }
+
+    fn addr(&mut self) -> Option<*mut u8> {
+        match self {
+            Self::Disconnected => None,
+            Self::Connected(addr) | Self::ConnectedRssi(addr, _, _) => Some(addr.as_mut_ptr()),
+        }
+    }
+
+    fn rssi(&self) -> Option<i8> {
+        match self {
+            Self::ConnectedRssi(_, r, _) => {
+                Some((r.iter().map(|rssi| *rssi as i32).sum::<i32>() / 8) as i8)
+            }
+            _ => None,
+        }
+    }
+}
+
+pub struct Ble {
+    config: esp_bt_controller_config_t,
+    adv_data: esp_ble_adv_data_t,
+    name: CString,
+    was_initialized: bool,
+}
+
+impl Default for Ble {
+    fn default() -> Self {
+        let config = 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,
+        };
+
+        let adv_data = esp_ble_adv_data_t {
+            set_scan_rsp: false,
+            include_name: true,
+            include_txpower: true,
+            min_interval: 0x0006,
+            max_interval: 0x0010,
+            appearance: 0x00,
+            manufacturer_len: 0, //TEST_MANUFACTURER_DATA_LEN,
+            p_manufacturer_data: std::ptr::null_mut(), //&test_manufacturer[0],
+            service_data_len: 0,
+            p_service_data: std::ptr::null_mut(),
+            service_uuid_len: 0,
+            p_service_uuid: std::ptr::null_mut(),
+            // service_uuid_len: 2,
+            // p_service_uuid: unsafe {std::ptr::addr_of_mut!(SERVICE_A_UUID) as *mut u8},
+            flag: (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT) as u8,
+        };
+
+        Self {
+            config,
+            adv_data,
+            name: CString::new("").unwrap(),
+            was_initialized: false,
+        }
+    }
+}
+
+impl Ble {
+    pub fn new_with_name<S>(name: S) -> Self
+    where
+        S: AsRef<str>,
+    {
+        Self {
+            name: CString::new(name.as_ref()).unwrap(),
+            ..Default::default()
+        }
+    }
+
+    pub fn find_attribute_by_uuid(uuid: UUID) -> Option<Box<&'static dyn IsAttribute>> {
+        unsafe {
+            for service in SERVICES.iter() {
+                if service.uuid == uuid {
+                    return None;
+                }
+                for char in service.characteristics.iter() {
+                    if char.uuid == uuid {
+                        return Some(Box::new(char));
+                    }
+                    for desc in char.descriptors.iter() {
+                        if desc.uuid == uuid {
+                            return Some(Box::new(desc));
+                        }
+                    }
+                }
+            }
+        }
+
+        None
+    }
+
+    pub fn add_service(&mut self, service: Service) {
+        debug_assert!(
+            !self.was_initialized,
+            "You have to add all services before initializing!"
+        );
+
+        // TODO: UUIDs are not advertised
+        //let mut uuid = std::mem::ManuallyDrop::new(service.uuid.to_le());
+        //self.adv_data.service_uuid_len = uuid.len() as _;
+        //self.adv_data.p_service_uuid = uuid.as_mut_ptr();
+
+        unsafe {
+            SERVICES.push(service);
+        }
+    }
+
+    pub fn init(&mut self) -> std::result::Result<(), EspError> {
+        esp_try!(nvs_flash_init(), "Initializing nvs flash");
+
+        esp_try!(
+            esp_bt_controller_init(&mut self.config),
+            "Initializing BT controller"
+        );
+        esp_try!(
+            esp_bt_controller_enable(esp_bt_mode_t_ESP_BT_MODE_BLE),
+            "Enabling BT controller"
+        );
+        esp_try!(esp_bluedroid_init(), "Initializing Bluedroid");
+        esp_try!(esp_bluedroid_enable(), "Enabling Bluedroid");
+
+        esp_try!(
+            esp_ble_gap_register_callback(Some(ble_gap_event_handler)),
+            "Registering BLE GAP event handler"
+        );
+
+        esp_try!(
+            esp_ble_gap_set_device_name(self.name.as_ptr()),
+            "Setting device name"
+        );
+
+        esp_try!(
+            esp_ble_gap_config_adv_data(&mut self.adv_data),
+            "Setting BLE GAP advertising data"
+        );
+
+        esp_try!(
+            esp_ble_gatts_register_callback(Some(ble_gatt_event_handler)),
+            "Registering GATT callback"
+        );
+
+        esp_try!(
+            esp_ble_gatts_app_register(MAIN_PROFILE_ID),
+            "Registering GATT server application"
+        );
+
+        log::debug!(
+            "Setting MTU size to ESP_GATT_MAX_MTU_SIZE ({})",
+            ESP_GATT_MAX_MTU_SIZE
+        );
+        esp_try!(
+            esp_ble_gatt_set_local_mtu(ESP_GATT_MAX_MTU_SIZE as _),
+            "Setting max MTU size to ESP_GATT_MAX_MTU_SIZE"
+        );
+
+        Ok(())
+    }
+
+    pub fn rssi() -> Option<i8> {
+        unsafe { STATE.rssi() }
+    }
+}
+
+#[macro_export]
+macro_rules! esp_try {
+    ($cmd:expr, $str: tt) => {
+        // catch error, log, and rethrow
+        if let Err(e) = unsafe { esp!($cmd) } {
+            log::error!("Error in \"{}\": {}", $str, e);
+            Err(e)?;
+        }
+    };
+}