#![warn(missing_docs)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]
mod error;
mod model;
pub mod permission;
mod session;
use std::cell::RefCell;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use std::str::FromStr;
use session::Session;
use uuid::Uuid;
thread_local! {
static LAST_ERROR: RefCell<CString> = RefCell::new(CString::default());
}
fn set_last_error(msg: impl Into<String>) {
let c = CString::new(msg.into()).unwrap_or_default();
LAST_ERROR.with(|e| *e.borrow_mut() = c);
}
fn clear_last_error() {
LAST_ERROR.with(|e| *e.borrow_mut() = CString::default());
}
#[no_mangle]
pub extern "C" fn bt_last_error() -> *const c_char {
LAST_ERROR.with(|e| e.borrow().as_ptr())
}
#[no_mangle]
pub extern "C" fn bt_permission_status() -> c_int {
permission::status()
}
#[no_mangle]
pub extern "C" fn bt_request_permission() -> c_int {
permission::request()
}
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_com_manymath_bluetooth_1flutter_BtleplugInitProvider_nativeInit(
env: jni::JNIEnv,
_class: jni::objects::JClass,
) {
clear_last_error();
if let Err(e) = btleplug::platform::init(&env) {
set_last_error(format!("btleplug Android init failed: {e}"));
}
}
#[no_mangle]
pub extern "C" fn bt_session_new() -> *mut Session {
clear_last_error();
match Session::new() {
Ok(s) => Box::into_raw(Box::new(s)),
Err(e) => {
set_last_error(e.to_string());
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn bt_session_free(session: *mut Session) {
if session.is_null() {
return;
}
drop(unsafe { Box::from_raw(session) });
}
fn with_session<'a>(session: *mut Session) -> Option<&'a Session> {
if session.is_null() {
set_last_error("null session");
None
} else {
Some(unsafe { &*session })
}
}
fn ok_or_record(result: Result<(), impl std::fmt::Display>) -> c_int {
match result {
Ok(()) => 0,
Err(e) => {
set_last_error(e.to_string());
-1
}
}
}
unsafe fn cstr<'a>(ptr: *const c_char, what: &str) -> Option<&'a str> {
if ptr.is_null() {
set_last_error(format!("null {what}"));
return None;
}
match CStr::from_ptr(ptr).to_str() {
Ok(s) => Some(s),
Err(_) => {
set_last_error(format!("{what} is not valid UTF-8"));
None
}
}
}
fn fill_buffer(bytes: &[u8], buf: *mut u8, cap: usize) -> isize {
if buf.is_null() {
set_last_error("null output buffer");
return -1;
}
if bytes.len() > cap {
set_last_error(format!(
"output buffer too small: need {}, have {cap}",
bytes.len()
));
return -1;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
}
bytes.len() as isize
}
#[no_mangle]
pub unsafe extern "C" fn bt_start_scan(session: *mut Session, filter_json: *const c_char) -> c_int {
let Some(session) = with_session(session) else {
return -1;
};
let filter_str = if filter_json.is_null() {
None
} else {
match CStr::from_ptr(filter_json).to_str() {
Ok(s) => Some(s),
Err(_) => {
set_last_error("filter_json is not valid UTF-8");
return -1;
}
}
};
let uuids = match parse_filter(filter_str) {
Ok(u) => u,
Err(msg) => {
set_last_error(msg);
return -1;
}
};
let slice = if uuids.is_empty() {
None
} else {
Some(uuids.as_slice())
};
ok_or_record(session.start_scan(slice))
}
fn parse_filter(filter_json: Option<&str>) -> Result<Vec<Uuid>, String> {
let Some(raw) = filter_json.map(str::trim).filter(|s| !s.is_empty() && *s != "null") else {
return Ok(Vec::new());
};
let uuids: Vec<String> =
serde_json::from_str(raw).map_err(|e| format!("invalid scan filter JSON: {e}"))?;
uuids
.iter()
.map(|u| Uuid::from_str(u).map_err(|e| format!("invalid service UUID {u}: {e}")))
.collect()
}
#[no_mangle]
pub extern "C" fn bt_stop_scan(session: *mut Session) -> c_int {
let Some(session) = with_session(session) else {
return -1;
};
ok_or_record(session.stop_scan())
}
#[no_mangle]
pub unsafe extern "C" fn bt_session_poll_event(
session: *mut Session,
buf: *mut u8,
cap: usize,
) -> isize {
let Some(session) = with_session(session) else {
return -1;
};
let Some(event) = session.poll_event() else {
return 0;
};
match serde_json::to_vec(&event) {
Ok(bytes) => fill_buffer(&bytes, buf, cap),
Err(e) => {
set_last_error(format!("failed to encode event: {e}"));
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn bt_connect(session: *mut Session, id: *const c_char) -> c_int {
let Some(session) = with_session(session) else {
return -1;
};
let Some(id) = cstr(id, "id") else { return -1 };
ok_or_record(session.connect(id))
}
#[no_mangle]
pub unsafe extern "C" fn bt_disconnect(session: *mut Session, id: *const c_char) -> c_int {
let Some(session) = with_session(session) else {
return -1;
};
let Some(id) = cstr(id, "id") else { return -1 };
ok_or_record(session.disconnect(id))
}
#[no_mangle]
pub unsafe extern "C" fn bt_discover_services(
session: *mut Session,
id: *const c_char,
buf: *mut u8,
cap: usize,
) -> isize {
let Some(session) = with_session(session) else {
return -1;
};
let Some(id) = cstr(id, "id") else { return -1 };
match session.discover_services(id) {
Ok(services) => match serde_json::to_vec(&services) {
Ok(json) => fill_buffer(&json, buf, cap),
Err(e) => {
set_last_error(format!("failed to serialize services: {e}"));
-1
}
},
Err(e) => {
set_last_error(e.to_string());
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn bt_read(
session: *mut Session,
id: *const c_char,
char_uuid: *const c_char,
buf: *mut u8,
cap: usize,
) -> isize {
let Some(session) = with_session(session) else {
return -1;
};
let Some(id) = cstr(id, "id") else { return -1 };
let Some(char_uuid) = cstr(char_uuid, "char_uuid") else {
return -1;
};
match session.read(id, char_uuid) {
Ok(value) => fill_buffer(&value, buf, cap),
Err(e) => {
set_last_error(e.to_string());
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn bt_write(
session: *mut Session,
id: *const c_char,
char_uuid: *const c_char,
data: *const u8,
len: usize,
with_response: c_int,
) -> c_int {
let Some(session) = with_session(session) else {
return -1;
};
let Some(id) = cstr(id, "id") else { return -1 };
let Some(char_uuid) = cstr(char_uuid, "char_uuid") else {
return -1;
};
if data.is_null() && len != 0 {
set_last_error("null data");
return -1;
}
let bytes = if len == 0 {
&[][..]
} else {
std::slice::from_raw_parts(data, len)
};
ok_or_record(session.write(id, char_uuid, bytes, with_response != 0))
}
#[no_mangle]
pub unsafe extern "C" fn bt_subscribe(
session: *mut Session,
id: *const c_char,
char_uuid: *const c_char,
) -> c_int {
let Some(session) = with_session(session) else {
return -1;
};
let Some(id) = cstr(id, "id") else { return -1 };
let Some(char_uuid) = cstr(char_uuid, "char_uuid") else {
return -1;
};
ok_or_record(session.subscribe(id, char_uuid))
}
#[no_mangle]
pub unsafe extern "C" fn bt_unsubscribe(
session: *mut Session,
id: *const c_char,
char_uuid: *const c_char,
) -> c_int {
let Some(session) = with_session(session) else {
return -1;
};
let Some(id) = cstr(id, "id") else { return -1 };
let Some(char_uuid) = cstr(char_uuid, "char_uuid") else {
return -1;
};
ok_or_record(session.unsubscribe(id, char_uuid))
}
pub use error::Error;
pub use model::{BleCharacteristic, BleDevice, BleEvent, BleService};
pub use session::Session as BleSession;
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn empty_filter_scans_everything() {
let f = parse_filter(None).unwrap();
assert!(f.is_empty());
let f = parse_filter(Some(" ")).unwrap();
assert!(f.is_empty());
let f = parse_filter(Some("null")).unwrap();
assert!(f.is_empty());
}
#[test]
fn filter_parses_uuid_array() {
let json = r#"["0000180d-0000-1000-8000-00805f9b34fb"]"#;
let f = parse_filter(Some(json)).unwrap();
assert_eq!(f.len(), 1);
}
#[test]
fn filter_parses_multiple_uuids() {
let json = r#"["0000180d-0000-1000-8000-00805f9b34fb", "0000180f-0000-1000-8000-00805f9b34fb"]"#;
let f = parse_filter(Some(json)).unwrap();
assert_eq!(f.len(), 2);
}
#[test]
fn filter_rejects_bad_uuid() {
assert!(parse_filter(Some(r#"["not-a-uuid"]"#)).is_err());
}
#[test]
fn filter_rejects_bad_json() {
assert!(parse_filter(Some("not json at all")).is_err());
}
#[test]
fn filter_empty_array_returns_empty() {
let f = parse_filter(Some("[]")).unwrap();
assert!(f.is_empty());
}
#[test]
fn fill_buffer_fits_exactly() {
let data = b"hello";
let mut buf = vec![0u8; 5];
let n = fill_buffer(data, buf.as_mut_ptr(), buf.len());
assert_eq!(n, 5);
assert_eq!(&buf, b"hello");
}
#[test]
fn fill_buffer_larger_than_data() {
let data = b"hi";
let mut buf = vec![0u8; 10];
let n = fill_buffer(data, buf.as_mut_ptr(), buf.len());
assert_eq!(n, 2);
assert_eq!(&buf[..2], b"hi");
}
#[test]
fn fill_buffer_too_small() {
let data = b"hello world";
let mut buf = vec![0u8; 3];
let n = fill_buffer(data, buf.as_mut_ptr(), buf.len());
assert_eq!(n, -1);
}
#[test]
fn fill_buffer_null_pointer() {
let data = b"hello";
let n = fill_buffer(data, std::ptr::null_mut(), 10);
assert_eq!(n, -1);
}
#[test]
fn fill_buffer_empty_data() {
let data = b"";
let mut buf = vec![0u8; 5];
let n = fill_buffer(data, buf.as_mut_ptr(), buf.len());
assert_eq!(n, 0);
}
#[test]
fn device_from_none_properties() {
let device = BleDevice::from_properties("dev1".into(), None, false);
assert_eq!(device.id, "dev1");
assert_eq!(device.address, "");
assert!(device.name.is_none());
assert!(device.rssi.is_none());
assert!(device.tx_power.is_none());
assert!(device.manufacturer_data.is_empty());
assert!(device.service_data.is_empty());
assert!(device.services.is_empty());
assert!(!device.connected);
}
#[test]
fn device_from_none_properties_connected() {
let device = BleDevice::from_properties("dev2".into(), None, true);
assert!(device.connected);
assert_eq!(device.id, "dev2");
}
#[test]
fn error_display_no_adapter() {
let e = Error::NoAdapter;
assert_eq!(e.to_string(), "no Bluetooth adapter found");
}
#[test]
fn error_display_timeout() {
let e = Error::Timeout("operation timed out".to_string());
assert_eq!(e.to_string(), "operation timed out");
}
#[test]
fn error_display_not_connected() {
let e = Error::NotConnected;
assert_eq!(e.to_string(), "peripheral is not connected");
}
#[test]
fn error_display_not_found() {
let e = Error::NotFound("characteristic abc".into());
assert_eq!(e.to_string(), "characteristic abc");
}
#[test]
fn error_display_btleplug() {
let e = Error::Btleplug("adapter gone".into());
assert_eq!(e.to_string(), "adapter gone");
}
#[test]
fn error_display_invalid_uuid() {
let e = Error::InvalidUuid("bad-value".into());
assert_eq!(e.to_string(), "invalid UUID: bad-value");
}
#[test]
fn error_display_internal() {
let e = Error::Internal("something broke".into());
assert_eq!(e.to_string(), "something broke");
}
#[test]
fn error_display_json() {
let bad: Result<serde_json::Value, _> = serde_json::from_str("{invalid");
let e = Error::Json(bad.unwrap_err());
let msg = e.to_string();
assert!(msg.starts_with("JSON error:"));
}
#[test]
fn error_implements_std_error() {
let e = Error::NoAdapter;
let _: &dyn std::error::Error = &e;
}
#[test]
fn ok_or_record_returns_zero_on_success() {
let result: Result<(), Error> = Ok(());
assert_eq!(ok_or_record(result), 0);
}
#[test]
fn ok_or_record_returns_neg_one_on_error() {
let result: Result<(), Error> = Err(Error::NoAdapter);
assert_eq!(ok_or_record(result), -1);
}
#[test]
fn connected_event_serializes_correctly() {
let event = BleEvent::Connected {
id: "abc".into(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"connected\""));
assert!(json.contains("\"id\":\"abc\""));
}
#[test]
fn disconnected_event_serializes_correctly() {
let event = BleEvent::Disconnected {
id: "xyz".into(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"disconnected\""));
}
#[test]
fn state_update_event_serializes_correctly() {
let event = BleEvent::StateUpdate {
state: "poweredon".into(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"state_update\""));
assert!(json.contains("\"state\":\"poweredon\""));
}
#[test]
fn device_with_manufacturer_data_round_trips() {
let mut md = HashMap::new();
md.insert("76".to_string(), vec![0x01, 0x02]);
let device = BleDevice {
id: "aa:bb".into(),
address: "aa:bb:cc:dd:ee:ff".into(),
name: None,
rssi: Some(-70),
tx_power: None,
manufacturer_data: md,
service_data: HashMap::new(),
services: vec![],
connected: false,
};
let event = BleEvent::Device { device };
let json = serde_json::to_string(&event).unwrap();
let back: BleEvent = serde_json::from_str(&json).unwrap();
match back {
BleEvent::Device { device } => {
assert_eq!(device.rssi, Some(-70));
assert!(device.name.is_none());
assert_eq!(
device.manufacturer_data.get("76"),
Some(&vec![0x01, 0x02])
);
}
_ => panic!("wrong variant"),
}
}
}