Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Echo Wireless Transfer

An open-source Raspberry Pi bridge that lets you pull studies off an older USB-only echocardiography (or any old USB-only medical imaging) machine — with no internet, no USB shuffling, in near real time — onto your Windows + macOS workstations.

Plug a tiny Pi Zero 2 W into the echo's USB-A port. The echo sees it as a normal USB flash drive. Studies the echo writes to "USB" appear on the room's PCs as a Wi-Fi network share within seconds.


Table of contents


What it solves

Common clinic situation:

  • The echo / ultrasound machine is old; it has no internet, or its network port is locked behind an admin password no one in the clinic still knows
  • The DICOM/PACS network config is similarly locked
  • Every study ends with someone walking a USB stick from the machine to a workstation
  • The device is 2010–2018 vintage — it has USB 2.0 host ports, but that's the only way out

This project replaces the USB stick with a tiny Raspberry Pi Zero 2 W that:

  1. Presents itself to the echo as a normal USB flash drive
  2. Lets the echo "Save to USB" / "Export" exactly as it always did
  3. Auto-sorts whatever the echo wrote into dicom/ / images/ / video/ + date-stamped folders
  4. Exposes those folders as a Samba network share over Wi-Fi (\\echo-bridge\studies) — accessible from both Windows and macOS in the exam room
  5. (Optional) Posts each new file to your own web app's ingest endpoint

No USB shuffling. No cloud. The device's firmware doesn't change. From the device's point of view, the Pi is just "a well-behaved USB stick".


How it works

 ┌──────────────────┐  USB-A (host)       ┌──────────────────┐  Wi-Fi  ┌──────────────────┐
 │  Echo / US dev.  │ ── one cable ─────► │  Pi Zero 2 W     │ ──────► │ Windows Mobile   │
 │  USB host port   │   USB-A↔micro-USB   │  • dwc2 gadget   │         │ Hotspot (room)   │
 └──────────────────┘   5V + USB 2.0 data │  • loopback mnt  │         │   │              │
                                          │  • Samba         │         │   ├─► MacBook    │
                                          │  • sync daemon   │         │   └─► Windows PC │
                                          │  • (opt) POST    │         └──────────────────┘
                                          │    to web app    │
                                          └──────────────────┘

Data flow:

  1. A 16 GB FAT32 image file lives on the Pi (/srv/echo/usb.img)
  2. The kernel's g_mass_storage gadget module presents that file to the host (echo) as a real USB stick
  3. The echo writes its DICOM / AVI / JPG into the image as normal
  4. When the echo ejects (or stops writing for long enough), the Python sync daemon kicks in
  5. The daemon read-only loopback-mounts the image and finds new files
  6. New files are classified into dicom/ / images/ / video/ / unknown/ by extension first, magic bytes second (so extension-less DICOM exports like Q65DPJ80 or IM00012 still land in dicom/), and placed under a YYYY-MM-DD/ subfolder
  7. That same folder is published over Samba as \\echo-bridge\studies
  8. Windows and macOS open it as a normal network drive

The daemon never touches the image while a host is connected (it watches the UDC's state file). The host is never disconnected mid-write.


Hardware

Item Notes Approx. price (TL)
Raspberry Pi Zero 2 W The brain. Second-hand is fine (~1,000–1,500 TL). New stock is intermittent. Must be "Zero 2 W", not the older "Zero W" — the SoC silkscreen reads BCM2710A1. 1,000–2,500
microSD card, 16 GB+ A2 / U3 strongly recommended (SanDisk Extreme / Samsung Evo Plus). 16 GB minimum, 32 GB more comfortable. 250–500
USB-A ↔ micro-USB cable Must be a data cable, not charge-only. ~50 cm is plenty. Quick sanity check: plug it between your phone and a PC — does file transfer work? 50–100
(Optional) second cable or GPIO power lead For continuous power. Single-cable operation works too, but the Pi loses power as soon as you unplug from the echo. 50–100
(Optional) 5 V USB-A power adapter Use if the echo's USB port is undersupplied, or if you want the Pi up regardless of the echo. Plugs into the Pi's PWR IN (left micro-USB) via a second cable. 100–300
(Optional) passive heatsink / aluminium case The Pi Zero 2 W stays under ~75 °C on this workload, but a case is hygienic and tidy in a clinical setting. 30–500

Minimum total: ~1,500–2,500 TL.

Will Pi 4B / Pi 5 work? Yes, but they only have one USB-C port that doubles as power and data. To use it for gadget mode you have to inject 5 V via the GPIO pins (bypassing the polyfuse). The Pi Zero 2 W has two physically separate micro-USB ports (PWR IN and USB/OTG), which makes the wiring trivially clean.

Will LattePanda / an x86 mini PC work? No. Intel xHCI USB controllers don't support device/gadget mode — they cannot physically present themselves as a USB drive to the echo. An ARM Pi is required.


Software prerequisites

On your workstation (MacBook or Windows)

Tool Install
Raspberry Pi Imager macOS: brew install --cask raspberry-pi-imager
Windows: download from raspberrypi.com
SSH client macOS / Linux: built-in (Terminal.app)
Windows 10/11: built-in (ssh in Command Prompt or PowerShell)
(macOS) SMB client Built in (Finder → ⌘+K)
(Windows) Mobile Hotspot Built into Win10/11 (Settings → Network → Mobile Hotspot). Will host the Wi-Fi network the Pi joins.

On the Pi

Raspberry Pi OS Lite (64-bit, Bookworm). The repo's install.sh handles the rest — apt packages, dwc2 overlay, Samba config, sync daemon, systemd unit.


Setup — step by step

This guide assumes macOS + Windows hotspot. If you want the Pi to join your clinic's real Wi-Fi instead, substitute that SSID/password in step 2.

1) Set up Mobile Hotspot on a Windows PC

On a Windows 10/11 PC:

  1. Settings → Network & Internet → Mobile Hotspot
  2. Edit → set SSID and password (e.g. echo-net / Pi12345678). Write the password down.
  3. Power saving: OFF (important — the hotspot must stay up even when no client is talking)
  4. Frequency: 2.4 GHz (the Pi Zero 2 W has no 5 GHz radio)
  5. Mobile Hotspot: ON

This hotspot is the LAN the Pi, MacBook and Windows PC will share.

Alternative: macOS Internet Sharing (System Settings → Sharing → Internet Sharing). Works fine but goes down when the Mac sleeps — the Windows route is more reliable.

2) Flash Pi OS Lite to the SD card

Open rpi-imager on the MacBook:

  1. CHOOSE DEVICERaspberry Pi Zero 2 W
  2. CHOOSE OS → Raspberry Pi OS (other) → Raspberry Pi OS Lite (64-bit)
  3. CHOOSE STORAGE → the SD card (double-check you've picked the right disk)
  4. NEXTEDIT SETTINGS

General tab:

  • Hostname: echo-bridge
  • Username: pi, Password: a strong password (save it — you'll need it for every sudo)
  • ☑ Configure wireless LAN
    • SSID: your Windows hotspot SSID
    • Password: hotspot password
    • Country: TR (or whatever applies)
  • ☑ Set locale: Europe/Istanbul, keyboard tr (or your locale)

Services tab:

  • ☑ Enable SSH → Use password authentication

SAVEYESYES. Writing takes ~5–10 minutes.

3) Pop the SD into the Pi and power it on

  • Slot the SD into the Pi Zero 2 W
  • Plug a USB-A↔micro-USB cable into the left micro-USB port (PWR IN)
  • Plug the other end into any 5 V USB source (phone charger, MacBook USB-A port, etc.)
  • Make sure the Windows Mobile Hotspot is ON

The Pi boots. The green ACT LED blinks erratically during boot. Wait ~60 seconds.

4) SSH in from the MacBook

In the MacBook's Terminal:

ssh pi@echo-bridge.local
  • Are you sure you want to continue connecting? yes
  • Password: the one you set in rpi-imager (no characters echoed, normal)

If it works:

pi@echo-bridge:~ $

If .local doesn't resolve: find the Pi's IP from the Windows hotspot's "Connected devices" list (usually 192.168.137.X) and ssh pi@192.168.137.X.

If you get Connection refused: the Pi may still be running its first-boot config; wait another 30–60 seconds and try again.

5) Send the project files to the Pi

In a new MacBook terminal tab (don't close the SSH session):

git clone https://github.com/afstudy20-gif/echo-wireless-transfer.git
cd echo-wireless-transfer
ssh pi@echo-bridge.local 'mkdir -p ~/pi-bridge'
scp install.sh echo-sync echo-sync.service smb.conf pi@echo-bridge.local:~/pi-bridge/

6) Run install.sh on the Pi

Back in the SSH session:

sudo bash ~/pi-bridge/install.sh

What it does (~5–10 minutes):

  • apt update + upgrade
  • Installs Samba + Python + utility packages
  • Adds dtoverlay=dwc2,dr_mode=peripheral to /boot/firmware/config.txt (fixes the wrong section if needed)
  • Adds modules-load=dwc2 to /boot/firmware/cmdline.txt
  • Allocates a 16 GB FAT32 backing image at /srv/echo/usb.img
  • Creates the Samba user echothe smbpasswd prompt will ask you to set the password; this is what Windows and macOS use to connect
  • Installs echo-sync to /usr/local/bin/
  • Installs the echo-sync.service systemd unit (Restart=always)

The script prints "DONE" and the next steps at the end.

"No space left on device" error: the backing image doesn't fit on your SD. Drop IMG_SIZE_GB from 16 to 8 (or smaller):

sudo rm -f /srv/echo/usb.img
sudo sed -i 's/IMG_SIZE_GB=16/IMG_SIZE_GB=8/' ~/pi-bridge/install.sh
sudo bash ~/pi-bridge/install.sh

7) Reboot

sudo reboot

SSH drops (expected). The Pi comes back in ~60 seconds.

8) Verify gadget mode

SSH back in:

ssh pi@echo-bridge.local

Then:

lsmod | grep dwc2
ls /sys/class/udc/
sudo systemctl status echo-sync --no-pager | head -10

Expected:

  • dwc2 module is loaded
  • /sys/class/udc/ lists at least one device (e.g. 3f980000.usb)
  • echo-sync.service is active (running)

If /sys/class/udc/ is empty, your dtoverlay=dwc2 line may have ended up under the wrong section ([cm5], [pi4], etc.) of config.txt. Fix:

sudo sed -i '/^dtoverlay=dwc2/d' /boot/firmware/config.txt
echo 'dtoverlay=dwc2,dr_mode=peripheral' | sudo tee -a /boot/firmware/config.txt
sudo reboot

9) MacBook test — does the gadget really appear?

Unplug the Pi from the power source. Plug a USB-A↔micro-USB cable into the Pi's OTG port (right micro-USB, the one closer to the HDMI) and into a USB port on the MacBook.

In Terminal on the Mac:

diskutil list | grep ECHO

Expected output:

0:    ECHO    *17.2 GB   disk4

→ The gadget is working. Finder will also show a drive called ECHO.

10) SMB share test

On the Pi:

sudo -u echo bash -c 'echo "smb hello" > /srv/echo/share/dicom/_smbtest.txt'

On the MacBook (Finder):

  • ⌘+Ksmb://echo-bridge.local/studies → Connect
  • Username: echo, Password: the SMB password you set in step 6
  • ☑ Remember in keychain
  • Open dicom/ → you should see _smbtest.txt

On Windows (File Explorer):

  • Address bar: \\echo-bridge\studies
  • Username: echo, Password: the SMB password
  • Same folder

11) Plug the Pi into the echo — the real test

Take the Pi to the echo machine:

  1. Plug a USB-A↔micro-USB cable between the Pi's OTG port (right micro-USB) and the echo's USB-A port
  2. The echo enumerates the Pi as a USB stick. ~10–20 seconds.
  3. Open the echo's "Save to USB" / "Export" menu — it should show ECHO with ~16 GB free
  4. Record a test study, export it to USB
  5. Press "Eject USB" / "Safely Remove" on the echo (if it has one)
  6. Wait ~30–60 seconds — the sync daemon will copy the new files
  7. From the MacBook, open the SMB share — the echo's files should appear under dicom/2026-XX-XX/ or video/2026-XX-XX/

If the echo has no eject button: finish the export, wait ~60 seconds (the daemon needs the UDC to drop out of configured state via mtime idle), then unplug. Unplugging kills the Pi but the sync has already happened — your files are safe in the share.


Verification

Test Command / action Expected
Gadget loaded? lsmod | grep g_mass g_mass_storage is listed
UDC registered? ls /sys/class/udc/ At least one device (e.g. 3f980000.usb)
Host attached? cat /sys/class/udc/*/state configured (host is present) or not attached
Daemon running? sudo systemctl is-active echo-sync active
Daemon log sudo journalctl -u echo-sync -n 20 daemon up; watching ... at boot, then quiet while a host is attached
Samba running? sudo systemctl is-active smbd active
Samba user exists? sudo pdbedit -L echo:... line
Backing image ls -lh /srv/echo/usb.img A 16 GB file
Share layout ls /srv/echo/share/ dicom/, images/, video/, unknown/

Troubleshooting

SSH says Connection refused or Host unreachable

  • The Pi may not be fully booted yet; wait another 60 seconds
  • Is the Windows hotspot up? Is the Pi listed in "Connected devices"?
  • Skip ping echo-bridge.local and SSH straight by IP (mDNS is flaky on Windows hotspots)

dwc2 doesn't load / no UDC

  • cat /boot/firmware/config.txt | grep dwc2 should show dtoverlay=dwc2,dr_mode=peripheral, not dr_mode=host
  • If it's nested under a [cm5] or other model-specific section, move it under [all] (the model-specific sections don't apply to the Pi Zero 2 W)
  • Fix:
    sudo sed -i '/^dtoverlay=dwc2/d' /boot/firmware/config.txt
    echo 'dtoverlay=dwc2,dr_mode=peripheral' | sudo tee -a /boot/firmware/config.txt
    sudo reboot

The echo doesn't see the ECHO drive

  • Could be a charge-only cable — plug it between your phone and a PC and confirm file transfer works
  • Unplug from the echo and replug — some older devices fail enumeration the first time
  • 16 GB is too large for your echo: shrink IMG_SIZE_GB to 8 in install.sh and rebuild
  • Some older devices want FAT16 instead of FAT32:
    sudo systemctl stop echo-sync
    sudo rm /srv/echo/usb.img
    sudo fallocate -l 8G /srv/echo/usb.img
    sudo mkfs.vfat -F 16 -n ECHO /srv/echo/usb.img
    sudo systemctl start echo-sync

Echo writes, but files don't show up in the share

  • sudo journalctl -u echo-sync -n 50 — do you see copied … lines?
  • If the UDC is still configured, the daemon is correctly waiting. Press eject on the echo or unplug
  • After the sync, refresh the SMB mount on the client: unmount and remount

"Low voltage warning" on the Pi

  • The echo's USB port may be under-supplying (especially pre-2010 devices)
  • Fix: plug a 5 V/1 A wall adapter into the Pi's PWR IN (left micro-USB) via a second cable
  • Or feed 5 V through GPIO pin 2 / 4 + GND on pin 6

I plug the Pi into the MacBook but ECHO doesn't appear

  • Are you using the Pi's OTG port (right, closer to HDMI)? PWR IN will never do gadget mode
  • lsmod | grep g_mass — is g_mass_storage loaded?
  • cat /sys/class/udc/*/state — should be configured while the Mac is plugged in
  • Is the cable a data cable? Charge-only cables silently fail to enumerate

The echo writes extension-less files (e.g. Q65DPJ80, IM00012)

  • These are real DICOM files; the daemon's magic-byte classifier picks them up automatically by detecting the DICM signature at byte offset 128 and puts them in dicom/
  • If a file still lands in unknown/, inspect its first 132 bytes — sudo head -c 132 <file> | xxd — bytes 128–131 should be 44 49 43 4d. If they aren't, the file is not Part 10 DICOM (some very old raw exports skip the preamble entirely)
  • Magic bytes for JPG/PNG/BMP/TIFF/GIF and AVI/MP4/MOV/MKV/FLV are also detected as fall-backs for extension-less image / video files
  • Manual one-off fix if you want to move an old pre-magic-byte batch: sudo mv /srv/echo/share/unknown/2026-XX-XX/* /srv/echo/share/dicom/2026-XX-XX/

Configuration

echo-sync is configured via environment variables, set on the systemd unit (/etc/systemd/system/echo-sync.service, Environment= lines).

Variable Default Meaning
ECHO_IMG /srv/echo/usb.img Backing FAT32 image path
ECHO_SHARE /srv/echo/share Samba share root
ECHO_DB /var/lib/echo-bridge/synced.db SQLite dedupe ledger
ECHO_IDLE_SECONDS 30 How long the image's mtime must be quiet before triggering a sync
ECHO_POLL_INTERVAL 5 How often the daemon polls mtime / UDC state
ECHO_SHARE_USER echo User who owns the files in the share
ECHO_WEBAPP_URL (empty) If set, POST each new file to this URL
ECHO_WEBAPP_TOKEN (empty) Bearer token for the POST
ECHO_WEBAPP_FIELD file multipart form field name for the file
ECHO_ORTHANC_URL (empty) If set, POST each DICOM file to <URL>/instances
ECHO_ORTHANC_USER (empty) Orthanc HTTP Basic username
ECHO_ORTHANC_PASSWORD (empty) Orthanc HTTP Basic password

After changing anything:

sudo systemctl daemon-reload
sudo systemctl restart echo-sync

Optional: auto-push to a web app

If you already have a web app that ingests DICOM / echo files, the daemon can POST every new file straight to it.

Uncomment the relevant Environment= lines in /etc/systemd/system/echo-sync.service:

Environment=ECHO_WEBAPP_URL=https://your-app.example.com/api/ingest
Environment=ECHO_WEBAPP_TOKEN=changeme
Environment=ECHO_WEBAPP_FIELD=file

Install the requests Python library on the Pi:

sudo pip3 install --break-system-packages requests
sudo systemctl daemon-reload
sudo systemctl restart echo-sync

After each sync, the daemon POSTs new files as multipart/form-data:

  • Fields: file (the file), modality (dicom/images/video), sha1 (for idempotency on your side)
  • Header: Authorization: Bearer <token> (if ECHO_WEBAPP_TOKEN is set)

Optional: push DICOMs to an Orthanc PACS

If you already run (or want to run) an Orthanc PACS server in the clinic — on the Windows PC, on a Mac, on a NAS, in Docker, anywhere reachable on the LAN — echo-sync can POST every DICOM file it captures straight into Orthanc. From there you get indexing, search, OHIF/web viewer, and DICOM C-STORE forwarding for free.

Quick setup, on the Pi:

sudo systemctl edit echo-sync

Drop in:

[Service]
Environment=ECHO_ORTHANC_URL=http://192.168.137.10:8042
Environment=ECHO_ORTHANC_USER=orthanc
Environment=ECHO_ORTHANC_PASSWORD=changeme

Then:

sudo systemctl daemon-reload
sudo systemctl restart echo-sync

The daemon will POST every file classified as DICOM (extension .dcm/.ima or magic-byte sniffed) to <ORTHANC_URL>/instances as application/dicom, with HTTP Basic auth. It records pushed_orthanc=1 per file in its SQLite ledger so it never double-sends.

The Orthanc REST call uses only Python's stdlib urllib, so no extra pip dependency is required.

Full walk-through (where to install Orthanc, how to verify, how to reset the ledger if you nuke Orthanc) is in docs/orthanc.md.


Optional: harden SSH to key-only auth

The default rpi-imager configuration leaves password SSH enabled. For a device that holds PHI, switch to key-only authentication with the included harden-ssh.sh script. It's idiot-proofed: it verifies your key works and that sshd -t is happy before it disables the password fall-back, and keeps a backup so you can roll back.

One-time setup:

# 1) Push your key to the Pi (uses password one last time)
ssh-copy-id pi@echo-bridge.local

# 2) Sanity-check key auth works
ssh -o PasswordAuthentication=no pi@echo-bridge.local 'echo OK'

# 3) Disable password auth on the Pi
scp harden-ssh.sh pi@echo-bridge.local:~/
ssh pi@echo-bridge.local 'sudo bash ~/harden-ssh.sh'

The script asks for confirmation and then installs /etc/ssh/sshd_config.d/10-echo-bridge-hardening.conf with:

PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PermitRootLogin prohibit-password

To roll back if anything goes wrong:

ssh pi@echo-bridge.local 'sudo bash ~/harden-ssh.sh --rollback'

Or, if you're already locked out, pop the SD card into another machine and either edit /etc/ssh/sshd_config.d/10-echo-bridge-hardening.conf back to allow passwords, or delete it.

Full walk-through (including key generation, lock-out recovery, and optional further hardening) is in docs/ssh-key-auth.md.


Security notes

This bridge handles medical imaging (PHI). For real-world use, harden it:

  • Replace the SSH password with a key — see Optional: harden SSH to key-only auth above. There's a one-shot harden-ssh.sh in this repo
  • Use a strong Samba password — Win/Mac clients will remember it in their keychain, you only type it once
  • Encrypt the SD card with LUKS on /srv/echo (planned for v2 — currently manual)
  • Hotspot client isolation: some Windows builds block client-to-client traffic on Mobile Hotspot. Verify Pi ↔ Win PC ping works
  • Keep the firmware current: sudo apt update && sudo apt full-upgrade -y once a month
  • Retention policy: decide how long the backing image and share keep PHI, and who has access. Write it down

Legal / disclaimer

This project is released under the MIT license, "as-is", without any warranty. It is not a registered medical device (no FDA / CE / equivalent clearance). If you want to use it clinically:

  • Get sign-off from your institution's information security and compliance team
  • Encrypt patient data at rest
  • The device's firmware is never modified by this bridge, but verify against the vendor's warranty terms anyway
  • Set up backup and retention policies

License

MIT

Contributing

Pull requests and issues welcome. Specifically:

  • Test results on other echo / ultrasound makes and models
  • exFAT support (for >4 GB files)
  • AirPlay / DLNA TV mirroring mode
  • Hardware-trigger build (push-button GPIO 17 + status LED)
  • LUKS-encrypted backing storage

Built for clinics stuck in the USB-shuffle.

About

Wireless USB bridge for legacy USB-only echo / ultrasound machines — Raspberry Pi Zero 2 W + Samba. The device sees it as a USB stick; studies land instantly on a Win+Mac network share.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages