feat(presence-detect): 4-state caffeine widget + steadier, faster detection
Rework the Eww caffeine widget into two independent inputs — manual inhibit and presence detected — yielding four states across all three bars: neither → sleep, purple manual only → ☕ coffee, purple presence only → eye, pink manual + presence → ☕ coffee, pink Colour tracks presence; the coffee cup shows only while manually inhibiting. To make the two inputs independent, the daemon now writes a standalone /tmp/presence-detected flag each tick (decoupled from lock ownership, so it's true even during a manual session). presence-status.sh reports that flag (presence input); new caffeine-manual-status.sh reports lock-held-and-not- presence-owned (manual input). Make detection less finnicky: space the detector's frame grabs (0.06s) so slow movement registers and loopback reads never diff a stale duplicate frame, and lower the thresholds (delta 25→18, area 2%→0.8%). Add a 90s grace window so brief stillness never flickers presence off; any motion resets the clock. Add a third poll tier — INTERVAL_FAST=2s when CPU-or-RAM usage is below 30% (LOAD_LIGHT) — alongside the existing 20s normal and 120s busy tiers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>main
parent
dc9cf6d830
commit
1fa5ebee70
|
|
@ -115,18 +115,29 @@
|
|||
(defpoll disks :interval "600s"
|
||||
"~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh")
|
||||
|
||||
(defpoll caffeine-active :interval "2s"
|
||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh")
|
||||
; caffeine-manual poll — true only when a MANUAL caffeine toggle holds the shared
|
||||
; idle lock (i.e. the lock is held but the presence daemon does not own it).
|
||||
(defpoll caffeine-manual :interval "2s"
|
||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-manual-status.sh")
|
||||
|
||||
; presence-active poll — true only when the presence-detect daemon (not a manual
|
||||
; caffeine toggle) is holding the idle lock, i.e. the camera currently sees you.
|
||||
; presence-active poll — true when the webcam presence-detect daemon currently
|
||||
; sees you, independent of the idle lock. A distinct input from a manual caffeine
|
||||
; toggle, so the two can be true at the same time.
|
||||
(defpoll presence-active :interval "2s"
|
||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/presence-status.sh")
|
||||
|
||||
; caffeine widget — two inputs (manual inhibit, presence detected) → four states:
|
||||
; neither: sleep, purple
|
||||
; manual only: ☕ coffee, purple
|
||||
; presence only: eye, pink (.caffeine-presence)
|
||||
; manual + presence: ☕ coffee, pink
|
||||
; Colour tracks presence (pink accent); the glyph is coffee whenever manually
|
||||
; inhibiting, else the eye when only presence holds it, else the sleep glyph.
|
||||
(defwidget caffeine []
|
||||
(button :class {presence-active == "true" ? "music caffeine-presence" : "music"}
|
||||
:onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh"
|
||||
:tooltip {presence-active == "true" ? "Caffeine: ON (presence)"
|
||||
: caffeine-active == "true" ? "Caffeine: ON" : "Caffeine: OFF"}
|
||||
{caffeine-active == "true" ? "☕" : ""}))
|
||||
:tooltip {caffeine-manual == "true"
|
||||
? (presence-active == "true" ? "Caffeine: ON (manual + presence)" : "Caffeine: ON (manual)")
|
||||
: (presence-active == "true" ? "Awake: presence detected" : "Idle: no inhibit")}
|
||||
{caffeine-manual == "true" ? "☕" : presence-active == "true" ? "" : ""}))
|
||||
|
||||
|
|
|
|||
|
|
@ -161,18 +161,29 @@
|
|||
(defpoll calender :interval "600s"
|
||||
"~/Dotfiles/desktopenvs/hyprland/scripts/calender-fix.sh")
|
||||
|
||||
(defpoll caffeine-active :interval "2s"
|
||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh")
|
||||
; caffeine-manual poll — true only when a MANUAL caffeine toggle holds the shared
|
||||
; idle lock (i.e. the lock is held but the presence daemon does not own it).
|
||||
(defpoll caffeine-manual :interval "2s"
|
||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-manual-status.sh")
|
||||
|
||||
; presence-active poll — true only when the presence-detect daemon (not a manual
|
||||
; caffeine toggle) is holding the idle lock, i.e. the camera currently sees you.
|
||||
; presence-active poll — true when the webcam presence-detect daemon currently
|
||||
; sees you, independent of the idle lock. A distinct input from a manual caffeine
|
||||
; toggle, so the two can be true at the same time.
|
||||
(defpoll presence-active :interval "2s"
|
||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/presence-status.sh")
|
||||
|
||||
; caffeine widget — two inputs (manual inhibit, presence detected) → four states:
|
||||
; neither: sleep, purple
|
||||
; manual only: ☕ coffee, purple
|
||||
; presence only: eye, pink (.caffeine-presence)
|
||||
; manual + presence: ☕ coffee, pink
|
||||
; Colour tracks presence (pink accent); the glyph is coffee whenever manually
|
||||
; inhibiting, else the eye when only presence holds it, else the sleep glyph.
|
||||
(defwidget caffeine []
|
||||
(button :class {presence-active == "true" ? "music caffeine-presence" : "music"}
|
||||
:onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh"
|
||||
:tooltip {presence-active == "true" ? "Caffeine: ON (presence)"
|
||||
: caffeine-active == "true" ? "Caffeine: ON" : "Caffeine: OFF"}
|
||||
{caffeine-active == "true" ? "☕" : ""}))
|
||||
:tooltip {caffeine-manual == "true"
|
||||
? (presence-active == "true" ? "Caffeine: ON (manual + presence)" : "Caffeine: ON (manual)")
|
||||
: (presence-active == "true" ? "Awake: presence detected" : "Idle: no inhibit")}
|
||||
{caffeine-manual == "true" ? "☕" : presence-active == "true" ? "" : ""}))
|
||||
|
||||
|
|
|
|||
|
|
@ -201,25 +201,28 @@
|
|||
(defpoll disks :interval "600s"
|
||||
"~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh")
|
||||
|
||||
; caffeine-active poll — checks whether the caffeine systemd-inhibit lock is active.
|
||||
; Returns "true" or "false" string every 2 seconds. Used to toggle the icon.
|
||||
(defpoll caffeine-active :interval "2s"
|
||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh")
|
||||
; caffeine-manual poll — true only when a MANUAL caffeine toggle holds the shared
|
||||
; idle lock (i.e. the lock is held but the presence daemon does not own it).
|
||||
(defpoll caffeine-manual :interval "2s"
|
||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-manual-status.sh")
|
||||
|
||||
; presence-active poll — true only when the presence-detect daemon (not a manual
|
||||
; caffeine toggle) is holding the idle lock, i.e. the camera currently sees you.
|
||||
; Used to tint the caffeine widget in the Active Hot Pink accent.
|
||||
; presence-active poll — true when the webcam presence-detect daemon currently
|
||||
; sees you, independent of the idle lock. A distinct input from a manual caffeine
|
||||
; toggle, so the two can be true at the same time.
|
||||
(defpoll presence-active :interval "2s"
|
||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/presence-status.sh")
|
||||
|
||||
; caffeine widget — a button that toggles the caffeine idle-inhibit lock.
|
||||
; ☕ = caffeine is ON (screen won't sleep), = caffeine is OFF (normal idle).
|
||||
; When presence detection is the reason the lock is held, the widget also turns
|
||||
; the Active Hot Pink accent (.caffeine-presence) so auto-awake is distinct from
|
||||
; a manual caffeine session. Tooltip shows the current state for clarity.
|
||||
; caffeine widget — two inputs (manual inhibit, presence detected) → four states:
|
||||
; neither: sleep, purple
|
||||
; manual only: ☕ coffee, purple
|
||||
; presence only: eye, pink (.caffeine-presence)
|
||||
; manual + presence: ☕ coffee, pink
|
||||
; Colour tracks presence (pink accent); the glyph is coffee whenever manually
|
||||
; inhibiting, else the eye when only presence holds it, else the sleep glyph.
|
||||
(defwidget caffeine []
|
||||
(button :class {presence-active == "true" ? "music caffeine-presence" : "music"}
|
||||
:onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh"
|
||||
:tooltip {presence-active == "true" ? "Caffeine: ON (presence)"
|
||||
: caffeine-active == "true" ? "Caffeine: ON" : "Caffeine: OFF"}
|
||||
{caffeine-active == "true" ? "☕" : ""}))
|
||||
:tooltip {caffeine-manual == "true"
|
||||
? (presence-active == "true" ? "Caffeine: ON (manual + presence)" : "Caffeine: ON (manual)")
|
||||
: (presence-active == "true" ? "Awake: presence detected" : "Idle: no inhibit")}
|
||||
{caffeine-manual == "true" ? "☕" : presence-active == "true" ? "" : ""}))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
# Reports the MANUAL-INHIBIT input for the Eww caffeine widget: whether a manual
|
||||
# caffeine toggle currently holds the shared idle lock.
|
||||
#
|
||||
# The lock is shared with presence-detect.sh, so "PID alive" alone can't tell the
|
||||
# two sources apart. OWNED_FLAG is present only while the daemon owns the lock, so
|
||||
# "PID alive AND OWNED_FLAG absent" uniquely means a manual caffeine session holds
|
||||
# it. (For the PRESENCE input — camera sees you — see presence-status.sh.)
|
||||
#
|
||||
# Prints "true" for a manual caffeine session, else "false".
|
||||
PID_FILE="/tmp/caffeine-inhibit.pid"
|
||||
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
||||
|
||||
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null && [[ ! -f "$OWNED_FLAG" ]]; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
|
|
@ -29,17 +29,30 @@ PID_FILE="/tmp/caffeine-inhibit.pid"
|
|||
# Marks that *this daemon* (not the manual caffeine toggle) currently holds
|
||||
# the lock, so a "no motion" tick never releases a manually-started session.
|
||||
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
||||
# Records whether the camera currently SEES the user (motion on the last tick),
|
||||
# INDEPENDENT of who holds the idle lock. Unlike OWNED_FLAG this is set even when
|
||||
# a manual caffeine session already owns the lock (so the daemon never starts its
|
||||
# own inhibitor). presence-status.sh reads this so the Eww widget can show the
|
||||
# presence input as a distinct signal from a manual caffeine toggle.
|
||||
PRESENCE_FLAG="/tmp/presence-detected"
|
||||
PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf"
|
||||
|
||||
# Must match the video_nr in etc-v4l2loopback/modprobe.d/v4l2loopback.conf.
|
||||
LOOPBACK_ID=42
|
||||
FEEDER_PID_FILE="/tmp/presence-loopback-feeder.pid"
|
||||
|
||||
INTERVAL_FAST=2 # near-idle system (< LOAD_LIGHT): poll fast for responsiveness
|
||||
INTERVAL_IDLE=20 # seconds between checks under normal load
|
||||
INTERVAL_BUSY=120 # seconds between checks when CPU or RAM is >= LOAD_THRESHOLD
|
||||
LOAD_THRESHOLD=0.70
|
||||
INTERVAL_BUSY=120 # seconds between checks when CPU or RAM is >= LOAD_BUSY
|
||||
LOAD_BUSY=0.70 # CPU-or-RAM fraction at/above which we back off to INTERVAL_BUSY
|
||||
LOAD_LIGHT=0.30 # CPU-or-RAM fraction below which we speed up to INTERVAL_FAST
|
||||
NPROC="$(nproc)"
|
||||
|
||||
# Grace window: keep reporting "present" for this long after the last detected
|
||||
# motion, so brief stillness (reading, thinking, a slow moment) never drops the
|
||||
# lock the instant one tick sees nothing. Any motion tick resets the clock.
|
||||
GRACE_SECONDS=90
|
||||
|
||||
# Resolve camera ID: env var takes highest priority, then config file, then default 0.
|
||||
_camera_id() {
|
||||
if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then
|
||||
|
|
@ -52,23 +65,27 @@ _camera_id() {
|
|||
fi
|
||||
}
|
||||
|
||||
# True if 1-minute load average (normalized by core count) or RAM usage is
|
||||
# at/above LOAD_THRESHOLD. Load average is a cheap, sampling-free proxy for
|
||||
# "CPU busy" — avoids adding an extra measurement delay to every tick.
|
||||
_system_busy() {
|
||||
local load1 cpu_busy mem_frac mem_busy
|
||||
# Prints current CPU-or-RAM usage as a fraction (0..1): the higher of the
|
||||
# core-count-normalized 1-minute load average and the used-RAM fraction. Load
|
||||
# average is a cheap, sampling-free proxy for "CPU busy" — no extra measurement
|
||||
# delay per tick.
|
||||
_resource_usage() {
|
||||
local load1 cpu_frac mem_frac
|
||||
load1="$(awk '{print $1}' /proc/loadavg)"
|
||||
cpu_busy="$(awk -v l="$load1" -v n="$NPROC" -v t="$LOAD_THRESHOLD" \
|
||||
'BEGIN{print (l/n >= t) ? 1 : 0}')"
|
||||
[[ "$cpu_busy" == "1" ]] && return 0
|
||||
|
||||
cpu_frac="$(awk -v l="$load1" -v n="$NPROC" 'BEGIN{print l/n}')"
|
||||
mem_frac="$(free | awk '/^Mem:/ {print $3/$2}')"
|
||||
mem_busy="$(awk -v m="$mem_frac" -v t="$LOAD_THRESHOLD" 'BEGIN{print (m >= t) ? 1 : 0}')"
|
||||
[[ "$mem_busy" == "1" ]]
|
||||
awk -v c="$cpu_frac" -v m="$mem_frac" 'BEGIN{print (c>m)?c:m}'
|
||||
}
|
||||
|
||||
# Three-tier poll interval based on current resource usage:
|
||||
# >= LOAD_BUSY -> INTERVAL_BUSY (heavy load: back off, stay out of the way)
|
||||
# < LOAD_LIGHT -> INTERVAL_FAST (near-idle: poll fast, cheap and responsive)
|
||||
# otherwise -> INTERVAL_IDLE (normal load)
|
||||
_next_interval() {
|
||||
if _system_busy; then echo "$INTERVAL_BUSY"; else echo "$INTERVAL_IDLE"; fi
|
||||
local u; u="$(_resource_usage)"
|
||||
awk -v u="$u" -v busy="$LOAD_BUSY" -v light="$LOAD_LIGHT" \
|
||||
-v fb="$INTERVAL_BUSY" -v ff="$INTERVAL_FAST" -v fi="$INTERVAL_IDLE" \
|
||||
'BEGIN{ if (u >= busy) print fb; else if (u < light) print ff; else print fi }'
|
||||
}
|
||||
|
||||
# Returns true if the loopback feeder (ffmpeg mirroring real cam -> loopback)
|
||||
|
|
@ -145,13 +162,20 @@ _stop_inhibit() {
|
|||
_cleanup() {
|
||||
# On daemon stop (systemd unit stop, user logout, etc.), release the idle
|
||||
# lock (only if we're the one holding it) and stop mirroring the camera.
|
||||
# Also clear the presence flag — with the daemon gone, nothing is watching.
|
||||
_stop_inhibit
|
||||
_stop_feeder
|
||||
rm -f "$PRESENCE_FLAG"
|
||||
exit 0
|
||||
}
|
||||
# Intercept termination signals to ensure the inhibitor PID is never orphaned.
|
||||
trap _cleanup SIGTERM SIGINT SIGHUP
|
||||
|
||||
# Epoch seconds of the last tick that saw motion; drives the GRACE_SECONDS
|
||||
# window below. 0 = never seen yet, so a no-motion tick at startup releases
|
||||
# immediately (nothing is held anyway).
|
||||
LAST_MOTION=0
|
||||
|
||||
while true; do
|
||||
# Self-healing: retry the feeder every tick if it isn't up (camera
|
||||
# unplugged/replugged, ffmpeg crashed, module loaded after daemon start).
|
||||
|
|
@ -160,9 +184,19 @@ while true; do
|
|||
# Run the OpenCV motion detector; stderr suppressed to keep the journal clean.
|
||||
python3 "$PYTHON_DETECT" "$DEVICE" 2>/dev/null
|
||||
rc=$?
|
||||
now="$(date +%s)"
|
||||
case $rc in
|
||||
0) _start_inhibit ;;
|
||||
1) _stop_inhibit ;;
|
||||
# Motion: refresh the grace clock and mark presence. PRESENCE_FLAG is
|
||||
# updated *independently* of the inhibit lock — it reflects "camera sees
|
||||
# you" even during a manual caffeine session (where _start_inhibit is a
|
||||
# no-op because the lock is already held).
|
||||
0) LAST_MOTION="$now"; touch "$PRESENCE_FLAG"; _start_inhibit ;;
|
||||
# No motion: only actually release once we've been still for the whole
|
||||
# grace window. Within it, leave the flag/lock exactly as they were so a
|
||||
# brief pause in movement doesn't flicker presence off.
|
||||
1) if (( now - LAST_MOTION >= GRACE_SECONDS )); then
|
||||
rm -f "$PRESENCE_FLAG"; _stop_inhibit
|
||||
fi ;;
|
||||
# rc=2 = camera busy/unavailable — silently skip, state unchanged
|
||||
esac
|
||||
sleep "$(_next_interval)"
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
#!/bin/bash
|
||||
# Reports whether the *presence-detect daemon* is currently holding the idle
|
||||
# inhibitor (i.e. the camera saw motion and is auto-keeping the screen awake),
|
||||
# as opposed to a manual caffeine toggle.
|
||||
# Reports the PRESENCE input for the Eww caffeine widget: whether the webcam
|
||||
# presence-detect daemon currently sees the user (motion on its last tick),
|
||||
# INDEPENDENT of whether the idle lock is held or who holds it.
|
||||
#
|
||||
# The lock is shared with caffeine.sh, so PID_FILE alone can't tell the two
|
||||
# sources apart. OWNED_FLAG is touched only by presence-detect.sh when it starts
|
||||
# the inhibitor and cleared by both a manual caffeine toggle and the daemon's
|
||||
# own release — so "PID alive AND OWNED_FLAG present" uniquely means the daemon
|
||||
# owns the lock right now.
|
||||
# presence-detect.sh writes PRESENCE_FLAG on every "motion" tick and removes it
|
||||
# on every "no motion" tick, so this is a clean, standalone signal — true even
|
||||
# during a manual caffeine session (where the daemon never starts its own
|
||||
# inhibitor). For the MANUAL-inhibit input, see caffeine-manual-status.sh.
|
||||
#
|
||||
# Prints "true" when presence-detect owns the lock, else "false". Used by the
|
||||
# Eww caffeine widget to highlight itself in the Active Hot Pink accent.
|
||||
PID_FILE="/tmp/caffeine-inhibit.pid"
|
||||
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
||||
# Prints "true" when presence is detected, else "false".
|
||||
PRESENCE_FLAG="/tmp/presence-detected"
|
||||
|
||||
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null && [[ -f "$OWNED_FLAG" ]]; then
|
||||
if [[ -f "$PRESENCE_FLAG" ]]; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
|
|
|
|||
|
|
@ -5,14 +5,22 @@ Exit codes: 0 = motion detected, 1 = no motion, 2 = camera error (busy/unavailab
|
|||
Usage: presence_detect.py [camera_id]
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
FRAMES_TO_CHECK = 6
|
||||
FRAMES_TO_CHECK = 8
|
||||
DIFFS_NEEDED = 2 # require motion in at least N consecutive-frame diffs
|
||||
PIXEL_DELTA_THRESHOLD = 25 # per-pixel grayscale delta to count as "changed"
|
||||
MOTION_AREA_RATIO = 0.02 # fraction of pixels that must change to call it motion
|
||||
PIXEL_DELTA_THRESHOLD = 18 # per-pixel grayscale delta to count as "changed"
|
||||
MOTION_AREA_RATIO = 0.008 # fraction of pixels that must change to call it motion
|
||||
BLUR_KSIZE = (21, 21) # Gaussian blur kernel to suppress sensor noise
|
||||
# Spacing between the frames we diff. Without it the frames are grabbed
|
||||
# back-to-back within a few milliseconds, so (a) slow, small movements barely
|
||||
# differ between adjacent frames and (b) reading off the ffmpeg v4l2loopback
|
||||
# mirror can hand back the SAME buffered frame twice → a zero delta even while
|
||||
# you're moving. Spacing the grabs out gives slow motion a real temporal
|
||||
# baseline and guarantees each compared frame is a fresh one.
|
||||
INTER_FRAME_DELAY = 0.06 # seconds between grabs (~0.5s total observation window)
|
||||
|
||||
|
||||
def detect(camera_id: int) -> int:
|
||||
|
|
@ -23,7 +31,9 @@ def detect(camera_id: int) -> int:
|
|||
motion_diffs = 0
|
||||
prev_gray = None
|
||||
try:
|
||||
for _ in range(FRAMES_TO_CHECK):
|
||||
for i in range(FRAMES_TO_CHECK):
|
||||
if i > 0:
|
||||
time.sleep(INTER_FRAME_DELAY)
|
||||
ok, frame = cap.read()
|
||||
if not ok:
|
||||
continue
|
||||
|
|
|
|||
Loading…
Reference in New Issue