ui: implement a screen timeout after a period of user inactivity
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/tag/woodpecker Pipeline was successful Details

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 #3
pull/20/head v0.1.0
alex 2 years ago
parent 021c810dc7
commit fd49235f9e
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -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| {

@ -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);

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

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

@ -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");

@ -6,6 +6,15 @@
#include "lv_drivers/indev/evdev.h"
#include "lvgl/lvgl.h"
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#if USE_BSD_EVDEV
#include <dev/evdev/input.h>
#else
#include <linux/input.h>
#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;
}

@ -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);

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

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

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