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 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
+ )