Skip to content

mkhnsn/remote-mass-storage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

remote-mass-storage

Provision a Raspberry Pi Zero 2 W as a remotely-updateable USB mass storage device.
The Pi pretends to be a USB thumb drive (via USB Gadget Mode), while you update its contents over the network (Ansible for provisioning, rsync/SSH for day-to-day sync).

This is the prototype version: a single-image virtual USB drive that updates on command. Future versions may add A/B double-buffering, idle-aware swaps, and remote triggers.


Features

  • Pi Zero 2 W appears as a USB drive (FAT32) to any host (CNC controller, PC, etc.)
  • Contents come from a folder on the Pi: ~/file_sync
  • Update script:
    • Temporarily “unplugs” the virtual USB from the host (unbinds the gadget)
    • Mounts /usbdrive.img locally on the Pi
    • Syncs files from ~/file_sync into the image
    • Unmounts the image
    • “Replugs” the drive to the host (re-binds the gadget), so the host sees updated contents
  • Fully managed and reproducible via Ansible for initial provisioning
  • No hand-editing on the Pi after the initial USB-gadget enable step

Note: The host filesystem is FAT32. The design assumes the host behaves as read-only (only reads files, does not write back). Writes from the host are not relied on or tested in this prototype.


Prerequisites

  • Raspberry Pi Zero 2 W
  • Raspberry Pi OS Lite (64-bit recommended)
  • Wi-Fi configured & SSH enabled
  • Your workstation must have:
    • git
    • ansible
    • ssh and rsync

One-time: Enable USB Gadget Mode on the Pi

These two edits must be done once on the Pi before running Ansible.

Edit /boot/firmware/config.txt

sudo nano /boot/firmware/config.txt

Add this line at the bottom, after [all]:

dtoverlay=dwc2,dr_mode=peripheral

Edit /boot/firmware/cmdline.txt

sudo nano /boot/firmware/cmdline.txt

/boot/firmware/cmdline.txt is a single long line.

Find rootwait and insert modules-load=dwc2 right after it, separated by a space.

Example before:

console=serial0,115200 console=tty1 root=PARTUUID=... rootfstype=ext4 fsck.repair=yes rootwait quiet splash

Example after:

console=serial0,115200 console=tty1 root=PARTUUID=... rootfstype=ext4 fsck.repair=yes rootwait modules-load=dwc2 quiet splash

Reboot to apply:

sudo reboot

Ansible Structure

ansible/
  inventory/
    hosts.ini
  playbooks/
    deploy.yml
  files/
    usb-gadget-setup.sh
    update_usb_image.sh
    gadget-init.service

Inventory

Edit ansible/inventory/hosts.ini:

[masso_sync]
masso-sync.local ansible_user=pi

Adjust hostname / user as needed for your setup.


Deploy to the Pi

From the repo root:

cd ansible
ansible-playbook -i inventory/hosts.ini playbooks/deploy.yml

The playbook will:

  • Install required packages (e.g. rsync)
  • Create /usbdrive.img if missing (FAT32, ~4 GB by default)
  • Create ~/file_sync
  • Install:
    • /usr/local/bin/usb-gadget-setup.sh
    • /usr/local/bin/update_usb_image.sh
    • /etc/systemd/system/gadget-init.service
  • Enable + start gadget-init.service so the USB gadget appears automatically on boot

usb-gadget-setup.sh uses configfs (/sys/kernel/config/usb_gadget) to:

  • Define the USB gadget (pi)
  • Attach a mass_storage function to /usbdrive.img
  • Bind it to the Pi’s USB device controller (/sys/class/udc/3f980000.usb on Zero 2 W)

Using the Virtual USB Drive

Hardware wiring:

  • PWR IN micro-USB port → 5 V power (no data)
  • USB micro-USB port → host (CNC controller, Mac/PC, etc.) using a data-capable cable

Once the Pi boots:

  1. The gadget service (gadget-init.service) runs and binds the USB gadget.
  2. The host should detect a USB mass storage device with a FAT32 filesystem.
  3. The volume name will be whatever the FAT label is set to (you can rename it in Finder / Windows / etc., e.g. sync-files).

On the host, this looks like a normal thumb drive; on the Pi, it is backed by /usbdrive.img.


Updating USB Drive Contents

Source folder on the Pi:

/home/pi/file_sync

Typical workflow:

  1. Copy or sync your files into ~/file_sync.

  2. Trigger an update:

    sudo /usr/local/bin/update_usb_image.sh

The update script will:

  1. Read the current gadget binding from /sys/kernel/config/usb_gadget/pi/UDC
  2. If bound, unbind the gadget (host sees USB device disappear)
  3. Loop-mount /usbdrive.img at /mnt/file_sync_image
  4. rsync from ~/file_sync//mnt/file_sync_image/ with:
    • recursive copy
    • timestamps preserved
    • no attempt to change ownership / permissions (FAT32 doesn’t support them)
  5. Unmount /mnt/file_sync_image
  6. Re-bind the gadget to the original UDC (host sees USB device re-appear with updated files)

From the host’s perspective, this is equivalent to unplugging the stick, rewriting files on a PC, and plugging it back in.


Example workstation helper script

#!/usr/bin/env bash
set -euo pipefail

PI_HOST="pi@masso-sync.local"
REMOTE_DIR="/home/pi/file_sync"

if [ $# -lt 1 ]; then
  echo "Usage: $0 <file-to-send>"
  exit 1
fi

FILE="$1"
BASENAME="$(basename "$FILE")"

echo "⌁ Copying '$FILE' to ${PI_HOST}:${REMOTE_DIR} ..."
rsync -av --progress "$FILE" "${PI_HOST}:${REMOTE_DIR}/"

echo "⌁ Triggering USB image refresh..."
ssh "$PI_HOST" "sudo /usr/local/bin/update_usb_image.sh"
echo "✓ Done. Updated USB image now visible to host."

Future Improvements (planned)

Not implemented yet in this prototype:

  • A/B dual-image double-buffering
  • Automatic refresh only when the host is idle
  • Syncthing / Dropbox / S3-style auto-sync
  • Web UI (local dashboard)
  • “Force refresh” HTTP endpoint
  • Pi self-updating via ansible-pull
  • Smarter safety interlocks (e.g., only refresh when host is not accessing the fs)

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors