diff --git a/daemon/src/config.rs b/daemon/src/config.rs index e6effbba..58fe839b 100644 --- a/daemon/src/config.rs +++ b/daemon/src/config.rs @@ -26,6 +26,8 @@ pub struct Config { pub colorblind_mode: bool, /// Key input mode pub key_input_mode: u8, + /// Keep the device screen awake (device-specific; currently Orbic) + pub keep_screen_on: bool, /// ntfy.sh URL pub ntfy_url: Option, /// Vector containing the types of enabled notifications @@ -46,6 +48,7 @@ impl Default for Config { ui_level: 1, colorblind_mode: false, key_input_mode: 0, + keep_screen_on: false, analyzers: AnalyzerConfig::default(), ntfy_url: None, enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery], diff --git a/daemon/src/display/orbic.rs b/daemon/src/display/orbic.rs index 215938ba..a16e5640 100644 --- a/daemon/src/display/orbic.rs +++ b/daemon/src/display/orbic.rs @@ -2,6 +2,7 @@ use crate::config; use crate::display::DisplayState; use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer}; use async_trait::async_trait; +use log::{debug, warn}; use tokio::sync::mpsc::Receiver; use tokio_util::sync::CancellationToken; @@ -9,6 +10,123 @@ use tokio_util::task::TaskTracker; const FB_PATH: &str = "/dev/fb0"; +// Orbic display sysfs controls discovered on device. +// These are used to re-enable the display after the stock UI sleeps it. +const SYSFS_BASE: &str = "/sys/devices/78b6000.spi/spi_master/spi1/spi1.0"; +const SYSFS_SLEEP_MODE: &str = "/sys/devices/78b6000.spi/spi_master/spi1/spi1.0/sleep_mode"; +const SYSFS_BL_GPIO: &str = "/sys/devices/78b6000.spi/spi_master/spi1/spi1.0/bl_gpio"; +const SYSFS_DISPLAY_ON: &str = "/sys/devices/78b6000.spi/spi_master/spi1/spi1.0/display_on"; + +// Global autosleep control: +// "mem" => auto-suspend enabled +// "off" => auto-suspend disabled +// On battery, autosleep="mem" caused rapid suspend/resume cycles that disrupted display updates, +// making the overlay blink. For keep_screen_on we force autosleep="off". +const SYSFS_AUTOSLEEP: &str = "/sys/power/autosleep"; + +async fn read_sysfs_bool(path: &str) -> Option { + match tokio::fs::read_to_string(path).await { + Ok(s) => match s.trim() { + "0" => Some(false), + "1" => Some(true), + _ => None, + }, + Err(_) => None, + } +} + +async fn read_sysfs_string(path: &str) -> Option { + tokio::fs::read_to_string(path) + .await + .ok() + .map(|s| s.trim().to_string()) +} + +async fn write_sysfs_value(path: &str, value: &[u8]) { + if let Err(e) = tokio::fs::write(path, value).await { + warn!( + "failed writing '{:?}' to {path}: {e}", + std::str::from_utf8(value).unwrap_or("") + ); + } +} + +// +async fn write_sysfs_one(path: &str) { + write_sysfs_value(path, b"1").await; +} + +fn spawn_keep_screen_on(task_tracker: &TaskTracker, shutdown_token: CancellationToken) { + task_tracker.spawn(async move { + // If the expected sysfs does not exist, do nothing. + if tokio::fs::metadata(SYSFS_BASE).await.is_err() { + warn!("keep_screen_on enabled, but Orbic sysfs path not found: {SYSFS_BASE}"); + return; + } + + let autosleep_available = tokio::fs::metadata(SYSFS_AUTOSLEEP).await.is_ok(); + if !autosleep_available { + warn!("keep_screen_on enabled, but autosleep sysfs not found: {SYSFS_AUTOSLEEP}"); + } + + // Poll frequency to catch sleeping. + const POLL_MS: u64 = 500; + + loop { + if shutdown_token.is_cancelled() { + break; + } + + // On Orbic sleep_mode=0 and bl_gpio=0 indicates the display is sleep. + let sleep_mode = read_sysfs_bool(SYSFS_SLEEP_MODE).await; + let bl_gpio = read_sysfs_bool(SYSFS_BL_GPIO).await; + + let should_wake = matches!(sleep_mode, Some(false)) || matches!(bl_gpio, Some(false)); + + // While keep_screen_on is on, autosleep is off. + // Write when autosleep="mem". + let autosleep = if autosleep_available { + read_sysfs_string(SYSFS_AUTOSLEEP).await + } else { + None + }; + let autosleep_needs_off = matches!(autosleep.as_deref(), Some("mem")); + + if should_wake || autosleep_needs_off { + debug!( + "keep_screen_on: waking display (sleep_mode={:?}, bl_gpio={:?}, autosleep={:?})", + sleep_mode, bl_gpio, autosleep + ); + + if autosleep_available && autosleep_needs_off { + write_sysfs_value(SYSFS_AUTOSLEEP, b"off").await; + } + + if should_wake { + // Observed wake sequence + // 1) display_on=1 (this has not been observed to change but we set it anyway) + // 2) bl_gpio=1 (backlight) + // 3) sleep_mode=1 (resume UI) + write_sysfs_one(SYSFS_DISPLAY_ON).await; + write_sysfs_one(SYSFS_BL_GPIO).await; + write_sysfs_one(SYSFS_SLEEP_MODE).await; + } + } + + tokio::time::sleep(std::time::Duration::from_millis(POLL_MS)).await; + } + + // keep_screen_on disabled we restore autosleep back to "mem" + if autosleep_available { + let autosleep = read_sysfs_string(SYSFS_AUTOSLEEP).await; + if matches!(autosleep.as_deref(), Some("off")) { + debug!("keep_screen_on: restoring autosleep=\"mem\" (was {:?})", autosleep); + write_sysfs_value(SYSFS_AUTOSLEEP, b"mem").await; + } + } + }); +} + #[derive(Copy, Clone, Default)] struct Framebuffer; @@ -41,6 +159,10 @@ pub fn update_ui( shutdown_token: CancellationToken, ui_update_rx: Receiver, ) { + if config.keep_screen_on { + spawn_keep_screen_on(task_tracker, shutdown_token.clone()); + } + generic_framebuffer::update_ui( task_tracker, config, diff --git a/daemon/web/src/lib/components/ConfigForm.svelte b/daemon/web/src/lib/components/ConfigForm.svelte index 6f100146..98df2d94 100644 --- a/daemon/web/src/lib/components/ConfigForm.svelte +++ b/daemon/web/src/lib/components/ConfigForm.svelte @@ -148,6 +148,21 @@ +
+ + +
+

+ Prevents the Orbic display from blanking by re-enabling backlight/display when it sleeps. +

+

Notification Settings

diff --git a/daemon/web/src/lib/utils.svelte.ts b/daemon/web/src/lib/utils.svelte.ts index d8b24066..4bae1ab2 100644 --- a/daemon/web/src/lib/utils.svelte.ts +++ b/daemon/web/src/lib/utils.svelte.ts @@ -22,6 +22,7 @@ export interface Config { ui_level: number; colorblind_mode: boolean; key_input_mode: number; + keep_screen_on: boolean; ntfy_url: string; enabled_notifications: enabled_notifications[]; analyzers: AnalyzerConfig; diff --git a/dist/config.toml.in b/dist/config.toml.in index fb64a329..11f9627f 100644 --- a/dist/config.toml.in +++ b/dist/config.toml.in @@ -19,6 +19,9 @@ colorblind_mode = false # 1..3 = show emoji for status. :) for running, ! for warnings, no mouth for paused. ui_level = 1 +# (Orbic) Keep the display awake by re-enabling backlight/display when the stock UI blanks it. +keep_screen_on = false + # 0 = rayhunter does not read button presses # 1 = double-tapping the power button starts/stops recordings key_input_mode = 0 diff --git a/doc/configuration.md b/doc/configuration.md index aebd65f9..eae88630 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -15,6 +15,7 @@ Through web UI you can set: - *Disable button control*: built-in power button of the device is not used by Rayhunter. - *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristics is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button. - **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness. +- **Keep Screen On** (Orbic only) Keeps the display awake by re-enabling backlight/display when the stock UI blanks it. Allows monitoring Rayhunter at a glance. - **ntfy URL**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/). - **Enabled Notification Types** allows enabling or disabling the following types of notifications: - *Warnings*, which will alert when a heuristic is triggered. Alerts will be sent at most once every five minutes.