No description
Find a file
2026-07-31 20:31:11 -05:00
.cargo Add support for rpicam h264 stream mode 2026-07-12 17:41:22 -05:00
src Implement hough transform diff and bin pixel diff 2026-07-31 20:31:11 -05:00
test_images Add a testing system and a basic suite of test images 2023-12-28 14:45:44 -06:00
.env.dist Add GPIO button handling and wire up mqtt event for doorbell press 2026-07-31 16:37:46 -05:00
.gitignore Add output folder to gitignore 2026-07-12 17:42:46 -05:00
Cargo.lock Implement hough transform diff and bin pixel diff 2026-07-31 20:31:11 -05:00
Cargo.toml Implement hough transform diff and bin pixel diff 2026-07-31 20:31:11 -05:00
crab-cam.png Add a testing system and a basic suite of test images 2023-12-28 14:45:44 -06:00
LICENSE Add LICENSE 2023-12-27 07:22:24 +00:00
README.md Add GPIO button handling and wire up mqtt event for doorbell press 2026-07-31 16:37:46 -05:00
TODO.md Add embedded webserver via touche with endpoint to toggle detection 2024-01-01 21:49:17 -06:00

motion-camera

A CLI app intended to run on raspberry pi (or other other computer with a camera attached) and detect motion.

Features

  • Multiple strategies for motion detection
  • Record short video when motion is detected
  • Call configured webhooks when motion is detected
  • Support all common camera image formats

Building

You'll likely want to cross-compile this to aarch64 to run on the raspberry pi. To do this, you need to install the appropriate cross-compiler package for your system.

Debian:

$ sudo apt-get install gcc-aarch64-linux-gnu

Arch:

$ sudo pacman -S aarch64-linux-gnu-gcc

You'll also need to install the rust toolchain for the target architecture:

$ rustup target add aarch64-unknown-linux-gnu

Ensure the linker is specified correctly in .cargo/config

Build:

$ cargo build --release --target=aarch64-unknown-linux-gnu

Camera Backends

The default v4l backend captures YUYV frames from USB and other conventional V4L2 cameras. Select a device with:

$ ./motion-camera --backend v4l --cam /dev/video0

Raspberry Pi CSI cameras use libcamera instead of acting like conventional V4L2 capture devices. On Raspberry Pi OS, install the camera applications and verify that the camera is detected:

$ sudo apt install rpicam-apps
$ rpicam-hello --list-cameras

Then run motion-camera with the rpicam backend:

$ mkdir -p clips
$ ./motion-camera --backend rpicam --out-dir ./clips

This backend starts the system-provided rpicam-vid command and reads its raw YUV420 output. It captures 1280x720 video at 10 fps and downsamples the luma plane to 320x180 for motion detection. Recorded clips retain the full 1280x720 frames. Since libcamera remains outside the Rust binary, cross-compilation does not require Raspberry Pi libraries or a target sysroot.

The --cam option only applies to the v4l backend. The initial rpicam backend uses camera index 0.

For higher-quality clips without software H.264 encoding, install FFmpeg and use the rpicam-h264 backend:

$ sudo apt install rpicam-apps ffmpeg
$ mkdir -p clips
$ ./motion-camera --backend rpicam-h264 --out-dir ./clips

This backend asks rpicam-vid for a continuous 1920x1080, 30 fps, High-profile H.264 stream at 8 Mbps. It decodes one IDR frame every 500 ms for motion detection and writes the original encoded stream to clips without re-encoding. Completed raw clips are remuxed to MP4 by a background FFmpeg worker, so camera capture continues during finalization.

The H.264 backend retains approximately two seconds of encoded video before motion is detected. Clips therefore begin at an independently decodable IDR before the notification timestamp. The pre-roll buffer is also limited to 8 MiB for memory safety. Existing five-second motion expiry and 30-second maximum clip duration behavior still applies.

--interval values above 500 ms skip IDRs for detection while retaining all encoded frames. Values below 500 ms cannot make detection faster because the initial H.264 GOP is fixed at 15 frames.

RTSP Live Stream

The rpicam-h264 backend can expose its existing encoded stream over RTSP without re-encoding it. Start an opt-in listener with:

$ ./motion-camera --backend rpicam-h264 \
    --rtsp-listen 0.0.0.0:8554 \
    --out-dir ./clips

Then connect to rtsp://CAMERA_ADDRESS:8554/live. For example:

$ ffplay -rtsp_transport tcp rtsp://CAMERA_ADDRESS:8554/live

The initial implementation supports H.264 video over RTSP TCP interleaving. UDP transport, audio, authentication, and TLS are not yet supported. New and lagging clients wait for the next IDR before receiving video, normally no more than 500 ms with the default camera configuration. Up to four clients can be connected at once.

RTSP is disabled unless --rtsp-listen is provided. Binding to 0.0.0.0 exposes the unauthenticated stream on every network interface, so use an appropriate firewall or bind to a private interface.

FFmpeg is required only when --out-dir is used with rpicam-h264. If remuxing fails, the hidden raw .h264 file is retained in the output directory for diagnosis. Temporary raw and .part.mp4 files are removed after a successful remux.

To verify the raw frame layout expected by motion-camera, capture ten frames on the Pi:

$ rpicam-vid --camera 0 --nopreview --frames 10 --width 1280 --height 720 \
    --framerate 10 --codec yuv420 --flush --output test.yuv
$ stat -c '%s bytes' test.yuv

The expected size is 13824000 bytes (10 frames of 1,382,400 bytes each).

Testing

A suite of detection pair images can be found in ./test_images. They're sorted into directories. All of the image pairs in ./test_images/motion should trigger motion detection, while all of the pairs in ./test_images/no-motion should not trigger detection. There are test cases in the detect bundle which will run every image pair against every detection strategy.

Test for ./test_images/light-change are ignored, because some of the strategies cannot pass them.

One way to generate test images is to set environment variable MOTION_CAM_WRITE_DEBUG_IMAGES=true before running the program. When motion is detected, the pair of images that triggered detection will be saved to the current working directory. Set the similarity threshold very high to write a detection pair for frames without motion.

Detection Handlers

There is a DetectionHandler trait for generic detection handlers. Potential handlers could include:

  • webhook: multi-purpose
  • discord: send direct message
  • twilio: send text message
  • email: send email
  • cloud uploader: upload detection clip to cloud service

Discord

Set the environment variables:

  • MOTION_CAM_DISCORD_BOT_TOKEN Create a bot https://discord.com/developers/applications and get the token
  • MOTION_CAM_DICORD_DM_USER Currently needs to be the user ID who will recieve DMs when motion is detected. This user must be in at least one server with the bot. The bot will not send any messages in the server.

When both of these variables are set, the DiscordHandler will be registered.

MQTT / Home Assistant

Set the environment variables:

  • MOTION_CAM_MQTT_HOST Hostname or IP of the MQTT broker. When unset, the MqttHandler is not registered.
  • MOTION_CAM_MQTT_PORT Broker port (default 1883). Plain TCP only; TLS is not compiled in.
  • MOTION_CAM_MQTT_USERNAME / MOTION_CAM_MQTT_PASSWORD Broker credentials.
  • MOTION_CAM_MQTT_CLIENT_ID Defaults to motion-camera-<location>.

The handler publishes a retained Home Assistant MQTT discovery config, so a binary_sensor.<location>_motion entity (device class motion) appears in Home Assistant automatically with no YAML required. Motion start publishes ON and motion end publishes OFF to motion-camera/<location>/motion/state. Availability is tracked on motion-camera/<location>/status using a broker last-will, so the entity is marked unavailable if motion-camera disconnects ungracefully. Topics are derived from the --location CLI flag.

A live integration test can be run against a local broker with:

$ mosquitto -p 18883 &
$ MOTION_CAM_MQTT_TEST_BROKER=127.0.0.1:18883 cargo test broker

Doorbell Button

When MQTT is configured, a doorbell button is also published as a Home Assistant MQTT event entity (event.<location>_button, device class doorbell) under the same device as the motion sensor. Each press publishes {"event_type": "button_press"} to motion-camera/<location>/button/event.

The button is read from a GPIO pin that is externally pulled up to 3.3 V and shorts to ground when pressed (active low). Set the BCM pin number with MOTION_CAM_DOORBELL_GPIO_PIN (default 4, physical pin 7). Presses are detected on the falling edge with a 250 ms debounce. The internal pull-up is enabled as a fallback, so the pin can be left unwired without generating phantom presses; the external pull-up dominates once the button circuit is connected. If the GPIO hardware is unavailable (e.g. when developing on another machine), a warning is logged and the rest of the program keeps running.

To exercise the Home Assistant integration without button hardware, set MOTION_CAM_DOORBELL_TEST_INTERVAL to a number of seconds; a button_press event is then published on that interval.

Embedded Web Server

A webserver is hosted on port 4444 with an API endpoint at PUT /detecting. Send a boolean value (1, 0, true, false, etc) to turn motion detection on or off. This can be used to automate turning on detection when you leave your house for example.

In the future, this server may host a web UI to view or manage settings, and hopefully to view the live video stream eventually (via HLS or something similar).

Logging

This project uses env_logger with a default level of info. If you want to see debug logs then set RUST_LOG=debug, or just pass the -v flag.