bitcoin tab #24

Manually merged
x1ddos merged 3 commits from bitcoin-tab into master 1 year ago

@ -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", "bitcoind RPC client playground");
btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc).step);
}
}
const DriverTarget = enum {

@ -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,34 @@ 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,
},
mempool: struct {
loaded: bool,
txcount: usize,
usage: u64, // in memory, bytes
max: u64, // bytes
totalfee: f32, // in BTC
minfee: f32, // BTC/kvB
fullrbf: bool,
},
};
};
/// it is important to preserve ordinal values for future compatiblity,
@ -69,7 +98,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 +142,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 +160,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;

@ -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,
bitcoin_report_interval: u64 = 1 * 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() > self.bitcoin_report_interval) {
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,54 @@ fn readWPACtrlMsg(self: *Daemon) !void {
}
}
fn sendBitcoindReport(self: *Daemon) !void {
var client = bitcoindrpc.Client{
.allocator = self.allocator,
.cookiepath = "/ssd/bitcoind/mainnet/.cookie",
};
const bcinfo = try client.call(.getblockchaininfo, {});
defer bcinfo.deinit();
const netinfo = try client.call(.getnetworkinfo, {});
defer netinfo.deinit();
const mempool = try client.call(.getmempoolinfo, {});
defer mempool.deinit();
const btcrep: comm.Message.BitcoindReport = .{
.blocks = bcinfo.value.blocks,
.headers = bcinfo.value.headers,
.timestamp = bcinfo.value.time,
.hash = bcinfo.value.bestblockhash,
.ibd = bcinfo.value.initialblockdownload,
.diskusage = bcinfo.value.size_on_disk,
.version = netinfo.value.subversion,
.conn_in = netinfo.value.connections_in,
.conn_out = netinfo.value.connections_out,
.warnings = bcinfo.value.warnings, // TODO: netinfo.result.warnings
.localaddr = &.{}, // TODO: populate
// something similar to this:
// @round(bcinfo.verificationprogress * 100)
.verifyprogress = 0,
.mempool = .{
.loaded = mempool.value.loaded,
.txcount = mempool.value.size,
.usage = mempool.value.usage,
.max = mempool.value.maxmempool,
.totalfee = mempool.value.total_fee,
.minfee = mempool.value.mempoolminfee,
.fullrbf = mempool.value.fullrbf,
},
};
try comm.write(self.allocator, self.uiwriter, .{ .bitcoind_report = btcrep });
}
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 +591,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();

@ -0,0 +1,296 @@
//! a bitcoin core RPC client.
const std = @import("std");
const ArenaAllocator = std.heap.ArenaAllocator;
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 Method = enum {
getblockchaininfo,
getblockhash,
getmempoolinfo,
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,
};
pub fn Result(comptime m: Method) type {
return struct {
value: ResultValue(m),
arena: *ArenaAllocator,
pub fn deinit(self: @This()) void {
const allocator = self.arena.child_allocator;
self.arena.deinit();
allocator.destroy(self.arena);
}
};
}
pub fn ResultValue(comptime m: Method) type {
return switch (m) {
.getblockchaininfo => BlockchainInfo,
.getblockhash => []const u8,
.getmempoolinfo => MempoolInfo,
.getnetworkinfo => NetworkInfo,
};
}
pub fn MethodArgs(comptime m: Method) type {
return switch (m) {
.getblockchaininfo, .getmempoolinfo, .getnetworkinfo => void,
.getblockhash => struct { height: u64 },
};
}
fn RpcRequest(comptime m: Method) type {
return struct {
jsonrpc: []const u8 = "1.0",
id: u64,
method: []const u8,
params: MethodArgs(m),
};
}
fn RpcResponse(comptime m: Method) type {
return struct {
id: u64,
result: ?ResultValue(m),
@"error": ?struct {
code: isize,
//message: ?[]const u8, // no use for it atm
},
};
}
/// makes an RPC call to the addr:port endpoint.
/// the returned value must be deinit'ed when done.
pub fn call(self: *Client, comptime method: Method, args: MethodArgs(method)) !Result(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);
defer stream.close();
const reader = stream.reader();
_ = try stream.writer().writeAll(reqbytes);
// read and parse the response
try skipResponseHeaders(reader, 4096);
const body = try reader.readAllAlloc(self.allocator, 1 << 20); // 1Mb should be enough for all response types
defer self.allocator.free(body);
return self.parseResponse(method, body);
}
/// reads all response headers, at most `limit` bytes, and returns the index
/// at which response body starts or error.EndOfStream.
/// single header length must be at most `limit` or 1024, whichever is smaller.
fn skipResponseHeaders(r: anytype, comptime limit: usize) !void {
var n: usize = 0;
var buf: [@min(1024, limit)]u8 = undefined;
while (true) {
const slice = try r.readUntilDelimiter(&buf, '\n');
n += slice.len + 1; // delimiter is not included in the slice
if (n > limit) {
return error.StreamTooLong;
}
if (slice.len == 0 or (slice.len == 1 and slice[0] == '\r')) {
return;
}
}
}
fn parseResponse(self: Client, comptime m: Method, b: []const u8) !Result(m) {
var result = Result(m){
.value = undefined,
.arena = try self.allocator.create(ArenaAllocator),
};
errdefer self.allocator.destroy(result.arena);
result.arena.* = ArenaAllocator.init(self.allocator);
var jstream = std.json.TokenStream.init(b);
const jopt = std.json.ParseOptions{ .allocator = result.arena.allocator(), .ignore_unknown_fields = true };
const resp = try std.json.parse(RpcResponse(m), &jstream, jopt);
errdefer result.arena.deinit();
if (resp.@"error") |errfield| {
return rpcErrorFromCode(errfield.code) orelse error.UnknownError;
}
if (resp.result == null) {
return error.NullResult;
}
result.value = resp.result.?;
return result;
}
fn formatreq(self: *Client, comptime m: Method, args: MethodArgs(m)) ![]const u8 {
const req = RpcRequest(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", .{jreq.items.len});
try w.writeAll("\r\n");
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 MempoolInfo = struct {
loaded: bool, // whether the mempool is fully loaded
size: usize, // tx count
bytes: u64, // sum of all virtual transaction sizes as per BIP-141 (discounted witness data)
usage: u64, // total memory usage
total_fee: f32, // total fees in BTC ignoring modified fees through prioritisetransaction
maxmempool: u64, // memory usage cap, in bytes
mempoolminfee: f32, // min fee rate in BTC/kvB for tx to be accepted
minrelaytxfee: f32, // current min relay fee rate
incrementalrelayfee: f32, // min fee rate increment for replacement, in BTC/kvB
unbroadcastcount: u64, // number of transactions that haven't passed initial broadcast yet
fullrbf: bool, // whether the mempool accepts RBF without replaceability signaling inspection
};
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,
};

@ -67,8 +67,12 @@ export fn nm_get_curr_tick() u32 {
export fn nm_check_idle_time(_: *lvgl.LvTimer) void {
const standby_idle_ms = 60000; // 60sec
const idle_ms = lvgl.idleTime();
if (idle_ms > standby_idle_ms and state != .alert) {
state = .standby;
if (idle_ms < standby_idle_ms) {
return;
}
switch (state) {
.alert, .standby => {},
.active => state = .standby,
}
}
@ -105,10 +109,8 @@ export fn nm_wifi_start_connect(ssid: [*:0]const u8, password: [*:0]const u8) vo
};
}
/// callers must hold ui mutex for the whole duration.
fn updateNetworkStatus(report: comm.Message.NetworkReport) !void {
ui_mutex.lock();
defer ui_mutex.unlock();
var wifi_list: ?[:0]const u8 = null;
var wifi_list_ptr: ?[*:0]const u8 = null;
if (report.wifi_scan_networks.len > 0) {
@ -171,21 +173,32 @@ fn commThreadLoop() void {
/// runs one cycle of the commThreadLoop: read messages from stdin and update
/// the UI accordingly.
/// holds ui mutex for most of the duration.
fn commThreadLoopCycle() !void {
const msg = try comm.read(gpa, stdin);
defer comm.free(gpa, msg);
logger.debug("got msg: {s}", .{@tagName(msg)});
switch (msg) {
.ping => try comm.write(gpa, stdout, comm.Message.pong),
.network_report => |report| {
updateNetworkStatus(report) catch |err| logger.err("updateNetworkStatus: {any}", .{err});
ui_mutex.lock(); // guards state and all UI calls below
defer ui_mutex.unlock();
switch (state) {
.standby => switch (msg) {
.ping => try comm.write(gpa, stdout, comm.Message.pong),
else => logger.debug("ignoring: in standby", .{}),
},
.poweroff_progress => |report| {
ui_mutex.lock();
defer ui_mutex.unlock();
ui.poweroff.updateStatus(report) catch |err| logger.err("poweroff.updateStatus: {any}", .{err});
.active, .alert => switch (msg) {
.ping => try comm.write(gpa, stdout, comm.Message.pong),
.network_report => |report| {
updateNetworkStatus(report) catch |err| logger.err("updateNetworkStatus: {any}", .{err});
},
.poweroff_progress => |report| {
ui.poweroff.updateStatus(report) catch |err| logger.err("poweroff.updateStatus: {any}", .{err});
},
.bitcoind_report => |rep| {
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err});
},
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
},
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
}
}
@ -207,7 +220,7 @@ fn uiThreadLoop() void {
comm.write(gpa, stdout, comm.Message.standby) catch |err| {
logger.err("comm.write standby: {any}", .{err});
};
screen.sleep(&wakeup); // blocking
screen.sleep(&ui_mutex, &wakeup); // blocking
// wake up due to touch screen activity or wakeup event is set
logger.info("waking up from sleep", .{});

@ -0,0 +1,20 @@
const std = @import("std");
const base64enc = std.base64.standard.Encoder;
const bitcoinrpc = @import("bitcoindrpc");
pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) {
std.debug.print("memory leaks detected!", .{});
};
const gpa = gpa_state.allocator();
var client = bitcoinrpc.Client{
.allocator = gpa,
.cookiepath = "/ssd/bitcoind/mainnet/.cookie",
};
const res = try client.call(.getmempoolinfo, {});
defer res.deinit();
std.debug.print("{any}\n", .{res.value});
}

@ -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,47 @@ 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 = 801365;
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 = "00000000000000000002bf8029f6be4e40b4a3e0e161b6a1044ddaf9eb126504",
.ibd = false,
.verifyprogress = 100,
.diskusage = 567119364054,
.version = "/Satoshi:24.0.1/",
.conn_in = 8,
.conn_out = 10,
.warnings = "",
.localaddr = &.{},
.mempool = .{
.loaded = true,
.txcount = 100000 + block_count,
.usage = std.math.min(200123456 + block_count * 10, 300000000),
.max = 300000000,
.totalfee = 2.23049932,
.minfee = 0.00004155,
.fullrbf = false,
},
};
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 +186,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 },

@ -0,0 +1,125 @@
//! 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);
/// label color mark start to make "label:" part of a "label: value"
/// in a different color.
const cmark = "#bbbbbb ";
var tab: struct {
// blockchain section
currblock: lvgl.Label,
timestamp: lvgl.Label,
blockhash: lvgl.Label,
// usage section
diskusage: lvgl.Label,
conn_in: lvgl.Label,
conn_out: lvgl.Label,
// mempool section
mempool: struct {
txcount: lvgl.Label,
totalfee: lvgl.Label,
usage_bar: lvgl.Bar,
usage_lab: lvgl.Label,
},
} = undefined;
/// creates the tab content with all elements.
/// must be called only once at UI init.
pub fn initTabPanel(cont: lvgl.Container) !void {
const parent = cont.flex(.column, .{});
// blockchain section
{
const card = try lvgl.Card.new(parent, "BLOCKCHAIN");
const row = try lvgl.FlexLayout.new(card, .row, .{});
row.setWidth(lvgl.sizePercent(100));
row.clearFlag(.scrollable);
const left = try lvgl.FlexLayout.new(row, .column, .{});
left.setWidth(lvgl.sizePercent(50));
left.setPad(10, .row, .{});
tab.currblock = try lvgl.Label.new(left, "HEIGHT\n", .{ .recolor = true });
tab.timestamp = try lvgl.Label.new(left, "TIMESTAMP\n", .{ .recolor = true });
tab.blockhash = try lvgl.Label.new(row, "BLOCK HASH\n", .{ .recolor = true });
tab.blockhash.flexGrow(1);
}
// mempool section
{
const card = try lvgl.Card.new(parent, "MEMPOOL");
const row = try lvgl.FlexLayout.new(card, .row, .{});
row.setWidth(lvgl.sizePercent(100));
row.clearFlag(.scrollable);
const left = try lvgl.FlexLayout.new(row, .column, .{});
left.setWidth(lvgl.sizePercent(50));
left.setPad(8, .top, .{});
left.setPad(10, .row, .{});
tab.mempool.usage_bar = try lvgl.Bar.new(left);
tab.mempool.usage_lab = try lvgl.Label.new(left, "0Mb out of 0Mb (0%)", .{ .recolor = true });
const right = try lvgl.FlexLayout.new(row, .column, .{});
right.setWidth(lvgl.sizePercent(50));
right.setPad(10, .row, .{});
tab.mempool.txcount = try lvgl.Label.new(right, "TRANSACTIONS COUNT\n", .{ .recolor = true });
tab.mempool.totalfee = try lvgl.Label.new(right, "TOTAL FEES\n", .{ .recolor = true });
}
// usage section
{
const card = try lvgl.Card.new(parent, "USAGE");
const row = try lvgl.FlexLayout.new(card, .row, .{});
row.setWidth(lvgl.sizePercent(100));
row.clearFlag(.scrollable);
const left = try lvgl.FlexLayout.new(row, .column, .{});
left.setWidth(lvgl.sizePercent(50));
left.setPad(10, .row, .{});
tab.diskusage = try lvgl.Label.new(left, "DISK USAGE\n", .{ .recolor = true });
const right = try lvgl.FlexLayout.new(row, .column, .{});
right.setWidth(lvgl.sizePercent(50));
right.setPad(10, .row, .{});
tab.conn_in = try lvgl.Label.new(right, "CONNECTIONS IN\n", .{ .recolor = true });
tab.conn_out = try lvgl.Label.new(right, "CONNECTIONS OUT\n", .{ .recolor = true });
}
}
/// updates the tab with new data from the report.
/// the tab must be inited first with initTabPanel.
pub fn updateTabPanel(rep: comm.Message.BitcoindReport) !void {
var buf: [512]u8 = undefined;
// blockchain section
try tab.currblock.setTextFmt(&buf, cmark ++ "HEIGHT#\n{d}", .{rep.blocks});
try tab.timestamp.setTextFmt(&buf, cmark ++ "TIMESTAMP#\n{}", .{xfmt.unix(rep.timestamp)});
try tab.blockhash.setTextFmt(&buf, cmark ++ "BLOCK HASH#\n{s}\n{s}", .{ rep.hash[0..32], rep.hash[32..] });
// mempool section
const mempool_pct: f32 = pct: {
if (rep.mempool.usage > rep.mempool.max) {
break :pct 100;
}
if (rep.mempool.max == 0) {
break :pct 0;
}
const v = @intToFloat(f64, rep.mempool.usage) / @intToFloat(f64, rep.mempool.max);
break :pct @floatCast(f32, v * 100);
};
tab.mempool.usage_bar.setValue(@floatToInt(i32, @round(mempool_pct)));
try tab.mempool.usage_lab.setTextFmt(&buf, "{:.1} " ++ cmark ++ "out of# {:.1} ({d:.1}%)", .{
fmt.fmtIntSizeBin(rep.mempool.usage),
fmt.fmtIntSizeBin(rep.mempool.max),
mempool_pct,
});
try tab.mempool.txcount.setTextFmt(&buf, cmark ++ "TRANSACTIONS COUNT#\n{d}", .{rep.mempool.txcount});
try tab.mempool.totalfee.setTextFmt(&buf, cmark ++ "TOTAL FEES#\n{d:10} BTC", .{rep.mempool.totalfee});
// usage section
try tab.diskusage.setTextFmt(&buf, cmark ++ "DISK USAGE#\n{:.1}", .{fmt.fmtIntSizeBin(rep.diskusage)});
try tab.conn_in.setTextFmt(&buf, cmark ++ "CONNECTIONS IN#\n{d}", .{rep.conn_in});
try tab.conn_out.setTextFmt(&buf, cmark ++ "CONNECTIONS OUT#\n{d}", .{rep.conn_out});
}

@ -20,6 +20,11 @@ void nm_sys_shutdown();
*/
int nm_create_info_panel(lv_obj_t *parent);
/**
* creates the bitcoin tab panel.
*/
int nm_create_bitcoin_panel(lv_obj_t *parent);
/**
* invoken when the UI is switched to the network settings tab.
*/
@ -103,15 +108,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);
@ -338,7 +334,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) {

@ -13,10 +13,19 @@ const logger = std.log.scoped(.screen);
/// a touch screen activity or wake event is triggered.
/// sleep removes all input devices at enter and reinstates them at exit so that
/// a touch event triggers no accidental action.
pub fn sleep(wake: *const Thread.ResetEvent) void {
///
/// the UI mutex is held while calling LVGL UI functions, and released during
/// idling or waiting for wake event.
/// although sleep is safe for concurrent use, the input drivers init/deinit
/// implementation used on entry and exit might not be.
pub fn sleep(ui: *std.Thread.Mutex, wake: *const Thread.ResetEvent) void {
ui.lock();
drv.deinitInput();
widget.topdrop(.show);
ui.unlock();
defer {
ui.lock();
defer ui.unlock();
drv.initInput() catch |err| logger.err("drv.initInput: {any}", .{err});
widget.topdrop(.remove);
}

@ -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(lvgl.Container{ .lvobj = parent }) catch |err| {
logger.err("createBitcoinPanel: {any}", .{err});
return -1;
};
return 0;
}
fn createInfoPanel(cont: lvgl.Container) !void {
const flex = cont.flex(.column, .{});
var buf: [100]u8 = undefined;

@ -0,0 +1,31 @@
//! extra formatting utilities, missing from std.fmt.
const std = @import("std");
/// formats a unix timestamp in YYYY-MM-DD HH:MM:SS UTC.
/// if the sec value greater than u47, outputs raw digits.
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;
if (sec > std.math.maxInt(u47)) {
// EpochSeconds.getEpochDay trucates to u47 which results in a "truncated bits"
// panic for too big numbers. so, just print raw digits.
return std.fmt.format(w, "{d}", .{sec});
}
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(),
});
}