Compare commits

...

6 Commits

Author SHA1 Message Date
alex 281c3b6e18
merge zig: upgrade from 0.11 to 0.12.0
ci/woodpecker/push/woodpecker Pipeline was successful Details
6 months ago
alex 729af48569
zig: upgrade from 0.11 to 0.12.0
ci/woodpecker/push/woodpecker Pipeline was successful Details
mostly lots of language improvements and bugfixes, leading to better
code here, from the programming language point of view.

zig v0.12.0 release notes:
https://ziglang.org/download/0.12.0/release-notes.html
6 months ago
alex e07b1557c7
lib/ini: sync with upstream at 19e1210
exact command:

    git subtree --prefix=lib/ini --squash pull \
      https://github.com/ziglibs/ini \
      19e1210063882ab7db73a8aaa60e733d4aaafe9f
6 months ago
alex e0851a5057 Squashed 'lib/ini/' changes from 2b11e8fef..19e121006
19e121006 FIX: local variable is never mutated
91775fd5c UPDATE: updated Build.zig to zig master

git-subtree-dir: lib/ini
git-subtree-split: 19e1210063882ab7db73a8aaa60e733d4aaafe9f
6 months ago
alex 285cff1d51
ui: increase keyboard buttons font size
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/tag/woodpecker Pipeline was successful Details
simply make the keyboard buttons text larger.
makes it easier to type.
6 months ago
alex e55471e48c
ngui,nd: screenlock feature implementation
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/tag/woodpecker Pipeline was successful Details
the screenlock use case is a way to restrict access to the touchscreen,
for example from children. it is in no way a security measure against
theft or prolonged physical access.

however, nd daemon will refuse to connect to a wifi, switch sysupdates
channel, set a new nodename or init lnd wallet during active screenlock

screenlock pin code can be enabled and disabled from the settings screen.
upon loss of the code, the only way to disable screenlock is to set
slock field to null in the nd daemon conf file.
6 months ago

@ -9,27 +9,27 @@ clone:
recursive: false
pipeline:
lint:
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
image: git.qcode.ch/nakamochi/ci-zig0.12.0:v1
commands:
- ./tools/fmt-check.sh
test:
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
image: git.qcode.ch/nakamochi/ci-zig0.12.0:v1
commands:
- zig build test
sdl2:
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
image: git.qcode.ch/nakamochi/ci-zig0.12.0:v1
commands:
- zig build -Ddriver=sdl2
x11:
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
image: git.qcode.ch/nakamochi/ci-zig0.12.0:v1
commands:
- zig build -Ddriver=x11
aarch64:
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
image: git.qcode.ch/nakamochi/ci-zig0.12.0:v1
commands:
- zig build -Ddriver=fbev -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe -Dstrip
- sha256sum zig-out/bin/nd zig-out/bin/ngui
playground:
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
image: git.qcode.ch/nakamochi/ci-zig0.12.0:v1
commands:
- zig build guiplay btcrpc lndhc

@ -61,17 +61,17 @@ to make a new image and switch the CI to use it, first modify the
[ci-containerfile](tools/ci-containerfile) and produce the image locally:
podman build --rm -t ndg-ci -f ./tools/ci-containerfile \
--build-arg ZIGURL=https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz
--build-arg ZIGURL=https://ziglang.org/download/0.12.0/zig-linux-x86_64-0.12.0.tar.xz
then tag it with the target URL, for example:
podman tag localhost/ndg-ci git.qcode.ch/nakamochi/ci-zig0.11.0:v2
podman tag localhost/ndg-ci git.qcode.ch/nakamochi/ci-zig0.12.0:v1
generate an [access token](https://git.qcode.ch/user/settings/applications),
login to the container registry and push the image to remote:
podman login git.qcode.ch
podman push git.qcode.ch/nakamochi/ci-zig0.11.0:v2
podman push git.qcode.ch/nakamochi/ci-zig0.12.0:v1
the image will be available at
https://git.qcode.ch/nakamochi/-/packages/

@ -11,18 +11,16 @@ pub fn build(b: *std.Build) void {
const inver = b.option([]const u8, "version", "semantic version of the build; must match git tag when available");
const buildopts = b.addOptions();
const buildopts_mod = buildopts.createModule();
buildopts.addOption(DriverTarget, "driver", drv);
const semver_step = VersionStep.create(b, buildopts, inver);
buildopts.step.dependOn(semver_step);
// network interface (nif) standalone library used by the daemon and tests.
const libnif_dep = b.anonymousDependency("lib/nif", @import("lib/nif/build.zig"), .{
.target = target,
.optimize = optimize,
});
const libnif_dep = b.lazyDependency("nif", .{ .target = target, .optimize = optimize }) orelse return;
const libnif = libnif_dep.artifact("nif");
// ini file format parser
const libini = b.addModule("ini", .{ .source_file = .{ .path = "lib/ini/src/ini.zig" } });
const libini_dep = b.lazyDependency("ini", .{ .target = target, .optimize = optimize }) orelse return;
const common_cflags = .{
"-Wall",
@ -35,16 +33,16 @@ pub fn build(b: *std.Build) void {
// gui build
const ngui = b.addExecutable(.{
.name = "ngui",
.root_source_file = .{ .path = "src/ngui.zig" },
.root_source_file = b.path("src/ngui.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.strip = strip,
});
ngui.pie = true;
ngui.strip = strip;
ngui.addOptions("build_options", buildopts);
ngui.addIncludePath(.{ .path = "lib" });
ngui.addIncludePath(.{ .path = "src/ui/c" });
ngui.root_module.addImport("build_options", buildopts_mod);
ngui.addIncludePath(b.path("lib"));
ngui.addIncludePath(b.path("src/ui/c"));
const lvgl_flags = .{
"-std=c11",
@ -52,7 +50,7 @@ pub fn build(b: *std.Build) void {
"-Wformat",
"-Wformat-security",
} ++ common_cflags;
ngui.addCSourceFiles(lvgl_generic_src, &lvgl_flags);
ngui.addCSourceFiles(.{ .files = lvgl_generic_src, .flags = &lvgl_flags });
const ngui_cflags = .{
"-std=c11",
@ -60,39 +58,46 @@ pub fn build(b: *std.Build) void {
"-Wunused-parameter",
"-Werror",
} ++ common_cflags;
ngui.addCSourceFiles(&.{
"src/ui/c/ui.c",
"src/ui/c/lv_font_courierprimecode_14.c",
"src/ui/c/lv_font_courierprimecode_16.c",
"src/ui/c/lv_font_courierprimecode_24.c",
}, &ngui_cflags);
ngui.defineCMacroRaw(b.fmt("NM_DISP_HOR={}", .{disp_horiz}));
ngui.defineCMacroRaw(b.fmt("NM_DISP_VER={}", .{disp_vert}));
ngui.defineCMacro("LV_CONF_INCLUDE_SIMPLE", null);
ngui.addCSourceFiles(.{
.root = b.path("src/ui/c"),
.files = &.{
"ui.c",
"lv_font_courierprimecode_14.c",
"lv_font_courierprimecode_16.c",
"lv_font_courierprimecode_24.c",
},
.flags = &ngui_cflags,
});
ngui.root_module.addCMacro("NM_DISP_HOR", b.fmt("{d}", .{disp_horiz}));
ngui.root_module.addCMacro("NM_DISP_VER", b.fmt("{d}", .{disp_vert}));
ngui.defineCMacro("LV_CONF_INCLUDE_SIMPLE", "1");
ngui.defineCMacro("LV_LOG_LEVEL", lvgl_loglevel.text());
ngui.defineCMacro("LV_TICK_CUSTOM", "1");
ngui.defineCMacro("LV_TICK_CUSTOM_INCLUDE", "\"lv_custom_tick.h\"");
ngui.defineCMacro("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(nm_get_curr_tick())");
switch (drv) {
.sdl2 => {
ngui.addCSourceFiles(lvgl_sdl2_src, &lvgl_flags);
ngui.addCSourceFile(.{ .file = .{ .path = "src/ui/c/drv_sdl2.c" }, .flags = &ngui_cflags });
ngui.addCSourceFiles(.{ .files = lvgl_sdl2_src, .flags = &lvgl_flags });
ngui.addCSourceFile(.{ .file = b.path("src/ui/c/drv_sdl2.c"), .flags = &ngui_cflags });
ngui.defineCMacro("USE_SDL", "1");
ngui.linkSystemLibrary("SDL2");
},
.x11 => {
ngui.addCSourceFiles(lvgl_x11_src, &lvgl_flags);
ngui.addCSourceFiles(&.{
"src/ui/c/drv_x11.c",
"src/ui/c/mouse_cursor_icon.c",
}, &ngui_cflags);
ngui.addCSourceFiles(.{ .files = lvgl_x11_src, .flags = &lvgl_flags });
ngui.addCSourceFiles(.{
.files = &.{
"src/ui/c/drv_x11.c",
"src/ui/c/mouse_cursor_icon.c",
},
.flags = &ngui_cflags,
});
ngui.defineCMacro("USE_X11", "1");
ngui.linkSystemLibrary("X11");
},
.fbev => {
ngui.addCSourceFiles(lvgl_fbev_src, &lvgl_flags);
ngui.addCSourceFile(.{ .file = .{ .path = "src/ui/c/drv_fbev.c" }, .flags = &ngui_cflags });
ngui.addCSourceFiles(.{ .files = lvgl_fbev_src, .flags = &lvgl_flags });
ngui.addCSourceFile(.{ .file = b.path("src/ui/c/drv_fbev.c"), .flags = &ngui_cflags });
ngui.defineCMacro("USE_FBDEV", "1");
ngui.defineCMacro("USE_EVDEV", "1");
},
@ -104,15 +109,15 @@ pub fn build(b: *std.Build) void {
// daemon build
const nd = b.addExecutable(.{
.name = "nd",
.root_source_file = .{ .path = "src/nd.zig" },
.root_source_file = b.path("src/nd.zig"),
.target = target,
.optimize = optimize,
.strip = strip,
});
nd.pie = true;
nd.strip = strip;
nd.addOptions("build_options", buildopts);
nd.addModule("nif", libnif_dep.module("nif"));
nd.addModule("ini", libini);
nd.root_module.addImport("build_options", buildopts_mod);
nd.root_module.addImport("nif", libnif_dep.module("nif"));
nd.root_module.addImport("ini", libini_dep.module("ini"));
nd.linkLibrary(libnif);
const nd_build_step = b.step("nd", "build nd (nakamochi daemon)");
@ -121,15 +126,15 @@ pub fn build(b: *std.Build) void {
// automated tests
{
const tests = b.addTest(.{
.root_source_file = .{ .path = "src/test.zig" },
.root_source_file = b.path("src/test.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.filter = b.option([]const u8, "test-filter", "run tests matching the filter"),
});
tests.addOptions("build_options", buildopts);
tests.addModule("nif", libnif_dep.module("nif"));
tests.addModule("ini", libini);
tests.root_module.addImport("build_options", buildopts_mod);
tests.root_module.addImport("nif", libnif_dep.module("nif"));
tests.root_module.addImport("ini", libini_dep.module("ini"));
tests.linkLibrary(libnif);
const run_tests = b.addRunArtifact(tests);
@ -141,11 +146,11 @@ pub fn build(b: *std.Build) void {
{
const guiplay = b.addExecutable(.{
.name = "guiplay",
.root_source_file = .{ .path = "src/test/guiplay.zig" },
.root_source_file = b.path("src/test/guiplay.zig"),
.target = target,
.optimize = optimize,
});
guiplay.addModule("comm", b.createModule(.{ .source_file = .{ .path = "src/comm.zig" } }));
guiplay.root_module.addImport("comm", b.createModule(.{ .root_source_file = b.path("src/comm.zig") }));
const guiplay_build_step = b.step("guiplay", "build GUI playground");
guiplay_build_step.dependOn(&b.addInstallArtifact(guiplay, .{}).step);
@ -156,12 +161,12 @@ pub fn build(b: *std.Build) void {
{
const btcrpc = b.addExecutable(.{
.name = "btcrpc",
.root_source_file = .{ .path = "src/test/btcrpc.zig" },
.root_source_file = b.path("src/test/btcrpc.zig"),
.target = target,
.optimize = optimize,
.strip = strip,
});
btcrpc.strip = strip;
btcrpc.addModule("bitcoindrpc", b.createModule(.{ .source_file = .{ .path = "src/bitcoindrpc.zig" } }));
btcrpc.root_module.addImport("bitcoindrpc", b.createModule(.{ .root_source_file = b.path("src/bitcoindrpc.zig") }));
const btcrpc_build_step = b.step("btcrpc", "bitcoind RPC client playground");
btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc, .{}).step);
@ -171,12 +176,12 @@ pub fn build(b: *std.Build) void {
{
const lndhc = b.addExecutable(.{
.name = "lndhc",
.root_source_file = .{ .path = "src/test/lndhc.zig" },
.root_source_file = b.path("src/test/lndhc.zig"),
.target = target,
.optimize = optimize,
.strip = strip,
});
lndhc.strip = strip;
lndhc.addModule("lightning", b.createModule(.{ .source_file = .{ .path = "src/lightning.zig" } }));
lndhc.root_module.addImport("lightning", b.createModule(.{ .root_source_file = b.path("src/lightning.zig") }));
const lndhc_build_step = b.step("lndhc", "lnd HTTP API client playground");
lndhc_build_step.dependOn(&b.addInstallArtifact(lndhc, .{}).step);
@ -423,7 +428,7 @@ const VersionStep = struct {
}
fn make(step: *std.Build.Step, _: *std.Progress.Node) anyerror!void {
const self = @fieldParentPtr(VersionStep, "step", step);
const self: *@This() = @fieldParentPtr("step", step);
const semver = try self.eval();
std.log.info("build version: {any}", .{semver});
self.buildopts.addOption(std.SemanticVersion, "semver", semver);
@ -460,7 +465,7 @@ const VersionStep = struct {
const matchTag = self.b.fmt("{s}*.*.*", .{prefix});
const cmd = [_][]const u8{ git, "-C", self.b.pathFromRoot("."), "describe", "--match", matchTag, "--tags", "--abbrev=8" };
var code: u8 = undefined;
const git_describe = self.b.execAllowFail(&cmd, &code, .Ignore) catch return null;
const git_describe = self.b.runAllowFail(&cmd, &code, .Ignore) catch return null;
const repotag = std.mem.trim(u8, git_describe, " \n\r")[prefix.len..];
return std.SemanticVersion.parse(repotag) catch |err| ret: {
std.log.err("unparsable git tag semver '{s}': {any}", .{ repotag, err });

@ -0,0 +1,17 @@
.{
.name = "ndg",
.version = "0.8.1",
.dependencies = .{
.nif = .{
.path = "lib/nif",
},
.ini = .{
.path = "lib/ini",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

@ -2,9 +2,10 @@ const std = @import("std");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const target = b.standardTargetOptions(.{});
_ = b.addModule("ini", .{
.source_file = .{
.root_source_file = .{
.path = "src/ini.zig",
},
});
@ -12,9 +13,10 @@ pub fn build(b: *std.Build) void {
const lib = b.addStaticLibrary(.{
.name = "ini",
.root_source_file = .{ .path = "src/lib.zig" },
.target = b.standardTargetOptions(.{}),
.target = target,
.optimize = optimize,
});
lib.bundle_compiler_rt = true;
lib.addIncludePath(.{ .path = "src" });
lib.linkLibC();
@ -24,6 +26,7 @@ pub fn build(b: *std.Build) void {
const example_c = b.addExecutable(.{
.name = "example-c",
.optimize = optimize,
.target = target,
});
example_c.addCSourceFile(.{
.file = .{
@ -45,8 +48,9 @@ pub fn build(b: *std.Build) void {
.name = "example-zig",
.root_source_file = .{ .path = "example/example.zig" },
.optimize = optimize,
.target = target,
});
example_zig.addModule("ini", b.modules.get("ini").?);
example_zig.root_module.addImport("ini", b.modules.get("ini").?);
b.installArtifact(example_zig);

@ -0,0 +1,10 @@
.{
.name = "libini",
.version = "0.0.0",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

@ -78,7 +78,7 @@ test "buffer parser" {
}
test "file parser" {
var file = c.fopen("example/example.ini", "rb") orelse unreachable;
const file = c.fopen("example/example.ini", "rb") orelse unreachable;
defer _ = c.fclose(file);
var parser: c.ini_Parser = undefined;

@ -1,30 +1,33 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
_ = b.addModule("nif", .{ .source_file = .{ .path = "nif.zig" } });
_ = b.addModule("nif", .{ .root_source_file = b.path("nif.zig") });
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const lib = b.addStaticLibrary(.{
.name = "nif",
.root_source_file = .{ .path = "nif.zig" },
.root_source_file = b.path("nif.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
lib.defineCMacro("CONFIG_CTRL_IFACE", null);
lib.defineCMacro("CONFIG_CTRL_IFACE_UNIX", null);
lib.addIncludePath(.{ .path = "wpa_supplicant" });
lib.addCSourceFiles(&.{
"wpa_supplicant/wpa_ctrl.c",
"wpa_supplicant/os_unix.c",
}, &.{
"-Wall",
"-Wextra",
"-Wshadow",
"-Wundef",
"-Wunused-parameter",
"-Werror",
lib.addIncludePath(b.path("wpa_supplicant"));
lib.addCSourceFiles(.{
.files = &.{
"wpa_supplicant/wpa_ctrl.c",
"wpa_supplicant/os_unix.c",
},
.flags = &.{
"-Wall",
"-Wextra",
"-Wshadow",
"-Wundef",
"-Wunused-parameter",
"-Werror",
},
});
b.installArtifact(lib);
}

@ -0,0 +1,10 @@
.{
.name = "libnif",
.version = "0.0.1",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

@ -1,7 +1,7 @@
const std = @import("std");
const mem = std.mem;
const net = std.net;
const os = std.os;
const posix = std.posix;
pub const wpa = @import("wpa.zig");
@ -12,11 +12,11 @@ const ifaddrs = extern struct {
next: ?*ifaddrs,
name: [*:0]const u8,
flags: c_uint, // see IFF_xxx SIOCGIFFLAGS in netdevice(7)
addr: ?*std.os.sockaddr,
netmask: ?*std.os.sockaddr,
addr: ?*std.posix.sockaddr,
netmask: ?*std.posix.sockaddr,
ifu: extern union {
broad: *os.sockaddr, // flags & IFF_BROADCAST
dst: *os.sockaddr, // flags & IFF_POINTOPOINT
broad: *posix.sockaddr, // flags & IFF_BROADCAST
dst: *posix.sockaddr, // flags & IFF_POINTOPOINT
},
data: ?*anyopaque,
};
@ -37,8 +37,8 @@ pub fn pubAddresses(allocator: mem.Allocator, ifname: ?[]const u8) ![]net.Addres
var list = std.ArrayList(net.Address).init(allocator);
var it: ?*ifaddrs = res;
while (it) |ifa| : (it = ifa.next) {
const sa: *os.sockaddr = ifa.addr orelse continue;
if (sa.family != os.AF.INET and sa.family != os.AF.INET6) {
const sa: *posix.sockaddr = ifa.addr orelse continue;
if (sa.family != posix.AF.INET and sa.family != posix.AF.INET6) {
// not an IP address
continue;
}
@ -47,7 +47,7 @@ pub fn pubAddresses(allocator: mem.Allocator, ifname: ?[]const u8) ![]net.Addres
continue;
}
const ipaddr = net.Address.initPosix(@alignCast(sa)); // initPosix makes a copy
if (ipaddr.any.family == os.AF.INET6 and ipaddr.in6.sa.scope_id > 0) {
if (ipaddr.any.family == posix.AF.INET6 and ipaddr.in6.sa.scope_id > 0) {
// want only global, with 0 scope
// non-zero scopes make sense for link-local addr only.
continue;

@ -2,7 +2,7 @@
const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator;
const Atomic = std.atomic.Atomic;
const Atomic = std.atomic.Value;
const base64enc = std.base64.standard.Encoder;
const types = @import("types.zig");
@ -13,7 +13,7 @@ pub const Client = struct {
addr: []const u8 = "127.0.0.1",
port: u16 = 8332,
// each request gets a new ID with a value of reqid.fetchAdd(1, .Monotonic)
// each request gets a new ID with a value of reqid.fetchAdd(1, .monotonic)
reqid: Atomic(u64) = Atomic(u64).init(1),
pub const Method = enum {
@ -153,7 +153,7 @@ pub const Client = struct {
/// callers own returned value.
fn formatreq(self: *Client, comptime m: Method, args: MethodArgs(m)) ![]const u8 {
const req = RpcRequest(m){
.id = self.reqid.fetchAdd(1, .Monotonic),
.id = self.reqid.fetchAdd(1, .monotonic),
.method = @tagName(m),
.params = args,
};
@ -183,7 +183,7 @@ pub const Client = struct {
defer file.close();
const cookie = try file.readToEndAlloc(self.allocator, 1024);
defer self.allocator.free(cookie);
var auth = try self.allocator.alloc(u8, base64enc.calcSize(cookie.len));
const auth = try self.allocator.alloc(u8, base64enc.calcSize(cookie.len));
return base64enc.encode(auth, cookie);
}

@ -56,15 +56,15 @@ pub const MessageTag = enum(u16) {
ping = 0x01,
pong = 0x02,
poweroff = 0x03,
wifi_connect = 0x04,
network_report = 0x05,
get_network_report = 0x06,
// nd -> ngui: reports poweroff progress
poweroff_progress = 0x09,
// 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,
wifi_connect = 0x04,
network_report = 0x05,
get_network_report = 0x06,
// nd -> ngui: bitcoin core daemon status report
onchain_report = 0x0a,
// nd -> ngui: lnd status and stats report
@ -89,7 +89,13 @@ pub const MessageTag = enum(u16) {
set_nodename = 0x15,
// nd -> ngui: all ndg settings
settings = 0x0d,
// next: 0x16
// ngui -> nd: verify pincode
unlock_screen = 0x17,
// nd -> ngui: result of try_unlock
screen_unlock_result = 0x18,
// ngui -> nd: set or disable screenlock pin code
slock_set_pincode = 0x19,
// next: 0x1a
};
/// daemon and gui exchange messages of this type.
@ -97,12 +103,12 @@ pub const Message = union(MessageTag) {
ping: void,
pong: void,
poweroff: void,
poweroff_progress: PoweroffProgress,
standby: void,
wakeup: void,
wifi_connect: WifiConnect,
network_report: NetworkReport,
get_network_report: GetNetworkReport,
poweroff_progress: PoweroffProgress,
onchain_report: OnchainReport,
lightning_report: LightningReport,
lightning_error: LightningError,
@ -115,6 +121,9 @@ pub const Message = union(MessageTag) {
switch_sysupdates: SysupdatesChan,
set_nodename: []const u8,
settings: Settings,
unlock_screen: []const u8, // pincode
screen_unlock_result: ScreenUnlockResult,
slock_set_pincode: ?[]const u8,
pub const WifiConnect = struct {
ssid: []const u8,
@ -250,11 +259,17 @@ pub const Message = union(MessageTag) {
};
pub const Settings = struct {
slock_enabled: bool,
hostname: []const u8, // see .set_nodename
sysupdates: struct {
channel: SysupdatesChan,
},
};
pub const ScreenUnlockResult = struct {
ok: bool,
err: ?[]const u8 = null, // error message when !ok
};
};
/// the return value type from `read` fn.
@ -279,8 +294,8 @@ pub const ParsedMessage = struct {
/// callers must deallocate resources with ParsedMessage.deinit when done.
pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
// alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic.
const tag = try reader.readEnum(MessageTag, .Little);
const len = try reader.readIntLittle(u64);
const tag = try reader.readEnum(MessageTag, .little);
const len = try reader.readInt(u64, .little);
if (len == 0) {
return switch (tag) {
.lightning_get_ctrlconn => .{ .value = .lightning_get_ctrlconn },
@ -303,7 +318,7 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
.wakeup,
=> unreachable, // handled above
inline else => |t| {
var bytes = try allocator.alloc(u8, len);
const bytes = try allocator.alloc(u8, len);
defer allocator.free(bytes);
try reader.readNoEof(bytes);
@ -347,13 +362,16 @@ pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
.switch_sysupdates => try json.stringify(msg.switch_sysupdates, .{}, data.writer()),
.set_nodename => try json.stringify(msg.set_nodename, .{}, data.writer()),
.settings => try json.stringify(msg.settings, .{}, data.writer()),
.unlock_screen => try json.stringify(msg.unlock_screen, .{}, data.writer()),
.screen_unlock_result => try json.stringify(msg.screen_unlock_result, .{}, data.writer()),
.slock_set_pincode => try json.stringify(msg.slock_set_pincode, .{}, data.writer()),
}
if (data.items.len > std.math.maxInt(u64)) {
return Error.CommWriteTooLarge;
}
try writer.writeIntLittle(u16, @intFromEnum(msg));
try writer.writeIntLittle(u64, data.items.len);
try writer.writeInt(u16, @intFromEnum(msg), .little);
try writer.writeInt(u64, data.items.len, .little);
try writer.writeAll(data.items);
}
@ -383,8 +401,8 @@ test "read" {
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
try buf.writer().writeIntLittle(u16, @intFromEnum(msg));
try buf.writer().writeIntLittle(u64, data.items.len);
try buf.writer().writeInt(u16, @intFromEnum(msg), .little);
try buf.writer().writeInt(u64, data.items.len, .little);
try buf.writer().writeAll(data.items);
var bs = std.io.fixedBufferStream(buf.items);
@ -406,8 +424,8 @@ test "write" {
const payload = "{\"ssid\":\"wlan\",\"password\":\"secret\"}";
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.writer().writeInt(u16, @intFromEnum(msg), .little);
try js.writer().writeInt(u64, payload.len, .little);
try js.appendSlice(payload);
try t.expectEqualStrings(js.items, buf.items);
@ -424,8 +442,8 @@ test "write enum" {
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.writer().writeInt(u16, @intFromEnum(msg), .little);
try js.writer().writeInt(u64, payload.len, .little);
try js.appendSlice(payload);
try t.expectEqualStrings(js.items, buf.items);

@ -36,7 +36,7 @@ pub const Section = struct {
const vdup = try self.alloc.dupe(u8, value);
errdefer self.alloc.free(vdup);
var res = try self.props.getOrPut(try self.alloc.dupe(u8, key));
const res = try self.props.getOrPut(try self.alloc.dupe(u8, key));
if (!res.found_existing) {
res.value_ptr.* = .{ .str = vdup };
return;
@ -180,7 +180,7 @@ pub fn appendDefaultSection(self: *LndConf) !*Section {
/// the section name ascii is converted to lower case.
pub fn appendSection(self: *LndConf, name: []const u8) !*Section {
const alloc = self.arena.allocator();
var low_name = try std.ascii.allocLowerString(alloc, name);
const low_name = try std.ascii.allocLowerString(alloc, name);
try self.sections.append(.{
.name = low_name,
.props = std.StringArrayHashMap(PropValue).init(alloc),

@ -143,22 +143,29 @@ pub const Client = struct {
pub fn call(self: *Client, comptime apimethod: ApiMethod, args: MethodArgs(apimethod)) !Result(apimethod) {
const formatted = try self.formatreq(apimethod, args);
defer formatted.deinit();
var headersbuf: [8 * 1024]u8 = undefined;
const reqinfo = formatted.value;
const opt = std.http.Client.Options{ .handle_redirects = false }; // no redirects in REST API
var req = try self.httpClient.request(reqinfo.httpmethod, reqinfo.url, reqinfo.headers, opt);
const opt = std.http.Client.RequestOptions{
.redirect_behavior = .not_allowed, // no redirects in REST API
.headers = reqinfo.stdheaders,
.privileged_headers = reqinfo.xheaders,
.server_header_buffer = &headersbuf,
};
var req = try self.httpClient.open(reqinfo.httpmethod, reqinfo.url, opt);
defer req.deinit();
if (reqinfo.payload) |p| {
req.transfer_encoding = .{ .content_length = p.len };
}
try req.start();
try req.send();
if (reqinfo.payload) |p| {
req.writer().writeAll(p) catch return Error.LndPayloadWriteFail;
req.writeAll(p) catch return Error.LndPayloadWriteFail;
try req.finish();
}
try req.wait();
if (req.response.status.class() != .success) {
// a structured error reporting in lnd is in a less than desirable state.
// a structured error reporting in lnd is unclear:
// https://github.com/lightningnetwork/lnd/issues/5586
// TODO: return a more detailed error when the upstream improves.
return Error.LndHttpBadStatusCode;
@ -181,8 +188,9 @@ pub const Client = struct {
const HttpReqInfo = struct {
httpmethod: std.http.Method,
url: std.Uri,
headers: std.http.Headers,
payload: ?[]const u8,
stdheaders: std.http.Client.Request.Headers = .{}, // overridable standard headers
xheaders: []const std.http.Header = &.{}, // any extra headers
payload: ?[]const u8 = null,
};
fn formatreq(self: Client, comptime apimethod: ApiMethod, args: MethodArgs(apimethod)) !types.Deinitable(HttpReqInfo) {
@ -194,12 +202,10 @@ pub const Client = struct {
.genseed, .walletstatus => |m| .{
.httpmethod = .GET,
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
.headers = std.http.Headers{ .allocator = arena },
.payload = null,
},
.initwallet => |m| blk: {
const payload = p: {
var params: struct {
const params: struct {
wallet_password: []const u8, // base64
cipher_seed_mnemonic: []const []const u8,
aezeed_passphrase: ?[]const u8 = null, // base64
@ -215,13 +221,12 @@ pub const Client = struct {
break :blk .{
.httpmethod = .POST,
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
.headers = std.http.Headers{ .allocator = arena },
.payload = payload,
};
},
.unlockwallet => |m| blk: {
const payload = p: {
var params: struct {
const params: struct {
wallet_password: []const u8, // base64
} = .{
.wallet_password = try base64EncodeAlloc(arena, args.unlock_password),
@ -233,20 +238,19 @@ pub const Client = struct {
break :blk .{
.httpmethod = .POST,
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
.headers = std.http.Headers{ .allocator = arena },
.payload = payload,
};
},
.feereport, .getinfo, .getnetworkinfo, .pendingchannels, .walletbalance => |m| .{
.httpmethod = .GET,
.url = try std.Uri.parse(try std.fmt.allocPrint(arena, "{s}/{s}", .{ self.apibase, m.apipath() })),
.headers = blk: {
.xheaders = blk: {
if (self.macaroon.readonly == null) {
return Error.LndHttpMissingMacaroon;
}
var h = std.http.Headers{ .allocator = arena };
try h.append(authHeaderName, self.macaroon.readonly.?);
break :blk h;
var h = std.ArrayList(std.http.Header).init(arena);
try h.append(.{ .name = authHeaderName, .value = self.macaroon.readonly.? });
break :blk try h.toOwnedSlice();
},
.payload = null,
},
@ -270,13 +274,13 @@ pub const Client = struct {
}
break :blk try std.Uri.parse(buf.items); // uri point to the original buf
},
.headers = blk: {
.xheaders = blk: {
if (self.macaroon.readonly == null) {
return Error.LndHttpMissingMacaroon;
}
var h = std.http.Headers{ .allocator = arena };
try h.append(authHeaderName, self.macaroon.readonly.?);
break :blk h;
var h = std.ArrayList(std.http.Header).init(arena);
try h.append(.{ .name = authHeaderName, .value = self.macaroon.readonly.? });
break :blk try h.toOwnedSlice();
},
.payload = null,
},
@ -299,7 +303,7 @@ pub const Client = struct {
}
fn base64EncodeAlloc(gpa: std.mem.Allocator, v: []const u8) ![]const u8 {
var buf = try gpa.alloc(u8, base64enc.calcSize(v.len));
const buf = try gpa.alloc(u8, base64enc.calcSize(v.len));
return base64enc.encode(buf, v); // always returns a slice of buf.len
}
};

@ -1,13 +1,13 @@
const buildopts = @import("build_options");
const std = @import("std");
const os = std.os;
const sys = os.system;
const posix = std.posix;
const time = std.time;
const Address = std.net.Address;
const nif = @import("nif");
const comm = @import("comm.zig");
const Config = @import("nd/Config.zig");
const Daemon = @import("nd/Daemon.zig");
const screen = @import("ui/screen.zig");
@ -136,7 +136,7 @@ fn sighandler(sig: c_int) callconv(.C) void {
return;
}
switch (sig) {
os.SIG.INT, os.SIG.TERM => sigquit.set(),
posix.SIG.INT, posix.SIG.TERM => sigquit.set(),
else => {},
}
}
@ -158,9 +158,19 @@ pub fn main() !void {
// of its previous state.
screen.backlight(.on) catch |err| logger.err("backlight: {any}", .{err});
// load config file to figure out whether to start ngui in screenlocked mode.
const conf = try Config.init(gpa, args.conf.?);
defer conf.deinit();
// start ngui, unless -nogui mode
const gui_path = args.gui.?; // guaranteed to be non-null
var ngui = std.ChildProcess.init(&.{gui_path}, gpa);
var ngui_args = std.ArrayList([]const u8).init(gpa);
defer ngui_args.deinit();
try ngui_args.append(gui_path);
if (conf.data.slock != null) {
try ngui_args.append("-slock");
}
var ngui = std.ChildProcess.init(ngui_args.items, gpa);
ngui.stdin_behavior = .Pipe;
ngui.stdout_behavior = .Pipe;
ngui.stderr_behavior = .Inherit;
@ -200,7 +210,7 @@ pub fn main() !void {
var nd = try Daemon.init(.{
.allocator = gpa,
.confpath = args.conf.?,
.conf = conf,
.uir = uireader,
.uiw = uiwriter,
.wpa = args.wpa.?,
@ -209,13 +219,13 @@ pub fn main() !void {
try nd.start();
// graceful shutdown; see sigaction(2)
const sa = os.Sigaction{
const sa = posix.Sigaction{
.handler = .{ .handler = sighandler },
.mask = os.empty_sigset,
.mask = posix.empty_sigset,
.flags = 0,
};
try os.sigaction(os.SIG.INT, &sa, null);
try os.sigaction(os.SIG.TERM, &sa, null);
try posix.sigaction(posix.SIG.INT, &sa, null);
try posix.sigaction(posix.SIG.TERM, &sa, null);
sigquit.wait();
logger.info("sigquit: terminating ...", .{});

@ -40,7 +40,13 @@ data: Data,
/// top struct stored on disk.
/// access with `safeReadOnly` or lock/unlock `mu`.
///
/// for backwards compatibility, all newly introduced fields must have default values.
pub const Data = struct {
slock: ?struct { // null indicates screenlock is disabled
bcrypt_hash: []const u8, // std.crypto.bcrypt .phc format
incorrect_attempts: u8, // reset after each successful unlock
} = null,
syschannel: SysupdatesChannel,
syscronscript: []const u8,
sysrunscript: []const u8,
@ -139,7 +145,7 @@ fn inferStaticData(allocator: std.mem.Allocator) !StaticData {
}
fn inferLndTorHostname(allocator: std.mem.Allocator) ![]const u8 {
var raw = try std.fs.cwd().readFileAlloc(allocator, TOR_DATA_DIR ++ "/lnd/hostname", 1024);
const raw = try std.fs.cwd().readFileAlloc(allocator, TOR_DATA_DIR ++ "/lnd/hostname", 1024);
const hostname = std.mem.trim(u8, raw, &std.ascii.whitespace);
logger.info("inferred lnd tor hostname: [{s}]", .{hostname});
return hostname;
@ -150,7 +156,7 @@ fn inferBitcoindRpcPass(allocator: std.mem.Allocator) ![]const u8 {
// the password was placed on a separate comment line, preceding another comment
// line containing "rpcauth.py".
// TODO: get rid of the hack; do something more robust
var conf = try std.fs.cwd().readFileAlloc(allocator, BITCOIND_CONFIG_PATH, 1024 * 1024);
const conf = try std.fs.cwd().readFileAlloc(allocator, BITCOIND_CONFIG_PATH, 1024 * 1024);
var it = std.mem.tokenizeScalar(u8, conf, '\n');
var next_is_pass = false;
while (it.next()) |line| {
@ -175,6 +181,50 @@ pub fn safeReadOnly(self: *Config, comptime F: anytype) @typeInfo(@TypeOf(F)).Fn
return F(self.data, self.static);
}
/// matches the `input` against the hash in `Data.slock.bcrypt_hash` previously set with `setSlockPin`.
/// incrementing `Data.slock.incorrect_attempts` each unsuccessful result.
/// the number of attemps are persisted at `Config.confpath` upon function return.
pub fn verifySlockPin(self: *Config, input: []const u8) !void {
self.mu.lock();
defer self.mu.unlock();
const slock = self.data.slock orelse return;
defer self.dumpUnguarded() catch |errdump| logger.err("dumpUnguarded: {!}", .{errdump});
std.crypto.pwhash.bcrypt.strVerify(slock.bcrypt_hash, input, .{}) catch |err| {
if (err == error.PasswordVerificationFailed) {
self.data.slock.?.incorrect_attempts += 1;
return error.IncorrectSlockPin;
}
logger.err("bcrypt.strVerify: {!}", .{err});
return err;
};
self.data.slock.?.incorrect_attempts = 0;
}
/// enables or disables screenlock, persistently. null `code` indicates disabled.
/// safe for concurrent use.
pub fn setSlockPin(self: *Config, code: ?[]const u8) !void {
self.mu.lock();
defer self.mu.unlock();
// TODO: free existing slock.bcrypt_hash? it is in arena but still
if (code) |s| {
const bcrypt = std.crypto.pwhash.bcrypt;
const opt: bcrypt.HashOptions = .{
.params = .{ .rounds_log = 12 },
.encoding = .phc,
.silently_truncate_password = false,
};
var buf: [bcrypt.hash_length * 2]u8 = undefined;
const hash = try bcrypt.strHash(s, opt, &buf);
self.data.slock = .{
.bcrypt_hash = try self.arena.allocator().dupe(u8, hash),
.incorrect_attempts = 0,
};
} else {
self.data.slock = null;
}
try self.dumpUnguarded();
}
/// used by mutateLndConf to guard concurrent access.
var lndconf_mu: std.Thread.Mutex = .{};
@ -296,7 +346,7 @@ fn genSysupdatesCronScript(self: Config) !void {
///
/// the caller must serialize this function calls.
fn runSysupdates(allocator: std.mem.Allocator, scriptpath: []const u8) !void {
const res = try std.ChildProcess.exec(.{ .allocator = allocator, .argv = &.{scriptpath} });
const res = try std.ChildProcess.run(.{ .allocator = allocator, .argv = &.{scriptpath} });
defer {
allocator.free(res.stdout);
allocator.free(res.stderr);
@ -333,7 +383,7 @@ pub fn lndConnectWaitMacaroonFile(self: Config, allocator: std.mem.Allocator, ty
defer allocator.free(macaroon);
const base64enc = std.base64.url_safe_no_pad.Encoder;
var buf = try allocator.alloc(u8, base64enc.calcSize(macaroon.len));
const buf = try allocator.alloc(u8, base64enc.calcSize(macaroon.len));
defer allocator.free(buf);
const macaroon_b64 = base64enc.encode(buf, macaroon);
const port: u16 = switch (typ) {
@ -549,7 +599,7 @@ test "ndconfig: switch sysupdates with .run=true" {
const tt = @import("../test.zig");
// no arena deinit here: expecting Config to auto-deinit.
var conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
const conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
conf_arena.* = std.heap.ArenaAllocator.init(std.testing.allocator);
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
@ -594,7 +644,7 @@ test "ndconfig: genLndConfig" {
const tt = @import("../test.zig");
// Config auto-deinits the arena.
var conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
const conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
conf_arena.* = std.heap.ArenaAllocator.init(std.testing.allocator);
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
@ -645,7 +695,7 @@ test "ndconfig: mutate LndConf" {
const tt = @import("../test.zig");
// Config auto-deinits the arena.
var conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
const conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
conf_arena.* = std.heap.ArenaAllocator.init(t.allocator);
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
@ -681,3 +731,92 @@ test "ndconfig: mutate LndConf" {
\\
, cont);
}
test "ndconfig: screen lock" {
const t = std.testing;
const tt = @import("../test.zig");
// Config auto-deinits the arena.
const conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator);
conf_arena.* = std.heap.ArenaAllocator.init(t.allocator);
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
// nonexistent config file
{
var conf = try init(t.allocator, "/nonexistent.json");
defer conf.deinit();
try t.expect(conf.data.slock == null);
try conf.verifySlockPin("");
try conf.verifySlockPin("any");
}
// conf file without slock field
{
const confpath = try tmp.join(&.{"conf.json"});
try tmp.dir.writeFile(confpath,
\\{
\\"syschannel": "dev",
\\"syscronscript": "/cron/sysupdates.sh",
\\"sysrunscript": "/sysupdates/run.sh"
\\}
);
var conf = try init(t.allocator, confpath);
defer conf.deinit();
try t.expect(conf.data.slock == null);
try conf.verifySlockPin("");
try conf.verifySlockPin("0000");
}
// conf file with null slock
{
const confpath = try tmp.join(&.{"conf.json"});
try tmp.dir.writeFile(confpath,
\\{
\\"slock": null,
\\"syschannel": "dev",
\\"syscronscript": "/cron/sysupdates.sh",
\\"sysrunscript": "/sysupdates/run.sh"
\\}
);
var conf = try init(t.allocator, confpath);
defer conf.deinit();
try t.expect(conf.data.slock == null);
try conf.verifySlockPin("");
try conf.verifySlockPin("1111");
}
const newpinconf = try tmp.join(&.{"newconf.json"});
{
var conf = Config{
.arena = conf_arena,
.confpath = newpinconf,
.data = .{
.slock = null,
.syschannel = .master, // unused
.syscronscript = undefined, // unused
.sysrunscript = undefined, // unused
},
.static = undefined, // unused
};
defer conf.deinit();
// any pin should workd because slock is null
try conf.verifySlockPin("");
try conf.verifySlockPin("any");
// set a new pin code
try conf.setSlockPin("1357");
try conf.verifySlockPin("1357");
try t.expectError(error.IncorrectSlockPin, conf.verifySlockPin(""));
try t.expectError(error.IncorrectSlockPin, conf.verifySlockPin("any"));
}
// load conf from file and check
{
var conf = try init(t.allocator, newpinconf);
defer conf.deinit();
try t.expect(conf.data.slock != null);
try conf.setSlockPin("1357");
try conf.verifySlockPin("1357");
try t.expectError(error.IncorrectSlockPin, conf.verifySlockPin("any2"));
}
}

@ -32,6 +32,9 @@ uireader: std.fs.File.Reader, // ngui stdout
uiwriter: std.fs.File.Writer, // ngui stdin
wpa_ctrl: types.WpaControl, // guarded by mu once start'ed
/// used only in comm thread; move under mu when no longer the case
screenstate: enum { locked, unlocked },
/// guards all the fields below to sync between pub fns and main/poweroff threads.
mu: std.Thread.Mutex = .{},
@ -118,7 +121,7 @@ const Error = error{
const InitOpt = struct {
allocator: std.mem.Allocator,
confpath: []const u8,
conf: Config,
uir: std.fs.File.Reader,
uiw: std.fs.File.Writer,
wpa: [:0]const u8,
@ -138,15 +141,14 @@ pub fn init(opt: InitOpt) !Daemon {
try svlist.append(sys.Service.init(opt.allocator, sys.Service.LND, .{ .stop_wait_sec = 600 }));
try svlist.append(sys.Service.init(opt.allocator, sys.Service.BITCOIND, .{ .stop_wait_sec = 600 }));
const conf = try Config.init(opt.allocator, opt.confpath);
errdefer conf.deinit();
return .{
.allocator = opt.allocator,
.conf = conf,
.conf = opt.conf,
.uireader = opt.uir,
.uiwriter = opt.uiw,
.wpa_ctrl = try types.WpaControl.open(opt.wpa),
.state = .stopped,
.screenstate = if (opt.conf.data.slock != null) .locked else .unlocked,
.services = .{ .list = try svlist.toOwnedSlice() },
// send persisted settings immediately on start
.want_settings = true,
@ -168,7 +170,6 @@ pub fn init(opt: InitOpt) !Daemon {
pub fn deinit(self: *Daemon) void {
self.wpa_ctrl.close() catch |err| logger.err("deinit: wpa_ctrl.close: {any}", .{err});
self.services.deinit(self.allocator);
self.conf.deinit();
}
/// start launches daemon threads and returns immediately.
@ -184,6 +185,7 @@ pub fn start(self: *Daemon) !void {
}
try self.wpa_ctrl.attach();
self.want_stop = false;
errdefer {
self.wpa_ctrl.detach() catch {};
self.want_stop = true;
@ -224,7 +226,6 @@ pub fn wait(self: *Daemon) void {
}
self.wpa_ctrl.detach() catch |err| logger.err("wait: wpa_ctrl.detach: {any}", .{err});
self.want_stop = false;
self.state = .stopped;
}
@ -237,8 +238,16 @@ fn standby(self: *Daemon) !void {
.stopped, .poweroff => return Error.InvalidState,
.wallet_reset => return Error.WalletResetActive,
.running => {
try screen.backlight(.off);
const has_lock = self.conf.safeReadOnly(struct {
fn f(data: Config.Data, _: Config.StaticData) bool {
return data.slock != null;
}
}.f);
if (has_lock) {
self.screenstate = .locked;
}
self.state = .standby;
try screen.backlight(.off);
},
}
}
@ -338,6 +347,7 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
fn f(conf: Config.Data, static: Config.StaticData) bool {
const msg: comm.Message.Settings = .{
.hostname = static.hostname,
.slock_enabled = conf.slock != null,
.sysupdates = .{
.channel = switch (conf.syschannel) {
.dev => .edge,
@ -374,6 +384,7 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
// onchain bitcoin stats
if (self.want_onchain_report or self.bitcoin_timer.read() > self.onchain_report_interval) {
// TODO: this takes too long; run in a separate thread
if (self.sendOnchainReport()) {
self.bitcoin_timer.reset();
self.want_onchain_report = false;
@ -385,6 +396,7 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
// lightning stats
if (self.state != .wallet_reset) {
if (self.want_lnd_report or self.lnd_timer.read() > self.lnd_report_interval) {
// TODO: this takes too long; run in a separate thread
if (self.sendLightningReport()) {
self.lnd_timer.reset();
self.want_lnd_report = false;
@ -439,9 +451,13 @@ fn commThreadLoop(self: *Daemon) void {
self.reportNetworkStatus(.{ .scan = req.scan });
},
.wifi_connect => |req| {
self.startConnectWifi(req.ssid, req.password) catch |err| {
logger.err("startConnectWifi: {any}", .{err});
};
if (self.screenstate != .locked) {
self.startConnectWifi(req.ssid, req.password) catch |err| {
logger.err("startConnectWifi: {any}", .{err});
};
} else {
logger.warn("refusing wifi connect: screen is locked", .{});
}
},
.standby => {
logger.info("entering standby mode", .{});
@ -452,38 +468,68 @@ fn commThreadLoop(self: *Daemon) void {
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
};
if (self.screenstate != .locked) {
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("ignoring sysupdates switch: screen is locked", .{});
}
},
.set_nodename => |newname| {
self.setNodename(newname) catch |err| {
logger.err("setNodename: {!}", .{err});
// TODO: send err back to ngui
};
if (self.screenstate != .locked) {
self.setNodename(newname) catch |err| {
logger.err("setNodename: {!}", .{err});
// TODO: send err back to ngui
};
} else {
logger.warn("ignoring nodename change: screen is locked", .{});
}
},
.lightning_genseed => {
// non commital: ok even if the screen is locked
self.generateWalletSeed() catch |err| {
logger.err("generateWalletSeed: {!}", .{err});
// TODO: send err back to ngui
};
},
.lightning_init_wallet => |req| {
self.initWallet(req) catch |err| {
logger.err("initWallet: {!}", .{err});
// TODO: send err back to ngui
};
if (self.screenstate != .locked) {
self.initWallet(req) catch |err| {
logger.err("initWallet: {!}", .{err});
// TODO: send err back to ngui
};
} else {
logger.warn("ignoring lnd wallet init: screen is locked", .{});
}
},
.lightning_get_ctrlconn => {
self.sendLightningPairingConn() catch |err| {
logger.err("sendLightningPairingConn: {!}", .{err});
// TODO: send err back to ngui
};
if (self.screenstate != .locked) {
self.sendLightningPairingConn() catch |err| {
logger.err("sendLightningPairingConn: {!}", .{err});
// TODO: send err back to ngui
};
} else {
logger.warn("refusing to give out lnd pairing: screen is locked", .{});
}
},
.lightning_reset => {
self.resetLndNode() catch |err| logger.err("resetLndNode: {!}", .{err});
if (self.screenstate != .locked) {
self.resetLndNode() catch |err| logger.err("resetLndNode: {!}", .{err});
} else {
logger.warn("refusing lnd reset: screen is locked", .{});
}
},
.slock_set_pincode => |pincode_or_null| {
self.conf.setSlockPin(pincode_or_null) catch |err| logger.err("conf.setSlockPin: {!}", .{err});
self.mu.lock();
self.want_settings = true;
self.mu.unlock();
},
.unlock_screen => |pincode| {
self.unlockScreen(pincode) catch |err| logger.err("unlockScreen: {!}", .{err});
},
else => |v| logger.warn("unhandled msg tag {s}", .{@tagName(v)}),
}
@ -496,9 +542,29 @@ fn commThreadLoop(self: *Daemon) void {
logger.info("exiting comm thread loop", .{});
}
/// all callers must belong to comm thread due to self.screenstate access.
fn unlockScreen(self: *Daemon, pincode: []const u8) !void {
const pindup = try self.allocator.dupe(u8, pincode);
defer self.allocator.free(pindup);
// TODO: slow down
self.conf.verifySlockPin(pindup) catch |err| {
if (!builtin.is_test) { // logging err makes some tests fail
logger.err("verifySlockPin: {!}", .{err});
}
const errmsg: comm.Message = .{ .screen_unlock_result = .{
.ok = false,
.err = if (err == error.IncorrectSlockPin) "incorrect pin code" else "unlock failed",
} };
return comm.write(self.allocator, self.uiwriter, errmsg);
};
const ok: comm.Message = .{ .screen_unlock_result = .{ .ok = true } };
comm.write(self.allocator, self.uiwriter, ok) catch |err| logger.err("{!}", .{err});
self.screenstate = .unlocked;
}
/// sends poweroff progress to uiwriter in comm.Message.PoweroffProgress format.
fn sendPoweroffReport(self: *Daemon) !void {
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.list.len);
const svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.list.len);
defer self.allocator.free(svstat);
for (self.services.list, svstat) |*sv, *stat| {
stat.* = .{
@ -898,7 +964,7 @@ fn processLndReportError(self: *Daemon, err: anyerror) !void {
error.FileNotFound, // tls cert file missing, not re-generated by lnd yet
=> return comm.write(self.allocator, self.uiwriter, msg_starting),
// old tls cert, refused by our http client
std.http.Client.ConnectUnproxiedError.TlsInitializationFailed => {
std.http.Client.ConnectTcpError.TlsInitializationFailed => {
try self.resetLndTlsUnguarded();
return error.LndReportRetryLater;
},
@ -942,7 +1008,7 @@ fn sendLightningPairingConn(self: *Daemon) !void {
defer self.allocator.free(tor_rpc);
const tor_http = try self.conf.lndConnectWaitMacaroonFile(self.allocator, .tor_http);
defer self.allocator.free(tor_http);
var conn: comm.Message.LightningCtrlConn = &.{
const conn: comm.Message.LightningCtrlConn = &.{
.{ .url = tor_rpc, .typ = .lnd_rpc, .perm = .admin },
.{ .url = tor_http, .typ = .lnd_http, .perm = .admin },
};
@ -1193,7 +1259,7 @@ fn setNodenameInternal(self: *Daemon, newname: []const u8) !void {
/// replaces whitespace with space literal and ignores ascii control chars.
/// caller owns returned value.
fn allocSanitizeNodename(allocator: std.mem.Allocator, name: []const u8) ![]const u8 {
if (name.len == 0 or try std.unicode.utf8CountCodepoints(name) > std.os.HOST_NAME_MAX) {
if (name.len == 0 or try std.unicode.utf8CountCodepoints(name) > std.posix.HOST_NAME_MAX) {
return error.InvalidNodenameLength;
}
var sanitized = try std.ArrayList(u8).initCapacity(allocator, name.len);
@ -1217,17 +1283,18 @@ fn allocSanitizeNodename(allocator: std.mem.Allocator, name: []const u8) ![]cons
return allocator.dupe(u8, trimmed);
}
test "start-stop" {
test "daemon: start-stop" {
const t = std.testing;
const pipe = try types.IoPipe.create();
var daemon = try Daemon.init(.{
.allocator = t.allocator,
.confpath = "/unused.json",
.conf = try dummyTestConfig(),
.uir = pipe.reader(),
.uiw = pipe.writer(),
.wpa = "/dev/null",
});
defer daemon.conf.deinit();
daemon.want_settings = false;
daemon.want_network_report = false;
daemon.want_onchain_report = false;
@ -1262,7 +1329,7 @@ test "start-stop" {
try t.expect(!daemon.wpa_ctrl.opened);
}
test "start-poweroff" {
test "daemon: start-poweroff" {
const t = std.testing;
const tt = @import("../test.zig");
@ -1275,7 +1342,7 @@ test "start-poweroff" {
const gui_reader = gui_stdin.reader();
var daemon = try Daemon.init(.{
.allocator = arena,
.confpath = "/unused.json",
.conf = try dummyTestConfig(),
.uir = gui_stdout.reader(),
.uiw = gui_stdin.writer(),
.wpa = "/dev/null",
@ -1286,6 +1353,7 @@ test "start-poweroff" {
daemon.want_lnd_report = false;
defer {
daemon.deinit();
daemon.conf.deinit();
gui_stdin.close();
}
@ -1326,3 +1394,78 @@ test "start-poweroff" {
// need custom runner to set up a global registry for child processes.
// https://github.com/ziglang/zig/pull/13411
}
test "daemon: screen unlock" {
const t = std.testing;
const tt = @import("../test.zig");
var arena_alloc = std.heap.ArenaAllocator.init(t.allocator);
defer arena_alloc.deinit();
const arena = arena_alloc.allocator();
var tmp = try tt.TempDir.create();
defer tmp.cleanup();
const correct_pin = "12345";
var ndconf = try Config.init(t.allocator, try tmp.join(&.{"ndconf.json"}));
defer ndconf.deinit();
try ndconf.setSlockPin(correct_pin);
const gui_stdin = try types.IoPipe.create();
const gui_stdout = try types.IoPipe.create();
const gui_reader = gui_stdin.reader();
var daemon = try Daemon.init(.{
.allocator = arena,
.conf = ndconf,
.uir = gui_stdout.reader(),
.uiw = gui_stdin.writer(),
.wpa = "/dev/null",
});
defer {
daemon.deinit();
gui_stdin.close();
}
daemon.want_settings = false;
daemon.want_network_report = false;
daemon.want_onchain_report = false;
daemon.want_lnd_report = false;
try daemon.start();
try t.expect(daemon.screenstate == .locked);
try comm.write(arena, gui_stdout.writer(), .{ .unlock_screen = "000" });
try comm.write(arena, gui_stdout.writer(), .{ .unlock_screen = correct_pin });
{
const msg = try comm.read(arena, gui_reader);
try t.expect(!msg.value.screen_unlock_result.ok);
}
{
const msg = try comm.read(arena, gui_reader);
try t.expect(msg.value.screen_unlock_result.ok);
}
daemon.stop();
gui_stdout.close();
daemon.wait();
try t.expect(daemon.screenstate == .unlocked);
}
fn dummyTestConfig() !Config {
const talloc = std.testing.allocator;
const arena = try talloc.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(talloc);
return Config{
.arena = arena,
.confpath = "/dummy.conf",
.data = .{
.slock = null,
.syschannel = .master,
.syscronscript = "",
.sysrunscript = "",
},
.static = .{
.hostname = "testhost",
.lnd_user = null,
.lnd_tor_hostname = null,
.bitcoind_rpc_pass = null,
},
};
}

@ -86,7 +86,7 @@ pub fn sendReport(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, w: anytype) !
};
// fetch available wifi networks from scan results using WPA ctrl
var wifi_networks: ?types.StringList = if (queryWifiScanResults(arena, wpa_ctrl)) |v| v else |err| blk: {
const wifi_networks: ?types.StringList = if (queryWifiScanResults(arena, wpa_ctrl)) |v| v else |err| blk: {
logger.err("queryWifiScanResults: {any}", .{err});
break :blk null;
};

@ -1,6 +1,6 @@
const buildopts = @import("build_options");
const std = @import("std");
const os = std.os;
const posix = std.posix;
const time = std.time;
const comm = @import("comm.zig");
@ -8,6 +8,7 @@ const types = @import("types.zig");
const ui = @import("ui/ui.zig");
const lvgl = @import("ui/lvgl.zig");
const screen = @import("ui/screen.zig");
const screenlock = @import("ui/screenlock.zig");
const symbol = @import("ui/symbol.zig");
const logger = std.log.scoped(.ngui);
@ -26,13 +27,18 @@ var gpa: std.mem.Allocator = undefined;
var ui_mutex: std.Thread.Mutex = .{};
/// current state of the GUI.
/// guarded by ui_mutex since some nm_xxx funcs branch based off of the state.
/// guarded by `ui_mutex` since some `nm_xxx` funcs branch based off of the state.
var state: enum {
active, // normal operational mode
standby, // idling
alert, // draw user attention; never go standby
} = .active;
var slock_status: enum {
enabled,
disabled,
} = undefined; // set in main after parsing cmd line flags
/// last report received from comm.
/// deinit'ed at program exit.
/// while deinit and replace handle concurrency, field access requires holding mu.
@ -224,68 +230,58 @@ fn commThreadLoopCycle() !void {
const msg = try comm.pipeRead(); // blocking
ui_mutex.lock(); // guards the state and all UI calls below
defer ui_mutex.unlock();
switch (state) {
.standby => switch (msg.value) {
.ping => {
defer msg.deinit();
try comm.pipeWrite(comm.Message.pong);
},
.network_report,
.onchain_report,
.lightning_report,
.lightning_error,
=> last_report.replace(msg),
.lightning_genseed_result,
.lightning_ctrlconn,
// TODO: merge standby vs active switch branches
=> {
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
msg.deinit();
},
.settings => |sett| {
ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err});
msg.deinit();
},
else => {
logger.debug("ignoring {s}: in standby", .{@tagName(msg.value)});
msg.deinit();
},
switch (msg.value) {
.ping => {
defer msg.deinit();
try comm.pipeWrite(comm.Message.pong);
},
.active, .alert => switch (msg.value) {
.ping => {
defer msg.deinit();
try comm.pipeWrite(comm.Message.pong);
},
.poweroff_progress => |rep| {
ui.poweroff.updateStatus(rep) catch |err| logger.err("poweroff.updateStatus: {any}", .{err});
msg.deinit();
},
.network_report => |rep| {
.poweroff_progress => |rep| {
ui.poweroff.updateStatus(rep) catch |err| logger.err("poweroff.updateStatus: {any}", .{err});
msg.deinit();
},
.network_report => |rep| {
if (state != .standby) {
updateNetworkStatus(rep) catch |err| logger.err("updateNetworkStatus: {any}", .{err});
last_report.replace(msg);
},
.onchain_report => |rep| {
}
last_report.replace(msg);
},
.onchain_report => |rep| {
if (state != .standby) {
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err});
last_report.replace(msg);
},
.lightning_report, .lightning_error => {
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
last_report.replace(msg);
},
.lightning_genseed_result,
.lightning_ctrlconn,
=> {
}
last_report.replace(msg);
},
.lightning_report, .lightning_error => {
if (state != .standby) {
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
msg.deinit();
},
.settings => |sett| {
ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err});
msg.deinit();
},
else => {
logger.warn("unhandled msg tag {s}", .{@tagName(msg.value)});
msg.deinit();
},
}
last_report.replace(msg);
},
.lightning_genseed_result,
.lightning_ctrlconn,
=> {
ui.lightning.updateTabPanel(msg.value) catch |err| logger.err("lightning.updateTabPanel: {any}", .{err});
msg.deinit();
},
.settings => |sett| {
ui.settings.update(sett) catch |err| logger.err("settings.update: {any}", .{err});
slock_status = if (sett.slock_enabled) .enabled else .disabled;
msg.deinit();
},
.screen_unlock_result => |unlock| {
if (unlock.ok) {
ui.screenlock.unlockSuccess();
} else {
var errmsg: [:0]const u8 = "invalid pin code";
if (unlock.err) |s| {
errmsg = gpa.dupeZ(u8, s) catch errmsg;
}
ui.screenlock.unlockFailure(errmsg);
}
},
else => {
logger.warn("unhandled msg tag {s}", .{@tagName(msg.value)});
msg.deinit();
},
}
}
@ -295,7 +291,7 @@ fn commThreadLoopCycle() !void {
fn uiThreadLoop() void {
while (true) {
ui_mutex.lock();
var till_next_ms = lvgl.loopCycle(); // UI loop
const till_next_ms = lvgl.loopCycle(); // UI loop
const do_state = state;
ui_mutex.unlock();
@ -306,6 +302,9 @@ fn uiThreadLoop() void {
// go into a screen sleep mode due to no user activity
wakeup.reset();
comm.pipeWrite(comm.Message.standby) catch |err| logger.err("standby: {any}", .{err});
if (slock_status == .enabled) {
screenlock.activate();
}
screen.sleep(&ui_mutex, &wakeup); // blocking
// wake up due to touch screen activity or wakeup event is set
@ -346,7 +345,25 @@ fn uiThreadLoop() void {
logger.info("exiting UI thread loop", .{});
}
fn parseArgs(alloc: std.mem.Allocator) !void {
/// prints usage help text to stderr.
fn usage(prog: []const u8) !void {
try stderr.print(
\\usage: {s} [-v] [-slock]
\\
\\ngui is nakamochi GUI interface. it communicates with nd, nakamochi daemon,
\\via stdio and is typically launched by the daemon as a child process.
\\
\\-slock makes the interface start up in a screenlocked mode.
, .{prog});
}
const CmdFlags = struct {
slock: bool, // whether to start the UI in screen locked mode
};
fn parseArgs(alloc: std.mem.Allocator) !CmdFlags {
var flags = CmdFlags{ .slock = false };
var args = try std.process.ArgIterator.initWithAllocator(alloc);
defer args.deinit();
const prog = args.next() orelse return error.NoProgName;
@ -358,22 +375,14 @@ fn parseArgs(alloc: std.mem.Allocator) !void {
} else if (std.mem.eql(u8, a, "-v")) {
try stderr.print("{any}\n", .{buildopts.semver});
std.process.exit(0);
} else if (std.mem.eql(u8, a, "-slock")) {
flags.slock = true;
} else {
logger.err("unknown arg name {s}", .{a});
return error.UnknownArgName;
}
}
}
/// prints usage help text to stderr.
fn usage(prog: []const u8) !void {
try stderr.print(
\\usage: {s} [-v]
\\
\\ngui is nakamochi GUI interface. it communicates with nd, nakamochi daemon,
\\via stdio and is typically launched by the daemon as a child process.
\\
, .{prog});
return flags;
}
/// handles sig TERM and INT: makes the program exit.
@ -382,7 +391,7 @@ fn sighandler(sig: c_int) callconv(.C) void {
return;
}
switch (sig) {
os.SIG.INT, os.SIG.TERM => sigquit.set(),
posix.SIG.INT, posix.SIG.TERM => sigquit.set(),
else => {},
}
}
@ -395,8 +404,9 @@ pub fn main() anyerror!void {
logger.err("memory leaks detected", .{});
};
gpa = gpa_state.allocator();
try parseArgs(gpa);
const flags = try parseArgs(gpa);
logger.info("ndg version {any}", .{buildopts.semver});
slock_status = if (flags.slock) .enabled else .disabled;
// ensure timer is available on this platform before doing anything else;
// the UI is unusable otherwise.
@ -406,7 +416,7 @@ pub fn main() anyerror!void {
comm.initPipe(gpa, .{ .r = std.io.getStdIn(), .w = std.io.getStdOut() });
// initalizes display, input driver and finally creates the user interface.
ui.init(gpa) catch |err| {
ui.init(.{ .allocator = gpa, .slock = flags.slock }) catch |err| {
logger.err("ui.init: {any}", .{err});
return err;
};
@ -422,6 +432,7 @@ pub fn main() anyerror!void {
const th = try std.Thread.spawn(.{}, uiThreadLoop, .{});
th.detach();
}
{
// start comms with daemon in a seaparate thread.
const th = try std.Thread.spawn(.{}, commThreadLoop, .{});
@ -429,13 +440,13 @@ pub fn main() anyerror!void {
}
// set up a sigterm handler for clean exit.
const sa = os.Sigaction{
const sa = posix.Sigaction{
.handler = .{ .handler = sighandler },
.mask = os.empty_sigset,
.mask = posix.empty_sigset,
.flags = 0,
};
try os.sigaction(os.SIG.INT, &sa, null);
try os.sigaction(os.SIG.TERM, &sa, null);
try posix.sigaction(posix.SIG.INT, &sa, null);
try posix.sigaction(posix.SIG.TERM, &sa, null);
sigquit.wait();
logger.info("sigquit: terminating ...", .{});

@ -5,8 +5,8 @@ const types = @import("../types.zig");
/// caller owns memory; must dealloc using `allocator`.
pub fn hostname(allocator: std.mem.Allocator) ![]const u8 {
var buf: [std.os.HOST_NAME_MAX]u8 = undefined;
const name = try std.os.gethostname(&buf);
var buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
const name = try std.posix.gethostname(&buf);
return allocator.dupe(u8, name);
}
@ -40,8 +40,8 @@ pub fn setHostname(allocator: std.mem.Allocator, name: []const u8) !void {
const newname = sanitized.items;
// need not continue if current name matches the new one.
var buf: [std.os.HOST_NAME_MAX]u8 = undefined;
const currname = try std.os.gethostname(&buf);
var buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
const currname = try std.posix.gethostname(&buf);
if (std.mem.eql(u8, currname, newname)) {
return;
}

@ -37,7 +37,7 @@ pub fn initGlobal() void {
fn initGlobalFn() void {
global_gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
global_gpa = global_gpa_state.allocator();
var pipe = types.IoPipe.create() catch |err| {
const pipe = types.IoPipe.create() catch |err| {
std.debug.panic("IoPipe.create: {any}", .{err});
};
comm.initPipe(global_gpa, pipe);
@ -118,7 +118,7 @@ pub const TestChildProcess = struct {
argv: []const []const u8,
pub fn init(argv: []const []const u8, allocator: std.mem.Allocator) TestChildProcess {
var adup = allocator.alloc([]u8, argv.len) catch unreachable;
const adup = allocator.alloc([]u8, argv.len) catch unreachable;
for (argv, adup) |v, *dup| {
dup.* = allocator.dupe(u8, v) catch unreachable;
}
@ -242,7 +242,7 @@ pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
.Slice => {
switch (@typeInfo(p.child)) {
.Pointer, .Struct, .Optional, .Union => {
var err: ?anyerror = blk: {
const err: ?anyerror = blk: {
if (expected.len != actual.len) {
std.debug.print("expected.len = {d}, actual.len = {d}\n", .{ expected.len, actual.len });
break :blk error.ExpectDeepEqual;
@ -331,6 +331,7 @@ test {
_ = @import("ngui.zig");
_ = @import("lightning.zig");
_ = @import("sys.zig");
_ = @import("xfmt.zig");
std.testing.refAllDecls(@This());
}

@ -1,6 +1,6 @@
const std = @import("std");
const time = std.time;
const os = std.os;
const posix = std.posix;
const comm = @import("comm");
const types = @import("../types.zig");
@ -12,9 +12,9 @@ var ngui_proc: std.ChildProcess = undefined;
var sigquit: std.Thread.ResetEvent = .{};
fn sighandler(sig: c_int) callconv(.C) void {
logger.info("received signal {} (TERM={} INT={})", .{ sig, os.SIG.TERM, os.SIG.INT });
logger.info("received signal {} (TERM={} INT={})", .{ sig, posix.SIG.TERM, posix.SIG.INT });
switch (sig) {
os.SIG.INT, os.SIG.TERM => sigquit.set(),
posix.SIG.INT, posix.SIG.TERM => sigquit.set(),
else => {},
}
}
@ -29,6 +29,7 @@ fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
const Flags = struct {
ngui_path: ?[:0]const u8 = null,
slock: bool = false, // gui screen lock
fn deinit(self: @This(), allocator: std.mem.Allocator) void {
if (self.ngui_path) |p| allocator.free(p);
@ -57,6 +58,8 @@ fn parseArgs(gpa: std.mem.Allocator) !Flags {
}
if (std.mem.eql(u8, a, "-ngui")) {
lastarg = .ngui_path;
} else if (std.mem.eql(u8, a, "-slock")) {
flags.slock = true;
} else {
fatal("unknown arg name {s}", .{a});
}
@ -74,9 +77,18 @@ fn parseArgs(gpa: std.mem.Allocator) !Flags {
}
/// global vars for comm read/write threads
var mu: std.Thread.Mutex = .{};
var nodename: types.BufTrimString(std.os.HOST_NAME_MAX) = .{};
var settings_sent = false;
var state: struct {
mu: std.Thread.Mutex = .{},
nodename: types.BufTrimString(std.posix.HOST_NAME_MAX) = .{},
slock_pincode: ?[]const u8 = null, // disabled when null
settings_sent: bool = false,
fn deinit(self: @This(), gpa: std.mem.Allocator) void {
if (self.slock_pincode) |s| {
gpa.free(s);
}
}
} = .{};
fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
comm.write(gpa, w, .ping) catch |err| logger.err("comm.write ping: {any}", .{err});
@ -102,7 +114,7 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
},
.poweroff => {
logger.info("sending poweroff status1", .{});
var s1: comm.Message.PoweroffProgress = .{ .services = &.{
const s1: comm.Message.PoweroffProgress = .{ .services = &.{
.{ .name = "lnd", .stopped = false, .err = null },
.{ .name = "bitcoind", .stopped = false, .err = null },
} };
@ -110,7 +122,7 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
time.sleep(2 * time.ns_per_s);
logger.info("sending poweroff status2", .{});
var s2: comm.Message.PoweroffProgress = .{ .services = &.{
const s2: comm.Message.PoweroffProgress = .{ .services = &.{
.{ .name = "lnd", .stopped = true, .err = null },
.{ .name = "bitcoind", .stopped = false, .err = null },
} };
@ -118,7 +130,7 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
time.sleep(3 * time.ns_per_s);
logger.info("sending poweroff status3", .{});
var s3: comm.Message.PoweroffProgress = .{ .services = &.{
const s3: comm.Message.PoweroffProgress = .{ .services = &.{
.{ .name = "lnd", .stopped = true, .err = null },
.{ .name = "bitcoind", .stopped = true, .err = null },
} };
@ -137,17 +149,46 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
time.sleep(3 * time.ns_per_s);
},
.lightning_get_ctrlconn => {
var conn: comm.Message.LightningCtrlConn = &.{
const conn: comm.Message.LightningCtrlConn = &.{
.{ .url = "lndconnect://adfkjhadwaepoijsadflkjtrpoijawokjafulkjsadfkjhgjfdskjszd.onion:10009?macaroon=Adasjsadkfljhfjhasdpiuhfiuhawfffoihgpoiadsfjharpoiuhfdsgpoihafdsgpoiheafoiuhasdfhisdufhiuhfewiuhfiuhrfl6prrx", .typ = .lnd_rpc, .perm = .admin },
.{ .url = "lndconnect://adfkjhadwaepoijsadflkjtrpoijawokjafulkjsadfkjhgjfdskjszd.onion:10010?macaroon=Adasjsadkfljhfjhasdpiuhfiuhawfffoihgpoiadsfjharpoiuhfdsgpoihafdsgpoiheafoiuhasdfhisdufhiuhfewiuhfiuhrfl6prrx", .typ = .lnd_http, .perm = .admin },
};
comm.write(gpa, w, .{ .lightning_ctrlconn = conn }) catch |err| logger.err("{!}", .{err});
},
.set_nodename => |s| {
mu.lock();
defer mu.unlock();
nodename.set(s);
settings_sent = false;
state.mu.lock();
defer state.mu.unlock();
state.nodename.set(s);
state.settings_sent = false;
},
.unlock_screen => |pin| {
logger.info("unlock pincode: {s}", .{pin});
time.sleep(1 * time.ns_per_s);
state.mu.lock();
defer state.mu.unlock();
if (state.slock_pincode == null or std.mem.eql(u8, pin, state.slock_pincode.?)) {
const res: comm.Message.ScreenUnlockResult = .{
.ok = true,
.err = null,
};
comm.write(gpa, w, .{ .screen_unlock_result = res }) catch |err| logger.err("{!}", .{err});
} else {
comm.write(gpa, w, .{ .screen_unlock_result = .{
.ok = false,
.err = "incorrect pin code",
} }) catch |err| logger.err("{!}", .{err});
}
},
.slock_set_pincode => |newpin| {
logger.info("slock_set_pincode: {?s}", .{newpin});
time.sleep(1 * time.ns_per_s);
state.mu.lock();
defer state.mu.unlock();
if (state.slock_pincode) |s| {
gpa.free(s);
}
state.slock_pincode = if (newpin) |pin| gpa.dupe(u8, pin) catch unreachable else null;
state.settings_sent = false;
},
else => {},
}
@ -169,18 +210,19 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
}
sectimer.reset();
mu.lock();
defer mu.unlock();
state.mu.lock();
defer state.mu.unlock();
if (!settings_sent) {
settings_sent = true;
if (!state.settings_sent) {
state.settings_sent = true;
const sett: comm.Message.Settings = .{
.hostname = nodename.val(),
.slock_enabled = state.slock_pincode != null,
.hostname = state.nodename.val(),
.sysupdates = .{ .channel = .edge },
};
comm.write(gpa, w, .{ .settings = sett }) catch |err| {
logger.err("{}", .{err});
settings_sent = false;
state.settings_sent = false;
};
}
@ -306,9 +348,17 @@ pub fn main() !void {
const flags = try parseArgs(gpa);
defer flags.deinit(gpa);
nodename.set("guiplayhost");
state.slock_pincode = if (flags.slock) try gpa.dupe(u8, "0000") else null;
state.nodename.set("guiplayhost");
defer state.deinit(gpa);
ngui_proc = std.ChildProcess.init(&.{flags.ngui_path.?}, gpa);
var a = std.ArrayList([]const u8).init(gpa);
defer a.deinit();
try a.append(flags.ngui_path.?);
if (flags.slock) {
try a.append("-slock");
}
ngui_proc = std.ChildProcess.init(a.items, gpa);
ngui_proc.stdin_behavior = .Pipe;
ngui_proc.stdout_behavior = .Pipe;
ngui_proc.stderr_behavior = .Inherit;
@ -324,13 +374,13 @@ pub fn main() !void {
const th2 = try std.Thread.spawn(.{}, commWriteThread, .{ gpa, uiwriter });
th2.detach();
const sa = os.Sigaction{
const sa = posix.Sigaction{
.handler = .{ .handler = sighandler },
.mask = os.empty_sigset,
.mask = posix.empty_sigset,
.flags = 0,
};
try os.sigaction(os.SIG.INT, &sa, null);
try os.sigaction(os.SIG.TERM, &sa, null);
try posix.sigaction(posix.SIG.INT, &sa, null);
try posix.sigaction(posix.SIG.TERM, &sa, null);
sigquit.wait();
logger.info("killing ngui", .{});

@ -40,8 +40,8 @@ pub const IoPipe = struct {
w: std.fs.File,
/// a pipe must be close'ed when done.
pub fn create() std.os.PipeError!IoPipe {
const fds = try std.os.pipe();
pub fn create() std.posix.PipeError!IoPipe {
const fds = try std.posix.pipe();
return .{
.r = std.fs.File{ .handle = fds[0] },
.w = std.fs.File{ .handle = fds[1] },
@ -138,7 +138,7 @@ pub fn Deinitable(comptime T: type) type {
const Self = @This();
pub fn init(allocator: std.mem.Allocator) !Self {
var res = Self{
const res = Self{
.arena = try allocator.create(std.heap.ArenaAllocator),
.value = undefined,
};

@ -10,6 +10,13 @@
#include <stdlib.h>
#include <unistd.h>
static lv_style_t style_title;
static lv_style_t style_text_muted;
static lv_style_t style_btn_red;
static const lv_font_t *font_large;
static lv_obj_t *virt_keyboard;
static lv_obj_t *tabview; /* main tabs content parent; lv_tabview_create */
/**
* initiates system shutdown leading to poweroff.
*/
@ -35,6 +42,11 @@ int nm_create_lightning_panel(lv_obj_t *parent);
*/
lv_obj_t *nm_create_settings_nodename(lv_obj_t *parent);
/**
* creates screenlock card of the settings panel.
*/
lv_obj_t *nm_create_settings_screenlock(lv_obj_t *parent);
/**
* creates the sysupdates section of the settings panel.
*/
@ -56,13 +68,6 @@ int nm_wifi_start_connect(const char *ssid, const char *password);
*/
void nm_poweroff_btn_callback(lv_event_t *e);
static lv_style_t style_title;
static lv_style_t style_text_muted;
static lv_style_t style_btn_red;
static const lv_font_t *font_large;
static lv_obj_t *virt_keyboard;
static lv_obj_t *tabview; /* main tabs content parent; lv_tabview_create */
/**
* returns user-managed data previously set on an object with nm_obj_set_userdata.
* the returned value may be NULL.
@ -97,6 +102,11 @@ extern lv_style_t *nm_style_title()
return &style_title;
}
extern const lv_font_t *nm_font_large()
{
return font_large;
}
/**
* a hack to prevent tabview from switching to the next tab
* on a scroll event, for example coming from a top layer window.
@ -254,6 +264,7 @@ static int create_settings_panel(lv_obj_t *parent)
********************/
// ported to zig;
lv_obj_t *nodename_panel = nm_create_settings_nodename(parent);
lv_obj_t *screenlock_panel = nm_create_settings_screenlock(parent);
lv_obj_t *sysupdates_panel = nm_create_settings_sysupdates(parent);
/********************
@ -263,14 +274,16 @@ static int create_settings_panel(lv_obj_t *parent)
static lv_coord_t parent_grid_rows[] = {/**/
LV_GRID_CONTENT, /* wifi panel */
LV_GRID_CONTENT, /* nodename panel */
LV_GRID_CONTENT, /* screenlock 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(nodename_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, 2, 1);
lv_obj_set_grid_cell(sysupdates_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 3, 1);
lv_obj_set_grid_cell(screenlock_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 2, 1);
lv_obj_set_grid_cell(power_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 3, 1);
lv_obj_set_grid_cell(sysupdates_panel, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_CENTER, 4, 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[] = {/**/
@ -325,7 +338,7 @@ static void tab_changed_event_cb(lv_event_t *e)
}
}
extern int nm_ui_init(lv_disp_t *disp)
extern void nm_ui_init_theme(lv_disp_t *disp)
{
/* default theme is static */
lv_theme_t *theme = lv_theme_default_init(disp, /**/
@ -344,18 +357,22 @@ extern int nm_ui_init(lv_disp_t *disp)
lv_style_init(&style_btn_red);
lv_style_set_bg_color(&style_btn_red, lv_palette_main(LV_PALETTE_RED));
}
extern int nm_ui_init_main_tabview(lv_obj_t *scr)
{
/* global virtual keyboard */
virt_keyboard = lv_keyboard_create(lv_scr_act());
virt_keyboard = lv_keyboard_create(scr);
if (virt_keyboard == NULL) {
/* TODO: or continue without keyboard? */
return -1;
}
lv_obj_set_style_text_font(virt_keyboard, font_large, LV_PART_ITEMS);
lv_obj_set_style_max_height(virt_keyboard, NM_DISP_HOR * 2 / 3, 0);
lv_obj_add_flag(virt_keyboard, LV_OBJ_FLAG_HIDDEN);
/* the paren of all main tabs */
const lv_coord_t tabh = 60;
tabview = lv_tabview_create(lv_scr_act(), LV_DIR_TOP, tabh);
tabview = lv_tabview_create(scr, LV_DIR_TOP, tabh);
if (tabview == NULL) {
return -1;
}

@ -46,9 +46,9 @@ pub usingnamespace switch (buildopts.driver) {
}
},
.fbev => struct {
extern "c" fn nm_open_evdev_nonblock() std.os.fd_t;
extern "c" fn nm_close_evdev(fd: std.os.fd_t) void;
extern "c" fn nm_consume_input_events(fd: std.os.fd_t) bool;
extern "c" fn nm_open_evdev_nonblock() std.posix.fd_t;
extern "c" fn nm_close_evdev(fd: std.posix.fd_t) void;
extern "c" fn nm_consume_input_events(fd: std.posix.fd_t) bool;
pub fn InputWatcher() !EvdevWatcher {
const fd = nm_open_evdev_nonblock();
@ -59,7 +59,7 @@ pub usingnamespace switch (buildopts.driver) {
}
pub const EvdevWatcher = struct {
evdev_fd: std.os.fd_t,
evdev_fd: std.posix.fd_t,
pub fn consume(self: @This()) bool {
return nm_consume_input_events(self.evdev_fd);

@ -99,7 +99,7 @@ var tab: struct {
} = null,
fn initSetup(self: *@This(), topwin: lvgl.Window) !void {
var arena = try self.allocator.create(std.heap.ArenaAllocator);
const arena = try self.allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(tab.allocator);
self.seed_setup = .{ .arena = arena, .topwin = topwin };
}

@ -213,6 +213,9 @@ pub const LvStyle = opaque {
};
};
/// represents lv_font_t in C.
pub const LvFont = opaque {};
/// a simplified color type compatible with LVGL which defines lv_color_t
/// as a union containing an bit-fields struct unsupported in zig cImport.
pub const Color = u16; // originally c.lv_color_t; TODO: comptime switch for u32
@ -427,6 +430,11 @@ pub const WidgetMethods = struct {
lv_obj_align(self.lvobj, @intFromEnum(a), xoffset, yoffset);
}
/// similar to `posAlign` but the alignment is in relation to another object `rel`.
pub fn posAlignTo(self: anytype, rel: anytype, a: PosAlign, xoffset: Coord, yoffset: Coord) void {
lv_obj_align_to(self.lvobj, rel.lvobj, @intFromEnum(a), xoffset, yoffset);
}
/// sets flex layout growth property; same meaning as in CSS flex.
pub fn flexGrow(self: anytype, val: u8) void {
lv_obj_set_flex_grow(self.lvobj, val);
@ -495,7 +503,7 @@ pub const Screen = struct {
/// makes a screen active.
pub fn load(scr: Screen) void {
lv_disp_load_scr(scr.obj);
lv_disp_load_scr(scr.lvobj);
}
};
@ -710,10 +718,11 @@ pub const Label = struct {
};
/// the text value is copied into a heap-allocated alloc.
pub fn new(parent: anytype, text: [*:0]const u8, opt: Opt) !Label {
var lv_label = lv_label_create(parent.lvobj) orelse return error.OutOfMemory;
//lv_label_set_text_static(lb, text); // static doesn't work with .dot
lv_label_set_text(lv_label, text);
pub fn new(parent: anytype, text: ?[*:0]const u8, opt: Opt) !Label {
const lv_label = lv_label_create(parent.lvobj) orelse return error.OutOfMemory;
if (text) |s| {
lv_label_set_text(lv_label, s);
}
//lv_obj_set_height(lb, sizeContent); // default
if (opt.long_mode) |m| {
lv_label_set_long_mode(lv_label, @intFromEnum(m));
@ -736,8 +745,8 @@ pub const Label = struct {
/// sets label text to a new value.
/// previous value is dealloc'ed.
pub fn setText(self: Label, text: [*:0]const u8) void {
lv_label_set_text(self.lvobj, text);
pub fn setText(self: Label, text: [:0]const u8) void {
lv_label_set_text(self.lvobj, text.ptr);
}
/// sets label text without heap alloc but assumes text outlives the label obj.
@ -748,7 +757,7 @@ pub const Label = struct {
/// formats a new label text and passes it on to `setText`.
/// the buffer can be dropped once the function returns.
pub fn setTextFmt(self: Label, buf: []u8, comptime format: []const u8, args: anytype) !void {
var s = try std.fmt.bufPrintZ(buf, format, args);
const s = try std.fmt.bufPrintZ(buf, format, args);
self.setText(s);
}
@ -955,6 +964,40 @@ pub const QrCode = struct {
}
};
pub const Keyboard = struct {
lvobj: *LvObj,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
const Mode = enum(c.lv_keyboard_mode_t) {
lower = c.LV_KEYBOARD_MODE_TEXT_LOWER,
upper = c.LV_KEYBOARD_MODE_TEXT_UPPER,
special = c.LV_KEYBOARD_MODE_SPECIAL,
number = c.LV_KEYBOARD_MODE_NUMBER,
user1 = c.LV_KEYBOARD_MODE_USER_1,
user2 = c.LV_KEYBOARD_MODE_USER_2,
user3 = c.LV_KEYBOARD_MODE_USER_3,
user4 = c.LV_KEYBOARD_MODE_USER_4,
};
pub fn new(parent: anytype, mode: Mode) !Keyboard {
const kb = lv_keyboard_create(parent.lvobj) orelse return error.OutOfMemory;
lv_keyboard_set_mode(kb, @intFromEnum(mode));
const sel = LvStyle.Selector{ .part = .item };
lv_obj_set_style_text_font(kb, nm_font_large(), sel.value());
return .{ .lvobj = kb };
}
pub fn attach(self: Keyboard, ta: TextArea) void {
lv_keyboard_set_textarea(self.lvobj, ta.lvobj);
}
pub fn setMode(self: Keyboard, m: Mode) void {
lv_keyboard_set_mode(self.lvobj, m);
}
};
/// represents lv_obj_t type in C.
pub const LvObj = opaque {
/// feature-flags controlling object's behavior.
@ -1088,6 +1131,8 @@ pub const PosAlign = enum(c.lv_align_t) {
pub extern fn nm_style_btn_red() *LvStyle; // TODO: make it private
/// returns a title style with a larger font.
pub extern fn nm_style_title() *LvStyle; // TODO: make it private
/// returns default font of large size.
pub extern fn nm_font_large() *const LvFont; // TODO: make it private
// the "native" lv_obj_set/get user_data are static inline, so make our own funcs.
extern "c" fn nm_obj_userdata(obj: *LvObj) ?*anyopaque;
@ -1160,6 +1205,7 @@ extern fn lv_obj_remove_style(obj: *LvObj, style: ?*LvStyle, sel: c.lv_style_sel
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_text_font(obj: *LvObj, font: *const LvFont, 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;
@ -1191,6 +1237,7 @@ extern fn lv_obj_clear_flag(obj: *LvObj, v: c.lv_obj_flag_t) void;
extern fn lv_obj_has_flag(obj: *LvObj, v: c.lv_obj_flag_t) bool;
extern fn lv_obj_align(obj: *LvObj, a: c.lv_align_t, x: c.lv_coord_t, y: c.lv_coord_t) void;
extern fn lv_obj_align_to(obj: *LvObj, rel: *LvObj, a: c.lv_align_t, x: c.lv_coord_t, y: c.lv_coord_t) void;
extern fn lv_obj_set_height(obj: *LvObj, h: c.lv_coord_t) void;
extern fn lv_obj_set_width(obj: *LvObj, w: c.lv_coord_t) void;
extern fn lv_obj_set_size(obj: *LvObj, w: c.lv_coord_t, h: c.lv_coord_t) void;
@ -1243,3 +1290,7 @@ extern fn lv_win_get_content(win: *LvObj) *LvObj;
extern fn lv_qrcode_create(parent: *LvObj, size: c.lv_coord_t, dark: Color, light: Color) ?*LvObj;
extern fn lv_qrcode_update(qrcode: *LvObj, data: *const anyopaque, data_len: u32) c.lv_res_t;
extern fn lv_keyboard_create(parent: *LvObj) ?*LvObj;
extern fn lv_keyboard_set_textarea(kb: *LvObj, ta: *LvObj) void;
extern fn lv_keyboard_set_mode(kb: *LvObj, mode: c.lv_keyboard_mode_t) void;

@ -0,0 +1,90 @@
const std = @import("std");
const comm = @import("../comm.zig");
const lvgl = @import("lvgl.zig");
const logger = std.log.scoped(.ui_screenlock);
const infoTextInit = "please enter pin code to unlock the screen";
var main_screen: lvgl.Screen = undefined;
var locked_screen: lvgl.Screen = undefined;
var pincode: lvgl.TextArea = undefined;
var info: lvgl.Label = undefined;
var spinner: lvgl.Spinner = undefined;
var keyboard: lvgl.Keyboard = undefined;
var is_active: bool = false;
pub fn init(main_scr: lvgl.Screen) !void {
main_screen = main_scr;
locked_screen = try lvgl.Screen.new();
pincode = try lvgl.TextArea.new(locked_screen, .{ .password_mode = true });
pincode.posAlign(.top_mid, 0, 20);
_ = pincode.on(.ready, nm_pincode_input, null);
info = try lvgl.Label.new(locked_screen, null, .{});
info.setTextStatic(infoTextInit);
info.posAlignTo(pincode, .out_bottom_mid, 0, 10);
spinner = try lvgl.Spinner.new(locked_screen);
spinner.center();
spinner.hide();
keyboard = try lvgl.Keyboard.new(locked_screen, .number);
keyboard.attach(pincode);
}
pub fn activate() void {
if (is_active) {
logger.info("screenlock already active", .{});
return;
}
is_active = true;
locked_screen.load();
spinner.hide();
pincode.enable();
pincode.setText("");
info.setTextStatic(infoTextInit);
info.posAlignTo(pincode, .out_bottom_mid, 0, 10);
keyboard.show();
logger.info("screenlock active", .{});
}
/// msg lifetime is only until function return.
pub fn unlockFailure(msg: [:0]const u8) void {
info.setText(msg);
info.posAlignTo(pincode, .out_bottom_mid, 0, 10);
spinner.hide();
pincode.setText("");
pincode.enable();
keyboard.show();
}
pub fn unlockSuccess() void {
logger.info("deactivating screenlock", .{});
pincode.setText("");
main_screen.load();
is_active = false;
}
export fn nm_pincode_input(e: *lvgl.LvEvent) void {
switch (e.code()) {
.ready => {
keyboard.hide();
pincode.disable();
spinner.show();
comm.pipeWrite(.{ .unlock_screen = pincode.text() }) catch |err| {
logger.err("unlock_screen pipe write: {!}", .{err});
var buf: [256]u8 = undefined;
const msg = std.fmt.bufPrintZ(&buf, "internal error: {!}", .{err}) catch {
unlockFailure("internal error");
return;
};
unlockFailure(msg);
};
},
else => {},
}
}

@ -16,9 +16,13 @@ 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 ";
/// buttons text
/// button labels and other text
const textSwitch = "SWITCH";
const textChange = "CHANGE";
const textDisable = "DISABLE";
const textSlockBtnEnable = "SET PIN CODE";
const textSlockDisabled = "screenlock is disabled\nset a pin code to activate";
const textSlockEnabled = "screenlock is enabled\nit activates once in standby mode";
// global allocator set in init.
// must be set before any call into pub funcs in this module.
@ -32,6 +36,33 @@ var tab: struct {
textarea: lvgl.TextArea,
changebtn: lvgl.TextButton,
},
screenlock: struct {
card: lvgl.Card,
textlabel: lvgl.Label,
enbtn: lvgl.TextButton,
disbtn: lvgl.TextButton,
setpin_win: lvgl.Window,
setpin_label: lvgl.Label,
setpin_input: lvgl.TextArea,
fn beginSetPin(self: *@This()) !void {
self.setpin_win = try lvgl.Window.newTop(60, "SET SCREENLOCK PIN CODE");
const wincont = self.setpin_win.content().flex(.column, .{ .cross = .center, .track = .center });
self.setpin_input = try lvgl.TextArea.new(wincont, .{ .password_mode = true });
self.setpin_input.posAlign(.top_mid, 0, 20);
_ = self.setpin_input.on(.ready, nm_screenlock_pincode_input, null);
self.setpin_label = try lvgl.Label.new(wincont, null, .{});
self.setpin_label.posAlignTo(self.setpin_input, .out_bottom_mid, 0, 10);
self.setpin_label.setTextStatic("please enter a new pin code");
const kb = try lvgl.Keyboard.new(self.setpin_win, .number);
kb.attach(self.setpin_input);
}
fn endSetPin(self: @This()) void {
self.setpin_win.destroy();
}
},
sysupdates: struct {
card: lvgl.Card,
chansel: lvgl.Dropdown,
@ -42,8 +73,12 @@ var tab: struct {
/// holds last values received from the daemon.
var state: struct {
// node name
nodename_change_inprogress: bool = false,
curr_nodename: types.BufTrimString(std.os.HOST_NAME_MAX) = .{},
curr_nodename: types.BufTrimString(std.posix.HOST_NAME_MAX) = .{},
// screenlock
slock_pin_input1: ?[]const u8 = null, // verified against a second time input
// sysupdates channel
curr_sysupdates_chan: ?comm.Message.SysupdatesChan = null,
} = .{};
@ -76,7 +111,7 @@ pub fn initNodenamePanel(cont: lvgl.Container) !lvgl.Card {
right.setPad(0, .column, .{});
tab.nodename.textarea = try lvgl.TextArea.new(right, .{
.maxlen = std.os.HOST_NAME_MAX,
.maxlen = std.posix.HOST_NAME_MAX,
.oneline = true,
});
tab.nodename.textarea.setWidth(lvgl.sizePercent(100));
@ -94,6 +129,38 @@ pub fn initNodenamePanel(cont: lvgl.Container) !lvgl.Card {
return tab.nodename.card;
}
/// creates a settings panel for setting screenlock pin code.
pub fn initScreenlockPanel(cont: lvgl.Container) !lvgl.Card {
tab.screenlock.card = try lvgl.Card.new(cont, symbol.EyeClose ++ " SCREENLOCK", .{ .spinner = true });
const row = try lvgl.FlexLayout.new(tab.screenlock.card, .row, .{});
row.setWidth(lvgl.sizePercent(100));
row.setHeightToContent();
// left column
const left = try lvgl.FlexLayout.new(row, .column, .{ .height = .content });
left.flexGrow(1);
left.setPad(10, .row, .{});
tab.screenlock.textlabel = try lvgl.Label.new(left, null, .{});
tab.screenlock.textlabel.setTextStatic("no info available yet");
// right column
const right = try lvgl.FlexLayout.new(row, .column, .{ .height = .content });
right.flexGrow(1);
right.setPad(10, .row, .{});
tab.screenlock.enbtn = try lvgl.TextButton.new(right, textSlockBtnEnable);
tab.screenlock.enbtn.setWidth(lvgl.sizePercent(100));
tab.screenlock.enbtn.hide();
_ = tab.screenlock.enbtn.on(.click, nm_screenlock_enbtn_click, null);
tab.screenlock.disbtn = try lvgl.TextButton.new(right, textDisable);
tab.screenlock.disbtn.setWidth(lvgl.sizePercent(100));
tab.screenlock.disbtn.addStyle(lvgl.nm_style_btn_red(), .{});
tab.screenlock.disbtn.hide();
_ = tab.screenlock.disbtn.on(.click, nm_screenlock_disbtn_click, null);
return tab.screenlock.card;
}
/// 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 {
@ -171,6 +238,20 @@ pub fn update(sett: comm.Message.Settings) !void {
tab.nodename.changebtn.disable();
}
}
// screenlock
tab.screenlock.card.spin(.off);
if (sett.slock_enabled) {
tab.screenlock.textlabel.setTextStatic(textSlockEnabled);
tab.screenlock.enbtn.hide();
tab.screenlock.disbtn.enable();
tab.screenlock.disbtn.show();
} else {
tab.screenlock.textlabel.setTextStatic(textSlockDisabled);
tab.screenlock.enbtn.enable();
tab.screenlock.enbtn.show();
tab.screenlock.disbtn.hide();
}
}
export fn nm_nodename_textarea_input(e: *lvgl.LvEvent) void {
@ -202,6 +283,60 @@ export fn nm_nodename_change_btn_click(_: *lvgl.LvEvent) void {
tab.nodename.card.spin(.on);
}
export fn nm_screenlock_enbtn_click(_: *lvgl.LvEvent) void {
tab.screenlock.beginSetPin() catch |err| {
logger.err("screenlock.beginSetPin: {!}", .{err});
};
}
export fn nm_screenlock_pincode_input(_: *lvgl.LvEvent) void {
// first time input; prompt user for second input to verify
if (state.slock_pin_input1 == null) {
state.slock_pin_input1 = allocator.dupe(u8, tab.screenlock.setpin_input.text()) catch |err| {
logger.err("unable to continue setting screenlock pin code: {!}", .{err});
tab.screenlock.endSetPin();
return;
};
tab.screenlock.setpin_label.setTextStatic("please enter the pin once more to verify");
tab.screenlock.setpin_input.setText("");
return;
}
// ensure first and second time inputs match
const pininput2 = tab.screenlock.setpin_input.text();
if (!std.mem.eql(u8, pininput2, state.slock_pin_input1.?)) {
allocator.free(state.slock_pin_input1.?);
state.slock_pin_input1 = null;
tab.screenlock.setpin_label.setTextStatic("pin codes mismatch, please try again");
tab.screenlock.setpin_input.setText("");
return;
}
// send the pin code to nd and return to the main settings screen
defer {
tab.screenlock.endSetPin();
allocator.free(state.slock_pin_input1.?);
state.slock_pin_input1 = null;
}
tab.screenlock.card.spin(.on);
tab.screenlock.enbtn.disable();
comm.pipeWrite(.{ .slock_set_pincode = pininput2 }) catch |err| {
logger.err("comm slock_set_pincode: {!}", .{err});
tab.screenlock.card.spin(.off);
tab.screenlock.enbtn.enable();
};
}
export fn nm_screenlock_disbtn_click(_: *lvgl.LvEvent) void {
tab.screenlock.card.spin(.on);
tab.screenlock.disbtn.disable();
comm.pipeWrite(.{ .slock_set_pincode = null }) catch |err| {
logger.err("comm slock_set_pincode(null): {!}", .{err});
tab.screenlock.card.spin(.off);
tab.screenlock.disbtn.enable();
};
}
export fn nm_sysupdates_chansel_changed(_: *lvgl.LvEvent) void {
var buf = [_]u8{0} ** 32;
const name = tab.sysupdates.chansel.getSelectedStr(&buf);

@ -1,5 +1,6 @@
///! see lv_symbols_def.h
pub const Edit = &[_]u8{ 0xef, 0x8c, 0x84 };
pub const EyeClose = &[_]u8{ 0xef, 0x81, 0xb0 };
pub const LightningBolt = &[_]u8{ 0xef, 0x83, 0xa7 };
pub const Loop = &[_]u8{ 0xef, 0x81, 0xb9 };
pub const Ok = &[_]u8{ 0xef, 0x80, 0x8c };

@ -10,21 +10,28 @@ const widget = @import("widget.zig");
pub const bitcoin = @import("bitcoin.zig");
pub const lightning = @import("lightning.zig");
pub const poweroff = @import("poweroff.zig");
pub const screenlock = @import("screenlock.zig");
pub const settings = @import("settings.zig");
const logger = std.log.scoped(.ui);
// defined in src/ui/c/ui.c
extern "c" fn nm_ui_init_theme(disp: *lvgl.LvDisp) void;
// calls back into nm_create_xxx_panel functions defined here during init.
extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int;
extern "c" fn nm_ui_init_main_tabview(screen: *lvgl.LvObj) c_int;
// global allocator set on init.
// must be set before a call to nm_ui_init.
var allocator: std.mem.Allocator = undefined;
pub fn init(gpa: std.mem.Allocator) !void {
allocator = gpa;
settings.allocator = gpa;
pub const InitOpt = struct {
allocator: std.mem.Allocator,
slock: bool, // whether to start the UI in screen locked mode
};
pub fn init(opt: InitOpt) !void {
allocator = opt.allocator;
settings.allocator = opt.allocator;
lvgl.init();
const disp = try drv.initDisplay();
drv.initInput() catch |err| {
@ -33,8 +40,16 @@ pub fn init(gpa: std.mem.Allocator) !void {
// otherwise, impossible to wake up the screen. */
return err;
};
if (nm_ui_init(disp) != 0) {
return error.UiInitFailure;
nm_ui_init_theme(disp);
const main_scr = try lvgl.Screen.active();
if (nm_ui_init_main_tabview(main_scr.lvobj) != 0) {
return error.UiInitMainTabview;
}
try screenlock.init(main_scr);
if (opt.slock) {
screenlock.activate();
}
}
@ -70,6 +85,14 @@ export fn nm_create_settings_nodename(parent: *lvgl.LvObj) ?*lvgl.LvObj {
return card.lvobj;
}
export fn nm_create_settings_screenlock(parent: *lvgl.LvObj) ?*lvgl.LvObj {
const card = settings.initScreenlockPanel(lvgl.Container{ .lvobj = parent }) catch |err| {
logger.err("initScreenlockPanel: {any}", .{err});
return null;
};
return card.lvobj;
}
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});

@ -41,12 +41,11 @@ fn formatUnix(sec: u64, comptime fmt: []const u8, opts: std.fmt.FormatOptions, w
}
fn formatMetricI(value: i64, comptime fmt: []const u8, opts: std.fmt.FormatOptions, w: anytype) !void {
const uval: u64 = std.math.absCast(value);
const uval: u64 = @abs(value);
const base: u64 = 1000;
if (uval < base) {
return std.fmt.formatIntValue(value, fmt, opts, w);
}
if (value < 0) {
try w.writeByte('-');
}
@ -64,8 +63,60 @@ fn formatMetricU(value: u64, comptime fmt: []const u8, opts: std.fmt.FormatOptio
const mags_si = " kMGTPEZY";
const log2 = std.math.log2(value);
const m = @min(log2 / comptime std.math.log2(base), mags_si.len - 1);
const newval = lossyCast(f64, value) / std.math.pow(f64, lossyCast(f64, base), lossyCast(f64, m));
const suffix = mags_si[m];
try std.fmt.formatFloatDecimal(newval, opts, w);
const newval: f64 = lossyCast(f64, value) / std.math.pow(f64, lossyCast(f64, base), lossyCast(f64, m));
try std.fmt.formatType(newval, "d", opts, w, 0);
try w.writeByte(suffix);
}
test "unix" {
const t = std.testing;
var buf: [1024]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
try std.fmt.format(fbs.writer(), "{}", .{unix(1136239445)});
try t.expectEqualStrings("2006-01-02 22:04:05 UTC", fbs.getWritten());
}
test "imetric" {
const t = std.testing;
var buf: [1024]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
const table: []const struct { val: i64, str: []const u8 } = &.{
.{ .val = 0, .str = "0" },
.{ .val = -13, .str = "-13" },
.{ .val = 1000, .str = "1k" },
.{ .val = -1234, .str = "-1.234k" },
.{ .val = 12340, .str = "12.34k" },
.{ .val = -123400, .str = "-123.4k" },
.{ .val = 1234000, .str = "1.234M" },
.{ .val = -1234000000, .str = "-1.234G" },
};
for (table) |item| {
fbs.reset();
try std.fmt.format(fbs.writer(), "{}", .{imetric(item.val)});
try t.expectEqualStrings(item.str, fbs.getWritten());
}
}
test "umetric" {
const t = std.testing;
var buf: [1024]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
const table: []const struct { val: u64, str: []const u8 } = &.{
.{ .val = 0, .str = "0" },
.{ .val = 13, .str = "13" },
.{ .val = 1000, .str = "1k" },
.{ .val = 1234, .str = "1.234k" },
.{ .val = 12340, .str = "12.34k" },
.{ .val = 123400, .str = "123.4k" },
.{ .val = 1234000, .str = "1.234M" },
.{ .val = 1234000000, .str = "1.234G" },
};
for (table) |item| {
fbs.reset();
try std.fmt.format(fbs.writer(), "{}", .{umetric(item.val)});
try t.expectEqualStrings(item.str, fbs.getWritten());
}
}

@ -1,9 +1,9 @@
# ci container file for compiling and testing zig projects.
# requires a ZIGURL build arg. for instance:
# podman build --rm -t ci-zig0.11.0 -f ci-containerfile \
# --build-arg ZIGURL=https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz
# podman build --rm -t ci-zig0.12.0 -f ci-containerfile \
# --build-arg ZIGURL=https://ziglang.org/download/0.12.0/zig-linux-x86_64-0.12.0.tar.xz
FROM alpine:3.18.3
FROM alpine:3.18.6
ARG ZIGURL
RUN apk add --no-cache git curl xz libc-dev sdl2-dev clang16-extra-tools && \