ui: visualize bitcoind status report on the tab panel
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details

the bitcoin tab has now some basic status and stats: parts of blockchain
info, network info and mempool. this is far from complete but makes a
good start for an initial version.

the gui playground is also updated to sent some stub info via comms
periodically.
alex 1 year ago
parent cb2e98cd8b
commit beae868e56
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -185,6 +185,11 @@ fn commThreadLoopCycle() !void {
defer ui_mutex.unlock(); defer ui_mutex.unlock();
ui.poweroff.updateStatus(report) catch |err| logger.err("poweroff.updateStatus: {any}", .{err}); 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)}), else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
} }
} }

@ -72,7 +72,7 @@ fn parseArgs(gpa: std.mem.Allocator) !Flags {
return 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}); comm.write(gpa, w, .ping) catch |err| logger.err("comm.write ping: {any}", .{err});
while (true) { while (true) {
@ -125,6 +125,47 @@ fn commThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
sigquit.set(); 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 { pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) { 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. // ngui proc stdio is auto-closed as soon as its main process terminates.
const uireader = ngui_proc.stdout.?.reader(); const uireader = ngui_proc.stdout.?.reader();
const uiwriter = ngui_proc.stdin.?.writer(); 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{ const sa = os.Sigaction{
.handler = .{ .handler = sighandler }, .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); 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. * 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) static void create_lnd_panel(lv_obj_t *parent)
{ {
lv_obj_t *label = lv_label_create(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) { if (tab_btc == NULL) {
return -1; 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"); lv_obj_t *tab_lnd = lv_tabview_add_tab(tabview, NM_SYMBOL_BOLT " LIGHTNING");
if (tab_lnd == NULL) { if (tab_lnd == NULL) {

@ -7,6 +7,7 @@ const drv = @import("drv.zig");
const symbol = @import("symbol.zig"); const symbol = @import("symbol.zig");
const widget = @import("widget.zig"); const widget = @import("widget.zig");
pub const poweroff = @import("poweroff.zig"); pub const poweroff = @import("poweroff.zig");
pub const bitcoin = @import("bitcoin.zig");
const logger = std.log.scoped(.ui); const logger = std.log.scoped(.ui);
@ -34,6 +35,14 @@ export fn nm_create_info_panel(parent: *lvgl.LvObj) c_int {
return 0; 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 { fn createInfoPanel(cont: lvgl.Container) !void {
const flex = cont.flex(.column, .{}); const flex = cont.flex(.column, .{});
var buf: [100]u8 = undefined; var buf: [100]u8 = undefined;

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