#!/usr/bin/env python3 """ epson_survey.py -- kompletny READ-ONLY zber dat z EPSON L1210 (jedna navsteva) Zoberie naraz vsetko, co sa neskor moze hodit, a ulozi do dvoch suborov: epson_survey__.json strojovo citatelny report epson_survey__.log kompletny textovy prepis vratane hexdumpov BEZPECNOST -- co tento skript NEROBI: * neobsahuje EEPROM write opcode ('|','B') vobec * neposiela ziadny prikaz, ktory by mohol menit nastavenia (vynechany je aj 'ot' / power-off timer, lebo ten vie aj zapisovat) * jedine bajty, ktore odchadzaju, su EJL init, D4 riadiace pakety a EEPROM READ dotazy Sekcie zberu: 1. prostredie a zoznam USB tlaciarni (VID/PID/serial) 2. IEEE-1284 Device ID 3. D4 init + revizia protokolu + casovanie 4. GetSocketID pre vsetky zname EPSON sluzby 5. read-only EPSON-CTRL prikazy (st, vi, di, ia, ii) -- raw odpovede 6. kompletny dump EEPROM 0x000-0x7FF 7. ak zlyha znamy read_key -> sweep 170 znamych kandidatov 8. statistiky odoziev Pouzitie: python epson_survey.py # vsetko (par minut) python epson_survey.py --quick # bez plneho dumpu python epson_survey.py --dump-to 0x1FF # skrateny dump python epson_survey.py --device 1 """ import argparse import binascii import datetime import json import os import platform import re import socket import struct import sys import time VERSION = '1.0' # --------------------------------------------------------------- L1210 spec KNOWN_READ_KEY = 0x364A ADDR_LEN = 2 MEM_LOW, MEM_HIGH = 0x000, 0x7FF WASTE_MAP = { 0x1C: 'flag/counter [reset -> 0x00]', 0x2F: 'waste counter', 0x30: 'waste counter (lo)', 0x31: 'waste counter (hi)', 0x32: 'waste counter (lo)', 0x33: 'waste counter (hi)', 0x34: 'flag/counter [reset -> 0x00]', 0x35: 'flag/counter [reset -> 0x00]', 0x36: 'maintenance level [reset -> 0x5E]', 0x37: 'maintenance level [reset -> 0x5E]', 0xFC: 'waste counter (lo)', 0xFD: 'waste counter (hi)', 0xFE: 'waste counter', 0xFF: 'maintenance level [reset -> 0x5E]', } COUNTER_PAIRS = [(0x30, 0x31), (0x32, 0x33), (0xFC, 0xFD)] LEVEL_ADDRS = (0x36, 0x37, 0xFF) LEVEL_FULL = 0x5E # vsetky distinktne read_key z verejnej databazy modelov (170 ks) CANDIDATE_KEYS = [ 0x0001, 0x0005, 0x0006, 0x000D, 0x0013, 0x0015, 0x0028, 0x0065, 0x0066, 0x0069, 0x0077, 0x0106, 0x0143, 0x0144, 0x0145, 0x0161, 0x0163, 0x0168, 0x0169, 0x0175, 0x0193, 0x0264, 0x0282, 0x0301, 0x0338, 0x0371, 0x0378, 0x0388, 0x0392, 0x0395, 0x0406, 0x0414, 0x0443, 0x0450, 0x0474, 0x0479, 0x0501, 0x0504, 0x0508, 0x0511, 0x0518, 0x0528, 0x0533, 0x0539, 0x0543, 0x054A, 0x0551, 0x0553, 0x0555, 0x0557, 0x0565, 0x0585, 0x0596, 0x0604, 0x0607, 0x0614, 0x0619, 0x0624, 0x0625, 0x0631, 0x0636, 0x0637, 0x0649, 0x0704, 0x0718, 0x0719, 0x0720, 0x0736, 0x0768, 0x0789, 0x0797, 0x0810, 0x0823, 0x0824, 0x0832, 0x0837, 0x0849, 0x0855, 0x0872, 0x0877, 0x0879, 0x0881, 0x0882, 0x0886, 0x0928, 0x0941, 0x0946, 0x0950, 0x0953, 0x0972, 0x0976, 0x0981, 0x0986, 0x0987, 0x0A0D, 0x0C0D, 0x0C0F, 0x0E10, 0x1108, 0x110F, 0x120E, 0x1404, 0x1413, 0x1504, 0x1542, 0x1606, 0x1710, 0x1713, 0x1808, 0x180D, 0x184A, 0x1907, 0x1A07, 0x1E09, 0x1F0D, 0x2010, 0x204A, 0x220B, 0x234B, 0x240D, 0x2710, 0x2810, 0x2A06, 0x2B0D, 0x2D08, 0x2D0B, 0x3010, 0x310C, 0x3208, 0x330B, 0x334B, 0x340C, 0x354B, 0x364A, 0x364B, 0x370E, 0x3848, 0x394B, 0x3B10, 0x3E11, 0x450E, 0x4513, 0x4548, 0x460E, 0x4C04, 0x4C07, 0x4D04, 0x4E04, 0x4F10, 0x5010, 0x5110, 0x5202, 0x520C, 0x520E, 0x530A, 0x5311, 0x540B, 0x5411, 0x560F, 0x574B, 0x5807, 0x5B0E, 0x5E0A, 0x5F11, 0x600F, 0x6241, 0x6300, 0x6311, 0x884A, 0xE6B5, ] EPSON_SERVICES = [ 'EPSON-CTRL', 'EPSON-DATA', 'EPSON-SCAN', 'EPSON-STAT', 'EPSON-USBDEVMGR', 'EPSON-EPSONNET', 'EPSON-FAX', 'EPSON-STORAGE', ] # read-only informacne prikazy. 'ot' je zamerne vynechany (vie aj zapisovat). INFO_COMMANDS = [ ('st', b'\x01', 'printer status (ST2)'), ('vi', b'', 'version info'), ('di', b'', 'device id'), ('ia', b'', 'cartridge types'), ('ii', b'', 'cartridge properties'), ] 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' # ============================================================= logovanie class Tee: def __init__(self, path): self.f = open(path, 'w', encoding='utf-8') def __call__(self, *parts): line = ' '.join(str(p) for p in parts) print(line) self.f.write(line + '\n') self.f.flush() def quiet(self, *parts): """len do suboru, nie na obrazovku""" self.f.write(' '.join(str(p) for p in parts) + '\n') def close(self): self.f.close() 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: kind = 'usbprint.sys' def __init__(self, path, hwid): self.path, self.hwid, self.h, self._q = path, hwid, None, 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) hwid = '' pb = ctypes.create_string_buffer(1024) 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 describe(self): d = {'kind': self.kind, 'path': self.path, 'hardware_id': self.hwid} m = re.search(r'VID_([0-9A-Fa-f]{4})&PID_([0-9A-Fa-f]{4})', self.hwid or '') if m: d['vid'] = '0x' + m.group(1).upper() d['pid'] = '0x' + m.group(2).upper() m = re.search(r'#([0-9a-fA-F]+)#\{', self.path or '') if m: try: d['serial_hex'] = m.group(1) d['serial'] = bytes.fromhex(m.group(1)).decode('ascii') except Exception: pass return d 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 pouziva ina aplikacia alebo spooler' if e == 32 else '')) self.h, self._q, self._stop = h, queue.Queue(), False threading.Thread(target=self._reader, daemon=True).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(2048) n = wintypes.DWORD(0) while not self._stop: if not k32.ReadFile(ctypes.c_void_p(self.h), buf, 2048, ctypes.byref(n), None): break if n.value: self._q.put(buf.raw[:n.value]) def device_id(self): import ctypes from ctypes import wintypes k32 = ctypes.WinDLL('kernel32', use_last_error=True) buf = ctypes.create_string_buffer(1024) ret = wintypes.DWORD(0) if not k32.DeviceIoControl(ctypes.c_void_p(self.h), 0x00220034, None, 0, buf, 1024, ctypes.byref(ret), None): raise OSError('IOCTL_USBPRINT_GET_1284_ID: %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: %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: kind = 'usblp' 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 describe(self): return {'kind': self.kind, 'path': 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 device_id(self): import fcntl size = 1024 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, timeout=2.0): import select r, _, _ = select.select([self.fd], [], [], timeout) return os.read(self.fd, 2048) if r else b'' def close(self): if self.fd is not None: os.close(self.fd) self.fd = None # ============================================================= D4 HDR = struct.Struct('>BBHBB') HDR_LEN = 6 TX_CID = (0x00, 0x00) class D4Error(Exception): pass class D4: def __init__(self, link, log, verbose=False): self.link, self.log, self.verbose = link, log, verbose self.buf = b'' self.credit = {TX_CID: 0} self.timings = [] def _trace(self, arrow, data): line = ' %s %s' % (arrow, data.hex(' ').upper()) if self.verbose: self.log(line) else: self.log.quiet(line) 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._trace('<<', 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, _ctl = HDR.unpack(self.buf[:HDR_LEN]) if length < HDR_LEN or length > 0x4000: self.buf = b'' raise D4Error('nezmyselna dlzka paketu: %d' % length) if len(self.buf) >= length: pkt, self.buf = self.buf[:length], self.buf[length:] self._trace('>>', 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): if cost and self.credit.get(TX_CID, 0) < cost: 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 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])) 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: %r' % reply) return reply 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, timeout=2.0) result, sid = struct.unpack('>BB', r[:2]) if result != 0x00: raise D4Error('result=0x%02X' % result) return sid def open_channel(self, cid): r = self._txn(struct.pack('>BBBHHH', 0x01, cid[0], cid[1], 0x0100, 0x0100, 0x0000), 0x81) if r[0] != 0x00: raise D4Error('OpenChannel%s result=0x%02X' % (cid, r[0])) if len(r) >= 11: self.credit[cid] = self.credit.get(cid, 0) + struct.unpack('>H', r[9:11])[0] return r[0] def credit_request(self, cid, want=1): r = self._txn(struct.pack('>BBBH', 0x04, cid[0], cid[1], want), 0x84) add = struct.unpack('>H', r[3:5])[0] if len(r) >= 5 else 0 self.credit[cid] = self.credit.get(cid, 0) + add return r[0], 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): t0 = time.time() 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 is None: break if rcid == cid and data: self.timings.append(time.time() - t0) return data self.timings.append(time.time() - t0) return b'' # ============================================================= EPSON-CTRL def encode_factory(op, payload, rkey): c = ord(op) body = struct.pack('> 1 & 0x7F) | (c << 7 & 0x80)) + payload return b'||' + struct.pack('HB', bytes.fromhex(m.group(1).decode())) return (val, resp) if got == addr else (None, resp) def info(self, name, payload): return self.d4.exchange(self.cid, encode_simple(name, payload)) # ============================================================= zber def run_survey(args, log, report): # --- 1. prostredie ------------------------------------------------- report['meta'] = { 'tool': 'epson_survey.py', 'version': VERSION, 'timestamp': datetime.datetime.now().isoformat(timespec='seconds'), 'host': socket.gethostname(), 'os': '%s %s %s' % (platform.system(), platform.release(), platform.machine()), 'python': platform.python_version(), } log('=' * 72) log(' epson_survey %s -- READ ONLY' % VERSION) for k, v in report['meta'].items(): log(' %-10s %s' % (k, v)) log('=' * 72) # --- 2. zoznam zariadeni ------------------------------------------- system = platform.system() if system == 'Windows': links = WinLink.enumerate() elif system == 'Linux': links = LinuxLink.enumerate() else: log('\nmacOS: nativny kanal neexistuje, pouzi WebUSB test alebo pyusb.') report['error'] = 'unsupported os' return report['devices'] = [l.describe() for l in links] log('\n[1] NAJDENE ZARIADENIA (%d)' % len(links)) for i, l in enumerate(links): log(' [%d] %s' % (i, json.dumps(l.describe(), ensure_ascii=False))) if not links: report['error'] = 'no devices' return idx = args.device if idx is None: idx = next((i for i, l in enumerate(links) if '130B' in (getattr(l, 'hwid', '') or '').upper()), 0) link = links[idx] report['selected_device'] = link.describe() report['selected_index'] = idx log('\n --> pouzivam [%d]' % idx) link.open() d4 = D4(link, log, verbose=args.verbose) cid = None try: # --- 3. IEEE-1284 ID ------------------------------------------- log('\n[2] IEEE-1284 DEVICE ID') try: raw = link.device_id() body = raw if len(raw) >= 2 and raw[0] < 8: n = (raw[0] << 8) | raw[1] body = raw[2:n] if 2 < n <= len(raw) else raw[2:] text = body.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() report['device_id'] = {'raw_hex': raw.hex(), 'text': text, 'fields': fields} log(' %s' % text) for k in sorted(fields): log(' %-4s = %s' % (k, fields[k])) except Exception as e: report['device_id'] = {'error': str(e)} log(' ZLYHALO: %s' % e) # --- 4. D4 init ------------------------------------------------- log('\n[3] D4 / IEEE-1284.4') t0 = time.time() ejl = d4.enter() report['d4'] = {'ejl_reply_hex': ejl.hex(), 'ejl_reply_expected': EXPECT_D4_REPLY in ejl} rev = d4.init() report['d4']['revision'] = '0x%02X' % rev report['d4']['init_seconds'] = round(time.time() - t0, 3) log(' EJL reply : %s (%s)' % ( ejl.hex(' ').upper(), 'zhoda' if EXPECT_D4_REPLY in ejl else 'INA NEZ OCAKAVANA')) log(' Init OK, revizia protokolu 0x%02X (%.2f s)' % (rev, report['d4']['init_seconds'])) # --- 5. sluzby / socket ID ------------------------------------- log('\n[4] EPSON SLUZBY (GetSocketID)') services = {} for name in EPSON_SERVICES: try: sid = d4.get_socket_id(name) services[name] = '0x%02X' % sid log(' %-18s -> socket 0x%02X' % (name, sid)) except D4Error as e: services[name] = None log(' %-18s -- %s' % (name, e)) report['services'] = services ctrl_sid = services.get('EPSON-CTRL') cid = (int(ctrl_sid, 16), int(ctrl_sid, 16)) if ctrl_sid else (0x02, 0x02) if not ctrl_sid: log(' (EPSON-CTRL sa nenasiel, skusam fixne (2,2))') d4.open_channel(cid) report['ctrl_channel'] = {'cid': list(cid), 'credit': d4.credit.get(cid, 0)} log(' kanal %s otvoreny, kredit=%d' % (cid, d4.credit.get(cid, 0))) ctrl = Ctrl(d4, cid) # --- 6. informacne prikazy ------------------------------------- log('\n[5] READ-ONLY PRIKAZY') cmds = {} for name, payload, desc in INFO_COMMANDS: try: r = ctrl.info(name, payload) except Exception as e: r = b'' log(' %-4s %-24s CHYBA: %s' % (name, desc, e)) cmds[name] = {'desc': desc, 'payload_hex': payload.hex(), 'response_hex': r.hex(), 'response_len': len(r)} log(' %-4s %-24s %d B' % (name, desc, len(r))) if r: log.quiet(hexdump(r)) printable = re.sub(rb'[^\x20-\x7e]', b'.', r).decode('ascii') log(' %s' % printable[:120]) report['commands'] = cmds # --- 7. overenie read_key -------------------------------------- log('\n[6] READ_KEY') probe_addr = 0x30 val, raw = ctrl.read_eeprom(probe_addr) report['read_key'] = {'known': '0x%04X' % KNOWN_READ_KEY, 'probe_addr': '0x%03X' % probe_addr, 'works': val is not None, 'probe_raw_hex': raw.hex()} if val is not None: log(' 0x%04X funguje (0x%03X = 0x%02X)' % (KNOWN_READ_KEY, probe_addr, val)) else: log(' 0x%04X NEFUNGUJE. Odpoved: %r' % (KNOWN_READ_KEY, raw[:80])) log(' Skusam %d znamych kandidatov...' % len(CANDIDATE_KEYS)) hits = [] for n, k in enumerate(CANDIDATE_KEYS): if n % 20 == 0: log(' ... %d/%d' % (n, len(CANDIDATE_KEYS))) v, _ = ctrl.read_eeprom(probe_addr, rkey=k) if v is not None: hits.append('0x%04X' % k) log(' TREFA: read_key 0x%04X -> 0x%02X' % (k, v)) report['read_key']['sweep_hits'] = hits if not hits: log(' Ziadny z 170 klucov nefunguje.') log(' -> firmvér L1210 EEPROM citanie blokuje.') ctrl = None # --- 8. dump EEPROM -------------------------------------------- if ctrl is not None: if report['read_key'].get('sweep_hits'): ctrl.rkey = int(report['read_key']['sweep_hits'][0], 16) hi = MEM_HIGH if not args.quick else 0xFF if args.dump_to: hi = int(args.dump_to, 16) hi = max(MEM_LOW, min(hi, MEM_HIGH)) log('\n[7] DUMP EEPROM 0x%03X-0x%03X (%d adries)' % (MEM_LOW, hi, hi - MEM_LOW + 1)) mem, fails, t0 = {}, 0, time.time() # zapisujeme priebezne, aby Ctrl-C nezahodil to, co uz mame report['eeprom'] = { 'range': ['0x%03X' % MEM_LOW, '0x%03X' % hi], 'read_key': '0x%04X' % ctrl.rkey, 'complete': False, 'failures': 0, 'seconds': 0.0, 'values': {}, } for a in range(MEM_LOW, hi + 1): v, raw = ctrl.read_eeprom(a) mem[a] = v report['eeprom']['values']['0x%03X' % a] = v if v is None: fails += 1 report['eeprom']['failures'] = fails report['eeprom']['seconds'] = round(time.time() - t0, 1) if a % 0x40 == 0x3F or a == hi: done = a - MEM_LOW + 1 rate = done / max(0.001, time.time() - t0) log(' 0x%03X %d/%d (%d chyb, %.1f s, %.0f adr/s)' % (a, done, hi - MEM_LOW + 1, fails, time.time() - t0, rate)) report['eeprom']['complete'] = True log(' hotovo za %.1f s, %d neuspesnych' % (time.time() - t0, fails)) # citatelny vypis + odvodene hodnoty log('\n[8] ZNAME ADRESY POCITADIEL') log(' %-8s %-6s %s' % ('ADRESA', 'HODN.', 'POPIS')) for a in sorted(WASTE_MAP): v = mem.get(a) log(' 0x%03X %s %s' % (a, ('0x%02X' % v) if v is not None else ' --', WASTE_MAP[a])) derived = {} for lo, hi2 in COUNTER_PAIRS: if mem.get(lo) is not None and mem.get(hi2) is not None: n = mem[lo] | (mem[hi2] << 8) derived['0x%03X/0x%03X' % (lo, hi2)] = n log(' 16-bit 0x%03X/0x%03X = %d (0x%04X)' % (lo, hi2, n, n)) lv = [mem.get(a) for a in LEVEL_ADDRS] if all(x is not None for x in lv): derived['maintenance_level'] = ['0x%02X' % x for x in lv] derived['at_threshold'] = all(x == LEVEL_FULL for x in lv) log(' maintenance level = %s -> %s' % (', '.join('0x%02X' % x for x in lv), 'NA PRAHU (0x5E)' if derived['at_threshold'] else 'pod prahom')) report['derived'] = derived # hexdump celej EEPROM do logu log.quiet('\n--- EEPROM hexdump ---') for base in range(MEM_LOW, hi + 1, 16): row = [mem.get(base + i) for i in range(16) if base + i <= hi] log.quiet(' %03X %s %s' % ( base, ' '.join('%02X' % v if v is not None else '--' for v in row), ''.join(chr(v) if v is not None and 32 <= v < 127 else '.' for v in row))) # --- 9. casovanie ------------------------------------------------ if d4.timings: ts = sorted(d4.timings) report['timing'] = { 'count': len(ts), 'min_ms': round(ts[0] * 1000, 1), 'median_ms': round(ts[len(ts) // 2] * 1000, 1), 'max_ms': round(ts[-1] * 1000, 1), } log('\n[9] ODOZVA n=%d min=%.1f ms median=%.1f ms max=%.1f ms' % (len(ts), ts[0] * 1000, ts[len(ts) // 2] * 1000, ts[-1] * 1000)) finally: if cid: d4.close_channel(cid) d4.exit() link.close() def main(): ap = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument('--device', type=int, default=None) ap.add_argument('--quick', action='store_true', help='dump len 0x000-0x0FF namiesto celej EEPROM') ap.add_argument('--dump-to', metavar='HEX', help='horna hranica dumpu, napr. 1FF') ap.add_argument('-v', '--verbose', action='store_true', help='hexdump paketov aj na obrazovku') args = ap.parse_args() stamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') base = 'epson_survey_%s_%s' % (socket.gethostname(), stamp) log = Tee(base + '.log') report = {} rc = 0 try: run_survey(args, log, report) except KeyboardInterrupt: report['interrupted'] = True log('\nPRERUSENE POUZIVATELOM -- ciastocne data sa aj tak ulozia.') rc = 130 except Exception as e: import traceback report['fatal_error'] = str(e) report['traceback'] = traceback.format_exc() log('\nFATALNA CHYBA: %s' % e) log(traceback.format_exc()) rc = 1 finally: with open(base + '.json', 'w', encoding='utf-8') as f: json.dump(report, f, indent=2, ensure_ascii=False) log('\n' + '=' * 72) log(' Ulozene:') log(' %s.json' % base) log(' %s.log' % base) log(' Oba subory zober so sebou -- obsahuju vsetko potrebne.') log('=' * 72) log.close() return rc if __name__ == '__main__': sys.exit(main())