| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A Raspberry Pi picture frame that runs on its own. It boots straight into a fullscreen, shuffled slideshow of photos from an SMB/CIFS share on a NAS, and switches the display off at night.
| File | Goes to | Purpose |
|---|---|---|
| picframe.py | /opt/picframe/ | Slideshow, rendering, display power and MQTT (Pygame, Pillow, paho-mqtt) |
| config.ini | /etc/picframe/ | Photo folder, delay, wake and sleep times, display method |
| picframe.service | /etc/systemd/system/ | Starts at boot, waits for the network and the share, restarts after a crash |
| fstab.example | appended to /etc/fstab | Mounts the SMB share at /mnt/photos |
| nas-credentials.example | /etc/samba/nas-credentials | NAS login, readable by root only |
| install.sh | run once | Installs everything above |
boot ─► network-online.target ─► desktop autologin (labwc, Wayland)
│
/mnt/photos (systemd automount) ◄─┤
▼
picframe.service (runs as your user)
ExecStartPre: wait for compositor + share
ExecStart: picframe.py
├─ scanner thread: walks the share, shuffles photos
├─ loader thread: picks the next photo (+ a portrait partner),
│ reads EXIF, renders matte + layout ahead of time
├─ MQTT thread: Home Assistant discovery, commands → queue
└─ main loop: shows slides, crossfades, clock/captions,
schedule + MQTT commands
└─ wlopm --off/--on '*' for display power
The slideshow only reads from the NAS on background threads. If the NAS drops off the network, the current photo stays on screen, and the wake and sleep times and MQTT control keep working. When the share comes back, the slideshow picks up again on its own.
sudo apt update && sudo apt full-upgrade -yOn the Pi, over SSH:
sudo apt install -y git
git clone https://github.com/ChrisPotDev/NasPicFrame.git
cd NasPicFrame
sudo ./install.shTo upgrade later: cd ~/NasPicFrame && git pull && sudo ./install.sh. Your config is migrated, not replaced; see Upgrading below.
Without git: copy the files from your PCscp -r NasPicFrame <user>@<pi-address>:~/
ssh <user>@<pi-address>
cd ~/NasPicFrame
sed -i 's/\r$//' install.sh # only needed if the files went through Windows
chmod +x install.sh
sudo ./install.shThe installer:
If the share is public (no username or password), skip this step and use the guest fstab line in Public shares below.
sudo nano /etc/samba/nas-credentialsusername=photoframe password=your-password domain=WORKGROUP
sudo chown root:root /etc/samba/nas-credentials
sudo chmod 600 /etc/samba/nas-credentialsA read-only NAS account just for the frame is a good idea.
Get your UID and GID with id. They are usually 1000. Then add this single line to /etc/fstab (sudo nano /etc/fstab):
//192.168.1.10/Photos /mnt/photos cifs credentials=/etc/samba/nas-credentials,uid=1000,gid=1000,ro,vers=3.0,iocharset=utf8,file_mode=0444,dir_mode=0555,soft,_netdev,nofail,x-systemd.automount,x-systemd.mount-timeout=30,x-systemd.after=network-online.target 0 0
| Option | Why |
|---|---|
| //192.168.1.10/Photos | Use the NAS's IP address. Name lookup (nas.local) may not work yet this early in boot. Give the NAS a fixed IP or a DHCP reservation. Write a space in the share name as \040. |
| credentials= | Keeps the password out of the world-readable /etc/fstab. |
| uid=,gid= | Files appear owned by the frame user. |
| ro, file_mode=0444, dir_mode=0555 | The frame never writes, so it can't damage your photos. |
| vers=3.0 | Modern SMB. Use 3.1.1 for current NAS firmware, or 2.1 for very old units. Never 1.0. |
| iocharset=utf8 | Accented and non-Latin file names work. |
| soft | An unreachable server returns an error instead of hanging programs forever. |
| _netdev | Marks it as a network filesystem, so it is ordered after the network. |
| nofail | The Pi still boots if the NAS is off. |
| x-systemd.automount | Boot never waits on the NAS. The share mounts the first time something reads /mnt/photos, and mounts again on the next access after an outage. This is what makes it reliable. |
| x-systemd.mount-timeout=30 | A dead NAS blocks a mount attempt for at most 30 s. |
| x-systemd.after=network-online.target | Mount attempts wait until the network is really up. |
Some shares are open to anyone on the network (guest access). This is common on routers with a USB-disk share and on NAS folders marked public. For those, replace credentials=… with guest:
//192.168.1.10/Public /mnt/photos cifs guest,uid=1000,gid=1000,ro,vers=3.0,iocharset=utf8,file_mode=0444,dir_mode=0555,soft,_netdev,nofail,x-systemd.automount,x-systemd.mount-timeout=30,x-systemd.after=network-online.target 0 0
The /etc/samba/nas-credentials file the installer created isn't used then; you can leave it or delete it. Everything else (automount, read-only, nofail) works the same way.
Guest access is where SMB is fussiest. If the test in 2c fails, work down this list, changing one thing at a time:
| Symptom (sudo dmesg | tail) | Try |
|---|---|
| error -13 (permission denied) | Some servers expect the guest user name rather than an anonymous login. Replace guest with username=guest,password=. |
| Still -13 on a Windows PC share | Windows maps guests to the Guest account, which is disabled by default. Either share the folder to Everyone, or (better) create a local user for the frame and use the credentials file from 2a. |
| error -95 or -22 (not supported / invalid) | The server may refuse guest logins over newer SMB versions, because guest sessions can't sign traffic. Try vers=2.1, then vers=2.0. |
| Mounts, but folders appear empty | The share allows guest connections but guests can't read the photos folder. Fix the folder permissions on the NAS: give guest or everyone read access. |
| Old router or NAS that only offers SMB1 | vers=1.0 works with guest, but SMB1 is insecure and deprecated. Use it only on a trusted home network, and prefer updating the device's firmware or enabling SMB2 on it. |
To check what the server offers before touching fstab:
sudo apt install -y smbclient
smbclient -L //192.168.1.10 -N # -N = no password: lists shares if guest access works
smbclient //192.168.1.10/Public -N -c 'ls' # can a guest actually read the share?Security: a guest share is readable by every device on your network. That's fine for holiday photos on a home LAN. For private photos, a password-protected share with a read-only account (2a) is better. Either way, the frame mounts the share read-only, so it can never change or delete anything.
sudo systemctl daemon-reload # regenerate mount units from fstab
sudo systemctl restart remote-fs.target
ls /mnt/photos | head # first access triggers the mount
findmnt /mnt/photos # should show type cifsIf it fails, sudo dmesg | tail and journalctl -u mnt-photos.mount show the reason. Usually it's a wrong password (-13), a wrong share name (-2), or an SMB version mismatch (-95; change vers=).
On Wi-Fi, turn off power saving. It is the most common cause of a share that keeps dropping:
nmcli -t -f NAME connection show --active # find the connection name
sudo nmcli connection modify "<name>" 802-11-wireless.powersave 2sudo nano /etc/picframe/config.ini[slideshow]
image_dir = /mnt/photos
delay = 30
[schedule]
wake_time = 07:00
sleep_time = 22:00Every option is explained in the file. It has these sections: [slideshow], [overlay], [schedule], [display] and [mqtt]. The app checks the file every 5 seconds and restarts itself when it changes, so you don't need to restart the service.
image_dir takes several folders, either separated by commas or one per indented line:
image_dir = /mnt/photos
/home/pi/Pictures
/media/pi/USBSTICK/PhotosCopy the new files to the Pi and run sudo ./install.sh again. It never replaces your config. Instead it migrates it:
Example output: /etc/picframe/config.ini: added [slideshow] pair_portraits, [slideshow] matting, …, [overlay], [mqtt] (backup: …)
To run the migration on its own, for example after restoring an old config:
sudo python3 /opt/picframe/picframe.py --config /etc/picframe/config.ini \
--migrate-config /opt/picframe/config.default.iniMissing options always fall back to built-in defaults anyway, so an old config never stops the frame from starting. The migration just makes new options visible and documented in your file.
What the slideshow does:
The app first checks how each photo is oriented. It reads only the file header and applies the EXIF rotation, so the check is cheap, and the result is cached. When the shuffle picks a portrait photo and the screen is landscape, the app looks for another portrait among the next 30 photos in the shuffle. It takes that photo out of the shuffle, so it isn't shown again later in the round, and shows the two side by side with pair_gap pixels between them. If no partner is found, for example at the end of a round, the portrait is shown alone on its blurred mat. On a portrait-mounted screen this flips: two landscape photos are stacked one above the other.
Any space a photo doesn't cover is filled with a version of the same photo. The photo is cropped to fill the area, blurred heavily (mat_blur), darkened (mat_brightness), and the sharp photo is placed on top. To keep this cheap on a Pi, the blur is done at 1/10 resolution and then enlarged; at this blur strength it looks the same. Each half of a portrait pair gets its own mat. All rendering happens on the loader thread while the previous photo is still on screen, so transitions stay smooth. matting = color gives plain background-coloured bars instead.
At sleep_time the app blanks its window and powers the display output off. At wake_time it turns the output on again. Power-off happens on the Pi side: the HDMI signal stops, so almost every monitor or TV drops into standby. This saves power and prevents burn-in. Schedules that cross midnight work (wake 18:00 / sleep 01:00). If the Pi reboots during the night, the display goes off again within a second of the app starting.
method = auto picks the first that applies:
| Method | When | Notes |
|---|---|---|
| wlopm --off '*' | Wayland (labwc/wayfire): current Pi OS | Recommended. Uses the wlr-output-power-management protocol, which is real DPMS. The output stays configured, so the fullscreen window survives the night untouched. Pi OS's own screen blanking uses this tool. |
| wlr-randr --output HDMI-A-1 --off | Wayland, if wlopm is missing | Disables the output completely. Works, but the compositor treats it as unplugged. The app recreates its fullscreen window after waking to handle this. |
| xset dpms force off | X11 session (legacy Pi OS / "X11" in raspi-config) | DPMS through the X server. |
| vcgencmd display_power 0 | Legacy firmware display driver only | Does nothing on current Pi OS. Since Bookworm the KMS driver (vc4-kms-v3d) owns the display, not the firmware. Kept only for old installs. |
| cec | Set it manually | Sends HDMI-CEC standby / image-view-on. Use it if your frame is a TV, because some TVs show "No signal" instead of going to standby. Needs sudo apt install v4l-utils and a CEC-capable TV. |
Test it by hand. Run this over SSH as the desktop user:
XDG_RUNTIME_DIR=/run/user/$(id -u) python3 /opt/picframe/picframe.py --display off
XDG_RUNTIME_DIR=/run/user/$(id -u) python3 /opt/picframe/picframe.py --display onIf you'd rather use cron than the built-in schedule: leave wake_time and sleep_time empty in the config (display always on), then add to crontab -e:
0 22 * * * XDG_RUNTIME_DIR=/run/user/1000 /usr/bin/python3 /opt/picframe/picframe.py --display off 0 7 * * * XDG_RUNTIME_DIR=/run/user/1000 /usr/bin/python3 /opt/picframe/picframe.py --display on
The built-in schedule is still the better choice. It also pauses the slideshow at night, so the Pi doesn't read photos from the NAS, and it puts the right state back after a reboot.
/etc/systemd/system/picframe.service (installed by install.sh with your user and UID filled in):
[Unit]
Description=NasPicFrame photo slideshow
Wants=network-online.target
After=network-online.target remote-fs.target systemd-user-sessions.service
StartLimitIntervalSec=0
[Service]
Type=simple
User=pi
Group=pi
Environment=XDG_RUNTIME_DIR=/run/user/1000
Environment=WAYLAND_DISPLAY=wayland-0
Environment=DISPLAY=:0
Environment=PYTHONUNBUFFERED=1
ExecStartPre=/usr/bin/python3 /opt/picframe/picframe.py --config /etc/picframe/config.ini --wait --wait-timeout 120
ExecStart=/usr/bin/python3 /opt/picframe/picframe.py --config /etc/picframe/config.ini
Restart=always
RestartSec=10
TimeoutStartSec=180
TimeoutStopSec=15
Nice=5
[Install]
WantedBy=graphical.targetDesign notes:
[mqtt]
enabled = yes
host = 192.168.1.20
username = picframe
password = your-password| Entity | Type | Does |
|---|---|---|
| Display | switch | Turns the screen on or off (same mechanism as the schedule). |
| Pause slideshow | switch | Stops advancing; Next/Previous still work. |
| Clock | switch | Shows or hides the on-screen clock. |
| Photo info | switch | Shows or hides the date and place captions. |
| Next photo / Previous photo | button | Steps forward, or back through history. |
| Current photo | sensor | File name(s) on screen; the attributes hold the full paths and captions. |
The device shows as unavailable when the frame is off or disconnected: an MQTT last-will message marks it offline. All states are published retained. Discovery is sent again whenever the broker reconnects or Home Assistant restarts, because the app listens for Home Assistant's homeassistant/status birth message. If the broker is down at boot, the app keeps retrying in the background.
Manual vs scheduled display power: a Display command from Home Assistant lasts until the next scheduled switch. Turning the frame on at 23:00 keeps it on until the next sleep_time; turning it off at 15:00 keeps it off until the next wake_time. For presence-based control, leave wake_time/sleep_time empty and automate it in Home Assistant instead. Entity IDs depend on the device name, so check yours in Home Assistant:
automation:
- alias: "Picture frame follows presence"
trigger:
- platform: state
entity_id: binary_sensor.living_room_occupancy
for: { minutes: 10 }
action:
- service: "switch.turn_{{ 'on' if trigger.to_state.state == 'on' else 'off' }}"
target: { entity_id: switch.picture_frame_display }Raw topics, for other MQTT clients (<node> is the hostname, or node_id if set):
picframe/<node>/display/set ON | OFF picframe/<node>/display/state picframe/<node>/pause/set ON | OFF picframe/<node>/pause/state picframe/<node>/clock/set ON | OFF picframe/<node>/clock/state picframe/<node>/info/set ON | OFF picframe/<node>/info/state picframe/<node>/next/set PRESS picframe/<node>/previous/set PRESS picframe/<node>/photo/state, …/photo/attributes, …/availability (online | offline)
For example: mosquitto_pub -h 192.168.1.20 -u picframe -P … -t picframe/livingroom/next/set -m PRESS
journalctl -u picframe -f # live log: photo count, skips, display on/off
sudo systemctl restart picframe # restart
sudo systemctl stop picframe # stop (also turns the display back on)
systemctl status picframe mnt-photos.automount mnt-photos.mount| Symptom | Fix |
|---|---|
| No Wayland/X11 display in the log | Desktop autologin is off: sudo raspi-config nonint do_boot_behaviour B4, then reboot. |
| Desktop or taskbar flashes during boot | Normal. It lasts the few seconds before the service starts. |
| Display never turns off | journalctl -u picframe | grep Display. Check that wlopm is installed, or set method = wlr-randr. |
| Screen goes black during the day | Something else is blanking it. Run sudo raspi-config nonint do_blanking 1 and reboot. |
| Window not fullscreen / wrong monitor (native Wayland) | Add Environment=SDL_VIDEODRIVER=x11 (or wayland) under [Service] and test both. |
| Slow on very large PNG/HEIC files | Export JPEGs at about 2–4K for the frame. JPEG decoding uses the fast reduced-size path. |
| Photos sideways | They lack EXIF orientation data. Rotate the files themselves. |
| Frame doesn't appear in Home Assistant | journalctl -u picframe | grep MQTT. If the connection is refused, check the username and password. Check that discovery_prefix matches Home Assistant's setting. |
| Few portrait pairs | Partners come from the next 30 photos in the shuffle. A library with very few portraits pairs less often. |
| Captions show coordinates, not places | Set geocode = nominatim; the Pi needs internet access. |
Testing with a keyboard attached: →/← next/previous, Space pause, C clock, I info, D display, Esc quit (systemd restarts it).
Requirements: Raspberry Pi 3B+ / 4 / 5 / Zero 2 W, Raspberry Pi OS Bookworm or newer, Python 3.9+.
| Back | FazBrowse Home | New Git URL |