diff --git a/src/comm.zig b/src/comm.zig index c8b8bbb..e1e5c91 100644 --- a/src/comm.zig +++ b/src/comm.zig @@ -50,6 +50,32 @@ pub const Error = error{ CommWriteTooLarge, }; +/// it is important to preserve ordinal values for future compatiblity, +/// especially when nd and gui may temporary diverge in their implementations. +pub const MessageTag = enum(u16) { + ping = 0x01, + pong = 0x02, + poweroff = 0x03, + wifi_connect = 0x04, + network_report = 0x05, + get_network_report = 0x06, + // ngui -> nd: screen timeout, no user activity; no reply + standby = 0x07, + // ngui -> nd: resume screen due to user touch; no reply + wakeup = 0x08, + // nd -> ngui: reports poweroff progress + poweroff_progress = 0x09, + // nd -> ngui: bitcoin core daemon status report + bitcoind_report = 0x0a, + // nd -> ngui: lnd status and stats report + lightning_report = 0x0b, + // ngui -> nd: switch sysupdates channel + switch_sysupdates = 0x0c, + // nd -> ngui: all ndg settings + settings = 0x0d, + // next: 0x0e +}; + /// daemon and gui exchange messages of this type. pub const Message = union(MessageTag) { ping: void, @@ -63,6 +89,8 @@ pub const Message = union(MessageTag) { poweroff_progress: PoweroffProgress, bitcoind_report: BitcoinReport, lightning_report: LightningReport, + switch_sysupdates: SysupdatesChan, + settings: Settings, pub const WifiConnect = struct { ssid: []const u8, @@ -159,28 +187,17 @@ pub const Message = union(MessageTag) { }, }, }; -}; -/// it is important to preserve ordinal values for future compatiblity, -/// especially when nd and gui may temporary diverge in their implementations. -pub const MessageTag = enum(u16) { - ping = 0x01, - pong = 0x02, - poweroff = 0x03, - wifi_connect = 0x04, - network_report = 0x05, - get_network_report = 0x06, - // ngui -> nd: screen timeout, no user activity; no reply - standby = 0x07, - // ngui -> nd: resume screen due to user touch; no reply - wakeup = 0x08, - // nd -> ngui: reports poweroff progress - poweroff_progress = 0x09, - // nd -> ngui: bitcoin core daemon status report - bitcoind_report = 0x0a, - // nd -> ngui: lnd status and stats report - lightning_report = 0x0b, - // next: 0x0c + pub const SysupdatesChan = enum { + stable, // master branch in sysupdates + edge, // dev branch in sysupdates + }; + + pub const Settings = struct { + sysupdates: struct { + channel: SysupdatesChan, + }, + }; }; /// the return value type from `read` fn. @@ -254,6 +271,8 @@ pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void { .poweroff_progress => try json.stringify(msg.poweroff_progress, .{}, data.writer()), .bitcoind_report => try json.stringify(msg.bitcoind_report, .{}, data.writer()), .lightning_report => try json.stringify(msg.lightning_report, .{}, data.writer()), + .switch_sysupdates => try json.stringify(msg.switch_sysupdates, .{}, data.writer()), + .settings => try json.stringify(msg.settings, .{}, data.writer()), } if (data.items.len > std.math.maxInt(u64)) { return Error.CommWriteTooLarge; @@ -320,6 +339,24 @@ test "write" { try t.expectEqualStrings(js.items, buf.items); } +test "write enum" { + const t = std.testing; + + var buf = std.ArrayList(u8).init(t.allocator); + defer buf.deinit(); + const msg = Message{ .switch_sysupdates = .edge }; + try write(t.allocator, buf.writer(), msg); + + const payload = "\"edge\""; + var js = std.ArrayList(u8).init(t.allocator); + defer js.deinit(); + try js.writer().writeIntLittle(u16, @intFromEnum(msg)); + try js.writer().writeIntLittle(u64, payload.len); + try js.appendSlice(payload); + + try t.expectEqualStrings(js.items, buf.items); +} + test "write/read void tags" { const t = std.testing; diff --git a/src/nd/Daemon.zig b/src/nd/Daemon.zig index 144866c..6e9ac23 100644 --- a/src/nd/Daemon.zig +++ b/src/nd/Daemon.zig @@ -48,6 +48,8 @@ comm_thread: ?std.Thread = null, poweroff_thread: ?std.Thread = null, want_stop: bool = false, // tells daemon main loop to quit +// send all settings to ngui +want_settings: bool = false, // network flags want_network_report: bool, // start gathering network status and send out as soon as ready want_wifi_scan: bool, // initiate wifi scan at the next loop cycle @@ -102,6 +104,8 @@ pub fn init(opt: InitOpt) !Daemon { .wpa_ctrl = try types.WpaControl.open(opt.wpa), .state = .stopped, .services = try svlist.toOwnedSlice(), + // send persisted settings immediately on start + .want_settings = true, // send a network report right at start without wifi scan to make it faster. .want_network_report = true, .want_wifi_scan = false, @@ -285,6 +289,28 @@ fn mainThreadLoop(self: *Daemon) void { fn mainThreadLoopCycle(self: *Daemon) !void { self.mu.lock(); defer self.mu.unlock(); + + if (self.want_settings) { + const ok = self.conf.safeReadOnly(struct { + fn f(conf: Config.Data) bool { + const msg: comm.Message.Settings = .{ + .sysupdates = .{ + .channel = switch (conf.syschannel) { + .dev => .edge, + .master => .stable, + }, + }, + }; + comm.pipeWrite(.{ .settings = msg }) catch |err| { + logger.err("{}", .{err}); + return false; + }; + return true; + } + }.f); + self.want_settings = !ok; + } + self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err}); if (self.want_wifi_scan) { if (self.startWifiScan()) { @@ -300,6 +326,7 @@ fn mainThreadLoopCycle(self: *Daemon) !void { logger.err("network.sendReport: {any}", .{err}); } } + if (self.want_bitcoind_report or self.bitcoin_timer.read() > self.bitcoin_report_interval) { if (self.sendBitcoindReport()) { self.bitcoin_timer.reset(); @@ -374,6 +401,13 @@ fn commThreadLoop(self: *Daemon) void { logger.info("wakeup from standby", .{}); self.wakeup() catch |err| logger.err("nd.wakeup: {any}", .{err}); }, + .switch_sysupdates => |chan| { + logger.info("switching sysupdates channel to {s}", .{@tagName(chan)}); + self.switchSysupdates(chan) catch |err| { + logger.err("switchSysupdates: {any}", .{err}); + // TODO: send err back to ngui + }; + }, else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}), } @@ -736,6 +770,26 @@ fn sendLightningReport(self: *Daemon) !void { try comm.write(self.allocator, self.uiwriter, .{ .lightning_report = lndrep }); } +fn switchSysupdates(self: *Daemon, chan: comm.Message.SysupdatesChan) !void { + const th = try std.Thread.spawn(.{}, switchSysupdatesThread, .{ self, chan }); + th.detach(); +} + +fn switchSysupdatesThread(self: *Daemon, chan: comm.Message.SysupdatesChan) void { + const conf_chan: Config.SysupdatesChannel = switch (chan) { + .stable => .master, + .edge => .dev, + }; + self.conf.switchSysupdates(conf_chan, .{ .run = true }) catch |err| { + logger.err("config.switchSysupdates: {any}", .{err}); + // TODO: send err back to ngui + }; + // schedule settings report for ngui + self.mu.lock(); + defer self.mu.unlock(); + self.want_settings = true; +} + test "start-stop" { const t = std.testing; @@ -747,6 +801,7 @@ test "start-stop" { .uiw = pipe.writer(), .wpa = "/dev/null", }); + daemon.want_settings = false; daemon.want_network_report = false; daemon.want_bitcoind_report = false; daemon.want_lnd_report = false; @@ -798,6 +853,7 @@ test "start-poweroff" { .uiw = gui_stdin.writer(), .wpa = "/dev/null", }); + daemon.want_settings = false; daemon.want_network_report = false; daemon.want_bitcoind_report = false; daemon.want_lnd_report = false; diff --git a/src/ngui.zig b/src/ngui.zig index bfce8a5..3c74e5a 100644 --- a/src/ngui.zig +++ b/src/ngui.zig @@ -251,6 +251,9 @@ fn commThreadLoopCycle() !void { ui.lightning.updateTabPanel(rep) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err}); last_report.replace(msg); }, + .settings => |sett| { + ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err}); + }, else => logger.warn("unhandled msg tag {s}", .{@tagName(msg.value)}), }, } diff --git a/src/test/guiplay.zig b/src/test/guiplay.zig index 1e7d144..2a05d3b 100644 --- a/src/test/guiplay.zig +++ b/src/test/guiplay.zig @@ -129,6 +129,7 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void { fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void { var sectimer = try time.Timer.start(); var block_count: u32 = 801365; + var settings_sent = false; while (true) { time.sleep(time.ns_per_s); @@ -137,6 +138,17 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void { } sectimer.reset(); + if (!settings_sent) { + settings_sent = true; + const sett: comm.Message.Settings = .{ + .sysupdates = .{ .channel = .edge }, + }; + comm.write(gpa, w, .{ .settings = sett }) catch |err| { + logger.err("{}", .{err}); + settings_sent = false; + }; + } + block_count += 1; const now = time.timestamp(); diff --git a/src/ui/c/ui.c b/src/ui/c/ui.c index eb28f4c..ce4f142 100644 --- a/src/ui/c/ui.c +++ b/src/ui/c/ui.c @@ -30,6 +30,11 @@ int nm_create_bitcoin_panel(lv_obj_t *parent); */ int nm_create_lightning_panel(lv_obj_t *parent); +/** + * creates the sysupdates section of the settings panel. + */ +lv_obj_t *nm_create_settings_sysupdates(lv_obj_t *parent); + /** * invoken when the UI is switched to the network settings tab. */ @@ -149,7 +154,7 @@ static void wifi_connect_btn_callback(lv_event_t *e) nm_wifi_start_connect(buf, lv_textarea_get_text(settings.wifi_pwd_obj)); } -static void create_settings_panel(lv_obj_t *parent) +static int create_settings_panel(lv_obj_t *parent) { /******************** * wifi panel @@ -221,6 +226,12 @@ static void create_settings_panel(lv_obj_t *parent) lv_label_set_text_static(power_halt_btn_label, "SHUTDOWN"); lv_obj_center(power_halt_btn_label); + /******************** + * sysupdates panel + ********************/ + // ported to zig; + lv_obj_t *sysupdates_panel = nm_create_settings_sysupdates(parent); + /******************** * layout ********************/ @@ -228,10 +239,12 @@ static void create_settings_panel(lv_obj_t *parent) static lv_coord_t parent_grid_rows[] = {/**/ LV_GRID_CONTENT, /* wifi panel */ LV_GRID_CONTENT, /* power panel */ + LV_GRID_CONTENT, /* sysupdates panel */ LV_GRID_TEMPLATE_LAST}; lv_obj_set_grid_dsc_array(parent, parent_grid_cols, parent_grid_rows); lv_obj_set_grid_cell(wifi_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 0, 1); lv_obj_set_grid_cell(power_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 1, 1); + lv_obj_set_grid_cell(sysupdates_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 2, 1); static lv_coord_t wifi_grid_cols[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; static lv_coord_t wifi_grid_rows[] = {/**/ @@ -269,6 +282,8 @@ static void create_settings_panel(lv_obj_t *parent) lv_obj_set_grid_cell(poweroff_text, LV_GRID_ALIGN_START, 0, 1, LV_GRID_ALIGN_START, 2, 1); /* column 1 */ lv_obj_set_grid_cell(power_halt_btn, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 2, 1); + + return 0; } static void tab_changed_event_cb(lv_event_t *e) @@ -346,7 +361,9 @@ extern int nm_ui_init(lv_disp_t *disp) if (tab_settings == NULL) { return -1; } - create_settings_panel(tab_settings); + if (create_settings_panel(tab_settings) != 0) { + return -1; + } lv_obj_t *tab_info = lv_tabview_add_tab(tabview, NM_SYMBOL_INFO); if (tab_info == NULL) { diff --git a/src/ui/settings.zig b/src/ui/settings.zig new file mode 100644 index 0000000..c022361 --- /dev/null +++ b/src/ui/settings.zig @@ -0,0 +1,131 @@ +//! settings main tab. +//! all functions assume LVGL is init'ed and ui mutex is locked on entry. +//! +//! TODO: at the moment, most of the code is still in C; need to port to zig from src/ui/c/ui.c + +const std = @import("std"); + +const comm = @import("../comm.zig"); +const lvgl = @import("lvgl.zig"); +const symbol = @import("symbol.zig"); + +const logger = std.log.scoped(.ui); + +/// label color mark start to make "label:" part of a "label: value" +/// in a different color. +const cmark = "#bbbbbb "; +/// button text +const textSwitch = "SWITCH"; + +/// the settings tab alive for the whole duration of the process. +var tab: struct { + sysupdates: struct { + card: lvgl.Card, + chansel: lvgl.Dropdown, + switchbtn: lvgl.TextButton, + currchan: lvgl.Label, + }, +} = undefined; + +/// holds last values received from the daemon. +var state: struct { + curr_sysupdates_chan: ?comm.Message.SysupdatesChan = null, +} = .{}; + +/// creates a settings panel UI to control system updates channel. +/// must be called only once at program startup. +pub fn initSysupdatesPanel(cont: lvgl.Container) !lvgl.Card { + tab.sysupdates.card = try lvgl.Card.new(cont, symbol.Loop ++ " SYSUPDATES", .{ .spinner = true }); + const l1 = try lvgl.Label.new(tab.sysupdates.card, "" // + ++ "https://git.qcode.ch/nakamochi/sysupdates " // TODO: make this configurable? + ++ "is the source of system updates.", .{}); + l1.setPad(15, .top, .{}); + l1.setWidth(lvgl.sizePercent(100)); + l1.setHeightToContent(); + + const row = try lvgl.FlexLayout.new(tab.sysupdates.card, .row, .{}); + row.setWidth(lvgl.sizePercent(100)); + row.setHeightToContent(); + + // left column + const left = try lvgl.FlexLayout.new(row, .column, .{}); + left.flexGrow(1); + left.setPad(10, .row, .{}); + left.setHeightToContent(); + tab.sysupdates.currchan = try lvgl.Label.new(left, cmark ++ "CURRENT CHANNEL:# unknown", .{ .recolor = true }); + tab.sysupdates.currchan.setHeightToContent(); + const lab = try lvgl.Label.new(left, "edge channel may contain some experimental and unstable features.", .{}); + lab.setWidth(lvgl.sizePercent(100)); + lab.setHeightToContent(); + + // right column + const right = try lvgl.FlexLayout.new(row, .column, .{}); + right.flexGrow(1); + right.setPad(10, .row, .{}); + right.setHeightToContent(); + tab.sysupdates.chansel = try lvgl.Dropdown.newStatic(right, blk: { + // items order must match that of the switch in update fn. + break :blk @tagName(comm.Message.SysupdatesChan.stable) // index 0 + ++ "\n" ++ @tagName(comm.Message.SysupdatesChan.edge); // index 1 + }); + tab.sysupdates.chansel.setWidth(lvgl.sizePercent(100)); + tab.sysupdates.chansel.setText(""); // show no pre-selected value + _ = tab.sysupdates.chansel.on(.value_changed, nm_sysupdates_chansel_changed, null); + tab.sysupdates.switchbtn = try lvgl.TextButton.new(right, textSwitch); + tab.sysupdates.switchbtn.setWidth(lvgl.sizePercent(100)); + // disable channel switch button 'till data received from the daemon + // or user-selected value. + tab.sysupdates.switchbtn.disable(); + _ = tab.sysupdates.switchbtn.on(.click, nm_sysupdates_switch_click, null); + + return tab.sysupdates.card; +} + +/// updates the UI with the data from the provided settings arg. +pub fn update(sett: comm.Message.Settings) !void { + var buf: [512]u8 = undefined; + try tab.sysupdates.currchan.setTextFmt(&buf, cmark ++ "CURRENT CHANNEL:# {s}", .{@tagName(sett.sysupdates.channel)}); + state.curr_sysupdates_chan = sett.sysupdates.channel; +} + +export fn nm_sysupdates_chansel_changed(_: *lvgl.LvEvent) void { + var buf = [_]u8{0} ** 32; + const name = tab.sysupdates.chansel.getSelectedStr(&buf); + const chan = std.meta.stringToEnum(comm.Message.SysupdatesChan, name) orelse return; + if (state.curr_sysupdates_chan) |curr_chan| { + if (chan != curr_chan) { + tab.sysupdates.switchbtn.enable(); + tab.sysupdates.chansel.clearText(); // show selected value + } else { + tab.sysupdates.switchbtn.disable(); + tab.sysupdates.chansel.setText(""); // hide selected value + } + } else { + tab.sysupdates.switchbtn.enable(); + tab.sysupdates.chansel.clearText(); // show selected value + } +} + +export fn nm_sysupdates_switch_click(_: *lvgl.LvEvent) void { + var buf = [_]u8{0} ** 32; + const name = tab.sysupdates.chansel.getSelectedStr(&buf); + switchSysupdates(name) catch |err| logger.err("switchSysupdates: {any}", .{err}); +} + +fn switchSysupdates(name: []const u8) !void { + const chan = std.meta.stringToEnum(comm.Message.SysupdatesChan, name) orelse return error.InvalidSysupdateChannel; + logger.debug("switching sysupdates to channel {}", .{chan}); + + tab.sysupdates.switchbtn.disable(); + tab.sysupdates.switchbtn.label.setTextStatic("UPDATING ..."); + tab.sysupdates.chansel.disable(); + tab.sysupdates.card.spin(.on); + errdefer { + tab.sysupdates.card.spin(.off); + tab.sysupdates.chansel.enable(); + tab.sysupdates.switchbtn.enable(); + tab.sysupdates.switchbtn.label.setTextStatic(textSwitch); + } + + try comm.pipeWrite(.{ .switch_sysupdates = chan }); +} diff --git a/src/ui/symbol.zig b/src/ui/symbol.zig index e830295..9ee63b0 100644 --- a/src/ui/symbol.zig +++ b/src/ui/symbol.zig @@ -1,4 +1,5 @@ ///! see lv_symbols_def.h +pub const Loop = &[_]u8{ 0xef, 0x81, 0xb9 }; pub const Ok = &[_]u8{ 0xef, 0x80, 0x8c }; pub const Power = &[_]u8{ 0xef, 0x80, 0x91 }; pub const Warning = &[_]u8{ 0xef, 0x81, 0xb1 }; diff --git a/src/ui/ui.zig b/src/ui/ui.zig index 5940bb9..e892f46 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -2,13 +2,15 @@ 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 lvgl = @import("lvgl.zig"); const symbol = @import("symbol.zig"); const widget = @import("widget.zig"); -pub const poweroff = @import("poweroff.zig"); + pub const bitcoin = @import("bitcoin.zig"); pub const lightning = @import("lightning.zig"); +pub const poweroff = @import("poweroff.zig"); +pub const settings = @import("settings.zig"); const logger = std.log.scoped(.ui); @@ -52,6 +54,14 @@ export fn nm_create_lightning_panel(parent: *lvgl.LvObj) c_int { return 0; } +export fn nm_create_settings_sysupdates(parent: *lvgl.LvObj) ?*lvgl.LvObj { + const card = settings.initSysupdatesPanel(lvgl.Container{ .lvobj = parent }) catch |err| { + logger.err("initSysupdatesPanel: {any}", .{err}); + return null; + }; + return card.lvobj; +} + fn createInfoPanel(cont: lvgl.Container) !void { const flex = cont.flex(.column, .{}); var buf: [100]u8 = undefined;