diff --git a/src/comm.zig b/src/comm.zig index 6b4031f..1dcd33d 100644 --- a/src/comm.zig +++ b/src/comm.zig @@ -19,6 +19,8 @@ pub const Message = union(MessageTag) { ping: void, pong: void, poweroff: void, + standby: void, + wakeup: void, wifi_connect: WifiConnect, network_report: NetworkReport, get_network_report: GetNetworkReport, @@ -48,7 +50,11 @@ pub const MessageTag = enum(u16) { wifi_connect = 0x04, network_report = 0x05, get_network_report = 0x06, - // next: 0x07 + // ngui -> nd: screen timeout, no user activity; no reply + standby = 0x07, + // ngui -> nd: resume screen due to user touch; no reply + wakeup = 0x08, + // next: 0x09 }; /// reads and parses a single message from the input stream reader. @@ -64,6 +70,8 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !Message { .ping => Message{ .ping = {} }, .pong => Message{ .pong = {} }, .poweroff => Message{ .poweroff = {} }, + .standby => Message{ .standby = {} }, + .wakeup => Message{ .wakeup = {} }, else => Error.CommReadZeroLenInNonVoidTag, }; } @@ -74,7 +82,7 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !Message { const jopt = json.ParseOptions{ .allocator = allocator, .ignore_unknown_fields = true }; var jstream = json.TokenStream.init(bytes); return switch (tag) { - .ping, .pong, .poweroff => unreachable, // handled above + .ping, .pong, .poweroff, .standby, .wakeup => unreachable, // handled above .wifi_connect => Message{ .wifi_connect = try json.parse(Message.WifiConnect, &jstream, jopt), }, @@ -94,7 +102,7 @@ pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void { var data = ByteArrayList.init(allocator); defer data.deinit(); switch (msg) { - .ping, .pong, .poweroff => {}, // zero length payload + .ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload .wifi_connect => try json.stringify(msg.wifi_connect, jopt, data.writer()), .network_report => try json.stringify(msg.network_report, jopt, data.writer()), .get_network_report => try json.stringify(msg.get_network_report, jopt, data.writer()), @@ -110,7 +118,7 @@ pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void { pub fn free(allocator: mem.Allocator, m: Message) void { switch (m) { - .ping, .pong, .poweroff => {}, // zero length payload + .ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload else => |v| { json.parseFree(@TypeOf(v), v, .{ .allocator = allocator }); }, @@ -167,6 +175,8 @@ test "write/read void tags" { Message.ping, Message.pong, Message.poweroff, + Message.standby, + Message.wakeup, }; for (msg) |m| { diff --git a/src/nd.zig b/src/nd.zig index 3c499b9..ab4e984 100644 --- a/src/nd.zig +++ b/src/nd.zig @@ -8,6 +8,7 @@ const nif = @import("nif"); const comm = @import("comm.zig"); const Daemon = @import("nd/Daemon.zig"); +const screen = @import("ui/screen.zig"); const logger = std.log.scoped(.nd); const stderr = std.io.getStdErr().writer(); @@ -126,6 +127,12 @@ pub fn main() !void { const args = try parseArgs(gpa); defer args.deinit(gpa); + // reset the screen backlight to normal power regardless + // of its previous state. + screen.backlight(.on) catch |err| { + logger.err("backlight: {any}", .{err}); + }; + // start ngui, unless -nogui mode var ngui = std.ChildProcess.init(&.{args.gui.?}, gpa); ngui.stdin_behavior = .Pipe; @@ -198,6 +205,18 @@ pub fn main() !void { logger.err("startConnectWifi: {any}", .{err}); }; }, + .standby => { + logger.info("entering standby mode", .{}); + nd.standby() catch |err| { + logger.err("nd.standby: {any}", .{err}); + }; + }, + .wakeup => { + logger.info("wakeup from standby", .{}); + nd.wakeup() catch |err| { + logger.err("nd.wakeup: {any}", .{err}); + }; + }, else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}), } comm.free(gpa, msg); diff --git a/src/nd/Daemon.zig b/src/nd/Daemon.zig index 0cba88f..6bd6268 100644 --- a/src/nd/Daemon.zig +++ b/src/nd/Daemon.zig @@ -7,7 +7,7 @@ const time = std.time; const nif = @import("nif"); const comm = @import("../comm.zig"); -//const ioq = @import("../ioq.zig"); +const screen = @import("../ui/screen.zig"); const logger = std.log.scoped(.netmon); @@ -42,6 +42,14 @@ pub fn stop(self: *Daemon) void { } } +pub fn standby(_: *Daemon) !void { + try screen.backlight(.off); +} + +pub fn wakeup(_: *Daemon) !void { + try screen.backlight(.on); +} + /// main thread entry point. fn mainThreadLoop(self: *Daemon) !void { try self.wpa_ctrl.attach(); diff --git a/src/ngui.zig b/src/ngui.zig index 90e9fc9..5b19a7b 100644 --- a/src/ngui.zig +++ b/src/ngui.zig @@ -7,6 +7,7 @@ const comm = @import("comm.zig"); const types = @import("types.zig"); const ui = @import("ui/ui.zig"); const lvgl = @import("ui/lvgl.zig"); +const screen = @import("ui/screen.zig"); const symbol = @import("ui/symbol.zig"); /// SIGPIPE is triggered when a process attempts to write to a broken pipe. @@ -30,6 +31,19 @@ var gpa: mem.Allocator = undefined; var ui_mutex: Thread.Mutex = .{}; /// the program runs until quit is true. var quit: bool = false; +var state: enum { + active, // normal operational mode + standby, // idling + alert, // draw user attention; never go standby +} = .active; + +/// setting wakeup brings the screen back from sleep()'ing without waiting +/// for user action. +/// can be used by comms when an alert is received from the daemon, to draw +/// user attention. +/// safe for concurrent use except wakeup.reset() is UB during another thread +/// wakeup.wait()'ing or timedWait'ing. +var wakeup = Thread.ResetEvent{}; /// a monotonic clock for reporting elapsed ticks to LVGL. /// the timer runs throughout the whole duration of the UI program. @@ -46,6 +60,15 @@ export fn nm_get_curr_tick() u32 { return @truncate(u32, ms); } +export fn nm_check_idle_time(_: *lvgl.LvTimer) void { + const standby_idle_ms = 60000; // 60sec + const idle_ms = lvgl.lv_disp_get_inactive_time(null); + logger.debug("idle: {d}", .{idle_ms}); + if (idle_ms > standby_idle_ms and state != .alert) { + state = .standby; + } +} + /// initiate system shutdown. export fn nm_sys_shutdown() void { logger.info("initiating system shutdown", .{}); @@ -179,16 +202,45 @@ pub fn main() anyerror!void { const th = try Thread.spawn(.{}, commThread, .{}); th.detach(); + // run idle timer indefinitely + if (lvgl.lv_timer_create(nm_check_idle_time, 2000, null)) |t| { + lvgl.lv_timer_set_repeat_count(t, -1); + } else { + logger.err("lv_timer_create idle failed: OOM?", .{}); + } + // main UI thread; must never block unless in idle/sleep mode // TODO: handle sigterm while (true) { ui_mutex.lock(); var till_next_ms = lvgl.lv_timer_handler(); const do_quit = quit; + const do_state = state; ui_mutex.unlock(); if (do_quit) { return; } + if (do_state == .standby) { + // go into a screen sleep mode due to no user activity + wakeup.reset(); + comm.write(gpa, stdout, comm.Message.standby) catch |err| { + logger.err("comm.write standby: {any}", .{err}); + }; + screen.sleep(&wakeup); + // wake up due to touch screen activity or wakeup event is set + logger.info("waking up from sleep", .{}); + ui_mutex.lock(); + if (state == .standby) { + state = .active; + comm.write(gpa, stdout, comm.Message.wakeup) catch |err| { + logger.err("comm.write wakeup: {any}", .{err}); + }; + lvgl.lv_disp_trig_activity(null); + } + ui_mutex.unlock(); + continue; + } + std.atomic.spinLoopHint(); // sleep at least 1ms time.sleep(@max(1, till_next_ms) * time.ns_per_ms); } diff --git a/src/test.zig b/src/test.zig index 841acaa..7ab39c5 100644 --- a/src/test.zig +++ b/src/test.zig @@ -8,6 +8,11 @@ export fn lv_timer_del(timer: *opaque {}) void { _ = timer; } +export fn lv_disp_get_inactive_time(disp: *opaque {}) u32 { + _ = disp; + return 0; +} + test { _ = @import("comm.zig"); _ = @import("ngui.zig"); diff --git a/src/ui/c/drv_fbev.c b/src/ui/c/drv_fbev.c index b83ffd4..e8f8bd7 100644 --- a/src/ui/c/drv_fbev.c +++ b/src/ui/c/drv_fbev.c @@ -6,6 +6,15 @@ #include "lv_drivers/indev/evdev.h" #include "lvgl/lvgl.h" +#include +#include +#include +#if USE_BSD_EVDEV +#include +#else +#include +#endif + #define DISP_BUF_SIZE (NM_DISP_HOR * NM_DISP_VER / 10) /* returns NULL on error */ @@ -56,3 +65,42 @@ int nm_indev_init(void) return 0; } + +int nm_open_evdev_nonblock(void) +{ + // see lib/lv_drivers/indev/evdev.c +#if USE_BSD_EVDEV + int fd = open(EVDEV_NAME, O_RDWR | O_NOCTTY); +#else + int fd = open(EVDEV_NAME, O_RDWR | O_NOCTTY | O_NDELAY); +#endif + if (fd == -1) { + return -1; + } +#if USE_BSD_EVDEV + fcntl(fd, F_SETFL, O_NONBLOCK); +#else + fcntl(fd, F_SETFL, O_ASYNC | O_NONBLOCK); +#endif + return fd; +} + +void nm_close_evdev(int fd) +{ + if (fd != -1) { + close(fd); + } +} + +bool nm_consume_input_events(int fd) +{ + if (fd == -1) { + return false; + } + struct input_event in; + int count = 0; + while (read(fd, &in, sizeof(struct input_event)) > 0) { + count++; + } + return count > 0; +} diff --git a/src/ui/c/ui.c b/src/ui/c/ui.c index 9de7981..e0d8599 100644 --- a/src/ui/c/ui.c +++ b/src/ui/c/ui.c @@ -14,6 +14,18 @@ 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 */ +lv_obj_t *nm_make_topdrop() +{ + lv_obj_t *topdrop = lv_obj_create(lv_layer_top()); + if (!topdrop) { + return NULL; + } + lv_obj_set_style_bg_color(topdrop, lv_color_black(), 0); + lv_obj_clear_flag(topdrop, LV_OBJ_FLAG_IGNORE_LAYOUT); + lv_obj_set_size(topdrop, LV_PCT(100), LV_PCT(100)); + return topdrop; +} + static void textarea_event_cb(lv_event_t *e) { lv_obj_t *textarea = lv_event_get_target(e); diff --git a/src/ui/drv.zig b/src/ui/drv.zig index ef4f342..7770398 100644 --- a/src/ui/drv.zig +++ b/src/ui/drv.zig @@ -26,3 +26,48 @@ pub fn initInput() !void { return error.InputInitFailed; } } + +/// deactivate and remove all input devices. +pub fn deinitInput() void { + var indev = lvgl.lv_indev_get_next(null); + var count: usize = 0; + while (indev) |d| { + lvgl.lv_indev_delete(d); + count += 1; + indev = lvgl.lv_indev_get_next(null); + } + logger.debug("deinited {d} indev(s)", .{count}); +} + +pub usingnamespace switch (buildopts.driver) { + .sdl2 => struct { + pub fn InputWatcher() !type { + return error.InputWatcherUnavailable; + } + }, + .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; + + pub fn InputWatcher() !EvdevWatcher { + const fd = nm_open_evdev_nonblock(); + if (fd == -1) { + return error.InputWatcherUnavailable; + } + return .{ .evdev_fd = fd }; + } + + pub const EvdevWatcher = struct { + evdev_fd: std.os.fd_t, + + pub fn consume(self: @This()) bool { + return nm_consume_input_events(self.evdev_fd); + } + + pub fn close(self: @This()) void { + nm_close_evdev(self.evdev_fd); + } + }; + }, +}; diff --git a/src/ui/screen.zig b/src/ui/screen.zig new file mode 100644 index 0000000..7004ade --- /dev/null +++ b/src/ui/screen.zig @@ -0,0 +1,43 @@ +///! display and touch screen helper functions. +const std = @import("std"); +const Thread = std.Thread; + +const lvgl = @import("lvgl.zig"); +const drv = @import("drv.zig"); +const ui = @import("ui.zig"); + +const logger = std.log.scoped(.screen); + +/// cover the whole screen in black (top layer) and block until either +/// 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 { + drv.deinitInput(); + ui.topdrop(.show); + defer { + drv.initInput() catch |err| logger.err("drv.initInput: {any}", .{err}); + ui.topdrop(.remove); + } + + const watcher = drv.InputWatcher() catch |err| { + logger.err("drv.InputWatcher: {any}", .{err}); + return; + }; + defer watcher.close(); + while (!wake.isSet()) { + if (watcher.consume()) { + return; + } + std.atomic.spinLoopHint(); + } +} + +/// turn on or off display backlight. +pub fn backlight(onoff: enum { on, off }) !void { + const blpath = "/sys/class/backlight/rpi_backlight/bl_power"; + const f = try std.fs.openFileAbsolute(blpath, .{ .mode = .write_only }); + defer f.close(); + const v = if (onoff == .on) "0" else "1"; + _ = try f.write(v); +} diff --git a/src/ui/ui.zig b/src/ui/ui.zig index 343610b..75f8938 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -6,6 +6,8 @@ const drv = @import("drv.zig"); const logger = std.log.scoped(.ui); extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int; +extern "c" fn nm_make_topdrop() ?*lvgl.LvObj; +extern "c" fn nm_remove_topdrop() void; pub fn init() !void { lvgl.init(); @@ -20,3 +22,27 @@ pub fn init() !void { return error.UiInitFailure; } } + +/// unsafe for concurrent use. +pub fn topdrop(onoff: enum { show, remove }) void { + // a static construct: there can be only one global topdrop. + // see https://ziglang.org/documentation/master/#Static-Local-Variables + const S = struct { + var lv_obj: ?*lvgl.LvObj = null; + }; + switch (onoff) { + .show => { + if (S.lv_obj != null) { + return; + } + S.lv_obj = nm_make_topdrop(); + lvgl.lv_refr_now(null); + }, + .remove => { + if (S.lv_obj) |v| { + lvgl.lv_obj_del(v); + S.lv_obj = null; + } + }, + } +}