Victor Meunier

Engineering student

Revive your picture frame with a RPi Zero W

Few years back, I bought a digital picture frame for my mom. At that time, I thought it would be really cool to finally have a way to see all of our digital photos. Unfortunately, the hard truth is, after some time, we started to forget about it and the photos were always the same. Eventually, it started to be useless and we unplugged it.

I thought it would be cool if we were able to add photos easily to this device, and after some digging, I found out about gadgets. A gadget is a piece of code that enable a certain function for your Pi. There are many gadgets, but the USB Mass Storage got my attention. This gadget enables your pi to be seen as a USB Mass Storage, like a USB Stick. This was perfect as I would be able to plug the Pi directly to the digital frame !

So today, I'm going to show you how I converted my old digital frame to a new connected one, with a nice upload web interface.

This project will make use of the following principles :

  • Raspbian gadget - USB Mass Storage
  • Web developpement - Javascript and NodeJS
  • HTTP POST request

If you want to follow along I'll detail every step, or you can grab the finished code here on my Github.

Installing and setting up the gadget

1 - Enable the USB driver

We need to enable the USB driver which provides the gadget mode, by editing two configuration files.

                    sudo nano /boot/config.txt
                  

Scroll to the bottom and append the line below:

                    dtoverlay=dwc2
                  

Press CTRL+X to exit, type Y and ENTER to save.

Now go to the next config file :

                    sudo nano /etc/modules
                  

Append the line below :

                    dwc2
                  

Press CTRL+X to exit, type Y and ENTER to save.

2 - Creating a container for your files

Because we set our Pi as a USB Mass Storage, we have to create that storage space for the gadget. The following line will create a 4 GB file.

                    sudo dd bs=1M if=/dev/zero of=/piusb.bin count=4096
                  

We will then format the file as FAT32 so that the picture frame will be able to read it.

                    sudo mkdosfs /piusb.bin -F 32 -I
                  

3 - Mounting the file

First, you'll create a folder where you'll want your container to be mounted.

                    sudo mkdir /mnt/usb_share
                  

We'll then add it to fstab, to record it as an available partition.

                    sudo nano /etc/fstab
                  

Append the line below to the end of the file. Make sure to replace "/mnt/usb_share" by your container file name.

                    /piusb.bin /mnt/usb_share vfat users,umask=000 0 2
                  

Press CTRL+X to exit, type Y and ENTER to save.

Use the following command to mount the file without having to reboot :

                    sudo mount -a
                  

4 - The commands

You have two commands. One to enable the gadget, and the other to disable.

                    sudo modprobe g_mass_storage file=/piusb.bin stall=0 ro=1

                    sudo modprobe -r g_mass_storage
                  

Put an image inside your container file, plug your Pi to your picture frame with a Micro USB to USB cable and use the command to start to mount your file and dismount it. Wait a few seconds as the picture frame could take a bit of time to show you the image or the inside of the file. Make sure to connect the Pi through the USB port and not the PWR port. Also please do not power the Pi through it's power port while also using USB power !

If you saw the file, you're good to go. Otherwise, try switching input, or finding your file under the picture frame menu. Next part, we'll use NodeJS to make a small web app that let user add their own photos to the container file we've created.

The web app

Prerequesites

Before diving into developpement you need to setup your dev environment. To install node on your Pi, follow this link and there you'll find all the lastest release. I'll be using v9.9.0 in this tutorial.

You can also setup node on your Windows or macOS machine to ease out the developpement. Download page.

Client side

From there, I'll assume you're all setup and ready to program. I'll first show you the client side. I'll not get into the details of the HTML and CSS as it's not the point here. The layout and the functionnalities are actually pretty simple. The only interesting thing is the ability to do drag & drop. I think the code is self explanatory.

Here's the HTML :

                    <!doctype html>
                    <html lang="en">
                      <head>
                        

                        ZeroFrame
                        
                        

                        

                        

                        
                      </head>

                      <body>
                        
</body> </html>

We simply have a form with an input that accepts files and calls handleFiles() when a new file is added. Now let's take a look at the javascript that does all the magic !

                    function handleFiles(files) {
                      files = [...files];
                      filesToBeUploaded = filesToBeUploaded.concat(files);
                      numberOfFiles = filesToBeUploaded.length;
                      initializeProgress(files.length);
                      files.forEach(previewFile);
                    }
                  

Here, we first spread the files passed in parameters to an array called the same way. We also add the file to a variable filesToBeUploaded that'll keep track of the files we have to upload. The function initializeProgress() will take care of updating the progress bar and previewFile() will add a thumbail to the gallery element.

After the user open at least one file, the upload button becomes visible. When we click it, uploadFiles() is called.

                    function uploadFiles(){ 
                      progressBar.style.display = "inline-block";
                    
                      var url = 'http://192.168.0.16:8081/upload'
                      var xhr = new XMLHttpRequest();
                      
                      // First request to tell server how many files we're going to send
                      xhr.open('POST', url, true);
                      xhr.setRequestHeader("NumberOfFiles", filesToBeUploaded.length)
                      xhr.send();
                    
                      filesToBeUploaded.forEach(uploadFile);
                    }
                  

In this function, we create an HTTP POST request that'll be directed to our server. This is a very simple request where we set a custom header with the title NumberOfFiles and the actual number of files to be uploaded. I've used this technique because I send every file as a unique HTTP POST request. That way, the server knows of many files it should receive and can act accordingly. Of course, it would be better to send a multipart request with every file in it. At the end, I call uploadFile() for each file.

                    function uploadFile(file, i) {
                      var url = 'http://192.168.0.16:8081/upload'
                      var xh = new XMLHttpRequest();
                      
                      // Second request with the files
                      var xhr = new XMLHttpRequest();
                      xhr.open('POST', url, true);
                      xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
                      
                      var formData = new FormData();
                      // Update progress (can be used to show progress indicator)
                      xhr.upload.addEventListener("progress", function(e) {
                        updateProgress(i, (e.loaded * 100.0 / e.total) || 100)
                      })
                    
                      xhr.addEventListener('readystatechange', function(e) {
                        if (xhr.readyState == 4 && xhr.status == 200) {
                          updateProgress(i, 100)
                        }
                        else if (xhr.readyState == 4 && xhr.status != 200) {
                          // Error. Inform the user
                        }
                      })
                    
                      formData.append('file', file);
                      xhr.send(formData);
                    }
                  

In this function we'll create a an HTTP POST request for each file. We use formData to add the file and then send the request with the formData we created. The two Event listeners are added to monitor the uplaod progress and update the progress bar accordingly.

Now that we have the basics for our client page, we'll move on to the server.

Server side

I'll not get into the details of the server as it is a pretty classic architecture I'm building. The really interesting part is when it receive the POST request

                    // POST METHOD when user want to send something
                    } else if(req.method === "POST") {
                    // CHECK IF USER WANTS TO UPLOAD
                      if (req.url === "/upload") {
                        if(req.headers['numberoffiles']){
                          NumberOfFiles = req.headers['numberoffiles'];
                          console.log("Unmounting USB to upload new files..");
                          UnmoutUSB();
                        }
                        else{
                          var form = new formidable.IncomingForm();
          
                          form.parse(req, function (err, fields, files) {
                            if(err) throw err;
            
                            console.log("Uploading " + files.file.name);
            
                            fs.readFile(files.file.path, function(err, data){
                              if(err) throw err;
      
                              var fileName = files.file.name;
                              var directory = '/mnt/usb_share/';
      
                              fs.writeFile(directory + fileName, data, function(err){
                                if(err) throw err;  
                                console.log('File ' + fileName + ' was successfully saved.');
                              });  
                            });
                            FilesReceived++; 
                            if(FilesReceived == NumberOfFiles){
                              console.log("Mounting USB back..");  
                              MountUSB();
                              FilesReceived = 0;
                              NumberOfFiles = 0;
                            }   
                          });
                        }   
                      }
                    }
                  

We check the value of req.url, if it's "/upload" we then check the header to see if it's the special one we send to indicate the number of files. If it is, we simply get the number of files and call UnmountUSB(). That function will call the command line we saw earlier to dismount our container file.

If the header is not the special one we created, we'll just gather the data. For that, we'll use formidable, a great NPM package for NodeJS. We'll then read every file in our request and write them to the file container directory using fs. We incremente FilesReceived to keep track of the number of files written to the container. Once that number is the same as the one we read in the header, we simply mount the container back with MountUSB().

Here are the two functions to mount and unmount. Please note that they require the sudo-js package for NodeJS. This is to execute the command as SUDO.

                    function MountUSB(){
                      // Bash command to remount the USB
                      var command = ['modprobe', 'g_mass_storage', 'file=/piusb.bin', 'stall=0', 'ro=1'];
                      sudo.exec(command, function(err, pid, result){
                          console.log(result);
                      });
                    }
                  
                    function UnmoutUSB(){
                      // Bash command to unmount the USB
                      var command = ['modprobe', '-r', 'g_mass_storage'];
                      sudo.exec(command, function(err, pid, result){
                        console.log(result);
                      });
                    }
                  

Note that the command is actually an array of strings. So you should put every "word" of your command inside a separate array element.

Finished !

Now, you should have everything to revive your old digital picture frame ! Of course, you can find all the source code in the GitHub repo.

The web page needs alot of features, as for now, it's more a proof of concept. Few things that I'll try to add :

  • One multipart POST request with all the files.
  • A way to visualize what's already in the Pi and manage it.
  • Being able to supress files when their preview is loaded, before uploading.
  • Securing the page access with credentials to access it from the internet.
  • Connect to Google Photos' API to select photos from there.