Have you ever wanted a digital picture frame Raspberry Pi setup that automatically detects a USB stick with photos, copies its contents, and integrates seamlessly with your Pi3D PictureFrame?
That’s precisely what I describe in this article!
This feature was requested several times in the past. But it wasn’t until reader Ian C. came up with a solution that got me going.
Building on Ian’s work, I have modified the script so that no reboot or any file modification of the existing setup is necessary.
Tested with a Raspberry Pi 5 with OS Bookworm (November 2024). All other Pis from model Pi Zero 2 W upwards should work fine. Please let me know if they don’t.
How this script works
The script listens for USB “events”. When you plug in a USB stick, it immediately recognizes it and gets going.
To access the files on the USB stick, the script mounts it to a specific folder on the Raspberry Pi, in this case, /home/pi/mnt/usb
. This is the USB’s temporary home while the script runs its course.
Once the USB is mounted, the script scans it for image files with extensions like .jpg
, .jpeg
, or .heic
. Any matching files are copied to a folder called /home/pi/Pictures/fromUSB
.
After copying the files, the script tells the Pi3D PictureFrame software to switch to the subdirectory fromUSB
via an HTTP command. The software dynamically switches to this subdirectory, so your USB photos, and only those, start showing up immediately.
When you remove the USB stick, the “fromUSB
” directory is deleted, and the display directory is changed back to “main”/home/pi/Pictures”. It then waits for another USB stick to be inserted, which may or may not happen. Ask Godot how that feels.
I have repeatedly inserted and removed the USB stick, and the script seems rock solid.
The only point to remember is to insert the USB stick after PictureFrame is up and running, not before. But that’s how it should be anyway, because who turns their Raspberry Pi off anyway?
Requirements
This solution assumes that you have installed Pi3D PictureFrame as described here or here.
Here is what else you need to do:
Activate HTTP in configuration.yaml
.
For this to work, just set use_http
to True
like below. Most people won’t have to change anything else.
http:
use_http: True # default=False. Set True to enable http NB THIS SERVER IS FOR LOCAL NETWORK AND SHOULD NOT BE EXPOSED TO EXTERNAL ACCESS
path: "/home/pi/picframe_data/html" # path to where html files are located
port: 9000 # port used to serve pages by http server < 1024 requires root which is *bad* idea
auth: false # default=False. Set True if enable basic auth for http
username: admin # username for basic auth
password: null # password for basic auth. If set null generate random password in file basic_auth.txt in parent http directory
use_ssl: False
keyfile: "path/to/key.pem" # private-key
certfile: "path/to/cert.pem" # server certificate
Next, install two Python packages
source venv_picframe/bin/activate
pip install pyudev psutil
and do a sudo reboot
at the end.
The Python script
Make sure that you are in the /home/pi directory and create a new file with
sudo nano usb_stick.py
Paste the content below in the file.
#!/usr/bin/env python3
import pyudev
import os
import shutil
import urllib.request
import subprocess
import psutil # For identifying processes using the mount point
from time import sleep
# Constants
USB_MOUNT_POINT = "/home/pi/mnt/usb" # Mount point for the USB stick
PICTURES_ROOT = "/home/pi/Pictures" # Root directory for Pi3D PictureFrame
USB_PICTURES_DIR = "fromUSB" # Subdirectory for USB pictures
DEFAULT_PICTURES_SUBDIR = "" # Default subdirectory for Pi3D
VALID_EXTENSIONS = (".jpg", ".jpeg", ".heic", ".JPG", ".JPEG", ".HEIC")
PI3D_CONTROL_URL = "http://localhost:9000/?subdirectory=" # Pi3D control URL
def delete_usbpictures():
"""Delete the USB pictures directory if it exists."""
destination_path = os.path.join(PICTURES_ROOT, USB_PICTURES_DIR)
if os.path.exists(destination_path):
shutil.rmtree(destination_path)
print(f"Deleted existing directory: {destination_path}")
def find_usb_device(max_retries=5, retry_delay=1):
"""Find the first unmounted USB device with a valid file system."""
for attempt in range(max_retries):
print(f"Checking for USB devices (attempt {attempt + 1}/{max_retries})...")
result = subprocess.run(["lsblk", "-o", "NAME,FSTYPE,MOUNTPOINT", "-n"], capture_output=True, text=True)
print("lsblk output:")
print(result.stdout)
for line in result.stdout.splitlines():
parts = line.strip().split()
if len(parts) >= 2 and parts[1] in ["vfat", "exfat", "ntfs", "ext4"]:
device_name = parts[0].strip("└─")
mount_point = parts[2] if len(parts) > 2 else ""
if not mount_point:
print(f"Detected USB device: /dev/{device_name}")
return f"/dev/{device_name}"
sleep(retry_delay)
print("No unmounted USB devices detected.")
return None
def mount_usb():
"""Mount the USB stick to the predefined location."""
if os.path.ismount(USB_MOUNT_POINT):
print(f"USB stick already mounted at {USB_MOUNT_POINT}. Skipping mount.")
return True
os.makedirs(USB_MOUNT_POINT, exist_ok=True)
usb_device = find_usb_device()
if not usb_device:
print("No USB device found.")
return False
result = subprocess.run(["sudo", "mount", usb_device, USB_MOUNT_POINT], capture_output=True, text=True)
if result.returncode == 0:
print(f"Mounted USB stick at {USB_MOUNT_POINT}")
return True
else:
print(f"Failed to mount USB stick: {result.stderr}")
return False
def unmount_usb():
"""Attempt to unmount the USB device and reset the main directory."""
# Check if the USB mount point is actually mounted
if os.path.ismount(USB_MOUNT_POINT):
try:
# Attempt to unmount the USB using sudo
subprocess.check_call(["sudo", "umount", USB_MOUNT_POINT])
print(f"Unmounted USB stick from {USB_MOUNT_POINT}.")
except subprocess.CalledProcessError as e:
print(f"Failed to unmount USB stick at {USB_MOUNT_POINT}: {e}")
else:
print(f"No USB stick mounted at {USB_MOUNT_POINT}. Skipping unmount.")
# Reset Pi3D to the main directory
try:
url = "http://localhost:9000/?subdirectory="
response = urllib.request.urlopen(url)
print("Successfully reverted to the main directory.")
print(f"Response: {response.read().decode('utf-8')}")
except Exception as e:
print(f"Failed to revert to the main directory: {e}")
def copy_photos_to_usbpictures():
"""Copy valid photos from USB to the designated directory."""
delete_usbpictures()
destination_path = os.path.join(PICTURES_ROOT, USB_PICTURES_DIR)
os.makedirs(destination_path, exist_ok=True)
files_copied = 0
print(f"Scanning mounted USB directory: {USB_MOUNT_POINT}")
for root, dirs, files in os.walk(USB_MOUNT_POINT):
print(f"Scanning directory: {root}")
files = [f for f in files if not f.startswith("._")]
for file in files:
print(f"Found file: {file}")
if file.lower().endswith(VALID_EXTENSIONS):
src_file = os.path.join(root, file)
dest_file = os.path.join(destination_path, os.path.relpath(src_file, USB_MOUNT_POINT))
os.makedirs(os.path.dirname(dest_file), exist_ok=True)
try:
shutil.copy2(src_file, dest_file)
files_copied += 1
print(f"Copied: {src_file} -> {dest_file}")
except Exception as e:
print(f"Failed to copy {src_file}: {e}")
if files_copied > 0:
print(f"Successfully copied {files_copied} files.")
else:
print("No valid files found to copy.")
def change_subdirectory(subdir_name):
"""Update the subdirectory in Pi3D PictureFrame."""
if subdir_name in os.listdir(PICTURES_ROOT):
full_url = PI3D_CONTROL_URL + subdir_name
try:
urllib.request.urlopen(full_url)
print(f"Successfully changed subdirectory to: {subdir_name}")
except Exception as e:
print(f"Failed to change subdirectory to: {subdir_name}. Error: {e}")
else:
print(f"Subdirectory {subdir_name} does not exist. Skipping change.")
# Monitor USB events
context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by(subsystem='usb')
print("Listening for USB events...")
# Handle USB already inserted at boot
if os.path.ismount(USB_MOUNT_POINT):
print(f"USB stick detected at boot, treating as newly inserted.")
copy_photos_to_usbpictures()
change_subdirectory(USB_PICTURES_DIR)
# Monitor for new USB events
for device in iter(monitor.poll, None):
if device.action == "add":
print(f"USB device added: {device.device_path}")
sleep(2)
if mount_usb():
copy_photos_to_usbpictures()
change_subdirectory(USB_PICTURES_DIR)
elif device.action == "remove":
print(f"USB device removed: {device.device_path}")
change_subdirectory(DEFAULT_PICTURES_SUBDIR)
delete_usbpictures()
unmount_usb()
Save and close.
Make it executable with
sudo chmod +x usb_stick.py
I experimented for a few hours to make the script as robust as possible and could not crash the file even after twenty USB in/out hotplugging.
Automatically start at boot
The last step is to start the script in the background, where it will wait like a sleeper cell until someone with a USB drive comes to visit you and shares his photographic magic.
Or you might wish to have a particular theme day without damaging your carefully curated photo library. In that case, you could just put your photos on a USB stick. Of course, you can also do this.
To be prepared for whatever may come at you, create a new service file for your script with
sudo nano /etc/systemd/system/usb_stick.service
Paste the following content into the file:
[Unit]
Description=USB Stick Management Script
After=network.target
[Service]
ExecStart=/home/pi/venv_picframe/bin/python /home/pi/usb_stick.py
WorkingDirectory=/home/pi
Restart=always
User=pi
[Install]
WantedBy=multi-user.target
Reload the Systemd Daemon, enable and start the service with
sudo systemctl daemon-reload
sudo systemctl enable usb_stick.service
sudo systemctl start usb_stick.service
You can check if everything works with
sudo systemctl status usb_stick.service
On boot, your Raspberry Pi will now wait until a USB media drive comes along.
Conclusion
Thanks for the inspiration to Ian C., and let me know your particular use case for this USB option.
Was this article helpful?
Thank you for your support and motivation.