From ff88304278f7038117c992bd50b509fa1e5176d5 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 31 Jul 2023 02:23:21 +0200 Subject: [PATCH] bitcoin rpc doesn't seem to work; resort to cmdline --- build.zig | 10 ++ src/comm.zig | 28 ++++- src/nd/Daemon.zig | 68 ++++++++++ src/nd/bitcoindrpc.zig | 275 +++++++++++++++++++++++++++++++++++++++++ src/ngui.zig | 5 + src/test/btcrpc.zig | 31 +++++ src/test/guiplay.zig | 39 +++++- src/ui/bitcoin.zig | 64 ++++++++++ src/ui/c/ui.c | 29 +++-- src/ui/lvgl.zig | 2 + src/ui/ui.zig | 9 ++ src/xfmt.zig | 24 ++++ 12 files changed, 571 insertions(+), 13 deletions(-) create mode 100644 src/nd/bitcoindrpc.zig create mode 100644 src/test/btcrpc.zig create mode 100644 src/ui/bitcoin.zig create mode 100644 src/xfmt.zig diff --git a/build.zig b/build.zig index 33aebc7..e91f491 100644 --- a/build.zig +++ b/build.zig @@ -135,6 +135,16 @@ pub fn build(b: *std.build.Builder) void { guiplay_build_step.dependOn(&b.addInstallArtifact(guiplay).step); guiplay_build_step.dependOn(ngui_build_step); } + + { + const btcrpc = b.addExecutable("btcrpc", "src/test/btcrpc.zig"); + btcrpc.setTarget(target); + btcrpc.setBuildMode(mode); + btcrpc.strip = strip; + btcrpc.addPackagePath("bitcoindrpc", "src/nd/bitcoindrpc.zig"); + const btcrpc_build_step = b.step("btcrpc", "bitcoin core playground"); + btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc).step); + } } const DriverTarget = enum { diff --git a/src/comm.zig b/src/comm.zig index ea90080..32735b9 100644 --- a/src/comm.zig +++ b/src/comm.zig @@ -27,6 +27,7 @@ pub const Message = union(MessageTag) { network_report: NetworkReport, get_network_report: GetNetworkReport, poweroff_progress: PoweroffProgress, + bitcoind_report: BitcoindReport, pub const WifiConnect = struct { ssid: []const u8, @@ -52,6 +53,25 @@ pub const Message = union(MessageTag) { err: ?[]const u8, }; }; + + pub const BitcoindReport = struct { + blocks: u64, + //headers: u64, + timestamp: u64, // unix epoch + hash: []const u8, // best block hash + ibd: bool, // initial block download + verifyprogress: u8, // 0-100% + diskusage: u64, // estimated size on disk, in bytes + version: []const u8, // bitcoin core version string + conn_in: u16, + conn_out: u16, + warnings: []const u8, + localaddr: []struct { + addr: []const u8, + port: u16, + score: i16, + }, + }; }; /// it is important to preserve ordinal values for future compatiblity, @@ -69,7 +89,9 @@ pub const MessageTag = enum(u16) { wakeup = 0x08, // nd -> ngui: reports poweroff progress poweroff_progress = 0x09, - // next: 0x0a + // nd -> ngui: bitcoin core daemon status report + bitcoind_report = 0x0a, + // next: 0x0b }; /// reads and parses a single message from the input stream reader. @@ -111,6 +133,9 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !Message { .poweroff_progress => Message{ .poweroff_progress = try json.parse(Message.PoweroffProgress, &jstream, jopt), }, + .bitcoind_report => Message{ + .bitcoind_report = try json.parse(Message.BitcoindReport, &jstream, jopt), + }, }; } @@ -126,6 +151,7 @@ pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void { .network_report => try json.stringify(msg.network_report, jopt, data.writer()), .get_network_report => try json.stringify(msg.get_network_report, jopt, data.writer()), .poweroff_progress => try json.stringify(msg.poweroff_progress, jopt, data.writer()), + .bitcoind_report => try json.stringify(msg.bitcoind_report, jopt, data.writer()), } if (data.items.len > std.math.maxInt(u64)) { return Error.CommWriteTooLarge; diff --git a/src/nd/Daemon.zig b/src/nd/Daemon.zig index b74c2dd..b1269b6 100644 --- a/src/nd/Daemon.zig +++ b/src/nd/Daemon.zig @@ -20,6 +20,7 @@ const network = @import("network.zig"); const screen = @import("../ui/screen.zig"); const types = @import("../types.zig"); const SysService = @import("SysService.zig"); +const bitcoindrpc = @import("bitcoindrpc.zig"); const logger = std.log.scoped(.daemon); @@ -50,6 +51,10 @@ want_wifi_scan: bool, // initiate wifi scan at the next loop cycle network_report_ready: bool, // indicates whether the network status is ready to be sent wifi_scan_in_progress: bool = false, wpa_save_config_on_connected: bool = false, +// bitcoin flags +want_bitcoind_report: bool, +bitcoin_timer: time.Timer, +bitcoind_report_interval: u64 = time.ns_per_min, /// system services actively managed by the daemon. /// these are stop'ed during poweroff and their shutdown progress sent to ngui. @@ -82,6 +87,9 @@ pub fn init(a: std.mem.Allocator, r: std.fs.File.Reader, w: std.fs.File.Writer, .want_network_report = true, .want_wifi_scan = false, .network_report_ready = true, + // report bitcoind status immediately on start + .want_bitcoind_report = true, + .bitcoin_timer = try time.Timer.start(), }; } @@ -269,6 +277,14 @@ fn mainThreadLoopCycle(self: *Daemon) !void { logger.err("network.sendReport: {any}", .{err}); } } + if (self.want_bitcoind_report or self.bitcoin_timer.read() > time.ns_per_min) { + if (self.sendBitcoindReport()) { + self.bitcoin_timer.reset(); + self.want_bitcoind_report = false; + } else |err| { + logger.err("sendBitcoinReport: {any}", .{err}); + } + } } /// comm thread entry point: reads messages sent from ngui and acts accordinly. @@ -484,12 +500,63 @@ fn readWPACtrlMsg(self: *Daemon) !void { } } +fn sendBitcoindReport(self: *Daemon) !void { + var arena_state = std.heap.ArenaAllocator.init(self.allocator); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + + //var client = bitcoindrpc.Client{ + // .allocator = arena, + // .cookiepath = "/ssd/bitcoind/mainnet/.cookie", + //}; + //const bcinfo = try client.call(.getblockchaininfo, {}); + ////defer bcinfo.free(); + //const netinfo = try client.call(.getnetworkinfo, {}); + ////defer netinfo.free(); + + const bcinfo = try runBitcoinCli(arena, bitcoindrpc.BlockchainInfo, "getblockchaininfo"); + const netinfo = try runBitcoinCli(arena, bitcoindrpc.NetworkInfo, "getnetworkinfo"); + + //logger.info("bcinfo:\n{any}", .{bcinfo}); + + const btcrep: comm.Message.BitcoindReport = .{ + .blocks = bcinfo.blocks, + //.headers = bcinfo.headers, + .timestamp = bcinfo.time, + .hash = bcinfo.bestblockhash, + .ibd = bcinfo.initialblockdownload, + .diskusage = bcinfo.size_on_disk, + .version = netinfo.subversion, + .conn_in = netinfo.connections_in, + .conn_out = netinfo.connections_out, + .warnings = bcinfo.warnings, // TODO: netinfo.result.warnings + .localaddr = &.{}, // TODO: populate + // something similar to this: + // @round(bcinfo.verificationprogress * 100) + .verifyprogress = 0, + }; + + //logger.info("sending bitcoin report:\n{any}", .{btcrep}); + try comm.write(self.allocator, self.uiwriter, .{ .bitcoind_report = btcrep }); +} + +fn runBitcoinCli(arena: std.mem.Allocator, comptime T: type, method: []const u8) !T { + const res = try std.ChildProcess.exec(.{ + .allocator = arena, + .argv = &.{ "/opt/bin/bitcoin-cli.sh", method }, + }); + var jstream = std.json.TokenStream.init(res.stdout); + const jopt = std.json.ParseOptions{ .allocator = arena, .ignore_unknown_fields = true }; + return try std.json.parse(T, &jstream, jopt); +} + test "start-stop" { const t = std.testing; const pipe = try types.IoPipe.create(); var daemon = try Daemon.init(t.allocator, pipe.reader(), pipe.writer(), "/dev/null"); daemon.want_network_report = false; + daemon.want_bitcoind_report = false; try t.expect(daemon.state == .stopped); try daemon.start(); @@ -533,6 +600,7 @@ test "start-poweroff" { const gui_reader = gui_stdin.reader(); var daemon = try Daemon.init(arena, gui_stdout.reader(), gui_stdin.writer(), "/dev/null"); daemon.want_network_report = false; + daemon.want_bitcoind_report = false; defer { daemon.deinit(); gui_stdin.close(); diff --git a/src/nd/bitcoindrpc.zig b/src/nd/bitcoindrpc.zig new file mode 100644 index 0000000..f1c8e20 --- /dev/null +++ b/src/nd/bitcoindrpc.zig @@ -0,0 +1,275 @@ +const std = @import("std"); +const Atomic = std.atomic.Atomic; +const base64enc = std.base64.standard.Encoder; + +pub const Client = struct { + allocator: std.mem.Allocator, + cookiepath: []const u8, + addr: []const u8 = "127.0.0.1", + port: u16 = 8332, + + // each request gets a new ID with a value of reqid.fetchAdd(1, .Monotonic) + reqid: Atomic(u64) = Atomic(u64).init(1), + + pub const MethodTag = enum { + getblockhash, + getblockchaininfo, + getnetworkinfo, + }; + + pub const RpcError = error{ + // json-rpc 2.0 + RpcInvalidRequest, + RpcMethodNotFound, + RpcInvalidParams, + RpcInternalError, + RpcParseError, + // general purpose errors + RpcMiscError, + RpcTypeError, + RpcInvalidAddressOrKey, + RpcOutOfMemory, + RpcInvalidParameter, + RpcDatabaseError, + RpcDeserializationError, + RpcVerifyError, + RpcVerifyRejected, + RpcVerifyAlreadyInChain, + RpcInWarmup, + RpcMethodDeprecated, + // p2p client errors + RpcClientNotConnected, + RpcClientInInitialDownload, + RpcClientNodeAlreadyAdded, + RpcClientNodeNotAdded, + RpcClientNodeNotConnected, + RpcClientInvalidIpOrSubnet, + RpcClientP2pDisabled, + RpcClientNodeCapacityReached, + // chain errors + RpcClientMempoolDisabled, + }; + + /// makes an RPC call to the addr:port endpoint. + /// the returned value always has a .result field of type ResultType(method); + /// all other fields are for internal use. + /// + /// callers must free resources allocated for the retuned value using its `free` function. + pub fn call(self: *Client, comptime method: MethodTag, args: MethodArgs(method)) !CallRetType(method) { + const addrport = try std.net.Address.resolveIp(self.addr, self.port); + const reqbytes = try self.formatreq(method, args); + defer self.allocator.free(reqbytes); + + // connect and send the request + const stream = try std.net.tcpConnectToAddress(addrport); + errdefer stream.close(); + const reader = stream.reader(); + _ = try stream.writer().writeAll(reqbytes); + + // read response + var buf: [512]u8 = undefined; + var resbytes = std.ArrayList(u8).init(self.allocator); + defer resbytes.deinit(); + while (true) { + // TODO: use LimitedReader + const n = try reader.read(&buf); + if (n == 0) { + break; // EOS + } + try resbytes.appendSlice(buf[0..n]); + } + + // search for end of headers in the response + var last_byte: u8 = 0; + var idx: usize = 0; + for (resbytes.items) |v, i| { + if (v == '\r') { + continue; + } + if (v == '\n' and last_byte == '\n') { + idx = i + 1; + break; + } + last_byte = v; + } + if (idx == 0 or idx >= resbytes.items.len) { + return error.NoBodyInResponse; + } + + // parse the response body and return its .result field or an error. + var arena_state = std.heap.ArenaAllocator.init(self.allocator); + errdefer arena_state.deinit(); + const arena = arena_state.allocator(); + var jstream = std.json.TokenStream.init(resbytes.items[idx..]); + const jopt = std.json.ParseOptions{ .allocator = arena, .ignore_unknown_fields = true }; + const Typ = RpcRespType(method); + @setEvalBranchQuota(2000); // std/json.zig:1520:24: error: evaluation exceeded 1000 backwards branches + const resp = try std.json.parse(Typ, &jstream, jopt); + if (resp.@"error") |errfield| { + return rpcErrorFromCode(errfield.code) orelse error.UnknownError; + } + if (resp.result == null) { + return error.NullResult; + } + return .{ .result = &resp.result.?, .arena = arena_state }; + } + + fn CallRetType(comptime method: MethodTag) type { + return struct { + result: *const ResultType(method), + arena: std.heap.ArenaAllocator, + + pub fn free(self: @This()) void { + self.arena.deinit(); + } + }; + } + + pub fn MethodArgs(comptime m: MethodTag) type { + return switch (m) { + .getblockchaininfo, .getnetworkinfo => void, + .getblockhash => struct { height: u64 }, + }; + } + pub fn ResultType(comptime m: MethodTag) type { + return switch (m) { + .getblockchaininfo => BlockchainInfo, + .getnetworkinfo => NetworkInfo, + .getblockhash => []const u8, + }; + } + + fn RpcReqType(comptime m: MethodTag) type { + return struct { + jsonrpc: []const u8 = "1.0", + id: u64, + method: []const u8, + params: MethodArgs(m), + }; + } + + fn RpcRespType(comptime m: MethodTag) type { + return struct { + id: u64, + result: ?ResultType(m), // keep field name or modify free fn in CallRetType + @"error": ?struct { + code: isize, + //message: []const u8, // no use for this atm + }, + }; + } + + fn formatreq(self: *Client, comptime m: MethodTag, args: MethodArgs(m)) ![]const u8 { + const req = RpcReqType(m){ + .id = self.reqid.fetchAdd(1, .Monotonic), + .method = @tagName(m), + .params = args, + }; + var jreq = std.ArrayList(u8).init(self.allocator); + defer jreq.deinit(); + try std.json.stringify(req, .{}, jreq.writer()); + + const auth = try self.getAuthBase64(); + defer self.allocator.free(auth); + + var bytes = std.ArrayList(u8).init(self.allocator); + const w = bytes.writer(); + try w.writeAll("POST / HTTP/1.0\r\n"); + //try w.writeAll("Host: 127.0.0.1\n", .{}); + try w.writeAll("Connection: close\r\n"); + try w.print("Authorization: Basic {s}\r\n", .{auth}); + try w.writeAll("Accept: application/json-rpc\r\n"); + try w.writeAll("Content-Type: application/json-rpc\r\n"); + try w.print("Content-Length: {d}\r\n\r\n", .{jreq.items.len}); + try w.writeAll(jreq.items); + return bytes.toOwnedSlice(); + } + + fn getAuthBase64(self: Client) ![]const u8 { + const file = try std.fs.openFileAbsolute(self.cookiepath, .{ .mode = .read_only }); + 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)); + return base64enc.encode(auth, cookie); + } + + // taken from bitcoind source code. + // see https://github.com/bitcoin/bitcoin/blob/64440bb73/src/rpc/protocol.h#L23 + fn rpcErrorFromCode(code: isize) ?RpcError { + return switch (code) { + // json-rpc 2.0 + -32600 => error.RpcInvalidRequest, + -32601 => error.RpcMethodNotFound, + -32602 => error.RpcInvalidParams, + -32603 => error.RpcInternalError, + -32700 => error.RpcParseError, + // general purpose errors + -1 => error.RpcMiscError, + -3 => error.RpcTypeError, + -5 => error.RpcInvalidAddressOrKey, + -7 => error.RpcOutOfMemory, + -8 => error.RpcInvalidParameter, + -20 => error.RpcDatabaseError, + -22 => error.RpcDeserializationError, + -25 => error.RpcVerifyError, + -26 => error.RpcVerifyRejected, + -27 => error.RpcVerifyAlreadyInChain, + -28 => error.RpcInWarmup, + -32 => error.RpcMethodDeprecated, + // p2p client errors + -9 => error.RpcClientNotConnected, + -10 => error.RpcClientInInitialDownload, + -23 => error.RpcClientNodeAlreadyAdded, + -24 => error.RpcClientNodeNotAdded, + -29 => error.RpcClientNodeNotConnected, + -30 => error.RpcClientInvalidIpOrSubnet, + -31 => error.RpcClientP2pDisabled, + -34 => error.RpcClientNodeCapacityReached, + // chain errors + -33 => error.RpcClientMempoolDisabled, + else => null, + }; + } +}; + +pub const BlockchainInfo = struct { + chain: []const u8, + blocks: u64, + //headers: u64, + bestblockhash: []const u8, + //difficulty: f64, + time: u64, // block time, unix epoch + //mediantime: u64, // median block time, unix epoch + verificationprogress: f32, // estimate in [0..1] + initialblockdownload: bool, + size_on_disk: u64, + pruned: bool, + //pruneheight: ?u64, // present if pruning is enabled + //automatic_prunning: ?bool, // present if pruning is enabled + //prune_target_size: ?u64, // present if automatic is enabled + warnings: []const u8, +}; + +pub const NetworkInfo = struct { + version: u32, + subversion: []const u8, + protocolversion: u32, + connections: u16, // in + out + connections_in: u16, + connections_out: u16, + networkactive: bool, + networks: []struct { + name: []const u8, // ipv4, ipv6, onion, i2p, cjdns + limited: bool, // whether this network is limited with -onlynet flag + reachable: bool, + }, + relayfee: f32, // min rate, in BTC/kvB + incrementalfee: f32, // min rate increment for RBF in BTC/vkB + localaddresses: []struct { + address: []const u8, + port: u16, + score: i16, + }, + warnings: []const u8, +}; diff --git a/src/ngui.zig b/src/ngui.zig index 747306d..1f010ed 100644 --- a/src/ngui.zig +++ b/src/ngui.zig @@ -185,6 +185,11 @@ fn commThreadLoopCycle() !void { defer ui_mutex.unlock(); ui.poweroff.updateStatus(report) catch |err| logger.err("poweroff.updateStatus: {any}", .{err}); }, + .bitcoind_report => |rep| { + ui_mutex.lock(); + defer ui_mutex.unlock(); + ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err}); + }, else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}), } } diff --git a/src/test/btcrpc.zig b/src/test/btcrpc.zig new file mode 100644 index 0000000..420dc5b --- /dev/null +++ b/src/test/btcrpc.zig @@ -0,0 +1,31 @@ +const std = @import("std"); +const base64enc = std.base64.standard.Encoder; +const bitcoinrpc = @import("bitcoindrpc"); + +pub fn main() !void { + //var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); + //defer arena_state.deinit(); + //const arena = arena_state.allocator(); + var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; + defer if (gpa_state.deinit()) { + std.debug.print("!!!!!!!!!! memory leaks detected", .{}); + }; + const arena = gpa_state.allocator(); + + var client = bitcoinrpc.Client{ + .allocator = arena, + .cookiepath = "/ssd/bitcoind/mainnet/.cookie", + }; + + const hash = try client.call(.getblockhash, .{ .height = 0 }); + defer hash.free(); + std.debug.print("hash of 1001: {s}\n", .{hash.result}); + + const bcinfo = try client.call(.getblockchaininfo, {}); + defer bcinfo.free(); + std.debug.print("{any}\n", .{bcinfo.result}); + + const netinfo = try client.call(.getnetworkinfo, {}); + defer netinfo.free(); + std.debug.print("{any}\n", .{netinfo.result}); +} diff --git a/src/test/guiplay.zig b/src/test/guiplay.zig index 988e118..4e6c25e 100644 --- a/src/test/guiplay.zig +++ b/src/test/guiplay.zig @@ -72,7 +72,7 @@ fn parseArgs(gpa: std.mem.Allocator) !Flags { return flags; } -fn commThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void { +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}); while (true) { @@ -125,6 +125,38 @@ fn commThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void { sigquit.set(); } +fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void { + var sectimer = try time.Timer.start(); + var block_count: u64 = 0; + + while (true) { + time.sleep(time.ns_per_s); + if (sectimer.read() < time.ns_per_s) { + continue; + } + + sectimer.reset(); + block_count += 1; + const now = time.timestamp(); + + const btcrep: comm.Message.BitcoindReport = .{ + .blocks = block_count, + .headers = block_count, + .timestamp = @intCast(u64, now), + .hash = "00000012345", + .ibd = false, + .verifyprogress = 100, + .diskusage = 567119364054, + .version = "/Satoshi:24.0.1/", + .conn_in = 8, + .conn_out = 10, + .warnings = "", + .localaddr = &.{}, + }; + comm.write(gpa, w, .{ .bitcoind_report = btcrep }) catch |err| logger.err("comm.write: {any}", .{err}); + } +} + pub fn main() !void { var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; defer if (gpa_state.deinit()) { @@ -145,7 +177,10 @@ pub fn main() !void { // ngui proc stdio is auto-closed as soon as its main process terminates. const uireader = ngui_proc.stdout.?.reader(); const uiwriter = ngui_proc.stdin.?.writer(); - _ = try std.Thread.spawn(.{}, commThread, .{ gpa, uireader, uiwriter }); + const th1 = try std.Thread.spawn(.{}, commReadThread, .{ gpa, uireader, uiwriter }); + th1.detach(); + const th2 = try std.Thread.spawn(.{}, commWriteThread, .{ gpa, uiwriter }); + th2.detach(); const sa = os.Sigaction{ .handler = .{ .handler = sighandler }, diff --git a/src/ui/bitcoin.zig b/src/ui/bitcoin.zig new file mode 100644 index 0000000..33a29e6 --- /dev/null +++ b/src/ui/bitcoin.zig @@ -0,0 +1,64 @@ +//! bitcoin main tab panel. +//! all functions assume LVGL is init'ed and ui mutex is locked on entry. + +const std = @import("std"); +const fmt = std.fmt; + +const lvgl = @import("lvgl.zig"); +const comm = @import("../comm.zig"); +const xfmt = @import("../xfmt.zig"); + +const logger = std.log.scoped(.ui); + +var tab: struct { + // blockchain + currblock: *lvgl.LvObj, // label + timestamp: *lvgl.LvObj, // label + blockhash: *lvgl.LvObj, // label + // usage + diskusage: *lvgl.LvObj, // label + conn_in: *lvgl.LvObj, // label + conn_out: *lvgl.LvObj, // label +} = undefined; + +pub fn initTabPanel(parent: *lvgl.LvObj) !void { + parent.flexFlow(.column); + + const box1 = try lvgl.createFlexObject(parent, .column); + box1.setHeightToContent(); + box1.setWidth(lvgl.sizePercent(100)); + const l1 = try lvgl.createLabel(box1, "BLOCKCHAIN", .{}); + l1.addStyle(lvgl.nm_style_title(), .{}); + + tab.currblock = try lvgl.createLabel(box1, "current height: 0", .{}); + tab.timestamp = try lvgl.createLabel(box1, "timestamp:", .{}); + tab.blockhash = try lvgl.createLabel(box1, "block hash:", .{}); + + const box2 = try lvgl.createFlexObject(parent, .column); + box2.setHeightToContent(); + box2.setWidth(lvgl.sizePercent(100)); + const l2 = try lvgl.createLabel(box2, "USAGE", .{}); + l2.addStyle(lvgl.nm_style_title(), .{}); + + tab.diskusage = try lvgl.createLabel(box2, "disk usage:", .{}); + tab.conn_in = try lvgl.createLabel(box2, "connections in:", .{}); + tab.conn_out = try lvgl.createLabel(box2, "connections out:", .{}); +} + +pub fn updateTabPanel(rep: comm.Message.BitcoindReport) !void { + var buf: [512]u8 = undefined; + var s = try fmt.bufPrintZ(&buf, "height: {d}", .{rep.blocks}); + tab.currblock.setLabelText(s); + s = try fmt.bufPrintZ(&buf, "timestamp: {}", .{xfmt.unix(rep.timestamp)}); + //s = try fmt.bufPrintZ(&buf, "timestamp: {}", .{rep.timestamp}); + tab.timestamp.setLabelText(s); + s = try fmt.bufPrintZ(&buf, "block hash: {s}", .{rep.hash}); + tab.blockhash.setLabelText(s); + + s = try fmt.bufPrintZ(&buf, "disk usage: {.1}", .{fmt.fmtIntSizeBin(rep.diskusage)}); + tab.diskusage.setLabelText(s); + s = try fmt.bufPrintZ(&buf, "connections in: {d}", .{rep.conn_in}); + tab.conn_in.setLabelText(s); + s = try fmt.bufPrintZ(&buf, "connections out: {d}", .{rep.conn_out}); + tab.conn_out.setLabelText(s); +} diff --git a/src/ui/c/ui.c b/src/ui/c/ui.c index b87a291..effb889 100644 --- a/src/ui/c/ui.c +++ b/src/ui/c/ui.c @@ -20,6 +20,17 @@ void nm_sys_shutdown(); */ int nm_create_info_panel(lv_obj_t *parent); +int nm_create_bitcoin_panel(lv_obj_t *parent); +//static void create_bitcoin_panel(lv_obj_t *parent) +//{ +// lv_obj_t *label = lv_label_create(parent); +// lv_label_set_text_static(label, +// "bitcoin tab isn't designed yet\n" +// "follow https://nakamochi.io"); +// lv_obj_center(label); +//} + + /** * invoken when the UI is switched to the network settings tab. */ @@ -72,6 +83,11 @@ extern lv_style_t *nm_style_btn_red() return &style_btn_red; } +extern lv_style_t *nm_style_title() +{ + return &style_title; +} + static void textarea_event_cb(lv_event_t *e) { lv_obj_t *textarea = lv_event_get_target(e); @@ -98,15 +114,6 @@ static void textarea_event_cb(lv_event_t *e) } } -static void create_bitcoin_panel(lv_obj_t *parent) -{ - lv_obj_t *label = lv_label_create(parent); - lv_label_set_text_static(label, - "bitcoin tab isn't designed yet\n" - "follow https://nakamochi.io"); - lv_obj_center(label); -} - static void create_lnd_panel(lv_obj_t *parent) { lv_obj_t *label = lv_label_create(parent); @@ -333,7 +340,9 @@ extern int nm_ui_init(lv_disp_t *disp) if (tab_btc == NULL) { return -1; } - create_bitcoin_panel(tab_btc); + if (nm_create_bitcoin_panel(tab_btc) != 0) { + return -1; + } lv_obj_t *tab_lnd = lv_tabview_add_tab(tabview, NM_SYMBOL_BOLT " LIGHTNING"); if (tab_lnd == NULL) { diff --git a/src/ui/lvgl.zig b/src/ui/lvgl.zig index 77bada1..0f28065 100644 --- a/src/ui/lvgl.zig +++ b/src/ui/lvgl.zig @@ -720,6 +720,8 @@ pub fn createSpinner(parent: *LvObj) !*LvObj { /// returns a red button style. 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 // 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; diff --git a/src/ui/ui.zig b/src/ui/ui.zig index 8e3e5d3..6c1c15a 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -7,6 +7,7 @@ const drv = @import("drv.zig"); const symbol = @import("symbol.zig"); const widget = @import("widget.zig"); pub const poweroff = @import("poweroff.zig"); +pub const bitcoin = @import("bitcoin.zig"); const logger = std.log.scoped(.ui); @@ -34,6 +35,14 @@ export fn nm_create_info_panel(parent: *lvgl.LvObj) c_int { return 0; } +export fn nm_create_bitcoin_panel(parent: *lvgl.LvObj) c_int { + bitcoin.initTabPanel(parent) catch |err| { + logger.err("createBitcoinPanel: {any}", .{err}); + return -1; + }; + return 0; +} + fn createInfoPanel(parent: *lvgl.LvObj) !void { parent.flexFlow(.column); parent.flexAlign(.start, .start, .start); diff --git a/src/xfmt.zig b/src/xfmt.zig new file mode 100644 index 0000000..0b8cb66 --- /dev/null +++ b/src/xfmt.zig @@ -0,0 +1,24 @@ +//! extra formatting utilities, missing from std.fmt. + +const std = @import("std"); + +pub fn unix(sec: u64) std.fmt.Formatter(formatUnix) { + return .{ .data = sec }; +} + +fn formatUnix(sec: u64, comptime fmt: []const u8, opts: std.fmt.FormatOptions, w: anytype) !void { + _ = fmt; // unused + _ = opts; + const epoch: std.time.epoch.EpochSeconds = .{ .secs = sec }; + const daysec = epoch.getDaySeconds(); + const yearday = epoch.getEpochDay().calculateYearDay(); + const monthday = yearday.calculateMonthDay(); + return std.fmt.format(w, "{d}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2} UTC", .{ + yearday.year, + monthday.month.numeric(), + monthday.day_index + 1, + daysec.getHoursIntoDay(), + daysec.getMinutesIntoHour(), + daysec.getSecondsIntoMinute(), + }); +}