Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 139 additions & 1 deletion src/callbacks/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::sync::mpsc::{self, Receiver, Sender};

use qtbridge::{QmlMethodInvoker, QmlObject, invoke_method};

use penumbra_mtk::hacc::LockState;
use penumbra_mtk::hacc::{BootPartition, IntoBytes, LockState, OFFSET_SLOT_SUFFIX};
use penumbra_mtk::port::{MtkPort, PortBackend, PortType};

use crate::logs;
Expand Down Expand Up @@ -43,6 +43,13 @@ pub(crate) enum DeviceCommand {
bootmode: penumbra_mtk::BootMode,
invoker: QmlMethodInvoker,
},
RequestGetActiveSlot {
invoker: QmlMethodInvoker,
},
RequestSetActiveSlot {
slot: BootPartition,
invoker: QmlMethodInvoker,
},
}

/// Spawns the device worker thread and returns the command sender. The worker
Expand Down Expand Up @@ -155,6 +162,31 @@ fn worker_loop(rx: Receiver<DeviceCommand>) {
);
}
}
DeviceCommand::RequestGetActiveSlot { invoker } => {
let slot = match device.as_mut() {
Some(dev) => dev
.get_bootctrl()
.ok()
.map(|bootctrl| format!("{:?}", bootctrl.get_active_slot()))
.unwrap_or_default(),

None => String::new(),
};

invoke_method!(invoker, "activeSlotLoaded", slot);
}
DeviceCommand::RequestSetActiveSlot { slot, invoker } => {
if let Some(dev) = device.as_mut() {
set_active_slot_on_worker(dev, slot, &invoker);
} else {
invoke_method!(
invoker,
"slotSwitchFinished",
false,
"No device connected".to_string()
);
}
}
}
}
}
Expand Down Expand Up @@ -635,6 +667,9 @@ pub fn disconnect_device(state: &mut AppState) {
state.connection_error.clear();
state.connection_error_changed();

state.active_slot.clear();
state.active_slot_changed();

logs::info(&mut state.logs, "Device disconnected");
state.logs_changed();

Expand Down Expand Up @@ -840,3 +875,106 @@ pub fn reboot_to_mode(state: &mut AppState, bootmode: penumbra_mtk::BootMode) {

let _ = tx.send(DeviceCommand::RebootToMode { bootmode, invoker });
}

pub fn get_active_slot(state: &mut AppState) {
let invoker = state.get_qml_method_invoker();

let tx = { state.device_tx.lock().unwrap().clone() };
let Some(tx) = tx else {
state.active_slot_loaded(String::new());
return;
};

let _ = tx.send(DeviceCommand::RequestGetActiveSlot { invoker });
}

fn set_active_slot_on_worker(
dev: &mut penumbra_mtk::Device<'static, PortType>,
slot: BootPartition,
invoker: &QmlMethodInvoker,
) {
let name = format!("{slot:?}");

let log = |msg: String| {
invoke_method!(invoker, "appendLog", msg.clone());
invoke_method!(invoker, "actionProgress", msg);
};

log("[Switch slot] Acquiring device handle...".to_string());

let mut bootctrl = match dev.get_bootctrl() {
Ok(b) => b,
Err(e) => {
let msg = format!(
"Failed to switch the active slot: {e}. The device may not support A/B slots."
);
log(format!("[Switch slot] ERROR: {msg}"));
invoke_method!(invoker, "slotSwitchFinished", false, msg);
return;
}
};

if bootctrl.get_active_slot() == slot {
log(format!("[Switch slot] Active slot is already {name}."));
invoke_method!(
invoker,
"slotSwitchFinished",
true,
format!("Active slot is already {name}.")
);
return;
}

bootctrl.set_active_slot(slot);

// Boot control sits at a fixed offset in misc.
let bytes = bootctrl.as_bytes();
let mut data = vec![0u8; OFFSET_SLOT_SUFFIX + bytes.len()];
data[OFFSET_SLOT_SUFFIX..].copy_from_slice(bytes);

if let Err(e) = dev.write_partition("misc", data.len(), &data[..], |_, _| {}) {
let msg = format!("Failed to write the boot control to 'misc': {e}.");
log(format!("[Switch slot] ERROR: {msg}"));
invoke_method!(invoker, "slotSwitchFinished", false, msg);
return;
}

// get_bootctrl() serves a cached copy, so store the slot we just wrote;
// otherwise the next toggle reads the old one and short-circuits above.
dev.devinfo().set_bootctrl(bootctrl);

log(format!("[Switch slot] Active slot set to {name}."));

// Update the badge before ending the dialog's wait.
invoke_method!(invoker, "activeSlotLoaded", name.clone());
invoke_method!(
invoker,
"slotSwitchFinished",
true,
format!("Active slot set to {name}.")
);
}

pub fn set_active_slot(state: &mut AppState, slot: BootPartition) {
if !state.connected {
state.append_log("ERROR: No device connected".to_string());
return;
}

let invoker = state.get_qml_method_invoker();

let tx = match state.device_tx.lock().unwrap().as_ref() {
Some(tx) => tx.clone(),
None => {
invoke_method!(
invoker,
"slotSwitchFinished",
false,
"No device connection available".to_string()
);
return;
}
};

let _ = tx.send(DeviceCommand::RequestSetActiveSlot { slot, invoker });
}
36 changes: 36 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub struct AppState {
pub(crate) setup_command: String,
pub(crate) setup_installing: bool,
pub(crate) templates_cancel: Arc<AtomicBool>,
pub(crate) active_slot: String,
}

#[qobject(Singleton, ConvertToCamelCase)]
Expand Down Expand Up @@ -114,6 +115,12 @@ impl AppState {
Notify = setup_installing_changed
);

qproperty!(
"active_slot",
Member = active_slot,
Notify = active_slot_changed
);

#[qsignal]
pub(crate) fn page_changed(&mut self);

Expand Down Expand Up @@ -201,6 +208,9 @@ impl AppState {
#[qsignal]
pub(crate) fn reboot_to_mode_finished(&mut self, success: bool, message: String);

#[qsignal]
pub(crate) fn slot_switch_finished(&mut self, success: bool, message: String);

#[qsignal]
pub(crate) fn partition_progress(&mut self, message: String, percent: i32);

Expand All @@ -219,6 +229,9 @@ impl AppState {
#[qsignal]
pub(crate) fn scatter_file_failed(&mut self, message: String);

#[qsignal]
pub(crate) fn active_slot_changed(&mut self);

#[qslot]
fn request_get_templates(&mut self) {
callbacks::templates::request_get_templates(self);
Expand Down Expand Up @@ -368,6 +381,28 @@ impl AppState {

callbacks::device::reboot_to_mode(self, mode);
}

#[qslot]
fn request_get_active_slot(&mut self) {
callbacks::device::get_active_slot(self);
}

#[qslot]
fn request_set_active_slot(&mut self, slot: String) {
let slot = match slot.as_str() {
"A" => penumbra_mtk::hacc::BootPartition::A,
"B" => penumbra_mtk::hacc::BootPartition::B,
_ => panic!("unknown boot slot: {slot}"),
};

callbacks::device::set_active_slot(self, slot);
}

#[qslot]
pub(crate) fn active_slot_loaded(&mut self, slot: String) {
self.active_slot = slot;
self.active_slot_changed();
}
}

impl Default for AppState {
Expand Down Expand Up @@ -395,6 +430,7 @@ impl Default for AppState {
setup_command: String::new(),
setup_installing: false,
templates_cancel: Arc::new(AtomicBool::new(false)),
active_slot: String::new(),
}
}
}
3 changes: 3 additions & 0 deletions src/ui/Assets/slot-a-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/ui/Assets/slot-b-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion src/ui/Components/ModalDialog.qml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Popup {
property bool showCancel: true
property bool showConfirm: true

// Set false once an operation succeeds to block a second press; onOpened resets it.
property bool confirmEnabled: true

// Progress in 0..1 range. Negative hides the bar.
property real progress: -1
property string progressText: ""
Expand All @@ -50,6 +53,8 @@ Popup {

focus: true

onOpened: root.confirmEnabled = true

background: Rectangle {
color: Styles.surface
radius: Styles.radiusMedium
Expand Down Expand Up @@ -202,7 +207,7 @@ Popup {
UButton {
visible: root.showConfirm
text: root.confirmText
disabled: root.busy
disabled: root.busy || !root.confirmEnabled

onClicked: root.confirmed()
}
Expand Down
47 changes: 34 additions & 13 deletions src/ui/Components/Navigation/MainMenu.qml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import umbrage
import UmbrageKit 1.0
import UmbrageStyles 1.0
import Components 1.0

Expand All @@ -19,6 +20,8 @@ Item {
// `key` matches an entry in the caller-provided `actions`.
signal actionRequested(string key)

signal slotSwitchRequested

// Each entry: { key, section: "bootloader"|"flashing", text }
property var actions: []

Expand Down Expand Up @@ -174,24 +177,42 @@ Item {
}
}

Button {
Layout.preferredWidth: 42
Layout.preferredHeight: 48
RowLayout {

spacing: Styles.spacing

display: Button.IconOnly
UButton {
Layout.preferredWidth: 42
Layout.preferredHeight: 48
iconDisplay: Button.IconOnly

icon.source: "qrc:/assets/disconnect-icon.svg"
icon.color: Styles.surfaceForeground
icon.width: 20
icon.height: 20
visible: AppState.active_slot !== ""
icon.source: AppState.active_slot === "A" ? "qrc:/assets/slot-a-icon.svg" : "qrc:/assets/slot-b-icon.svg"
icon.color: Styles.surfaceForeground
icon.width: 20
icon.height: 20

background: Rectangle {
radius: Styles.radiusMedium
color: Styles.surfaceHigh
backgroundColor: Styles.surfaceHigh

onClicked: root.slotSwitchRequested()
}

onClicked: {
AppState.disconnectDevice();
UButton {
Layout.preferredWidth: 42
Layout.preferredHeight: 48

iconDisplay: Button.IconOnly

icon.source: "qrc:/assets/disconnect-icon.svg"
icon.color: Styles.surfaceForeground
icon.width: 20
icon.height: 20

backgroundColor: Styles.surfaceHigh

onClicked: {
AppState.disconnectDevice();
}
}
}
}
Expand Down
Loading
Loading