|
55 | 55 | from .debuglink import DebugLink |
56 | 56 |
|
57 | 57 |
|
58 | | -try: |
59 | | - from PIL import Image |
60 | | - SCREENSHOT = os.environ.get('KEEPKEY_SCREENSHOT', '') == '1' |
61 | | -except ImportError: |
62 | | - SCREENSHOT = False |
| 58 | +import struct as _struct |
| 59 | +import zlib as _zlib |
| 60 | + |
| 61 | +SCREENSHOT = os.environ.get('KEEPKEY_SCREENSHOT', '') == '1' |
| 62 | + |
| 63 | + |
| 64 | +def _write_png(path, width, height, pixels): |
| 65 | + """Write a minimal grayscale PNG. No Pillow needed.""" |
| 66 | + def _chunk(tag, data): |
| 67 | + raw = tag + data |
| 68 | + return _struct.pack('>I', len(data)) + raw + _struct.pack('>I', _zlib.crc32(raw) & 0xffffffff) |
| 69 | + ihdr = _struct.pack('>IIBBBBB', width, height, 8, 0, 0, 0, 0) |
| 70 | + raw_data = b''.join(b'\x00' + row for row in pixels) |
| 71 | + return b'\x89PNG\r\n\x1a\n' + _chunk(b'IHDR', ihdr) + _chunk(b'IDAT', _zlib.compress(raw_data)) + _chunk(b'IEND', b'') |
63 | 72 |
|
64 | 73 | DEFAULT_CURVE = 'secp256k1' |
65 | 74 |
|
@@ -423,25 +432,8 @@ def set_mnemonic(self, mnemonic): |
423 | 432 |
|
424 | 433 | def call_raw(self, msg): |
425 | 434 |
|
426 | | - if SCREENSHOT and self.debug: |
427 | | - try: |
428 | | - layout = self.debug.read_layout() |
429 | | - if layout and len(layout) >= 2048: |
430 | | - # KeepKey OLED: 256x64, packed as 1bpp (2048 bytes) |
431 | | - im = Image.new("RGB", (256, 64)) |
432 | | - pix = im.load() |
433 | | - for x in range(256): |
434 | | - for y in range(64): |
435 | | - byte_idx = x + (y // 8) * 256 |
436 | | - b = layout[byte_idx] if isinstance(layout[byte_idx], int) else ord(layout[byte_idx]) |
437 | | - if (b >> (y % 8)) & 1: |
438 | | - pix[x, y] = (255, 255, 255) |
439 | | - screenshot_dir = getattr(self, 'screenshot_dir', os.environ.get('SCREENSHOT_DIR', '.')) |
440 | | - os.makedirs(screenshot_dir, exist_ok=True) |
441 | | - im.save(os.path.join(screenshot_dir, 'scr%05d.png' % self.screenshot_id)) |
442 | | - self.screenshot_id += 1 |
443 | | - except Exception: |
444 | | - pass # Don't let screenshot failures break tests |
| 435 | + # Screenshot capture disabled in call_raw (captures idle screens, adds latency). |
| 436 | + # Real confirmation screenshots are captured in callback_ButtonRequest instead. |
445 | 437 |
|
446 | 438 | resp = super(DebugLinkMixin, self).call_raw(msg) |
447 | 439 | self._check_request(resp) |
@@ -469,6 +461,29 @@ def callback_ButtonRequest(self, msg): |
469 | 461 | if self.verbose: |
470 | 462 | log("ButtonRequest code: " + get_buttonrequest_value(msg.code)) |
471 | 463 |
|
| 464 | + # Capture OLED screenshot BEFORE pressing button (confirmation screen) |
| 465 | + if SCREENSHOT and self.debug: |
| 466 | + try: |
| 467 | + layout = self.debug.read_layout() |
| 468 | + if layout and len(layout) >= 2048: |
| 469 | + rows = [] |
| 470 | + for y in range(64): |
| 471 | + row = bytearray(256) |
| 472 | + for x in range(256): |
| 473 | + byte_idx = x + (y // 8) * 256 |
| 474 | + b = layout[byte_idx] if isinstance(layout[byte_idx], int) else ord(layout[byte_idx]) |
| 475 | + if (b >> (y % 8)) & 1: |
| 476 | + row[x] = 255 |
| 477 | + rows.append(bytes(row)) |
| 478 | + screenshot_dir = getattr(self, 'screenshot_dir', os.environ.get('SCREENSHOT_DIR', '.')) |
| 479 | + os.makedirs(screenshot_dir, exist_ok=True) |
| 480 | + png_path = os.path.join(screenshot_dir, 'btn%05d.png' % self.screenshot_id) |
| 481 | + with open(png_path, 'wb') as f: |
| 482 | + f.write(_write_png(png_path, 256, 64, rows)) |
| 483 | + self.screenshot_id += 1 |
| 484 | + except Exception: |
| 485 | + pass |
| 486 | + |
472 | 487 | if self.auto_button: |
473 | 488 | if self.verbose: |
474 | 489 | log("Pressing button " + str(self.button)) |
|
0 commit comments