Use

Troubleshooting and recovery

On this page

Use this page when something is not working. Each entry starts with what you see, says why it happens, and then lists what to do. The most common problems come first in each group.

Find your symptom:

Most problems leave a line in the PHP error log. Look there first. On many servers that is the Apache error log. Lines that start frame:, frame admin: or frame photos: come from this system.

Server and admin

Every request answers 503 maintenance

You see: The admin or a frame gets {"error":"maintenance","code":"maintenance"} with HTTP 503 and a Retry-After: 30 header. The log has frame: frames.db is at schema N, code expects M.

Why: You deployed new code, and the database has not been brought up to date.

What to do:

  1. Run the migration as the web user:

    sudo -u www-data php /srv/frame/app/bin/migrate.php
    
  2. Check that it ends with schema at <number>. Run it again after every update of app/.

Frames and the admin recover on their own once the schema matches.

Every request answers 500 on a new install

You see: HTTP 500 with {"error":"internal error","code":"internal"} before you have run any command.

Why: The database does not exist yet.

What to do: Run the migration as above. Create your admin login next. See Install the frame server.

The server answers 500 "server not configured"

You see: HTTP 500 with {"error":"server not configured","code":"not_configured"}.

Why: config.php is missing, unreadable, or has a bad value. The log line says which, after frame: config error:.

What to do: Run the check as the web user and read the message:

sudo -u www-data php -r 'require "/srv/frame/app/bootstrap.php"; echo "ok\n";'
Message Fix
...config.php is missing or unreadable Copy config.example.php to config.php next to app/. Make it readable by the web user (group www-data, mode 0640).
unknown key: ... Remove or fix the key. See the configuration reference.
frames_url is required ... Set it, ending in /.
owner_name is required ... Set a short name: at most 40 characters, with no <, >, & or quotes.
calendar_hosts: each entry must be a lowercase host name Use lowercase host names only, as a list.
client_ip_header needs trusted_proxies ... Set trusted_proxies too, or remove the header.
kiosk_release_pubkey must be a PEM EC public key (needs the openssl extension) Install php-openssl support, or fix the key. It must be an EC P-256 public key in PEM form.
widgets.photos: unknown option ... Remove the option. See Photos: limits and config keys.

The admin answers 500

You see: https://frames.example.com/frames/admin/ shows HTTP 500, either as plain Apache or as the not_configured JSON.

Why: One of two things.

  • frames_url does not start with https://. The admin refuses to run and logs frame admin: refusing to run: frames_url must be https.
  • Apache does not allow the .htaccess files to work. The Apache error log says a directive "not allowed here".

What to do: Set frames_url to an https:// address. Check the AllowOverride line in your virtual host includes all four classes: AuthConfig FileInfo Indexes Options. See Install the frame server.

The login page says "The page expired"

You see: After you enter your details, the sign-in page shows "The page expired. Try again." The log has frame admin: login rejected origin or login rejected token.

Why: The sign-in form posts only when the request's origin matches the page, and the form's token matches its cookie. One of these failed:

  • The browser sent Origin: null. This happens when a response header Referrer-Policy: no-referrer is set. The admin sends Referrer-Policy: same-origin itself, but a header added by Apache or a proxy for the whole site can replace it.
  • A proxy or CDN changes the Host header, so it no longer matches the Origin.
  • You opened the page by another name than the host in frames_url, such as an IP address or www.. The sign-in page sends you to the frames_url host if the names differ.
  • The form was open for more than 15 minutes, or the browser blocks cookies for the site. The form's token is a cookie that lasts 15 minutes.
  • The site is not on HTTPS. The admin's cookies start with __Host-, so a browser stores them only over HTTPS.

What to do:

  1. Open the admin at exactly the address in frames_url, over HTTPS.
  2. Reload the sign-in page and try again.
  3. Look at the response headers (browser developer tools, Network tab). If Referrer-Policy says no-referrer, change your server or proxy to leave the header alone for /frames/admin/, or to use same-origin.
  4. If a proxy sits in front, make it pass the original Host header to Apache.

Sign-in failed

You see: "Sign-in failed. Check your details and try again."

Why: The user name, the password or the code is wrong. The message is the same for all three on purpose. A reused code also fails: each code works once.

What to do:

  1. Check the user name. Capital letters and surrounding spaces do not matter.
  2. If two-step sign-in is on, wait for the next code and enter it.
  3. Check your server's clock. Codes depend on it. Run NTP.
  4. If you cannot get in, reset from the shell: user.php passwd yourname, or user.php totp-disable yourname. See Use the admin.

Too many attempts

You see: "Too many attempts. Try again later."

Why: A lockout is active: 10 failed sign-ins from your address in 15 minutes, or 20 for the user name in an hour. A correct password is refused while the lockout lasts.

What to do: Wait for the window to pass. Behind a proxy, make sure client_ip_header and trusted_proxies are set, or all visitors share one address.

The admin shows "Signed out"

You see: A box that says "Your session has ended. Sign in again to continue."

Why: You were idle for 30 minutes, 12 hours passed since you signed in, or your password or two-step setting changed.

What to do: Select Sign in. Unsaved edits on that page are lost.

Changes I make do not reach the frame

You see: You saved a setting, and the frame still shows the old thing.

What to do:

  1. Wait a minute or two. Frames pick up changes on their next check-in.
  2. Check that the frame is Online on Frames.
  3. Use Screen and Refresh to see what the frame shows now.
  4. Check the page shows no "Unsaved changes" bar at the bottom.

Uploads

An upload fails with 413

You see: An upload fails with "Too large", or the response is HTTP 413 "Request Entity Too Large".

Why: A request is bigger than a limit. There are four places a limit can live: the app, the .htaccess files, PHP, and anything in front of Apache.

Upload Where the limit is Value
Photo in the admin public/admin/.htaccess and upload_bytes 30 MiB
Photo from a drop link public/.htaccess and upload_bytes 30 MiB
Card cover or stamp public/admin/.htaccess 4 MiB
Kiosk release public/admin/.htaccess 16 MiB
Anything else The default LimitRequestBody 16 KiB in the admin, 64 KiB elsewhere

What to do:

  1. Check post_max_size and upload_max_filesize are at least 31M (see PHP settings). Restart PHP-FPM or Apache after a change.
  2. If you copied the .htaccess rules into the virtual host, copy the <If> blocks too. They match on %{THE_REQUEST}, not REQUEST_URI, because Apache evaluates the request-size limit on an internal subrequest where REQUEST_URI is the front controller. A rule written against REQUEST_URI never matches, and the small default limit applies.
  3. If a proxy or CDN sits in front, check its upload limit too (for nginx, client_max_body_size).
  4. If you raised upload_bytes, raise LimitRequestBody in both .htaccess exceptions to match.

Photo uploads are off

You see: A red banner in the album: "Photo uploads are off. The server's image decoder isn't working, so new photos can't be added." Uploads stop with "Photo processing is unavailable on the server".

Why: The photo library's self-test failed. The cause is on the server, in the decoder or the sandbox.

What to do: Run the migration as the web user and read the WARNING (photos) lines:

sudo -u www-data php /srv/frame/app/bin/migrate.php

Photos: troubleshooting explains each line. On Ubuntu 24.04 the usual cause is that bwrap is blocked. Switch to 'sandbox' => 'user'. Frames keep showing photos already stored.

iPhone photos are refused

You see: "iPhone HEIC photos aren't supported on this server yet".

What to do: Install libheif-examples and libheif-plugin-libde265, then run the migration to check. See Photos: troubleshooting.

"The server is out of space" or "This album is full"

Why: The library reached library_bytes, the disk has less than min_free_bytes free, or the album reached album_photos.

What to do: Delete photos you no longer need, make room on the disk, or raise the limit in config.php. See Photos: limits and config keys.

Photos

A new photo does not show on the frame

Why: The frame shows a daily set, picked once a day at the frame's pick time. A photo added after the pick appears in the next day's set. A photo waiting for approval never reaches a frame.

What to do:

  1. Open the frame's Photos section. The contact sheet says when the set was picked and when the next pick is.
  2. Check the photo is approved, not under Waiting for approval.
  3. See How a frame picks its photos.

The Nextcloud album shows "Not fetched yet" or an error

You see: Under Album link: "Not fetched yet", or "Error: ...".

What to do:

  1. Check the cron job runs. Run it by hand as the cron user:

    php /srv/frame/app/fetch/fetch.php
    

    It prints nothing when all is well. cache/cache.db should exist.

  2. Check the Nextcloud host is in photo_hosts in config.php. If it is not, the admin refuses to save the link with "album_url must be a Nextcloud Photos public album link".

  3. Check the link looks like https://cloud.example.org/apps/photos/public/<token>.

  4. Check your server can reach the Nextcloud host, and that the album is shared with a public link.

You see: "album_url must be a Nextcloud Photos public album link".

Why: The link's host is not in photo_hosts, or the address is not a public album link.

What to do: Add the host in lowercase to photo_hosts and use the album's public link.

Calendar, weather, Home Assistant and Cards

A calendar shows Error

You see: In Library, the calendar's status is Error, with a message.

What to do:

  1. Check the calendar's host is in calendar_hosts, in lowercase. The admin refuses to save a calendar whose host is not listed ("host not allowed").
  2. Check the link is HTTPS with no port, and that it returns an iCal file. Nextcloud's /apps/calendar/p/<token> and /remote.php/dav/public-calendars/<token> forms are handled for you.
  3. Check the fetcher runs. Run it by hand as in the entry above.

The calendar widget shows nothing

Why: A calendar with nothing ticked shows no events. There is no "show all".

What to do: Open the frame's Calendar section, tick the calendars, and save.

Weather is missing or empty

What to do:

  1. Check the location is in the US. The National Weather Service has no data elsewhere.
  2. Check the frame's Weather widget is on and has a Location chosen.
  3. Check the fetcher runs. Weather refreshes about every 10 to 30 minutes.
  4. If you entered Stations, check each is a valid four-character ID, or clear the field to use the nearest three.

Home Assistant shows nothing or asks me to sign in

What to do:

  1. Check the address. http works only for private addresses (10.x, 172.16-31.x, 192.168.x) and .local names. Use https for anything else. The address has no path.
  2. Check the token is saved. The admin shows Token set. Changing the address removes the token, so enter it again.
  3. Without a token, Home Assistant must trust the frame's address. See Widgets: Home Assistant.
  4. The Dashboard list fills in after the frame reports its dashboards. Until then pick the default.

You see: "This link isn't working anymore. Ask for a new one." Photo drop links also say when a link has expired or been turned off.

Why: The link was replaced (New link), revoked, or has expired. A drop link lasts 90 days by default.

What to do: Open the sender link or drop link in the admin. Extend an expired drop link, Restore a revoked drop link, or make a New link and send it again.

A card sender sees "mailbox is full"

Why: The frame already holds 30 unopened cards, or the sender has 10 unopened ones.

What to do: Wait until someone opens cards on the frame, or delete unopened cards from the sender in the Cards section.

The frame

The frame never shows a pairing code

You see: The frame shows a "Can't reach ..." screen, or "Setting the clock...", or no pairing code.

What to do:

  1. Check the frame is on Wi-Fi. Hold the top-left corner for 10 seconds, enter the PIN, and open Wi-Fi.
  2. Check the frame can reach your server. The "Can't reach ..." screen shows the network name, address and frame ID.
  3. Check the kiosk was built with the right FRAME_SERVER_URL. It must match frames_url on the server, over HTTPS.
  4. If the site uses a Let's Encrypt certificate, check you installed the ISRG Root X1 certificate on the frame. Without it Android 6 cannot load the page. See Provisioning, step 4.
  5. If the screen says "Setting the clock...", the clock is wrong. See the next entry.

The pairing code is rejected

You see: "That code was wrong or has expired."

Why: The code was mistyped, it is older than 24 hours, or you entered five wrong codes in 10 minutes.

What to do: Read the code on the frame again (it can change), wait a few minutes if you hit the limit, and try again.

The frame's clock is wrong (it says 1969)

Why: The frame has no battery-backed clock. After a power cut it starts in 1969 until it reaches a time server.

What to do:

  1. Check you ran scripts/provision.sh time and installed the boot script. The boot script restores the last saved time.
  2. Check the NTP_SERVER is reachable from the frame's network.

The frame goes offline or has a weak signal

You see: Offline or a Weak Wi-Fi badge on Frames.

What to do: Move the frame or the access point. The badge shows below -75 dBm. Check the frame's Details for the network and signal. Reserve the frame's address in your DHCP server. The boot script holds its MAC address fixed.

The Dim button only makes the screen dark

Why: The boot script did not hand the backlight to the kiosk. Dim then only lowers brightness.

What to do: Check the boot script is installed and has run. Look for lcdbl -> uid N in adb logcat -d -s frame-fixmac. If you uninstalled and reinstalled the kiosk, reboot the frame. See Provisioning, step 9.

The Wi-Fi picker shows no networks

Why: Android 6 hides scan results when location is off.

What to do: The kiosk normally turns location on. If it did not:

adb shell settings put secure location_providers_allowed +network

The frame did not take an update

See Update the kiosk: failure states.

ADB

adb devices shows nothing

What to do: On WSL, attach the frame from PowerShell: usbipd list, then usbipd attach --wsl --busid <id>. A running frame shows as 1f3a:1001. A frame in FEL mode shows as 1f3a:efe8.

adb says the device is offline

Why: After adb reboot, an old network session is stale.

What to do: Run adb disconnect, then adb connect 192.0.2.50:5555 again.

I cannot connect over the network

Why: The boot script limits ADB over Wi-Fi to the addresses in ADB_ALLOW. If one entry is not a valid IPv4 address or range, the script allows nobody over Wi-Fi (it logs bad ADB_ALLOW entry). Wi-Fi ADB is also off on IPv6 when a list is set.

What to do:

  1. Connect over USB. USB ADB is never limited.
  2. Fix ADB_ALLOW in .env. Include your computer's address.
  3. Run scripts/provision.sh boot-script again, and reboot.

Several devices are connected

Add -s <serial> to adb and to scripts/provision.sh. Use the address (192.0.2.50:5555) for a network device.

Recovery

The frame does not boot

You see: The frame stays on a black screen, or on the boot logo, and never reaches the kiosk or the stock app.

What to do, in this order:

  1. Power-cycle it. Unplug the USB-C power, wait, and plug it in.
  2. Boot Android with the microSD card out. The FEL card makes RESET enter FEL mode. Take the card out and press RESET. See Unlock: first boot.
  3. If Android starts but the kiosk is wrong, connect with ADB (USB works even when Wi-Fi is limited) and fix it:
    • Reinstall the kiosk: adb install -r kiosk/build/frame-kiosk.apk.
    • Undo the boot script: restore the frame-fixmac.sh.v<N> backup and remove /system/etc/frame.conf.
    • Restore a .bak of a system file you changed, for example build.prop.bak.
  4. If Android does not start, put the frame in FEL mode and talk to its storage:
    1. Put the FEL microSD card in and press RESET. usbipd list shows 1f3a:efe8.
    2. Run scripts/boot-ums.sh in WSL.
    3. To undo the ADB patch only, run patch-sector.ps1 -Restore in PowerShell.
    4. To put the whole storage back, write backups/emmc-full.img to the disk. No script does this for you. It is the reverse of backup-emmc.ps1, and needs a raw disk write from an Administrator PowerShell. Double-check the disk number: this overwrites the whole disk.

The backup from the unlock is what makes step 4 possible. Keep it.

I lost the PIN

You see: The maintenance panel does not accept the PIN.

What to do:

  1. In the admin, open the frame, then Security. Enter a new PIN and select Set PIN, or select Reset to factory PIN to go back to the KIOSK_PIN from the build. The frame picks it up on its next check-in.
  2. The frame must reach your server to get a new PIN. If it is offline, get it back onto a network it knows first, then set the PIN in the admin.

A rebuilt kiosk with a new KIOSK_PIN changes only the factory PIN. It applies when the frame has no custom PIN. A custom PIN stays on the frame, through adb install -r, until you change or reset it in the admin.

After five wrong PINs the pad locks for 5 minutes, then 30 minutes, 2 hours and 8 hours for each further five. A reboot restarts a lock, so do not use it to try again.

I lost the APK keystore or the release key

Lost the release key. Frames keep running. You cannot send them self-updates any more, because they check the old public key.

  1. Make a new release key. See Provisioning, step 5.
  2. Build a kiosk with the new RELEASE_PUBKEY, signed with the same keystore.
  3. Install it on every frame with adb install -r.
  4. After that, self-updates work again.

Lost the APK keystore. Android refuses an update signed with another key, even over ADB. On every frame you must:

  1. Undo device owner.
  2. Uninstall the kiosk: adb shell pm uninstall com.calevans.frame.
  3. Make a new keystore and release key, and build the kiosk.
  4. Run Provisioning, step 7 again and claim the frame again.

Back up both keys somewhere private so this does not happen. See Update the kiosk.

A frame shows as Revoked

Why: Someone revoked the frame in the admin, which stops it working at once.

What to do: Open the frame, select More, then Restore. It asks for your password. If you revoked it because it was lost or stolen, leave it revoked.

I cannot sign in and need a new password or code

What to do: From a shell on the server, as the web user:

sudo -u www-data php /srv/frame/app/bin/user.php passwd yourname
sudo -u www-data php /srv/frame/app/bin/user.php totp-disable yourname

Both sign the user out of every browser. See Use the admin.

I need to restore the server

  1. Install the server as in Install the frame server.
  2. Put back config.php, and the contents of var/ from your backup: frames.db, photos/, notes/ and kiosk/.
  3. Run the migration as the web user.
  4. Frames reconnect on their own. If you restored an older frames.db, a frame claimed after that backup shows up as unclaimed. Claim it again.

See Back up the server.

Gotchas

Small things that cost time:

  • Use adb exec-out, not adb shell, to read binary files or compare bytes. shell turns \n into \r\n.
  • The frame's sed -i mangles replacement text with &. Rebuild the file from its .bak plus echo >> instead.
  • Do not run grep -r / on the frame. It hangs in /proc.
  • After adb reboot, run adb disconnect and adb connect again. The old session shows "device offline".
  • adb exec-out screencap -p > shot.png shows what the kiosk is displaying.
  • Keep a record of each change you make to a frame, with its undo.
  • Never attach 1f3a:1010 (U-Boot's disk) with usbipd, and cancel every "format this disk" prompt in Windows. See Unlock, step 5.
  • Do not run scripts/provision.sh steps out of order. Install the boot script after the kiosk, because it hands the backlight to the kiosk's user ID.
  • The .env reader is strict. A line ending in a carriage return, or an unknown key, stops every script that reads it.

Back to: Start here.