Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/native.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ defmodule RustlerBtleplug.Native do
@spec unsubscribe(peripheral(), uuid(), number()) :: {:ok, peripheral()} | {:error, term()}
def unsubscribe(_peripheral, _characteristic, _timeout \\ @default_timeout), do: error()

## ✅ Read & Write Characteristics
@doc """
Write data to a characteristic.

Returns the peripheral reference. The operation is asynchronous.
"""
@spec write_characteristic(peripheral(), uuid(), binary(), number()) ::
{:ok, peripheral()} | {:error, term()}
def write_characteristic(_peripheral, _characteristic, _data, _timeout \\ @default_timeout),
do: error()

@doc """
Read data from a characteristic.

Returns the peripheral reference. The read data will be sent as a message:
`{:btleplug_characteristic_read, uuid, data}`
"""
@spec read_characteristic(peripheral(), uuid(), number()) ::
{:ok, peripheral()} | {:error, term()}
def read_characteristic(_peripheral, _characteristic, _timeout \\ @default_timeout),
do: error()

## ✅ Adapter State Queries (Graph & Mindmap)
@doc """
Retrieve the adapter state as a **GraphViz** or **Mermaid mindmap**.
Expand Down
1 change: 1 addition & 0 deletions native/btleplug_client/src/atoms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ rustler::atoms! {
btleplug_services_advertisement,

btleplug_characteristic_value_changed,
btleplug_characteristic_read,
}
237 changes: 237 additions & 0 deletions native/btleplug_client/src/peripheral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,240 @@ pub fn unsubscribe(

Ok(resource)
}

#[rustler::nif]
pub fn write_characteristic(
env: Env,
resource: ResourceArc<PeripheralRef>,
characteristic_uuid: String,
data: Vec<u8>,
timeout_ms: u64,
) -> Result<ResourceArc<PeripheralRef>, RustlerError> {
let peripheral_arc = resource.0.clone();
let env_pid = env.pid();

RUNTIME.spawn(async move {
let (peripheral, state, pid) = {
let state_guard = peripheral_arc.lock().unwrap();
(
state_guard.peripheral.clone(),
state_guard.state,
state_guard.pid,
)
};

info!(
"✍️ Writing to Peripheral: {:?}, characteristic: {}, caller pid: {:?}, state pid: {:?}",
peripheral.id(),
characteristic_uuid,
env_pid.as_c_arg(),
pid.as_c_arg()
);

if state != PeripheralStateEnum::ServicesDiscovered {
warn!("⚠️ Services not yet discovered. Manually triggering discovery...");
if let Err(e) = timeout(
Duration::from_millis(timeout_ms),
peripheral.discover_services(),
)
.await
{
warn!("❌ Service discovery failed: {:?}", e);
return;
}

RUNTIME.spawn({
let peripheral_arc_clone = peripheral_arc.clone();
async move {
if !discover_services_internal(&peripheral_arc_clone, timeout_ms).await {
warn!("⚠️ No services discovered, but proceeding with write.");
}
}
});
}

info!("🔍 Waiting 2s before checking characteristics...");
tokio::time::sleep(Duration::from_millis(2000)).await;

let characteristics = peripheral.characteristics();
let characteristic = characteristics
.iter()
.find(|c| c.uuid.to_string() == characteristic_uuid)
.cloned();

match characteristic {
Some(char) => {
debug!("✍️ Writing to characteristic: {:?}", char.uuid);
info!(
"✍️ Found characteristic: {:?}, Properties: {:?}, Data length: {}",
char.uuid,
char.properties,
data.len()
);

if !char.properties.contains(CharPropFlags::WRITE)
&& !char.properties.contains(CharPropFlags::WRITE_WITHOUT_RESPONSE)
{
warn!(
"⚠️ Characteristic {:?} does NOT support write operations!",
char.uuid
);
return;
}

let write_type = if char.properties.contains(CharPropFlags::WRITE) {
btleplug::api::WriteType::WithResponse
} else {
btleplug::api::WriteType::WithoutResponse
};

match timeout(
Duration::from_millis(timeout_ms),
peripheral.write(&char, &data, write_type),
)
.await
{
Ok(Ok(_)) => info!(
"✅ Successfully wrote {} bytes to characteristic: {:?}",
data.len(),
char.uuid
),
Ok(Err(e)) => {
warn!("❌ Failed to write to {:?}: {:?}", char.uuid, e);
}
Err(_) => {
warn!("❌ Write timeout for {:?}", char.uuid);
}
}
}
None => warn!(
"❌ Characteristic with UUID {} not found! Available UUIDs: {:?}",
characteristic_uuid,
characteristics
.iter()
.map(|c| c.uuid.to_string())
.collect::<Vec<_>>()
),
}
});

Ok(resource)
}

#[rustler::nif]
pub fn read_characteristic(
env: Env,
resource: ResourceArc<PeripheralRef>,
characteristic_uuid: String,
timeout_ms: u64,
) -> Result<ResourceArc<PeripheralRef>, RustlerError> {
let peripheral_arc = resource.0.clone();
let env_pid = env.pid();

RUNTIME.spawn(async move {
let (peripheral, state, pid) = {
let state_guard = peripheral_arc.lock().unwrap();
(
state_guard.peripheral.clone(),
state_guard.state,
state_guard.pid,
)
};

info!(
"📖 Reading from Peripheral: {:?}, characteristic: {}, caller pid: {:?}, state pid: {:?}",
peripheral.id(),
characteristic_uuid,
env_pid.as_c_arg(),
pid.as_c_arg()
);

if state != PeripheralStateEnum::ServicesDiscovered {
warn!("⚠️ Services not yet discovered. Manually triggering discovery...");
if let Err(e) = timeout(
Duration::from_millis(timeout_ms),
peripheral.discover_services(),
)
.await
{
warn!("❌ Service discovery failed: {:?}", e);
return;
}

RUNTIME.spawn({
let peripheral_arc_clone = peripheral_arc.clone();
async move {
if !discover_services_internal(&peripheral_arc_clone, timeout_ms).await {
warn!("⚠️ No services discovered, but proceeding with read.");
}
}
});
}

info!("🔍 Waiting 2s before checking characteristics...");
tokio::time::sleep(Duration::from_millis(2000)).await;

let characteristics = peripheral.characteristics();
let characteristic = characteristics
.iter()
.find(|c| c.uuid.to_string() == characteristic_uuid)
.cloned();

match characteristic {
Some(char) => {
debug!("📖 Reading from characteristic: {:?}", char.uuid);
info!(
"📖 Found characteristic: {:?}, Properties: {:?}",
char.uuid, char.properties
);

if !char.properties.contains(CharPropFlags::READ) {
warn!(
"⚠️ Characteristic {:?} does NOT support read operations!",
char.uuid
);
return;
}

match timeout(Duration::from_millis(timeout_ms), peripheral.read(&char)).await {
Ok(Ok(data)) => {
info!(
"✅ Successfully read {} bytes from characteristic: {:?}",
data.len(),
char.uuid
);

// Send the read data back to Elixir via a message
let mut owned_env = OwnedEnv::new();
owned_env.send_and_clear(&pid, |env| {
let data_term = data.encode(env);
let uuid_term = char.uuid.to_string().encode(env);
(
atoms::btleplug_characteristic_read().encode(env),
uuid_term,
data_term,
)
.encode(env)
});
}
Ok(Err(e)) => {
warn!("❌ Failed to read from {:?}: {:?}", char.uuid, e);
}
Err(_) => {
warn!("❌ Read timeout for {:?}", char.uuid);
}
}
}
None => warn!(
"❌ Characteristic with UUID {} not found! Available UUIDs: {:?}",
characteristic_uuid,
characteristics
.iter()
.map(|c| c.uuid.to_string())
.collect::<Vec<_>>()
),
}
});

Ok(resource)
}
Loading