#!/usr/bin/env python3 """ epson_probe.py -- read-only diagnostika Epson USB tlaciarne (cielene EPSON L1210) Co robi: 1. Najde pripojene USB tlaciarne cez natívny kanál OS Windows : usbprint.sys / GUID_DEVINTERFACE_USBPRINT (BEZ Zadig, BEZ WinUSB) Linux : /dev/usb/lp* (usblp) macOS : nema device node -> vypise navod na pyusb 2. Precita IEEE-1284 Device ID (vyrobca, model, command set) [read-only] 3. Volitelne posle EJL "exit packet mode" sekvenciu a ukaze odpoved -> test obojsmernosti kanala Co NEROBI: Nezapisuje do EEPROM. Neresetuje ziadne pocitadlo. Nemeni nastavenia tlaciarne. Pouzitie: python3 epson_probe.py # zoznam + Device ID python3 epson_probe.py --d4 # + test EJL handshake (obojsmernost) python3 epson_probe.py --device N # vyber konkretne zariadenie zo zoznamu python3 epson_probe.py --send 1B0140 # raw hex (zapisy do EEPROM su blokovane) """ import argparse import os import platform import sys # ---------------------------------------------------------------- konstanty # "Exit packet mode" / vstup do D4 (IEEE-1284.4). Posiela ho aj bezny ovladac. 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' # Signatura factory-write prikazu ('|','B'): 0x42, ~0x42, 0x42>>1 WRITE_OPCODE_SIG = b'\x42\xbd\x21' EPSON_VID = 0x04B8 def hexdump(data, width=16): if not data: return ' ' out = [] for off in range(0, len(data), width): chunk = data[off:off + width] hexs = ' '.join('%02X' % b for b in chunk) text = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk) out.append(' %04X %-*s %s' % (off, width * 3 - 1, hexs, text)) return '\n'.join(out) def parse_1284_id(raw): """IEEE-1284 Device ID -> dict. Prvé 2 bajty su big-endian dlzka.""" if len(raw) >= 2 and raw[0] < 8: # ma dlzkovu hlavicku n = (raw[0] << 8) | raw[1] raw = raw[2:n] if 2 < n <= len(raw) else raw[2:] text = raw.decode('ascii', 'replace').strip().strip('\x00') fields = {} for part in text.split(';'): if ':' in part: k, _, v = part.partition(':') fields[k.strip().upper()] = v.strip() return text, fields def guard_raw(data): """Odmietni cokolvek, co vyzera ako zapis do EEPROM.""" if b'||' in data and WRITE_OPCODE_SIG in data: sys.exit('ODMIETNUTE: sekvencia obsahuje EEPROM write opcode (42 BD 21).\n' 'Tato sonda je zamerne read-only.') # ================================================================ Windows class WindowsPrinter: """Pristup cez usbprint.sys -- bez vymeny ovladaca.""" def __init__(self, path, hwid, handle=None): self.path, self.hwid, self.h = path, hwid, handle @staticmethod def enumerate(): import ctypes from ctypes import wintypes setupapi = ctypes.WinDLL('setupapi', use_last_error=True) class GUID(ctypes.Structure): _fields_ = [('Data1', wintypes.DWORD), ('Data2', wintypes.WORD), ('Data3', wintypes.WORD), ('Data4', ctypes.c_ubyte * 8)] class SP_DEVICE_INTERFACE_DATA(ctypes.Structure): _fields_ = [('cbSize', wintypes.DWORD), ('InterfaceClassGuid', GUID), ('Flags', wintypes.DWORD), ('Reserved', ctypes.POINTER(ctypes.c_ulonglong))] class SP_DEVINFO_DATA(ctypes.Structure): _fields_ = [('cbSize', wintypes.DWORD), ('ClassGuid', GUID), ('DevInst', wintypes.DWORD), ('Reserved', ctypes.POINTER(ctypes.c_ulonglong))] # {28d78fad-5a12-11D1-ae5b-0000f803a8c2} guid = GUID(0x28D78FAD, 0x5A12, 0x11D1, (ctypes.c_ubyte * 8)(0xAE, 0x5B, 0x00, 0x00, 0xF8, 0x03, 0xA8, 0xC2)) DIGCF_PRESENT, DIGCF_DEVICEINTERFACE = 0x02, 0x10 SPDRP_HARDWAREID = 0x01 INVALID = ctypes.c_void_p(-1).value setupapi.SetupDiGetClassDevsW.restype = ctypes.c_void_p hdev = setupapi.SetupDiGetClassDevsW( ctypes.byref(guid), None, None, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE) if hdev in (None, INVALID): raise OSError('SetupDiGetClassDevs zlyhalo: %d' % ctypes.get_last_error()) found = [] try: idx = 0 while True: iface = SP_DEVICE_INTERFACE_DATA() iface.cbSize = ctypes.sizeof(iface) if not setupapi.SetupDiEnumDeviceInterfaces( ctypes.c_void_p(hdev), None, ctypes.byref(guid), idx, ctypes.byref(iface)): break idx += 1 need = wintypes.DWORD(0) setupapi.SetupDiGetDeviceInterfaceDetailW( ctypes.c_void_p(hdev), ctypes.byref(iface), None, 0, ctypes.byref(need), None) if not need.value: continue buf = ctypes.create_string_buffer(need.value) # cbSize: 8 na x64, 6 na x86 (NIE velkost celeho bufferu) ctypes.memmove(buf, (8 if ctypes.sizeof(ctypes.c_void_p) == 8 else 6).to_bytes(4, 'little'), 4) devinfo = SP_DEVINFO_DATA() devinfo.cbSize = ctypes.sizeof(devinfo) if not setupapi.SetupDiGetDeviceInterfaceDetailW( ctypes.c_void_p(hdev), ctypes.byref(iface), ctypes.cast(buf, ctypes.c_void_p), need.value, None, ctypes.byref(devinfo)): continue path = ctypes.wstring_at(ctypes.addressof(buf) + 4) hwid = '' pbuf = ctypes.create_string_buffer(1024) if setupapi.SetupDiGetDeviceRegistryPropertyW( ctypes.c_void_p(hdev), ctypes.byref(devinfo), SPDRP_HARDWAREID, None, ctypes.cast(pbuf, ctypes.c_void_p), 1024, None): hwid = ctypes.wstring_at(ctypes.addressof(pbuf)) found.append(WindowsPrinter(path, hwid)) finally: setupapi.SetupDiDestroyDeviceInfoList(ctypes.c_void_p(hdev)) return found def open(self): import ctypes k32 = ctypes.WinDLL('kernel32', use_last_error=True) k32.CreateFileW.restype = ctypes.c_void_p GENERIC_RW, SHARE_RW, OPEN_EXISTING = 0xC0000000, 0x03, 3 h = k32.CreateFileW(self.path, GENERIC_RW, SHARE_RW, None, OPEN_EXISTING, 0, None) if h == ctypes.c_void_p(-1).value: err = ctypes.get_last_error() raise OSError('CreateFile zlyhalo (chyba %d)%s' % ( err, ' -- zariadenie drzi ina aplikacia alebo spooler tlaci' if err == 32 else '')) self.h = h return self def device_id(self): """IOCTL_USBPRINT_GET_1284_ID = CTL_CODE(FILE_DEVICE_USB, 13, BUFFERED, ANY)""" import ctypes from ctypes import wintypes k32 = ctypes.WinDLL('kernel32', use_last_error=True) buf = ctypes.create_string_buffer(1024) ret = wintypes.DWORD(0) ok = k32.DeviceIoControl(ctypes.c_void_p(self.h), 0x00220034, None, 0, buf, 1024, ctypes.byref(ret), None) if not ok: raise OSError('IOCTL_USBPRINT_GET_1284_ID zlyhalo: %d' % ctypes.get_last_error()) return buf.raw[:ret.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 zlyhalo: %d' % ctypes.get_last_error()) return n.value def read(self, size=512): import ctypes from ctypes import wintypes k32 = ctypes.WinDLL('kernel32', use_last_error=True) buf = ctypes.create_string_buffer(size) n = wintypes.DWORD(0) if not k32.ReadFile(ctypes.c_void_p(self.h), buf, size, ctypes.byref(n), None): raise OSError('ReadFile zlyhalo: %d' % ctypes.get_last_error()) return buf.raw[:n.value] def close(self): if self.h: import ctypes ctypes.WinDLL('kernel32').CloseHandle(ctypes.c_void_p(self.h)) self.h = None def label(self): return '%s\n hwid: %s' % (self.path, self.hwid or '?') # ================================================================ Linux class LinuxPrinter: """Pristup cez usblp (/dev/usb/lpN).""" def __init__(self, path): self.path, self.fd = path, None @staticmethod def enumerate(): out = [] for d in ('/dev/usb', '/dev'): if not os.path.isdir(d): continue for name in sorted(os.listdir(d)): if name.startswith('lp') and name[2:].isdigit(): out.append(LinuxPrinter(os.path.join(d, name))) return out def open(self): try: self.fd = os.open(self.path, os.O_RDWR) except PermissionError: raise OSError('%s: pristup zamietnuty -- pridaj sa do skupiny lp:\n' ' sudo usermod -aG lp $USER (a odhlas/prihlas sa)' % self.path) return self def device_id(self): import fcntl size = 1024 # LPIOC_GET_DEVICE_ID(len) = _IOC(_IOC_READ, 'P', 1, len) req = (2 << 30) | (size << 16) | (ord('P') << 8) | 1 buf = bytearray(size) fcntl.ioctl(self.fd, req, buf) return bytes(buf) def write(self, data): return os.write(self.fd, data) def read(self, size=512, timeout=3.0): import select r, _, _ = select.select([self.fd], [], [], timeout) return os.read(self.fd, size) if r else b'' def close(self): if self.fd is not None: os.close(self.fd) self.fd = None def label(self): return self.path # ================================================================ main def enumerate_devices(): system = platform.system() if system == 'Windows': return WindowsPrinter.enumerate() if system == 'Linux': return LinuxPrinter.enumerate() if system == 'Darwin': print('macOS nema device node pre tlaciarne -- usbprint/usblp ekvivalent') print('neexistuje. Pouzi pyusb:\n') print(' pip install pyusb') print(' python3 -c "import usb.core; d=usb.core.find(idVendor=0x04b8);' ' print(d)"\n') print('Ak CUPS drzi tlaciaren, pozastav tlacovy front:') print(' cupsdisable ') return [] print('Nepodporovany OS: %s' % system) return [] def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument('--device', type=int, default=None, help='index zariadenia zo zoznamu (default: prve Epson)') ap.add_argument('--d4', action='store_true', help='posli EJL exit-packet-mode a ukaz odpoved') ap.add_argument('--send', metavar='HEX', help='posli raw hex sekvenciu a precitaj odpoved') args = ap.parse_args() print('=' * 68) print(' epson_probe -- READ ONLY (nezapisuje do EEPROM)') print(' OS: %s %s Python %s' % (platform.system(), platform.machine(), platform.python_version())) print('=' * 68) devices = enumerate_devices() if not devices: print('\nZiadna USB tlaciaren nenajdena.') print('Skontroluj ze je zapnuta a pripojena cez USB.') return 1 print('\nNajdene zariadenia:') for i, d in enumerate(devices): print(' [%d] %s' % (i, d.label())) idx = args.device if idx is None: idx = next((i for i, d in enumerate(devices) if '04B8' in (getattr(d, 'hwid', '') or '').upper()), 0) dev = devices[idx] print('\n--- pouzivam [%d] ---' % idx) try: dev.open() except OSError as e: print('\nOTVORENIE ZLYHALO: %s' % e) return 1 rc = 0 try: # --- 1. IEEE-1284 Device ID ------------------------------------- print('\n[1] IEEE-1284 Device ID') try: raw = dev.device_id() text, fields = parse_1284_id(raw) print(' %s' % text) if fields: print() for k in ('MFG', 'MDL', 'CMD', 'DES', 'SN'): if k in fields: print(' %-4s = %s' % (k, fields[k])) mdl = fields.get('MDL', '') if 'L1210' in mdl: print('\n >> L1210 potvrdena.') elif mdl: print('\n >> Model hlasi: %s (ocakaval som L1210)' % mdl) except Exception as e: print(' ZLYHALO: %s' % e) rc = 1 # --- 2. EJL handshake ------------------------------------------ if args.d4: print('\n[2] EJL exit-packet-mode (test obojsmernosti)') print(' TX (%d B):' % len(CMD_ENTER_D4)) print(hexdump(CMD_ENTER_D4)) try: dev.write(CMD_ENTER_D4) reply = dev.read(64) print(' RX (%d B):' % len(reply)) print(hexdump(reply)) if reply == EXPECT_D4_REPLY: print('\n >> PRESNA ZHODA s ocakavanou D4 odpovedou.') print(' >> Kanal je obojsmerny. Cesta k EEPROM je otvorena.') elif reply: print('\n >> Odpoved prisla, ale je ina nez ocakavana.') print(' >> Obojsmernost funguje; D4 framing treba doladit.') else: print('\n >> ZIADNA ODPOVED (timeout).') print(' >> Bud tlaciaren neodpoveda, alebo kanal je len') print(' >> jednosmerny. Toto je stop-signal pre projekt.') rc = 2 except Exception as e: print(' ZLYHALO: %s' % e) rc = 1 # --- 3. raw ------------------------------------------------------ if args.send: data = bytes.fromhex(args.send.replace(' ', '')) guard_raw(data) print('\n[3] RAW send') print(' TX (%d B):' % len(data)) print(hexdump(data)) dev.write(data) reply = dev.read(512) print(' RX (%d B):' % len(reply)) print(hexdump(reply)) finally: dev.close() print('\n' + '=' * 68) print({0: 'OK', 1: 'CIASTOCNE ZLYHANIE', 2: 'KANAL NEODPOVEDA'}[rc]) print('=' * 68) return rc if __name__ == '__main__': sys.exit(main())