#!/usr/bin/env python3 """ epson_l1210.py -- READ-ONLY citanie EEPROM z EPSON L1210 cez IEEE-1284.4 (D4) Transport: Windows : usbprint.sys / GUID_DEVINTERFACE_USBPRINT (bez Zadig, bez WinUSB) Linux : /dev/usb/lp* (usblp) BEZPECNOST: Tento subor NEOBSAHUJE zapisovu cestu. Write opcode ('|','B' = 42 BD 21) tu nie je implementovany vobec. Nic sa neda resetovat ani omylom. Protokol: Ramcovanie a prikazy transaction-channelu su z IEEE 1284.4. EPSON-CTRL obalka a EEPROM opcody su verejne zdokumentovane (Ircama/epson_print_conf, atufi/reinkpy). Parametre L1210 (zdielane s ET-2800/ET-2810/L3210/L3250): read_key 0x364A, adresa 2 bajty, rozsah 0x000-0x7FF Pouzitie: python epson_l1210.py --waste # zname adresy pocitadiel python epson_l1210.py --read 2F 30 31 # konkretne adresy (hex) python epson_l1210.py --dump 0 FF # rozsah python epson_l1210.py --status # ST2 stav tlaciarne python epson_l1210.py --device 1 # iny index zo zoznamu """ import argparse import os import platform import re import struct import sys import time # --------------------------------------------------------------- L1210 spec READ_KEY = 0x364A ADDR_LEN = 2 MEM_LOW, MEM_HIGH = 0x000, 0x7FF # addr -> popis. Hodnoty v 'reset' su len INFORMATIVNE, nepouzivaju sa. WASTE_MAP = [ (0x2F, 'waste counter'), (0x30, 'waste counter (lo)'), (0x31, 'waste counter (hi)'), (0x32, 'waste counter (lo)'), (0x33, 'waste counter (hi)'), (0xFC, 'waste counter (lo)'), (0xFD, 'waste counter (hi)'), (0xFE, 'waste counter'), (0x1C, 'flag / counter [reset -> 0x00]'), (0x34, 'flag / counter [reset -> 0x00]'), (0x35, 'flag / counter [reset -> 0x00]'), (0x36, 'maintenance level [reset -> 0x5E]'), (0x37, 'maintenance level [reset -> 0x5E]'), (0xFF, 'maintenance level [reset -> 0x5E]'), ] COUNTER_PAIRS = [(0x30, 0x31), (0x32, 0x33), (0xFC, 0xFD)] LEVEL_ADDRS = (0x36, 0x37, 0xFF) LEVEL_FULL = 0x5E CMD_ENTER_D4 = b'\x00\x00\x00\x1b\x01@EJL 1284.4\n@EJL\n@EJL\n' EXPECT_D4_REPLY = b'\x00\x00\x00\x08\x01\x00\xc5\x00' def hexdump(data, width=16, indent=' '): if not data: return indent + '' out = [] for off in range(0, len(data), width): c = data[off:off + width] out.append('%s%04X %-*s %s' % ( indent, off, width * 3 - 1, ' '.join('%02X' % b for b in c), ''.join(chr(b) if 32 <= b < 127 else '.' for b in c))) return '\n'.join(out) # ============================================================= transporty class WinLink: """usbprint.sys cez CreateFile/ReadFile/WriteFile, citanie na vlakne.""" def __init__(self, path, hwid): self.path, self.hwid, self.h = path, hwid, None self._q = None @staticmethod def enumerate(): import ctypes from ctypes import wintypes api = ctypes.WinDLL('setupapi', use_last_error=True) class GUID(ctypes.Structure): _fields_ = [('D1', wintypes.DWORD), ('D2', wintypes.WORD), ('D3', wintypes.WORD), ('D4', ctypes.c_ubyte * 8)] class IFACE(ctypes.Structure): _fields_ = [('cbSize', wintypes.DWORD), ('guid', GUID), ('Flags', wintypes.DWORD), ('Reserved', ctypes.POINTER(ctypes.c_ulonglong))] class DEVINFO(ctypes.Structure): _fields_ = [('cbSize', wintypes.DWORD), ('ClassGuid', GUID), ('DevInst', wintypes.DWORD), ('Reserved', ctypes.POINTER(ctypes.c_ulonglong))] guid = GUID(0x28D78FAD, 0x5A12, 0x11D1, (ctypes.c_ubyte * 8)(0xAE, 0x5B, 0, 0, 0xF8, 0x03, 0xA8, 0xC2)) api.SetupDiGetClassDevsW.restype = ctypes.c_void_p hdev = api.SetupDiGetClassDevsW(ctypes.byref(guid), None, None, 0x12) if hdev in (None, ctypes.c_void_p(-1).value): raise OSError('SetupDiGetClassDevs: %d' % ctypes.get_last_error()) found, i = [], 0 try: while True: ifc = IFACE() ifc.cbSize = ctypes.sizeof(ifc) if not api.SetupDiEnumDeviceInterfaces( ctypes.c_void_p(hdev), None, ctypes.byref(guid), i, ctypes.byref(ifc)): break i += 1 need = wintypes.DWORD(0) api.SetupDiGetDeviceInterfaceDetailW( ctypes.c_void_p(hdev), ctypes.byref(ifc), None, 0, ctypes.byref(need), None) if not need.value: continue buf = ctypes.create_string_buffer(need.value) ctypes.memmove(buf, (8 if ctypes.sizeof(ctypes.c_void_p) == 8 else 6).to_bytes(4, 'little'), 4) di = DEVINFO() di.cbSize = ctypes.sizeof(di) if not api.SetupDiGetDeviceInterfaceDetailW( ctypes.c_void_p(hdev), ctypes.byref(ifc), ctypes.cast(buf, ctypes.c_void_p), need.value, None, ctypes.byref(di)): continue path = ctypes.wstring_at(ctypes.addressof(buf) + 4) pb = ctypes.create_string_buffer(1024) hwid = '' if api.SetupDiGetDeviceRegistryPropertyW( ctypes.c_void_p(hdev), ctypes.byref(di), 0x01, None, ctypes.cast(pb, ctypes.c_void_p), 1024, None): hwid = ctypes.wstring_at(ctypes.addressof(pb)) found.append(WinLink(path, hwid)) finally: api.SetupDiDestroyDeviceInfoList(ctypes.c_void_p(hdev)) return found def label(self): return '%s [%s]' % (self.hwid or '?', self.path) def open(self): import ctypes import queue import threading k32 = ctypes.WinDLL('kernel32', use_last_error=True) k32.CreateFileW.restype = ctypes.c_void_p h = k32.CreateFileW(self.path, 0xC0000000, 0x03, None, 3, 0, None) if h == ctypes.c_void_p(-1).value: e = ctypes.get_last_error() raise OSError('CreateFile zlyhalo (%d)%s' % ( e, ' -- zariadenie prave pouziva ina aplikacia alebo spooler' if e == 32 else '')) self.h = h self._q = queue.Queue() self._stop = False t = threading.Thread(target=self._reader, daemon=True) t.start() return self def _reader(self): import ctypes from ctypes import wintypes k32 = ctypes.WinDLL('kernel32', use_last_error=True) buf = ctypes.create_string_buffer(1024) n = wintypes.DWORD(0) while not self._stop: if not k32.ReadFile(ctypes.c_void_p(self.h), buf, 1024, ctypes.byref(n), None): break if n.value: self._q.put(buf.raw[:n.value]) def write(self, data): import ctypes from ctypes import wintypes k32 = ctypes.WinDLL('kernel32', use_last_error=True) n = wintypes.DWORD(0) if not k32.WriteFile(ctypes.c_void_p(self.h), data, len(data), ctypes.byref(n), None): raise OSError('WriteFile: %d' % ctypes.get_last_error()) return n.value def read(self, timeout=2.0): import queue try: return self._q.get(timeout=timeout) except queue.Empty: return b'' def close(self): if self.h: import ctypes self._stop = True k32 = ctypes.WinDLL('kernel32') try: k32.CancelIoEx(ctypes.c_void_p(self.h), None) except Exception: pass k32.CloseHandle(ctypes.c_void_p(self.h)) self.h = None class LinuxLink: def __init__(self, path): self.path, self.fd = path, None @staticmethod def enumerate(): out = [] for d in ('/dev/usb', '/dev'): if os.path.isdir(d): for n in sorted(os.listdir(d)): if n.startswith('lp') and n[2:].isdigit(): out.append(LinuxLink(os.path.join(d, n))) return out def label(self): return self.path def open(self): try: self.fd = os.open(self.path, os.O_RDWR) except PermissionError: raise OSError('%s: pristup zamietnuty. sudo usermod -aG lp $USER' % self.path) return self def write(self, data): return os.write(self.fd, data) def read(self, timeout=2.0): import select r, _, _ = select.select([self.fd], [], [], timeout) return os.read(self.fd, 1024) if r else b'' def close(self): if self.fd is not None: os.close(self.fd) self.fd = None # ============================================================= D4 vrstva HDR = struct.Struct('>BBHBB') # psid, ssid, length, credit, control HDR_LEN = 6 TX_CID = (0x00, 0x00) class D4Error(Exception): pass class D4: """IEEE 1284.4 transaction/data kanaly nad bajtovym linkom.""" def __init__(self, link, verbose=False): self.link = link self.verbose = verbose self.buf = b'' self.credit = {TX_CID: 0} def _log(self, arrow, data): if self.verbose: print(' %s %s' % (arrow, data.hex(' ').upper())) # ---- ramcovanie def _send_packet(self, cid, payload, credit=1, control=0): pkt = HDR.pack(cid[0], cid[1], HDR_LEN + len(payload), credit, control) + payload self._log('<<', pkt) self.link.write(pkt) def _recv_packet(self, timeout=2.0): deadline = time.time() + timeout while True: if len(self.buf) >= HDR_LEN: psid, ssid, length, credit, control = HDR.unpack(self.buf[:HDR_LEN]) if length < HDR_LEN or length > 0x2000: raise D4Error('nezmyselna dlzka paketu: %d' % length) if len(self.buf) >= length: pkt, self.buf = self.buf[:length], self.buf[length:] self._log('>>', pkt) cid = (psid, ssid) self.credit[cid] = self.credit.get(cid, 0) + credit return cid, pkt[HDR_LEN:] if time.time() >= deadline: return None, b'' chunk = self.link.read(timeout=max(0.05, deadline - time.time())) if chunk: self.buf += chunk def _txn(self, payload, expect_code, cost=1, timeout=3.0): """Posli prikaz na transaction channel a pockaj na odpoved.""" if cost and self.credit.get(TX_CID, 0) < cost: # Kredit na TX kanali chodi piggyback v hlavicke odpovedi. # Ak sme ziadny nedostali, skusame aj tak -- tlaciarne to tolreuju. self.credit[TX_CID] = cost self.credit[TX_CID] = self.credit.get(TX_CID, 0) - cost self._send_packet(TX_CID, payload) for _ in range(8): cid, data = self._recv_packet(timeout) if cid is None: break if not data: continue # paket len s kreditom if cid == TX_CID and data[0] == expect_code: return data[1:] raise D4Error('nedosla odpoved 0x%02X na prikaz 0x%02X' % (expect_code, payload[0])) # ---- prikazy def enter(self): self.link.write(CMD_ENTER_D4) reply = b'' for _ in range(5): reply += self.link.read(timeout=2.0) if EXPECT_D4_REPLY in reply: break else: raise D4Error('EJL exit-packet-mode zlyhal, odpoved: %r' % reply) return self.init() def init(self, revision=0x20): r = self._txn(struct.pack('>BB', 0x00, revision), 0x80, cost=0) result, rev = struct.unpack('>BB', r[:2]) if result == 0x02 and rev != revision: return self.init(rev) if result != 0x00: raise D4Error('Init odmietnuty, result=0x%02X' % result) return rev def get_socket_id(self, name): r = self._txn(b'\x09' + name.encode('ascii'), 0x89) result, sid = struct.unpack('>BB', r[:2]) if result != 0x00: raise D4Error('GetSocketID(%s) result=0x%02X' % (name, result)) return sid def open_channel(self, cid, max_pts=0x0100, max_stp=0x0100): r = self._txn(struct.pack('>BBBHHH', 0x01, cid[0], cid[1], max_pts, max_stp, 0x0000), 0x81) result = r[0] if result != 0x00: raise D4Error('OpenChannel%s result=0x%02X' % (cid, result)) if len(r) >= 11: granted = struct.unpack('>H', r[9:11])[0] self.credit[cid] = self.credit.get(cid, 0) + granted return result def credit_request(self, cid, want=1): r = self._txn(struct.pack('>BBBH', 0x04, cid[0], cid[1], want), 0x84) result = r[0] add = struct.unpack('>H', r[3:5])[0] if len(r) >= 5 else 0 self.credit[cid] = self.credit.get(cid, 0) + add return result, add def close_channel(self, cid): try: self._txn(struct.pack('>BBB', 0x02, cid[0], cid[1]), 0x82) except D4Error: pass def exit(self): try: self._txn(b'\x08', 0x88) except D4Error: pass def exchange(self, cid, payload, timeout=3.0): """Posli data na kanal a vrat odpoved z toho isteho kanala.""" if self.credit.get(cid, 0) < 1: self.credit_request(cid, 1) self.credit[cid] = self.credit.get(cid, 0) - 1 self._send_packet(cid, payload) for _ in range(8): rcid, data = self._recv_packet(timeout) if rcid == cid and data: return data if rcid is None: break return b'' # ============================================================= EPSON-CTRL CTRL_CID = (0x02, 0x02) def encode_factory(op, payload): """'||' obalka + read_key + opcode triplet + payload.""" c = ord(op) body = struct.pack('> 1 & 0x7F) | (c << 7 & 0x80)) + payload return b'||' + struct.pack('HB', bytes.fromhex(m.group(1).decode())) if got_addr != addr: return None, resp return val, resp def status(self): return self.d4.exchange(self.cid, encode_simple('st', b'\x01')) # ============================================================= main def pick_link(index): system = platform.system() if system == 'Windows': links = WinLink.enumerate() elif system == 'Linux': links = LinuxLink.enumerate() else: sys.exit('macOS: pouzi pyusb, usbprint/usblp tu neexistuje.') if not links: sys.exit('Ziadna USB tlaciaren nenajdena.') print('Najdene zariadenia:') for i, l in enumerate(links): print(' [%d] %s' % (i, l.label())) if index is None: index = next((i for i, l in enumerate(links) if '130B' in (getattr(l, 'hwid', '') or '').upper()), 0) print('--- pouzivam [%d] ---\n' % index) return links[index] def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument('--device', type=int, default=None) ap.add_argument('--waste', action='store_true', help='zname adresy pocitadiel') ap.add_argument('--read', nargs='+', metavar='HEX', help='konkretne adresy') ap.add_argument('--dump', nargs=2, metavar=('OD', 'DO'), help='rozsah adries') ap.add_argument('--status', action='store_true', help='ST2 stav') ap.add_argument('-v', '--verbose', action='store_true', help='hexdump paketov') args = ap.parse_args() if not (args.waste or args.read or args.dump or args.status): args.waste = True print('=' * 68) print(' epson_l1210 -- READ ONLY (ziadna zapisova cesta nie je v kode)') print(' %s %s Python %s' % (platform.system(), platform.machine(), platform.python_version())) print('=' * 68) link = pick_link(args.device).open() d4 = D4(link, verbose=args.verbose) cid = CTRL_CID opened = False try: rev = d4.enter() print('D4 Init OK, revizia protokolu 0x%02X' % rev) try: sid = d4.get_socket_id('EPSON-CTRL') print('EPSON-CTRL socket ID = 0x%02X' % sid) cid = (sid, sid) except D4Error as e: print('GetSocketID zlyhalo (%s), skusam fixne %s' % (e, (CTRL_CID,))) ctrl = Ctrl(d4, cid) d4.open_channel(cid) opened = True print('Kanal %s otvoreny, kredit=%d\n' % (cid, d4.credit.get(cid, 0))) if args.status: r = ctrl.status() print('ST2 odpoved (%d B):' % len(r)) print(hexdump(r)) print() addrs, labels = [], {} if args.waste: addrs = [a for a, _ in WASTE_MAP] labels = dict(WASTE_MAP) if args.read: addrs += [int(x, 16) for x in args.read] if args.dump: lo, hi = int(args.dump[0], 16), int(args.dump[1], 16) if not (MEM_LOW <= lo <= hi <= MEM_HIGH): sys.exit('rozsah musi byt v 0x%03X-0x%03X' % (MEM_LOW, MEM_HIGH)) addrs += list(range(lo, hi + 1)) if addrs: print('%-8s %-6s %s' % ('ADRESA', 'HODN.', 'POPIS')) print('-' * 68) values = {} fails = 0 for a in addrs: v, raw = ctrl.read_eeprom(a) values[a] = v if v is None: fails += 1 note = ' <-- ziadna/nevalidna odpoved' if raw: note += ' raw=%r' % raw[:48] print('0x%03X -- %s%s' % (a, labels.get(a, ''), note)) else: print('0x%03X 0x%02X %-3d %s' % (a, v, v, labels.get(a, ''))) if args.waste and fails == 0: print('\n' + '-' * 68) print('Odvodene hodnoty:') for lo_a, hi_a in COUNTER_PAIRS: if values.get(lo_a) is not None and values.get(hi_a) is not None: n = values[lo_a] | (values[hi_a] << 8) print(' 0x%03X/0x%03X -> %5d (0x%04X)' % (lo_a, hi_a, n, n)) lv = [values.get(a) for a in LEVEL_ADDRS] if all(x is not None for x in lv): full = all(x == LEVEL_FULL for x in lv) print(' maintenance level 0x36/0x37/0xFF = %s -> %s' % (', '.join('0x%02X' % x for x in lv), 'na prahu (0x5E)' if full else 'pod prahom')) print('\nPoznamka: prepocet na % nie je mozny -- divider pre L1210') print('nie je v ziadnej verejnej databaze. Treba ho odvodit') print('porovnanim hodnot pred/po vycisteni hlavy.') if fails: print('\n%d z %d adries neodpovedalo.' % (fails, len(addrs))) print('Ak neodpoveda ZIADNA, firmvér L1210 pravdepodobne') print('EEPROM pristup blokuje -- rovnako ako u ET-2800/L3250.') except D4Error as e: print('\nD4 CHYBA: %s' % e) return 1 finally: if opened: d4.close_channel(cid) d4.exit() link.close() return 0 if __name__ == '__main__': sys.exit(main())