Skip to content

Commit 127f2c7

Browse files
BitHighlanderclaude
andcommitted
feat: OLED screenshot capture + test report generator
- Remove Pillow dependency from screenshot capture (client.py) Pure Python PNG writer using stdlib struct+zlib. Zero build time. - Move screenshot capture from call_raw to callback_ButtonRequest Captures the actual confirmation screen, not idle state. - Per-test screenshot directories in common.py setUp KEEPKEY_SCREENSHOT=1 SCREENSHOT_DIR=path enables capture. - Add scripts/generate-test-report.py (stdlib only, no deps) Auto-detects firmware version, reads JUnit XML for pass/fail, embeds real OLED PNGs in PDF, version-gated sections. - Remove old generate-zoo-report.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0fe662d commit 127f2c7

4 files changed

Lines changed: 1093 additions & 371 deletions

File tree

keepkeylib/client.py

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,21 @@
5555
from .debuglink import DebugLink
5656

5757

58-
try:
59-
from PIL import Image
60-
SCREENSHOT = os.environ.get('KEEPKEY_SCREENSHOT', '') == '1'
61-
except ImportError:
62-
SCREENSHOT = False
58+
import struct
59+
import 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. pixels = list of rows, each row = bytes."""
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+
70+
ihdr = struct.pack('>IIBBBBB', width, height, 8, 0, 0, 0, 0)
71+
raw_data = b''.join(b'\x00' + row for row in pixels)
72+
return b'\x89PNG\r\n\x1a\n' + _chunk(b'IHDR', ihdr) + _chunk(b'IDAT', zlib.compress(raw_data)) + _chunk(b'IEND', b'')
6373

6474
DEFAULT_CURVE = 'secp256k1'
6575

@@ -423,25 +433,8 @@ def set_mnemonic(self, mnemonic):
423433

424434
def call_raw(self, msg):
425435

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
436+
# Screenshot capture disabled in call_raw (too slow, captures idle screens).
437+
# Real confirmation screenshots are captured in callback_ButtonRequest instead.
445438

446439
resp = super(DebugLinkMixin, self).call_raw(msg)
447440
self._check_request(resp)
@@ -469,6 +462,29 @@ def callback_ButtonRequest(self, msg):
469462
if self.verbose:
470463
log("ButtonRequest code: " + get_buttonrequest_value(msg.code))
471464

465+
# Capture OLED screenshot BEFORE pressing button (this is the confirmation screen)
466+
if SCREENSHOT and self.debug:
467+
try:
468+
layout = self.debug.read_layout()
469+
if layout and len(layout) >= 2048:
470+
rows = []
471+
for y in range(64):
472+
row = bytearray(256)
473+
for x in range(256):
474+
byte_idx = x + (y // 8) * 256
475+
b = layout[byte_idx] if isinstance(layout[byte_idx], int) else ord(layout[byte_idx])
476+
if (b >> (y % 8)) & 1:
477+
row[x] = 255
478+
rows.append(bytes(row))
479+
screenshot_dir = getattr(self, 'screenshot_dir', os.environ.get('SCREENSHOT_DIR', '.'))
480+
os.makedirs(screenshot_dir, exist_ok=True)
481+
png_data = _write_png(os.path.join(screenshot_dir, 'btn%05d.png' % self.screenshot_id), 256, 64, rows)
482+
with open(os.path.join(screenshot_dir, 'btn%05d.png' % self.screenshot_id), 'wb') as f:
483+
f.write(png_data)
484+
self.screenshot_id += 1
485+
except Exception:
486+
pass
487+
472488
if self.auto_button:
473489
if self.verbose:
474490
log("Pressing button " + str(self.button))

0 commit comments

Comments
 (0)