From fd49235f9e440b9fcc839e37cea1fff7b45d2957 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 24 Feb 2023 17:51:39 +0100 Subject: [PATCH] ui: implement a screen timeout after a period of user inactivity passed 60 sec of no touch screen activity, daemon turns off backlight and gui places a black "topdrop", opposite of backdrop. simply touching the screen reactivates it immediately. there seem to be no way to turn screen power off, so backlight plus black topdrop is the next best. there's no user settings to change 60 sec timeout at the moment because it is unclear whether it's worth adding UI elements. can always do so later. the implementation also provides means to reactivate the screen in an event of an alert from the daemon in the future. closes https://git.qcode.ch/nakamochi/ndg/issues/3 --- src/comm.zig | 18 ++++++++++++---- src/nd.zig | 19 +++++++++++++++++ src/nd/Daemon.zig | 10 ++++++++- src/ngui.zig | 52 +++++++++++++++++++++++++++++++++++++++++++++ src/test.zig | 5 +++++ src/ui/c/drv_fbev.c | 48 +++++++++++++++++++++++++++++++++++++++++ src/ui/c/ui.c | 12 +++++++++++ src/ui/drv.zig | 45 +++++++++++++++++++++++++++++++++++++++ src/ui/screen.zig | 43 +++++++++++++++++++++++++++++++++++++ src/ui/ui.zig | 26 +++++++++++++++++++++++ 10 files changed, 273 insertions(+), 5 deletions(-) create mode 100644 src/ui/screen.zig 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; + } + }, + } +}