From 5ed3ca15e125a05980ab0bb1ac3b4377424115c4 Mon Sep 17 00:00:00 2001 From: Ken Carpenter Date: Thu, 20 May 2021 10:49:46 -0700 Subject: [PATCH 1/6] Add microSD test to "self-test" flow --- .../stm32/boards/Passport/modules/actions.py | 6 +- .../boards/Passport/modules/self_test_ux.py | 62 +++++++++++++++++-- ports/stm32/boards/Passport/modules/utils.py | 5 ++ 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/ports/stm32/boards/Passport/modules/actions.py b/ports/stm32/boards/Passport/modules/actions.py index f6a43dee..22a16365 100644 --- a/ports/stm32/boards/Passport/modules/actions.py +++ b/ports/stm32/boards/Passport/modules/actions.py @@ -21,7 +21,7 @@ from common import settings, system, noise, dis from utils import (UXStateMachine, imported, pretty_short_delay, xfp2str, to_str, truncate_string_to_width, set_next_addr, scan_for_address, get_accounts, run_chooser, - make_account_name_num, is_valid_address, save_next_addr) + make_account_name_num, is_valid_address, save_next_addr, needs_microsd) from wallets.utils import get_export_mode, get_addr_type_from_address, get_deriv_path_from_addr_type_and_acct from ux import (the_ux, ux_confirm, ux_enter_pin, ux_enter_text, ux_scan_qr_code, ux_shutdown, @@ -31,10 +31,6 @@ import trezorcrypto from seed_check_ux import SeedCheckUX -async def needs_microsd(): - # Standard msg shown if no SD card detected when we need one. - return await ux_show_story("Please insert a microSD card.", title='MicroSD', center=True, center_vertically=True) - async def about_info(*a): from common import system from display import FontTiny diff --git a/ports/stm32/boards/Passport/modules/self_test_ux.py b/ports/stm32/boards/Passport/modules/self_test_ux.py index 50fa7fb9..93009f73 100644 --- a/ports/stm32/boards/Passport/modules/self_test_ux.py +++ b/ports/stm32/boards/Passport/modules/self_test_ux.py @@ -5,10 +5,50 @@ # from common import system -from utils import UXStateMachine +from utils import UXStateMachine, needs_microsd from ux import ux_show_text_as_ur, ux_keypad_test, ux_scan_qr_code, ux_show_story, ux_draw_alignment_grid from data_codecs.qr_type import QRType +async def microsd_test(): + import uos + import os + from files import CardSlot, CardMissingError + from utils import file_exists + + msg = 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks' + + while True: + try: + with CardSlot() as card: + filename = '{}/microsd-test.txt'.format(card.get_sd_root()) + + if file_exists(filename): + os.remove(filename) + + with open(filename, 'wt') as fd: + fd.write(msg) + + with open(filename, 'rt') as fd: + read_msg = fd.read() + + if read_msg != msg: + await ux_show_story('The text read back from the microSD card did not match that written. Read:\n\n {}'.format(read_msg), title='Error') + return False + + os.remove(filename) + await ux_show_story('microSD card is working properly!', title='microSD Test', center=True, center_vertically=True) + return True + + except CardMissingError: + result = await needs_microsd() + if result == 'x': + return False + + except Exception as e: + await ux_show_story('{}'.format(e), title='Exception') + return False + + class SelfTestUX(UXStateMachine): def __init__(self): @@ -18,6 +58,7 @@ def __init__(self): self.CAMERA_TEST = 3 self.CAMERA_TEST_RESULT = 4 self.SCREEN_ALIGNMENT = 5 + self.MICROSD_TEST = 6 self.qr_data = None # print('SelfTestUX init') @@ -39,14 +80,14 @@ async def show(self): # print('Keypad Test!') result = await ux_keypad_test() if result == 'x': - self.goto(self.SHOW_SERIAL_NUMBER) + self.goto_prev() else: self.goto(self.SCREEN_ALIGNMENT) elif self.state == self.SCREEN_ALIGNMENT: result = await ux_draw_alignment_grid(title='Align Screen') if result == 'x': - self.goto(self.KEYPAD_TEST) + self.goto_prev() else: self.goto(self.CAMERA_TEST) @@ -56,19 +97,28 @@ async def show(self): self.qr_data = await ux_scan_qr_code('Camera Test') # print('qr_data=|{}|'.format(self.qr_data)) system.turbo(False) - self.goto(self.CAMERA_TEST_RESULT) + self.goto(self.CAMERA_TEST_RESULT, save_curr=False) elif self.state == self.CAMERA_TEST_RESULT: if self.qr_data == None: result = await ux_show_story('No QR code scanned.', right_btn='RETRY') if result == 'x': - self.goto(self.SCREEN_ALIGNMENT) + self.goto_prev() else: self.goto(self.CAMERA_TEST) else: # Show the data - The QR code used in the factory starts with "Camera Test Passed!" result = await ux_show_story(self.qr_data, right_btn='DONE') if result == 'x': - self.goto(self.SCREEN_ALIGNMENT) + self.goto_prev() + else: + self.goto(self.MICROSD_TEST) + + elif self.state == self.MICROSD_TEST: + # Describe the microSD test + result = await ux_show_story('This test will exercise the read/write features of the microSD card.', title='microSD Test', right_btn='START', center=True, center_vertically=True) + if result == 'x': + self.goto_prev() else: + await microsd_test() return diff --git a/ports/stm32/boards/Passport/modules/utils.py b/ports/stm32/boards/Passport/modules/utils.py index 0126737c..add205e8 100644 --- a/ports/stm32/boards/Passport/modules/utils.py +++ b/ports/stm32/boards/Passport/modules/utils.py @@ -758,4 +758,9 @@ def is_alphanumeric_qr(buf): return True +async def needs_microsd(): + from ux import ux_show_story + # Standard msg shown if no SD card detected when we need one. + return await ux_show_story("Please insert a microSD card.", title='MicroSD', center=True, center_vertically=True) + # EOF From 83cbe174d56e4dde2b9c8cc8d30dda972ec8bab8 Mon Sep 17 00:00:00 2001 From: Ken Carpenter Date: Thu, 20 May 2021 21:46:04 -0700 Subject: [PATCH 2/6] Update provisioning script to better manage OCD connection --- .../Passport/tools/provisioning/provision.py | 98 +++++++++++++------ 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/ports/stm32/boards/Passport/tools/provisioning/provision.py b/ports/stm32/boards/Passport/tools/provisioning/provision.py index fd58703f..9044e66c 100644 --- a/ports/stm32/boards/Passport/tools/provisioning/provision.py +++ b/ports/stm32/boards/Passport/tools/provisioning/provision.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # @@ -33,10 +34,6 @@ telnet_proc = None -# FACTORY SETTINGS -DIAGNOSTIC_MODE = False # Set to True to get more menu options - - def connect_to_telnet(): # Connect global tn @@ -45,6 +42,27 @@ def connect_to_telnet(): # We still see the commands we send echoed from the remote side, but they are not also echoed locally now. tn.write(b'' + telnetlib.IAC + telnetlib.DONT + telnetlib.ECHO) +def banner(s): + divider = '=' * len(s) + print(divider) + print(s) + print(divider) + +# def poll_for_device(): +# attempts_remaining = 10 +# print('Waiting for device to be ready...') +# while attempts_remaining > 0: +# print(' {}...'.format(attempts_remaining)) +# result = init_device() +# if result: +# print('Device Ready!') +# return True +# +# time.sleep(1) +# attempts_remaining -= 1 +# +# print('Timeout! Device did not respond.') +# return False # Numato 32 channel GPIO board over USB serial: # @@ -103,19 +121,42 @@ def wait_for_prompt(timeout=None): else: return True + +MAX_ATTEMPTS = 10 + # Put device into halt state, and discard all unread data to get ready for a new command def init_device(timeout=None): - r = tn.read_very_eager() - # print('Halting device...') - tn.write(b'reset halt\r') - return wait_for_prompt(timeout) + attempts_remaining = MAX_ATTEMPTS + if not tn: + print('Connecting to OpenOCD...') + + while attempts_remaining > 0: + if not tn: + connect_to_telnet() + + if tn: + r = tn.read_very_eager() + # print('Halting device...') + tn.write(b'reset halt\r') + result = wait_for_prompt(timeout) + if result: + if attempts_remaining < MAX_ATTEMPTS: + banner('Connected to device!') + return True + + print(' {}...'.format(attempts_remaining)) + time.sleep(1) + attempts_remaining -= 1 + + banner('Timeout! Device did not respond.') + return False def random_fn_ext(l): import os, binascii return binascii.b2a_hex(os.urandom(l)).decode('utf-8') def provision_device(flash_bootloader=False, flash_firmware=False, with_secrets=False): - init_device() + if not init_device(): return # Check to see if the device was already provisioned - it will have data in its secrets if flash_bootloader and not with_secrets: @@ -183,7 +224,7 @@ def provision_device(flash_bootloader=False, flash_firmware=False, with_secrets= print('Complete!') def write_supply_chain_secret(): - init_device() + if not init_device(): return # Write the supply chain secret print('Setting Supply Chain Validation Secret...') @@ -196,19 +237,15 @@ def write_supply_chain_secret(): tn.write(bytes(cmd, 'utf-8')) wait_for_prompt() -def test_device_connection(): - tn.read_very_eager() - device_found = init_device(timeout=5) - if device_found: - print('Passport is connected and responding to commands.') - else: - print('===================================================================') - print('Unable to connect to device (Error or timeout connecting to device)') - print('===================================================================') +# def test_device_connection(): +# # tn.read_very_eager() +# device_found = init_device(timeout=5) +# if device_found: +# print('Passport is connected and responding to commands.') def read_supply_chain_secret(do_init=True): if do_init: - init_device() + if not init_device(): return # Read the supply chain secret to make sure the device is ready for provisioning tn.write(bytes('mdb {} 32\r'.format(hex(SUPPLY_CHAIN_SECRET_ADDRESS)), 'utf-8')) @@ -235,7 +272,7 @@ def is_already_provisioned(secrets): return any(map(lambda b: b != 0xFF, secrets)) def get_secrets(): - init_device() + if not init_device(): return [] cmd = bytes('mdb {} 256\r'.format(hex(BL_NVROM_BASE)), 'utf-8') tn.write(cmd) @@ -266,7 +303,7 @@ def save_secrets(): print('\nUnable to read secrets from device!') def reset_device(): - init_device() + if not init_device(): return print('Resetting Device...') tn.write(b'reset\r') @@ -274,7 +311,7 @@ def reset_device(): print('Done.') def erase_all_flash(): - init_device() + if not init_device(): return print('Erasing all internal flash (bootloader, secrets, firmware, user settings)...') tn.write(b'flash erase_address 0x8000000 0x200000\r') @@ -298,9 +335,14 @@ def erase_all_flash(): # def main(): + DIAGNOSTIC_MODE = False + if len(sys.argv) > 1: + if sys.argv[1] == '--diag': + DIAGNOSTIC_MODE = True + if DIAGNOSTIC_MODE: options = [ - '[1] Test Device Connection', + '[1] Connect to Test Device', '[2] Provision Device', '[3] Update Bootloader Only (with secrets.bin)', '[4] Update Firmware Only', @@ -314,7 +356,7 @@ def main(): ] else: options = [ - '[1] Test Device Connection', + '[1] Connect to Test Device', '[2] Provision Device', '[3] Print Secrets', '[4] Reset Device', @@ -331,8 +373,7 @@ def main(): if DIAGNOSTIC_MODE: if selection == 0: - connect_to_telnet() - test_device_connection() + init_device() elif selection == 1: provision_device(flash_bootloader=True, flash_firmware=True) elif selection == 2: @@ -355,8 +396,7 @@ def main(): exit = True else: if selection == 0: - connect_to_telnet() - test_device_connection() + init_device() elif selection == 1: provision_device(flash_bootloader=True, flash_firmware=True) elif selection == 2: From dab423a45680cd36ff5ade40d02c557d77b8b1fd Mon Sep 17 00:00:00 2001 From: Ken Carpenter Date: Mon, 24 May 2021 12:37:50 -0700 Subject: [PATCH 3/6] Fix crash when picking a microSD file from a folder that doesn't exist Was found when trying to pick a backup to verify from /sd/backups/ --- ports/stm32/boards/Passport/modules/actions.py | 8 ++++++-- ports/stm32/boards/Passport/modules/utils.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/ports/stm32/boards/Passport/modules/actions.py b/ports/stm32/boards/Passport/modules/actions.py index 22a16365..01f15731 100644 --- a/ports/stm32/boards/Passport/modules/actions.py +++ b/ports/stm32/boards/Passport/modules/actions.py @@ -618,7 +618,7 @@ async def create_new_seed(*a): Experienced users can always view and record the 24-word seed in the Advanced settings menu.''', title='Backup') if ch == 'x': - if await ux_confirm("Are you sure you want to cancel the backup?\n\nWithout a microSD backup or the seed phrase, you won't be able to recover your funds"): + if await ux_confirm("Are you sure you want to cancel the backup?\n\nWithout a microSD backup or the seed phrase, you won't be able to recover your funds."): # Go back to the outer loop and show the selection again break @@ -1015,7 +1015,7 @@ async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=Non # - escape: allow these chars to skip picking process from menu import MenuSystem, MenuItem import uos - from utils import get_filesize + from utils import get_filesize, folder_exists system.turbo(True) @@ -1031,6 +1031,10 @@ async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=Non folder_path = [folder_path] for path in folder_path: + # If the folder doesn't exist, skip it (e.g., if /sd/backups/ doesn't exist) + if not folder_exists(path): + continue + files = uos.ilistdir(path) for fn, ftype, *var in files: # print("fn={} ftype={} var={} suffix={}".format(fn, ftype, var, suffix)) diff --git a/ports/stm32/boards/Passport/modules/utils.py b/ports/stm32/boards/Passport/modules/utils.py index add205e8..27a89cf5 100644 --- a/ports/stm32/boards/Passport/modules/utils.py +++ b/ports/stm32/boards/Passport/modules/utils.py @@ -494,6 +494,17 @@ def file_exists(path): except: return False +def folder_exists(path): + import os + from stat import S_ISDIR + + try: + s = os.stat(path) + mode = s[0] + return S_ISDIR(mode) + except OSError as e: + return False + # Derive addresses from the specified path until we find the address or have tried max_to_check addresses # If single sig, we need `path`. # If multisig, we need `ms_wallet`, but not `path` From e685de5899176a78546a5b77cbe7bb274ee068c6 Mon Sep 17 00:00:00 2001 From: Ken Carpenter Date: Mon, 24 May 2021 21:43:27 -0700 Subject: [PATCH 4/6] Add screenshot control menu items to Extras menu --- ports/stm32/boards/Passport/modules/actions.py | 4 ++++ ports/stm32/boards/Passport/modules/flow.py | 6 ++++-- ports/stm32/boards/Passport/modules/utils.py | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ports/stm32/boards/Passport/modules/actions.py b/ports/stm32/boards/Passport/modules/actions.py index 01f15731..2f1056fe 100644 --- a/ports/stm32/boards/Passport/modules/actions.py +++ b/ports/stm32/boards/Passport/modules/actions.py @@ -1943,6 +1943,10 @@ async def test_read_flash_cache(*a): async def toggle_screenshot_mode(*a): import common common.screenshot_mode_enabled = not common.screenshot_mode_enabled + + if common.screenshot_mode_enabled: + await ux_show_story('Press and release the aA1 key in the lower right corner of the keypad to save a screenshot to the microSD card.\n\nIf no microSD is inserted, nothing will happen.', + title='Screenshots', center=True, center_vertically=True) # print('common.screenshot_mode_enabled={}'.format(common.screenshot_mode_enabled)) async def toggle_snapshot_mode(*a): diff --git a/ports/stm32/boards/Passport/modules/flow.py b/ports/stm32/boards/Passport/modules/flow.py index c2f97379..c848e486 100644 --- a/ports/stm32/boards/Passport/modules/flow.py +++ b/ports/stm32/boards/Passport/modules/flow.py @@ -19,7 +19,7 @@ from multisig import make_multisig_menu from wallets.utils import has_export_mode from export import view_backup_password -from utils import is_new_wallet_in_progress, get_accounts +from utils import is_new_wallet_in_progress, get_accounts, is_screenshot_mode_enabled from new_wallet import pair_new_wallet from ie import show_browser @@ -184,5 +184,7 @@ def not_account_zero(): # MenuItem('Developer Menu', menu=DeveloperMenu), MenuItem('Snakamoto', f=play_snake), MenuItem('Stacking Sats', f=play_stacking_sats), - MenuItem('Internet Browser', f=show_browser) + MenuItem('Internet Browser', f=show_browser), + MenuItem('Enable Screenshots', f=toggle_screenshot_mode, predicate=lambda: not is_screenshot_mode_enabled()), + MenuItem('Disable Screenshots', f=toggle_screenshot_mode, predicate=is_screenshot_mode_enabled) ] diff --git a/ports/stm32/boards/Passport/modules/utils.py b/ports/stm32/boards/Passport/modules/utils.py index 27a89cf5..35454b54 100644 --- a/ports/stm32/boards/Passport/modules/utils.py +++ b/ports/stm32/boards/Passport/modules/utils.py @@ -661,6 +661,10 @@ def is_new_wallet_in_progress(): ap = settings.get('wallet_prog', None) return ap != None +def is_screenshot_mode_enabled(): + from common import screenshot_mode_enabled + return screenshot_mode_enabled + async def do_rename_account(acct_num, new_name): from common import settings from export import auto_backup From 287ff555d425beb02a814ffe8717596a215fae3b Mon Sep 17 00:00:00 2001 From: Ken Carpenter Date: Mon, 24 May 2021 21:45:06 -0700 Subject: [PATCH 5/6] Remove serial # display & don't boot to firmware after test is complete --- .../stm32/boards/Passport/modules/actions.py | 8 +--- .../boards/Passport/modules/self_test_ux.py | 38 +++++++++---------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/ports/stm32/boards/Passport/modules/actions.py b/ports/stm32/boards/Passport/modules/actions.py index 2f1056fe..cf382d5e 100644 --- a/ports/stm32/boards/Passport/modules/actions.py +++ b/ports/stm32/boards/Passport/modules/actions.py @@ -37,13 +37,10 @@ async def about_info(*a): from utils import swab32 while True: - serial = system.get_serial_number() my_xfp = settings.get('xfp', 0) xpub = settings.get('xpub', None) - msg = '''Serial Number: -{serial} - + msg = ''' Master Fingerprint: {xfp} @@ -51,8 +48,7 @@ async def about_info(*a): {rev_xfp} Master XPUB: -{xpub}'''.format(serial=serial, - xfp=xfp2str(my_xfp) if my_xfp else '', +{xpub}'''.format(xfp=xfp2str(my_xfp) if my_xfp else '', rev_xfp=xfp2str(swab32(my_xfp)) if my_xfp else '', xpub=xpub if xpub != None else '') diff --git a/ports/stm32/boards/Passport/modules/self_test_ux.py b/ports/stm32/boards/Passport/modules/self_test_ux.py index 93009f73..36d8eed0 100644 --- a/ports/stm32/boards/Passport/modules/self_test_ux.py +++ b/ports/stm32/boards/Passport/modules/self_test_ux.py @@ -53,30 +53,20 @@ class SelfTestUX(UXStateMachine): def __init__(self): # States - self.SHOW_SERIAL_NUMBER = 1 - self.KEYPAD_TEST = 2 - self.CAMERA_TEST = 3 - self.CAMERA_TEST_RESULT = 4 - self.SCREEN_ALIGNMENT = 5 - self.MICROSD_TEST = 6 + self.KEYPAD_TEST = 1 + self.CAMERA_TEST = 2 + self.CAMERA_TEST_RESULT = 3 + self.SCREEN_ALIGNMENT = 4 + self.MICROSD_TEST = 5 + self.TESTS_COMPLETE = 6 self.qr_data = None # print('SelfTestUX init') - super().__init__(self.SHOW_SERIAL_NUMBER) + super().__init__(self.KEYPAD_TEST) async def show(self): while True: - # print('show: state={}'.format(self.state)) - if self.state == self.SHOW_SERIAL_NUMBER: - serial = system.get_serial_number() - result = await ux_show_text_as_ur(title='Serial Num.', qr_text=serial, qr_type=QRType.QR, msg=serial, - right_btn='NEXT') # If right_btn is specified, then RESIZE doesn't appear/work, which is fine here - if result == 'x': - return - else: - self.goto(self.KEYPAD_TEST) - - elif self.state == self.KEYPAD_TEST: + if self.state == self.KEYPAD_TEST: # print('Keypad Test!') result = await ux_keypad_test() if result == 'x': @@ -117,8 +107,16 @@ async def show(self): elif self.state == self.MICROSD_TEST: # Describe the microSD test result = await ux_show_story('This test will exercise the read/write features of the microSD card.', title='microSD Test', right_btn='START', center=True, center_vertically=True) + if result == 'x': + self.goto_prev() + continue + + if await microsd_test(): + self.goto(self.TESTS_COMPLETE) + + elif self.state == self.TESTS_COMPLETE: + result = await ux_show_story('All tests complete!', title='Complete', right_btn='SHUTDOWN', center=True, center_vertically=True) if result == 'x': self.goto_prev() else: - await microsd_test() - return + system.shutdown() From ccb1d6d9888d5496a5e654d7c6f00bcafe8a3fe1 Mon Sep 17 00:00:00 2001 From: Ken Carpenter Date: Wed, 26 May 2021 14:46:09 -0700 Subject: [PATCH 6/6] Allow going back from microSD test result screen --- ports/stm32/boards/Passport/modules/self_test_ux.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ports/stm32/boards/Passport/modules/self_test_ux.py b/ports/stm32/boards/Passport/modules/self_test_ux.py index 36d8eed0..9db3754e 100644 --- a/ports/stm32/boards/Passport/modules/self_test_ux.py +++ b/ports/stm32/boards/Passport/modules/self_test_ux.py @@ -36,7 +36,9 @@ async def microsd_test(): return False os.remove(filename) - await ux_show_story('microSD card is working properly!', title='microSD Test', center=True, center_vertically=True) + result = await ux_show_story('microSD card is working properly!', title='microSD Test', center=True, center_vertically=True) + if result == 'x': + return False return True except CardMissingError: