epson-usb 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.
epson_usb/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Onur Kesim
4
+ Copyright (c) 2026 Ircama (his modifications and additions to this extraction,
5
+ relicensed under the MIT License with his permission:
6
+ https://github.com/Ircama/epson_print_conf/issues/35#issuecomment-5805756995)
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
epson_usb/README.md ADDED
@@ -0,0 +1,393 @@
1
+ Not affiliated with Seiko Epson.
2
+
3
+ # epson_usb
4
+
5
+ USB (IEEE 1284.4 / D4) access to Epson printers.
6
+
7
+ ```console
8
+ pip install epson-usb
9
+ ```
10
+
11
+ The distribution is called `epson-usb`; the import name is `epson_usb`.
12
+
13
+ **Status.** This package is a demonstration of what has been measured, not a
14
+ supported product. No maintenance is promised: patches are welcome, but fixes for
15
+ models the maintainer cannot reproduce are not, and no dates are given (in the
16
+ maintainer's own words on the tracker: "for those I can take patches but cannot
17
+ promise fixes, and I would rather not put a date on it",
18
+ [#35](https://github.com/Ircama/epson_print_conf/issues/35#issuecomment-5798831443)).
19
+
20
+ ## What has been measured on real hardware
21
+
22
+ Each row is one physical unit. "Standalone tool" means
23
+ [`epson_l3251_usb_reset.py`](https://github.com/onur-kesim/epson-l3251-usb-reset),
24
+ the program this library was extracted from, **not** the code in this package.
25
+
26
+ | # | Printer | OS | Who | Code that ran | What was observed | Source |
27
+ |---|---|---|---|---|---|---|
28
+ | 1 | L3251 | Windows | Onur Kesim | standalone tool | D4 session with the credit handshake over the Windows `USBPRINT` interface, no driver replacement. EEPROM reads and writes are answered on the secondary USB interface (`MI_01` on this unit). Full 256-cell bank-0 backup; counter reset with read-back verification. | [#35, 28 Aug 2026](https://github.com/Ircama/epson_print_conf/issues/35#issuecomment-5452048045) |
29
+ | 2 | L3251 | Windows | Onur Kesim | standalone tool | The waste counter is little-endian. What settled it was the printer's own error state: `0xCC 0x18` reads 6348 (100.0 %) little-endian and the printer was in the error state; `0x3B 0x18` reads 6203 (97.7 %) and it was not. | [#35, 4 Sep 2026](https://github.com/Ircama/epson_print_conf/issues/35#issuecomment-5538152419) |
30
+ | 3 | L3251 | Windows | Onur Kesim | standalone tool | The main waste counter is mirrored in three cells. After ten bordered photo pages, `0x30/0x31`, `0x34/0x35` and `0xC0/0xC1` each went 0 → 17 (`0xFC/0xFD` stayed 0). After zeroing the full set, all three stayed at 0 across two power cycles. | [#35, 4 Sep 2026](https://github.com/Ircama/epson_print_conf/issues/35#issuecomment-5538152419) |
31
+ | 4 | L3250 (firmware XF26P8, USB ID 04B8:118A) | Linux | endafk | `epson_usb` from `epson_print_conf` v8.0.0 | Printer at 100 % (`0x30/0x31` = 6346, status error `0x10`). Reset over USB by writing the 14-cell ET-2810 `raw_waste_reset` set (not `0xC0/0xC1`). After a power cycle everything stayed at 0, and `0xC0/0xC1` went to 0 by itself, as on the L3251. The maintainer of `epson_print_conf` replied: "it looks like the reset works as expected." | [#35, 24 Sep 2026](https://github.com/Ircama/epson_print_conf/issues/35#issuecomment-5810314920), [reply](https://github.com/Ircama/epson_print_conf/issues/35#issuecomment-5810422246) |
32
+
33
+ Row 4 ran the library code of v8.0.0. The code in this package is identical to it
34
+ apart from docstrings and comments (the parsed syntax trees of the five changed
35
+ library modules are equal once docstrings are removed; compared on 24 Sep 2026);
36
+ the tests are newer.
37
+
38
+ ## What has not been measured
39
+
40
+ | What | Status |
41
+ |---|---|
42
+ | This release (`epson-usb` 0.1.0) against a real printer | No record of it. Rows 1-3 used the standalone tool; row 4 used the earlier code described above. That this library behaves like the standalone tool is checked by hardware-free tests (`tests/test_fidelity.py`, against a frozen copy of the tool), not by a hardware run. |
43
+ | Printer models other than the two units above | Not measured. One unit of each; the L3251's firmware version was not recorded. |
44
+ | Firmware versions other than XF26P8 (L3250) | Not measured. |
45
+ | macOS | No backend measured. |
46
+ | Windows: the `libusb`, `pyusb` and `raw` backends | Not measured. Only the native `usbprint` route was used (rows 1-3, by the standalone tool). |
47
+ | Linux: which backend row 4 used | Not reported. `libusb`, `pyusb` and `raw` were not measured individually. |
48
+ | The `rw` service command over USB | Not measured. |
49
+ | Persistence beyond the power cycles listed | Not measured (two on the L3251, one on the L3250). |
50
+ | The test suite (no hardware) on platforms other than Linux / Python 3.10 (CI) and Windows / Python 3.14 (maintainer's machine, 24 Sep 2026) | Not run. |
51
+
52
+ The sections below were written for the copy of this package that lives inside
53
+ [`epson_print_conf`](https://github.com/Ircama/epson_print_conf). Where they
54
+ mention `epson_print_conf.py`, `ui.py`, `find_printers.py` or "the repository
55
+ root", they mean that project; those files are not part of this distribution.
56
+ The backends are described as that project describes them: only the rows above
57
+ were measured by the people named in them.
58
+
59
+ ## Platforms and backends
60
+
61
+ The library talks to the device through a *transport*; five are registered, and
62
+ the right one is chosen per platform, so the common case needs no options.
63
+
64
+ | backend | what it uses | available on |
65
+ |---|---|---|
66
+ | `usbprint` | Windows `USBPRINT` device interface, `ctypes` over SetupAPI + kernel32 — **no driver change** (no Zadig, no WinUSB) | Windows (tried first) |
67
+ | `libusb` | `libusb-1.0` through the package's own `ctypes` binding (no PyUSB needed) | Linux, macOS (on Windows: fallback, see below) |
68
+ | `pyusb` | PyUSB, if you prefer it (`pip install pyusb`) | Linux, macOS (on Windows: fallback) |
69
+ | `raw` | a POSIX character device (`/dev/usb/lp0`) | Linux, macOS |
70
+ | `mock` | the in-memory fake printer | everywhere (tests, demos) |
71
+
72
+ **On Windows the native backend is tried first.** `usbprint` talks to the channel
73
+ the installed Epson driver already publishes, so nothing has to be installed and
74
+ the driver is never replaced; that is what was verified on hardware. `libusb` and
75
+ `pyusb` stay available, but come after it in the default order, because both reach
76
+ the printer's vendor-specific interface directly and would therefore need that
77
+ driver swapped for WinUSB (Zadig) — plus a `libusb-1.0.dll` on the machine. Ask
78
+ for one explicitly if you have set that up:
79
+
80
+ ```console
81
+ python epson_print_conf.py -m XP-205 --usb --backend libusb -i # Windows: only if
82
+ python epson_print_conf.py -m XP-205 --usb --backend usbprint -i # the usual choice
83
+ ```
84
+
85
+ On Linux and macOS the order is `libusb` → `pyusb` → `raw`. `mock` is never
86
+ chosen automatically, so a test can never be mistaken for a printer.
87
+
88
+ ```console
89
+ python -c "from epson_usb.backends import describe_environment; print(describe_environment())"
90
+ ```
91
+
92
+ ## Quick start
93
+
94
+ `import epson_usb` works from the repository root,
95
+ because the package sits next to the scripts.
96
+
97
+ ### From the command line
98
+
99
+ `--usb` switches the transport; `-a/--address` is then not required (and is not
100
+ used). `--backend` and `--device` imply `--usb`:
101
+
102
+ ```console
103
+ python epson_print_conf.py -m XP-205 --usb -i # read the printer
104
+ python epson_print_conf.py -m XP-205 --usb --backend libusb -i # force a backend
105
+ python epson_print_conf.py -m XP-205 --usb --device 1:4 -i # force a device
106
+ python epson_print_conf.py -m XP-205 --backend mock -i # no hardware at all
107
+ python epson_print_conf.py -m XP-205 -a 192.168.1.87 -i # unchanged: SNMP
108
+ ```
109
+
110
+ On Windows `--backend` is rarely needed: `usbprint` is tried first, and
111
+ `libusb`/`pyusb` are the fallbacks. On Linux and macOS it chooses between
112
+ libusb, PyUSB and a character device.
113
+
114
+ ### From the GUI
115
+
116
+ `ui.py` has a **Printer Connection** box with two choices, `TCP/IP` and `USB`
117
+ (in the same row as the model and the address). Selecting USB replaces the
118
+ address field with a **`USB Port`** list: the devices found on this machine
119
+ *that can actually be opened*, refreshed by the ⟳ button next to it and by
120
+ `Detect Printers`. That filter is the point of the list — the enumeration
121
+ reports candidate paths, and on a Windows XP-205 three of the five candidates
122
+ are interface paths that `CreateFile` cannot open at all. Leave the first entry,
123
+ `Auto: first device found`, to let the library choose the device (its default
124
+ backend order); choose one to pin it, which is what tells two attached printers
125
+ apart or picks another interface of a multi-interface device. The choice is
126
+ handed to the transport as `backend`/`device`, the same two options the command
127
+ line fills from `--backend`/`--device`.
128
+
129
+ Selecting USB also disables the features that need the network, with a tooltip
130
+ explaining why each one is off: the printer web interface, the print tests and
131
+ nozzle cleaning (they print through LPR), and the configuration detection (it
132
+ reads SNMP-only MIB values). Everything else — status, EEPROM read/write, waste
133
+ resets, serial/MAC/TI/power-off parameters, access-key detection — works over
134
+ USB.
135
+
136
+ `Detect Printers` becomes a USB device listing in this mode (the backends
137
+ available on the machine, and every candidate the library enumerated), and the
138
+ transport can also be preselected from the command line:
139
+
140
+ ```console
141
+ python ui.py --usb -m XP-205 # or: python ui.py --usb
142
+ ```
143
+
144
+ ### From any other tool: the environment variable
145
+
146
+ `find_printers.py`, `parse_devices.py` and anything else that does
147
+ `from epson_print_conf import EpsonPrinter` follows automatically when
148
+ `EPSON_USB` is set, because the hook at the end of `epson_print_conf.py`
149
+ replaces the class at import time:
150
+
151
+ ```console
152
+ EPSON_USB=1 python3 find_printers.py
153
+ ```
154
+
155
+ ## Using the library directly
156
+
157
+ The library can also be used on its own, without `epson_print_conf`.
158
+
159
+ ### Opening a printer
160
+
161
+ Read and write keys are per-model facts, and the library carries none: it asks
162
+ for them. A host program does not even need them, because its OIDs already
163
+ contain the frames it built from its own configuration.
164
+
165
+ ```python
166
+ from epson_usb import EpsonUsbPrinter
167
+
168
+ with EpsonUsbPrinter(read_key=(0x11, 0x22), # your model's keys, not ours
169
+ write_key=b"example8") as printer:
170
+ print(printer.describe()) # e.g. "usbprint ... (D4 revision 0x10)"
171
+ ```
172
+
173
+ To take the keys from the parameters `epson_print_conf` already has:
174
+
175
+ ```python
176
+ from epson_print_conf import EpsonPrinter
177
+
178
+ parm = EpsonPrinter(model="XP-205").parm
179
+ printer = EpsonUsbPrinter(read_key=parm["read_key"], write_key=parm["write_key"])
180
+ ```
181
+
182
+ Useful constructor arguments: `device=` (a path or `bus:address`), `backend=`
183
+ (`usbprint`, `libusb`, `pyusb`, `raw`, `mock`), `transport=` (an already-built
184
+ transport, which is how the tests inject the fake printer), `dry_run=True`,
185
+ `timeouts=Timeouts.scaled(...)`, and anything the chosen backend needs.
186
+
187
+ ### Reading and writing EEPROM
188
+
189
+ ```python
190
+ printer.read_eeprom(0x30) # '3B' (two hex digits, like epson_print_conf)
191
+ printer.read_cell(0x30) # 59 (an int)
192
+ printer.read_serial(range(0x644, 0x64E)) # printable characters only
193
+ printer.dump_eeprom(0x00, 0xFF) # {address: value} for a whole bank
194
+ printer.write_eeprom(0x30, 0x00) # True only when the printer answered ':OK;'
195
+ printer.write_cells([(0x30, 0), (0x31, 0)]) # a set, each write read back
196
+ ```
197
+
198
+ Safety rules, unchanged from the tool this grew out of: nothing here writes
199
+ unless a write method is called; `dry_run=True` turns every write into a read
200
+ and logs what it would have done; a write that is not confirmed by `:OK;` is
201
+ reported as a failure, never as success.
202
+
203
+ ### Backups
204
+
205
+ ```python
206
+ path = printer.save_backup() # bank 0 -> JSON
207
+ report = printer.restore_backup(path, addresses=[0x30, 0x31, 0x1C])
208
+ print(report) # e.g. "3 written, 0 failed, 0 missing from the backup"
209
+ ```
210
+
211
+ The file format (`{"time": ..., "bank0": {"00": 12, ..., "FF": 94}}`) is the
212
+ historical one, so backups stay interchangeable with the source project's tool.
213
+ `addresses` is the caller's set: which cells a write path can touch is model
214
+ knowledge. A safety backup is taken before restoring unless told otherwise.
215
+
216
+ ### Status, serial, identification
217
+
218
+ ```python
219
+ printer.get_printer_status() # the @BDC ST2 block, decoded
220
+ printer.get_serial_number() # epson_print_conf's format, '?' for unreadable cells
221
+ printer.get_firmware_version() # 'AB11I5 11 May 2018'
222
+ printer.get_device_identification() # {Manufacturer: [...], Model: [...], ...}
223
+ printer.get_cartridges() # ['18XL', ...]
224
+ ```
225
+
226
+ `get_printer_status()` delegates to `epson_print_conf.status_parser` when that
227
+ package is importable, so USB users get the full decode rather than a second,
228
+ drifting copy of it. Without it, `epson_usb.status.parse_st2()` extracts a
229
+ documented subset.
230
+
231
+ ### The `rw` service command
232
+
233
+ ```python
234
+ printer.service_rw("SERIAL") # temporary waste reset, raw reply
235
+ ```
236
+
237
+ `rw` needs only the serial number, which is why it still works on firmware that
238
+ locks the EEPROM. Which serial string to hash is the caller's decision: the
239
+ EEPROM serial and the USB descriptor serial are not the same string. On Windows,
240
+ `epson_usb.backends.usbprint_win.usb_serial_candidates()` lists the forms the
241
+ USB stack reports.
242
+
243
+ ### The SNMP-shaped door (for host programs)
244
+
245
+ A host that already speaks the SNMP dialect can keep building OIDs and hand them
246
+ to the USB transport, which is exactly how the `epson_print_conf` integration
247
+ works:
248
+
249
+ ```python
250
+ oid = printer.eeprom_oid_read_address(0x30) # the OID SNMP would have used
251
+ printer.fetch_oid_values(oid) # [(OctetString, b'...')] over USB
252
+ ```
253
+
254
+ `epson_usb.epson_ctrl.snmp_oid()` and `parse_snmp_oid()` are the two directions
255
+ of that translation, and `is_epson_ctrl_oid()` tells an EPSON-CTRL OID apart
256
+ from a plain MIB one. Plain MIB OIDs cannot travel over a cable: they are
257
+ answered `(None, False)` and logged.
258
+
259
+ ### Bridging an existing class
260
+
261
+ ```python
262
+ from epson_usb.compat import patch_epson_print_conf, usb_printer
263
+
264
+ patch_epson_print_conf() # epson_print_conf.EpsonPrinter is now USB-capable
265
+ UsbPrinter = usb_printer() # or build the subclass without touching upstream
266
+ printer = UsbPrinter(model="XP-205") # no hostname: the device is on the USB bus
267
+ ```
268
+
269
+ `epson_print_conf.enable_usb_transport()` (used by the CLI and by the GUI) is a
270
+ thin wrapper around `patch_epson_print_conf()`, and the original class is kept as
271
+ `epson_print_conf.NetworkEpsonPrinter`.
272
+
273
+ A note on the reply check, because it cost a session. `epson_print_conf`'s own
274
+ `invalid_response()` required the leading zero byte that some firmwares pad
275
+ `@BDC` blocks with. A real XP-205 sends none -- it answers
276
+ `@BDC PS\r\nEE:01660F;\x0C` -- so every EEPROM value came back `None` on a printer
277
+ that had answered correctly, over SNMP *and* over USB, since the bridge
278
+ deliberately does not override that method. The library's rule was always the
279
+ tolerant one: `EpsonUsbPrinter.read_eeprom()` answered `0F` where the host
280
+ printed `None`. That firmware also answers *bare* blocks, with no `@BDC PS`
281
+ header (`b'ii:NA;\x0C'` for an empty ink slot, `b'||:41:NA;\x0C'` for a read it
282
+ refuses), so an element is now looked for anywhere in the reply — and the element
283
+ itself may contain colons, because a write is confirmed with the write opcode in
284
+ the middle: `b'||:42:OK;\x0C'`. A `:NA;` reply
285
+ is an *answer* -- the printer saying no (wrong key, locked EEPROM) -- not a
286
+ malformed one, so a wrong access key is reported as `Invalid read key` at info
287
+ level instead of one error per attempt, which is what makes the 65536 attempts of
288
+ `--detect-key` readable.
289
+
290
+ ### Errors
291
+
292
+ Everything raised derives from `epson_usb.EpsonUsbError`, with three names that
293
+ ask for different action: `DeviceNotFoundError` (nothing on the USB tree looks
294
+ like the printer, or the backend is not available here), `DeviceBusyError`
295
+ (something else holds it), `ProtocolError` (we reached it, the conversation did
296
+ not make sense). Transport errors also derive from `OSError`. The host program
297
+ converts them to its own `TimeoutError`, so a GUI or CLI user learns that the
298
+ printer is missing instead of seeing every value silently become `None`.
299
+
300
+ ## Choosing a backend
301
+
302
+ ```console
303
+ --backend usbprint | libusb | pyusb | raw | mock
304
+ --device 1:4 # bus:address (libusb/pyusb)
305
+ --device /dev/usb/lp0 # character device (raw)
306
+ --device '\\?\usb#vid_04b8&pid_...' # Windows interface path (usbprint)
307
+ ```
308
+
309
+ Environment variables, for the cases where a flag is inconvenient:
310
+
311
+ | variable | effect |
312
+ |---|---|
313
+ | `EPSON_USB` | any value: the host tools use the USB transport |
314
+ | `EPSON_PRINT_CONF_PATH` | directory holding `epson_print_conf.py`, when it is not installed |
315
+ | `EPSON_USB_LIBUSB` | path of the `libusb-1.0` shared library, if discovery fails (needed on Windows only for the `libusb`/`pyusb` fallbacks) |
316
+ | `EPSON_USB_RAW_DEVICE` | extra character device for the `raw` backend |
317
+ | `EPSON_INSTANCE_ID` | Windows device instance id, for the `usbprint` backend |
318
+ | `EPSON_USB_REQUIRE_UPSTREAM` | tests only: `1` turns the skipped host-program test classes into errors (see *Tests*) |
319
+
320
+ Permissions and packages:
321
+
322
+ * **Windows** — the `usbprint` backend needs nothing at all: SetupAPI and
323
+ kernel32 ship with the system, and the printer keeps working normally without
324
+ touching its driver. The `libusb`/`pyusb` fallbacks, if you ever need them,
325
+ would require replacing that driver with WinUSB (Zadig) and a
326
+ `libusb-1.0.dll` on the machine.
327
+ * **Linux** — install `libusb-1.0-0` (`apt`) or `libusb1` (`dnf`), and let your
328
+ user write the device:
329
+ `SUBSYSTEM=="usb", ATTR{idVendor}=="04b8", MODE="0666"` in a udev rule. The
330
+ kernel's `usblp` driver is detached automatically and re-attached on close.
331
+ * **macOS** — `brew install libusb`; no device node is involved.
332
+
333
+ When none of the three is available there, selecting USB would only fail on the
334
+ first command: `epson_print_conf.usb_transport_warning()` answers with what is
335
+ missing, and both the GUI (a line in the status box as soon as USB is chosen) and
336
+ the command line (`--usb`) ask it. On Windows it always answers `None`, because
337
+ the native backend needs nothing installed.
338
+
339
+ A USB session that is left behind (an interrupted command, a device the system
340
+ re-enumerated) answers nothing instead of failing: the key scan notices, closes
341
+ it so the next command opens a fresh one, and gives up with a message when
342
+ silence continues — a scan of 65536 attempts is not the place to discover that
343
+ the cable is not connected.
344
+
345
+ ## Tests
346
+
347
+ Everything runs without hardware:
348
+
349
+ ```console
350
+ python -m unittest discover -s epson_usb/tests -t .
351
+ python epson_usb/tests/mutant_run.py # proves the suite would catch a porting mistake
352
+ ```
353
+
354
+ Seven test classes need the host program, `epson_print_conf`. Without it they are
355
+ skipped, and `unittest` summarises that as `Ran 30 tests ... OK (skipped=7)` even
356
+ though 36 of the 66 tests never ran (it counts one skip per class and none of the
357
+ tests inside). Set `EPSON_USB_REQUIRE_UPSTREAM=1` and that skip becomes an error, so
358
+ a green run always means all 66 ran. CI does this; because `epson_print_conf` is not
359
+ on PyPI, CI checks it out at a pinned commit and puts it on `PYTHONPATH`.
360
+
361
+ They all rest on the `mock` backend: an in-memory printer that consumes the same
362
+ bytes a real one receives and produces the same framing back, so the protocol
363
+ code under test never learns that it is talking to a fake.
364
+
365
+ | file | what it pins |
366
+ | --- | --- |
367
+ | `tests/test_epson_usb.py` | the package's own behaviour: sessions, keys, safety gates, the EEPROM convention, backups, the upstream bridge, the OID bridge (`OidBridgeTests`) and end-to-end parity between the USB and SNMP envelopes (`TransportParityTests`). |
368
+ | `tests/test_fidelity.py` | that this is a *port*: it replays the historical implementation frozen in `tests/referans/` against the same fake printer and compares key agreement, handshake packets, frame builders and golden hex. |
369
+ | `tests/mutant_run.py` | that the suite is not vacuous: it flips the counter's byte order and the write frame's byte order, expects the suite to go red both times, and restores the files byte for byte. |
370
+
371
+ Both transports are driven through the same printer in `TransportParityTests`:
372
+ the question is never "does USB work?" but "does USB ask the printer exactly
373
+ what the network transport asked, and get the same answer?".
374
+
375
+ ## What does not work over USB
376
+
377
+ * **Plain MIB queries** (`get_snmp_info()`, the model/MAC/power-off values the
378
+ GUI shows in a status report) — there is no SNMP agent on a cable. They answer
379
+ "unavailable" and are logged; the corresponding GUI features are disabled in
380
+ USB mode.
381
+ * **Printing** (`print_check_nozzles()`, `print_clean_nozzles()`,
382
+ `print_test_color_pattern()`) — the host sends those through LPR to a network
383
+ address and raises `NotImplementedError` over USB.
384
+
385
+ **## onur-kesim/epson-l3251-usb-reset**
386
+
387
+ The implementation in the `epson_usb` directory currently reuses and extends code from [`onur-kesim/epson-l3251-usb-reset`](https://github.com/onur-kesim/epson-l3251-usb-reset).
388
+
389
+ Ideally, the `epson_usb` directory could serve as a preliminary implementation of a possible future backend layer for `epson-l3251-usb-reset`.
390
+
391
+ The `epson_usb` implementation is not intended to remain permanently embedded in `epson_print_conf`. A possible future approach would be to rely on an evolution of `onur-kesim/epson-l3251-usb-reset`, with its USB functionality exposed as a reusable backend/library.
392
+
393
+ We are deeply grateful to `onur-kesim/epson-l3251-usb-reset` for providing the USB D4 protocol implementation and the native Windows interface on which this work builds.
epson_usb/__init__.py ADDED
@@ -0,0 +1,141 @@
1
+ """USB access to Epson printers: D4 session, EPSON-CTRL, EEPROM, status.
2
+
3
+ This package is the *transport* half of a tool that talks to Epson
4
+ printers over USB. It knows the protocol and the framing, and nothing about
5
+ which printer family is on the other end of the cable: read/write keys,
6
+ address ranges and counter divisors are supplied by the caller, because they
7
+ are per-model facts and a library that carries them is a library that goes
8
+ stale and disagrees with its host.
9
+
10
+ What it does provide, in one import:
11
+
12
+ * **transports** -- USBPRINT on Windows, libusb (own ``ctypes`` binding, no
13
+ dependency), PyUSB when installed, a raw ``/dev/usb/lp*`` device, and an
14
+ in-memory fake printer that is a full protocol implementation
15
+ (:class:`~epson_usb.backends.mock.MockPrinter`), so every layer above the
16
+ wire can be tested without hardware;
17
+ * **protocol** -- the D4 session (:class:`~epson_usb.d4.D4Session`), the
18
+ EPSON-CTRL frame grammar and the SNMP-OID bridge
19
+ (:func:`~epson_usb.epson_ctrl.parse_snmp_oid`), so the same program can run
20
+ over SNMP and over USB;
21
+ * **printer** -- :class:`~epson_usb.printer.EpsonUsbPrinter`, EEPROM read and
22
+ write, backups, status, serial numbers, the ``rw`` service command;
23
+ * **host integration** -- :func:`~epson_usb.compat.usb_printer`, which makes an
24
+ existing ``epson_print_conf`` install work over USB through its own
25
+ single entry point (``fetch_oid_values``).
26
+
27
+ The data-driven half -- which key belongs to which model, what a full counter
28
+ is, which cells a reset touches -- lives with the client. In this repository
29
+ that is ``epson_l3250.py`` (the L3250/ET-28xx tables, including the conversion
30
+ into ``epson_print_conf``'s parameter format) and ``epson_l3250_cli.py`` (the
31
+ command line interface built on them).
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from .__version__ import __version__
37
+ from .backends import (
38
+ available_backends,
39
+ backend_names,
40
+ describe_environment,
41
+ find_devices,
42
+ open_mock,
43
+ open_transport,
44
+ )
45
+ from .compat import (
46
+ UsbEpsonPrinterMixin,
47
+ load_epson_print_conf,
48
+ patch_epson_print_conf,
49
+ upstream_knows_model,
50
+ usb_printer,
51
+ )
52
+ from .d4 import D4Session, EpsonCtrlSession, Timeouts
53
+ from .eeprom import decode_counter, percentage
54
+ from .epson_ctrl import (
55
+ eeprom_read_frame,
56
+ eeprom_read_payload,
57
+ eeprom_write_frame,
58
+ eeprom_write_payload,
59
+ is_epson_ctrl_oid,
60
+ packet_command,
61
+ parse_snmp_oid,
62
+ rw_frame,
63
+ snmp_oid,
64
+ )
65
+ from .errors import (
66
+ AccessDeniedError,
67
+ BackendNotAvailableError,
68
+ DeviceBusyError,
69
+ DeviceNotFoundError,
70
+ D4Error,
71
+ EepromError,
72
+ EpsonUsbError,
73
+ NoReplyError,
74
+ ProtocolError,
75
+ TransportError,
76
+ WriteNotConfirmedError,
77
+ )
78
+ from .printer import CellReader, EpsonUsbPrinter, RestoreReport
79
+
80
+ __all__ = [
81
+ "__version__",
82
+ # high level
83
+ "EpsonUsbPrinter",
84
+ "CellReader",
85
+ "RestoreReport",
86
+ "open_printer",
87
+ # transports
88
+ "open_transport",
89
+ "open_mock",
90
+ "find_devices",
91
+ "backend_names",
92
+ "available_backends",
93
+ "describe_environment",
94
+ "D4Session",
95
+ "EpsonCtrlSession",
96
+ "Timeouts",
97
+ # protocol
98
+ "packet_command",
99
+ "snmp_oid",
100
+ "parse_snmp_oid",
101
+ "is_epson_ctrl_oid",
102
+ "eeprom_read_payload",
103
+ "eeprom_write_payload",
104
+ "eeprom_read_frame",
105
+ "eeprom_write_frame",
106
+ "rw_frame",
107
+ # EEPROM value conventions (no model data: see the module docstring)
108
+ "decode_counter",
109
+ "percentage",
110
+ # upstream integration
111
+ "usb_printer",
112
+ "patch_epson_print_conf",
113
+ "load_epson_print_conf",
114
+ "upstream_knows_model",
115
+ "UsbEpsonPrinterMixin",
116
+ # errors
117
+ "EpsonUsbError",
118
+ "TransportError",
119
+ "BackendNotAvailableError",
120
+ "DeviceNotFoundError",
121
+ "DeviceBusyError",
122
+ "AccessDeniedError",
123
+ "ProtocolError",
124
+ "NoReplyError",
125
+ "D4Error",
126
+ "EepromError",
127
+ "WriteNotConfirmedError",
128
+ ]
129
+
130
+
131
+ def open_printer(**kwargs) -> EpsonUsbPrinter:
132
+ """Open a printer and return it, already connected.
133
+
134
+ ``EpsonUsbPrinter(auto_open=True)`` does the same thing; this is the
135
+ function form, for call sites that prefer ``with open_printer() as p:``.
136
+
137
+ Keys are not implied: a caller that wants EEPROM access passes
138
+ ``read_key=``/``write_key=``, or uses one of its own model tables to supply
139
+ them, as the client modules in this repository do.
140
+ """
141
+ return EpsonUsbPrinter(**kwargs)
@@ -0,0 +1,7 @@
1
+ """Single source of truth for the package version.
2
+
3
+ Read by ``pyproject.toml`` (``dynamic = ["version"]``) without importing the
4
+ package, so it must not import anything.
5
+ """
6
+
7
+ __version__ = "0.1.0"