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.
- Portrait pairing: two portrait photos are shown side by side, instead of one with wide empty borders.
- Blurred matting: any space a photo doesn't fill shows a blurred, darkened copy of the photo, not black bars.
- EXIF overlay: the date and place each photo was taken appear in its corner, along with an on-screen clock.
- Home Assistant: the frame shows up as a device over MQTT. From there you can turn the display on or off, go to the next or previous photo, pause, and toggle the clock and photo info.
| 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.
- Flash Raspberry Pi OS (64-bit) with desktop, Bookworm or newer, using Raspberry Pi Imager. In the Imager settings, set the user name, Wi-Fi, locale/timezone and enable SSH. Use the desktop image, not Lite. On recent Pi OS the desktop already runs a Wayland compositor (labwc), which handles HDMI modes, hot-plug and display power for you.
- Boot the Pi, SSH in, and update it:
sudo apt update && sudo apt full-upgrade -y - Check that the compositor is labwc. It is the default since late 2024; older installs used wayfire:
sudo raspi-config→ Advanced Options → Wayland → W3 labwc. - Check the timezone, because wake and sleep times use local time:
timedatectl. To change it:sudo raspi-config→ Localisation → Timezone.
On 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 PC
scp -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:
- installs
python3-pygame python3-pil cifs-utils wlr-randr wlopm - copies the app to
/opt/picframeand the config to/etc/picframe/config.ini. If a config already exists, it is kept and only new options are added to it (see Upgrading below) - creates
/mnt/photosand/etc/samba/nas-credentials(mode 600) - installs and enables
picframe.service, with your user name and UID filled in - sets boot to desktop with autologin (
raspi-config nonint do_boot_behaviour B4) - turns off the OS's own screen blanking (
do_blanking 1), so only the schedule turns the screen off - enables
NetworkManager-wait-online.service, sonetwork-online.targetmeans the network is really up
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/Photos- All the folders feed one shuffle, so each photo is equally likely to come up. A folder with more photos therefore appears more often.
- Local photos keep showing when the NAS is down. A folder that comes back empty or unreachable is rescanned every 30 seconds, and its photos rejoin the shuffle when it's back.
- At boot, the service waits up to 120 s for all the folders, so the first shuffle includes the NAS. Local folders are ready immediately.
- Local folders: copy photos to
~/Pictures(or any folder) withscp, a USB stick, or straight onto the SD card. The service runs as your user, so the folder must be readable by that user. - USB sticks: the desktop mounts them automatically at
/media/<user>/<LABEL>. If you give the stick a fixed label, the path stays the same each time. When the stick is removed, its photos drop out at the next rescan, and plugging it back in brings them back. - To use only local photos, set
image_dirto the local folder and leave the fstab line out.
Copy the new files to the Pi and run sudo ./install.sh again. It never replaces your config. Instead it migrates it:
- Any section or option that is new in this version is added to
/etc/picframe/config.iniwith its explanatory comments and default value. A new option goes at the end of its section; a new section goes at the end of the file. - Existing values, comments, and options you've commented out are never changed. The file is merged as text rather than rewritten by
configparser, which would remove every comment. - Before changing anything, the installer saves a backup as
config.ini.bak-YYYYMMDD-HHMMSS. If nothing is missing, the file isn't touched. - The running slideshow is restarted so the new code takes effect.
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:
- Fullscreen and borderless (
pygame.FULLSCREEN | NOFRAME). The cursor is hidden twice:set_visible(False)plus a transparent cursor. - Shuffle without repeats. Every photo is shown once before any photo repeats, then the list is shuffled again, never showing the same photo twice in a row.
- Turns photos upright using the camera's EXIF rotation data. JPEGs are decoded at reduced resolution (
Image.draft), which makes 24 MP photos load several times faster on a Pi. - Loads the next photo while the current one is showing, then crossfades to it.
- Skips Synology/QNAP thumbnail, recycle and snapshot folders (
@eaDir,#recycle, ...). - If there are no photos to show, it shows a placeholder screen with "Waiting for photos in /mnt/photos ..." and keeps retrying. The built-in placeholder is a dark dusk gradient with a soft photo icon. It is drawn in code, so it's sharp at any resolution and there's no image file to ship. To use your own picture instead, set
placeholder_image = /home/pi/placeholder.jpg. Keep it on the Pi, not on the share, since the share may be what's unavailable. Your picture is displayed like any other photo (fitted, with the blurred background), with the status text along the bottom. If it can't be read, the built-in placeholder is used. - Keeps the last 100 slides so you can step back through them with previous. A pair comes back as the same pair.
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.
- Date: read from the EXIF DateTimeOriginal tag, falling back to DateTime, and formatted with
date_format. - Place: read from the EXIF GPS data. By default (
geocode = no) it shows coordinates, and nothing leaves the Pi. Withgeocode = nominatimit shows "Town, Country" from OpenStreetMap's Nominatim service. Coordinates are rounded to about 100 m before they are sent, requests stay under Nominatim's limit of one per second, and results are cached in~/.cache/picframe/geocode.json, so each place is looked up only once. - The caption sits in the bottom-right corner of the photo itself; each photo in a pair has its own caption. Both captions and clock use slightly translucent text with a soft shadow, so they are readable on any photo without drawing attention.
- Clock:
clock_sizeissmall,medium(the default),large,huge, or a percentage of the screen height such as12%.clock_positionis any corner or the top or bottom centre (bottom-leftby default).show_date = yesadds a smaller date line under the time, formatted withclock_date_format, e.g. Tuesday 20 October. If the clock shares an edge with a caption, the caption moves up or down out of its way. - Photos without EXIF data, such as screenshots or many photos saved from messaging apps, just get no caption.
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.
The slideshow runs as a systemd service, picframe.service. It starts at boot, waits for the network and the share, and restarts if it crashes. install.sh has already installed and enabled it (Step 1). This step explains the file, how to install it by hand if you skipped the installer, and how to check that it works.
/etc/systemd/system/picframe.service, as installed with your user and UID filled in (here pi and 1000):
[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:
- Waits for the network.
Wants=andAfter=network-online.targetwork together withNetworkManager-wait-online.service, which the installer enables. - Waits for the mount.
After=remote-fs.targetorders the service after the share's automount is set up.ExecStartPre … --waitthen reads/mnt/photos, which triggers the mount, and waits until it has content or 120 s pass. The same step waits for the desktop compositor's socket. If the socket isn't namedwayland-0, the app finds the actual one. - Why not
RequiresMountsFor=/mnt/photos? If the NAS is off at boot, that dependency fails, andRestart=does not retry dependency failures, so the frame would stay dark until someone rebooted it. Here the service always starts, and the app shows the "waiting" screen until the NAS appears. - Restarts after crashes.
Restart=alwayswithRestartSec=10andStartLimitIntervalSec=0means systemd never gives up. - It runs as your user inside the autologin desktop session (
XDG_RUNTIME_DIR/WAYLAND_DISPLAY). That is what lets it draw on the screen and callwlopm.
The repository's picframe.service has two placeholders, @USER@ and @UID@. Fill them in with your own user and copy the file into place. Run this from the repository folder, as the desktop user (not root):
sed -e "s/@USER@/$USER/g" -e "s/@UID@/$(id -u)/g" picframe.service \
| sudo tee /etc/systemd/system/picframe.service > /dev/null
sudo systemctl daemon-reload # make systemd read the new file
sudo systemctl enable picframe.service # start it at every bootThe app itself must be at /opt/picframe/picframe.py and the config at /etc/picframe/config.ini, as install.sh places them. Desktop autologin must also be on (sudo raspi-config nonint do_boot_behaviour B4), because the service draws on the logged-in desktop.
Check the file before starting it:
systemctl cat picframe # the file systemd actually uses
sudo systemd-analyze verify /etc/systemd/system/picframe.service # no output = no problemsYou don't need to reboot to test it. Start it and watch the log:
sudo systemctl start picframe
systemctl status picframe # should say "active (running)"; press q to exit
journalctl -u picframe -f # live log; Ctrl+C to stop watchingA healthy start looks like this:
INFO Display session ready (wayland-0)
INFO Photo directories ready: /mnt/photos
INFO Display power method: wlopm
INFO Screen 1920x1080 (x11)
INFO Found 2481 photos in /mnt/photos
The slideshow should now be on the screen. If status says activating (start-pre), the service is still in its waiting step (for the desktop or the share), which can take up to 120 s. The log shows what it's waiting for.
These checks confirm the frame looks after itself before you leave it alone. Each takes a minute or two.
| Test | How | Expected |
|---|---|---|
| Crash recovery | sudo systemctl kill -s KILL picframe |
The slideshow disappears and comes back about 10 s later. systemctl show picframe -p NRestarts counts up. |
| Boot | sudo reboot |
The slideshow appears after boot, without anyone logging in. journalctl -b -u picframe shows this boot's log. |
| NAS off at boot | Switch the NAS off (or unplug its network), then sudo reboot |
The Pi boots and shows the "Waiting for photos…" placeholder. Switch the NAS back on; photos appear within about a minute, without a reboot. |
| NAS drops while running | Switch the NAS off for a few minutes, then on again | The current photo stays up (or the placeholder appears). The slideshow resumes by itself. |
| Night mode | Set sleep_time to two minutes from now in /etc/picframe/config.ini |
The app restarts itself within 5 s of saving. At the set time the screen turns off (log: Display off (wlopm)). Put the real time back afterwards. |
| Config changes | Change delay in the config and save |
Log: Config file changed - restarting. The new delay applies. |
sudo systemctl restart picframe # restart (e.g. after changing fstab)
sudo systemctl stop picframe # stop until next boot (also turns the screen back on)
sudo systemctl disable --now picframe # stop and don't start at boot any more
sudo systemctl enable --now picframe # start now and at every boot again
journalctl -u picframe --since "1 hour ago" # recent logRunning it by hand for debugging: stop the service first, so two copies don't fight over the screen, then run it in the foreground with debug logging. Run this as the desktop user over SSH:
sudo systemctl stop picframe
XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0 \
python3 /opt/picframe/picframe.py --config /etc/picframe/config.ini -v
# Ctrl+C to quit, then: sudo systemctl start picframe- In Home Assistant, install a broker if you don't have one: Settings → Add-ons → Mosquitto broker. Then add the MQTT integration. Create a Home Assistant user for the frame, for example
picframe; the Mosquitto add-on accepts Home Assistant users as MQTT logins. - On the Pi, edit
/etc/picframe/config.ini:[mqtt] enabled = yes host = 192.168.1.20 username = picframe password = your-password
- Save the file. The app restarts itself within 5 seconds, and "Picture Frame" appears under Settings → Devices → MQTT with no YAML needed:
| 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+.