firmware/shared/selftest.py
2026-03-16 13:48:28 -04:00

418 lines
11 KiB
Python

# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# selftest.py - Interactive Selftest code
#
import ckcc
from uasyncio import sleep_ms
from glob import dis, settings
from ux import ux_wait_keydown, ux_clear_keys, ux_poll_key
from ux import ux_show_story
from callgate import get_is_bricked, get_genuine, clear_genuine
from utils import problem_file_line
import version
from charcodes import KEY_ENTER, KEY_CANCEL, KEY_QR
try:
from display import FontLarge
except ImportError:
FontLarge = None
async def wait_ok():
k = await ux_wait_keydown('xy' + KEY_ENTER + KEY_CANCEL)
if k not in 'y' + KEY_ENTER:
raise RuntimeError('Canceled')
def label_test(line1, line2=''):
dis.clear()
if version.has_qwerty:
dis.text(None, 1, line1)
dis.text(None, 3, line2)
else:
dis.text(None, 10, line1)
dis.text(None, 34, line2, font=FontLarge)
dis.show()
async def test_numpad():
# do an interactive self test
keys = list('123456789x0y')
for ch in keys:
dis.clear()
dis.text(0,0, "Numpad Test. Press:")
dis.text(None,24, ch if ch != 'y' else 'OK', FontLarge)
dis.show()
k = await ux_wait_keydown(ch + 'x')
if k == 'x' and ch != 'x':
raise RuntimeError("numpad test aborted")
assert k == ch
async def test_gpu():
import gpu_binary
try:
dis.gpu.take_spi()
dis.clear()
dis.text(0,-1, "GPU Test okay?")
dis.show()
assert dis.gpu.get_version() == gpu_binary.VERSION
dis.bootrom_takeover()
dis.gpu.show_test_pattern()
await wait_ok()
finally:
# not required?
dis.real_clear()
async def test_keyboard():
# for Q1 - just some of them: one in each row and column
keys = list('1wdvgy7k.p '+KEY_QR)
for ch in keys:
dis.clear()
dis.text(None,2, "Keyboard Test. Press:")
dis.text(None,4, 'SPACE' if ch == ' ' else ch.upper())
dis.show()
k = await ux_wait_keydown(ch + KEY_CANCEL)
if k == KEY_CANCEL:
raise RuntimeError("kbd test aborted")
assert k == ch
async def test_qr_scanner():
# QR Scanner module: assume pretested, just testing connection really
from glob import SCAN
# Can take 2+ seconds to setup the QR scanner (from powerup), and
# if batteries are right, we can hit that timeframe in factory test case.
for i in range(25):
if SCAN.setup_done:
break
await sleep_ms(100)
assert SCAN and SCAN.version, 'missing'
assert SCAN.version.startswith('V2.'), 'QR: ' + SCAN.version
async def test_battery():
from battery import get_batt_level
try:
from machine import ADC
# expect 11500 or so, and certainly not 65535
assert max(ADC(ADC.CORE_VREF).read_u16() for i in range(10)) < 60000, 'VREF fix not done'
except ImportError:
# simulator
pass
dis.clear()
dis.text(None, 1, "Battery Test")
dis.text(None, 3, "Connect 3.3v reference cells.")
while 1:
lvl = get_batt_level()
if lvl and 3.2 <= lvl <= 3.4:
# pass
return
if lvl:
dis.text(None, -1, "VIN Sense reads: %.1f volts" % lvl, dark=True)
else:
dis.text(None, -1, "Remove USB plug.", dark=True)
dis.show()
if settings.get('tested', False):
# auto-pass this test if we've been thru here before; customer may
# not have magic batteries and so on.
break
# show for 100ms or until they press key
ch = await ux_wait_keydown(KEY_CANCEL+'s', 100)
if not ch: continue
if ch == 's':
# undocumented: 'skip'
return
elif ch == KEY_CANCEL:
raise RuntimeError("Battery test aborted")
else:
raise ValueError(ch)
def set_genuine():
# PIN must be blank for this to work
# - or logged in already as main
from pincodes import pa
if not pa.is_successful():
# assume blank pin during factory selftest
pa.setup(b'')
if not pa.is_successful():
pa.login()
assert pa.is_successful() # "PIN not blank?"
# do verify step
dis.bootrom_takeover()
pa.greenlight_firmware()
dis.show()
async def test_secure_element():
assert not get_is_bricked() # bricked already
if ckcc.is_simulator(): return
for ph in range(5):
gg = get_genuine()
dis.clear()
if version.has_qwerty or version.mk_num == 5:
dis.text(0, 0, "^^-- Green? " if gg else " ^^-- Red?")
else:
if gg:
dis.text(-1, 8, "Green ON? -->")
else:
dis.text(-1,50, "Red ON? -->")
dis.show()
await wait_ok()
if ph and gg:
# stop once it's on and we've tested both states
return
# attempt to switch to other state
if gg:
clear_genuine()
else:
# very slow!
dis.fullscreen("Wait...")
set_genuine()
ux_clear_keys()
dis.busy_bar(False)
ng = get_genuine()
assert ng != gg # "Could not invert LED"
async def test_sd_active():
# Mark 2+: SD Card active light.
# Q1: dual slots
from machine import Pin
for num in range(version.num_sd_slots):
led = Pin('SD_ACTIVE' if not num else 'SD_ACTIVE2', Pin.OUT)
for ph in range(2):
gg = not ph
led.value(gg)
dis.clear()
if version.has_qwerty:
if num == 0:
dis.text(0, 2, "<-- SD A is %s? " % ('ON' if gg else 'off'))
else:
dis.text(0, 7, "<-- SD B is %s? " % ('ON' if gg else 'off'))
else:
if gg:
dis.text(0,16, "<-- Green ON?")
else:
dis.text(0,16, "<-- Green off?")
dis.show()
await wait_ok()
async def test_usb_light():
# Mk4's new USB activity light (right by connector)
from machine import Pin
p = Pin('USB_ACTIVE', Pin.OUT)
try:
p.value(1)
label_test("USB light is on?")
await wait_ok()
finally:
p.value(0)
async def test_nfc_light():
if not version.has_qwerty:
return
from machine import Pin
p = Pin('NFC_ACTIVE', Pin.OUT)
try:
p.value(1)
dis.clear()
dis.text(-1, -1, "NFC light green? --->")
dis.show()
await wait_ok()
finally:
p.value(0)
async def test_nfc():
# Mk4: NFC chip and field
if not version.has_nfc: return
from nfc import NFCHandler
await NFCHandler.selftest()
async def test_psram():
from glob import PSRAM
from ustruct import pack
import ngu
label_test('PSRAM Test')
test_len = PSRAM.length * 2
chk = bytearray(32)
spots = []
for pos in range(0, PSRAM.length, 4000 * 17):
if pos >= PSRAM.length: break
rnd = ngu.hash.sha256s(pack('I', pos))
PSRAM.write(pos, rnd)
PSRAM.read(pos, chk)
assert chk == rnd, "bad @ 0x%x" % pos
dis.progress_bar_show(pos / test_len)
spots.append(pos)
for pos in spots:
rnd = ngu.hash.sha256s(pack('I', pos))
PSRAM.read(pos, chk)
assert chk == rnd, "RB bad @ 0x%x" % pos
dis.progress_bar_show((PSRAM.length + pos) / test_len)
async def test_oled():
# all on/off tests
for ph in (1, 0):
dis.clear()
dis.dis.fill(ph)
dis.text(None,2, "Selftest", invert=ph)
dis.text(None,30, "All on?" if ph else 'All off?', invert=ph, font=FontLarge)
dis.show()
await wait_ok()
async def test_lcd():
# Very basic
try:
for nm, col in [('RED', 0xf800), ('GREEN', 0x07e0), ('BLUE', 0x001f)]:
dis.real_clear()
dis.dis.fill_screen(col)
dis.text(1,1, "Selftest")
dis.text(None,3, "All pixels are %s?" % nm)
dis.show()
await wait_ok()
finally:
dis.real_clear()
dis.draw_status(full=1)
dis.clear()
async def test_microsd():
if ckcc.is_simulator(): return
from version import num_sd_slots
from files import CardSlot
import os
def _is_inserted(slot_num):
if num_sd_slots > 1:
if slot_num == 0:
return CardSlot.sd_detect() == 0
elif slot_num == 1:
return CardSlot.sd_detect2() == 0
else:
assert False
else:
return CardSlot.is_inserted()
async def wait_til_state(num, want):
title = 'MicroSD Card'
if num_sd_slots > 1:
title += ' ' + chr(65+num)
label_test(title +':', 'Remove' if _is_inserted(num) else 'Insert')
while 1:
if want == _is_inserted(num): return
await sleep_ms(100)
if ux_poll_key():
raise RuntimeError("MicroSD test aborted")
for slot_num in range(num_sd_slots):
# test presence switch
for ph in range(7):
await wait_til_state(slot_num, not _is_inserted(slot_num))
if ph >= 2 and _is_inserted(slot_num):
# debounce
await sleep_ms(100)
if _is_inserted(slot_num): break
if ux_poll_key():
raise RuntimeError("MicroSD test aborted")
label_test('MicroSD Card:', 'Testing')
# card inserted
assert _is_inserted(slot_num) #, "SD not present?"
with CardSlot(slot_b=slot_num) as card:
fn, _ = card.pick_filename('test-delme.txt')
with open(fn, 'wt') as fd:
fd.write("Hello")
with open(fn, 'rt') as fd:
assert fd.read() == "Hello"
os.unlink(fn)
# force removal, so cards don't get stuck in finished units
await wait_til_state(slot_num, False)
async def start_selftest():
try:
if version.has_battery:
await test_battery()
if version.has_qr:
await test_qr_scanner()
if not version.has_qwerty:
await test_oled()
else:
await test_lcd()
await test_gpu()
await test_psram()
await test_nfc_light()
await test_nfc()
if version.has_qwerty:
await test_keyboard()
else:
await test_numpad()
await test_secure_element()
await test_sd_active()
await test_usb_light()
await test_microsd()
# add more tests here
settings.set('tested', True)
await ux_show_story("Selftest complete", 'PASS')
except (RuntimeError, AssertionError) as e:
e = str(e) or problem_file_line(e)
await ux_show_story("Test failed:\n" + str(e), 'FAIL')
# EOF