use core::ffi::c_int;
use myrmic_common::types::ble::{
ConnectRequest, ConnectionInfo, DisconnectReason, NotificationInfo, ReadOutcome, ReadRequest,
ScanRequest, SubscribeRequest, WriteOutcome, WriteRequest,
};
use myrmic_common::types::error::{EACCES, EPERM};
use serde::{Deserialize, Serialize};
use crate::error::ErrorCode;
use crate::{
ApiError, ApiResult, Bytes, Callback, Codec, Command, Decoder, Postcard, Result, String,
};
pub use myrmic_common::types::ble::{
Address, Advertisement, Characteristic, DiscoveredDevice, DiscoveryFilter, ManufacturerData,
NotifyError, ReadError, ScanMode, Service, ServiceData, Uuid, WriteError,
};
pub use myrmic_common::{mac_addr_pub, mac_addr_rand, uuid128};
mod c_functions {
use core::ffi::c_int;
#[link(wasm_import_module = "ble")]
unsafe extern "C" {
pub(super) fn ble_scan(request: *const u8, request_len: c_int) -> c_int;
pub(super) fn ble_stop_scan() -> c_int;
pub(super) fn ble_connect(request: *const u8, request_len: c_int) -> c_int;
pub(super) fn ble_disconnect(id: c_int) -> c_int;
pub(super) fn ble_subscribe(request: *const u8, request_len: c_int) -> c_int;
pub(super) fn ble_unsubscribe(id: c_int) -> c_int;
pub(super) fn ble_read(request: *const u8, request_len: c_int) -> c_int;
pub(super) fn ble_write(request: *const u8, request_len: c_int) -> c_int;
pub(super) fn ble_set_pair_passkey(passkey: c_int) -> c_int;
}
}
#[must_use = "dropping the handle loses the ability to stop the scan - store it in cell state"]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanHandle;
#[must_use = "dropping the connection loses the ability to disconnect it - store it in cell state"]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Connection {
info: ConnectionInfo,
}
#[must_use = "dropping the handle loses the ability to unsubscribe - store it in cell state"]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Subscription {
id: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Notification {
info: NotificationInfo,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Disconnect {
reason: DisconnectReason,
}
pub fn scan(
callback: Callback<DiscoveredDevice>,
filter: Option<DiscoveryFilter>,
mode: ScanMode,
) -> ApiResult<ScanHandle> {
let request = ScanRequest {
callback: command_name(callback),
filter,
mode,
};
send_request(&request, c_functions::ble_scan)?;
Ok(ScanHandle)
}
#[must_use]
pub fn connect(address: Address) -> ConnectBuilder {
ConnectBuilder {
address,
on_connected: None,
on_disconnected: None,
}
}
pub fn set_pair_passkey(passkey: u32) -> ApiResult<()> {
unsafe { c_functions::ble_set_pair_passkey(passkey.cast_signed()) }.to_result()
}
pub struct ConnectBuilder {
address: Address,
on_connected: Option<String>,
on_disconnected: Option<String>,
}
impl ConnectBuilder {
#[must_use]
pub fn on_connected(mut self, callback: Callback<Connection>) -> Self {
self.on_connected = Some(command_name(callback));
self
}
#[must_use]
pub fn on_disconnected(mut self, callback: Callback<Disconnect>) -> Self {
self.on_disconnected = Some(command_name(callback));
self
}
pub fn initiate(self) -> ApiResult<()> {
let request = ConnectRequest {
address: self.address,
on_connected: self.on_connected.ok_or(ApiError::Usage)?,
on_disconnected: self.on_disconnected.ok_or(ApiError::Usage)?,
};
send_request(&request, c_functions::ble_connect)?;
Ok(())
}
}
impl ScanHandle {
pub fn stop(self) -> ApiResult<()> {
unsafe { c_functions::ble_stop_scan() }.to_result()
}
}
impl Connection {
pub fn address(&self) -> Address {
self.info.mac_address
}
pub fn characteristic(&self, service: Uuid, characteristic: Uuid) -> Option<Characteristic> {
self.info
.gatt_services
.get(&service)?
.characteristics
.get(&characteristic)
.copied()
}
pub fn subscribe(
&self,
characteristic: Characteristic,
callback: Callback<Notification>,
) -> ApiResult<core::result::Result<Subscription, NotifyError>> {
let request = SubscribeRequest {
connection_id: self.info.id,
characteristic,
callback: command_name(callback),
};
let bytes = serialize(&request)?;
let ret = unsafe { c_functions::ble_subscribe(bytes.as_ptr(), bytes.len() as c_int) };
match ret {
id if id >= 0 => Ok(Ok(Subscription { id: id as u32 })),
EPERM => Ok(Err(NotifyError::NotNotifiable)),
EACCES => Ok(Err(NotifyError::RequiresSecurity)),
other => Err(other.into()),
}
}
pub fn read(
&self,
characteristic: Characteristic,
callback: Callback<ReadOutcome>,
) -> ApiResult<()> {
let request = ReadRequest {
connection_id: self.info.id,
characteristic,
callback: command_name(callback),
};
send_request(&request, c_functions::ble_read)?;
Ok(())
}
pub fn write(
&self,
characteristic: Characteristic,
data: &[u8],
callback: Callback<WriteOutcome>,
) -> ApiResult<()> {
let request = WriteRequest {
connection_id: self.info.id,
characteristic,
data: data.to_vec(),
callback: Some(command_name(callback)),
};
send_request(&request, c_functions::ble_write)?;
Ok(())
}
pub fn write_no_response(&self, characteristic: Characteristic, data: &[u8]) -> ApiResult<()> {
let request = WriteRequest {
connection_id: self.info.id,
characteristic,
data: data.to_vec(),
callback: None,
};
send_request(&request, c_functions::ble_write)?;
Ok(())
}
pub fn disconnect(self) -> ApiResult<()> {
unsafe { c_functions::ble_disconnect(self.info.id as c_int) }.to_result()
}
}
impl Subscription {
pub fn unsubscribe(self) -> ApiResult<()> {
unsafe { c_functions::ble_unsubscribe(self.id as c_int) }.to_result()
}
}
impl Notification {
pub fn characteristic(&self) -> Characteristic {
self.info.characteristic
}
pub fn data(&self) -> &[u8] {
&self.info.data
}
}
impl Disconnect {
pub fn reason(&self) -> DisconnectReason {
self.reason
}
}
impl core::fmt::Display for Disconnect {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.reason.fmt(f)
}
}
impl Decoder for DiscoveredDevice {
fn from_bytes(bytes: Bytes) -> Result<Self> {
<Postcard as Codec>::decode(&bytes)
}
}
impl Decoder for Connection {
fn from_bytes(bytes: Bytes) -> Result<Self> {
Ok(Connection {
info: <Postcard as Codec>::decode(&bytes)?,
})
}
}
impl Decoder for Notification {
fn from_bytes(bytes: Bytes) -> Result<Self> {
Ok(Notification {
info: <Postcard as Codec>::decode(&bytes)?,
})
}
}
impl Decoder for Disconnect {
fn from_bytes(bytes: Bytes) -> Result<Self> {
Ok(Disconnect {
reason: <Postcard as Codec>::decode(&bytes)?,
})
}
}
impl Decoder for ReadOutcome {
fn from_bytes(bytes: Bytes) -> Result<Self> {
<Postcard as Codec>::decode(&bytes)
}
}
impl Decoder for WriteOutcome {
fn from_bytes(bytes: Bytes) -> Result<Self> {
<Postcard as Codec>::decode(&bytes)
}
}
fn serialize(request: &impl Serialize) -> ApiResult<Bytes> {
postcard::to_allocvec(request).map_err(|_| ApiError::Serde("unable to serialise ble request"))
}
fn send_request(
request: &impl Serialize,
func: unsafe extern "C" fn(*const u8, c_int) -> c_int,
) -> ApiResult<c_int> {
let bytes = serialize(request)?;
let ret = unsafe { func(bytes.as_ptr(), bytes.len() as c_int) };
if ret >= 0 { Ok(ret) } else { Err(ret.into()) }
}
fn command_name<T>(callback: Callback<T>) -> String {
let command: Command = callback.into();
command.as_ref().into()
}