← Back to Blogs
HN Story

Building a Privacy-First Digital Photo Frame with Lisp and M5Stack

May 8, 2026

Building a Privacy-First Digital Photo Frame with Lisp and M5Stack

In an era where most consumer electronics require a cloud subscription and the surrender of personal data, the desire for a simple, private way to share family memories is surprisingly rare. Commercial digital photo frames often come with high price tags and mandatory accounts that risk leaking private photographs to the internet or using them to train AI models.

To solve this, developer chrisjj built a custom digital photo frame using the M5Stack Tab5 and uLisp. By shifting the logic to a Lisp-based firmware and hosting images on a private server, the project achieves a balance of high-resolution display and total data sovereignty.

Hardware and Display Specifications

The project centers on the M5Stack Tab5, which features a high-resolution display of 1280 x 720. This provides a widescreen 16:9 aspect ratio and excellent color rendition, making it a viable alternative to expensive commercial frames.

To handle the images, the author utilized a custom GIF decode/encode extension for uLisp. Because the hardware has specific memory and processing constraints, photos are formatted as 256-color GIFs using 75% diffusion dithering. This process, often handled via Photoshop or online conversion tools, keeps typical photograph sizes around 500KB while maintaining visual quality that is virtually indistinguishable from the originals.

System Architecture

The photo frame operates as a networked client that fetches a list of images from a remote website and cycles through them. This architecture allows the owner to update the photo gallery remotely and grant upload access to friends and family without requiring a complex app ecosystem.

The Backend

The images are served via a simple HTTP Content-Management System (CMS) at www.wackyimage.com. The site consists of a protected page containing a list of image tags:

<body>
<p>Insert photos here! GIF format, image size 1280 x 720.</p>
<p><img src="/pictures/k58/sleepycat.gif" alt="SleepyCat.gif" /></p>
<p><img src="/pictures/k58/wedding.gif" alt="Wedding.gif" /></p>
</body>

The Lisp Implementation

The firmware is written in uLisp and follows a linear execution path: connection, collection, and display.

1. Network Connectivity

The device initializes its connection to the local Wi-Fi network using the wifi-connect function, followed by a 5-second delay to ensure the handshake is complete:

(wifi-connect "Mynetwork" "mypassword")
(delay 5000)

2. Fetching the Image List

The collect-pics-list function handles the HTTP request. It uses a login cookie for security and a helper function, skip-headers, to bypass the HTTP response headers and reach the HTML body.

(defun collect-pics-list nil
  (with-client (s "www.wackyimage.com" 80)
    (format s "GET /show?K5A HTTP/1.0~a~%" #\Return)
    (format s "Host: www.wackyimage.com~a~%" #\Return)
    (format s "Cookie: LOGIN=WI1NDlkNmE4YzBmZmE4MTQyYzNjODk2YzRkZDEwMiIp~a~%" #\Return)
    (format s "Connection: close~a~%" #\Return)
    (format s "~a~%" #\Return)
    (skip-headers s)
    (collect-pics s))))

3. Parsing and Display

The collect-pics function scans the HTML for specific directory patterns (/k58/) and extracts the filenames. Once the list is compiled, display-pic fetches the individual GIF file and passes the stream to the decode-gif function for rendering on the M5Stack screen.

The Main Execution Loop

The entire process is wrapped in a photo-frame function that manages the rotation logic. It tracks the number of available pictures to ensure it doesn't attempt to access a non-existent index and uses a configurable *minutes* variable to control the transition speed.

(defun photo-frame ()
  (wifi-connect "Mynetwork" "mypassword")
  (delay 5000)
  (let ((nextpic 0) (lastpics 0))
    (loop
     (let* ((pics (collect-pics-list))
            (npics (length pics)))
       (when (> npics lastpics)
         (setq nextpic lastpics)
         (setq lastpics npics))
       (display-pic (nth (mod nextpic npics) pics)))
     (incf nextpic)
     (delay (* 1000 60 *minutes*)))))

To ensure the device functions as a standalone appliance, the author uses uLisp’s resetautorun feature via (save-image 'photo-frame), which triggers the loop automatically upon power-up.

Conclusion

By combining the flexibility of Lisp with affordable M5Stack hardware, this project demonstrates that functional, high-resolution home appliances can be built without relying on invasive cloud services. It transforms a simple microcontroller into a dynamic, remotely-updatable gallery while maintaining strict control over personal data.

References

HN Stories