pieusb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pieusb/__init__.py +17 -0
- pieusb/_device.py +37 -0
- pieusb/exceptions.py +91 -0
- pieusb/inquiry.py +158 -0
- pieusb/option.py +515 -0
- pieusb/postprocess.py +131 -0
- pieusb/scanner.py +489 -0
- pieusb/transport.py +263 -0
- pieusb/types.py +114 -0
- pieusb/usb_utils.py +35 -0
- pieusb-0.1.0.dist-info/METADATA +57 -0
- pieusb-0.1.0.dist-info/RECORD +14 -0
- pieusb-0.1.0.dist-info/WHEEL +4 -0
- pieusb-0.1.0.dist-info/licenses/LICENSE +674 -0
pieusb/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""pieusb -- direct-USB acquisition library for USB-connected Reflecta film scanners.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from importlib.metadata import version as _pkg_version
|
|
6
|
+
|
|
7
|
+
__version__ = _pkg_version("pieusb")
|
|
8
|
+
except Exception: # pragma: no cover - only when the package isn't installed
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
from pieusb._device import get_devices
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"__version__",
|
|
15
|
+
# module-level (python-sane-shaped)
|
|
16
|
+
"get_devices",
|
|
17
|
+
]
|
pieusb/_device.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pieusb.usb_utils import find_device
|
|
2
|
+
from pieusb.transport import UASDevice, SCSI_INQUIRY
|
|
3
|
+
from pieusb.scanner import Scanner
|
|
4
|
+
|
|
5
|
+
from pieusb.inquiry import KNOWN_PIDS, PIE_VENDOR_ID, parse_inquiry
|
|
6
|
+
from pieusb.types import DeviceInfo
|
|
7
|
+
|
|
8
|
+
def _get_inquiry(dev: UASDevice):# -> InquiryResponse:
|
|
9
|
+
header = dev.command(SCSI_INQUIRY, in_size=5, cdb_length=5)
|
|
10
|
+
additional_length = header[4]
|
|
11
|
+
total_size = additional_length + 4
|
|
12
|
+
raw = dev.command(SCSI_INQUIRY, in_size=total_size, cdb_length=total_size)
|
|
13
|
+
return parse_inquiry(raw)
|
|
14
|
+
|
|
15
|
+
def get_devices() -> list[DeviceInfo]:
|
|
16
|
+
devices: list[DeviceInfo] = []
|
|
17
|
+
|
|
18
|
+
for pid in KNOWN_PIDS:
|
|
19
|
+
dev = find_device(PIE_VENDOR_ID, pid)
|
|
20
|
+
|
|
21
|
+
if dev is not None:
|
|
22
|
+
with UASDevice(dev) as d:
|
|
23
|
+
r = _get_inquiry(d)
|
|
24
|
+
devices.append(DeviceInfo(
|
|
25
|
+
dev=dev,
|
|
26
|
+
vendor=r.vendor,
|
|
27
|
+
model=r.model_str,
|
|
28
|
+
inquiry=r
|
|
29
|
+
))
|
|
30
|
+
|
|
31
|
+
return devices
|
|
32
|
+
|
|
33
|
+
# Probably unnecessary.
|
|
34
|
+
# The user can just s = Scanner(devices[0]) or with Scanner(devices[0]) as s
|
|
35
|
+
# Device gets "opened" in the Scanner constructor
|
|
36
|
+
def open(dev: DeviceInfo) -> Scanner:
|
|
37
|
+
return Scanner(dev)
|
pieusb/exceptions.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Exception hierarchy for pieusb.
|
|
3
|
+
|
|
4
|
+
PieusbError
|
|
5
|
+
|-- TransportError USB/SCSI transaction failed below the command layer
|
|
6
|
+
|-- Timeout a command or a wait did not complete in time
|
|
7
|
+
|-- CheckCondition SCSI CHECK CONDITION, carrying decoded sense data
|
|
8
|
+
|-- DeviceNotReady device is reachable but cannot start a scan
|
|
9
|
+
| `-- WarmingUp ... specifically because the lamp is warming up
|
|
10
|
+
`-- ScanInProgress host-side: this Scanner already has a scan running
|
|
11
|
+
|
|
12
|
+
Layering note: `CheckCondition` is what the transport layer raises -- it reports
|
|
13
|
+
what the device said, nothing more. Turning a particular sense triple into
|
|
14
|
+
`WarmingUp` or `DeviceNotReady` is the Scanner's job, because that is an
|
|
15
|
+
interpretation of the sense data rather than a property of the transaction. The
|
|
16
|
+
C backend splits it the same way: `sanei_pieusb_command()` returns a status and
|
|
17
|
+
`sane_start()` decides what it means.
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
# SCSI sense keys we distinguish (SPC-3 table 27)
|
|
21
|
+
SENSE_KEY_NOT_READY = 0x02
|
|
22
|
+
SENSE_KEY_UNIT_ATTENTION = 0x06
|
|
23
|
+
|
|
24
|
+
class PieusbError(Exception):
|
|
25
|
+
'''Base class for every error raised by this package.'''
|
|
26
|
+
|
|
27
|
+
class TransportError(PieusbError):
|
|
28
|
+
'''A USB or SCSI transaction failed below the command layer.'''
|
|
29
|
+
|
|
30
|
+
class Timeout(PieusbError):
|
|
31
|
+
'''A command, or a wait for the device, did not complete in time.'''
|
|
32
|
+
|
|
33
|
+
class ScanInProgress(PieusbError):
|
|
34
|
+
'''This Scanner already has a scan running.
|
|
35
|
+
|
|
36
|
+
Host-side state, not something the device reported -- deliberately not a
|
|
37
|
+
DeviceNotReady, because "wait and retry" and "you have a bug" want different
|
|
38
|
+
handling.
|
|
39
|
+
'''
|
|
40
|
+
|
|
41
|
+
class DeviceNotReady(PieusbError):
|
|
42
|
+
'''The device is reachable but cannot start a scan.'''
|
|
43
|
+
|
|
44
|
+
class WarmingUp(DeviceNotReady):
|
|
45
|
+
'''The lamp is still warming up. Retryable.
|
|
46
|
+
|
|
47
|
+
A subclass of DeviceNotReady because that is what the wire says: both are
|
|
48
|
+
sense key NOT_READY, and warming up is the single case singled out by
|
|
49
|
+
code 0x04 / qualifier 0x01 (sanei_pieusb_decode_sense, pieusb_usb.c:387-397).
|
|
50
|
+
'''
|
|
51
|
+
|
|
52
|
+
class CheckCondition(PieusbError):
|
|
53
|
+
'''The device returned CHECK CONDITION; sense data has been read back.'''
|
|
54
|
+
|
|
55
|
+
def __init__(self, sense_key: int, sense_code: int, sense_qualifier: int) -> None:
|
|
56
|
+
self.sense_key = sense_key
|
|
57
|
+
self.sense_code = sense_code
|
|
58
|
+
self.sense_qualifier = sense_qualifier
|
|
59
|
+
super().__init__(
|
|
60
|
+
f"CHECK CONDITION key=0x{sense_key:02x} code=0x{sense_code:02x} "
|
|
61
|
+
f"qualifier=0x{sense_qualifier:02x}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def not_ready(self) -> bool:
|
|
66
|
+
'''Sense key NOT READY -- the device cannot act on commands yet.
|
|
67
|
+
|
|
68
|
+
Broader than `warming_up`: it also covers the not-ready reasons the C
|
|
69
|
+
backend lumps into a generic failure (pieusb_usb.c:393-396).
|
|
70
|
+
'''
|
|
71
|
+
return self.sense_key == SENSE_KEY_NOT_READY
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def warming_up(self) -> bool:
|
|
75
|
+
return self.not_ready and self.sense_code == 0x04 and self.sense_qualifier == 0x01
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def must_calibrate(self) -> bool:
|
|
79
|
+
# "Calibration disable not granted" -- returned by START SCAN when
|
|
80
|
+
# skipShadingAnalysis was requested but the scanner insists on
|
|
81
|
+
# calibrating. In the SANE backend (sanei_pieusb_decode_sense) this maps
|
|
82
|
+
# to PIEUSB_STATUS_MUST_CALIBRATE, which is NOT an error: it is the
|
|
83
|
+
# signal to enter the calibration phase and read shading reference data
|
|
84
|
+
# (pieusb.c:1091-1121). We treat it the same way.
|
|
85
|
+
return (self.sense_key == SENSE_KEY_UNIT_ATTENTION
|
|
86
|
+
and self.sense_code == 0x82
|
|
87
|
+
and self.sense_qualifier == 0x00)
|
|
88
|
+
|
|
89
|
+
class ParamError(PieusbError):
|
|
90
|
+
'''Some parameter values are incompatible with each other
|
|
91
|
+
'''
|
pieusb/inquiry.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from pieusb.types import (
|
|
2
|
+
InquiryResponse,
|
|
3
|
+
Filter,
|
|
4
|
+
ColorFormat,
|
|
5
|
+
ColorFormat,
|
|
6
|
+
ImageFormat,
|
|
7
|
+
ScanCapabilities,
|
|
8
|
+
OptionalDevices,
|
|
9
|
+
DeviceInfo
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
import struct
|
|
13
|
+
|
|
14
|
+
PIE_VENDOR_ID = 0x05E3
|
|
15
|
+
KNOWN_PIDS = [0x0142, 0x0144, 0x0145]
|
|
16
|
+
MODEL_NAMES = {
|
|
17
|
+
0x003a: 'DigitDia 6000 Multiple Slide Scanner',
|
|
18
|
+
0x0030: 'CrystalScan 7200',
|
|
19
|
+
0x0036: 'ProScan 7200',
|
|
20
|
+
0x0047: 'ProScan 10T',
|
|
21
|
+
0x002e: 'CrystalScan 3600',
|
|
22
|
+
0x002f: 'DigitDia 4000',
|
|
23
|
+
0x0048: 'RPS 10M (aka Pacific Image PrimeFilm XAs)',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def parse_filters(byte: int) -> tuple[Filter, ...]:
|
|
27
|
+
out = []
|
|
28
|
+
|
|
29
|
+
if byte & 0x01:
|
|
30
|
+
out.append(Filter.NEUTRAL)
|
|
31
|
+
if byte & 0x02:
|
|
32
|
+
out.append(Filter.RED)
|
|
33
|
+
if byte & 0x04:
|
|
34
|
+
out.append(Filter.GREEN)
|
|
35
|
+
if byte & 0x08:
|
|
36
|
+
out.append(Filter.BLUE)
|
|
37
|
+
if byte & 0x10:
|
|
38
|
+
out.append(Filter.INFRARED)
|
|
39
|
+
|
|
40
|
+
return tuple(out)
|
|
41
|
+
|
|
42
|
+
def parse_color_depths(byte: int) -> tuple[int, ...]:
|
|
43
|
+
out = []
|
|
44
|
+
|
|
45
|
+
if byte & 0x01:
|
|
46
|
+
out.append(1)
|
|
47
|
+
if byte & 0x02:
|
|
48
|
+
out.append(4)
|
|
49
|
+
if byte & 0x04:
|
|
50
|
+
out.append(8)
|
|
51
|
+
if byte & 0x08:
|
|
52
|
+
out.append(10)
|
|
53
|
+
if byte & 0x10:
|
|
54
|
+
out.append(12)
|
|
55
|
+
if byte & 0x20:
|
|
56
|
+
out.append(16)
|
|
57
|
+
|
|
58
|
+
return tuple(out)
|
|
59
|
+
|
|
60
|
+
def parse_color_format(byte: int) -> tuple[ColorFormat, ...]:
|
|
61
|
+
out = []
|
|
62
|
+
|
|
63
|
+
if byte & 0x01:
|
|
64
|
+
out.append(ColorFormat.PIXEL)
|
|
65
|
+
if byte & 0x02:
|
|
66
|
+
out.append(ColorFormat.LINE)
|
|
67
|
+
if byte & 0x04:
|
|
68
|
+
out.append(ColorFormat.INDEXED)
|
|
69
|
+
|
|
70
|
+
return tuple(out)
|
|
71
|
+
|
|
72
|
+
def parse_image_format(byte: int) -> tuple[ImageFormat, ...]:
|
|
73
|
+
out = []
|
|
74
|
+
|
|
75
|
+
if byte & 0x01:
|
|
76
|
+
out.append(ImageFormat.INTEL)
|
|
77
|
+
if byte & 0x02:
|
|
78
|
+
out.append(ImageFormat.MOTOROLA)
|
|
79
|
+
if byte & 0x04:
|
|
80
|
+
out.append(ImageFormat.BLACKONE)
|
|
81
|
+
if byte & 0x08:
|
|
82
|
+
out.append(ImageFormat.OKLINE)
|
|
83
|
+
|
|
84
|
+
return tuple(out)
|
|
85
|
+
|
|
86
|
+
def parse_scan_capabilities(byte: int) -> tuple[ScanCapabilities, ...]:
|
|
87
|
+
out = []
|
|
88
|
+
|
|
89
|
+
if byte & 0x80:
|
|
90
|
+
out.append(ScanCapabilities.PWRSAV)
|
|
91
|
+
if byte & 0x40:
|
|
92
|
+
out.append(ScanCapabilities.EXT_CAL)
|
|
93
|
+
if byte & 0x10:
|
|
94
|
+
out.append(ScanCapabilities.FAST_PREVIEW)
|
|
95
|
+
if byte & 0x08:
|
|
96
|
+
out.append(ScanCapabilities.DISABLE_CAL)
|
|
97
|
+
|
|
98
|
+
return tuple(out)
|
|
99
|
+
|
|
100
|
+
def parse_optional_devices(byte: int) -> tuple[OptionalDevices, ...]:
|
|
101
|
+
out = []
|
|
102
|
+
|
|
103
|
+
if byte & 0x80:
|
|
104
|
+
out.append(OptionalDevices.DEV_MPCL)
|
|
105
|
+
if byte & 0x04:
|
|
106
|
+
out.append(OptionalDevices.DEV_TP1)
|
|
107
|
+
if byte & 0x02:
|
|
108
|
+
out.append(OptionalDevices.DEV_TP)
|
|
109
|
+
if byte & 0x01:
|
|
110
|
+
out.append(OptionalDevices.DEV_ADF)
|
|
111
|
+
|
|
112
|
+
return tuple(out)
|
|
113
|
+
|
|
114
|
+
def parse_inquiry(raw: bytes) -> InquiryResponse:
|
|
115
|
+
def txt(b):
|
|
116
|
+
return b.rstrip(b"\x00").decode("ascii", errors="replace").strip()
|
|
117
|
+
|
|
118
|
+
model_nr = struct.unpack_from("<H", raw, 116)[0]
|
|
119
|
+
if model_nr in MODEL_NAMES:
|
|
120
|
+
model_str = MODEL_NAMES[model_nr]
|
|
121
|
+
else:
|
|
122
|
+
model_str = 'Unknown'
|
|
123
|
+
return InquiryResponse(
|
|
124
|
+
vendor=txt(raw[8:16]),
|
|
125
|
+
product=txt(raw[16:32]),
|
|
126
|
+
revision=txt(raw[32:36]),
|
|
127
|
+
max_resolution_x=struct.unpack_from("<H", raw, 36)[0],
|
|
128
|
+
max_resolution_y=struct.unpack_from("<H", raw, 38)[0],
|
|
129
|
+
max_scan_w=struct.unpack_from("<H", raw, 40)[0],
|
|
130
|
+
max_scan_h=struct.unpack_from("<H", raw, 42)[0],
|
|
131
|
+
filters=parse_filters(raw[44]),
|
|
132
|
+
color_depths=parse_color_depths(raw[45]),
|
|
133
|
+
color_formats=parse_color_format(raw[46]),
|
|
134
|
+
image_formats=parse_image_format(raw[48]),
|
|
135
|
+
scan_capabilities=parse_scan_capabilities(raw[49]),
|
|
136
|
+
scan_speeds=raw[49] & 7,
|
|
137
|
+
optional_devices=parse_optional_devices(raw[50]),
|
|
138
|
+
enhancements=raw[51],
|
|
139
|
+
gamma_bits=raw[52],
|
|
140
|
+
last_filter=raw[53],
|
|
141
|
+
preview_scan_resolution=struct.unpack_from("<H", raw, 54)[0],
|
|
142
|
+
halftones=raw[100],
|
|
143
|
+
minimum_highlight=raw[101],
|
|
144
|
+
maximum_shadow=raw[102],
|
|
145
|
+
calibration_equation=raw[103],
|
|
146
|
+
maximum_exposure=struct.unpack_from("<H", raw, 104)[0],
|
|
147
|
+
minimum_exposure=struct.unpack_from("<H", raw, 106)[0],
|
|
148
|
+
x0=struct.unpack_from("<H", raw, 108)[0],
|
|
149
|
+
x1=struct.unpack_from("<H", raw, 110)[0],
|
|
150
|
+
y0=struct.unpack_from("<H", raw, 112)[0],
|
|
151
|
+
y1=struct.unpack_from("<H", raw, 114)[0],
|
|
152
|
+
model=model_nr,
|
|
153
|
+
model_str=model_str,
|
|
154
|
+
production=txt(raw[120:124]),
|
|
155
|
+
timestamp=txt(raw[124:144]),
|
|
156
|
+
signature=txt(raw[144:184]),
|
|
157
|
+
slide_transport=model_nr == 0x3a or model_nr == 0x2f
|
|
158
|
+
)
|