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. ui-disconnected

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.