From 532581a24657349d0f7fbd305dcb8af954da945b Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 23 Jul 2023 12:22:24 +0200 Subject: [PATCH] ngui: display poweroff progress during system shutdown this greatly improves UX during shutdown: no need to guess at which point it's ok to unplug the power cord. the commit also includes a GUI playground. a sort of a fake daemon which sends hardcoded messages to ngui. useful for manual debugging and trying different UX scenarious. --- build.zig | 14 +++- src/ngui.zig | 89 ++++++++++++++++-------- src/test/guiplay.zig | 158 +++++++++++++++++++++++++++++++++++++++++++ src/ui/lvgl.zig | 74 ++++++++++++++++---- src/ui/poweroff.zig | 136 +++++++++++++++++++++++++++++++++++++ src/ui/ui.zig | 32 +-------- src/ui/widget.zig | 2 +- 7 files changed, 432 insertions(+), 73 deletions(-) create mode 100644 src/test/guiplay.zig create mode 100644 src/ui/poweroff.zig diff --git a/build.zig b/build.zig index 4b00c5d..33aebc7 100644 --- a/build.zig +++ b/build.zig @@ -104,7 +104,7 @@ pub fn build(b: *std.build.Builder) void { nd_build_step.dependOn(&b.addInstallArtifact(nd).step); // default build - const build_all_step = b.step("all", "build everything"); + const build_all_step = b.step("all", "build nd and ngui"); build_all_step.dependOn(ngui_build_step); build_all_step.dependOn(nd_build_step); b.default_step.dependOn(build_all_step); @@ -123,6 +123,18 @@ pub fn build(b: *std.build.Builder) void { const test_step = b.step("test", "run tests"); test_step.dependOn(&tests.step); } + + { + const guiplay = b.addExecutable("guiplay", "src/test/guiplay.zig"); + guiplay.setTarget(target); + guiplay.setBuildMode(mode); + guiplay.step.dependOn(semver_step); + guiplay.addPackagePath("comm", "src/comm.zig"); + + const guiplay_build_step = b.step("guiplay", "build GUI playground"); + guiplay_build_step.dependOn(&b.addInstallArtifact(guiplay).step); + guiplay_build_step.dependOn(ngui_build_step); + } } const DriverTarget = enum { diff --git a/src/ngui.zig b/src/ngui.zig index b2c7799..c11f052 100644 --- a/src/ngui.zig +++ b/src/ngui.zig @@ -1,5 +1,6 @@ const buildopts = @import("build_options"); const std = @import("std"); +const os = std.os; const time = std.time; const comm = @import("comm.zig"); @@ -22,25 +23,26 @@ const logger = std.log.scoped(.ngui); extern "c" fn ui_update_network_status(text: [*:0]const u8, wifi_list: ?[*:0]const u8) void; -/// global heap allocator used throughout the gui program. +/// global heap allocator used throughout the GUI program. /// TODO: thread-safety? var gpa: std.mem.Allocator = undefined; /// the mutex must be held before any call reaching into lv_xxx functions. /// all nm_xxx functions assume it is the case since they are invoked from lvgl c code. var ui_mutex: std.Thread.Mutex = .{}; + /// the program runs until quit is true. -var quit: bool = false; +/// set from sighandler or on unrecoverable comm failure with the daemon. +var want_quit: bool = false; + var state: enum { active, // normal operational mode standby, // idling alert, // draw user attention; never go standby } = .active; -/// setting wakeup brings the screen back from sleep()'ing without waiting -/// for user action. -/// can be used by comms when an alert is received from the daemon, to draw -/// user attention. +/// by setting wakeup brings the screen back from sleep()'ing without waiting for user action. +/// can be used by comms when an alert is received from the daemon, to draw user attention. /// safe for concurrent use except wakeup.reset() is UB during another thread /// wakeup.wait()'ing or timedWait'ing. var wakeup = std.Thread.ResetEvent{}; @@ -68,12 +70,13 @@ export fn nm_check_idle_time(_: *lvgl.LvTimer) void { } } -/// initiate system shutdown leading to power off. +/// tells the daemon to initiate system shutdown leading to power off. +/// once all's done, the daemon will send a SIGTERM back to ngui. export fn nm_sys_shutdown() void { - logger.info("initiating system shutdown", .{}); const msg = comm.Message.poweroff; comm.write(gpa, stdout, msg) catch |err| logger.err("nm_sys_shutdown: {any}", .{err}); - quit = true; + state = .alert; // prevent screen sleep + wakeup.set(); // wake up from standby, if any } export fn nm_tab_settings_active() void { @@ -144,17 +147,16 @@ fn updateNetworkStatus(report: comm.Message.NetworkReport) !void { } } -/// reads messages from nd; loops indefinitely until program exit -fn commThread() void { +/// reads messages from nd. +/// loops indefinitely until program exit or comm returns EOS. +fn commThreadLoop() void { while (true) { commThreadLoopCycle() catch |err| logger.err("commThreadLoopCycle: {any}", .{err}); - ui_mutex.lock(); - const do_quit = quit; - ui_mutex.unlock(); - if (do_quit) { - return; - } + + std.atomic.spinLoopHint(); + time.sleep(1 * time.ns_per_ms); } + logger.info("exiting commThreadLoop", .{}); } fn commThreadLoopCycle() !void { @@ -162,8 +164,9 @@ fn commThreadLoopCycle() !void { if (err == error.EndOfStream) { // pointless to continue running if comms is broken. // a parent/supervisor is expected to restart ngui. + logger.err("comm.read: EOS", .{}); ui_mutex.lock(); - quit = true; + want_quit = true; ui_mutex.unlock(); } return err; @@ -175,6 +178,11 @@ fn commThreadLoopCycle() !void { .network_report => |report| { updateNetworkStatus(report) catch |err| logger.err("updateNetworkStatus: {any}", .{err}); }, + .poweroff_progress => |report| { + ui_mutex.lock(); + defer ui_mutex.unlock(); + ui.poweroff.updateStatus(report) catch |err| logger.err("poweroff.updateStatus: {any}", .{err}); + }, else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}), } } @@ -218,6 +226,20 @@ fn usage(prog: []const u8) !void { , .{prog}); } +/// handles sig TERM and INT: makes the program exit. +/// +/// note: must avoid locking ui_mutex within the handler since it may lead to +/// a race and a deadlock where the sighandler is invoked while the mutex is held +/// by the UI loop because a sighandler invocation interrupts main execution flow, +/// and so the mutex would then remain locked indefinitely. +fn sighandler(sig: c_int) callconv(.C) void { + logger.info("received signal {}", .{sig}); + switch (sig) { + os.SIG.INT, os.SIG.TERM => want_quit = true, + else => {}, + } +} + /// nakamochi UI program entry point. pub fn main() anyerror!void { // main heap allocator used through the lifetime of nd @@ -240,8 +262,16 @@ pub fn main() anyerror!void { }; // start comms with daemon in a seaparate thread. - const th = try std.Thread.spawn(.{}, commThread, .{}); - th.detach(); + _ = try std.Thread.spawn(.{}, commThreadLoop, .{}); + + // set up a sigterm handler for clean exit. + const sa = os.Sigaction{ + .handler = .{ .handler = sighandler }, + .mask = os.empty_sigset, + .flags = 0, + }; + try os.sigaction(os.SIG.INT, &sa, null); + try os.sigaction(os.SIG.TERM, &sa, null); // run idle timer indefinitely _ = lvgl.createTimer(nm_check_idle_time, 2000, null) catch |err| { @@ -249,16 +279,12 @@ pub fn main() anyerror!void { }; // main UI thread; must never block unless in idle/sleep mode - // TODO: handle sigterm - while (true) { + while (!want_quit) { ui_mutex.lock(); - var till_next_ms = lvgl.loopCycle(); - const do_quit = quit; + var till_next_ms = lvgl.loopCycle(); // UI loop const do_state = state; ui_mutex.unlock(); - if (do_quit) { - return; - } + if (do_state == .standby) { // go into a screen sleep mode due to no user activity wakeup.reset(); @@ -266,6 +292,7 @@ pub fn main() anyerror!void { logger.err("comm.write standby: {any}", .{err}); }; screen.sleep(&wakeup); + // wake up due to touch screen activity or wakeup event is set logger.info("waking up from sleep", .{}); ui_mutex.lock(); @@ -279,10 +306,14 @@ pub fn main() anyerror!void { ui_mutex.unlock(); continue; } + std.atomic.spinLoopHint(); - // sleep at least 1ms - time.sleep(@max(1, till_next_ms) * time.ns_per_ms); + time.sleep(@max(1, till_next_ms) * time.ns_per_ms); // sleep at least 1ms } + logger.info("main UI loop terminated", .{}); + + // not waiting for comm thread because it is terminated at program exit here + // anyway. } test "tick" { diff --git a/src/test/guiplay.zig b/src/test/guiplay.zig new file mode 100644 index 0000000..6082574 --- /dev/null +++ b/src/test/guiplay.zig @@ -0,0 +1,158 @@ +const std = @import("std"); +const time = std.time; +const os = std.os; + +const comm = @import("comm"); + +const logger = std.log.scoped(.play); +const stderr = std.io.getStdErr().writer(); + +var ngui_proc: std.ChildProcess = undefined; +var sigquit = false; + +fn sighandler(sig: c_int) callconv(.C) void { + logger.info("received signal {} (TERM={} INT={})", .{ sig, os.SIG.TERM, os.SIG.INT }); + switch (sig) { + os.SIG.INT, os.SIG.TERM => sigquit = true, + else => {}, + } +} + +fn fatal(comptime fmt: []const u8, args: anytype) noreturn { + stderr.print(fmt, args) catch {}; + if (fmt[fmt.len - 1] != '\n') { + stderr.writeByte('\n') catch {}; + } + std.process.exit(1); +} + +const Flags = struct { + ngui_path: ?[:0]const u8 = null, + + fn deinit(self: @This(), allocator: std.mem.Allocator) void { + if (self.ngui_path) |p| allocator.free(p); + } +}; + +fn parseArgs(gpa: std.mem.Allocator) !Flags { + var flags: Flags = .{}; + + var args = try std.process.ArgIterator.initWithAllocator(gpa); + defer args.deinit(); + const prog = args.next() orelse return error.NoProgName; + + var lastarg: enum { + none, + ngui_path, + } = .none; + while (args.next()) |a| { + switch (lastarg) { + .none => {}, + .ngui_path => { + flags.ngui_path = try gpa.dupeZ(u8, a); + lastarg = .none; + continue; + }, + } + if (std.mem.eql(u8, a, "-ngui")) { + lastarg = .ngui_path; + } else { + fatal("unknown arg name {s}", .{a}); + } + } + if (lastarg != .none) { + fatal("invalid arg: {s} requires a value", .{@tagName(lastarg)}); + } + + if (flags.ngui_path == null) { + const dir = std.fs.path.dirname(prog) orelse "/"; + flags.ngui_path = try std.fs.path.joinZ(gpa, &.{ dir, "ngui" }); + } + + return flags; +} + +pub fn main() !void { + var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; + defer if (gpa_state.deinit()) { + logger.err("memory leaks detected", .{}); + }; + const gpa = gpa_state.allocator(); + const flags = try parseArgs(gpa); + defer flags.deinit(gpa); + + ngui_proc = std.ChildProcess.init(&.{flags.ngui_path.?}, gpa); + ngui_proc.stdin_behavior = .Pipe; + ngui_proc.stdout_behavior = .Pipe; + ngui_proc.stderr_behavior = .Inherit; + ngui_proc.spawn() catch |err| { + fatal("unable to start ngui: {any}", .{err}); + }; + + const sa = os.Sigaction{ + .handler = .{ .handler = sighandler }, + .mask = os.empty_sigset, + .flags = 0, + }; + try os.sigaction(os.SIG.INT, &sa, null); + try os.sigaction(os.SIG.TERM, &sa, null); + + const uireader = ngui_proc.stdout.?.reader(); + const uiwriter = ngui_proc.stdin.?.writer(); + comm.write(gpa, uiwriter, .ping) catch |err| { + logger.err("comm.write ping: {any}", .{err}); + }; + + var poweroff = false; + while (!sigquit) { + std.atomic.spinLoopHint(); + time.sleep(100 * time.ns_per_ms); + if (poweroff) { + // GUI is not expected to send anything back at this point, + // so just loop until we're terminated by a SIGTERM (sigquit). + continue; + } + + const msg = comm.read(gpa, uireader) catch |err| { + logger.err("comm.read: {any}", .{err}); + continue; + }; + logger.debug("got ui msg tagged {s}", .{@tagName(msg)}); + switch (msg) { + .pong => { + logger.info("received pong from ngui", .{}); + }, + .poweroff => { + poweroff = true; + + logger.info("sending poweroff status1", .{}); + var s1: comm.Message.PoweroffProgress = .{ .services = &.{ + .{ .name = "lnd", .stopped = false, .err = null }, + .{ .name = "bitcoind", .stopped = false, .err = null }, + } }; + comm.write(gpa, uiwriter, .{ .poweroff_progress = s1 }) catch |err| logger.err("comm.write: {any}", .{err}); + + time.sleep(2 * time.ns_per_s); + logger.info("sending poweroff status2", .{}); + var s2: comm.Message.PoweroffProgress = .{ .services = &.{ + .{ .name = "lnd", .stopped = true, .err = null }, + .{ .name = "bitcoind", .stopped = false, .err = null }, + } }; + comm.write(gpa, uiwriter, .{ .poweroff_progress = s2 }) catch |err| logger.err("comm.write: {any}", .{err}); + + time.sleep(3 * time.ns_per_s); + logger.info("sending poweroff status3", .{}); + var s3: comm.Message.PoweroffProgress = .{ .services = &.{ + .{ .name = "lnd", .stopped = true, .err = null }, + .{ .name = "bitcoind", .stopped = true, .err = null }, + } }; + comm.write(gpa, uiwriter, .{ .poweroff_progress = s3 }) catch |err| logger.err("comm.write: {any}", .{err}); + }, + else => {}, + } + } + + logger.info("killing ngui", .{}); + const term = ngui_proc.kill(); + logger.info("ngui_proc.kill term: {any}", .{term}); +} diff --git a/src/ui/lvgl.zig b/src/ui/lvgl.zig index f61ff7e..77bada1 100644 --- a/src/ui/lvgl.zig +++ b/src/ui/lvgl.zig @@ -329,12 +329,17 @@ pub inline fn paletteDarken(p: Palette, l: PaletteModLevel) Color { /// represents lv_obj_t type in C. pub const LvObj = opaque { - /// deallocates all the resources used by the object, including its children. + /// deallocates all resources used by the object, including its children. /// user data pointers are untouched. pub fn destroy(self: *LvObj) void { lv_obj_del(self); } + /// deallocates all resources used by the object's children. + pub fn deleteChildren(self: *LvObj) void { + lv_obj_clean(self); + } + /// creates a new event handler where cb is called upon event with the filter code. /// to make cb called on any event, use EventCode.all filter. /// multiple event handlers are called in the same order as they were added. @@ -343,6 +348,11 @@ pub const LvObj = opaque { return lv_obj_add_event_cb(self, cb, filter, udata); } + /// sets label text to a new value. + pub fn setLabelText(self: *LvObj, text: [*:0]const u8) void { + lv_label_set_text(self, text); + } + /// sets or clears an object flag. pub fn setFlag(self: *LvObj, onoff: enum { on, off }, v: ObjFlag) void { switch (onoff) { @@ -388,11 +398,18 @@ pub const LvObj = opaque { } /// selects which side to pad in setPad func. - pub const PadSelector = enum { left, right, top, bottom, row, column }; + pub const PadSelector = enum { all, left, right, top, bottom, row, column }; /// adds a padding style to the object. pub fn setPad(self: *LvObj, v: Coord, p: PadSelector, sel: StyleSelector) void { switch (p) { + .all => { + const vsel = sel.value(); + lv_obj_set_style_pad_left(self, v, vsel); + lv_obj_set_style_pad_right(self, v, vsel); + lv_obj_set_style_pad_top(self, v, vsel); + lv_obj_set_style_pad_bottom(self, v, vsel); + }, .left => lv_obj_set_style_pad_left(self, v, sel.value()), .right => lv_obj_set_style_pad_right(self, v, sel.value()), .top => lv_obj_set_style_pad_top(self, v, sel.value()), @@ -453,6 +470,11 @@ pub const LvObj = opaque { pub fn setBackgroundColor(self: *LvObj, v: Color, sel: StyleSelector) void { lv_obj_set_style_bg_color(self, v, sel.value()); } + + /// sets the color of a text, typically a label object. + pub fn setTextColor(self: *LvObj, v: Color, sel: StyleSelector) void { + lv_obj_set_style_text_color(self, v, sel.value()); + } }; pub fn createObject(parent: *LvObj) !*LvObj { @@ -631,7 +653,7 @@ pub fn createWindow(parent: ?*LvObj, header_height: i16, title: [*:0]const u8) ! return .{ .winobj = winobj }; } -const Window = struct { +pub const Window = struct { winobj: *LvObj, pub fn content(self: Window) *LvObj { @@ -640,38 +662,58 @@ const Window = struct { }; pub const CreateLabelOpt = struct { - long_mode: enum(c.lv_label_long_mode_t) { + // LVGL defaults to .wrap + long_mode: ?enum(c.lv_label_long_mode_t) { wrap = c.LV_LABEL_LONG_WRAP, // keep the object width, wrap the too long lines and expand the object height dot = c.LV_LABEL_LONG_DOT, // keep the size and write dots at the end if the text is too long scroll = c.LV_LABEL_LONG_SCROLL, // keep the size and roll the text back and forth scroll_circular = c.LV_LABEL_LONG_SCROLL_CIRCULAR, // keep the size and roll the text circularly clip = c.LV_LABEL_LONG_CLIP, // keep the size and clip the text out of it - }, - pos: enum { - none, - centered, - }, + } = null, + pos: ?PosAlign = null, }; +/// creates a new label object. +/// the text is heap-duplicated for the lifetime of the object and free'ed automatically. pub fn createLabel(parent: *LvObj, text: [*:0]const u8, opt: CreateLabelOpt) !*LvObj { var lb = lv_label_create(parent) orelse return error.OutOfMemory; //lv_label_set_text_static(lb, text); // static doesn't work with .dot lv_label_set_text(lb, text); lv_label_set_recolor(lb, true); //lv_obj_set_height(lb, sizeContent); // default - lv_label_set_long_mode(lb, @enumToInt(opt.long_mode)); - if (opt.pos == .centered) { - lb.center(); + if (opt.long_mode) |m| { + lv_label_set_long_mode(lb, @enumToInt(m)); + } + if (opt.pos) |p| { + lb.posAlign(p, 0, 0); } return lb; } +/// formats label text using std.fmt.format and the provided buffer. +/// a label object is then created with the resulting text using createLabel. +/// the text is heap-dup'ed so no need to retain buf. see createLabel. +pub fn createLabelFmt(parent: *LvObj, buf: []u8, comptime format: []const u8, args: anytype, opt: CreateLabelOpt) !*LvObj { + const text = try std.fmt.bufPrintZ(buf, format, args); + return createLabel(parent, text, opt); +} + pub fn createButton(parent: *LvObj, label: [*:0]const u8) !*LvObj { const btn = lv_btn_create(parent) orelse return error.OutOfMemory; - _ = try createLabel(btn, label, .{ .long_mode = .dot, .pos = .centered }); + _ = try createLabel(btn, label, .{ .long_mode = .dot, .pos = .center }); return btn; } +/// creates a spinner object with hardcoded dimensions and animation speed +/// used througout the GUI. +pub fn createSpinner(parent: *LvObj) !*LvObj { + const spin = lv_spinner_create(parent, 1000, 60) orelse return error.OutOfMemory; + lv_obj_set_size(spin, 20, 20); + const ind: StyleSelector = .{ .part = .indicator }; + lv_obj_set_style_arc_width(spin, 4, ind.value()); + return spin; +} + // ========================================================================== // imports from nakamochi custom C code that extends LVGL // ========================================================================== @@ -748,12 +790,14 @@ extern fn lv_obj_add_style(obj: *LvObj, style: *LvStyle, sel: c.lv_style_selecto extern fn lv_obj_remove_style(obj: *LvObj, style: ?*LvStyle, sel: c.lv_style_selector_t) void; extern fn lv_obj_remove_style_all(obj: *LvObj) void; extern fn lv_obj_set_style_bg_color(obj: *LvObj, val: Color, sel: c.lv_style_selector_t) void; +extern fn lv_obj_set_style_text_color(obj: *LvObj, val: Color, sel: c.lv_style_selector_t) void; extern fn lv_obj_set_style_pad_left(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; extern fn lv_obj_set_style_pad_right(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; extern fn lv_obj_set_style_pad_top(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; extern fn lv_obj_set_style_pad_bottom(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; extern fn lv_obj_set_style_pad_row(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; extern fn lv_obj_set_style_pad_column(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; +extern fn lv_obj_set_style_arc_width(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; // TODO: port these to zig extern fn lv_palette_main(c.lv_palette_t) Color; @@ -766,6 +810,8 @@ extern fn lv_palette_darken(c.lv_palette_t, level: u8) Color; extern fn lv_obj_create(parent: ?*LvObj) ?*LvObj; /// deletes and deallocates an object and all its children from UI tree. extern fn lv_obj_del(obj: *LvObj) void; +/// deletes children of the obj. +extern fn lv_obj_clean(obj: *LvObj) void; extern fn lv_obj_add_flag(obj: *LvObj, v: c.lv_obj_flag_t) void; extern fn lv_obj_clear_flag(obj: *LvObj, v: c.lv_obj_flag_t) void; @@ -794,6 +840,8 @@ extern fn lv_label_set_text_static(label: *LvObj, text: [*:0]const u8) void; extern fn lv_label_set_long_mode(label: *LvObj, mode: c.lv_label_long_mode_t) void; extern fn lv_label_set_recolor(label: *LvObj, enable: bool) void; +extern fn lv_spinner_create(parent: *LvObj, speed_ms: u32, arc_deg: u32) ?*LvObj; + extern fn lv_win_create(parent: *LvObj, header_height: c.lv_coord_t) ?*LvObj; extern fn lv_win_add_title(win: *LvObj, title: [*:0]const u8) ?*LvObj; extern fn lv_win_get_content(win: *LvObj) *LvObj; diff --git a/src/ui/poweroff.zig b/src/ui/poweroff.zig new file mode 100644 index 0000000..2ff9c14 --- /dev/null +++ b/src/ui/poweroff.zig @@ -0,0 +1,136 @@ +//! poweroff workflow. +//! all functions assume ui_mutex is always locked. +const std = @import("std"); + +const comm = @import("../comm.zig"); +const lvgl = @import("lvgl.zig"); +const symbol = @import("symbol.zig"); +const widget = @import("widget.zig"); + +const logger = std.log.scoped(.ui); + +/// initiates system shutdown leading to power off. +/// defined in ngui.zig. +extern fn nm_sys_shutdown() void; + +/// called when "power off" button is pressed. +export fn nm_poweroff_btn_callback(_: *lvgl.LvEvent) void { + const proceed: [*:0]const u8 = "PROCEED"; + const abort: [*:0]const u8 = "CANCEL"; + const title = " " ++ symbol.Power ++ " SHUTDOWN"; + const text = + \\ARE YOU SURE? + \\ + \\once shut down, + \\payments cannot go through via bitcoin or lightning networks + \\until the node is powered back on. + ; + widget.modal(title, text, &.{ proceed, abort }, poweroffModalCallback) catch |err| { + logger.err("shutdown btn: modal: {any}", .{err}); + }; +} + +/// poweroff confirmation screen callback. +fn poweroffModalCallback(btn_idx: usize) void { + // proceed = 0, cancel = 1 + if (btn_idx != 0) { + return; + } + defer nm_sys_shutdown(); // initiate shutdown even if next lines fail + global_progress_win = ProgressWin.create() catch |err| { + logger.err("ProgressWin.create: {any}", .{err}); + return; + }; +} + +var global_progress_win: ?ProgressWin = null; + +/// updates the global poweroff process window with the status report. +/// the report is normally sent to GUI by the daemon. +pub fn updateStatus(report: comm.Message.PoweroffProgress) !void { + if (global_progress_win) |win| { + var all_stopped = true; + win.resetSvContainer(); + for (report.services) |sv| { + try win.addServiceStatus(sv.name, sv.stopped, sv.err); + all_stopped = all_stopped and sv.stopped; + } + if (all_stopped) { + win.status.setLabelText("powering off ..."); + } + } else { + return error.NoProgressWindow; + } +} + +/// represents a modal window in which the poweroff progress is reported until +/// the device turns off. +const ProgressWin = struct { + win: lvgl.Window, + status: *lvgl.LvObj, // text status label + svcont: *lvgl.LvObj, // services container + + /// symbol width next to the service name. this aligns all service names vertically. + /// has to be wide enough to accomodate the spinner, but not too wide + /// so that the service name is still close to the symbol. + const sym_width = 20; + + fn create() !ProgressWin { + const win = try lvgl.createWindow(null, 60, " " ++ symbol.Power ++ " SHUTDOWN"); + errdefer win.winobj.destroy(); // also deletes all children created below + const wincont = win.content(); + wincont.flexFlow(.column); + + // initial status message + const status = try lvgl.createLabel(wincont, "shutting down services. it may take up to a few minutes.", .{}); + status.setWidth(lvgl.sizePercent(100)); + // prepare a container for services status + const svcont = try lvgl.createObject(wincont); + svcont.removeBackgroundStyle(); + svcont.flexFlow(.column); + svcont.flexGrow(1); + svcont.padColumnDefault(); + svcont.setWidth(lvgl.sizePercent(100)); + + return .{ + .win = win, + .status = status, + .svcont = svcont, + }; + } + + fn resetSvContainer(self: ProgressWin) void { + self.svcont.deleteChildren(); + } + + fn addServiceStatus(self: ProgressWin, name: []const u8, stopped: bool, err: ?[]const u8) !void { + const row = try lvgl.createObject(self.svcont); + row.removeBackgroundStyle(); + row.flexFlow(.row); + row.flexAlign(.center, .center, .center); + row.padColumnDefault(); + row.setPad(10, .all, .{}); + row.setWidth(lvgl.sizePercent(100)); + row.setHeightToContent(); + + var buf: [100]u8 = undefined; + if (err) |e| { + const sym = try lvgl.createLabelFmt(row, &buf, symbol.Warning, .{}, .{ .long_mode = .clip }); + sym.setWidth(sym_width); + sym.setTextColor(lvgl.paletteMain(.red), .{}); + const lb = try lvgl.createLabelFmt(row, &buf, "{s}: {s}", .{ name, e }, .{ .long_mode = .dot }); + lb.setTextColor(lvgl.paletteMain(.red), .{}); + lb.flexGrow(1); + } else if (stopped) { + const sym = try lvgl.createLabelFmt(row, &buf, symbol.Ok, .{}, .{ .long_mode = .clip }); + sym.setWidth(sym_width); + const lb = try lvgl.createLabelFmt(row, &buf, "{s}", .{name}, .{ .long_mode = .dot }); + lb.flexGrow(1); + } else { + const spin = try lvgl.createSpinner(row); + spin.setWidth(sym_width); + const lb = try lvgl.createLabelFmt(row, &buf, "{s}", .{name}, .{ .long_mode = .dot }); + lb.flexGrow(1); + } + } +}; diff --git a/src/ui/ui.zig b/src/ui/ui.zig index 9321211..8e3e5d3 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -1,15 +1,16 @@ const buildopts = @import("build_options"); const std = @import("std"); +const comm = @import("../comm.zig"); const lvgl = @import("lvgl.zig"); const drv = @import("drv.zig"); const symbol = @import("symbol.zig"); const widget = @import("widget.zig"); +pub const poweroff = @import("poweroff.zig"); const logger = std.log.scoped(.ui); extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int; -extern fn nm_sys_shutdown() void; pub fn init() !void { lvgl.init(); @@ -25,33 +26,6 @@ pub fn init() !void { } } -/// called when "power off" button is pressed. -export fn nm_poweroff_btn_callback(e: *lvgl.LvEvent) void { - _ = e; - const proceed: [*:0]const u8 = "PROCEED"; - const abort: [*:0]const u8 = "CANCEL"; - const title = " " ++ symbol.Power ++ " SHUTDOWN"; - const text = - \\ARE YOU SURE? - \\ - \\once shut down, - \\payments cannot go through via bitcoin or lightning networks - \\until the node is powered back on. - ; - widget.modal(title, text, &.{ proceed, abort }, poweroffModalCallback) catch |err| { - logger.err("shutdown btn: modal: {any}", .{err}); - }; -} - -fn poweroffModalCallback(btn_idx: usize) void { - // proceed = 0, cancel = 1 - if (btn_idx != 0) { - return; - } - // proceed with shutdown - nm_sys_shutdown(); -} - export fn nm_create_info_panel(parent: *lvgl.LvObj) c_int { createInfoPanel(parent) catch |err| { logger.err("createInfoPanel: {any}", .{err}); @@ -66,5 +40,5 @@ fn createInfoPanel(parent: *lvgl.LvObj) !void { var buf: [100]u8 = undefined; const sver = try std.fmt.bufPrintZ(&buf, "GUI version: {any}", .{buildopts.semver}); - _ = try lvgl.createLabel(parent, sver, .{ .long_mode = .wrap, .pos = .none }); + _ = try lvgl.createLabel(parent, sver, .{}); } diff --git a/src/ui/widget.zig b/src/ui/widget.zig index 1338d04..4729347 100644 --- a/src/ui/widget.zig +++ b/src/ui/widget.zig @@ -58,7 +58,7 @@ pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const const wincont = win.content(); wincont.flexFlow(.column); wincont.flexAlign(.start, .center, .center); - const msg = try lvgl.createLabel(wincont, text, .{ .long_mode = .wrap, .pos = .centered }); + const msg = try lvgl.createLabel(wincont, text, .{ .pos = .center }); msg.setWidth(lvgl.displayHoriz() - 100); msg.flexGrow(1);