ngui: display poweroff progress during system shutdown
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details

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.
alex 1 year ago
parent c4fea2edcd
commit 7c9e30192b
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -104,7 +104,7 @@ pub fn build(b: *std.build.Builder) void {
nd_build_step.dependOn(&b.addInstallArtifact(nd).step); nd_build_step.dependOn(&b.addInstallArtifact(nd).step);
// default build // 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(ngui_build_step);
build_all_step.dependOn(nd_build_step); build_all_step.dependOn(nd_build_step);
b.default_step.dependOn(build_all_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"); const test_step = b.step("test", "run tests");
test_step.dependOn(&tests.step); 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 { const DriverTarget = enum {

@ -1,5 +1,6 @@
const buildopts = @import("build_options"); const buildopts = @import("build_options");
const std = @import("std"); const std = @import("std");
const os = std.os;
const time = std.time; const time = std.time;
const comm = @import("comm.zig"); 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; 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? /// TODO: thread-safety?
var gpa: std.mem.Allocator = undefined; var gpa: std.mem.Allocator = undefined;
/// the mutex must be held before any call reaching into lv_xxx functions. /// 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. /// all nm_xxx functions assume it is the case since they are invoked from lvgl c code.
var ui_mutex: std.Thread.Mutex = .{}; var ui_mutex: std.Thread.Mutex = .{};
/// the program runs until quit is true. /// 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 { var state: enum {
active, // normal operational mode active, // normal operational mode
standby, // idling standby, // idling
alert, // draw user attention; never go standby alert, // draw user attention; never go standby
} = .active; } = .active;
/// setting wakeup brings the screen back from sleep()'ing without waiting /// by setting wakeup brings the screen back from sleep()'ing without waiting for user action.
/// for user action. /// can be used by comms when an alert is received from the daemon, to draw user attention.
/// 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 /// safe for concurrent use except wakeup.reset() is UB during another thread
/// wakeup.wait()'ing or timedWait'ing. /// wakeup.wait()'ing or timedWait'ing.
var wakeup = std.Thread.ResetEvent{}; 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 { export fn nm_sys_shutdown() void {
logger.info("initiating system shutdown", .{});
const msg = comm.Message.poweroff; const msg = comm.Message.poweroff;
comm.write(gpa, stdout, msg) catch |err| logger.err("nm_sys_shutdown: {any}", .{err}); 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 { 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 /// reads messages from nd.
fn commThread() void { /// loops indefinitely until program exit or comm returns EOS.
fn commThreadLoop() void {
while (true) { while (true) {
commThreadLoopCycle() catch |err| logger.err("commThreadLoopCycle: {any}", .{err}); commThreadLoopCycle() catch |err| logger.err("commThreadLoopCycle: {any}", .{err});
ui_mutex.lock();
const do_quit = quit; std.atomic.spinLoopHint();
ui_mutex.unlock(); time.sleep(1 * time.ns_per_ms);
if (do_quit) {
return;
}
} }
logger.info("exiting commThreadLoop", .{});
} }
fn commThreadLoopCycle() !void { fn commThreadLoopCycle() !void {
@ -162,8 +164,9 @@ fn commThreadLoopCycle() !void {
if (err == error.EndOfStream) { if (err == error.EndOfStream) {
// pointless to continue running if comms is broken. // pointless to continue running if comms is broken.
// a parent/supervisor is expected to restart ngui. // a parent/supervisor is expected to restart ngui.
logger.err("comm.read: EOS", .{});
ui_mutex.lock(); ui_mutex.lock();
quit = true; want_quit = true;
ui_mutex.unlock(); ui_mutex.unlock();
} }
return err; return err;
@ -175,6 +178,11 @@ fn commThreadLoopCycle() !void {
.network_report => |report| { .network_report => |report| {
updateNetworkStatus(report) catch |err| logger.err("updateNetworkStatus: {any}", .{err}); 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)}), else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
} }
} }
@ -218,6 +226,20 @@ fn usage(prog: []const u8) !void {
, .{prog}); , .{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. /// nakamochi UI program entry point.
pub fn main() anyerror!void { pub fn main() anyerror!void {
// main heap allocator used through the lifetime of nd // 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. // start comms with daemon in a seaparate thread.
const th = try std.Thread.spawn(.{}, commThread, .{}); _ = try std.Thread.spawn(.{}, commThreadLoop, .{});
th.detach();
// 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 // run idle timer indefinitely
_ = lvgl.createTimer(nm_check_idle_time, 2000, null) catch |err| { _ = 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 // main UI thread; must never block unless in idle/sleep mode
// TODO: handle sigterm while (!want_quit) {
while (true) {
ui_mutex.lock(); ui_mutex.lock();
var till_next_ms = lvgl.loopCycle(); var till_next_ms = lvgl.loopCycle(); // UI loop
const do_quit = quit;
const do_state = state; const do_state = state;
ui_mutex.unlock(); ui_mutex.unlock();
if (do_quit) {
return;
}
if (do_state == .standby) { if (do_state == .standby) {
// go into a screen sleep mode due to no user activity // go into a screen sleep mode due to no user activity
wakeup.reset(); wakeup.reset();
@ -266,6 +292,7 @@ pub fn main() anyerror!void {
logger.err("comm.write standby: {any}", .{err}); logger.err("comm.write standby: {any}", .{err});
}; };
screen.sleep(&wakeup); screen.sleep(&wakeup);
// wake up due to touch screen activity or wakeup event is set // wake up due to touch screen activity or wakeup event is set
logger.info("waking up from sleep", .{}); logger.info("waking up from sleep", .{});
ui_mutex.lock(); ui_mutex.lock();
@ -279,10 +306,14 @@ pub fn main() anyerror!void {
ui_mutex.unlock(); ui_mutex.unlock();
continue; continue;
} }
std.atomic.spinLoopHint(); std.atomic.spinLoopHint();
// sleep at least 1ms time.sleep(@max(1, till_next_ms) * time.ns_per_ms); // sleep at least 1ms
time.sleep(@max(1, till_next_ms) * time.ns_per_ms);
} }
logger.info("main UI loop terminated", .{});
// not waiting for comm thread because it is terminated at program exit here
// anyway.
} }
test "tick" { test "tick" {

@ -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});
}

@ -329,12 +329,17 @@ pub inline fn paletteDarken(p: Palette, l: PaletteModLevel) Color {
/// represents lv_obj_t type in C. /// represents lv_obj_t type in C.
pub const LvObj = opaque { 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. /// user data pointers are untouched.
pub fn destroy(self: *LvObj) void { pub fn destroy(self: *LvObj) void {
lv_obj_del(self); 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. /// 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. /// to make cb called on any event, use EventCode.all filter.
/// multiple event handlers are called in the same order as they were added. /// 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); 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. /// sets or clears an object flag.
pub fn setFlag(self: *LvObj, onoff: enum { on, off }, v: ObjFlag) void { pub fn setFlag(self: *LvObj, onoff: enum { on, off }, v: ObjFlag) void {
switch (onoff) { switch (onoff) {
@ -388,11 +398,18 @@ pub const LvObj = opaque {
} }
/// selects which side to pad in setPad func. /// 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. /// adds a padding style to the object.
pub fn setPad(self: *LvObj, v: Coord, p: PadSelector, sel: StyleSelector) void { pub fn setPad(self: *LvObj, v: Coord, p: PadSelector, sel: StyleSelector) void {
switch (p) { 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()), .left => lv_obj_set_style_pad_left(self, v, sel.value()),
.right => lv_obj_set_style_pad_right(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()), .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 { pub fn setBackgroundColor(self: *LvObj, v: Color, sel: StyleSelector) void {
lv_obj_set_style_bg_color(self, v, sel.value()); 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 { 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 }; return .{ .winobj = winobj };
} }
const Window = struct { pub const Window = struct {
winobj: *LvObj, winobj: *LvObj,
pub fn content(self: Window) *LvObj { pub fn content(self: Window) *LvObj {
@ -640,38 +662,58 @@ const Window = struct {
}; };
pub const CreateLabelOpt = 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 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 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 = 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 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 clip = c.LV_LABEL_LONG_CLIP, // keep the size and clip the text out of it
}, } = null,
pos: enum { pos: ?PosAlign = null,
none,
centered,
},
}; };
/// 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 { pub fn createLabel(parent: *LvObj, text: [*:0]const u8, opt: CreateLabelOpt) !*LvObj {
var lb = lv_label_create(parent) orelse return error.OutOfMemory; 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_static(lb, text); // static doesn't work with .dot
lv_label_set_text(lb, text); lv_label_set_text(lb, text);
lv_label_set_recolor(lb, true); lv_label_set_recolor(lb, true);
//lv_obj_set_height(lb, sizeContent); // default //lv_obj_set_height(lb, sizeContent); // default
lv_label_set_long_mode(lb, @enumToInt(opt.long_mode)); if (opt.long_mode) |m| {
if (opt.pos == .centered) { lv_label_set_long_mode(lb, @enumToInt(m));
lb.center(); }
if (opt.pos) |p| {
lb.posAlign(p, 0, 0);
} }
return lb; 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 { pub fn createButton(parent: *LvObj, label: [*:0]const u8) !*LvObj {
const btn = lv_btn_create(parent) orelse return error.OutOfMemory; 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; 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 // 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(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_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_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_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_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_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_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_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_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 // TODO: port these to zig
extern fn lv_palette_main(c.lv_palette_t) Color; 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; extern fn lv_obj_create(parent: ?*LvObj) ?*LvObj;
/// deletes and deallocates an object and all its children from UI tree. /// deletes and deallocates an object and all its children from UI tree.
extern fn lv_obj_del(obj: *LvObj) void; 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_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; 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_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_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_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_add_title(win: *LvObj, title: [*:0]const u8) ?*LvObj;
extern fn lv_win_get_content(win: *LvObj) *LvObj; extern fn lv_win_get_content(win: *LvObj) *LvObj;

@ -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);
}
}
};

@ -1,15 +1,16 @@
const buildopts = @import("build_options"); const buildopts = @import("build_options");
const std = @import("std"); const std = @import("std");
const comm = @import("../comm.zig");
const lvgl = @import("lvgl.zig"); const lvgl = @import("lvgl.zig");
const drv = @import("drv.zig"); const drv = @import("drv.zig");
const symbol = @import("symbol.zig"); const symbol = @import("symbol.zig");
const widget = @import("widget.zig"); const widget = @import("widget.zig");
pub const poweroff = @import("poweroff.zig");
const logger = std.log.scoped(.ui); const logger = std.log.scoped(.ui);
extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int; extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int;
extern fn nm_sys_shutdown() void;
pub fn init() !void { pub fn init() !void {
lvgl.init(); 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 { export fn nm_create_info_panel(parent: *lvgl.LvObj) c_int {
createInfoPanel(parent) catch |err| { createInfoPanel(parent) catch |err| {
logger.err("createInfoPanel: {any}", .{err}); logger.err("createInfoPanel: {any}", .{err});
@ -66,5 +40,5 @@ fn createInfoPanel(parent: *lvgl.LvObj) !void {
var buf: [100]u8 = undefined; var buf: [100]u8 = undefined;
const sver = try std.fmt.bufPrintZ(&buf, "GUI version: {any}", .{buildopts.semver}); 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, .{});
} }

@ -58,7 +58,7 @@ pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const
const wincont = win.content(); const wincont = win.content();
wincont.flexFlow(.column); wincont.flexFlow(.column);
wincont.flexAlign(.start, .center, .center); 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.setWidth(lvgl.displayHoriz() - 100);
msg.flexGrow(1); msg.flexGrow(1);