I posted a question on Discord about how to manage a serial connection in Iced. I got some pointers and was referred to the websocket
example in the repo. This somewhat helped me to understand how to set this up but I have a couple more general questions about how Subscription
s work that I wasn’t able to find an answer to. I’ll post them here because they may be relevant to other people as well and it’s easier to find here than on Discord.
- What exactly is a subscription? The docs for
Application::subscription
say
A
Subscription
will be kept alive as long as you keep returning it, and the messages produced will be handled byupdate
.
What does “keep returning it” mean here? That bring my to the following question:
-
When is
Application::subscription
called? It returns aSubscription<Self::Message>
so with the above note from the docs that leads to me to think this function is called repeatedly. But in thewebsocket
example theconnect
function starts it’s own loop to listen for messages from the websocket so that impliessubscription
is only called once (otherwise you’d have multiple loops running at the same time). -
In my case I want to start and stop a connection when the user clicks “Connect” and “Disconnect” buttons. Does that mean I need to start and stop a subscription? Ho would I do that?
subscription
takes a reference to the struct that implements theApplication
trait but how do I use that here? Or should the subscription keep running (whatever that means) and communicate via messages that the connection should be opened and closed? -
I’m using the mavlink crate to communicate with a UAV and this library is not async. I can wrap everything in an
async
block but code that potentially blocks the thread should by run on a separate thread withtokio::task::spawn_blocking
. It’s not clear to me which apart of the code should be ran like this and how this interacts with the Iced runtime. I included my code below.
main.rs
:
use iced::{
executor,
widget::{column, container, scrollable, text, Button, Column},
Alignment, Application, Color, Command, Element, Length, Renderer, Settings, Subscription,
Theme,
};
use mavlink::ardupilotmega::MavMessage;
#[derive(Default)]
struct App {
state: ConnectionState,
messages: Vec<MavMessage>,
}
#[derive(Debug, Default)]
enum ConnectionState {
#[default]
Disconnected,
Connected(connection::Connection),
}
impl ConnectionState {
fn is_connected(&self) -> bool {
match self {
ConnectionState::Disconnected => false,
ConnectionState::Connected(_) => true,
}
}
}
#[derive(Debug, Clone)]
enum Message {
Connect,
Disconnect,
ConnectionEvent(connection::Event),
}
impl Application for App {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
(Self::default(), Command::none())
}
fn title(&self) -> String {
"Iced Mavlink Connection Question".to_string()
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Connect => todo!("Open the connection"),
Message::Disconnect => todo!("Close the connection"),
Message::ConnectionEvent(event) => match event {
connection::Event::Connected(connection) => {
println!("Connection started");
self.state = ConnectionState::Connected(connection);
}
connection::Event::FailedToConnect => {
println!("Failed to create connection")
}
connection::Event::Disconnected => {
println!("Connection closed");
self.state = ConnectionState::Disconnected;
}
connection::Event::MavMessage(message) => {
self.messages.push(message);
}
},
}
Command::none()
}
fn view(&self) -> Element<Message, Renderer<Theme>> {
let connect = Button::new("Connect")
.on_press_maybe((!self.state.is_connected()).then(|| Message::Connect));
let disconnect = Button::new("Disconnect")
.on_press_maybe(self.state.is_connected().then(|| Message::Disconnect));
let message_log: Element<_> = if self.messages.is_empty() {
container(
text("Your messages will appear here...").style(Color::from_rgb8(0x88, 0x88, 0x88)),
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
} else {
scrollable(
Column::with_children(
self.messages
.iter()
.cloned()
.map(|m| text(format!("{:?}", m)))
.map(Element::from)
.collect(),
)
.width(Length::Fill)
.spacing(10),
)
// .id(MESSAGE_LOG.clone())
.height(Length::Fill)
.into()
};
container(
column![connect, disconnect, message_log]
.spacing(10)
.align_items(Alignment::Center),
)
.width(Length::Fill)
.height(Length::Fill)
.padding(20)
.into()
}
fn subscription(&self) -> Subscription<Self::Message> {
connection::connect().map(Message::ConnectionEvent)
}
}
fn main() -> iced::Result {
App::run(Settings::default())
}
mod connection {
use std::time::Duration;
use iced::{
futures::{channel::mpsc, SinkExt},
subscription, Subscription,
};
use mavlink::{ardupilotmega::MavMessage, MavConnection};
enum State {
Disconnected,
Connected {
mavlink_connection: Box<dyn MavConnection<MavMessage> + Sync + Send>,
input_receiver: mpsc::Receiver<InputMessage>,
},
}
#[derive(Debug, Clone)]
pub enum Event {
Connected(Connection),
FailedToConnect,
Disconnected,
MavMessage(MavMessage),
}
enum InputMessage {
GetParameter(String),
}
#[derive(Debug, Clone)]
pub struct Connection(mpsc::Sender<InputMessage>);
pub fn connect() -> Subscription<Event> {
struct Connect;
subscription::channel(
std::any::TypeId::of::<Connect>(),
100,
|mut output| async move {
let mut state = State::Disconnected;
loop {
match &mut state {
State::Disconnected => {
const ADDRESS: &str = "serial:/dev/ttyACM0:115200";
match mavlink::connect::<MavMessage>(ADDRESS) {
Ok(conn) => {
let (sender, receiver) = mpsc::channel(100);
let _ = output.send(Event::Connected(Connection(sender))).await;
state = State::Connected {
mavlink_connection: conn,
input_receiver: receiver,
};
}
Err(_) => {
tokio::time::sleep(Duration::from_secs(1)).await;
let _ = output.send(Event::FailedToConnect).await;
}
}
}
State::Connected {
mavlink_connection,
input_receiver: _,
} => match mavlink_connection.recv() {
Ok((_header, msg)) => {
let _ = output.send(Event::MavMessage(msg)).await;
}
Err(_) => {
let _ = output.send(Event::Disconnected).await;
}
},
}
}
},
)
}
}
Cargo.toml
:
[package]
name = "iced-connection-question"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.75"
mavlink = "0.12.0"
iced = { version = "0.10.0", features = ["tokio"] }
rand = "0.8.5"
tokio = { version = "1.32.0", features = ["time"] }