Zambieslar
Zambieslar

Reputation: 35

Adding timeouts to receive Bluetooth devices in the GTK4 event loop

I'm running into an issue while writing my own Bluetooth client using Rust and GTK4.

Here's the idea. I have a main event loop that I want to add a timeout to poll for a new Bluetooth device every 5 seconds because GTK4 is only a synchronous library. I wrote an async function using bluer to enumerate Bluetooth devices. This program works fine in it's own program with an async main function. Taking that same function and moving it into a larger project with a basic GTK UI – for some reason – won't push any devices to the message queue.

The message queue in this context being a Arc<Mutex<Vec<String>>>. Just a standard Mutex being shared between threads. This also occurs with sync_channels and normal channels.

The main thread try's to lock the Mutex, read it, then drop the lock using drop(Mutex). The event on the main loop runs once every 5 seconds. The rest of the time our async function locks the mutex and attempts to push a device name to the message queue so that it can be read by the main thread.

Here's the code I have so far.

pub fn build_ui(app: Application) -> Application {
    app.connect_activate(move |app| {
        let mut queue = Arc::new(Mutex::new(Vec::<String>::new()));
        let mlayout = Grid::builder()
            .height_request(700)
            .width_request(200)
            .build();
        let text_buf = TextBuffer::new(None);
        let text_view = TextView::builder()
            .buffer(&text_buf)
            .width_request(200)
            .height_request(600)
            .vexpand(true)
            .build();
        let mwindow = ApplicationWindow::builder()
            .application(app)
            .resizable(false)
            .width_request(200)
            .height_request(700)
            .child(&mlayout)
            .build();
        let dev_list = Frame::builder()
            .height_request(700)
            .width_request(200)
            .vexpand(true)
            .child(&text_view)
            .build();
        let title = Label::builder()
            .label("Device List")
            .css_name("Title")
            .width_request(200)
            .height_request(50)
            .justify(gtk4::Justification::Center)
            .build();
        mlayout.attach(&title, 0, 0, 200, 50);
        mlayout.attach(&dev_list, 0, 50, 200, 1);
        mwindow.init_layer_shell();
        mwindow.set_anchor(Edge::Right, true);
        mwindow.set_anchor(Edge::Top, true);
        mwindow.present();
        let txqueue = Arc::clone(&queue);
        thread::spawn(move || async {
            blue_init(txqueue).await;
        });
        gtk4::glib::timeout_add_local(Duration::from_secs(5), move || {
            match queue.clone().try_lock() {
                Ok(dev) => {
                    println!("{:#?}", dev);
                    drop(dev);
                    ControlFlow::Continue
                }
                _ => ControlFlow::Continue,
            }
        });
    });

    app
}

pub async fn blue_init(queue: Arc<Mutex<Vec<String>>>) {
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();
    runtime.spawn(async move {
        let session = Session::new().await.unwrap();
        let adapter = session.default_adapter().await.unwrap();
        let discover = adapter.discover_devices().await.unwrap();
        pin_mut!(discover);
        while let Some(evt) = discover.next().await {
            match evt {
                AdapterEvent::DeviceAdded(addr) => {
                    let device = adapter.device(addr).unwrap();
                    let name = device.alias().await.unwrap();
                    match queue.try_lock() {
                        Ok(mut data) => {
                            data.push(name);
                            drop(data);
                        }
                        _ => {}
                    }
                }
                _ => {}
            }
        }
    });

Currently, the async task doesn't seem to be running at all when initialized from the main thread when using the GTK libraries. Seems like the blue_init() function is called, but when the async task either fails in general or never runs. Even adding a break point to the async task doesn't break at the break point. Any help with this would be very much appreciated.

Upvotes: 0

Views: 65

Answers (1)

Jmb
Jmb

Reputation: 23424

thread::spawn(move || async {
    blue_init(txqueue).await;
});

This does nothing: it starts a new thread that initializes a future but never polls it and returns immediately. So your blue_init function is never called (try adding a println in it to see for yourself).

Instead you should remove the async from the declaration of blue_init:

pub fn blue_init (queue: Arc<Mutex<Vec<String>>>) {
    todo!();
}

and call it directly in the thread:

thread::spawn (move || blue_init (txqueue));

Moreover quoting from the shudown section of the tokio::runtime::Runtime docs:

Tasks spawned through Runtime::spawn keep running until they yield. Then they are dropped. They are not guaranteed to run to completion, but might do so if they do not yield until completion.

In your case, the runtime is dropped immediately after spawning the task, which causes the task to be canceled as soon as it hits an await. The solution is to use block_on instead of spawn:

pub fn blue_init (queue: Arc<Mutex<Vec<String>>>) {
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();
    runtime.block_on (async move { todo!(); });
}

Upvotes: 0

Related Questions