Zustandsgesteuerte Subscriptions in iced 0.13
In diesem Beitrag erkunden wir, wie wir Subscriptions in iced 0.13 nutzen können, um von statischen Beispielen zu dynamischen, zustandsbehafteten Hintergrundaufgaben überzugehen und eine Serial-Port-GUI für den Arduino Nano zu bauen. 
iced 0.13 macht die Verwaltung von Hintergrundarbeit in GUI-Anwendungen mit seinem zustandsgesteuerten Subscription-System vorhersehbarer. Dies ist besonders nützlich bei der Arbeit mit IO-Aufgaben, die basierend auf dem App-Zustand gestartet und gestoppt werden müssen. Dieser Beitrag zeigt, wie man eine Controller-GUI für einen Arduino Nano über die serielle Schnittstelle baut, mit einer Subscription, die nur läuft, wenn eine Verbindung besteht — und sauber herunterfährt, wenn die Verbindung getrennt wird.
Wir behandeln:
-
Einrichten des Verbindungszustands
-
Bedingte Erstellung einer Serial-Event-Subscription
-
Erstellen eines benutzerdefinierten Recipe für serielle I/O
-
Verknüpfung von allem in update() und subscription()
Das Ziel
Wir wollen eine GUI, die:
-
Verfügbare serielle Geräte scannt und sich mit einem verbindet (Benutzerentscheidung)
-
Nur serielle Daten liest, wenn verbunden
-
Hintergrundaufgaben stoppt, wenn getrennt
-
Serielle Events als Messages in die UI streamt
# Schritt 1: Anwendungszustand modellieren
Definiere den Verbindungszustand klar, damit die Subscription-Logik ihn nutzen kann.
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum ConnectionState {
Disconnected,
Connected,
}
Der GUI-Zustand speichert dies in SerialCommanderGui:
nano_connection_state: ConnectionState,
/// Wrapper zum Arduino Nano über einen seriellen Port
nano: Arc<Nano>,# Schritt 2: Bedingte Subscription-Funktion hinzufügen
In iced 0.13 wird erwartet, dass Application::subscription die Subscriptions für den aktuellen Zustand zurückgibt. Diese Funktion wird nach Zustandsänderungen neu ausgewertet, sodass wir Subscriptions bedingt starten oder stoppen können:
fn subscription(state: &SerialCommanderGui) -> iced::Subscription<Message> {
use iced::advanced::subscription::from_recipe;
let nano = Arc::clone(&state.nano);
match state.nano_connection_state {
ConnectionState::Disconnected => {
// nichts im Hintergrund zu tun
Subscription::none()
}
ConnectionState::Connected => {
Subscription::batch([
if state.is_playing {
// falls wir Musik abspielen, senden wir `Message::StartWithTime` alle 250ms
time::every(Duration::from_millis(250)).map(|_| Message::StartWithTime)
} else {
// falls wir nicht abspielen, senden wir `Message::ScheduledPing` ab und zu
time::every(PING_RENEVAL_INTERVAL).map(|_| Message::ScheduledPing)
},
// in jedem Fall sammeln wir `SerialEvents` und wandeln sie in Messages für die UI um
from_recipe(SerialSubscription { nano }).map(Message::SerialEvent),
])
}
}
}
Hier ist SerialSubscription ein benutzerdefiniertes Recipe, das den seriellen Input-Stream erstellt.
## Schritt 3: Das SerialSubscription Recipe bauen
Definiere ein Recipe, das serielle Events an die UI streamt. Dies verwendet einen Hintergrund-Thread, um den seriellen Port abzufragen und Nachrichten weiterzuleiten.
```rust
pub struct SerialSubscription {
pub nano: Arc<Nano>,
}
impl Recipe for SerialSubscription {
type Output = SerialEvent;
fn stream(
self: Box<Self>,
_input: EventStream,
) -> iced::futures::stream::BoxStream<'static, Self::Output> {
let nano = Arc::clone(&self.nano);
subscribe_to_serial(nano).boxed()
}
fn hash(&self, state: &mut iced::advanced::subscription::Hasher) {
state.write(b"SerialSubscription");
}
}
Die eigentliche Leseschleife läuft in einem Thread und sendet Nachrichten über iced::stream::channel als Stream von SerialEvent.
pub fn subscribe_to_serial(nano: Arc<Nano>) -> impl Stream<Item = SerialEvent> {
stream::channel(1, move |mut output| async move {
std::thread::spawn(move || {
let mut retries_left = 5;
loop {
match nano.read_response() {
Ok(events) if !events.is_empty() => {
retries_left = 5;
for event in events {
let _ = executor::block_on(output.send(event.clone()));
if matches!(event, SerialEvent::Disconnected(_)) {
return;
}
}
let _ = executor::block_on(output.flush());
}
Ok(_) => {
std::thread::sleep(std::time::Duration::from_millis(500));
}
Err(err) => {
if matches!(err, SerialError::PortNotOpen) {
// implementiert einen Retry bei Fehler, 5 Mal, dann return
retries_left -= 1;
if retries_left == 0 {
let _ = executor::block_on(
output.send(SerialEvent::Disconnected(nano.device.clone())),
);
return;
}
}
return;
}
}
}
});
})
}
## Schritt 4: Serielle Events in `update()` behandeln
Serielle Events werden in Anwendungsnachrichten umgewandelt und an die Update-Funktion weitergeleitet. Hier ein einfaches Beispiel:
```SerialSubscription
Message::SerialEvent(SerialEvent::DataReceived(data)) => {
// fügt einfach das 2. String-Argument zum State hinzu, das in einer Textbox gerendert wird
add_to_log(state, format!("Data: {}", data));
}
Fehler und Zustandsübergänge werden hier ebenfalls behandelt, zum Beispiel:
Message::SerialEvent(SerialEvent::Disconnected(device)) => {
state.nano_connection_state = ConnectionState::Disconnected;
add_to_log(state, format!("Disconnected from {}", device));
}# Schritt 5: iced den Lifecycle verwalten lassen
Der wichtige Teil: Du verwaltest keine Threads oder Join-Handles direkt. Sobald sich der App-Zustand ändert, bricht iced die alte Subscription ab und startet eine neue — falls nötig. Dies vermeidet Nebenläufigkeitsprobleme und stellt sicher, dass die serielle Aufgabe eng an den UI-Zustand gebunden ist.
# Zusammenfassung
Mit iced 0.13 und Subscription sowie Recipe haben wir eine reaktive serielle GUI gebaut, die:
-
I/O basierend auf dem Verbindungszustand startet und stoppt
-
Serielle Nachrichten an die UI streamt
-
Fehler und Verbindungsabbrüche elegant behandelt
-
Saubere async-Grenzen ohne Race Conditions verwendet
Dieses Modell lässt sich leicht für zusätzliche Hintergrundaufgaben oder mehrere serielle Ports erweitern.