diff --git a/Cargo.toml b/Cargo.toml index fe783798..28404313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ png = "0.17" rand = { version = "0.8", optional = true} serde_json = "1.0" serde_repr = "0.1" +serde_yaml = "0.9" serde = { version = "1.0", features = ["derive"] } sha-1 = "0.10" surf = { version = "2.3", default-features = false, features = ["h1-client-no-tls"] } diff --git a/demo_files/etc/rauc/certificates-enabled/stable.cert.pem b/demo_files/etc/rauc/certificates-enabled/stable.cert.pem new file mode 100644 index 00000000..e69de29b diff --git a/demo_files/srv/tacd/state.json b/demo_files/srv/tacd/state.json index c493cd65..4443c558 100644 --- a/demo_files/srv/tacd/state.json +++ b/demo_files/srv/tacd/state.json @@ -1,6 +1,7 @@ { "format_version": 1, "persistent_topics": { + "/v1/tac/display/show_help": false, "/v1/tac/setup_mode": false } } \ No newline at end of file diff --git a/demo_files/usr/share/tacd/update_channels/01_stable.yaml b/demo_files/usr/share/tacd/update_channels/01_stable.yaml new file mode 100644 index 00000000..a8a5b9a2 --- /dev/null +++ b/demo_files/usr/share/tacd/update_channels/01_stable.yaml @@ -0,0 +1,8 @@ +name: stable +display_name: Stable +description: | + Official updates bundles provided by the Linux Automation GmbH. + The released bundles are manually tested and signed before publication. +url: | + https://downloads.linux-automation.com/lxatac/software/stable/latest/lxatac-core-bundle-base-lxatac.raucb +polling_interval: "4d" diff --git a/demo_files/usr/share/tacd/update_channels/05_testing.yaml b/demo_files/usr/share/tacd/update_channels/05_testing.yaml new file mode 100644 index 00000000..a4ebe378 --- /dev/null +++ b/demo_files/usr/share/tacd/update_channels/05_testing.yaml @@ -0,0 +1,9 @@ +name: testing +display_name: Testing +description: | + Testing bundles provided by the Linux Automation GmbH. + The released bundles are automatically tested on actual hardware and automatically uploaded. + Receives more frequent but less stable updates than the Stable channel. +url: | + https://downloads.linux-automation.com/lxatac/software/testing/latest/lxatac-core-bundle-base-lxatac.raucb +polling_interval: "24h" diff --git a/openapi.yaml b/openapi.yaml index cdf8dd4b..6c512352 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -30,6 +30,17 @@ paths: '400': description: The value could not be parsed into a screen name + /v1/tac/display/alerts: + get: + summary: A list of currently pending alerts shown on the local UI + tags: [User Interface] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Alerts' + /v1/tac/display/buttons: put: summary: Simulate a button press/release on the device @@ -78,6 +89,30 @@ paths: '400': description: The value could not be parsed into a boolean + /v1/tac/display/show_help: + get: + summary: Display a help menu on the local screen + tags: [User Interface] + responses: + '200': + content: + application/json: + schema: + type: boolean + put: + summary: Display a help menu on the local screen + tags: [User Interface] + requestBody: + content: + application/json: + schema: + type: boolean + responses: + '204': + description: Help will be shown or hidden as requested + '400': + description: The request could not be parsed as boolean + /v1/tac/led/{led}/pattern: parameters: - name: led @@ -712,6 +747,43 @@ paths: '400': description: The value could not be parsed as string + /v1/tac/update/channels: + get: + summary: Get a list of update channels and available updates + tags: [Updating] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateChannels' + + /v1/tac/update/channels/reload: + put: + summary: Request an update of the channels list and update availability + tags: [Updating] + requestBody: + content: + application/json: + schema: + type: boolean + responses: + '204': + description: An update was requested (if true was sent) + '400': + description: The value could not be parsed as boolean + + /v1/tac/update/should_reboot: + get: + summary: Should the system be rebooted as there is a new bundle in the other slot? + tags: [Updating] + responses: + '200': + content: + application/json: + schema: + type: boolean + /v1/tac/network/hostname: get: summary: Get the systems hostname @@ -772,38 +844,40 @@ components: - System - IoBus - Uart - - ScreenSaver - - Breakout - - RebootConfirm - - Rauc + + Alerts: + type: array + items: + type: string + enum: + - ScreenSaver + - Locator + - RebootConfirm + - UpdateAvailable + - UpdateInstallation + - Help + - Setup ButtonEvent: type: object properties: - Press: - type: object - properties: - btn: - type: string - enum: - - Upper - - Lower - Release: - type: object - properties: - btn: - type: string - enum: - - Upper - - Lower - dur: - type: string - enum: - - Short - - Long - oneOf: - - required: [Press] - - required: [Release] + type: object + properties: + dir: + type: string + enum: + - Press + - Release + btn: + type: string + enum: + - Upper + - Lower + dur: + type: string + enum: + - Short + - Long BlinkPattern: type: object @@ -933,6 +1007,38 @@ components: nesting_depth: type: number + UpdateChannels: + type: array + items: + type: object + properties: + name: + type: string + display_name: + type: string + description: + type: string + url: + type: string + polling_interval: + type: object + properties: + secs: + type: integer + nanos: + type: integer + enabled: + type: boolean + bundle: + type: object + properties: + compatible: + type: string + version: + type: string, + newer_than_installed: + type: boolean + ServiceStatus: type: object properties: diff --git a/src/broker/topic.rs b/src/broker/topic.rs index 50ccdf05..ef32e98a 100644 --- a/src/broker/topic.rs +++ b/src/broker/topic.rs @@ -17,6 +17,7 @@ use std::collections::VecDeque; use std::marker::PhantomData; +use std::ops::Not; use std::sync::{Arc, Mutex, Weak}; use async_std::channel::{unbounded, Receiver, Sender, TrySendError}; @@ -324,6 +325,30 @@ impl Topic { } } +impl Topic { + /// Set a new value for the topic and notify subscribers _if the value changed_ + /// + /// # Arguments + /// + /// * `msg` - Value to set the topic to + pub fn set_if_changed(&self, msg: E) { + let msg = Some(msg); + + self.modify(|prev| if prev != msg { msg } else { None }); + } +} + +impl> Topic { + /// Toggle the value of a topic + /// + /// # Arguments + /// + /// * `default` - The value to assume if none was set yet + pub fn toggle(&self, default: E) { + self.modify(|prev| Some(!prev.unwrap_or(default))); + } +} + pub trait AnyTopic: Sync + Send { fn path(&self) -> &TopicName; fn web_readable(&self) -> bool; diff --git a/src/dbus/rauc/mod.rs b/src/dbus/rauc/mod.rs index 42d7a910..4f215d79 100644 --- a/src/dbus/rauc/mod.rs +++ b/src/dbus/rauc/mod.rs @@ -15,26 +15,67 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +use std::cmp::Ordering; use std::collections::HashMap; +use std::time::{Duration, Instant}; +use async_std::channel::Receiver; +use async_std::stream::StreamExt; use async_std::sync::Arc; +use async_std::task::{sleep, spawn, JoinHandle}; +use log::warn; use serde::{Deserialize, Serialize}; -#[cfg(not(feature = "demo_mode"))] -use async_std::prelude::*; - -#[cfg(not(feature = "demo_mode"))] -use async_std::task::spawn; - use super::Connection; use crate::broker::{BrokerBuilder, Topic}; +mod update_channels; +pub use update_channels::Channel; + #[cfg(feature = "demo_mode")] mod demo_mode; #[cfg(not(feature = "demo_mode"))] mod installer; +#[cfg(not(feature = "demo_mode"))] +use installer::InstallerProxy; + +#[cfg(feature = "demo_mode")] +mod imports { + pub struct InstallerProxy<'a> { + _dummy: &'a (), + } + + impl<'a> InstallerProxy<'a> { + pub async fn new(_conn: C) -> Option> { + Some(Self { _dummy: &() }) + } + + pub async fn info(&self, _url: &str) -> anyhow::Result<(String, String)> { + let compatible = "LXA TAC".to_string(); + let version = "4.0-0-20230428214619".to_string(); + + Ok((compatible, version)) + } + } + + pub const CHANNELS_DIR: &str = "demo_files/usr/share/tacd/update_channels"; +} + +#[cfg(not(feature = "demo_mode"))] +mod imports { + pub use anyhow::{anyhow, bail, Result}; + pub use futures::{select, FutureExt}; + pub use log::{error, info}; + + pub const CHANNELS_DIR: &str = "/usr/share/tacd/update_channels"; +} + +const RELOAD_RATE_LIMIT: Duration = Duration::from_secs(10 * 60); + +use imports::*; + #[derive(Serialize, Deserialize, Clone)] pub struct Progress { pub percentage: i32, @@ -60,6 +101,163 @@ pub struct Rauc { pub slot_status: Arc>>, pub last_error: Arc>, pub install: Arc>, + pub channels: Arc>>, + pub reload: Arc>, + pub should_reboot: Arc>, +} + +fn compare_versions(v1: &str, v2: &str) -> Option { + // Version strings look something like this: "4.0-0-20230428214619" + // Use string sorting on the date part to determine which bundle is newer. + let date_1 = v1.rsplit_once('-').map(|(_, d)| d); + let date_2 = v2.rsplit_once('-').map(|(_, d)| d); + + // Return Sone if either version could not be split or a Some with the + // ordering between the dates. + date_1.zip(date_2).map(|(d1, d2)| d1.cmp(d2)) +} + +#[cfg(not(feature = "demo_mode"))] +fn booted_older_than_other(slot_status: &SlotStatus) -> Result { + let rootfs_0 = slot_status.get("rootfs_0"); + let rootfs_1 = slot_status.get("rootfs_1"); + + let rootfs_0_booted = rootfs_0.and_then(|r| r.get("state")).map(|s| s == "booted"); + let rootfs_1_booted = rootfs_1.and_then(|r| r.get("state")).map(|s| s == "booted"); + + let (booted, other) = match (rootfs_0_booted, rootfs_1_booted) { + (Some(true), Some(true)) => { + bail!("Two booted RAUC slots at the same time"); + } + (Some(true), _) => (rootfs_0, rootfs_1), + (_, Some(true)) => (rootfs_1, rootfs_0), + _ => { + bail!("No booted RAUC slot"); + } + }; + + // Not having version information for the booted slot is an error. + let booted_version = booted + .and_then(|r| r.get("bundle_version")) + .ok_or(anyhow!("No bundle version information for booted slot"))?; + + // Not having version information for the other slot just means that + // it is not newer. + if let Some(other_version) = other.and_then(|r| r.get("bundle_version")) { + if let Some(rel) = compare_versions(other_version, booted_version) { + Ok(rel.is_gt()) + } else { + Err(anyhow!( + "Failed to compare date for bundle versions \"{}\" and \"{}\"", + other_version, + booted_version + )) + } + } else { + Ok(false) + } +} + +async fn channel_polling_task( + conn: Arc, + channels: Arc>>, + slot_status: Arc>>, + name: String, +) { + let proxy = InstallerProxy::new(&conn).await.unwrap(); + + while let Some(mut channel) = channels + .try_get() + .and_then(|chs| chs.into_iter().find(|ch| ch.name == name)) + { + let polling_interval = channel.polling_interval; + let slot_status = slot_status.try_get(); + + if let Err(e) = channel.poll(&proxy, slot_status.as_deref()).await { + warn!( + "Failed to fetch update for update channel \"{}\": {}", + channel.name, e + ); + } + + channels.modify(|chs| { + let mut chs = chs?; + let channel_prev = chs.iter_mut().find(|ch| ch.name == name)?; + + // Check if the bundle we polled is the same as before and we don't need + // to send a message to the subscribers. + if *channel_prev == channel { + return None; + } + + // Update the channel description with the newly polled bundle info + *channel_prev = channel; + + Some(chs) + }); + + match polling_interval { + Some(pi) => sleep(pi).await, + None => break, + } + } +} + +async fn channel_list_update_task( + conn: Arc, + mut reload_stream: Receiver, + channels: Arc>>, + slot_status: Arc>>, +) { + let mut previous: Option = None; + let mut polling_tasks: Vec> = Vec::new(); + + while let Some(reload) = reload_stream.next().await { + if !reload { + continue; + } + + // Polling for updates is a somewhat expensive operation. + // Make sure it can not be abused to DOS the tacd. + if previous + .map(|p| p.elapsed() < RELOAD_RATE_LIMIT) + .unwrap_or(false) + { + continue; + } + + // Read the list of available update channels + let new_channels = match Channel::from_directory(CHANNELS_DIR) { + Ok(chs) => chs, + Err(e) => { + warn!("Failed to get list of update channels: {e}"); + continue; + } + }; + + // Stop the currently running polling tasks + for task in polling_tasks.drain(..) { + task.cancel().await; + } + + let names: Vec = new_channels.iter().map(|c| c.name.clone()).collect(); + + channels.set(new_channels); + + // Spawn new polling tasks. They will poll once immediately. + for name in names.into_iter() { + let polling_task = spawn(channel_polling_task( + conn.clone(), + channels.clone(), + slot_status.clone(), + name, + )); + + polling_tasks.push(polling_task); + } + + previous = Some(Instant::now()); + } } impl Rauc { @@ -70,6 +268,9 @@ impl Rauc { slot_status: bb.topic_ro("/v1/tac/update/slots", None), last_error: bb.topic_ro("/v1/tac/update/last_error", None), install: bb.topic_wo("/v1/tac/update/install", Some("".to_string())), + channels: bb.topic_ro("/v1/tac/update/channels", None), + reload: bb.topic_wo("/v1/tac/update/channels/reload", Some(true)), + should_reboot: bb.topic_ro("/v1/tac/update/should_reboot", Some(false)), } } @@ -81,6 +282,15 @@ impl Rauc { inst.slot_status.set(Arc::new(demo_mode::slot_status())); inst.last_error.set("".to_string()); + // Reload the channel list on request + let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); + spawn(channel_list_update_task( + Arc::new(Connection), + reload_stream, + inst.channels.clone(), + inst.slot_status.clone(), + )); + inst } @@ -91,9 +301,11 @@ impl Rauc { let conn_task = conn.clone(); let operation = inst.operation.clone(); let slot_status = inst.slot_status.clone(); + let channels = inst.channels.clone(); + let should_reboot = inst.should_reboot.clone(); spawn(async move { - let proxy = installer::InstallerProxy::new(&conn_task).await.unwrap(); + let proxy = InstallerProxy::new(&conn_task).await.unwrap(); let mut stream = proxy.receive_operation_changed().await; @@ -143,6 +355,30 @@ impl Rauc { }) .collect(); + // Update the `newer_than_installed` field for the upstream bundles inside + // of the update channels. + channels.modify(|prev| { + let prev = prev?; + + let mut new = prev.clone(); + + for ch in new.iter_mut() { + if let Some(bundle) = ch.bundle.as_mut() { + bundle.update_install(&slots); + } + } + + // Only send out messages if anything changed + (new != prev).then_some(new) + }); + + // Provide a simple yes/no "should reboot into other slot?" information + // based on the bundle versions in the booted slot and the other slot. + match booted_older_than_other(&slots) { + Ok(b) => should_reboot.set_if_changed(b), + Err(e) => warn!("Could not determine if TAC should be rebooted: {e}"), + } + // In the RAUC API the slot status is a list of (name, info) tuples. // It is once again easier in typescript to represent it as a dict with // the names as keys, so that is what's exposed here. @@ -165,7 +401,7 @@ impl Rauc { // Forward the "progress" property to the broker framework spawn(async move { - let proxy = installer::InstallerProxy::new(&conn_task).await.unwrap(); + let proxy = InstallerProxy::new(&conn_task).await.unwrap(); let mut stream = proxy.receive_progress_changed().await; @@ -185,7 +421,7 @@ impl Rauc { // Forward the "last_error" property to the broker framework spawn(async move { - let proxy = installer::InstallerProxy::new(&conn_task).await.unwrap(); + let proxy = InstallerProxy::new(&conn_task).await.unwrap(); let mut stream = proxy.receive_last_error_changed().await; @@ -205,18 +441,28 @@ impl Rauc { // Forward the "install" topic from the broker framework to RAUC spawn(async move { - let proxy = installer::InstallerProxy::new(&conn_task).await.unwrap(); + let proxy = InstallerProxy::new(&conn_task).await.unwrap(); while let Some(url) = install_stream.next().await { // Poor-mans validation. It feels wrong to let someone point to any // file on the TAC from the web interface. if url.starts_with("http://") || url.starts_with("https://") { - // TODO: some kind of error handling - let _ = proxy.install(&url).await; + if let Err(e) = proxy.install(&url).await { + error!("Failed to install bundle: {}", e); + } } } }); + // Reload the channel list on request + let (reload_stream, _) = inst.reload.clone().subscribe_unbounded(); + spawn(channel_list_update_task( + conn.clone(), + reload_stream, + inst.channels.clone(), + inst.slot_status.clone(), + )); + inst } } diff --git a/src/dbus/rauc/update_channels.rs b/src/dbus/rauc/update_channels.rs new file mode 100644 index 00000000..cfcaceac --- /dev/null +++ b/src/dbus/rauc/update_channels.rs @@ -0,0 +1,209 @@ +// This file is part of tacd, the LXA TAC system daemon +// Copyright (C) 2023 Pengutronix e.K. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use std::fs::{read_dir, read_to_string, DirEntry}; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::time::Duration; + +use anyhow::{anyhow, bail, Result}; +use serde::{Deserialize, Serialize}; + +use super::{compare_versions, InstallerProxy, SlotStatus}; + +#[cfg(feature = "demo_mode")] +const ENABLE_DIR: &str = "demo_files/etc/rauc/certificates-enabled"; + +#[cfg(not(feature = "demo_mode"))] +const ENABLE_DIR: &str = "/etc/rauc/certificates-enabled"; + +const ONE_MINUTE: Duration = Duration::from_secs(60); +const ONE_HOUR: Duration = Duration::from_secs(60 * 60); +const ONE_DAY: Duration = Duration::from_secs(24 * 60 * 60); + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct UpstreamBundle { + pub compatible: String, + pub version: String, + pub newer_than_installed: bool, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct Channel { + pub name: String, + pub display_name: String, + pub description: String, + pub url: String, + pub polling_interval: Option, + pub enabled: bool, + pub bundle: Option, +} + +#[derive(Deserialize)] +pub struct ChannelFile { + pub name: String, + pub display_name: String, + pub description: String, + pub url: String, + pub polling_interval: Option, +} + +impl Channel { + fn from_file(path: &Path) -> Result { + let file_name = || { + path.file_name() + .and_then(|f| f.to_str()) + .unwrap_or("") + }; + + let mut channel_file: ChannelFile = { + let content = read_to_string(path)?; + serde_yaml::from_str(&content)? + }; + + let polling_interval = match channel_file.polling_interval.take() { + Some(mut pi) => { + let multiplier = match pi.pop() { + Some('m') => ONE_MINUTE, + Some('h') => ONE_HOUR, + Some('d') => ONE_DAY, + _ => { + bail!( + "The polling_interval in \"{}\" does not have one of m, h or d as suffix", + file_name() + ); + } + }; + + let value: u32 = pi.parse().map_err(|e| { + anyhow!( + "Failed to parse polling_interval in \"{}\": {}", + file_name(), + e + ) + })?; + + (value != 0).then_some(multiplier * value) + } + None => None, + }; + + let mut ch = Self { + name: channel_file.name, + display_name: channel_file.display_name, + description: channel_file.description, + url: channel_file.url.trim().to_string(), + polling_interval, + enabled: false, + bundle: None, + }; + + ch.update_enabled(); + + Ok(ch) + } + + pub(super) fn from_directory(dir: &str) -> Result> { + // Find all .yaml files in CHANNELS_DIR + let mut dir_entries: Vec = read_dir(dir)? + .filter_map(|dir_entry| dir_entry.ok()) + .filter(|dir_entry| { + dir_entry + .file_name() + .as_os_str() + .as_bytes() + .ends_with(b".yaml") + }) + .collect(); + + // And sort them alphabetically, so that 01_stable.yaml takes precedence over + // 05_testing.yaml. + dir_entries.sort_by_key(|dir_entry| dir_entry.file_name()); + + let mut channels: Vec = Vec::new(); + + for dir_entry in dir_entries { + let channel = Self::from_file(&dir_entry.path())?; + + if channels.iter().any(|ch| ch.name == channel.name) { + bail!("Encountered duplicate channel name \"{}\"", channel.name); + } + + channels.push(channel); + } + + Ok(channels) + } + + fn update_enabled(&mut self) { + // Which channels are enabled is decided based on which RAUC certificates are enabled. + let cert_file = self.name.clone() + ".cert.pem"; + let cert_path = Path::new(ENABLE_DIR).join(cert_file); + + self.enabled = cert_path.exists(); + } + + /// Ask RAUC to determine the version of the bundle on the server + pub(super) async fn poll( + &mut self, + proxy: &InstallerProxy<'_>, + slot_status: Option<&SlotStatus>, + ) -> Result<()> { + self.update_enabled(); + + self.bundle = None; + + if self.enabled { + let (compatible, version) = proxy.info(&self.url).await?; + self.bundle = Some(UpstreamBundle::new(compatible, version, slot_status)); + } + + Ok(()) + } +} + +impl UpstreamBundle { + fn new(compatible: String, version: String, slot_status: Option<&SlotStatus>) -> Self { + let mut ub = Self { + compatible, + version, + newer_than_installed: false, + }; + + if let Some(slot_status) = slot_status { + ub.update_install(slot_status); + } + + ub + } + + pub(super) fn update_install(&mut self, slot_status: &SlotStatus) { + let slot_0_is_older = slot_status + .get("rootfs_0") + .and_then(|r| r.get("bundle_version")) + .and_then(|v| compare_versions(&self.version, v).map(|c| c.is_gt())) + .unwrap_or(true); + + let slot_1_is_older = slot_status + .get("rootfs_1") + .and_then(|r| r.get("bundle_version")) + .and_then(|v| compare_versions(&self.version, v).map(|c| c.is_gt())) + .unwrap_or(true); + + self.newer_than_installed = slot_0_is_older && slot_1_is_older; + } +} diff --git a/src/dut_power.rs b/src/dut_power.rs index 09d8f15d..e4b95752 100644 --- a/src/dut_power.rs +++ b/src/dut_power.rs @@ -496,13 +496,8 @@ impl DutPwrThread { loop { task::sleep(TASK_INTERVAL).await; - let curr_state = Some(state.load(Ordering::Relaxed).into()); - - // Only send publish events if the state changed - state_topic_task.modify(|prev_state| match prev_state != curr_state { - true => curr_state, - false => None, - }); + let curr_state = state.load(Ordering::Relaxed).into(); + state_topic_task.set_if_changed(curr_state); } }); diff --git a/src/iobus.rs b/src/iobus.rs index 6ffe2d4d..1dc562da 100644 --- a/src/iobus.rs +++ b/src/iobus.rs @@ -115,30 +115,14 @@ impl IoBus { .recv_json::() .await { - server_info_task.modify(|prev| { - let need_update = prev.map(|p| p != si).unwrap_or(true); - - if need_update { - Some(si) - } else { - None - } - }); + server_info_task.set_if_changed(si); } if let Ok(nodes) = http::get("http://127.0.0.1:8080/nodes/") .recv_json::() .await { - nodes_task.modify(|prev| { - let need_update = prev.map(|n| n != nodes).unwrap_or(true); - - if need_update { - Some(nodes) - } else { - None - } - }); + nodes_task.set_if_changed(nodes); } sleep(Duration::from_secs(1)).await; diff --git a/src/main.rs b/src/main.rs index d7835970..e63d1e23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,7 +47,7 @@ use regulators::Regulators; use setup_mode::SetupMode; use system::System; use temperatures::Temperatures; -use ui::{Ui, UiResources}; +use ui::{setup_display, Ui, UiResources}; use usb_hub::UsbHub; use watchdog::Watchdog; @@ -55,6 +55,9 @@ use watchdog::Watchdog; async fn main() -> Result<(), std::io::Error> { env_logger::init(); + // Show a splash screen very early on + let display = setup_display(); + // The BrokerBuilder collects topics that should be exported via the // MQTT/REST APIs. // The topics are also used to pass around data inside the tacd. @@ -105,6 +108,9 @@ async fn main() -> Result<(), std::io::Error> { // in the web interface. journal::serve(&mut http_server.server); + // Expose the display as a .png on the web server + ui::serve_display(&mut http_server.server, display.screenshooter()); + // Set up the user interface for the hardware display on the TAC. // The different screens receive updates via the topics provided in // the UiResources struct. @@ -125,7 +131,7 @@ async fn main() -> Result<(), std::io::Error> { usb_hub, }; - Ui::new(&mut bb, resources, &mut http_server.server) + Ui::new(&mut bb, resources) }; // Consume the BrokerBuilder (no further topics can be added or removed) @@ -138,13 +144,13 @@ async fn main() -> Result<(), std::io::Error> { // exits (with an error). if let Some(watchdog) = watchdog { select! { - ui_err = ui.run().fuse() => ui_err, + ui_err = ui.run(display).fuse() => ui_err, wi_err = http_server.serve().fuse() => wi_err, wd_err = watchdog.keep_fed().fuse() => wd_err, } } else { select! { - ui_err = ui.run().fuse() => ui_err, + ui_err = ui.run(display).fuse() => ui_err, wi_err = http_server.serve().fuse() => wi_err, } } diff --git a/src/setup_mode.rs b/src/setup_mode.rs index 10f094ae..d735cfef 100644 --- a/src/setup_mode.rs +++ b/src/setup_mode.rs @@ -34,6 +34,7 @@ const AUTHORIZED_KEYS_PATH: &str = "/home/root/.ssh/authorized_keys"; pub struct SetupMode { pub setup_mode: Arc>, + pub show_help: Arc>, } impl SetupMode { @@ -128,6 +129,14 @@ impl SetupMode { pub fn new(bb: &mut BrokerBuilder, server: &mut Server<()>) -> Self { let this = Self { setup_mode: bb.topic("/v1/tac/setup_mode", true, false, true, Some(true), 1), + show_help: bb.topic( + "/v1/tac/display/show_help", + true, + false, + true, + Some(true), + 1, + ), }; this.handle_leave_requests(bb); diff --git a/src/ui.rs b/src/ui.rs index 7150b520..49634bb2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -18,21 +18,24 @@ use std::time::Duration; use async_std::prelude::*; -use async_std::sync::{Arc, Mutex}; -use async_std::task::{sleep, spawn}; +use async_std::sync::Arc; +use async_std::task::spawn; +use futures::{select, FutureExt}; use tide::{Response, Server}; use crate::broker::{BrokerBuilder, Topic}; use crate::led::{BlinkPattern, BlinkPatternBuilder}; +mod alerts; mod buttons; -mod draw_fb; +mod display; mod screens; mod widgets; -use buttons::{handle_buttons, ButtonEvent}; -use draw_fb::FramebufferDrawTarget; -use screens::{MountableScreen, Screen}; +use alerts::{AlertList, Alerter}; +use buttons::{handle_buttons, Button, ButtonEvent, Direction, PressDuration, Source}; +pub use display::{Display, ScreenShooter}; +use screens::{splash, ActivatableScreen, AlertScreen, NormalScreen, Screen}; pub struct UiResources { pub adc: crate::adc::Adc, @@ -51,74 +54,89 @@ pub struct UiResources { } pub struct Ui { - draw_target: Arc>, - screen: Arc>, + screen: Arc>, + alerts: Arc>, locator: Arc>, - locator_dance: Arc>, buttons: Arc>, - screens: Vec>, + screens: Vec>, + reboot_message: Arc>>, res: UiResources, } -/// Add a web endpoint that serves the current framebuffer as png -fn serve_framebuffer(server: &mut Server<()>, draw_target: Arc>) { +enum InputEvent { + NextScreen, + ToggleAction(Source), + PerformAction(Source), +} + +impl InputEvent { + fn from_button(ev: ButtonEvent) -> Option { + match ev { + ButtonEvent { + dir: Direction::Press, + btn: Button::Upper, + dur: PressDuration::Short, + src: _, + } => Some(Self::NextScreen), + ButtonEvent { + dir: Direction::Release, + btn: Button::Lower, + dur: PressDuration::Short, + src, + } => Some(Self::ToggleAction(src)), + ButtonEvent { + dir: Direction::Press, + btn: Button::Lower, + dur: PressDuration::Long, + src, + } => Some(Self::PerformAction(src)), + _ => None, + } + } +} + +pub fn setup_display() -> Display { + let display = Display::new(); + + display.clear(); + display.with_lock(splash); + + display +} + +/// Add a web endpoint that serves the current display content as png +pub fn serve_display(server: &mut Server<()>, screenshooter: ScreenShooter) { server.at("/v1/tac/display/content").get(move |_| { - let draw_target = draw_target.clone(); + let png = screenshooter.as_png(); async move { Ok(Response::builder(200) .content_type("image/png") .header("Cache-Control", "no-store") - .body(draw_target.lock().await.as_png()) + .body(png) .build()) } }); } impl Ui { - pub fn new(bb: &mut BrokerBuilder, res: UiResources, server: &mut Server<()>) -> Self { - let screen = bb.topic_rw("/v1/tac/display/screen", Some(Screen::ScreenSaver)); + pub fn new(bb: &mut BrokerBuilder, res: UiResources) -> Self { + let screen = bb.topic_rw("/v1/tac/display/screen", Some(NormalScreen::first())); let locator = bb.topic_rw("/v1/tac/display/locator", Some(false)); - let locator_dance = bb.topic_ro("/v1/tac/display/locator_dance", Some(0)); let buttons = bb.topic("/v1/tac/display/buttons", true, true, false, None, 0); + let alerts = bb.topic_ro("/v1/tac/display/alerts", Some(AlertList::new())); + let reboot_message = Topic::anonymous(None); + + alerts.assert(AlertScreen::ScreenSaver); - // Initialize all the screens now so they can be mounted later - let screens: Vec> = screens::init(&res, &screen, &buttons); + // Initialize all the screens now so they can be activated later + let screens = screens::init(&res, &alerts, &buttons, &reboot_message, &locator); handle_buttons( "/dev/input/by-path/platform-gpio-keys-event", buttons.clone(), ); - // Animated Locator for the locator widget - let locator_task = locator.clone(); - let locator_dance_task = locator_dance.clone(); - spawn(async move { - let (mut rx, _) = locator_task.clone().subscribe_unbounded(); - - loop { - // As long as the locator is active: - // count down the value in locator_dance from 63 to 0 - // with some pause in between in a loop. - while locator_task.try_get().unwrap_or(false) { - locator_dance_task.modify(|v| match v { - None | Some(0) => Some(63), - Some(v) => Some(v - 1), - }); - sleep(Duration::from_millis(100)).await; - } - - // If the locator is empty stop the animation - locator_dance_task.set(0); - - match rx.next().await { - Some(true) => {} - Some(false) => continue, - None => break, - } - } - }); - // Blink the status LED when locator is active let led_status_pattern = res.led.status.clone(); let led_status_color = res.led.status_color.clone(); @@ -146,24 +164,38 @@ impl Ui { } }); - let draw_target = Arc::new(Mutex::new(FramebufferDrawTarget::new())); - - // Expose the framebuffer as png via the web interface - serve_framebuffer(server, draw_target.clone()); - Self { - draw_target, screen, + alerts, locator, - locator_dance, buttons, screens, + reboot_message, res, } } - pub async fn run(mut self) -> Result<(), std::io::Error> { + pub async fn run(mut self, display: Display) -> Result<(), std::io::Error> { let (mut screen_rx, _) = self.screen.clone().subscribe_unbounded(); + let (mut alerts_rx, _) = self.alerts.clone().subscribe_unbounded(); + let (mut button_events, _) = self.buttons.clone().subscribe_unbounded(); + + // Helper to go to the next screen and activate the screensaver after + // cycling once. + let cycle_screen = { + let screen = self.screen.clone(); + let alerts = self.alerts.clone(); + + move || { + let cur = screen.try_get().unwrap_or_else(NormalScreen::first); + let next = cur.next(); + screen.set(next); + + if next == NormalScreen::first() { + alerts.assert(AlertScreen::ScreenSaver); + } + } + }; // Take the screens out of self so we can hand out references to self // to the screen mounting methods. @@ -173,34 +205,76 @@ impl Ui { decoy }; - let mut curr_screen_type = None; + let mut screen = screen_rx.next().await.unwrap(); + let mut alerts = alerts_rx.next().await.unwrap(); - while let Some(next_screen_type) = screen_rx.next().await { - // Only unmount / mount the shown screen if a change was requested - let should_change = curr_screen_type - .map(|c| c != next_screen_type) - .unwrap_or(true); + let mut showing = alerts + .highest_priority() + .map(Screen::Alert) + .unwrap_or(Screen::Normal(screen)); - if should_change { - // Find the currently shown screen (if any) and unmount it - if let Some(curr) = curr_screen_type { - if let Some(screen) = screens.iter_mut().find(|s| s.is_my_type(curr)) { - screen.unmount().await; - } - } + let mut display = Some(display); + + 'exit: loop { + let mut active_screen = { + let display = display.take().unwrap(); + display.clear(); + + screens + .iter_mut() + .find(|s| s.my_type() == showing) + .unwrap() + .activate(&self, display) + }; + + 'this_screen: loop { + select! { + new = screen_rx.next().fuse() => match new { + Some(new) => screen = new, + None => break 'exit, + }, + new = alerts_rx.next().fuse() => match new { + Some(new) => alerts = new, + None => break 'exit, + }, + ev = button_events.next().fuse() => match ev { + Some(ev) => { + let st = active_screen.my_type(); + let ev = InputEvent::from_button(ev); - // Clear the screen as static elements are not cleared by the - // widget framework magic - self.draw_target.lock().await.clear(); + // The NextScreen event for normal screens can be handled + // here. + // The situation for alerts is a bit more complicated. + // (Some ignore all input. Some acknoledge via the upper button). + // Leave handling for NextScreen to them. + + match (st, ev) { + (Screen::Normal(_), Some(InputEvent::NextScreen)) => cycle_screen(), + (_, Some(ev)) => active_screen.input(ev), + (_, None) => {} + } + }, + None => break 'exit, + }, - // Find the screen to show (if any) and "mount" it - // (e.g. tell it to handle the screen by itself). - if let Some(screen) = screens.iter_mut().find(|s| s.is_my_type(next_screen_type)) { - screen.mount(&self).await; } - curr_screen_type = Some(next_screen_type); + // Show the highest priority alert (if one is asserted) + // or a normal screen instead. + let showing_next = alerts + .highest_priority() + .map(Screen::Alert) + .unwrap_or(Screen::Normal(screen)); + + // Tear down this screen if another one should be shown. + // Otherwise just continue looping. + if showing_next != showing { + showing = showing_next; + break 'this_screen; + } } + + display = Some(active_screen.deactivate().await); } Ok(()) diff --git a/src/ui/alerts.rs b/src/ui/alerts.rs new file mode 100644 index 00000000..2596bbd2 --- /dev/null +++ b/src/ui/alerts.rs @@ -0,0 +1,69 @@ +// This file is part of tacd, the LXA TAC system daemon +// Copyright (C) 2023 Pengutronix e.K. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use serde::{Deserialize, Serialize}; + +use super::AlertScreen; +use crate::broker::Topic; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct AlertList(Vec); + +pub trait Alerter { + fn assert(&self, screen: AlertScreen); + fn deassert(&self, screen: AlertScreen); +} + +impl AlertList { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn highest_priority(&self) -> Option { + self.0.last().copied() + } +} + +impl Alerter for Topic { + fn assert(&self, screen: AlertScreen) { + self.modify(|list| { + let mut list = list.unwrap(); + + if list.0.iter().any(|s| s == &screen) { + None + } else { + list.0.push(screen); + list.0.sort(); + + Some(list) + } + }); + } + + fn deassert(&self, screen: AlertScreen) { + self.modify(|list| { + let mut list = list.unwrap(); + + if let Some(idx) = list.0.iter().position(|s| s == &screen) { + list.0.remove(idx); + Some(list) + } else { + None + } + }); + } +} diff --git a/src/ui/buttons.rs b/src/ui/buttons.rs index dc270975..c5c0ea31 100644 --- a/src/ui/buttons.rs +++ b/src/ui/buttons.rs @@ -18,12 +18,12 @@ use std::time::Duration; use async_std::sync::Arc; -use async_std::task::spawn_blocking; +use async_std::task::{block_on, sleep, spawn, spawn_blocking, JoinHandle}; use serde::{Deserialize, Serialize}; use crate::broker::Topic; -pub const LONG_PRESS: Duration = Duration::from_millis(750); +pub const LONG_PRESS: Duration = Duration::from_millis(500); #[cfg(feature = "demo_mode")] mod evd { @@ -52,6 +52,12 @@ mod evd { use evd::{Device, EventType, InputEventKind, Key}; +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +pub enum Direction { + Press, + Release, +} + #[derive(Serialize, Deserialize, Clone, Copy, Debug)] pub enum Button { Upper, @@ -97,30 +103,27 @@ pub enum Source { } #[derive(Serialize, Deserialize, Clone, Copy, Debug)] -pub enum ButtonEvent { - Press { - btn: Button, - #[serde(skip)] - src: Source, - }, - Release { - btn: Button, - dur: PressDuration, - #[serde(skip)] - src: Source, - }, +pub struct ButtonEvent { + pub dir: Direction, + pub btn: Button, + pub dur: PressDuration, + #[serde(skip)] + pub src: Source, } impl ButtonEvent { - fn press_from_id(id: usize) -> Self { - ButtonEvent::Press { + fn press_from_id(id: usize, dur: PressDuration) -> Self { + Self { + dir: Direction::Press, btn: Button::from_id(id), + dur, src: Source::Local, } } fn release_from_id_duration(id: usize, duration: Duration) -> Self { - ButtonEvent::Release { + Self { + dir: Direction::Release, btn: Button::from_id(id), dur: PressDuration::from_duration(duration), src: Source::Local, @@ -131,10 +134,9 @@ impl ButtonEvent { /// Spawn a thread that blockingly reads user input and pushes them into /// a broker framework topic. pub fn handle_buttons(path: &'static str, topic: Arc>) { - use super::*; - spawn_blocking(move || { let mut device = Device::open(path).unwrap(); + let mut press_task: [Option>; 2] = [None, None]; let mut start_time = [None, None]; loop { @@ -149,6 +151,10 @@ pub fn handle_buttons(path: &'static str, topic: Arc>) { _ => continue, }; + if let Some(task) = press_task[id].take() { + block_on(task.cancel()); + } + if ev.value() == 0 { // Button release -> send event if let Some(start) = start_time[id].take() { @@ -160,7 +166,16 @@ pub fn handle_buttons(path: &'static str, topic: Arc>) { } else { // Button press -> register start time and send event start_time[id] = Some(ev.timestamp()); - topic.set(ButtonEvent::press_from_id(id)); + + let topic = topic.clone(); + topic.set(ButtonEvent::press_from_id(id, PressDuration::Short)); + + // This task will either run to completion (in case of a long press) + // or will be canceled while sleep()ing (in case of a short press). + press_task[id] = Some(spawn(async move { + sleep(LONG_PRESS).await; + topic.set(ButtonEvent::press_from_id(id, PressDuration::Long)); + })); } } } diff --git a/src/ui/draw_fb.rs b/src/ui/display.rs similarity index 66% rename from src/ui/draw_fb.rs rename to src/ui/display.rs index 6b56f6f2..fbb58532 100644 --- a/src/ui/draw_fb.rs +++ b/src/ui/display.rs @@ -16,6 +16,7 @@ // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. use std::io::Cursor; +use std::sync::{Arc, Mutex}; use embedded_graphics::{pixelcolor::BinaryColor, prelude::*}; use png::{BitDepth, ColorType, Encoder}; @@ -62,34 +63,64 @@ mod backend { use backend::Framebuffer; -pub struct FramebufferDrawTarget { - fb: Framebuffer, +pub struct DisplayExclusive(Framebuffer); + +pub struct Display { + inner: Arc>, +} + +pub struct ScreenShooter { + inner: Arc>, } -impl FramebufferDrawTarget { - pub fn new() -> FramebufferDrawTarget { +impl Display { + pub fn new() -> Self { let mut fb = Framebuffer::new("/dev/fb0").unwrap(); fb.var_screen_info.activate = 128; // FB_ACTIVATE_FORCE Framebuffer::put_var_screeninfo(&fb.device, &fb.var_screen_info).unwrap(); - FramebufferDrawTarget { fb } + let de = DisplayExclusive(fb); + let inner = Arc::new(Mutex::new(de)); + + Self { inner } } - pub fn clear(&mut self) { - self.fb.frame.iter_mut().for_each(|p| *p = 0x00); + pub fn with_lock(&self, cb: F) -> R + where + F: FnOnce(&mut DisplayExclusive) -> R, + { + cb(&mut self.inner.lock().unwrap()) + } + + pub fn clear(&self) { + self.with_lock(|target| target.0.frame.iter_mut().for_each(|p| *p = 0x00)); } + pub fn screenshooter(&self) -> ScreenShooter { + ScreenShooter { + inner: self.inner.clone(), + } + } +} + +impl ScreenShooter { pub fn as_png(&self) -> Vec { - let mut dst = Cursor::new(Vec::new()); + let (image, xres, yres) = { + let fb = &self.inner.lock().unwrap().0; - let bpp = (self.fb.var_screen_info.bits_per_pixel / 8) as usize; - let xres = self.fb.var_screen_info.xres; - let yres = self.fb.var_screen_info.yres; - let res = (xres as usize) * (yres as usize); + let bpp = (fb.var_screen_info.bits_per_pixel / 8) as usize; + let xres = fb.var_screen_info.xres; + let yres = fb.var_screen_info.yres; + let res = (xres as usize) * (yres as usize); - let image: Vec = (0..res) - .map(|i| if self.fb.frame[i * bpp] != 0 { 0xff } else { 0 }) - .collect(); + let image: Vec = (0..res) + .map(|i| if fb.frame[i * bpp] != 0 { 0xff } else { 0 }) + .collect(); + + (image, xres, yres) + }; + + let mut dst = Cursor::new(Vec::new()); let mut writer = { let mut enc = Encoder::new(&mut dst, xres, yres); @@ -105,7 +136,7 @@ impl FramebufferDrawTarget { } } -impl DrawTarget for FramebufferDrawTarget { +impl DrawTarget for DisplayExclusive { type Color = BinaryColor; type Error = core::convert::Infallible; @@ -113,10 +144,10 @@ impl DrawTarget for FramebufferDrawTarget { where I: IntoIterator>, { - let bpp = self.fb.var_screen_info.bits_per_pixel / 8; - let xres = self.fb.var_screen_info.xres; - let yres = self.fb.var_screen_info.yres; - let line_length = self.fb.fix_screen_info.line_length; + let bpp = self.0.var_screen_info.bits_per_pixel / 8; + let xres = self.0.var_screen_info.xres; + let yres = self.0.var_screen_info.yres; + let line_length = self.0.fix_screen_info.line_length; for Pixel(coord, color) in pixels { let x = coord.x as u32; @@ -129,7 +160,7 @@ impl DrawTarget for FramebufferDrawTarget { let offset = line_length * y + bpp * x; for b in 0..bpp { - self.fb.frame[(offset + b) as usize] = match color { + self.0.frame[(offset + b) as usize] = match color { BinaryColor::Off => 0x00, BinaryColor::On => 0xff, } @@ -140,8 +171,8 @@ impl DrawTarget for FramebufferDrawTarget { } } -impl OriginDimensions for FramebufferDrawTarget { +impl OriginDimensions for DisplayExclusive { fn size(&self) -> Size { - Size::new(self.fb.var_screen_info.xres, self.fb.var_screen_info.yres) + Size::new(self.0.var_screen_info.xres, self.0.var_screen_info.yres) } } diff --git a/src/ui/screens.rs b/src/ui/screens.rs index ebd63dd2..f0d3975c 100644 --- a/src/ui/screens.rs +++ b/src/ui/screens.rs @@ -15,145 +15,186 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use async_std::sync::{Arc, Mutex}; +use async_std::sync::Arc; use async_trait::async_trait; use embedded_graphics::{ mono_font::MonoTextStyle, pixelcolor::BinaryColor, prelude::*, - primitives::{Line, PrimitiveStyle}, - text::Text, + primitives::{Line, PrimitiveStyle, Rectangle}, + text::{Alignment, Text}, }; use serde::{Deserialize, Serialize}; mod dig_out; mod help; mod iobus; +mod locator; mod power; -mod rauc; mod reboot; mod screensaver; mod setup; mod system; mod uart; +mod update_available; +mod update_installation; mod usb; use dig_out::DigOutScreen; use help::HelpScreen; use iobus::IoBusScreen; +use locator::LocatorScreen; use power::PowerScreen; -use rauc::RaucScreen; use reboot::RebootConfirmScreen; use screensaver::ScreenSaverScreen; use setup::SetupScreen; use system::SystemScreen; use uart::UartScreen; +use update_available::UpdateAvailableScreen; +use update_installation::UpdateInstallationScreen; use usb::UsbScreen; use super::buttons; use super::widgets; -use super::{FramebufferDrawTarget, Ui, UiResources}; +use super::{AlertList, Alerter, InputEvent, Ui, UiResources}; use crate::broker::Topic; +use crate::ui::display::{Display, DisplayExclusive}; use buttons::ButtonEvent; use widgets::UI_TEXT_FONT; -#[derive(Serialize, Deserialize, PartialEq, Clone, Copy)] -pub enum Screen { +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] +pub enum NormalScreen { DutPower, Usb, DigOut, System, IoBus, Uart, +} + +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] +pub enum AlertScreen { ScreenSaver, + Locator, RebootConfirm, - Rauc, - Setup, + UpdateAvailable, + UpdateInstallation, Help, + Setup, } -impl Screen { +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] +pub enum Screen { + Normal(NormalScreen), + Alert(AlertScreen), +} + +impl NormalScreen { + pub fn first() -> Self { + Self::DutPower + } + /// What is the next screen to transition to when e.g. the button is pressed? - fn next(&self) -> Self { + pub fn next(&self) -> Self { match self { Self::DutPower => Self::Usb, Self::Usb => Self::DigOut, Self::DigOut => Self::System, Self::System => Self::IoBus, Self::IoBus => Self::Uart, - Self::Uart => Self::ScreenSaver, - Self::ScreenSaver => Self::DutPower, - Self::RebootConfirm => Self::System, - Self::Rauc => Self::ScreenSaver, - Self::Setup => Self::ScreenSaver, - Self::Help => Self::ScreenSaver, + Self::Uart => Self::DutPower, } } - - /// Should screensaver be automatically enabled when in this screen? - fn use_screensaver(&self) -> bool { - !matches!(self, Self::Rauc | Self::Setup | Self::Help) - } } #[async_trait] -pub(super) trait MountableScreen: Sync + Send { - fn is_my_type(&self, screen: Screen) -> bool; - async fn mount(&mut self, ui: &Ui); - async fn unmount(&mut self); +pub(super) trait ActiveScreen { + fn my_type(&self) -> Screen; + async fn deactivate(self: Box) -> Display; + fn input(&mut self, ev: InputEvent); +} + +pub(super) trait ActivatableScreen: Sync + Send { + fn my_type(&self) -> Screen; + fn activate(&mut self, ui: &Ui, display: Display) -> Box; } /// Draw static screen border containing a title and an indicator for the /// position of the screen in the list of screens. -async fn draw_border(text: &str, screen: Screen, draw_target: &Arc>) { - let mut draw_target = draw_target.lock().await; - - Text::new( - text, - Point::new(8, 17), - MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On), - ) - .draw(&mut *draw_target) - .unwrap(); - - Line::new(Point::new(0, 24), Point::new(230, 24)) - .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 2)) - .draw(&mut *draw_target) +fn draw_border(text: &str, screen: NormalScreen, display: &Display) { + display.with_lock(|target| { + Text::new( + text, + Point::new(8, 17), + MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On), + ) + .draw(target) .unwrap(); - let screen_idx = screen as i32; - let num_screens = Screen::ScreenSaver as i32; - let x_start = screen_idx * 240 / num_screens; - let x_end = (screen_idx + 1) * 240 / num_screens; - - Line::new(Point::new(x_start, 238), Point::new(x_end, 238)) - .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 4)) - .draw(&mut *draw_target) - .unwrap(); + Line::new(Point::new(0, 24), Point::new(240, 24)) + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 2)) + .draw(target) + .unwrap(); + + let screen_idx = screen as i32; + let num_screens = (NormalScreen::Uart as i32) + 1; + let x_start = screen_idx * 240 / num_screens; + let x_end = (screen_idx + 1) * 240 / num_screens; + + Line::new(Point::new(x_start, 240), Point::new(x_end, 240)) + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 4)) + .draw(target) + .unwrap(); + }); } const fn row_anchor(row_num: u8) -> Point { - assert!(row_num < 8); + assert!(row_num < 9); Point::new(8, 52 + (row_num as i32) * 20) } +pub(super) fn splash(target: &mut DisplayExclusive) -> Rectangle { + let ui_text_style: MonoTextStyle = + MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); + + let text = Text::with_alignment( + "Welcome", + Point::new(120, 120), + ui_text_style, + Alignment::Center, + ); + + text.draw(target).unwrap(); + + text.bounding_box() +} + pub(super) fn init( res: &UiResources, - screen: &Arc>, + alerts: &Arc>, buttons: &Arc>, -) -> Vec> { + reboot_message: &Arc>>, + locator: &Arc>, +) -> Vec> { vec![ Box::new(DigOutScreen::new()), - Box::new(HelpScreen::new()), Box::new(IoBusScreen::new()), Box::new(PowerScreen::new()), - Box::new(RaucScreen::new(screen, &res.rauc.operation)), - Box::new(RebootConfirmScreen::new()), - Box::new(ScreenSaverScreen::new(buttons, screen)), - Box::new(SetupScreen::new(screen, &res.setup_mode.setup_mode)), Box::new(SystemScreen::new()), Box::new(UartScreen::new()), Box::new(UsbScreen::new()), + Box::new(HelpScreen::new(alerts, &res.setup_mode.show_help)), + Box::new(UpdateInstallationScreen::new( + alerts, + &res.rauc.operation, + reboot_message, + &res.rauc.should_reboot, + )), + Box::new(UpdateAvailableScreen::new(alerts, &res.rauc.channels)), + Box::new(RebootConfirmScreen::new(alerts, reboot_message)), + Box::new(ScreenSaverScreen::new(buttons, alerts)), + Box::new(SetupScreen::new(alerts, &res.setup_mode.setup_mode)), + Box::new(LocatorScreen::new(alerts, locator)), ] } diff --git a/src/ui/screens/dig_out.rs b/src/ui/screens/dig_out.rs index 52521ad4..3c051203 100644 --- a/src/ui/screens/dig_out.rs +++ b/src/ui/screens/dig_out.rs @@ -15,22 +15,21 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use async_std::prelude::*; use async_std::sync::Arc; -use async_std::task::spawn; use async_trait::async_trait; - use embedded_graphics::{ mono_font::MonoTextStyle, pixelcolor::BinaryColor, prelude::*, text::Text, }; -use super::buttons::*; use super::widgets::*; -use super::{draw_border, row_anchor, MountableScreen, Screen, Ui}; -use crate::broker::{Native, SubscriptionHandle, Topic}; +use super::{ + draw_border, row_anchor, ActivatableScreen, ActiveScreen, Display, InputEvent, NormalScreen, + Screen, Ui, +}; +use crate::broker::Topic; use crate::measurement::Measurement; -const SCREEN_TYPE: Screen = Screen::DigOut; +const SCREEN_TYPE: NormalScreen = NormalScreen::DigOut; const VOLTAGE_MAX: f32 = 5.0; const OFFSET_INDICATOR: Point = Point::new(170, -10); const OFFSET_BAR: Point = Point::new(140, -14); @@ -38,34 +37,30 @@ const WIDTH_BAR: u32 = 72; const HEIGHT_BAR: u32 = 18; pub struct DigOutScreen { - highlighted: Arc>, - widgets: Vec>, - buttons_handle: Option>, + highlighted: Arc>, } impl DigOutScreen { pub fn new() -> Self { Self { highlighted: Topic::anonymous(Some(0)), - widgets: Vec::new(), - buttons_handle: None, } } } -#[async_trait] -impl MountableScreen for DigOutScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE - } +struct Active { + widgets: WidgetContainer, + port_enables: [Arc>; 2], + highlighted: Arc>, +} - async fn mount(&mut self, ui: &Ui) { - draw_border("Digital Out", SCREEN_TYPE, &ui.draw_target).await; +impl ActivatableScreen for DigOutScreen { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) + } - self.widgets.push(Box::new(DynamicWidget::locator( - ui.locator_dance.clone(), - ui.draw_target.clone(), - ))); + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + draw_border("Digital Out", SCREEN_TYPE, &display); let ports = [ ( @@ -82,113 +77,110 @@ impl MountableScreen for DigOutScreen { ), ]; - for (idx, name, status, voltage) in ports { - let anchor_name = row_anchor(idx * 4); + let ui_text_style: MonoTextStyle = + MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); + + display.with_lock(|target| { + for (idx, name, _, _) in ports { + let anchor_name = row_anchor(idx * 4); + + Text::new(name, anchor_name, ui_text_style) + .draw(target) + .unwrap(); + } + }); + + let mut widgets = WidgetContainer::new(display); + + for (idx, _, status, voltage) in ports { let anchor_assert = row_anchor(idx * 4 + 1); let anchor_indicator = anchor_assert + OFFSET_INDICATOR; let anchor_voltage = row_anchor(idx * 4 + 2); let anchor_bar = anchor_voltage + OFFSET_BAR; - { - let mut draw_target = ui.draw_target.lock().await; + widgets.push(|display| { + DynamicWidget::text( + self.highlighted.clone(), + display, + anchor_assert, + Box::new(move |highlight| { + if *highlight == (idx as usize) { + "> Asserted:".into() + } else { + " Asserted:".into() + } + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::indicator( + status.clone(), + display, + anchor_indicator, + Box::new(|state: &bool| match *state { + true => IndicatorState::On, + false => IndicatorState::Off, + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + voltage.clone(), + display, + anchor_voltage, + Box::new(|meas: &Measurement| format!(" Volt: {:>4.1}V", meas.value)), + ) + }); + + widgets.push(|display| { + DynamicWidget::bar( + voltage.clone(), + display, + anchor_bar, + WIDTH_BAR, + HEIGHT_BAR, + Box::new(|meas: &Measurement| meas.value.abs() / VOLTAGE_MAX), + ) + }); + } - let ui_text_style: MonoTextStyle = - MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); + let port_enables = [ui.res.dig_io.out_0.clone(), ui.res.dig_io.out_1.clone()]; + let highlighted = self.highlighted.clone(); - Text::new(name, anchor_name, ui_text_style) - .draw(&mut *draw_target) - .unwrap(); - } + let active = Active { + widgets, + port_enables, + highlighted, + }; - self.widgets.push(Box::new(DynamicWidget::text( - self.highlighted.clone(), - ui.draw_target.clone(), - anchor_assert, - Box::new(move |highlight: &u8| { - if *highlight == idx { - "> Asserted:".into() - } else { - " Asserted:".into() - } - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::indicator( - status.clone(), - ui.draw_target.clone(), - anchor_indicator, - Box::new(|state: &bool| match *state { - true => IndicatorState::On, - false => IndicatorState::Off, - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - voltage.clone(), - ui.draw_target.clone(), - anchor_voltage, - Box::new(|meas: &Measurement| format!(" Volt: {:>4.1}V", meas.value)), - ))); - - self.widgets.push(Box::new(DynamicWidget::bar( - voltage.clone(), - ui.draw_target.clone(), - anchor_bar, - WIDTH_BAR, - HEIGHT_BAR, - Box::new(|meas: &Measurement| meas.value.abs() / VOLTAGE_MAX), - ))); - } + Box::new(active) + } +} - let (mut button_events, buttons_handle) = ui.buttons.clone().subscribe_unbounded(); - let port_enables = [ui.res.dig_io.out_0.clone(), ui.res.dig_io.out_1.clone()]; - let port_highlight = self.highlighted.clone(); - let screen = ui.screen.clone(); - - spawn(async move { - while let Some(ev) = button_events.next().await { - let highlighted = port_highlight.get().await; - - match ev { - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Long, - src: _, - } => { - let port = &port_enables[highlighted as usize]; - - port.modify(|prev| Some(!prev.unwrap_or(true))); - } - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Short, - src: _, - } => { - port_highlight.set((highlighted + 1) % 2); - } - ButtonEvent::Release { - btn: Button::Upper, - dur: _, - src: _, - } => { - screen.set(SCREEN_TYPE.next()); - } - _ => {} - } - } - }); +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) + } - self.buttons_handle = Some(buttons_handle); + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await } - async fn unmount(&mut self) { - if let Some(handle) = self.buttons_handle.take() { - handle.unsubscribe(); - } + fn input(&mut self, ev: InputEvent) { + let highlighted = self.highlighted.try_get().unwrap_or(0); - for mut widget in self.widgets.drain(..) { - widget.unmount().await + match ev { + InputEvent::NextScreen => {} + InputEvent::ToggleAction(_) => { + self.highlighted.set((highlighted + 1) % 2); + } + InputEvent::PerformAction(_) => { + self.port_enables[highlighted].toggle(true); + } } } } diff --git a/src/ui/screens/help.rs b/src/ui/screens/help.rs index 199f6615..ac3ac210 100644 --- a/src/ui/screens/help.rs +++ b/src/ui/screens/help.rs @@ -16,16 +16,19 @@ // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. use async_std::prelude::*; +use async_std::sync::Arc; use async_std::task::spawn; use async_trait::async_trait; use embedded_graphics::prelude::Point; -use super::buttons::*; use super::widgets::*; -use super::{MountableScreen, Screen, Ui}; -use crate::broker::{Native, SubscriptionHandle, Topic}; +use super::Display; +use super::{ + ActivatableScreen, ActiveScreen, AlertList, AlertScreen, Alerter, InputEvent, Screen, Ui, +}; +use crate::broker::Topic; -const SCREEN_TYPE: Screen = Screen::Help; +const SCREEN_TYPE: AlertScreen = AlertScreen::Help; const PAGES: &[&str] = &[ "Hey there! @@ -52,104 +55,117 @@ Press it to leave this guide", ]; -pub struct HelpScreen { - widgets: Vec>, - buttons_handle: Option>, +pub struct HelpScreen; + +struct Active { + widgets: WidgetContainer, + up: Arc>, + page: Arc>, + show_help: Arc>, } impl HelpScreen { - pub fn new() -> Self { - Self { - widgets: Vec::new(), - buttons_handle: None, - } + pub fn new(alerts: &Arc>, show_help: &Arc>) -> Self { + let (mut show_help_events, _) = show_help.clone().subscribe_unbounded(); + let alerts = alerts.clone(); + + spawn(async move { + while let Some(show_help) = show_help_events.next().await { + if show_help { + alerts.assert(AlertScreen::Help); + } else { + alerts.deassert(AlertScreen::Help); + } + } + }); + + Self } } -#[async_trait] -impl MountableScreen for HelpScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE +impl ActivatableScreen for HelpScreen { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) } - async fn mount(&mut self, ui: &Ui) { + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + let mut widgets = WidgetContainer::new(display); + let up = Topic::anonymous(Some(false)); let page = Topic::anonymous(Some(0)); - self.widgets.push(Box::new(DynamicWidget::text( - page.clone(), - ui.draw_target.clone(), - Point::new(8, 24), - Box::new(|page| PAGES[*page].into()), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - up.clone(), - ui.draw_target.clone(), - Point::new(8, 200), - Box::new(|up| match up { - false => " Scroll up".into(), - true => "> Scroll up".into(), - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - up.clone(), - ui.draw_target.clone(), - Point::new(8, 220), - Box::new(|up| match up { - false => "> Scroll down".into(), - true => " Scroll down".into(), - }), - ))); - - let (mut button_events, buttons_handle) = ui.buttons.clone().subscribe_unbounded(); - let screen = ui.screen.clone(); + widgets.push(|display| { + DynamicWidget::text( + page.clone(), + display, + Point::new(8, 24), + Box::new(|page| PAGES[*page].into()), + ) + }); - spawn(async move { - while let Some(ev) = button_events.next().await { - match ev { - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Short, - src: _, - } => up.modify(|a| Some(!a.unwrap_or(false))), - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Long, - src: _, - } => { - let up = up.clone().get().await; - - page.modify(|page| match (page.unwrap_or(0), up) { - (0, true) => Some(0), - (p, true) => Some(p - 1), - (2, false) => Some(2), - (p, false) => Some(p + 1), - }); - } - ButtonEvent::Release { - btn: Button::Upper, - dur: _, - src: _, - } => { - screen.set(SCREEN_TYPE.next()); - } - ButtonEvent::Press { btn: _, src: _ } => {} - } - } + widgets.push(|display| { + DynamicWidget::text( + up.clone(), + display, + Point::new(8, 200), + Box::new(|up| match up { + false => " Scroll up".into(), + true => "> Scroll up".into(), + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + up.clone(), + display, + Point::new(8, 220), + Box::new(|up| match up { + false => "> Scroll down".into(), + true => " Scroll down".into(), + }), + ) }); - self.buttons_handle = Some(buttons_handle); + let show_help = ui.res.setup_mode.show_help.clone(); + + let active = Active { + widgets, + up, + page, + show_help, + }; + + Box::new(active) } +} - async fn unmount(&mut self) { - if let Some(handle) = self.buttons_handle.take() { - handle.unsubscribe(); - } +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } + + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } - for mut widget in self.widgets.drain(..) { - widget.unmount().await + fn input(&mut self, ev: InputEvent) { + match ev { + InputEvent::NextScreen => { + self.show_help.set(false); + } + InputEvent::ToggleAction(_) => self.up.toggle(false), + InputEvent::PerformAction(_) => { + let up = self.up.try_get().unwrap_or(false); + + self.page.modify(|page| match (page.unwrap_or(0), up) { + (0, true) => Some(0), + (p, true) => Some(p - 1), + (2, false) => Some(2), + (p, false) => Some(p + 1), + }); + } } } } diff --git a/src/ui/screens/iobus.rs b/src/ui/screens/iobus.rs index 9cbd50cb..2fce74d4 100644 --- a/src/ui/screens/iobus.rs +++ b/src/ui/screens/iobus.rs @@ -15,159 +15,149 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use async_std::prelude::*; -use async_std::task::spawn; +use async_std::sync::Arc; use async_trait::async_trait; - use embedded_graphics::{ mono_font::MonoTextStyle, pixelcolor::BinaryColor, prelude::*, text::Text, }; -use crate::broker::{Native, SubscriptionHandle}; -use crate::iobus::{LSSState, Nodes, ServerInfo}; - -use super::buttons::*; use super::widgets::*; -use super::{draw_border, row_anchor, MountableScreen, Screen, Ui}; +use super::{ + draw_border, row_anchor, ActivatableScreen, ActiveScreen, Display, InputEvent, NormalScreen, + Screen, Ui, +}; +use crate::broker::Topic; +use crate::iobus::{LSSState, Nodes, ServerInfo}; -const SCREEN_TYPE: Screen = Screen::IoBus; +const SCREEN_TYPE: NormalScreen = NormalScreen::IoBus; const OFFSET_INDICATOR: Point = Point::new(180, -10); -pub struct IoBusScreen { - widgets: Vec>, - buttons_handle: Option>, -} +pub struct IoBusScreen; impl IoBusScreen { pub fn new() -> Self { - Self { - widgets: Vec::new(), - buttons_handle: None, - } + Self } } -#[async_trait] -impl MountableScreen for IoBusScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE - } +struct Active { + widgets: WidgetContainer, + iobus_pwr_en: Arc>, +} - async fn mount(&mut self, ui: &Ui) { - draw_border("IOBus", SCREEN_TYPE, &ui.draw_target).await; +impl ActivatableScreen for IoBusScreen { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) + } - { - let mut draw_target = ui.draw_target.lock().await; + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + draw_border("IOBus", SCREEN_TYPE, &display); - let ui_text_style: MonoTextStyle = - MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); + let ui_text_style: MonoTextStyle = + MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); + display.with_lock(|target| { Text::new("CAN Status:", row_anchor(0), ui_text_style) - .draw(&mut *draw_target) + .draw(target) .unwrap(); Text::new("LSS Scan Status:", row_anchor(1), ui_text_style) - .draw(&mut *draw_target) + .draw(target) .unwrap(); Text::new("Power Fault:", row_anchor(2), ui_text_style) - .draw(&mut *draw_target) + .draw(target) .unwrap(); Text::new("> Power On:", row_anchor(5), ui_text_style) - .draw(&mut *draw_target) + .draw(target) .unwrap(); - } + }); - self.widgets.push(Box::new(DynamicWidget::text( - ui.res.iobus.nodes.clone(), - ui.draw_target.clone(), - row_anchor(3), - Box::new(move |nodes: &Nodes| format!("Connected Nodes: {}", nodes.result.len())), - ))); - - self.widgets.push(Box::new(DynamicWidget::locator( - ui.locator_dance.clone(), - ui.draw_target.clone(), - ))); - - self.widgets.push(Box::new(DynamicWidget::indicator( - ui.res.iobus.server_info.clone(), - ui.draw_target.clone(), - row_anchor(0) + OFFSET_INDICATOR, - Box::new(|info: &ServerInfo| match info.can_tx_error { - false => IndicatorState::On, - true => IndicatorState::Error, - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::indicator( - ui.res.iobus.server_info.clone(), - ui.draw_target.clone(), - row_anchor(1) + OFFSET_INDICATOR, - Box::new(|info: &ServerInfo| match info.lss_state { - LSSState::Scanning => IndicatorState::On, - LSSState::Idle => IndicatorState::Off, - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::indicator( - ui.res.dig_io.iobus_flt_fb.clone(), - ui.draw_target.clone(), - row_anchor(2) + OFFSET_INDICATOR, - Box::new(|state: &bool| match *state { - true => IndicatorState::Error, - false => IndicatorState::Off, - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::indicator( - ui.res.regulators.iobus_pwr_en.clone(), - ui.draw_target.clone(), - row_anchor(5) + OFFSET_INDICATOR, - Box::new(|state: &bool| match *state { - true => IndicatorState::On, - false => IndicatorState::Off, - }), - ))); - - let (mut button_events, buttons_handle) = ui.buttons.clone().subscribe_unbounded(); - let iobus_pwr_en = ui.res.regulators.iobus_pwr_en.clone(); - let screen = ui.screen.clone(); - - spawn(async move { - while let Some(ev) = button_events.next().await { - match ev { - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Long, - src: _, - } => iobus_pwr_en.modify(|prev| Some(!prev.unwrap_or(true))), - ButtonEvent::Release { - btn: Button::Upper, - dur: _, - src: _, - } => screen.set(SCREEN_TYPE.next()), - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Short, - src: _, - } => {} - ButtonEvent::Press { btn: _, src: _ } => {} - } - } + let mut widgets = WidgetContainer::new(display); + + widgets.push(|display| { + DynamicWidget::text( + ui.res.iobus.nodes.clone(), + display, + row_anchor(3), + Box::new(move |nodes: &Nodes| format!("Connected Nodes: {}", nodes.result.len())), + ) + }); + + widgets.push(|display| { + DynamicWidget::indicator( + ui.res.iobus.server_info.clone(), + display, + row_anchor(0) + OFFSET_INDICATOR, + Box::new(|info: &ServerInfo| match info.can_tx_error { + false => IndicatorState::On, + true => IndicatorState::Error, + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::indicator( + ui.res.iobus.server_info.clone(), + display, + row_anchor(1) + OFFSET_INDICATOR, + Box::new(|info: &ServerInfo| match info.lss_state { + LSSState::Scanning => IndicatorState::On, + LSSState::Idle => IndicatorState::Off, + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::indicator( + ui.res.dig_io.iobus_flt_fb.clone(), + display, + row_anchor(2) + OFFSET_INDICATOR, + Box::new(|state: &bool| match *state { + true => IndicatorState::Error, + false => IndicatorState::Off, + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::indicator( + ui.res.regulators.iobus_pwr_en.clone(), + display, + row_anchor(5) + OFFSET_INDICATOR, + Box::new(|state: &bool| match *state { + true => IndicatorState::On, + false => IndicatorState::Off, + }), + ) }); - self.buttons_handle = Some(buttons_handle); + let iobus_pwr_en = ui.res.regulators.iobus_pwr_en.clone(); + + let active = Active { + widgets, + iobus_pwr_en, + }; + + Box::new(active) } +} - async fn unmount(&mut self) { - if let Some(handle) = self.buttons_handle.take() { - handle.unsubscribe(); - } +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) + } + + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } - for mut widget in self.widgets.drain(..) { - widget.unmount().await + fn input(&mut self, ev: InputEvent) { + match ev { + InputEvent::NextScreen | InputEvent::ToggleAction(_) => {} + InputEvent::PerformAction(_) => self.iobus_pwr_en.toggle(true), } } } diff --git a/src/ui/screens/locator.rs b/src/ui/screens/locator.rs new file mode 100644 index 00000000..b4b9955e --- /dev/null +++ b/src/ui/screens/locator.rs @@ -0,0 +1,158 @@ +// This file is part of tacd, the LXA TAC system daemon +// Copyright (C) 2023 Pengutronix e.K. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use std::time::Instant; + +use async_std::prelude::*; +use async_std::sync::Arc; +use async_std::task::spawn; +use async_trait::async_trait; +use embedded_graphics::{ + mono_font::MonoTextStyle, + pixelcolor::BinaryColor, + prelude::*, + primitives::{Line, PrimitiveStyle}, + text::{Alignment, Text}, +}; + +use super::widgets::*; +use super::{ + ActivatableScreen, ActiveScreen, AlertList, AlertScreen, Alerter, Display, InputEvent, Screen, + Ui, +}; +use crate::broker::Topic; + +const SCREEN_TYPE: AlertScreen = AlertScreen::Locator; + +pub struct LocatorScreen; + +struct Active { + locator: Arc>, + widgets: WidgetContainer, +} + +impl LocatorScreen { + pub fn new(alerts: &Arc>, locator: &Arc>) -> Self { + let (mut locator_events, _) = locator.clone().subscribe_unbounded(); + let alerts = alerts.clone(); + + spawn(async move { + while let Some(locator) = locator_events.next().await { + if locator { + alerts.assert(SCREEN_TYPE); + } else { + alerts.deassert(SCREEN_TYPE); + } + } + }); + + Self + } +} + +impl ActivatableScreen for LocatorScreen { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } + + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + let ui_text_style: MonoTextStyle = + MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); + + display.with_lock(|target| { + Text::with_alignment( + "Locating this TAC", + Point::new(120, 80), + ui_text_style, + Alignment::Center, + ) + .draw(target) + .unwrap(); + + Text::with_alignment( + "> Found it!", + Point::new(120, 200), + ui_text_style, + Alignment::Center, + ) + .draw(target) + .unwrap(); + }); + + let mut widgets = WidgetContainer::new(display); + + widgets.push(|display| { + DynamicWidget::text_center( + ui.res.network.hostname.clone(), + display, + Point::new(120, 130), + Box::new(|hostname| hostname.clone()), + ) + }); + + let start = Instant::now(); + + widgets.push(|display| { + DynamicWidget::new( + ui.res.adc.time.clone(), + display, + Box::new(move |now, target| { + // Blink a bar below the hostname at 2Hz + let on = (now.duration_since(start).as_millis() / 500) % 2 == 0; + + if on { + let line = Line::new(Point::new(40, 135), Point::new(200, 135)) + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 2)); + + line.draw(target).unwrap(); + + Some(line.bounding_box()) + } else { + None + } + }), + ) + }); + + let locator = ui.locator.clone(); + + let active = Active { locator, widgets }; + + Box::new(active) + } +} + +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } + + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } + + fn input(&mut self, ev: InputEvent) { + match ev { + InputEvent::NextScreen => {} + InputEvent::ToggleAction(_) => {} + InputEvent::PerformAction(_) => { + self.locator.set(false); + } + } + } +} diff --git a/src/ui/screens/power.rs b/src/ui/screens/power.rs index 0fc202d1..0eea3999 100644 --- a/src/ui/screens/power.rs +++ b/src/ui/screens/power.rs @@ -15,20 +15,20 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use async_std::prelude::*; -use async_std::task::spawn; +use async_std::sync::Arc; use async_trait::async_trait; - use embedded_graphics::prelude::*; -use super::buttons::*; use super::widgets::*; -use super::{draw_border, row_anchor, MountableScreen, Screen, Ui}; -use crate::broker::{Native, SubscriptionHandle}; +use super::{ + draw_border, row_anchor, ActivatableScreen, ActiveScreen, Display, InputEvent, NormalScreen, + Screen, Ui, +}; +use crate::broker::Topic; use crate::dut_power::{OutputRequest, OutputState}; use crate::measurement::Measurement; -const SCREEN_TYPE: Screen = Screen::DutPower; +const SCREEN_TYPE: NormalScreen = NormalScreen::DutPower; const CURRENT_LIMIT: f32 = 5.0; const VOLTAGE_LIMIT: f32 = 48.0; const OFFSET_INDICATOR: Point = Point::new(155, -10); @@ -36,139 +36,136 @@ const OFFSET_BAR: Point = Point::new(112, -14); const WIDTH_BAR: u32 = 100; const HEIGHT_BAR: u32 = 18; -pub struct PowerScreen { - widgets: Vec>, - buttons_handle: Option>, -} +pub struct PowerScreen; impl PowerScreen { pub fn new() -> Self { - Self { - widgets: Vec::new(), - buttons_handle: None, - } + Self } } -#[async_trait] -impl MountableScreen for PowerScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE +struct Active { + widgets: WidgetContainer, + power_state: Arc>, + power_request: Arc>, +} + +impl ActivatableScreen for PowerScreen { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) } - async fn mount(&mut self, ui: &Ui) { - draw_border("DUT Power", SCREEN_TYPE, &ui.draw_target).await; - - self.widgets.push(Box::new(DynamicWidget::locator( - ui.locator_dance.clone(), - ui.draw_target.clone(), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - ui.res.adc.pwr_volt.topic.clone(), - ui.draw_target.clone(), - row_anchor(0), - Box::new(|meas: &Measurement| format!("V: {:-6.3}V", meas.value)), - ))); - - self.widgets.push(Box::new(DynamicWidget::bar( - ui.res.adc.pwr_volt.topic.clone(), - ui.draw_target.clone(), - row_anchor(0) + OFFSET_BAR, - WIDTH_BAR, - HEIGHT_BAR, - Box::new(|meas: &Measurement| meas.value / VOLTAGE_LIMIT), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - ui.res.adc.pwr_curr.topic.clone(), - ui.draw_target.clone(), - row_anchor(1), - Box::new(|meas: &Measurement| format!("I: {:-6.3}A", meas.value)), - ))); - - self.widgets.push(Box::new(DynamicWidget::bar( - ui.res.adc.pwr_curr.topic.clone(), - ui.draw_target.clone(), - row_anchor(1) + OFFSET_BAR, - WIDTH_BAR, - HEIGHT_BAR, - Box::new(|meas: &Measurement| meas.value / CURRENT_LIMIT), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - ui.res.dut_pwr.state.clone(), - ui.draw_target.clone(), - row_anchor(3), - Box::new(|state: &OutputState| match state { - OutputState::On => "> On".into(), - OutputState::Off => "> Off".into(), - OutputState::Changing => "> Changing".into(), - OutputState::OffFloating => "> Off (Float.)".into(), - OutputState::InvertedPolarity => "> Inv. Pol.".into(), - OutputState::OverCurrent => "> Ov. Curr.".into(), - OutputState::OverVoltage => "> Ov. Volt.".into(), - OutputState::RealtimeViolation => "> Rt Err.".into(), - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::indicator( - ui.res.dut_pwr.state.clone(), - ui.draw_target.clone(), - row_anchor(3) + OFFSET_INDICATOR, - Box::new(|state: &OutputState| match state { - OutputState::On => IndicatorState::On, - OutputState::Off | OutputState::OffFloating => IndicatorState::Off, - OutputState::Changing => IndicatorState::Unkown, - _ => IndicatorState::Error, - }), - ))); - - let (mut button_events, buttons_handle) = ui.buttons.clone().subscribe_unbounded(); + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + draw_border("DUT Power", SCREEN_TYPE, &display); + + let mut widgets = WidgetContainer::new(display); + + widgets.push(|display| { + DynamicWidget::text( + ui.res.adc.pwr_volt.topic.clone(), + display, + row_anchor(0), + Box::new(|meas: &Measurement| format!("V: {:-6.3}V", meas.value)), + ) + }); + + widgets.push(|display| { + DynamicWidget::bar( + ui.res.adc.pwr_volt.topic.clone(), + display, + row_anchor(0) + OFFSET_BAR, + WIDTH_BAR, + HEIGHT_BAR, + Box::new(|meas: &Measurement| meas.value / VOLTAGE_LIMIT), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + ui.res.adc.pwr_curr.topic.clone(), + display, + row_anchor(1), + Box::new(|meas: &Measurement| format!("I: {:-6.3}A", meas.value)), + ) + }); + + widgets.push(|display| { + DynamicWidget::bar( + ui.res.adc.pwr_curr.topic.clone(), + display, + row_anchor(1) + OFFSET_BAR, + WIDTH_BAR, + HEIGHT_BAR, + Box::new(|meas: &Measurement| meas.value / CURRENT_LIMIT), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + ui.res.dut_pwr.state.clone(), + display, + row_anchor(3), + Box::new(|state: &OutputState| match state { + OutputState::On => "> On".into(), + OutputState::Off => "> Off".into(), + OutputState::Changing => "> Changing".into(), + OutputState::OffFloating => "> Off (Float.)".into(), + OutputState::InvertedPolarity => "> Inv. Pol.".into(), + OutputState::OverCurrent => "> Ov. Curr.".into(), + OutputState::OverVoltage => "> Ov. Volt.".into(), + OutputState::RealtimeViolation => "> Rt Err.".into(), + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::indicator( + ui.res.dut_pwr.state.clone(), + display, + row_anchor(3) + OFFSET_INDICATOR, + Box::new(|state: &OutputState| match state { + OutputState::On => IndicatorState::On, + OutputState::Off | OutputState::OffFloating => IndicatorState::Off, + OutputState::Changing => IndicatorState::Unkown, + _ => IndicatorState::Error, + }), + ) + }); + let power_state = ui.res.dut_pwr.state.clone(); let power_request = ui.res.dut_pwr.request.clone(); - let screen = ui.screen.clone(); - - spawn(async move { - while let Some(ev) = button_events.next().await { - match ev { - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Long, - src: _, - } => { - let req = match power_state.get().await { - OutputState::On => OutputRequest::Off, - _ => OutputRequest::On, - }; - - power_request.set(req); - } - ButtonEvent::Release { - btn: Button::Upper, - dur: _, - src: _, - } => screen.set(SCREEN_TYPE.next()), - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Short, - src: _, - } => {} - ButtonEvent::Press { btn: _, src: _ } => {} - } - } - }); - self.buttons_handle = Some(buttons_handle); + let active = Active { + widgets, + power_state, + power_request, + }; + + Box::new(active) } +} - async fn unmount(&mut self) { - if let Some(handle) = self.buttons_handle.take() { - handle.unsubscribe(); - } +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) + } - for mut widget in self.widgets.drain(..) { - widget.unmount().await + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } + + fn input(&mut self, ev: InputEvent) { + match ev { + InputEvent::NextScreen | InputEvent::ToggleAction(_) => {} + InputEvent::PerformAction(_) => { + let req = match self.power_state.try_get() { + Some(OutputState::On) => OutputRequest::Off, + _ => OutputRequest::On, + }; + + self.power_request.set(req); + } } } } diff --git a/src/ui/screens/rauc.rs b/src/ui/screens/rauc.rs deleted file mode 100644 index 063aa66b..00000000 --- a/src/ui/screens/rauc.rs +++ /dev/null @@ -1,120 +0,0 @@ -// This file is part of tacd, the LXA TAC system daemon -// Copyright (C) 2022 Pengutronix e.K. -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, write to the Free Software Foundation, Inc., -// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -use async_std::prelude::*; -use async_std::sync::Arc; -use async_std::task::spawn; -use async_trait::async_trait; - -use embedded_graphics::prelude::*; - -use crate::broker::Topic; -use crate::dbus::rauc::Progress; - -use super::widgets::*; -use super::{MountableScreen, Screen, Ui}; - -const SCREEN_TYPE: Screen = Screen::Rauc; - -pub struct RaucScreen { - widgets: Vec>, -} - -impl RaucScreen { - pub fn new(screen: &Arc>, operation: &Arc>) -> Self { - // Activate the rauc screen if an update is started and deactivate - // if it is done - let screen = screen.clone(); - let (mut operation_events, _) = operation.clone().subscribe_unbounded(); - - spawn(async move { - let mut operation_prev = operation_events.next().await.unwrap(); - - while let Some(ev) = operation_events.next().await { - match (operation_prev.as_str(), ev.as_str()) { - (_, "installing") => screen.set(SCREEN_TYPE), - ("installing", _) => screen.set(SCREEN_TYPE.next()), - _ => {} - }; - - operation_prev = ev; - } - }); - - Self { - widgets: Vec::new(), - } - } -} - -#[async_trait] -impl MountableScreen for RaucScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE - } - - async fn mount(&mut self, ui: &Ui) { - self.widgets.push(Box::new(DynamicWidget::locator( - ui.locator_dance.clone(), - ui.draw_target.clone(), - ))); - - self.widgets.push(Box::new(DynamicWidget::text_center( - ui.res.rauc.progress.clone(), - ui.draw_target.clone(), - Point::new(120, 100), - Box::new(|progress: &Progress| { - let (_, text) = progress.message.split_whitespace().fold( - (0, String::new()), - move |(mut ll, mut text), word| { - let word_len = word.len(); - - if (ll + word_len) > 15 { - text.push('\n'); - ll = 0; - } else { - text.push(' '); - ll += 1; - } - - text.push_str(word); - ll += word_len; - - (ll, text) - }, - ); - - text - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::bar( - ui.res.rauc.progress.clone(), - ui.draw_target.clone(), - Point::new(20, 180), - 200, - 18, - Box::new(|progress: &Progress| progress.percentage as f32 / 100.0), - ))); - } - - async fn unmount(&mut self) { - for mut widget in self.widgets.drain(..) { - widget.unmount().await - } - } -} diff --git a/src/ui/screens/reboot.rs b/src/ui/screens/reboot.rs index cb8717ba..563bfa0e 100644 --- a/src/ui/screens/reboot.rs +++ b/src/ui/screens/reboot.rs @@ -16,10 +16,9 @@ // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. use async_std::prelude::*; +use async_std::sync::Arc; use async_std::task::spawn; use async_trait::async_trait; - -use crate::broker::{Native, SubscriptionHandle}; use embedded_graphics::{ mono_font::MonoTextStyle, pixelcolor::BinaryColor, @@ -27,90 +26,116 @@ use embedded_graphics::{ text::{Alignment, Text}, }; -use super::buttons::*; use super::widgets::*; -use super::{FramebufferDrawTarget, MountableScreen, Screen, Ui}; +use super::{ + ActivatableScreen, ActiveScreen, AlertList, AlertScreen, Alerter, Display, InputEvent, Screen, + Ui, +}; +use crate::broker::Topic; -const SCREEN_TYPE: Screen = Screen::RebootConfirm; +const SCREEN_TYPE: AlertScreen = AlertScreen::RebootConfirm; pub struct RebootConfirmScreen { - buttons_handle: Option>, + reboot_message: Arc>>, } impl RebootConfirmScreen { - pub fn new() -> Self { - Self { - buttons_handle: None, - } + pub fn new( + alerts: &Arc>, + reboot_message: &Arc>>, + ) -> Self { + // Receive questions like Some("Do you want to reboot?") and activate this screen + let (mut reboot_message_events, _) = reboot_message.clone().subscribe_unbounded(); + let reboot_message = reboot_message.clone(); + let alerts = alerts.clone(); + + spawn(async move { + while let Some(reboot_message) = reboot_message_events.next().await { + if reboot_message.is_some() { + alerts.assert(SCREEN_TYPE); + } else { + alerts.deassert(SCREEN_TYPE); + } + } + }); + + Self { reboot_message } } } -fn rly(draw_target: &mut FramebufferDrawTarget) { +fn rly(text: &str, display: &Display) { let text_style: MonoTextStyle = MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); - Text::with_alignment( - "Really reboot?\nLong press lower\nbutton to confirm.", - Point::new(120, 120), - text_style, - Alignment::Center, - ) - .draw(draw_target) - .unwrap(); + display.with_lock(|target| { + Text::with_alignment(text, Point::new(120, 80), text_style, Alignment::Center) + .draw(target) + .unwrap() + }); } -fn brb(draw_target: &mut FramebufferDrawTarget) { +fn brb(display: &Display) { let text_style: MonoTextStyle = MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); - draw_target.clear(); + display.clear(); + + display.with_lock(|target| { + Text::with_alignment( + "Hold tight\nBe right back", + Point::new(120, 120), + text_style, + Alignment::Center, + ) + .draw(target) + .unwrap(); + }); +} - Text::with_alignment( - "Hold tight\nBe right back", - Point::new(120, 120), - text_style, - Alignment::Center, - ) - .draw(draw_target) - .unwrap(); +struct Active { + display: Display, + reboot: Arc>, + reboot_message: Arc>>, } -#[async_trait] -impl MountableScreen for RebootConfirmScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE +impl ActivatableScreen for RebootConfirmScreen { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) } - async fn mount(&mut self, ui: &Ui) { - let draw_target = ui.draw_target.clone(); - rly(&mut *draw_target.lock().await); + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + let text = self.reboot_message.try_get().unwrap().unwrap(); + + rly(&text, &display); - let (mut button_events, buttons_handle) = ui.buttons.clone().subscribe_unbounded(); - let screen = ui.screen.clone(); let reboot = ui.res.systemd.reboot.clone(); + let reboot_message = self.reboot_message.clone(); - spawn(async move { - while let Some(ev) = button_events.next().await { - match ev { - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Long, - src: _, - } => { - brb(&mut *draw_target.lock().await); - reboot.set(true); - break; - } - ButtonEvent::Press { btn: _, src: _ } => {} - _ => screen.set(SCREEN_TYPE.next()), - } - } - }); + let active = Active { + display, + reboot, + reboot_message, + }; - self.buttons_handle = Some(buttons_handle); + Box::new(active) } +} - async fn unmount(&mut self) { - if let Some(handle) = self.buttons_handle.take() { - handle.unsubscribe(); +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } + + async fn deactivate(mut self: Box) -> Display { + self.display + } + + fn input(&mut self, ev: InputEvent) { + match ev { + InputEvent::NextScreen | InputEvent::ToggleAction(_) => self.reboot_message.set(None), + InputEvent::PerformAction(_) => { + brb(&self.display); + self.reboot.set(true); + } } } } diff --git a/src/ui/screens/screensaver.rs b/src/ui/screens/screensaver.rs index 156b9e87..0e286448 100644 --- a/src/ui/screens/screensaver.rs +++ b/src/ui/screens/screensaver.rs @@ -22,9 +22,7 @@ use async_std::future::timeout; use async_std::prelude::*; use async_std::sync::Arc; use async_std::task::spawn; - use async_trait::async_trait; - use embedded_graphics::{ mono_font::{ascii::FONT_10X20, MonoFont, MonoTextStyle}, pixelcolor::BinaryColor, @@ -35,12 +33,14 @@ use embedded_graphics::{ use super::buttons::*; use super::widgets::*; -use super::{MountableScreen, Screen, Ui}; - -use crate::broker::{Native, SubscriptionHandle, Topic}; +use super::{ + splash, ActivatableScreen, ActiveScreen, AlertList, AlertScreen, Alerter, Display, InputEvent, + Screen, Ui, +}; +use crate::broker::Topic; const UI_TEXT_FONT: MonoFont = FONT_10X20; -const SCREEN_TYPE: Screen = Screen::ScreenSaver; +const SCREEN_TYPE: AlertScreen = AlertScreen::ScreenSaver; const SCREENSAVER_TIMEOUT: Duration = Duration::from_secs(600); struct BounceAnimation { @@ -88,16 +88,14 @@ impl BounceAnimation { } } -pub struct ScreenSaverScreen { - widgets: Vec>, - buttons_handle: Option>, -} +pub struct ScreenSaverScreen; impl ScreenSaverScreen { - pub fn new(buttons: &Arc>, screen: &Arc>) -> Self { + pub fn new(buttons: &Arc>, alerts: &Arc>) -> Self { // Activate screensaver if no button is pressed for some time let (mut buttons_events, _) = buttons.clone().subscribe_unbounded(); - let screen_task = screen.clone(); + let alerts = alerts.clone(); + spawn(async move { loop { let ev = timeout(SCREENSAVER_TIMEOUT, buttons_events.next()).await; @@ -108,92 +106,85 @@ impl ScreenSaverScreen { }; if activate_screensaver { - screen_task.modify(|screen| { - screen.and_then(|s| { - if s.use_screensaver() { - Some(Screen::ScreenSaver) - } else { - None - } - }) - }); + alerts.assert(SCREEN_TYPE); } } }); - Self { - widgets: Vec::new(), - buttons_handle: None, - } + Self } } -#[async_trait] -impl MountableScreen for ScreenSaverScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE +struct Active { + widgets: WidgetContainer, + locator: Arc>, + alerts: Arc>, +} + +impl ActivatableScreen for ScreenSaverScreen { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) } - async fn mount(&mut self, ui: &Ui) { - let hostname = ui.res.network.hostname.get().await; + fn activate(&mut self, ui: &Ui, display: Display) -> Box { let bounce = BounceAnimation::new(Rectangle::with_corners( Point::new(0, 8), - Point::new(230, 240), + Point::new(240, 240), )); - self.widgets.push(Box::new(DynamicWidget::locator( - ui.locator_dance.clone(), - ui.draw_target.clone(), - ))); - - self.widgets.push(Box::new(DynamicWidget::new( - ui.res.adc.time.clone(), - ui.draw_target.clone(), - Box::new(move |_, target| { - let ui_text_style: MonoTextStyle = - MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); - - let text = Text::new(&hostname, Point::new(0, 0), ui_text_style); - let text = bounce.bounce(text); - - text.draw(target).unwrap(); - - Some(text.bounding_box()) - }), - ))); + let mut widgets = WidgetContainer::new(display); + + let hostname = ui.res.network.hostname.clone(); + + widgets.push(|display| { + DynamicWidget::new( + ui.res.adc.time.clone(), + display, + Box::new(move |_, target| { + let ui_text_style: MonoTextStyle = + MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); + + if let Some(hn) = hostname.try_get() { + let text = Text::new(&hn, Point::new(0, 0), ui_text_style); + let text = bounce.bounce(text); + text.draw(target).unwrap(); + + Some(text.bounding_box()) + } else { + Some(splash(target)) + } + }), + ) + }); - let (mut button_events, buttons_handle) = ui.buttons.clone().subscribe_unbounded(); let locator = ui.locator.clone(); - let screen = ui.screen.clone(); + let alerts = ui.alerts.clone(); - spawn(async move { - while let Some(ev) = button_events.next().await { - match ev { - ButtonEvent::Release { - btn: Button::Lower, - dur: _, - src: _, - } => locator.modify(|prev| Some(!prev.unwrap_or(false))), - ButtonEvent::Release { - btn: Button::Upper, - dur: _, - src: _, - } => screen.set(SCREEN_TYPE.next()), - _ => {} - } - } - }); + let active = Active { + widgets, + locator, + alerts, + }; - self.buttons_handle = Some(buttons_handle); + Box::new(active) } +} - async fn unmount(&mut self) { - if let Some(handle) = self.buttons_handle.take() { - handle.unsubscribe(); - } +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } + + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } - for mut widget in self.widgets.drain(..) { - widget.unmount().await + fn input(&mut self, ev: InputEvent) { + match ev { + InputEvent::NextScreen => self.alerts.deassert(SCREEN_TYPE), + InputEvent::ToggleAction(_) => {} + InputEvent::PerformAction(_) => self.locator.toggle(false), } } } diff --git a/src/ui/screens/setup.rs b/src/ui/screens/setup.rs index 77e530d6..c08a1f1d 100644 --- a/src/ui/screens/setup.rs +++ b/src/ui/screens/setup.rs @@ -19,15 +19,17 @@ use async_std::prelude::*; use async_std::sync::Arc; use async_std::task::spawn; use async_trait::async_trait; - use embedded_graphics::{prelude::Point, text::Alignment}; use serde::{Deserialize, Serialize}; use super::widgets::*; -use super::{MountableScreen, Screen, Ui}; +use super::{ + ActivatableScreen, ActiveScreen, AlertList, AlertScreen, Alerter, Display, InputEvent, Screen, + Ui, +}; use crate::broker::{Native, SubscriptionHandle, Topic}; -const SCREEN_TYPE: Screen = Screen::Setup; +const SCREEN_TYPE: AlertScreen = AlertScreen::Setup; #[derive(Serialize, Deserialize, Clone)] enum Connectivity { @@ -37,50 +39,39 @@ enum Connectivity { Both(String, String), } -pub struct SetupScreen { - widgets: Vec>, - hostname_update_handle: Option>, - ip_update_handle: Option, Native>>, +pub struct SetupScreen; + +struct Active { + widgets: WidgetContainer, + hostname_update_handle: SubscriptionHandle, + ip_update_handle: SubscriptionHandle, Native>, } impl SetupScreen { - pub fn new(screen: &Arc>, setup_mode: &Arc>) -> Self { + pub fn new(alerts: &Arc>, setup_mode: &Arc>) -> Self { let (mut setup_mode_events, _) = setup_mode.clone().subscribe_unbounded(); - let screen_task = screen.clone(); + let alerts = alerts.clone(); + spawn(async move { while let Some(setup_mode) = setup_mode_events.next().await { - /* If the setup mode is enabled and we are on the setup mode screen - * => Do nothing. - * If the setup mode is enabled and we are not on the setup mode screen - * => Go to the setup mode screen. - * If the setup mode is not enabled but we are on its screen - * => Go the "next" screen, as specified in screens.rs. - * None of the above - * => Do nothing. */ - screen_task.modify(|screen| match (setup_mode, screen) { - (true, Some(Screen::Setup)) => None, - (true, _) => Some(Screen::Setup), - (false, Some(Screen::Setup)) => Some(Screen::Setup.next()), - (false, _) => None, - }); + if setup_mode { + alerts.assert(AlertScreen::Setup); + } else { + alerts.deassert(AlertScreen::Setup); + } } }); - Self { - widgets: Vec::new(), - hostname_update_handle: None, - ip_update_handle: None, - } + Self } } -#[async_trait] -impl MountableScreen for SetupScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE +impl ActivatableScreen for SetupScreen { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) } - async fn mount(&mut self, ui: &Ui) { + fn activate(&mut self, ui: &Ui, display: Display) -> Box { /* We want to display hints on how to connect to this TAC. * We want to show: * - An URL based on the hostname, e.g. http://lxatac-12345 @@ -143,10 +134,12 @@ impl MountableScreen for SetupScreen { } }); - self.widgets.push(Box::new( + let mut widgets = WidgetContainer::new(display); + + widgets.push(|display| DynamicWidget::text_aligned( connectivity_topic, - ui.draw_target.clone(), + display, Point::new(120, 55), Box::new(|connectivity| match connectivity { Connectivity::Nothing => { @@ -160,25 +153,29 @@ impl MountableScreen for SetupScreen { ), }), Alignment::Center, - ) - , )); - self.hostname_update_handle = Some(hostname_update_handle); - self.ip_update_handle = Some(ip_update_handle); - } + let active = Active { + widgets, + hostname_update_handle, + ip_update_handle, + }; - async fn unmount(&mut self) { - if let Some(handle) = self.hostname_update_handle.take() { - handle.unsubscribe(); - } + Box::new(active) + } +} - if let Some(handle) = self.ip_update_handle.take() { - handle.unsubscribe(); - } +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } - for mut widget in self.widgets.drain(..) { - widget.unmount().await - } + async fn deactivate(mut self: Box) -> Display { + self.hostname_update_handle.unsubscribe(); + self.ip_update_handle.unsubscribe(); + self.widgets.destroy().await } + + fn input(&mut self, _ev: InputEvent) {} } diff --git a/src/ui/screens/system.rs b/src/ui/screens/system.rs index db13b40a..b40d7801 100644 --- a/src/ui/screens/system.rs +++ b/src/ui/screens/system.rs @@ -15,25 +15,28 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use async_std::prelude::*; -use async_std::task::spawn; +use async_std::sync::Arc; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use super::buttons::*; +use super::buttons::Source; use super::widgets::*; -use super::{draw_border, row_anchor, MountableScreen, Screen, Ui}; -use crate::broker::{Native, SubscriptionHandle, Topic}; +use super::{ + draw_border, row_anchor, ActivatableScreen, ActiveScreen, AlertList, AlertScreen, Alerter, + Display, InputEvent, NormalScreen, Screen, Ui, +}; +use crate::broker::Topic; use crate::dbus::networkmanager::LinkInfo; use crate::measurement::Measurement; -const SCREEN_TYPE: Screen = Screen::System; +const SCREEN_TYPE: NormalScreen = NormalScreen::System; #[derive(Serialize, Deserialize, Clone, Copy)] enum Action { Reboot, Help, SetupMode, + Updates, } impl Action { @@ -41,167 +44,179 @@ impl Action { match self { Self::Reboot => Self::Help, Self::Help => Self::SetupMode, - Self::SetupMode => Self::Reboot, + Self::SetupMode => Self::Updates, + Self::Updates => Self::Reboot, } } } -pub struct SystemScreen { - widgets: Vec>, - buttons_handle: Option>, -} +pub struct SystemScreen; impl SystemScreen { pub fn new() -> Self { - Self { - widgets: Vec::new(), - buttons_handle: None, - } + Self } } -#[async_trait] -impl MountableScreen for SystemScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE +struct Active { + widgets: WidgetContainer, + setup_mode: Arc>, + highlighted: Arc>, + reboot_message: Arc>>, + show_help: Arc>, + alerts: Arc>, +} + +impl ActivatableScreen for SystemScreen { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) } - async fn mount(&mut self, ui: &Ui) { - draw_border("System Status", SCREEN_TYPE, &ui.draw_target).await; + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + draw_border("System Status", SCREEN_TYPE, &display); + let mut widgets = WidgetContainer::new(display); let highlighted = Topic::anonymous(Some(Action::Reboot)); - self.widgets.push(Box::new(DynamicWidget::locator( - ui.locator_dance.clone(), - ui.draw_target.clone(), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - ui.res.temperatures.soc_temperature.clone(), - ui.draw_target.clone(), - row_anchor(0), - Box::new(|meas: &Measurement| format!("SoC: {:.0}C", meas.value)), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - ui.res.network.uplink_interface.clone(), - ui.draw_target.clone(), - row_anchor(1), - Box::new(|info: &LinkInfo| match info.carrier { - true => format!("Uplink: {}MBit/s", info.speed), - false => "Uplink: Down".to_string(), - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - ui.res.network.dut_interface.clone(), - ui.draw_target.clone(), - row_anchor(2), - Box::new(|info: &LinkInfo| match info.carrier { - true => format!("DUT: {}MBit/s", info.speed), - false => "DUT: Down".to_string(), - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - ui.res.network.bridge_interface.clone(), - ui.draw_target.clone(), - row_anchor(3), - Box::new(|ips: &Vec| { - let ip = ips.get(0).map(|s| s.as_str()).unwrap_or("-"); - format!("IP: {}", ip) - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - highlighted.clone(), - ui.draw_target.clone(), - row_anchor(5), - Box::new(|action| match action { - Action::Reboot => "> Reboot".into(), - _ => " Reboot".into(), - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - highlighted.clone(), - ui.draw_target.clone(), - row_anchor(6), - Box::new(|action| match action { - Action::Help => "> Help".into(), - _ => " Help".into(), - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::text( - highlighted.clone(), - ui.draw_target.clone(), - row_anchor(7), - Box::new(|action| match action { - Action::SetupMode => "> Setup Mode".into(), - _ => " Setup Mode".into(), - }), - ))); - - let (mut button_events, buttons_handle) = ui.buttons.clone().subscribe_unbounded(); - let setup_mode = ui.res.setup_mode.setup_mode.clone(); - let screen = ui.screen.clone(); - - spawn(async move { - while let Some(ev) = button_events.next().await { - let action = highlighted.get().await; - - match ev { - ButtonEvent::Release { - btn: Button::Lower, - dur: _, - src: Source::Web, - } => { - /* Only allow upper button interaction (going to the next screen) - * for inputs on the web. - * Triggering Reboots is possible via the API, so we do not have to - * protect against that and opening the help text is harmless as well, - * but we could think of an attacker that tricks a local user into - * long pressing the lower button right when the attacker goes to the - * "Setup Mode" entry in the menu so that they can deploy new keys. - * Prevent that by disabling navigation altogether. */ - } - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Long, - src: Source::Local, - } => match action { - Action::Reboot => screen.set(Screen::RebootConfirm), - Action::Help => screen.set(Screen::Help), - Action::SetupMode => setup_mode.modify(|prev| Some(!prev.unwrap_or(true))), - }, - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Short, - src: Source::Local, - } => highlighted.set(action.next()), - ButtonEvent::Release { - btn: Button::Upper, - dur: _, - src: _, - } => { - screen.set(SCREEN_TYPE.next()); - } - ButtonEvent::Press { btn: _, src: _ } => {} - } - } + widgets.push(|display| { + DynamicWidget::text( + ui.res.temperatures.soc_temperature.clone(), + display, + row_anchor(0), + Box::new(|meas: &Measurement| format!("SoC: {:.0}C", meas.value)), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + ui.res.network.uplink_interface.clone(), + display, + row_anchor(1), + Box::new(|info: &LinkInfo| match info.carrier { + true => format!("Uplink: {}MBit/s", info.speed), + false => "Uplink: Down".to_string(), + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + ui.res.network.dut_interface.clone(), + display, + row_anchor(2), + Box::new(|info: &LinkInfo| match info.carrier { + true => format!("DUT: {}MBit/s", info.speed), + false => "DUT: Down".to_string(), + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + ui.res.network.bridge_interface.clone(), + display, + row_anchor(3), + Box::new(|ips: &Vec| { + let ip = ips.get(0).map(|s| s.as_str()).unwrap_or("-"); + format!("IP: {}", ip) + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + highlighted.clone(), + display, + row_anchor(5), + Box::new(|action| match action { + Action::Reboot => "> Reboot".into(), + _ => " Reboot".into(), + }), + ) }); - self.buttons_handle = Some(buttons_handle); + widgets.push(|display| { + DynamicWidget::text( + highlighted.clone(), + display, + row_anchor(6), + Box::new(|action| match action { + Action::Help => "> Help".into(), + _ => " Help".into(), + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + highlighted.clone(), + display, + row_anchor(7), + Box::new(|action| match action { + Action::SetupMode => "> Setup Mode".into(), + _ => " Setup Mode".into(), + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::text( + highlighted.clone(), + display, + row_anchor(8), + Box::new(|action| match action { + Action::Updates => "> Updates".into(), + _ => " Updates".into(), + }), + ) + }); + + let reboot_message = ui.reboot_message.clone(); + let setup_mode = ui.res.setup_mode.setup_mode.clone(); + let show_help = ui.res.setup_mode.show_help.clone(); + let alerts = ui.alerts.clone(); + + let active = Active { + widgets, + highlighted, + reboot_message, + setup_mode, + show_help, + alerts, + }; + + Box::new(active) } +} - async fn unmount(&mut self) { - if let Some(handle) = self.buttons_handle.take() { - handle.unsubscribe(); - } +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) + } + + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } - for mut widget in self.widgets.drain(..) { - widget.unmount().await + fn input(&mut self, ev: InputEvent) { + let action = self.highlighted.try_get().unwrap_or(Action::Reboot); + + // Actions on this page are only allowed with Source::Local + // (in contrast to Source::Web) to prevent e.g. an attacker from + // re-enabling the setup mode. + + match ev { + InputEvent::ToggleAction(Source::Local) => self.highlighted.set(action.next()), + InputEvent::PerformAction(Source::Local) => match action { + Action::Reboot => self.reboot_message.set(Some( + "Really reboot?\nLong press lower\nbutton to confirm.".to_string(), + )), + Action::Help => self.show_help.set(true), + Action::SetupMode => self.setup_mode.set(true), + Action::Updates => self.alerts.assert(AlertScreen::UpdateAvailable), + }, + _ => {} } } } diff --git a/src/ui/screens/uart.rs b/src/ui/screens/uart.rs index 36a7408d..54db4fc7 100644 --- a/src/ui/screens/uart.rs +++ b/src/ui/screens/uart.rs @@ -15,50 +15,45 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use async_std::prelude::*; use async_std::sync::Arc; -use async_std::task::spawn; use async_trait::async_trait; - use embedded_graphics::prelude::*; -use crate::broker::{Native, SubscriptionHandle, Topic}; - -use super::buttons::*; use super::widgets::*; -use super::{draw_border, MountableScreen, Screen, Ui}; +use super::{ + draw_border, ActivatableScreen, ActiveScreen, Display, InputEvent, NormalScreen, Screen, Ui, +}; +use crate::broker::Topic; -const SCREEN_TYPE: Screen = Screen::Uart; +const SCREEN_TYPE: NormalScreen = NormalScreen::Uart; pub struct UartScreen { - highlighted: Arc>, - widgets: Vec>, - buttons_handle: Option>, + highlighted: Arc>, } impl UartScreen { pub fn new() -> Self { Self { highlighted: Topic::anonymous(Some(0)), - widgets: Vec::new(), - buttons_handle: None, } } } -#[async_trait] -impl MountableScreen for UartScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE +struct Active { + widgets: WidgetContainer, + dir_enables: [Arc>; 2], + highlighted: Arc>, +} + +impl ActivatableScreen for UartScreen { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) } - async fn mount(&mut self, ui: &Ui) { - draw_border("DUT UART", SCREEN_TYPE, &ui.draw_target).await; + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + draw_border("DUT UART", SCREEN_TYPE, &display); - self.widgets.push(Box::new(DynamicWidget::locator( - ui.locator_dance.clone(), - ui.draw_target.clone(), - ))); + let mut widgets = WidgetContainer::new(display); let ports = [ (0, "UART RX EN", 52, &ui.res.dig_io.uart_rx_en), @@ -66,76 +61,65 @@ impl MountableScreen for UartScreen { ]; for (idx, name, y, status) in ports { - self.widgets.push(Box::new(DynamicWidget::text( - self.highlighted.clone(), - ui.draw_target.clone(), - Point::new(8, y), - Box::new(move |highlight: &u8| { - format!( - "{} {}", - if *highlight as usize == idx { ">" } else { " " }, - name, - ) - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::indicator( - status.clone(), - ui.draw_target.clone(), - Point::new(160, y - 10), - Box::new(|state: &bool| match *state { - true => IndicatorState::On, - false => IndicatorState::Off, - }), - ))); + widgets.push(|display| { + DynamicWidget::text( + self.highlighted.clone(), + display, + Point::new(8, y), + Box::new(move |highlight| { + format!("{} {}", if *highlight == idx { ">" } else { " " }, name,) + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::indicator( + status.clone(), + display, + Point::new(160, y - 10), + Box::new(|state: &bool| match *state { + true => IndicatorState::On, + false => IndicatorState::Off, + }), + ) + }); } - let (mut button_events, buttons_handle) = ui.buttons.clone().subscribe_unbounded(); let dir_enables = [ ui.res.dig_io.uart_rx_en.clone(), ui.res.dig_io.uart_tx_en.clone(), ]; - let dir_highlight = self.highlighted.clone(); - let screen = ui.screen.clone(); - - spawn(async move { - while let Some(ev) = button_events.next().await { - let highlighted = dir_highlight.get().await; - let port = &dir_enables[highlighted as usize]; - - match ev { - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Long, - src: _, - } => port.modify(|prev| Some(!prev.unwrap_or(false))), - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Short, - src: _, - } => { - dir_highlight.set((highlighted + 1) % 2); - } - ButtonEvent::Release { - btn: Button::Upper, - dur: _, - src: _, - } => screen.set(SCREEN_TYPE.next()), - ButtonEvent::Press { btn: _, src: _ } => {} - } - } - }); + let highlighted = self.highlighted.clone(); - self.buttons_handle = Some(buttons_handle); + let active = Active { + widgets, + dir_enables, + highlighted, + }; + + Box::new(active) } +} - async fn unmount(&mut self) { - if let Some(handle) = self.buttons_handle.take() { - handle.unsubscribe(); - } +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) + } - for mut widget in self.widgets.drain(..) { - widget.unmount().await + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } + + fn input(&mut self, ev: InputEvent) { + let highlighted = self.highlighted.try_get().unwrap_or(0); + + match ev { + InputEvent::NextScreen => {} + InputEvent::ToggleAction(_) => { + self.highlighted.set((highlighted + 1) % 2); + } + InputEvent::PerformAction(_) => self.dir_enables[highlighted].toggle(false), } } } diff --git a/src/ui/screens/update_available.rs b/src/ui/screens/update_available.rs new file mode 100644 index 00000000..69e200e7 --- /dev/null +++ b/src/ui/screens/update_available.rs @@ -0,0 +1,262 @@ +// This file is part of tacd, the LXA TAC system daemon +// Copyright (C) 2022 Pengutronix e.K. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use async_std::prelude::*; +use async_std::sync::Arc; +use async_std::task::spawn; +use async_trait::async_trait; +use embedded_graphics::{ + mono_font::MonoTextStyle, pixelcolor::BinaryColor, prelude::*, text::Text, +}; +use serde::{Deserialize, Serialize}; + +use super::widgets::*; +use super::{ + row_anchor, ActivatableScreen, ActiveScreen, AlertList, AlertScreen, Alerter, Display, + InputEvent, Screen, Ui, +}; +use crate::broker::Topic; +use crate::dbus::rauc::Channel; + +const SCREEN_TYPE: AlertScreen = AlertScreen::UpdateAvailable; + +#[derive(Serialize, Deserialize, PartialEq, Clone)] +enum Highlight { + Channel(usize), + Dismiss, +} + +impl Highlight { + fn next(&self, num_channels: usize) -> Self { + if num_channels == 0 { + return Self::Dismiss; + } + + match self { + Self::Channel(ch) if (ch + 1) >= num_channels => Self::Dismiss, + Self::Channel(ch) => Self::Channel(ch + 1), + Self::Dismiss => Self::Channel(0), + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +struct Selection { + channels: Vec, + highlight: Highlight, +} + +impl Selection { + fn new() -> Self { + Self { + channels: Vec::new(), + highlight: Highlight::Dismiss, + } + } + + fn have_update(&self) -> bool { + !self.channels.is_empty() + } + + fn update_channels(&self, channels: Vec) -> Option { + let channels: Vec = channels + .into_iter() + .filter(|ch| { + ch.bundle + .as_ref() + .map(|b| b.newer_than_installed) + .unwrap_or(false) + }) + .collect(); + + if channels == self.channels { + return None; + } + + let highlight = match self.highlight { + Highlight::Channel(index) => { + let name = &self.channels[index].name; + + match channels.iter().position(|ch| &ch.name == name) { + Some(idx) => Highlight::Channel(idx), + None => Highlight::Dismiss, + } + } + Highlight::Dismiss => Highlight::Dismiss, + }; + + Some(Self { + channels, + highlight, + }) + } + + fn toggle(self) -> Option { + let num_channels = self.channels.len(); + let highlight = self.highlight.next(num_channels); + + if highlight != self.highlight { + Some(Self { + channels: self.channels, + highlight, + }) + } else { + None + } + } + + fn perform(&self, alerts: &Arc>, install: &Arc>) { + match self.highlight { + Highlight::Channel(ch) => install.set(self.channels[ch].url.clone()), + Highlight::Dismiss => alerts.deassert(SCREEN_TYPE), + } + } +} + +pub struct UpdateAvailableScreen { + selection: Arc>, +} + +struct Active { + widgets: WidgetContainer, + alerts: Arc>, + install: Arc>, + selection: Arc>, +} + +impl UpdateAvailableScreen { + pub fn new(alerts: &Arc>, channels: &Arc>>) -> Self { + let (mut channels_events, _) = channels.clone().subscribe_unbounded(); + let alerts = alerts.clone(); + let selection = Topic::anonymous(Some(Selection::new())); + let selection_task = selection.clone(); + + spawn(async move { + while let Some(channels) = channels_events.next().await { + selection_task.modify(|sel| sel.unwrap().update_channels(channels)); + + if selection_task.try_get().unwrap().have_update() { + alerts.assert(SCREEN_TYPE); + } else { + alerts.deassert(SCREEN_TYPE); + } + } + }); + + Self { selection } + } +} + +impl ActivatableScreen for UpdateAvailableScreen { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } + + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + let mut widgets = WidgetContainer::new(display); + + widgets.push(|display| { + DynamicWidget::new( + self.selection.clone(), + display, + Box::new(move |sel, target| { + let ui_text_style: MonoTextStyle = + MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); + + let num_updates = sel.channels.len(); + + let header = match num_updates { + 0 => "There are no updates\navailable.", + 1 => "There is an update\navailable.", + _ => "There are updates\navailable.", + }; + + Text::new(header, row_anchor(0), ui_text_style) + .draw(target) + .unwrap(); + + let sel_idx = match sel.highlight { + Highlight::Channel(idx) => idx, + Highlight::Dismiss => num_updates, + }; + + for (idx, ch) in sel.channels.iter().enumerate() { + let text = format!( + "{} Install {}", + if idx == sel_idx { ">" } else { " " }, + ch.display_name, + ); + + Text::new(&text, row_anchor(idx as u8 + 3), ui_text_style) + .draw(target) + .unwrap(); + } + + let dismiss = match sel.highlight { + Highlight::Channel(_) => " Dismiss", + Highlight::Dismiss => "> Dismiss", + }; + + Text::new(dismiss, row_anchor(num_updates as u8 + 3), ui_text_style) + .draw(target) + .unwrap(); + + // Don't bother tracking the actual bounding box and instead + // clear the whole screen on update. + Some(target.bounding_box()) + }), + ) + }); + + let alerts = ui.alerts.clone(); + let install = ui.res.rauc.install.clone(); + let selection = self.selection.clone(); + + Box::new(Active { + widgets, + alerts, + install, + selection, + }) + } +} + +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } + + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } + + fn input(&mut self, ev: InputEvent) { + match ev { + InputEvent::NextScreen => {} + InputEvent::ToggleAction(_) => { + self.selection + .modify(|selection| selection.and_then(|s| s.toggle())); + } + InputEvent::PerformAction(_) => { + if let Some(selection) = self.selection.try_get() { + selection.perform(&self.alerts, &self.install); + } + } + } + } +} diff --git a/src/ui/screens/update_installation.rs b/src/ui/screens/update_installation.rs new file mode 100644 index 00000000..83e7a0e3 --- /dev/null +++ b/src/ui/screens/update_installation.rs @@ -0,0 +1,146 @@ +// This file is part of tacd, the LXA TAC system daemon +// Copyright (C) 2022 Pengutronix e.K. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use async_std::prelude::*; +use async_std::sync::Arc; +use async_std::task::spawn; +use async_trait::async_trait; +use embedded_graphics::prelude::*; + +use super::widgets::*; +use super::{ + ActivatableScreen, ActiveScreen, AlertList, AlertScreen, Alerter, Display, InputEvent, Screen, + Ui, +}; +use crate::broker::Topic; +use crate::dbus::rauc::Progress; + +const SCREEN_TYPE: AlertScreen = AlertScreen::UpdateInstallation; +const REBOOT_MESSAGE: &str = "There is a newer +OS install in +another slot. + +Long Press to +boot it. +"; + +pub struct UpdateInstallationScreen; + +struct Active { + widgets: WidgetContainer, +} + +impl UpdateInstallationScreen { + pub fn new( + alerts: &Arc>, + operation: &Arc>, + reboot_message: &Arc>>, + should_reboot: &Arc>, + ) -> Self { + let (mut operation_events, _) = operation.clone().subscribe_unbounded(); + let alerts = alerts.clone(); + + spawn(async move { + while let Some(ev) = operation_events.next().await { + match ev.as_str() { + "installing" => alerts.assert(SCREEN_TYPE), + _ => alerts.deassert(SCREEN_TYPE), + }; + } + }); + + let (mut should_reboot_events, _) = should_reboot.clone().subscribe_unbounded(); + let reboot_message = reboot_message.clone(); + + spawn(async move { + while let Some(should_reboot) = should_reboot_events.next().await { + if should_reboot { + reboot_message.set(Some(REBOOT_MESSAGE.to_string())) + } + } + }); + + Self + } +} + +impl ActivatableScreen for UpdateInstallationScreen { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } + + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + let mut widgets = WidgetContainer::new(display); + + widgets.push(|display| { + DynamicWidget::text_center( + ui.res.rauc.progress.clone(), + display, + Point::new(120, 100), + Box::new(|progress: &Progress| { + let (_, text) = progress.message.split_whitespace().fold( + (0, String::new()), + move |(mut ll, mut text), word| { + let word_len = word.len(); + + if (ll + word_len) > 15 { + text.push('\n'); + ll = 0; + } else { + text.push(' '); + ll += 1; + } + + text.push_str(word); + ll += word_len; + + (ll, text) + }, + ); + + text + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::bar( + ui.res.rauc.progress.clone(), + display, + Point::new(20, 180), + 200, + 18, + Box::new(|progress: &Progress| progress.percentage as f32 / 100.0), + ) + }); + + Box::new(Active { widgets }) + } +} + +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Alert(SCREEN_TYPE) + } + + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } + + fn input(&mut self, _ev: InputEvent) {} +} diff --git a/src/ui/screens/usb.rs b/src/ui/screens/usb.rs index 99c896ab..aaaa979b 100644 --- a/src/ui/screens/usb.rs +++ b/src/ui/screens/usb.rs @@ -15,22 +15,21 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -use async_std::prelude::*; use async_std::sync::Arc; -use async_std::task::spawn; use async_trait::async_trait; - use embedded_graphics::{ mono_font::MonoTextStyle, pixelcolor::BinaryColor, prelude::*, text::Text, }; -use super::buttons::*; use super::widgets::*; -use super::{draw_border, row_anchor, MountableScreen, Screen, Ui}; -use crate::broker::{Native, SubscriptionHandle, Topic}; +use super::{ + draw_border, row_anchor, ActivatableScreen, ActiveScreen, Display, InputEvent, NormalScreen, + Screen, Ui, +}; +use crate::broker::Topic; use crate::measurement::Measurement; -const SCREEN_TYPE: Screen = Screen::Usb; +const SCREEN_TYPE: NormalScreen = NormalScreen::Usb; const CURRENT_LIMIT_PER_PORT: f32 = 0.5; const CURRENT_LIMIT_TOTAL: f32 = 0.7; const OFFSET_INDICATOR: Point = Point::new(92, -10); @@ -39,34 +38,41 @@ const WIDTH_BAR: u32 = 90; const HEIGHT_BAR: u32 = 18; pub struct UsbScreen { - highlighted: Arc>, - widgets: Vec>, - buttons_handle: Option>, + highlighted: Arc>, } impl UsbScreen { pub fn new() -> Self { Self { highlighted: Topic::anonymous(Some(0)), - widgets: Vec::new(), - buttons_handle: None, } } } -#[async_trait] -impl MountableScreen for UsbScreen { - fn is_my_type(&self, screen: Screen) -> bool { - screen == SCREEN_TYPE +struct Active { + widgets: WidgetContainer, + port_enables: [Arc>; 3], + highlighted: Arc>, +} + +impl ActivatableScreen for UsbScreen { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) } - async fn mount(&mut self, ui: &Ui) { - draw_border("USB Host", SCREEN_TYPE, &ui.draw_target).await; + fn activate(&mut self, ui: &Ui, display: Display) -> Box { + draw_border("USB Host", SCREEN_TYPE, &display); + + let ui_text_style: MonoTextStyle = + MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); + + display.with_lock(|target| { + Text::new("Total", row_anchor(0), ui_text_style) + .draw(target) + .unwrap(); + }); - self.widgets.push(Box::new(DynamicWidget::locator( - ui.locator_dance.clone(), - ui.draw_target.clone(), - ))); + let mut widgets = WidgetContainer::new(display); let ports = [ ( @@ -89,109 +95,94 @@ impl MountableScreen for UsbScreen { ), ]; - { - let mut draw_target = ui.draw_target.lock().await; - - let ui_text_style: MonoTextStyle = - MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); - - Text::new("Total", row_anchor(0), ui_text_style) - .draw(&mut *draw_target) - .unwrap(); - } - - self.widgets.push(Box::new(DynamicWidget::bar( - ui.res.adc.usb_host_curr.topic.clone(), - ui.draw_target.clone(), - row_anchor(0) + OFFSET_BAR, - WIDTH_BAR, - HEIGHT_BAR, - Box::new(|meas: &Measurement| meas.value / CURRENT_LIMIT_TOTAL), - ))); + widgets.push(|display| { + DynamicWidget::bar( + ui.res.adc.usb_host_curr.topic.clone(), + display, + row_anchor(0) + OFFSET_BAR, + WIDTH_BAR, + HEIGHT_BAR, + Box::new(|meas: &Measurement| meas.value / CURRENT_LIMIT_TOTAL), + ) + }); for (idx, name, status, current) in ports { let anchor_text = row_anchor(idx + 2); let anchor_indicator = anchor_text + OFFSET_INDICATOR; let anchor_bar = anchor_text + OFFSET_BAR; - self.widgets.push(Box::new(DynamicWidget::text( - self.highlighted.clone(), - ui.draw_target.clone(), - anchor_text, - Box::new(move |highlight: &u8| { - format!("{} {}", if *highlight == idx { ">" } else { " " }, name,) - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::indicator( - status.clone(), - ui.draw_target.clone(), - anchor_indicator, - Box::new(|state: &bool| match *state { - true => IndicatorState::On, - false => IndicatorState::Off, - }), - ))); - - self.widgets.push(Box::new(DynamicWidget::bar( - current.clone(), - ui.draw_target.clone(), - anchor_bar, - WIDTH_BAR, - HEIGHT_BAR, - Box::new(|meas: &Measurement| meas.value / CURRENT_LIMIT_PER_PORT), - ))); + widgets.push(|display| { + DynamicWidget::text( + self.highlighted.clone(), + display, + anchor_text, + Box::new(move |highlight| { + let hl = *highlight == (idx as usize); + format!("{} {}", if hl { ">" } else { " " }, name) + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::indicator( + status.clone(), + display, + anchor_indicator, + Box::new(|state: &bool| match *state { + true => IndicatorState::On, + false => IndicatorState::Off, + }), + ) + }); + + widgets.push(|display| { + DynamicWidget::bar( + current.clone(), + display, + anchor_bar, + WIDTH_BAR, + HEIGHT_BAR, + Box::new(|meas: &Measurement| meas.value / CURRENT_LIMIT_PER_PORT), + ) + }); } - let (mut button_events, buttons_handle) = ui.buttons.clone().subscribe_unbounded(); let port_enables = [ ui.res.usb_hub.port1.powered.clone(), ui.res.usb_hub.port2.powered.clone(), ui.res.usb_hub.port3.powered.clone(), ]; - let port_highlight = self.highlighted.clone(); - let screen = ui.screen.clone(); - - spawn(async move { - while let Some(ev) = button_events.next().await { - let highlighted = port_highlight.get().await; - let port = &port_enables[highlighted as usize]; - - match ev { - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Long, - src: _, - } => { - port.modify(|prev| Some(!prev.unwrap_or(true))); - } - ButtonEvent::Release { - btn: Button::Lower, - dur: PressDuration::Short, - src: _, - } => { - port_highlight.set((highlighted + 1) % 3); - } - ButtonEvent::Release { - btn: Button::Upper, - dur: _, - src: _, - } => screen.set(SCREEN_TYPE.next()), - ButtonEvent::Press { btn: _, src: _ } => {} - } - } - }); + let highlighted = self.highlighted.clone(); + + let active = Active { + widgets, + port_enables, + highlighted, + }; - self.buttons_handle = Some(buttons_handle); + Box::new(active) } +} - async fn unmount(&mut self) { - if let Some(handle) = self.buttons_handle.take() { - handle.unsubscribe(); - } +#[async_trait] +impl ActiveScreen for Active { + fn my_type(&self) -> Screen { + Screen::Normal(SCREEN_TYPE) + } - for mut widget in self.widgets.drain(..) { - widget.unmount().await + async fn deactivate(mut self: Box) -> Display { + self.widgets.destroy().await + } + + fn input(&mut self, ev: InputEvent) { + let highlighted = self.highlighted.try_get().unwrap_or(0); + + match ev { + InputEvent::NextScreen => {} + InputEvent::ToggleAction(_) => { + self.highlighted.set((highlighted + 1) % 3); + } + InputEvent::PerformAction(_) => self.port_enables[highlighted].toggle(false), } } } diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index ce7f92c6..8df6a446 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -15,8 +15,9 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +use anyhow::anyhow; use async_std::prelude::*; -use async_std::sync::{Arc, Mutex}; +use async_std::sync::Arc; use async_std::task::{spawn, JoinHandle}; use async_trait::async_trait; use embedded_graphics::{ @@ -29,8 +30,8 @@ use embedded_graphics::{ use serde::de::DeserializeOwned; use serde::Serialize; -use super::FramebufferDrawTarget; use crate::broker::{Native, SubscriptionHandle, Topic}; +use crate::ui::display::{Display, DisplayExclusive}; pub const UI_TEXT_FONT: MonoFont = FONT_10X20; @@ -41,8 +42,47 @@ pub enum IndicatorState { Unkown, } -pub trait DrawFn: Fn(&T, &mut FramebufferDrawTarget) -> Option {} -impl DrawFn for U where U: Fn(&T, &mut FramebufferDrawTarget) -> Option {} +pub struct WidgetContainer { + display: Arc, + widgets: Vec>, +} + +impl WidgetContainer { + pub fn new(display: Display) -> Self { + Self { + display: Arc::new(display), + widgets: Vec::new(), + } + } + + pub fn push(&mut self, create_fn: F) + where + F: FnOnce(Arc) -> W, + W: AnyWidget + 'static, + { + let display = self.display.clone(); + let widget = create_fn(display); + self.widgets.push(Box::new(widget)); + } + + pub async fn destroy(self) -> Display { + for widget in self.widgets.into_iter() { + widget.unmount().await; + } + + Arc::try_unwrap(self.display) + .map_err(|e| { + anyhow!( + "Failed to re-unite display references. Have {} references instead of 1", + Arc::strong_count(&e) + ) + }) + .unwrap() + } +} + +pub trait DrawFn: Fn(&T, &mut DisplayExclusive) -> Option {} +impl DrawFn for U where U: Fn(&T, &mut DisplayExclusive) -> Option {} pub trait IndicatorFormatFn: Fn(&T) -> IndicatorState {} impl IndicatorFormatFn for U where U: Fn(&T) -> IndicatorState {} @@ -54,7 +94,8 @@ pub trait FractionFormatFn: Fn(&T) -> f32 {} impl FractionFormatFn for U where U: Fn(&T) -> f32 {} pub struct DynamicWidget { - handles: Option<(SubscriptionHandle, JoinHandle<()>)>, + subscription_handle: SubscriptionHandle, + join_handle: JoinHandle>, } impl DynamicWidget { @@ -74,30 +115,33 @@ impl DynamicWid /// The widget system takes care of clearing this area before redrawing. pub fn new( topic: Arc>, - target: Arc>, + display: Arc, draw_fn: Box + Sync + Send>, ) -> Self { - let (mut rx, sub_handle) = topic.subscribe_unbounded(); + let (mut rx, subscription_handle) = topic.subscribe_unbounded(); let join_handle = spawn(async move { let mut prev_bb: Option = None; while let Some(val) = rx.next().await { - let mut target = target.lock().await; - - if let Some(bb) = prev_bb.take() { - // Clear the bounding box by painting it black - bb.into_styled(PrimitiveStyle::with_fill(BinaryColor::Off)) - .draw(&mut *target) - .unwrap(); - } + display.with_lock(|target| { + if let Some(bb) = prev_bb.take() { + // Clear the bounding box by painting it black + bb.into_styled(PrimitiveStyle::with_fill(BinaryColor::Off)) + .draw(&mut *target) + .unwrap(); + } - prev_bb = draw_fn(&val, &mut *target); + prev_bb = draw_fn(&val, &mut *target); + }); } + + display }); Self { - handles: Some((sub_handle, join_handle)), + subscription_handle, + join_handle, } } @@ -107,7 +151,7 @@ impl DynamicWid /// the fraction of the graph to fill. pub fn bar( topic: Arc>, - target: Arc>, + display: Arc, anchor: Point, width: u32, height: u32, @@ -115,7 +159,7 @@ impl DynamicWid ) -> Self { Self::new( topic, - target, + display, Box::new(move |msg, target| { let val = format_fn(msg).clamp(0.0, 1.0); let fill_width = ((width as f32) * val) as u32; @@ -141,13 +185,13 @@ impl DynamicWid /// Draw an indicator bubble in an "On", "Off" or "Error" state pub fn indicator( topic: Arc>, - target: Arc>, + display: Arc, anchor: Point, format_fn: Box + Sync + Send>, ) -> Self { Self::new( topic, - target, + display, Box::new(move |msg, target| { let ui_text_style: MonoTextStyle = MonoTextStyle::new(&UI_TEXT_FONT, BinaryColor::On); @@ -207,14 +251,14 @@ impl DynamicWid /// Draw self-updating text with configurable alignment pub fn text_aligned( topic: Arc>, - target: Arc>, + display: Arc, anchor: Point, format_fn: Box + Sync + Send>, alignment: Alignment, ) -> Self { Self::new( topic, - target, + display, Box::new(move |msg, target| { let text = format_fn(msg); @@ -235,57 +279,27 @@ impl DynamicWid /// Draw self-updating left aligned text pub fn text( topic: Arc>, - target: Arc>, + display: Arc, anchor: Point, format_fn: Box + Sync + Send>, ) -> Self { - Self::text_aligned(topic, target, anchor, format_fn, Alignment::Left) + Self::text_aligned(topic, display, anchor, format_fn, Alignment::Left) } /// Draw self-updating centered text pub fn text_center( topic: Arc>, - target: Arc>, + display: Arc, anchor: Point, format_fn: Box + Sync + Send>, ) -> Self { - Self::text_aligned(topic, target, anchor, format_fn, Alignment::Center) - } -} - -impl DynamicWidget { - /// Draw an animated locator widget at the side of the screen - /// (if the locator is active). - pub fn locator(topic: Arc>, target: Arc>) -> Self { - Self::new( - topic, - target, - Box::new(move |val, target| { - let size = 128 - (*val - 32).abs() * 4; - - if size != 0 { - let bounding = Rectangle::with_center( - Point::new(240 - 5, 120), - Size::new(10, size as u32), - ); - - bounding - .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)) - .draw(&mut *target) - .unwrap(); - - Some(bounding) - } else { - None - } - }), - ) + Self::text_aligned(topic, display, anchor, format_fn, Alignment::Center) } } #[async_trait] pub trait AnyWidget: Send + Sync { - async fn unmount(&mut self); + async fn unmount(self: Box) -> Arc; } #[async_trait] @@ -294,10 +308,8 @@ impl AnyWidget for Dyna /// /// This has to be async, which is why it can not be performed by /// implementing the Drop trait. - async fn unmount(&mut self) { - if let Some((sh, jh)) = self.handles.take() { - sh.unsubscribe(); - jh.await; - } + async fn unmount(mut self: Box) -> Arc { + self.subscription_handle.unsubscribe(); + self.join_handle.await } } diff --git a/src/usb_hub.rs b/src/usb_hub.rs index 9bf59735..1aad7aef 100644 --- a/src/usb_hub.rs +++ b/src/usb_hub.rs @@ -204,16 +204,7 @@ fn handle_port(bb: &mut BrokerBuilder, name: &'static str, base: &'static str) - _ => panic!("Read unexpected value for USB port disable state"), }; - powered.modify(|prev| { - let should_set = prev - .map(|prev_powered| prev_powered != is_powered) - .unwrap_or(true); - - match should_set { - true => Some(is_powered), - false => None, - } - }); + powered.set_if_changed(is_powered); } let id_product = read_to_string(&id_product_path).ok(); @@ -231,16 +222,7 @@ fn handle_port(bb: &mut BrokerBuilder, name: &'static str, base: &'static str) - product: pro.trim().to_string(), }); - device.modify(|prev| { - let should_set = prev - .map(|prev_dev_info| prev_dev_info != dev_info) - .unwrap_or(true); - - match should_set { - true => Some(dev_info), - false => None, - } - }); + device.set_if_changed(dev_info); sleep(POLL_INTERVAL).await; } diff --git a/web/src/App.tsx b/web/src/App.tsx index 554805a7..b57af7d6 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -16,6 +16,9 @@ // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import React from "react"; + +import Alert from "@cloudscape-design/components/alert"; +import Button from "@cloudscape-design/components/button"; import AppLayout from "@cloudscape-design/components/app-layout"; import SideNavigation from "@cloudscape-design/components/side-navigation"; @@ -26,7 +29,13 @@ import "@cloudscape-design/global-styles/index.css"; import "./App.css"; import { useMqttSubscription } from "./mqtt"; -import { ApiPickerButton } from "./MqttComponents"; +import { ApiPickerButton, MqttButton } from "./MqttComponents"; +import { + RebootNotification, + UpdateNotification, + ProgressNotification, + LocatorNotification, +} from "./TacComponents"; function Navigation() { const [activeHref, setActiveHref] = useState("#/"); @@ -89,12 +98,49 @@ function Navigation() { ]} />
+ + Find this TAC +
); } +function ConnectionNotification() { + const hostname = useMqttSubscription("/v1/tac/network/hostname"); + + return ( + window.location.reload()}>Reload + } + header="Connection Lost" + > + There is currently no connection to the TAC. Wait for the connection to be + re-established or reload the page. + + ); +} + +function Notifications() { + return ( + <> + + + + + + + ); +} + export default function App() { const [runningVersion, setRunningVersion] = useState(); const hostname = useMqttSubscription("/v1/tac/network/hostname"); @@ -131,6 +177,8 @@ export default function App() { return ( } + stickyNotifications={true} navigation={} content={} toolsHide={true} diff --git a/web/src/DashboardTac.tsx b/web/src/DashboardTac.tsx index 63838be7..6fe730b3 100644 --- a/web/src/DashboardTac.tsx +++ b/web/src/DashboardTac.tsx @@ -22,8 +22,8 @@ import Container from "@cloudscape-design/components/container"; import SpaceBetween from "@cloudscape-design/components/space-between"; import ColumnLayout from "@cloudscape-design/components/column-layout"; -import { MqttBox, MqttToggle, MqttButton } from "./MqttComponents"; -import { RaucContainer } from "./TacComponents"; +import { MqttBox, MqttButton } from "./MqttComponents"; +import { UpdateContainer } from "./TacComponents"; import { useEffect, useState } from "react"; @@ -166,47 +166,37 @@ export default function DashboardTac() { /> - Upper Button - - - Short press - - - Long press - - + Next Screen + + Next Screen + - Lower Button - - - Short press - - - Long press - - + Toggle Action + + Toggle Action + - Locator - Locator + Perform Action + + Perform Action + - + + ), }, - { - title: "Install Bundle", - description: "Install your vendored RAUC bundle", - content: , - }, { title: "Check Slot Status", description: "Make sure everything look correct", isOptional: true, - content: , + content: , }, { title: "Complete Setup", diff --git a/web/src/TacComponents.tsx b/web/src/TacComponents.tsx index 547c6e86..6d5247f1 100644 --- a/web/src/TacComponents.tsx +++ b/web/src/TacComponents.tsx @@ -17,22 +17,21 @@ import { useEffect, useState, useRef } from "react"; +import Alert from "@cloudscape-design/components/alert"; import Box from "@cloudscape-design/components/box"; -import Button from "@cloudscape-design/components/button"; import Cards from "@cloudscape-design/components/cards"; +import Checkbox from "@cloudscape-design/components/checkbox"; import ColumnLayout from "@cloudscape-design/components/column-layout"; import Container from "@cloudscape-design/components/container"; import Form from "@cloudscape-design/components/form"; -import FormField from "@cloudscape-design/components/form-field"; import Header from "@cloudscape-design/components/header"; -import Input from "@cloudscape-design/components/input"; import ProgressBar from "@cloudscape-design/components/progress-bar"; import SpaceBetween from "@cloudscape-design/components/space-between"; import Spinner from "@cloudscape-design/components/spinner"; -import StatusIndicator from "@cloudscape-design/components/status-indicator"; +import Table from "@cloudscape-design/components/table"; import { MqttButton } from "./MqttComponents"; -import { useMqttSubscription, useMqttState } from "./mqtt"; +import { useMqttSubscription } from "./mqtt"; type RootfsSlot = { activated_count: string; @@ -90,7 +89,28 @@ enum RaucInstallStep { Done, } -export function RaucSlotStatus() { +type Duration = { + secs: number; + nanos: number; +}; + +type UpstreamBundle = { + compatible: string; + version: string; + newer_than_installed: boolean; +}; + +type Channel = { + name: string; + display_name: string; + description: string; + url: string; + polling_interval?: Duration; + enabled: boolean; + bundle?: UpstreamBundle; +}; + +export function SlotStatus() { const slot_status = useMqttSubscription("/v1/tac/update/slots"); if (slot_status === undefined) { @@ -108,32 +128,6 @@ export function RaucSlotStatus() { return ( - - Bootloader Slot - - } - > - - - Status - {slot_status.bootloader_0.status} - - - Build Date - {slot_status.bootloader_0.bundle_build} - - - Installation Date - {slot_status.bootloader_0.installed_timestamp} - - - - + + + Bootloader Slot + + } + > + + + Status + {slot_status.bootloader_0.status} + + + Build Date + {slot_status.bootloader_0.bundle_build} + + + Installation Date + {slot_status.bootloader_0.installed_timestamp} + + + ); } } -export function RaucInstall() { - // eslint-disable-next-line - const [_install_settled, _install_payload, triggerInstall] = - useMqttState("/v1/tac/update/install"); +export function UpdateChannels() { + const channels_topic = useMqttSubscription>( + "/v1/tac/update/channels" + ); + + const channels = channels_topic !== undefined ? channels_topic : []; + return ( + + Update Channels + + } + footer={ + + Reload + + } + /> + } + columnDefinitions={[ + { + id: "name", + header: "Name", + cell: (e) => e.display_name, + }, + { + id: "enabled", + header: "Enabled", + cell: (e) => , + }, + { + id: "description", + header: "Description", + cell: (e) => ( + + {e.description.split("\n").map((p) => ( + {p} + ))} + + ), + }, + { + id: "interval", + header: "Update Interval", + cell: (e) => { + if (!e.polling_interval) { + return "Never"; + } + + let seconds = e.polling_interval.secs; + let minutes = seconds / 60; + let hours = minutes / 60; + let days = hours / 24; + + if (Math.floor(days) === days) { + return days === 1 ? "Daily" : `Every ${days} Days`; + } + + if (Math.floor(hours) === hours) { + return hours === 1 ? "Hourly" : `Every ${hours} Hours`; + } + + if (Math.floor(days) === days) { + return minutes === 1 + ? "Once a minute" + : `Every ${minutes} Minutes`; + } + + return `Every ${seconds} Seconds`; + }, + }, + { + id: "upgrade", + header: "Upgrade", + cell: (e) => { + if (!e.enabled) { + return "Not enabled"; + } + + if (!e.bundle) { + return ; + } + + if (!e.bundle.newer_than_installed) { + return "Up to date"; + } + + return ( + + Upgrade + + ); + }, + }, + ]} + items={channels} + sortingDisabled + trackBy="name" + /> + ); +} + +export function ProgressNotification() { const operation = useMqttSubscription("/v1/tac/update/operation"); const progress = useMqttSubscription("/v1/tac/update/progress"); const last_error = useMqttSubscription("/v1/tac/update/last_error"); - const [installUrl, setInstallUrl] = useState(""); const [installStep, setInstallStep] = useState(RaucInstallStep.Idle); const prev_operation = useRef(undefined); @@ -210,32 +342,6 @@ export function RaucInstall() { let inner = null; - if (installStep === RaucInstallStep.Idle) { - inner = ( - { - e.preventDefault(); - triggerInstall(installUrl); - setInstallUrl(""); - }} - > - Install}> - - setInstallUrl(detail.value)} - value={installUrl} - placeholder="https://some-host.example/bundle.raucb" - /> - - - - ); - } - if (installStep === RaucInstallStep.Installing) { let valid = progress !== undefined; let value = progress === undefined ? 0 : progress.percentage; @@ -252,55 +358,52 @@ export function RaucInstall() { } if (installStep === RaucInstallStep.Done) { - if (last_error === undefined || last_error === "") { + if (last_error !== undefined && last_error !== "") { inner = ( -
- Reboot - - } - > - Success - Bundle installation finished successfully - - ); - } else { - inner = ( -
setInstallStep(RaucInstallStep.Idle)} - > - Ok - - } - > - Failure - Bundle installation failed: {last_error} - + ); } } return ( - - Update - - } + {inner} - + + ); +} + +export function RebootNotification() { + const should_reboot = useMqttSubscription( + "/v1/tac/update/should_reboot" + ); + + return ( + + Reboot + + } + header="Reboot into other slot" + > + There is a newer operating system bundle installed in the other boot slot. + Reboot now to use it. + ); } -export function RaucContainer() { +export function UpdateContainer() { return ( - - + + ); } + +export function UpdateNotification() { + const channels = useMqttSubscription>( + "/v1/tac/update/channels" + ); + + let updates = []; + + if (channels !== undefined) { + for (let ch of channels) { + if (ch.enabled && ch.bundle && ch.bundle.newer_than_installed) { + updates.push(ch); + } + } + } + + const install_buttons = updates.map((u) => ( + + Install new {u.display_name} bundle + + )); + + let text = + "There is a new operating system update available for installation"; + + if (updates.length > 1) { + text = + "There are new operating system updates available available for installation"; + } + + return ( + 0} + action={{install_buttons}} + header="Update your LXA TAC" + > + {text} + + ); +} + +export function LocatorNotification() { + const locator = useMqttSubscription("/v1/tac/display/locator"); + + return ( + + Found it! + + } + header="Find this TAC" + > + Someone is looking for this TAC. + + ); +}