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:
- INQUIRY (0x12) - identify device
- MODE SELECT (0x15) - configure scanner settings
- SET WINDOW (0x24) - define scan area and parameters
- SEND (0x2A) - transfer lookup tables and calibration data
- OBJECT POSITION (0x31) - load paper from hopper
- SCAN (0x1B) - initiate scanning
- READ (0x28) - retrieve image data
See also:
- https://www.staff.uni-mainz.de/tacke/scsi/SCSI2-15.html (SCSI-2 scanner specification)
- https://gitlab.com/sane-project/backends/-/raw/master/backend/fujitsu.c (SANE implementation)
- https://gitlab.com/sane-project/backends/-/raw/master/backend/fujitsu-scsi.h
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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") )
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
Close releases any resources associated with the scanner and closes the underlying USB device.
func (*Scanner) Initialize ¶
Initialize prepares the scanner hardware for operation.
This method executes the required SCSI command sequence to configure the scanner:
- INQUIRY - verify device identification
- SEND DIAGNOSTIC (preread) - set 600 dpi resolution
- MODE SELECT (multiple) - configure ADF, double-feed detection, color dropout, buffering, etc.
- SET WINDOW - define front and back scan areas
- SEND - transfer lookup tables and quantization tables
- SCANNER_CONTROL (lamp on) - activate scanning lamp
- 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 ¶
IsButtonPressed checks if the scan button is currently pressed. It performs a non-blocking check of the hardware status.
func (*Scanner) Scan ¶
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:
- OBJECT POSITION - load paper from hopper
- SCAN - initiate scanning of both sides
- READ (vendor-specific) - query pixel dimensions
- 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)
}