use crate::{bsp::RadioRuntimeResources, runtime};
use esp_radio::ble::{Config, controller::BleConnector};
use heapless::{String, Vec};
pub struct BluetoothResources {
pub bluetooth: esp_hal::peripherals::BT<'static>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BleState {
Stopped,
HciReady,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BleError {
ResourcesUnavailable,
Init,
NotStarted,
AdvertisementTooLong,
NameTooLong,
Hci,
NotificationTooLong,
InvalidNotification,
}
pub struct Ble {
state: BleState,
resources: Option<BluetoothResources>,
runtime: Option<RadioRuntimeResources>,
runtime_started: bool,
connector: Option<BleConnector<'static>>,
}
impl Ble {
#[must_use]
pub const fn new(resources: BluetoothResources, runtime: RadioRuntimeResources) -> Self {
Self {
state: BleState::Stopped,
resources: Some(resources),
runtime: Some(runtime),
runtime_started: false,
connector: None,
}
}
#[must_use]
pub const fn new_started(resources: BluetoothResources) -> Self {
Self {
state: BleState::Stopped,
resources: Some(resources),
runtime: None,
runtime_started: true,
connector: None,
}
}
pub fn start_hci(&mut self) -> Result<&mut BleConnector<'static>, BleError> {
self.start_hci_with_config(Config::default())
}
pub fn into_hci_connector(mut self) -> Result<BleConnector<'static>, BleError> {
if self.connector.is_none() {
let _connector = self.start_hci()?;
}
self.connector.take().ok_or(BleError::NotStarted)
}
pub fn start_hci_with_config(
&mut self,
config: Config,
) -> Result<&mut BleConnector<'static>, BleError> {
if self.connector.is_none() {
let resources = self
.resources
.take()
.ok_or(BleError::ResourcesUnavailable)?;
if !self.runtime_started {
let runtime = self.runtime.take().ok_or(BleError::ResourcesUnavailable)?;
runtime::start_radio_runtime(runtime);
self.runtime_started = true;
}
let connector =
BleConnector::new(resources.bluetooth, config).map_err(|_| BleError::Init)?;
self.connector = Some(connector);
self.state = BleState::HciReady;
}
self.connector.as_mut().ok_or(BleError::NotStarted)
}
#[must_use]
pub const fn state(&self) -> BleState {
self.state
}
pub fn read_hci(&mut self, buffer: &mut [u8]) -> Result<usize, BleError> {
self.start_hci()?.read(buffer).map_err(|_| BleError::Hci)
}
pub fn write_hci(&mut self, buffer: &[u8]) -> Result<usize, BleError> {
self.start_hci()?.write(buffer).map_err(|_| BleError::Hci)
}
pub async fn read_hci_async(&mut self, buffer: &mut [u8]) -> Result<usize, BleError> {
self.start_hci()?
.read_async(buffer)
.await
.map_err(|_| BleError::Hci)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeviceIdentity {
name: String<32>,
}
impl DeviceIdentity {
pub fn new(name: &str) -> Result<Self, BleError> {
let mut stored = String::new();
stored.push_str(name).map_err(|_| BleError::NameTooLong)?;
Ok(Self { name: stored })
}
#[must_use]
pub fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Advertisement<const N: usize> {
bytes: Vec<u8, N>,
}
impl<const N: usize> Advertisement<N> {
#[must_use]
pub const fn new() -> Self {
Self { bytes: Vec::new() }
}
pub fn push_flags(&mut self) -> Result<(), BleError> {
self.push_field(0x01, &[0x06])
}
pub fn push_complete_name(&mut self, name: &str) -> Result<(), BleError> {
self.push_field(0x09, name.as_bytes())
}
pub fn push_service_uuid16(&mut self, uuid: u16) -> Result<(), BleError> {
self.push_field(0x03, &uuid.to_le_bytes())
}
pub fn push_service_data16(&mut self, uuid: u16, data: &[u8]) -> Result<(), BleError> {
let mut payload: Vec<u8, N> = Vec::new();
payload
.extend_from_slice(&uuid.to_le_bytes())
.map_err(|_| BleError::AdvertisementTooLong)?;
payload
.extend_from_slice(data)
.map_err(|_| BleError::AdvertisementTooLong)?;
self.push_field(0x16, payload.as_slice())
}
pub fn push_manufacturer_data(
&mut self,
company_identifier: u16,
data: &[u8],
) -> Result<(), BleError> {
let mut payload: Vec<u8, N> = Vec::new();
payload
.extend_from_slice(&company_identifier.to_le_bytes())
.map_err(|_| BleError::AdvertisementTooLong)?;
payload
.extend_from_slice(data)
.map_err(|_| BleError::AdvertisementTooLong)?;
self.push_field(0xFF, payload.as_slice())
}
#[must_use]
pub fn as_slice(&self) -> &[u8] {
self.bytes.as_slice()
}
fn push_field(&mut self, field_type: u8, payload: &[u8]) -> Result<(), BleError> {
let length = payload
.len()
.checked_add(1)
.and_then(|value| u8::try_from(value).ok())
.ok_or(BleError::AdvertisementTooLong)?;
self.bytes
.push(length)
.map_err(|_| BleError::AdvertisementTooLong)?;
self.bytes
.push(field_type)
.map_err(|_| BleError::AdvertisementTooLong)?;
self.bytes
.extend_from_slice(payload)
.map_err(|_| BleError::AdvertisementTooLong)
}
}
impl<const N: usize> Default for Advertisement<N> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BeaconFrame<const N: usize> {
advertisement: Advertisement<N>,
scan_response: Advertisement<N>,
duration_ms: u32,
}
impl<const N: usize> BeaconFrame<N> {
#[must_use]
pub const fn new(
advertisement: Advertisement<N>,
scan_response: Advertisement<N>,
duration_ms: u32,
) -> Self {
Self {
advertisement,
scan_response,
duration_ms,
}
}
#[must_use]
pub fn advertisement(&self) -> &[u8] {
self.advertisement.as_slice()
}
#[must_use]
pub fn scan_response(&self) -> &[u8] {
self.scan_response.as_slice()
}
#[must_use]
pub const fn duration_ms(&self) -> u32 {
self.duration_ms
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BeaconSchedule<const FRAMES: usize, const N: usize> {
frames: Vec<BeaconFrame<N>, FRAMES>,
cursor: usize,
}
impl<const FRAMES: usize, const N: usize> BeaconSchedule<FRAMES, N> {
#[must_use]
pub const fn new() -> Self {
Self {
frames: Vec::new(),
cursor: 0,
}
}
pub fn push(&mut self, frame: BeaconFrame<N>) -> Result<(), BleError> {
self.frames
.push(frame)
.map_err(|_| BleError::AdvertisementTooLong)
}
#[must_use]
pub fn len(&self) -> usize {
self.frames.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
#[must_use]
pub fn current(&self) -> Option<&BeaconFrame<N>> {
self.frames.get(self.cursor)
}
pub fn advance(&mut self) -> Option<&BeaconFrame<N>> {
if self.frames.is_empty() {
return None;
}
self.cursor = (self.cursor + 1) % self.frames.len();
self.current()
}
pub fn reset(&mut self) {
self.cursor = 0;
}
}
impl<const FRAMES: usize, const N: usize> Default for BeaconSchedule<FRAMES, N> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NotificationPriority {
Normal,
High,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MirroredNotification {
app: String<32>,
title: String<48>,
body: String<96>,
priority: NotificationPriority,
}
impl MirroredNotification {
pub fn new(
app: &str,
title: &str,
body: &str,
priority: NotificationPriority,
) -> Result<Self, BleError> {
let mut stored_app = String::new();
let mut stored_title = String::new();
let mut stored_body = String::new();
stored_app
.push_str(app)
.map_err(|_| BleError::NotificationTooLong)?;
stored_title
.push_str(title)
.map_err(|_| BleError::NotificationTooLong)?;
stored_body
.push_str(body)
.map_err(|_| BleError::NotificationTooLong)?;
Ok(Self {
app: stored_app,
title: stored_title,
body: stored_body,
priority,
})
}
pub fn from_wire(bytes: &[u8]) -> Result<Self, BleError> {
let bytes = trim_trailing_nul(bytes);
let text = core::str::from_utf8(bytes).map_err(|_| BleError::InvalidNotification)?;
let mut parts = text.splitn(3, '|');
let app = parts.next().ok_or(BleError::InvalidNotification)?;
let title = parts.next().ok_or(BleError::InvalidNotification)?;
let body = parts.next().ok_or(BleError::InvalidNotification)?;
if app.is_empty() || title.is_empty() {
return Err(BleError::InvalidNotification);
}
Self::new(app, title, body, NotificationPriority::Normal)
}
pub fn write_wire(&self, output: &mut [u8]) -> Result<usize, BleError> {
let fields = [
self.app.as_bytes(),
self.title.as_bytes(),
self.body.as_bytes(),
];
let required_len = fields
.iter()
.map(|field| field.len())
.sum::<usize>()
.saturating_add(2);
if output.len() < required_len {
return Err(BleError::NotificationTooLong);
}
let mut cursor = 0;
for (index, field) in fields.iter().enumerate() {
output[cursor..cursor + field.len()].copy_from_slice(field);
cursor += field.len();
if index < fields.len() - 1 {
output[cursor] = b'|';
cursor += 1;
}
}
Ok(cursor)
}
#[must_use]
pub fn app(&self) -> &str {
self.app.as_str()
}
#[must_use]
pub fn title(&self) -> &str {
self.title.as_str()
}
#[must_use]
pub fn body(&self) -> &str {
self.body.as_str()
}
#[must_use]
pub const fn priority(&self) -> NotificationPriority {
self.priority
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NotificationMirror<const N: usize> {
notifications: Vec<MirroredNotification, N>,
}
impl<const N: usize> NotificationMirror<N> {
#[must_use]
pub const fn new() -> Self {
Self {
notifications: Vec::new(),
}
}
pub fn push(&mut self, notification: MirroredNotification) -> Result<(), BleError> {
if self.notifications.is_full() {
let _dropped = self.notifications.remove(0);
}
self.notifications
.push(notification)
.map_err(|_| BleError::NotificationTooLong)
}
pub fn push_wire(&mut self, bytes: &[u8]) -> Result<&MirroredNotification, BleError> {
let notification = MirroredNotification::from_wire(bytes)?;
self.push(notification)?;
self.latest().ok_or(BleError::InvalidNotification)
}
#[must_use]
pub fn latest(&self) -> Option<&MirroredNotification> {
self.notifications.last()
}
pub fn iter(&self) -> impl Iterator<Item = &MirroredNotification> {
self.notifications.iter()
}
#[must_use]
pub const fn capacity(&self) -> usize {
N
}
#[must_use]
pub fn len(&self) -> usize {
self.notifications.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.notifications.is_empty()
}
pub fn clear(&mut self) {
self.notifications.clear();
}
}
impl<const N: usize> Default for NotificationMirror<N> {
fn default() -> Self {
Self::new()
}
}
fn trim_trailing_nul(bytes: &[u8]) -> &[u8] {
let end = bytes
.iter()
.rposition(|byte| *byte != 0)
.map_or(0, |index| index + 1);
&bytes[..end]
}
pub mod service {
pub const NESSO_SERVICE_UUID16: u16 = 0xF0A0;
pub const COMMAND_CHARACTERISTIC_UUID16: u16 = 0xF0A1;
pub const NOTIFY_CHARACTERISTIC_UUID16: u16 = 0xF0A2;
pub const MIRROR_CHARACTERISTIC_UUID16: u16 = 0xF0A3;
}