nd,ngui: let users switch sysupdates channel from the UI
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/tag/woodpecker Pipeline was successful Details

at the moment, there are two channels: edge and stable.

this builds up on few previous commits, most notably the persistent
configuration storage.
pull/28/head v0.5.0
alex 1 year ago
parent 1d8d67a987
commit c82848d186
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -50,6 +50,32 @@ pub const Error = error{
CommWriteTooLarge, 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. /// daemon and gui exchange messages of this type.
pub const Message = union(MessageTag) { pub const Message = union(MessageTag) {
ping: void, ping: void,
@ -63,6 +89,8 @@ pub const Message = union(MessageTag) {
poweroff_progress: PoweroffProgress, poweroff_progress: PoweroffProgress,
bitcoind_report: BitcoinReport, bitcoind_report: BitcoinReport,
lightning_report: LightningReport, lightning_report: LightningReport,
switch_sysupdates: SysupdatesChan,
settings: Settings,
pub const WifiConnect = struct { pub const WifiConnect = struct {
ssid: []const u8, ssid: []const u8,
@ -159,28 +187,17 @@ pub const Message = union(MessageTag) {
}, },
}, },
}; };
pub const SysupdatesChan = enum {
stable, // master branch in sysupdates
edge, // dev branch in sysupdates
}; };
/// it is important to preserve ordinal values for future compatiblity, pub const Settings = struct {
/// especially when nd and gui may temporary diverge in their implementations. sysupdates: struct {
pub const MessageTag = enum(u16) { channel: SysupdatesChan,
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
}; };
/// the return value type from `read` fn. /// 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()), .poweroff_progress => try json.stringify(msg.poweroff_progress, .{}, data.writer()),
.bitcoind_report => try json.stringify(msg.bitcoind_report, .{}, data.writer()), .bitcoind_report => try json.stringify(msg.bitcoind_report, .{}, data.writer()),
.lightning_report => try json.stringify(msg.lightning_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)) { if (data.items.len > std.math.maxInt(u64)) {
return Error.CommWriteTooLarge; return Error.CommWriteTooLarge;
@ -320,6 +339,24 @@ test "write" {
try t.expectEqualStrings(js.items, buf.items); 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" { test "write/read void tags" {
const t = std.testing; const t = std.testing;

@ -48,6 +48,8 @@ comm_thread: ?std.Thread = null,
poweroff_thread: ?std.Thread = null, poweroff_thread: ?std.Thread = null,
want_stop: bool = false, // tells daemon main loop to quit want_stop: bool = false, // tells daemon main loop to quit
// send all settings to ngui
want_settings: bool = false,
// network flags // network flags
want_network_report: bool, // start gathering network status and send out as soon as ready 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 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), .wpa_ctrl = try types.WpaControl.open(opt.wpa),
.state = .stopped, .state = .stopped,
.services = try svlist.toOwnedSlice(), .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. // send a network report right at start without wifi scan to make it faster.
.want_network_report = true, .want_network_report = true,
.want_wifi_scan = false, .want_wifi_scan = false,
@ -285,6 +289,28 @@ fn mainThreadLoop(self: *Daemon) void {
fn mainThreadLoopCycle(self: *Daemon) !void { fn mainThreadLoopCycle(self: *Daemon) !void {
self.mu.lock(); self.mu.lock();
defer self.mu.unlock(); 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}); self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
if (self.want_wifi_scan) { if (self.want_wifi_scan) {
if (self.startWifiScan()) { if (self.startWifiScan()) {
@ -300,6 +326,7 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
logger.err("network.sendReport: {any}", .{err}); logger.err("network.sendReport: {any}", .{err});
} }
} }
if (self.want_bitcoind_report or self.bitcoin_timer.read() > self.bitcoin_report_interval) { if (self.want_bitcoind_report or self.bitcoin_timer.read() > self.bitcoin_report_interval) {
if (self.sendBitcoindReport()) { if (self.sendBitcoindReport()) {
self.bitcoin_timer.reset(); self.bitcoin_timer.reset();
@ -374,6 +401,13 @@ fn commThreadLoop(self: *Daemon) void {
logger.info("wakeup from standby", .{}); logger.info("wakeup from standby", .{});
self.wakeup() catch |err| logger.err("nd.wakeup: {any}", .{err}); 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)}), 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 }); 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" { test "start-stop" {
const t = std.testing; const t = std.testing;
@ -747,6 +801,7 @@ test "start-stop" {
.uiw = pipe.writer(), .uiw = pipe.writer(),
.wpa = "/dev/null", .wpa = "/dev/null",
}); });
daemon.want_settings = false;
daemon.want_network_report = false; daemon.want_network_report = false;
daemon.want_bitcoind_report = false; daemon.want_bitcoind_report = false;
daemon.want_lnd_report = false; daemon.want_lnd_report = false;
@ -798,6 +853,7 @@ test "start-poweroff" {
.uiw = gui_stdin.writer(), .uiw = gui_stdin.writer(),
.wpa = "/dev/null", .wpa = "/dev/null",
}); });
daemon.want_settings = false;
daemon.want_network_report = false; daemon.want_network_report = false;
daemon.want_bitcoind_report = false; daemon.want_bitcoind_report = false;
daemon.want_lnd_report = false; daemon.want_lnd_report = false;

@ -251,6 +251,9 @@ fn commThreadLoopCycle() !void {
ui.lightning.updateTabPanel(rep) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err}); ui.lightning.updateTabPanel(rep) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
last_report.replace(msg); 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)}), else => logger.warn("unhandled msg tag {s}", .{@tagName(msg.value)}),
}, },
} }

@ -129,6 +129,7 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void { fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
var sectimer = try time.Timer.start(); var sectimer = try time.Timer.start();
var block_count: u32 = 801365; var block_count: u32 = 801365;
var settings_sent = false;
while (true) { while (true) {
time.sleep(time.ns_per_s); time.sleep(time.ns_per_s);
@ -137,6 +138,17 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
} }
sectimer.reset(); 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; block_count += 1;
const now = time.timestamp(); const now = time.timestamp();

@ -30,6 +30,11 @@ int nm_create_bitcoin_panel(lv_obj_t *parent);
*/ */
int nm_create_lightning_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. * 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)); 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 * 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_label_set_text_static(power_halt_btn_label, "SHUTDOWN");
lv_obj_center(power_halt_btn_label); lv_obj_center(power_halt_btn_label);
/********************
* sysupdates panel
********************/
// ported to zig;
lv_obj_t *sysupdates_panel = nm_create_settings_sysupdates(parent);
/******************** /********************
* layout * layout
********************/ ********************/
@ -228,10 +239,12 @@ static void create_settings_panel(lv_obj_t *parent)
static lv_coord_t parent_grid_rows[] = {/**/ static lv_coord_t parent_grid_rows[] = {/**/
LV_GRID_CONTENT, /* wifi panel */ LV_GRID_CONTENT, /* wifi panel */
LV_GRID_CONTENT, /* power panel */ LV_GRID_CONTENT, /* power panel */
LV_GRID_CONTENT, /* sysupdates panel */
LV_GRID_TEMPLATE_LAST}; LV_GRID_TEMPLATE_LAST};
lv_obj_set_grid_dsc_array(parent, parent_grid_cols, parent_grid_rows); 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(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(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_cols[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
static lv_coord_t wifi_grid_rows[] = {/**/ 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); lv_obj_set_grid_cell(poweroff_text, LV_GRID_ALIGN_START, 0, 1, LV_GRID_ALIGN_START, 2, 1);
/* column 1 */ /* column 1 */
lv_obj_set_grid_cell(power_halt_btn, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_CENTER, 2, 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) 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) { if (tab_settings == NULL) {
return -1; 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); lv_obj_t *tab_info = lv_tabview_add_tab(tabview, NM_SYMBOL_INFO);
if (tab_info == NULL) { if (tab_info == NULL) {

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

@ -1,4 +1,5 @@
///! see lv_symbols_def.h ///! see lv_symbols_def.h
pub const Loop = &[_]u8{ 0xef, 0x81, 0xb9 };
pub const Ok = &[_]u8{ 0xef, 0x80, 0x8c }; pub const Ok = &[_]u8{ 0xef, 0x80, 0x8c };
pub const Power = &[_]u8{ 0xef, 0x80, 0x91 }; pub const Power = &[_]u8{ 0xef, 0x80, 0x91 };
pub const Warning = &[_]u8{ 0xef, 0x81, 0xb1 }; pub const Warning = &[_]u8{ 0xef, 0x81, 0xb1 };

@ -2,13 +2,15 @@ const buildopts = @import("build_options");
const std = @import("std"); const std = @import("std");
const comm = @import("../comm.zig"); const comm = @import("../comm.zig");
const lvgl = @import("lvgl.zig");
const drv = @import("drv.zig"); const drv = @import("drv.zig");
const lvgl = @import("lvgl.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");
pub const bitcoin = @import("bitcoin.zig"); pub const bitcoin = @import("bitcoin.zig");
pub const lightning = @import("lightning.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); const logger = std.log.scoped(.ui);
@ -52,6 +54,14 @@ export fn nm_create_lightning_panel(parent: *lvgl.LvObj) c_int {
return 0; 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 { fn createInfoPanel(cont: lvgl.Container) !void {
const flex = cont.flex(.column, .{}); const flex = cont.flex(.column, .{});
var buf: [100]u8 = undefined; var buf: [100]u8 = undefined;