source and installation

TL;DR: s1500d is a small, fast background program that makes the scan button on a Fujitsu ScanSnap S1500 work on Linux. Once it’s set up: open the lid, load your document, press the button, and get a PDF.

should you use this?

s1500d is deliberately for one setup: a Fujitsu ScanSnap S1500 attached to a Linux machine. You can confirm the exact model with lsusb; an S1500 appears as ID 04c5:11a2. It is useful when you want the physical button or paper sensor to run a script without leaving a GUI open.

s1500d itself does not acquire images or require SANE; it detects hardware events and invokes your handler. The example scan-to-PDF handler uses SANE’s scanimage, but your handler can run any command you choose.

It is specific to this scanner model rather than a general-purpose button daemon. Other ScanSnap models and macOS/Windows are not currently supported. If that still sounds like your setup, the quickest safe evaluation is:

# Arch Linux
paru -S s1500d

# Open the scanner lid, then:
lsusb -d 04c5:11a2
s1500d --doctor
s1500d  # watch events; does not start a scan

For other Linux distributions and service setup, use the full installation guide.

If you prefer an assisted install, paste this into Codex, Claude Code, or another coding agent with web access:

Help me evaluate and install https://github.com/mmacpherson/s1500d. First
confirm that this machine runs Linux and that my scanner is exactly a ScanSnap
S1500 (`04c5:11a2`). Read README.md and INSTALL.md, explain the changes you
propose, and get event monitoring working before configuring a scan handler
or enabling the systemd service. Ask before using sudo.

the problem

The ScanSnap S1500 is a fantastic color duplex document scanner. It launched in 2009 and has been out of production for years, but I’ve had mine since 2013 and it still works great. We use it as the front door to our paperless household (a well-trodden path), paired with paperless-ngx and some LLM postprocessing via Modal, all running on an Arch Linux homelab server.

On Mac and Windows, the software situation is bleak — Fujitsu’s current ScanSnap Home doesn’t support it at all, and they discontinued ScanSnap Manager (the legacy software that did) in November 2024. You can still find old ScanSnap Manager installers floating around the web, and apparently they still work, but it’s not a great long-term bet. On Linux, none of that matters — SANE’s fujitsu backend handles scanning just fine. The hard part is the “one-touch” workflow: you want to press the physical scan button and have something happen automatically, without a GUI open and waiting.

The usual answer is scanbd, a general-purpose scanner button daemon. I’ve had mixed results with scanbd — over different OSes and installations, I’ve usually gotten it to work, but I struggled with it. Sometimes I couldn’t get button-press detection to work, but it could trigger on paper feed. Sometimes it would wait for nearly a minute before actually beginning to scan the document. I’d followed the docs and the ever-amazing Arch Wiki, and it’s ultimately probably user error. But now there’s Claude Code, and I wanted to see if I could get something that works more consistently for me.

I asked Claude to help me figure out how the scanner was actually communicating with the computer. It wrote a diagnostic script (explore.py) that captured USB traffic using termshark (a terminal UI for Wireshark), then guided me through a protocol — insert paper, remove paper, press the button, release the button — recording which bits changed at each step. Between that and reading the SANE fujitsu backend source, we reverse-engineered the USB protocol. It’s actually pretty simple: one command (GET_HW_STATUS), 12 bytes of response, a few status bits to decode. Full details in the protocol reference.

scanbd takes a different approach — it loads the full SANE stack, opens a connection to the backend, and polls by reading SANE options, about 25 SCSI commands per cycle. It needs a scanbm proxy to coordinate device access between polling and scanning. Those are reasonable decisions if you’re building something that supports every SANE-compatible scanner. But if you only need to talk to one device, you can skip all of that and send the one command directly via libusb.

That’s what s1500d does. The tradeoff is clear: it only works with the ScanSnap S1500 (and potentially other ScanSnap models with compatible protocols — I’ve only tested the S1500). If you have an S1500 and want something minimal that just works, read on. Or if you’re interested in a kind of template for using a coding agent to reverse-engineer a USB protocol and build a bespoke driver for some other piece of hardware, this might be a useful case study.

installation

Arch Linux (AUR)

I published an AUR package for my own convenience, but in case one of the other six people running an S1500 on Arch Linux reads this:

paru -S s1500d

This installs the binary, systemd unit, udev rules, and example config/handler.

from source

You’ll need libusb and a Rust toolchain:

# Arch/CachyOS
pacman -S libusb rust

# Debian 13+, Ubuntu 26.04+
apt install libusb-1.0-0-dev cargo

# Ubuntu 24.04 (its default Rust is 1.75, too old; use the 1.85 package)
apt install libusb-1.0-0-dev cargo-1.85
export PATH=/usr/lib/rust-1.85/bin:$PATH

# Fedora
dnf install libusb1-devel cargo

s1500d needs Rust 1.85 or newer; check with cargo --version. If your distribution’s Rust is older, install a current toolchain with rustup.

Then either:

# Get the source
git clone https://github.com/mmacpherson/s1500d.git
cd s1500d

# Install only the binary in your Cargo bin directory
cargo install --path .

# Or install the binary plus systemd, udev, config, and handler files
make release
sudo make install
sudo systemd-sysusers s1500d.conf
sudo udevadm control --reload-rules
sudo systemctl daemon-reload

See INSTALL.md for the full details.

quick start: seeing events

The simplest way to try s1500d is to just run it with no arguments. Open the scanner lid (which powers it on via USB), then:

s1500d

You’ll see events logged to stderr as you interact with the scanner.

supported actions and gestures

This is the complete set of physical scanner actions s1500d recognizes:

What you do Raw handler mode Config mode
Open the ADF lid device-arrived device-arrived
Close the ADF lid device-left device-left
Insert paper paper-in paper-in
Remove paper paper-out paper-out
Press the scan button button-down Starts or continues a multi-press gesture
Release the scan button button-up Completes one press and starts the gesture timeout

In raw mode, the handler receives the event name as $1. In config mode, button-down and button-up are not sent to the handler. Instead, one or more complete presses followed by the timeout produce scan <profile> when that press count appears in [profiles]. Any positive press count can be mapped; unmapped counts are logged and ignored.

The complete button-gesture behavior in config mode is:

What you do What s1500d does
Single press Dispatches the profile mapped to 1 after the timeout
Double press Dispatches the profile mapped to 2 after the timeout
Triple press Dispatches the profile mapped to 3 after the timeout
Any higher configured number of presses Dispatches the profile mapped to that number

The timeout runs from when s1500d sees each press end; the next press must be seen before it expires. Holding the button longer does not select a different profile. Press at a normal pace: very rapid taps can merge into one press, so a very fast double press may be counted as a single press.

To actually do something with these events, pass a handler script:

s1500d /path/to/handler.sh

The handler receives the event name as $1. Here’s a minimal example:

#!/bin/bash
EVENT="$1"
PROFILE="${2:-}"

case "$EVENT" in
    scan)
        logger -t s1500d "Scan gesture: profile=$PROFILE"
        # Your scan logic here — scanimage is safe to call,
        # s1500d has released the USB device.
        ;;
    paper-in)
        logger -t s1500d "Paper detected"
        ;;
    button-down)
        logger -t s1500d "Scan button pressed (legacy mode)"
        ;;
    device-arrived)
        logger -t s1500d "Scanner lid opened"
        ;;
    device-left)
        logger -t s1500d "Scanner lid closed"
        ;;
    *)
        logger -t s1500d "Event: $EVENT"
        ;;
esac

One important detail: s1500d releases the USB device before calling your handler. This means scanimage and other SANE tools can claim the scanner cleanly — no fighting over the device handle.

Handlers run one at a time, synchronously, with the USB device released — including device-arrived. Events observed in the same poll are delivered in order (paper before button). s1500d cannot see the scanner while a handler runs: afterwards, a changed button state is delivered as one press or release (timed when it was seen), paper changes are absorbed (a scan usually consumes the paper), and a press and release that both happen during the handler are not seen.

scan to PDF

The contrib/handler-scan-to-pdf.sh script is a practical handler that scans all pages in the ADF to a timestamped PDF using scanimage and img2pdf. Use the maintained script linked above. It acts only on the scan event, which only config mode (s1500d -c) sends; run as a raw handler (s1500d handler-scan-to-pdf.sh) it never scans. Set handler in your config to its path. With SCAN_DEVICE unset, the handler runs scanimage -L, selects exactly one ScanSnap S1500 and logs its exact name. No matches, multiple matches or a failed lookup stop the attempt with the device list and instructions. Other scanner models do not count as matches.

Discovery runs on every scan. With SANE_CONFIG_DIR unset, it looks for a readable fujitsu.conf in the current directory, /etc/sane.d, then /usr/local/etc/sane.d, and tries a private temporary Fujitsu-only configuration. This configuration is removed afterwards and never changes acquisition settings. Explicit SANE_CONFIG_DIR values, including empty values and search lists, use full discovery unchanged. Missing configuration, preparation errors, failed lookups, or fast lookups without an S1500 device entry also fall back to full discovery. Diagnostic text alone does not count as a device. That can add several seconds while enabled backends are probed. For regular use, set SCAN_DEVICE to the exact name in the detection log to skip that delay; the handler does not persist its selection between invocations. An off or unplugged scanner can leave the fast lookup empty, triggering the slower full lookup before failure. Pinning SCAN_DEVICE skips that lookup too.

For a single-scanner setup:

SCAN_DIR="$HOME/Scans" ./contrib/handler-scan-to-pdf.sh scan standard

An explicit SCAN_DEVICE skips lookup. Use the exact name (including serial number); a literal * is not a device selector. For example:

scanimage -L
SCAN_DEVICE='fujitsu:ScanSnap S1500:YOUR_SERIAL' SCAN_DIR="$HOME/Scans" \
    ./contrib/handler-scan-to-pdf.sh scan standard

The handler acquires TIFF pages into a private directory $SCAN_DIR/.s1500d-XXXXXXXXXX, then converts them to a staged PDF. It publishes standard_YYYYMMDD-HHMMSS.pdf only after successful acquisition and conversion. An existing destination is never overwritten: a same-second filename collision fails and retains the new attempt for recovery. Successful attempts remove their working files.

Acquisition errors (including a jam after some pages), conversion errors, and publication failures return nonzero, retain recovery files, and print their location to stderr and the system log. Scanner and converter diagnostics remain visible. HUP/INT/TERM also retain the attempt; after an uncatchable termination, look for the hidden .s1500d-* directories. Inspect partial TIFFs before using them: the last page may be incomplete. Nothing automatically retries or deletes failed attempts containing files. Attempts with no pages remove their working directory only if it is empty, then report failure with the scanner exit code. Recover or remove retained attempts manually after checking their contents; a retained scan.pdf is not proof of a complete acquisition.

Files belong to the invoking account. New scan directories are mode 0750, completed PDFs are 0640, and recovery directories are private (0700). Existing scan-directory permissions are not changed. Under the packaged service, the owner/group is s1500d and SCAN_DIR=/var/lib/s1500d/scans; readers need appropriate group membership and directory access. Recovery requires the service account or an administrator. Leave SCAN_DEVICE unset for auto-detection, or set it in the handler or a systemd environment override. An explicitly empty value is an error; unset SCAN_DEVICE restores auto-detection. Verify foreground scanning and access before enabling the service. This example requires a filesystem supporting hard links for atomic publication; publication failure keeps the recovery files.

The example converts 300 dpi colour pages to PDF losslessly, so files are large: a measured 4-page duplex scan was about 60 MB, and size varies with page content. For smaller files, change the scan mode or resolution, or run a downstream PDF optimization or compression step.

Keep SCAN_DIR outside recursively watched import or sync folders, or configure those consumers to exclude .s1500d-* directories. These directories contain raw pages and a PDF while it is still being written; only top-level published PDFs are ready for consumption.

You’ll need sane and img2pdf installed:

# Arch
pacman -S sane img2pdf

# Debian/Ubuntu
apt install sane-utils img2pdf

# Fedora
dnf install sane-backends img2pdf

configuration

Running s1500d with -c enables config mode, which reads a TOML file:

s1500d -c config.toml

Here’s a full example:

handler = "/path/to/your/handler.sh"
gesture_timeout_ms = 600
log_level = "info"

[profiles]
1 = "standard"
2 = "legal"
3 = "photo"

config keys

Key Required Default Description
handler yes — Path to the script called on events
gesture_timeout_ms no 600 How long to wait (in ms; recommended 100–5000, must be above 0) for additional button presses before dispatching a gesture
log_level no "info" Log verbosity: off, error, warn, info, debug, trace. The RUST_LOG environment variable overrides this if set.
profiles no (empty) Map of press count → profile name (see below). Profile names are arbitrary labels — your handler script decides what they mean.

s1500d exits at startup if the config has an unknown key, a gesture_timeout_ms of 0, an unknown log_level, an invalid profile, or a handler that is missing or not executable. A gesture_timeout_ms outside the recommended 100–5000 only logs a warning. Run s1500d --check-config /etc/s1500d/config.toml to check a config without the scanner.

events in config mode

In config mode, your handler receives these events as $1:

Event $2 When it fires
scan profile name Button gesture completed (press count mapped to a profile)
paper-in — Paper inserted into feeder
paper-out — Paper removed from feeder
device-arrived — Scanner lid opened (USB device appeared)
device-left — Scanner lid closed (USB device removed)

gesture detection

Instead of passing raw button-down/button-up events, config mode counts complete button presses and maps the count to a named profile via the [profiles] table. Each release starts the gesture_timeout_ms window; another press inside it increments the count, while letting it expire dispatches the gesture.

Press the button once, wait 600ms, and your handler gets called with scan standard. Press twice within the window and it gets scan legal. Three times for scan photo. Unmapped press counts are logged and ignored.

The config above maps three press counts, but s1500d does not hard-code a finite set of single-, double-, or triple-press gestures. Any positive press count can be mapped, and your handler can support as many profiles as you find practical.

Profile names such as standard, legal, and photo are arbitrary labels, not built-in scan presets. Your handler decides what they mean. With scanimage, a profile might select simplex or duplex scanning, color mode, resolution, page dimensions, or output format; the handler can also choose a destination, perform OCR or other post-processing, upload the result, send a notification, or run something unrelated to scanning. Here are some natural scan settings for reference:

For example, add this before the handler’s acquisition step:

SCAN_OPTIONS=(--source="ADF Duplex" --mode=Color --resolution=300)
case "$PROFILE" in
    legal) SCAN_OPTIONS+=(--page-width=215.872 --page-height=355.6 -x 215.872 -y 355.6) ;;
    a4) SCAN_OPTIONS+=(--page-width=210 --page-height=297 -x 210 -y 297) ;;
    photo) SCAN_OPTIONS=(--source="ADF Front" --mode=Color --resolution=600) ;;
    standard-bw) SCAN_OPTIONS=(--source="ADF Duplex" --mode=Lineart --resolution=300) ;;
    standard-gray) SCAN_OPTIONS=(--source="ADF Duplex" --mode=Gray --resolution=300) ;;
esac

Replace the handler’s fixed source, mode, and resolution arguments with "${SCAN_OPTIONS[@]}", keeping its exact device selection, batch path, and acquisition/conversion failure handling. The profile remains the filename prefix; use only letters, digits, underscores, and hyphens.

running as a systemd service

The repository ships a udev rule and systemd unit as starting points for an always-on setup. Review them for your machine rather than treating them as a turnkey policy:

Test your handler in the foreground, edit /etc/s1500d/config.toml, and only then enable the installed unit:

sudo systemctl daemon-reload
sudo systemctl enable --now s1500d

See the installation guide for package and from-source paths.

diagnosing hardware

If things aren’t working, the --doctor flag runs an interactive hardware check that walks you through each sensor:

s1500d --doctor

With the lid open, it asks you to insert and remove paper and to press and release the button, confirming that the daemon can see each event. If it cannot open the scanner, it says whether the scanner was not found, permission was denied, or another program (scanbd, saned, another s1500d, or a running scan) is using it.

under the hood

The S1500 uses a vendor-specific USB protocol (class FF:FF:FF) with SCSI commands wrapped in a 31-byte Fujitsu envelope. The daemon sends a GET_HW_STATUS command (SCSI opcode 0xC2) every 100 ms (every 20 ms while waiting for another press of a gesture) and decodes the 12-byte response (checking the 13-byte status reply that follows) to detect button presses and paper presence. State transitions are edge-triggered — the handler only fires when something changes.

Door state isn’t in the status response at all. Opening the ADF lid powers the scanner on (USB enumeration), closing it powers off (USB disconnect). So the daemon has two loops: an outer one watching for USB connect/disconnect, and an inner one polling GET_HW_STATUS while the device is present.

The protocol was reverse-engineered from USB captures and the SANE fujitsu backend source, then empirically verified with a physical scanner. The full details — including a SANE bit-map discrepancy I found during testing — are in the protocol reference.

references & prior art

s1500d exists because other people documented their ScanSnap-on-Linux setups and I could build on their work. These are the posts that informed the project:

Going paperless at home:

And the tools this project depends on or relates to: