ix500

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 2, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

ix500

A Linux driver and scan daemon for the Fujitsu ScanSnap iX500 document scanner based on the stapelberg/scan2drive implementation. Supports custom DPI, simplex/duplex scanning.

Implements the SCSI-2 scanner command set over Linux's usbdevfs bulk transfer interface. No SANE, libusb, or kernel module required.

Requirements

  • Linux
  • Fujitsu ScanSnap iX500
  • Read/write access to the USB device node (see Permissions)

Library

The thde.io/ix500 package can be used independently:

dev, err := ix500.FindDevice()
scn := ix500.New(dev, &ix500.Options{
    Resolution: ix500.DPI300,
    ScanMode:   ix500.Duplex,
})
defer scn.Close()

_ = scn.Initialize(ctx)
_ = scn.WaitForButton(ctx)

for page, err := range scn.Scan(ctx) {
    // page.Image implements image.Image
    // page.Sheet, page.Side identify position
}

Check out the CLI tool ix500 for an example of how to use the library.

CLI

Install
go install thde.io/ix500/cmd/ix500@latest
Usage
ix500 --help

The daemon waits for the scan button to be pressed, scans all pages from the ADF, and writes them as JPEG files named scan-<timestamp>-page-<NNN>.jpg. Pages are yielded in hardware scan order (last sheet first); use Page.Sheet and Page.Side to reorder if needed.

Permissions

The daemon opens the USB device node directly (e.g. /dev/bus/usb/001/005). To run without root, add a udev rule:

SUBSYSTEM=="usb", ATTRS{idVendor}=="04c5", ATTRS{idProduct}=="132b", MODE="0664", GROUP="scanner"

References

Documentation

Overview

Package ix500 implements a driver for the Fujitsu ScanSnap iX500 document scanner.

This driver was developed from scratch based on USB traffic captures and implements the SCSI-2 scanner command set over USB bulk transfer. The terminology is consistent with the SANE fujitsu driver where appropriate.

The SCSI-2 scanner device type uses a coordinate system with the upper-left corner as the origin, the x-axis extending left-to-right (cross-scan direction), and the y-axis extending top-to-bottom (scan direction). Measurements use a basic unit of 1/1200 inch by default.

Scanner operations follow this sequence:

  1. INQUIRY (0x12) - identify device
  2. MODE SELECT (0x15) - configure scanner settings
  3. SET WINDOW (0x24) - define scan area and parameters
  4. SEND (0x2A) - transfer lookup tables and calibration data
  5. OBJECT POSITION (0x31) - load paper from hopper
  6. SCAN (0x1B) - initiate scanning
  7. READ (0x28) - retrieve image data

See also:

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrShortRead indicates the scanner returned fewer bytes than requested.
	//
	// This error is derived from the SCSI REQUEST SENSE response when the ILI
	// (Incorrect Length Indicator) flag is set. The information field contains
	// the residue (difference between requested and actual transfer length).
	ErrShortRead = errors.New("short read")

	// ErrEndOfPaper indicates the scanner has reached the end of the document.
	//
	// This error is signaled by the EOM (End of Medium) flag in the REQUEST SENSE
	// response. It occurs when scanning reaches the trailing edge of the paper,
	// and no more scan lines are available for the current page.
	ErrEndOfPaper = errors.New("end of paper")

	// ErrTemporaryNoData indicates the scanner has no data ready at this moment.
	//
	// This is a vendor-specific condition (ASC 0x80, ASCQ 0x13) that occurs when
	// the scanner is still processing the image and buffered data is not yet available
	// for transfer. Callers should retry the operation after a brief delay.
	ErrTemporaryNoData = errors.New("temporary no data")

	// ErrHopperEmpty indicates no paper is loaded in the automatic document feeder.
	//
	// This error (ASC 0x80, ASCQ 0x03) is returned by OBJECT POSITION when attempting
	// to load paper from an empty hopper. It signals the normal end of a multi-page
	// scanning session.
	ErrHopperEmpty = errors.New("hopper empty")
)
View Source
var ErrNoDocument = errors.New("no document in hopper")

ErrNoDocument is returned when an operation requires a document in the hopper, but none is detected.

Functions

This section is empty.

Types

type Device

type Device struct {
	// contains filtered or unexported fields
}

Device represents a USB connection to a Fujitsu ScanSnap iX500 scanner on Linux.

It uses the Linux kernel's usbdevfs interface for USB bulk transfers and sysfs for device enumeration. The device interface must be claimed before use and released via Close when finished.

func FindDevice

func FindDevice() (*Device, error)

FindDevice locates and opens a connected Fujitsu ScanSnap iX500 scanner.

It searches /sys/bus/usb/devices for a device matching the iX500's vendor ID (0x04c5) and product ID (0x132b). If found, it opens the device, claims the scanner interface, and returns a ready-to-use Device. Returns an error if the scanner is not connected or cannot be accessed.

func (*Device) Close

func (d *Device) Close() error

Close releases the USB interface and closes the device file.

After calling Close, the Device must not be used. This releases interface 0 (the only interface used by the iX500) and closes the underlying file descriptor.

func (*Device) Read

func (d *Device) Read(p []byte) (n int, err error)

Read implements io.Reader by performing a USB bulk IN transfer.

It transfers up to len(p) bytes from the scanner to the host via the IN endpoint (endpoint 1). The transfer uses a 3-second timeout and blocks until data is available or the timeout expires. The number of bytes actually received is returned.

func (*Device) Write

func (d *Device) Write(p []byte) (n int, err error)

Write implements io.Writer by performing a USB bulk OUT transfer.

It transfers all of p from the host to the scanner via the OUT endpoint (endpoint 2). The transfer uses a 3-second timeout and blocks until all data is sent or the timeout expires. Returns the number of bytes written.

type Options

type Options struct {
	// ButtonPollInterval specifies how often to check the scan button status.
	// Default is 1 second.
	ButtonPollInterval time.Duration

	// DataPollInterval specifies the retry interval when the scanner reports
	// temporary no data during scanning.
	// Default is 500ms.
	DataPollInterval time.Duration

	// RicRetries specifies the maximum number of times to retry the ric command
	// when waiting for image data to become available. Each retry waits DataPollInterval
	// before the next attempt. Default is 120 retries (60 seconds at 500ms intervals).
	RicRetries int

	// Resolution specifies the scanning resolution in dots per inch.
	// Supported values: DPI150, DPI200, DPI300, DPI600. Default is DPI300.
	Resolution Resolution

	// ScanMode controls whether both sides (Duplex, default) or only the front
	// side (Simplex) of each sheet are scanned.
	ScanMode ScanMode
}

Options configures timing and retry behavior for Scanner operations.

type Page

type Page struct {
	image.Image
	// Side indicates which side of the sheet this page represents.
	// 0 usually denotes the front side, and 1 denotes the back side.
	Side Side
	// Sheet indicates the index of the sheet in the scanning sequence.
	// Note that scanners may scan the last sheet first.
	Sheet int
}

Page represents a single scanned side of a document. It embeds image.Image, allowing it to be used directly as an image.

type Resolution

type Resolution int

Resolution specifies the scanning resolution in dots per inch.

const (
	DPI150 Resolution = 150
	DPI200 Resolution = 200
	DPI300 Resolution = 300
	DPI600 Resolution = 600
)

type ScanMode

type ScanMode int

ScanMode controls whether both sides or only the front side of each sheet are scanned.

const (
	Duplex  ScanMode = 0 // both sides (default)
	Simplex ScanMode = 1 // front side only
)

type Scanner

type Scanner struct {
	// contains filtered or unexported fields
}

Scanner manages the lifecycle and operations of a Fujitsu ScanSnap iX500 scanner. It handles initialization, button polling, and scanning operations.

func New

func New(dev io.ReadWriteCloser, opts *Options) *Scanner

New creates a new Scanner instance wrapping the provided USB device. The opts parameter can be nil to use default options. The caller is responsible for closing the underlying device via the Close method when the scanner is no longer needed.

Example

ExampleNew shows how to configure a Scanner with custom options.

package main

import (
	"fmt"
	"os"

	"thde.io/ix500"
)

func main() {
	dev, err := ix500.FindDevice()
	if err != nil {
		fmt.Fprintln(os.Stderr, "scanner not found:", err)
		return
	}

	// Use 600 DPI simplex (front-only) scanning.
	scn := ix500.New(dev, &ix500.Options{
		Resolution: ix500.DPI600,
		ScanMode:   ix500.Simplex,
	})
	defer scn.Close()
}

func (*Scanner) Close

func (s *Scanner) Close() error

Close releases any resources associated with the scanner and closes the underlying USB device.

func (*Scanner) Initialize

func (s *Scanner) Initialize(ctx context.Context) error

Initialize prepares the scanner hardware for operation.

This method executes the required SCSI command sequence to configure the scanner:

  1. INQUIRY - verify device identification
  2. SEND DIAGNOSTIC (preread) - set 600 dpi resolution
  3. MODE SELECT (multiple) - configure ADF, double-feed detection, color dropout, buffering, etc.
  4. SET WINDOW - define front and back scan areas
  5. SEND - transfer lookup tables and quantization tables
  6. SCANNER_CONTROL (lamp on) - activate scanning lamp
  7. GET_HW_STATUS - verify hardware readiness

The full command sequence runs on every call. It is safe to call again after hardware errors or firmware resets to restore the scanner to a known state.

func (*Scanner) IsButtonPressed

func (s *Scanner) IsButtonPressed(ctx context.Context) (bool, error)

IsButtonPressed checks if the scan button is currently pressed. It performs a non-blocking check of the hardware status.

func (*Scanner) Scan

func (s *Scanner) Scan(ctx context.Context) iter.Seq2[*Page, error]

Scan performs a complete duplex scan operation, yielding pages as they are scanned.

This method returns an iterator that yields *Page values for each side of each sheet. The scanning sequence for each sheet is:

  1. OBJECT POSITION - load paper from hopper
  2. SCAN - initiate scanning of both sides
  3. READ (vendor-specific) - query pixel dimensions
  4. For each side (front=0, back=1): - Issue ric commands until data is ready - Execute READ commands to stream image data - Decode RGB data into image.Image - Yield the Page immediately

The iteration continues until the hopper is empty (ErrHopperEmpty), an error occurs, or the context is cancelled. Pages are yielded in hardware scan order: Sheet N Front, Sheet N Back, Sheet (N-1) Front, etc. The iX500 scans the last sheet first. Callers can use Page.Sheet and Page.Side to reorder as needed.

Example

ExampleScanner_Scan demonstrates a complete scan session: find the device, initialize, wait for the button, then stream all pages to JPEG files.

package main

import (
	"context"
	"fmt"
	"image/jpeg"
	"os"
	"path/filepath"

	"thde.io/ix500"
)

func main() {
	ctx := context.Background()

	dev, err := ix500.FindDevice()
	if err != nil {
		fmt.Fprintln(os.Stderr, "scanner not found:", err)
		return
	}

	scn := ix500.New(dev, &ix500.Options{
		Resolution: ix500.DPI300,
		ScanMode:   ix500.Duplex,
	})
	defer scn.Close()

	if err := scn.Initialize(ctx); err != nil {
		fmt.Fprintln(os.Stderr, "initialization failed:", err)
		return
	}

	if err := scn.WaitForButton(ctx); err != nil {
		fmt.Fprintln(os.Stderr, "button wait failed:", err)
		return
	}

	outDir := os.TempDir()
	pageNum := 0
	for page, err := range scn.Scan(ctx) {
		if err != nil {
			fmt.Fprintln(os.Stderr, "scan error:", err)
			return
		}

		path := filepath.Join(outDir, fmt.Sprintf("page-%03d.jpg", pageNum))
		f, err := os.Create(path)
		if err != nil {
			fmt.Fprintln(os.Stderr, "create file:", err)
			return
		}
		if encErr := jpeg.Encode(f, page, &jpeg.Options{Quality: 75}); encErr != nil {
			_ = f.Close()
			fmt.Fprintln(os.Stderr, "encode:", encErr)
			return
		}
		_ = f.Close()

		bounds := page.Bounds()
		fmt.Printf("saved %s (%dx%d, sheet=%d, side=%d)\n",
			filepath.Base(path), bounds.Dx(), bounds.Dy(), page.Sheet, page.Side)
		pageNum++
	}

	fmt.Printf("scan complete, %d pages\n", pageNum)
}

func (*Scanner) WaitForButton

func (s *Scanner) WaitForButton(ctx context.Context) error

WaitForButton blocks until the scan button is pressed or the context is cancelled. It polls the scanner status at the interval specified in Options.ButtonPollInterval.

type Side

type Side int

Side represents the side of a sheet.

const (
	// FrontSide indicates the front side of the sheet.
	Front Side = iota
	// BackSide indicates the back side of the sheet.
	Back
)

Directories

Path Synopsis
cmd
ix500 command
Package main provides a CLI tool for the Fujitsu ScanSnap iX500.
Package main provides a CLI tool for the Fujitsu ScanSnap iX500.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL