Skip to content

Instantly share code, notes, and snippets.

@ajmeese7
Created November 23, 2025 16:12
Show Gist options
  • Select an option

  • Save ajmeese7/cc0854cc522a328d470319c75b783435 to your computer and use it in GitHub Desktop.

Select an option

Save ajmeese7/cc0854cc522a328d470319c75b783435 to your computer and use it in GitHub Desktop.
Camera motion detection to enable Linux monitor (Raspberry Pi)

Presence‑Aware Wall Panel on Raspberry Pi (X11, Bookworm)

This Gist documents a complete, reproducible setup for a Raspberry Pi–driven wall touchscreen that:

  • Turns the monitor off after N minutes of no activity.

  • Turns the monitor on when:

    • The camera detects motion nearby, or
    • The user touches/moves the mouse/keyboard.
  • Re‑applies touchscreen rotation/calibration every time the display wakes.

Assumptions:

  • Raspberry Pi OS Bookworm.
  • Running an X11 session (Openbox/LXDE or similar).
  • A USB webcam (or any V4L2 camera) visible as /dev/video0.
  • Monitor supports DPMS standby (most do).

0) Packages you need

sudo apt update
sudo apt install motion x11-xserver-utils xprintidle v4l-utils
  • motion: detects movement using the camera.
  • x11-xserver-utils: provides xset for DPMS control.
  • xprintidle: reads the X11 idle timer (touch/KB/mouse).
  • v4l-utils: camera debugging tools.

1) Confirm X11 + DPMS control works

1.1 Verify you are on X11

echo $XDG_SESSION_TYPE
# should print: x11

1.2 Enable Screen Blanking (so DPMS extension exists)

On Bookworm/X11, DPMS may be disabled until Screen Blanking is enabled.

sudo raspi-config
# Display Options -> Screen Blanking -> Enable

Reboot once after changing this.

1.3 Test DPMS force commands

Run these in your desktop session:

export DISPLAY=:0
xset dpms force off
sleep 2
xset dpms force on

If force works, you’re good.


2) Disable OS idle blanking timers (optional but recommended)

We want our daemon to decide when to sleep, not the desktop’s own timers.

Run once (and later add to autostart if desired):

export DISPLAY=:0
xset s off
xset s noblank
xset -dpms
xset dpms 0 0 0

Notes:

  • This disables timeouts, but DPMS can still be forced on/off by the daemon.

3) Camera sanity check

Confirm your camera is detected:

v4l2-ctl --list-devices

You should see something like:

  • /dev/video0

If your camera appears under a different node, update Motion’s video_device accordingly.


4) Create a Motion hook for presence timestamps

4.1 Hook script

This script writes a fresh epoch timestamp every time Motion sees movement.

sudo tee /usr/local/bin/presence_event.sh >/dev/null <<'EOF'
#!/bin/bash
# Called by motion when movement is detected.
# Write a timestamp to a tmp file read by the presence daemon.
echo "motion $(date +%s)" > /tmp/presence_event
EOF

sudo chmod +x /usr/local/bin/presence_event.sh

4.2 Motion config

Overwrite /etc/motion/motion.conf with the file in the companion Gist motion.conf (see files list).

Key requirements inside the config:

  • daemon off (so systemd keeps it running)
  • video_device /dev/video0
  • on_motion_detected /usr/local/bin/presence_event.sh
  • outputs disabled (no photos/movies)

Restart Motion:

sudo systemctl enable motion
sudo systemctl restart motion
sudo systemctl status motion --no-pager -l

Expect:

  • Active: active (running)

4.3 Verify the hook fires

Move in front of the camera, then:

cat /tmp/presence_event

Example:

motion 1763827202

If this file doesn’t update while you move, Motion sensitivity needs tuning (section 7).


5) Touchscreen rotation script

If you already have a working rotation/calibration script, keep using it. This setup assumes it is located at:

~/auto-rotate.sh

Your daemon will re‑run this script on every wake.

If your script name/path differs, update the ROTATE_SCRIPT variable in presence_panel.py.


6) Presence daemon (DPMS + camera + X idle)

Place the Python daemon from the companion file presence_panel.py into:

~/presence_panel.py
chmod +x ~/presence_panel.py

Manual test:

python3 ~/presence_panel.py

Behavior:

  • If the monitor is off and motion is detected, DPMS wakes it.
  • If someone taps/moves input, xprintidle resets the timer even if the camera misses it.
  • After wake, ~/auto-rotate.sh runs so touch calibration matches rotation.
  • Screen turns off after OFF_TIMEOUT seconds of no motion/input.

7) Fine‑tuning Motion sensitivity

Most false positives/negatives are fixed by adjusting a small set of Motion parameters.

Edit:

sudo nano /etc/motion/motion.conf
sudo systemctl restart motion

7.1 Most important settings

Setting What it does How to tune
threshold How much change counts as motion Increase to reduce false positives; decrease if it misses people. Start around 1500.
noise_level Filters low‑level pixel noise Increase if flickering light triggers motion.
event_gap Seconds without motion before an event ends Increase if you want fewer start/stop events. Doesn’t affect wake timing much if you use on_motion_detected.
width/height Detection resolution Lower = less CPU and less sensitivity to small changes. 640×480 is a good start.
framerate Detection FPS Lower = less CPU. 5 FPS is usually enough for presence.

7.2 Practical tuning workflow

  1. Watch logs live:

    sudo journalctl -u motion -f
  2. Walk in/out, note triggers.

  3. Change one parameter.

  4. Restart Motion.

  5. Repeat.

7.3 Common problems

  • Triggers from TV/windows: raise threshold, raise noise_level, or physically aim the camera away.
  • Misses slow movement: lower threshold, increase framerate slightly.
  • Too much CPU: lower width/height, lower framerate, disable streaming (already disabled here).

8) Autostart the presence daemon (user systemd)

Create the service:

mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/presence-panel.service

Paste:

[Unit]
Description=Presence-based panel control (X11)

[Service]
ExecStart=/usr/bin/python3 %h/presence_panel.py
Restart=on-failure

[Install]
WantedBy=default.target

Enable/start:

systemctl --user daemon-reload
systemctl --user enable presence-panel
systemctl --user restart presence-panel
systemctl --user status presence-panel --no-pager -l

9) What to do if something breaks

Motion running but no /tmp/presence_event

  • Ensure hook lines are not commented.

  • Confirm Motion is reading /etc/motion/motion.conf.

  • Check for script exec errors in logs:

    sudo journalctl -u motion --no-pager | tail -n 120

Screen never wakes on motion

  • Ensure /tmp/presence_event timestamp changes when you move.

  • Confirm daemon is running:

    systemctl --user status presence-panel

Touch mapping wrong after wake

  • Verify your ~/auto-rotate.sh works by running it manually.
  • Ensure the path is correct in presence_panel.py.

Companion files in this Gist

  • motion.conf — full Motion configuration used here.
  • presence_panel.py — full presence daemon.
  • presence-panel.service — systemd user unit.
############################################################
# Motion configuration for RPi OS Bookworm + systemd
# Use case: presence detection only (no pictures/movies)
############################################################
########################
# Service / logging
########################
daemon off
setup_mode off
log_level 4
log_type all
log_file /var/log/motion/motion.log
########################
# Camera (USB / V4L2)
########################
video_device /dev/video0
# Preferred palette/format for V4L2 camera (8 = MJPEG on most cams)
video_params palette=8
# Low CPU / low bandwidth
width 640
height 480
framerate 5
minimum_frame_time 0
########################
# Motion detection
########################
# Start with these; raise threshold if you get false triggers
threshold 1500
noise_level 32
noise_tune on
# Seconds of quiet before an event ends
event_gap 10
pre_capture 1
post_capture 1
locate_motion_mode off
########################
# Outputs (disabled)
########################
# Target dir is still required even if outputs are off
target_dir /tmp/motion
picture_output off
picture_output_motion off
picture_quality 75
movie_output off
movie_output_motion off
movie_max_time 0
movie_quality 45
snapshot_interval 0
########################
# Web / streaming (disable remote access)
########################
stream_port 8081
stream_localhost on
stream_quality 50
stream_maxrate 1
webcontrol_port 8080
webcontrol_localhost on
webcontrol_interface off
########################
# Hooks for presence
########################
# Fires repeatedly while motion is seen (keeps timestamp fresh)
on_motion_detected /usr/local/bin/presence_event.sh
# Also fire on event boundaries (optional)
on_event_start /usr/local/bin/presence_event.sh
on_event_end /usr/local/bin/presence_event.sh
[Unit]
Description=Presence-based panel control (X11)
[Service]
ExecStart=/usr/bin/python3 %h/presence_panel.py
Restart=on-failure
[Install]
WantedBy=default.target
#!/usr/bin/env python3
import time
import subprocess
from pathlib import Path
DISPLAY = ":0"
# Seconds of no activity before turning off
OFF_TIMEOUT = 5 * 60 # 5 minutes
# Prevent flapping
MIN_ON_TIME = 30 # seconds
MOTION_FILE = Path("/tmp/presence_event")
HOME = Path.home()
XAUTHORITY = HOME / ".Xauthority"
# Rotation/calibration script (from your gist)
ROTATE_SCRIPT = HOME / "auto-rotate.sh"
REAPPLY_ROTATION_ON_WAKE = ROTATE_SCRIPT.exists()
WAKE_REAPPLY_DELAY = 2.0 # seconds
def sh(cmd: str):
subprocess.run(["bash", "-lc", cmd], check=False)
def screen_on():
sh(f"export DISPLAY={DISPLAY}; export XAUTHORITY='{XAUTHORITY}'; xset dpms force on")
if REAPPLY_ROTATION_ON_WAKE:
time.sleep(WAKE_REAPPLY_DELAY)
sh(f"export DISPLAY={DISPLAY}; export XAUTHORITY='{XAUTHORITY}'; '{ROTATE_SCRIPT}'")
def screen_off():
sh(f"export DISPLAY={DISPLAY}; export XAUTHORITY='{XAUTHORITY}'; xset dpms force off")
def get_x_idle_seconds() -> float:
out = subprocess.check_output(
["bash", "-lc", f"export DISPLAY={DISPLAY}; export XAUTHORITY='{XAUTHORITY}'; xprintidle"],
text=True
).strip()
return float(out) / 1000.0
last_motion_ts = time.time()
screen_is_on = True
last_on_ts = time.time()
while True:
now = time.time()
# Read camera motion timestamp (format: "motion <epoch>")
try:
txt = MOTION_FILE.read_text().strip()
if txt:
parts = txt.split()
if len(parts) >= 2:
ts = float(parts[1])
if ts > last_motion_ts:
last_motion_ts = ts
if not screen_is_on:
screen_on()
screen_is_on = True
last_on_ts = now
except FileNotFoundError:
pass
except Exception:
pass
# X11 idle (touch/mouse/keyboard)
try:
x_idle = get_x_idle_seconds()
except Exception:
x_idle = 10**9 # fall back to motion only
time_since_motion = now - last_motion_ts
effective_idle = min(time_since_motion, x_idle)
if screen_is_on:
if (now - last_on_ts) > MIN_ON_TIME and effective_idle > OFF_TIMEOUT:
screen_off()
screen_is_on = False
time.sleep(0.2)
@ajmeese7
Copy link
Author

Here's what the current (very basic) display setup looks like, its primary use is as a schedule reminder / discussion aid:

image

I've got other plans on the horizon, but for now it serves its purpose well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment