Hello,
I am a very beginner with rust and I want to use it to achieve a simple temperature reading on my BLE thermometer.
I use the rumble crate for that.
Here is my full code, derived from rumble example:
extern crate rumble;
use std::str;
use std::process;
use std::thread;
use std::time::Duration;
use rumble::bluez::manager::Manager;
use rumble::api::{UUID, ValueNotification, Central, Peripheral};
const TARGET_DEVICE_NAME: &str = "MJ_HT_V1";
const TARGET_CHARACTERISTIC_UUID: UUID =
UUID::B128([0x6d, 0x66, 0x70, 0x44, 0x73, 0x66, 0x62, 0x75, 0x66, 0x45, 0x76, 0x64, 0x55, 0xaa, 0x6c, 0x22]);
fn main() {
let manager = Manager::new().unwrap();
// get the first adapter
let adapters = manager.adapters().unwrap();
let mut adapter = adapters.into_iter().nth(0).unwrap();
// reset the adapter -- clears out any errant state
adapter = manager.down(&adapter).unwrap();
adapter = manager.up(&adapter).unwrap();
// connect to adapter
let central = adapter.connect().unwrap();
// start scanning for devices
central.start_scan().unwrap();
// instead of waiting, you can use central.on_event to be notified of new devices
thread::sleep(Duration::from_secs(1));
let device = central.peripherals().into_iter()
.find(|p| p.properties().local_name.iter()
.any(|name| name.contains(TARGET_DEVICE_NAME)));
let temperature_sensor;
match device {
Some(device_found) => {
temperature_sensor = device_found;
println!("Device found");
},
None => {
println!("Device not found");
process::exit(1);
}
};
// connect to device
temperature_sensor.connect().unwrap();
// discover characteristics
temperature_sensor.discover_characteristics().unwrap();
// get characs
let characs = temperature_sensor.characteristics();
// get temperature charac
let temperature_char = characs.iter().find(|c| c.uuid == TARGET_CHARACTERISTIC_UUID).unwrap();
temperature_sensor.on_notification(Box::new(|n: ValueNotification| {
println!(String::from_utf8(n.value).unwrap());
}));
temperature_sensor.subscribe(temperature_char);
thread::sleep(Duration::from_secs(10));
temperature_sensor.disconnect();
}
I want to exit the program as soon as I have been notified of a new value from the closure I passed to the on_notification event of the temperature_sensor. However, I just don't know how to get the information outside of the closure because this only a Fn closure and mutate outside variables is hence not allowed.
What is the right way to do that ?
Thanks to both of you but I am still lost a bit.
AtomicBool seems to be what I was looking for but I have the same problem than at the beginning:
let charac_read = AtomicBool::new(false);
temperature_sensor.on_notification(Box::new(|n: ValueNotification| {
println!("{}", String::from_utf8(n.value).unwrap());
charac_read.store(true, Ordering::Relaxed);
}));
temperature_sensor.subscribe(temperature_char);
while charac_read.load(Ordering::Relaxed) == false
{
}
This won't compile because borrowed value does not live long enough.
I tried that as well :
let charac_read = Cell::new(AtomicBool::new(false));
temperature_sensor.on_notification(Box::new(|n: ValueNotification| {
println!("{}", String::from_utf8(n.value).unwrap());
charac_read.get().store(true, Ordering::Relaxed);
}));
temperature_sensor.subscribe(temperature_char);
while charac_read.get().load(Ordering::Relaxed) == false
{
}
But It won't compile since no method named get found for type std::cell::Cell<std::sync::atomic::AtomicBool> in the current scope
= note: the method get exists but the following trait bounds were not satisfied: std::sync::atomic::AtomicBool : std::marker::Copy
Sorry I only read the Cell part, I was thinking about just making the atomic like you did but since you box it maybe you can't use a reference, what about Arc<AtomicBool>?
Nope i hadn't noticed it, so sorry. It compiles right now. Thanks you very much.
I will need a lot of time to understand what you made me do.
I have mostly developed in C/C++/C# and for now, I miss a basic theoric understanding of all borrowing and ownership concepts.
Why does the above code or the one with Arc work, or in other words, why did the code without both static and arc not work?
Because what the closure was capturing ("implicitly using") was a shared reference (e.g., &'a AtomicBool) to charac_read. However, once you spawn a thread (let's call it the "spawnee"), it may outlive the thread that spawned it!! (let's call it the spawner). Since the spawnee can outlive the spawner, it is thus able to witness the spawner returning from its function (in your example, main). So, if the spawnee was allowed to borrow locals from the spawner, the spawned closure could be referring to freed data!
That's why (the closure describing the behavior of) the spawnee cannot borrow locals (if you try to do it, you will get errors saying that the borrowed value does not live long enough, since it must not have a local lifetime 'a but the infinite lifetime called 'static).
in ::rumble's case, on_notification takes a Box<dyn Fn(ValueNotification) + Send + 'static>, so you can "see" the 'static lifetime requirement at the API level (actually the lifetime annotation is ellided, but when that happens with a trait object type definition, 'static is the lifetime "picked by Rust").
How to thus avoid borrowing a local variable
you may sometimes be able to use a static variable (when its initial value can be computed at compile time, such as with the literal false);
else you must wrap your variable in a Arc (Atomically Reference-Counted smart pointer, the equivalent of C++'s shared_ptr) to get multiple ownership (only when all the owners have been dropped does the value get deallocated, thus preventing the aforementioned use-after-free problem):
to create another owner, you just .clone() the smart pointer,
to give/send/move the newly created owner to the other thread, the name of the new owner must appear in the body of the closure (as usual) and the closure must be declared with the move keyword before its arguments (the part between pipes),
another overlooked option is to "promote a local to static" by leaking it (requires that the data be stored in the heap, e.g., boxed):
That is a choice of the API, I guess due to FFI constraints that require a specific type; although it is possible to "do FFI and generics", it is far more tedious / cumbersome for the library author, and more importantly, far more code is generated (one concrete function for each monomorphisation over the closure type), which, in the case of embedded, may be definitely noticeable.
The other path, taken here, is to abstract over all the closure types with the dyn Fn(...) -> ...trait object. The only way to technically unify stuff of potentially different size into a single type is through a layer of indirection (and a vtable to dynamically dispatch method calls), hence you will never see the type dyn Fn directly used but behind a (fat) pointer:
Box<dyn Fn(...)> for an owning pointer,
&dyn Fn() for a borrowedshared pointer,
&mut dyn FnMut() for a borrowedunique pointer
Yep, the initial heap allocation, and then the actual reference counting : an atomic counter increment for every clone, and an atomic counter decrement for every drop (and the final heap deallocation). In practice it can be quite negligeable, specially compared to heap allocation or thread spawning.
Thus in your example I'd definitely use the static variable declaration route.