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"
|
(defpoll disks :interval "600s"
|
||||||
"~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh")
|
"~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh")
|
||||||
|
|
||||||
(defpoll caffeine-active :interval "2s"
|
; caffeine-manual poll — true only when a MANUAL caffeine toggle holds the shared
|
||||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh")
|
; 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
|
; presence-active poll — true when the webcam presence-detect daemon currently
|
||||||
; caffeine toggle) is holding the idle lock, i.e. the camera currently sees you.
|
; 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"
|
(defpoll presence-active :interval "2s"
|
||||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/presence-status.sh")
|
"~/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 []
|
(defwidget caffeine []
|
||||||
(button :class {presence-active == "true" ? "music caffeine-presence" : "music"}
|
(button :class {presence-active == "true" ? "music caffeine-presence" : "music"}
|
||||||
:onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh"
|
:onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh"
|
||||||
:tooltip {presence-active == "true" ? "Caffeine: ON (presence)"
|
:tooltip {caffeine-manual == "true"
|
||||||
: caffeine-active == "true" ? "Caffeine: ON" : "Caffeine: OFF"}
|
? (presence-active == "true" ? "Caffeine: ON (manual + presence)" : "Caffeine: ON (manual)")
|
||||||
{caffeine-active == "true" ? "☕" : ""}))
|
: (presence-active == "true" ? "Awake: presence detected" : "Idle: no inhibit")}
|
||||||
|
{caffeine-manual == "true" ? "☕" : presence-active == "true" ? "" : ""}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,18 +161,29 @@
|
||||||
(defpoll calender :interval "600s"
|
(defpoll calender :interval "600s"
|
||||||
"~/Dotfiles/desktopenvs/hyprland/scripts/calender-fix.sh")
|
"~/Dotfiles/desktopenvs/hyprland/scripts/calender-fix.sh")
|
||||||
|
|
||||||
(defpoll caffeine-active :interval "2s"
|
; caffeine-manual poll — true only when a MANUAL caffeine toggle holds the shared
|
||||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh")
|
; 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
|
; presence-active poll — true when the webcam presence-detect daemon currently
|
||||||
; caffeine toggle) is holding the idle lock, i.e. the camera currently sees you.
|
; 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"
|
(defpoll presence-active :interval "2s"
|
||||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/presence-status.sh")
|
"~/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 []
|
(defwidget caffeine []
|
||||||
(button :class {presence-active == "true" ? "music caffeine-presence" : "music"}
|
(button :class {presence-active == "true" ? "music caffeine-presence" : "music"}
|
||||||
:onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh"
|
:onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh"
|
||||||
:tooltip {presence-active == "true" ? "Caffeine: ON (presence)"
|
:tooltip {caffeine-manual == "true"
|
||||||
: caffeine-active == "true" ? "Caffeine: ON" : "Caffeine: OFF"}
|
? (presence-active == "true" ? "Caffeine: ON (manual + presence)" : "Caffeine: ON (manual)")
|
||||||
{caffeine-active == "true" ? "☕" : ""}))
|
: (presence-active == "true" ? "Awake: presence detected" : "Idle: no inhibit")}
|
||||||
|
{caffeine-manual == "true" ? "☕" : presence-active == "true" ? "" : ""}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -201,25 +201,28 @@
|
||||||
(defpoll disks :interval "600s"
|
(defpoll disks :interval "600s"
|
||||||
"~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh")
|
"~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh")
|
||||||
|
|
||||||
; caffeine-active poll — checks whether the caffeine systemd-inhibit lock is active.
|
; caffeine-manual poll — true only when a MANUAL caffeine toggle holds the shared
|
||||||
; Returns "true" or "false" string every 2 seconds. Used to toggle the icon.
|
; idle lock (i.e. the lock is held but the presence daemon does not own it).
|
||||||
(defpoll caffeine-active :interval "2s"
|
(defpoll caffeine-manual :interval "2s"
|
||||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh")
|
"~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-manual-status.sh")
|
||||||
|
|
||||||
; presence-active poll — true only when the presence-detect daemon (not a manual
|
; presence-active poll — true when the webcam presence-detect daemon currently
|
||||||
; caffeine toggle) is holding the idle lock, i.e. the camera currently sees you.
|
; sees you, independent of the idle lock. A distinct input from a manual caffeine
|
||||||
; Used to tint the caffeine widget in the Active Hot Pink accent.
|
; toggle, so the two can be true at the same time.
|
||||||
(defpoll presence-active :interval "2s"
|
(defpoll presence-active :interval "2s"
|
||||||
"~/Dotfiles/desktopenvs/hyprlua/scripts/presence-status.sh")
|
"~/Dotfiles/desktopenvs/hyprlua/scripts/presence-status.sh")
|
||||||
|
|
||||||
; caffeine widget — a button that toggles the caffeine idle-inhibit lock.
|
; caffeine widget — two inputs (manual inhibit, presence detected) → four states:
|
||||||
; ☕ = caffeine is ON (screen won't sleep), = caffeine is OFF (normal idle).
|
; neither: sleep, purple
|
||||||
; When presence detection is the reason the lock is held, the widget also turns
|
; manual only: ☕ coffee, purple
|
||||||
; the Active Hot Pink accent (.caffeine-presence) so auto-awake is distinct from
|
; presence only: eye, pink (.caffeine-presence)
|
||||||
; a manual caffeine session. Tooltip shows the current state for clarity.
|
; 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 []
|
(defwidget caffeine []
|
||||||
(button :class {presence-active == "true" ? "music caffeine-presence" : "music"}
|
(button :class {presence-active == "true" ? "music caffeine-presence" : "music"}
|
||||||
:onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh"
|
:onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh"
|
||||||
:tooltip {presence-active == "true" ? "Caffeine: ON (presence)"
|
:tooltip {caffeine-manual == "true"
|
||||||
: caffeine-active == "true" ? "Caffeine: ON" : "Caffeine: OFF"}
|
? (presence-active == "true" ? "Caffeine: ON (manual + presence)" : "Caffeine: ON (manual)")
|
||||||
{caffeine-active == "true" ? "☕" : ""}))
|
: (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
|
# Marks that *this daemon* (not the manual caffeine toggle) currently holds
|
||||||
# the lock, so a "no motion" tick never releases a manually-started session.
|
# the lock, so a "no motion" tick never releases a manually-started session.
|
||||||
OWNED_FLAG="/tmp/presence-inhibit-owned"
|
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"
|
PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf"
|
||||||
|
|
||||||
# Must match the video_nr in etc-v4l2loopback/modprobe.d/v4l2loopback.conf.
|
# Must match the video_nr in etc-v4l2loopback/modprobe.d/v4l2loopback.conf.
|
||||||
LOOPBACK_ID=42
|
LOOPBACK_ID=42
|
||||||
FEEDER_PID_FILE="/tmp/presence-loopback-feeder.pid"
|
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_IDLE=20 # seconds between checks under normal load
|
||||||
INTERVAL_BUSY=120 # seconds between checks when CPU or RAM is >= LOAD_THRESHOLD
|
INTERVAL_BUSY=120 # seconds between checks when CPU or RAM is >= LOAD_BUSY
|
||||||
LOAD_THRESHOLD=0.70
|
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)"
|
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.
|
# Resolve camera ID: env var takes highest priority, then config file, then default 0.
|
||||||
_camera_id() {
|
_camera_id() {
|
||||||
if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then
|
if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then
|
||||||
|
|
@ -52,23 +65,27 @@ _camera_id() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# True if 1-minute load average (normalized by core count) or RAM usage is
|
# Prints current CPU-or-RAM usage as a fraction (0..1): the higher of the
|
||||||
# at/above LOAD_THRESHOLD. Load average is a cheap, sampling-free proxy for
|
# core-count-normalized 1-minute load average and the used-RAM fraction. Load
|
||||||
# "CPU busy" — avoids adding an extra measurement delay to every tick.
|
# average is a cheap, sampling-free proxy for "CPU busy" — no extra measurement
|
||||||
_system_busy() {
|
# delay per tick.
|
||||||
local load1 cpu_busy mem_frac mem_busy
|
_resource_usage() {
|
||||||
|
local load1 cpu_frac mem_frac
|
||||||
load1="$(awk '{print $1}' /proc/loadavg)"
|
load1="$(awk '{print $1}' /proc/loadavg)"
|
||||||
cpu_busy="$(awk -v l="$load1" -v n="$NPROC" -v t="$LOAD_THRESHOLD" \
|
cpu_frac="$(awk -v l="$load1" -v n="$NPROC" 'BEGIN{print l/n}')"
|
||||||
'BEGIN{print (l/n >= t) ? 1 : 0}')"
|
|
||||||
[[ "$cpu_busy" == "1" ]] && return 0
|
|
||||||
|
|
||||||
mem_frac="$(free | awk '/^Mem:/ {print $3/$2}')"
|
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}')"
|
awk -v c="$cpu_frac" -v m="$mem_frac" 'BEGIN{print (c>m)?c:m}'
|
||||||
[[ "$mem_busy" == "1" ]]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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() {
|
_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)
|
# Returns true if the loopback feeder (ffmpeg mirroring real cam -> loopback)
|
||||||
|
|
@ -145,13 +162,20 @@ _stop_inhibit() {
|
||||||
_cleanup() {
|
_cleanup() {
|
||||||
# On daemon stop (systemd unit stop, user logout, etc.), release the idle
|
# 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.
|
# 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_inhibit
|
||||||
_stop_feeder
|
_stop_feeder
|
||||||
|
rm -f "$PRESENCE_FLAG"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
# Intercept termination signals to ensure the inhibitor PID is never orphaned.
|
# Intercept termination signals to ensure the inhibitor PID is never orphaned.
|
||||||
trap _cleanup SIGTERM SIGINT SIGHUP
|
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
|
while true; do
|
||||||
# Self-healing: retry the feeder every tick if it isn't up (camera
|
# Self-healing: retry the feeder every tick if it isn't up (camera
|
||||||
# unplugged/replugged, ffmpeg crashed, module loaded after daemon start).
|
# 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.
|
# Run the OpenCV motion detector; stderr suppressed to keep the journal clean.
|
||||||
python3 "$PYTHON_DETECT" "$DEVICE" 2>/dev/null
|
python3 "$PYTHON_DETECT" "$DEVICE" 2>/dev/null
|
||||||
rc=$?
|
rc=$?
|
||||||
|
now="$(date +%s)"
|
||||||
case $rc in
|
case $rc in
|
||||||
0) _start_inhibit ;;
|
# Motion: refresh the grace clock and mark presence. PRESENCE_FLAG is
|
||||||
1) _stop_inhibit ;;
|
# 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
|
# rc=2 = camera busy/unavailable — silently skip, state unchanged
|
||||||
esac
|
esac
|
||||||
sleep "$(_next_interval)"
|
sleep "$(_next_interval)"
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Reports whether the *presence-detect daemon* is currently holding the idle
|
# Reports the PRESENCE input for the Eww caffeine widget: whether the webcam
|
||||||
# inhibitor (i.e. the camera saw motion and is auto-keeping the screen awake),
|
# presence-detect daemon currently sees the user (motion on its last tick),
|
||||||
# as opposed to a manual caffeine toggle.
|
# 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
|
# presence-detect.sh writes PRESENCE_FLAG on every "motion" tick and removes it
|
||||||
# sources apart. OWNED_FLAG is touched only by presence-detect.sh when it starts
|
# on every "no motion" tick, so this is a clean, standalone signal — true even
|
||||||
# the inhibitor and cleared by both a manual caffeine toggle and the daemon's
|
# during a manual caffeine session (where the daemon never starts its own
|
||||||
# own release — so "PID alive AND OWNED_FLAG present" uniquely means the daemon
|
# inhibitor). For the MANUAL-inhibit input, see caffeine-manual-status.sh.
|
||||||
# owns the lock right now.
|
|
||||||
#
|
#
|
||||||
# Prints "true" when presence-detect owns the lock, else "false". Used by the
|
# Prints "true" when presence is detected, else "false".
|
||||||
# Eww caffeine widget to highlight itself in the Active Hot Pink accent.
|
PRESENCE_FLAG="/tmp/presence-detected"
|
||||||
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
|
if [[ -f "$PRESENCE_FLAG" ]]; then
|
||||||
echo "true"
|
echo "true"
|
||||||
else
|
else
|
||||||
echo "false"
|
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]
|
Usage: presence_detect.py [camera_id]
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
FRAMES_TO_CHECK = 6
|
FRAMES_TO_CHECK = 8
|
||||||
DIFFS_NEEDED = 2 # require motion in at least N consecutive-frame diffs
|
DIFFS_NEEDED = 2 # require motion in at least N consecutive-frame diffs
|
||||||
PIXEL_DELTA_THRESHOLD = 25 # per-pixel grayscale delta to count as "changed"
|
PIXEL_DELTA_THRESHOLD = 18 # per-pixel grayscale delta to count as "changed"
|
||||||
MOTION_AREA_RATIO = 0.02 # fraction of pixels that must change to call it motion
|
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
|
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:
|
def detect(camera_id: int) -> int:
|
||||||
|
|
@ -23,7 +31,9 @@ def detect(camera_id: int) -> int:
|
||||||
motion_diffs = 0
|
motion_diffs = 0
|
||||||
prev_gray = None
|
prev_gray = None
|
||||||
try:
|
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()
|
ok, frame = cap.read()
|
||||||
if not ok:
|
if not ok:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue