#!/usr/bin/env python import hid_test import time import numpy from subprocess import run import usb.core import uuid import encode_usb_strings import serial import serial.tools.list_ports; import shutil import os # Locations for external utilities and files referenced by the test program file_locations = { 'iceprog':'tillitis-iceprog', 'chprog':'chprog', 'app_gateware':'binaries/top.bin', 'ch552_firmware':'binaries/usb_device_cdc.bin', 'ch552_firmware_injected':'/tmp/ch552_fw_injected.bin', 'pico_bootloader_source':'binaries/main.uf2', 'pico_bootloader_target_dir':'/media/lab/RPI-RP2/' } def enable_power(): """Enable power to the TK-1""" d = hid_test.ice40_flasher() d.gpio_set_direction(7, True) d.gpio_put(7, True) d.close() time.sleep(.2) return True def disable_power(): """Disable power to the TK-1""" time.sleep(.1) d = hid_test.ice40_flasher() d.gpio_set_direction(7, True) d.gpio_put(7, False) d.close() return True def measure_voltages(device, samples): """Measure the voltage levels of the three mta1 power rails multiple times, and return the average values""" adc_vals = numpy.array([0,0,0]) for i in range(0,samples): adc_vals = adc_vals + device.adc_read_all() adc_vals = dict(zip(['1.2','2.5','3.3'],adc_vals/samples)) return adc_vals def voltage_test(): """Measure 3.3V 2.5V, and 1.2V voltage rails on the TK-1""" enable_power() d = hid_test.ice40_flasher() vals = measure_voltages(d,20) d.close() disable_power() print('voltages:',', '.join('{:}V:{:.3f}'.format(val[0],val[1]) for val in vals.items())) if ( (abs(vals['1.2'] - 1.2) > .2) | (abs(vals['2.5'] - 2.5) > .2) | (abs(vals['3.3'] - 3.3) > .2) ): return False return True def flash_validate_id(): """Read the ID from TK-1 SPI flash, and verify that it matches the expected value""" result = run([ file_locations['iceprog'], '-t' ], capture_output=True) err = result.stderr.split(b'\n') for line in err: if line.startswith(b'flash ID:'): vals_b = line.split(b' 0x')[1:] flash_id = int(b''.join(vals_b),16) print(line, hex(flash_id)) # Note: Flash IDs reported by iceprog are slightly wrong flash_types = { 0xb40140b40140b40140b40140b4014: 'XT25F08BDFIGT-S (MTA1-USB-V1)', 0xef401400: 'W25Q80DVUXIE (TK-1)' } flash_type = flash_types.get(flash_id) if flash_type == None: print('Flash ID invalid') return False print('Detected flash type: {:}'.format(flash_type)) return True return (result.returncode == 0) def flash_program(): """Program and verify the TK-1 SPI flash with the application test gateware""" result = run([ file_locations['iceprog'], file_locations['app_gateware'] ]) print(result) return (result.returncode == 0) def flash_check(): """Verify the TK-1 SPI flash is programmed with the application test gateware""" result = run([ file_locations['iceprog'], '-c', file_locations['app_gateware'] ]) print(result) return (result.returncode == 0) def test_extra_io(): """Test the TK-1 RTS, CTS, and GPIO1-4 lines by measuring a test pattern generated by the app_test gateware""" enable_power() time.sleep(.1) d = hid_test.ice40_flasher() d.gpio_put(16, False) d.gpio_set_direction(16, True) expected_results = [1<<(i%5) for i in range(9,-1,-1)] results = [] for i in range(0,10): vals = d.gpio_get_all() pattern = (vals >> 17) & 0b11111 results.append(pattern) d.gpio_put(16, True) d.gpio_put(16, False) d.gpio_set_direction(16, False) d.close() disable_power() print(results,expected_results,results == expected_results) return results == expected_results def test_found_bootloader(): """Search for a CH552 in USB bootloader mode""" print('\n\n\nSearching for CH552 bootloader, plug in USB cable now (times out in 10 seconds)!') for trys in range(0,100): # retry every 0.1s, up to 10 seconds devices= usb.core.find(idVendor=0x4348, idProduct=0x55e0, find_all=True) count = len(list(devices)) if count == 1: return True time.sleep(0.1) post = usb.core.find(idVendor=0x4348, idProduct=0x55e0, find_all=True) post_count = len(list(post)) return (post_count == 1) def inject_serial_number(infile, outfile, serial): """Inject a serial number into the specified CH552 firmware file""" magic = encode_usb_strings.string_to_descriptor("68de5d27-e223-4874-bc76-a54d6e84068f") replacement = encode_usb_strings.string_to_descriptor(serial) f = bytearray(open(infile, 'rb').read()) pos = f.find(magic) if pos < 0: print('failed to find magic string') exit(1) f[pos:(pos+len(magic))] = replacement with open(outfile, 'wb') as of: of.write(f) def flash_ch552(serial): """Flash an attached CH552 device with the USB CDC firmware, injected with the given serial number""" print(serial) inject_serial_number( file_locations['ch552_firmware'], file_locations['ch552_firmware_injected'], serial) # Program the CH552 using CHPROG result = run([ file_locations['chprog'], file_locations['ch552_firmware_injected'] ]) print(result) return (result.returncode == 0) def find_serial_device(desc): """Look for a serial device that has the given attributes""" for port in serial.tools.list_ports.comports(): matched = True for key, value in desc.items(): if not getattr(port, key) == value: matched = False if matched: print(port.device) return port.device return None def find_ch552(serial): """Search all serial devices for one that has the correct description and serial number""" time.sleep(1) description = { 'vid':0x1207, 'pid':0x8887, 'manufacturer':'Tillitis', 'product':'MTA1-USB-V1', 'serial_number':serial } if find_serial_device(description) == None: return False return True def ch552_program(): """Load the CDC ACM firmware onto a CH552 with a randomly generated serial number, and verify that it boots correctly""" if not test_found_bootloader(): print('Error finding CH552!') return False serial = str(uuid.uuid4()) if not flash_ch552(serial): print('Error flashing CH552!') return False if not find_ch552(serial): print('Error finding flashed CH552!') return False return True def test_txrx_touchpad(): """Test UART communication, RGB LED, and touchpad by asking the operator to interact with the touch pad""" description = { 'vid':0x1207, 'pid':0x8887, 'manufacturer':'Tillitis', 'product':'MTA1-USB-V1' } s = serial.Serial(find_serial_device(description),9600, timeout=.2) if not s.isOpen(): print('couldn\'t find/open serial device') return False for i in range(0,5): # Attempt to clear any buffered data from the serial port s.write(b'0123') time.sleep(0.2) s.read(20) try: s.write(b'0') [count, touch_count] = s.read(2) print('read count:{:}, touch count:{:}'.format(count,touch_count)) input('\n\n\nPress touch pad once and check LED, then press Enter') s.write(b'0') [count_post, touch_count_post] = s.read(2) print('read count:{:}, touch count:{:}'.format(count_post,touch_count_post)) if (count_post - count != 1) or (touch_count_post - touch_count !=1): print('Unexpected values returned, trying again') continue return True except ValueError as e: print(e) continue print('Max retries exceeded, failure!') return False def program_pico(): """Load the ice40 flasher firmware onto the TP-1""" print('Attach test rig to USB (times out in 10 seconds)') firmware_filename = os.path.split(file_locations['pico_bootloader_source'])[1] for trys in range(0,100): # retry every 0.1s try: shutil.copyfile( file_locations['pico_bootloader_source'], file_locations['pico_bootloader_target_dir'] + firmware_filename ) # TODO: Test if the pico identifies as a USB-HID device after programming return True except FileNotFoundError: time.sleep(0.1) except PermissionError: time.sleep(0.1) return False def sleep_2(): """Sleep for 2 seconds""" time.sleep(2) return True manual_tests = [ program_pico, voltage_test, flash_validate_id, flash_program, flash_check, test_extra_io, ch552_program, test_txrx_touchpad, enable_power, disable_power ] test_sequences = { 'tk1_test_sequence' : [ voltage_test, flash_validate_id, flash_program, sleep_2, test_extra_io, ch552_program, test_txrx_touchpad ], 'tp1_test_sequence' : [ program_pico, sleep_2, flash_validate_id ], 'mta1_usb_v1_programmer_test_sequence' : [ program_pico, sleep_2, voltage_test, flash_validate_id, sleep_2, test_extra_io ], } pass_msg = ''' _____ _____ _____ | __ \ /\ / ____| / ____| | |__) | / \ | (___ | (___ | ___/ / /\ \ \___ \ \___ \ | | / ____ \ ____) | ____) | |_| /_/ \_\ |_____/ |_____/ ''' fail_msg = ''' ______ _____ _ | ____| /\ |_ _| | | | |__ / \ | | | | | __| / /\ \ | | | | | | / ____ \ _| |_ | |____ |_| /_/ \_\ |_____| |______| ''' ANSI = { 'fg_black':"\u001b[30m", 'fg_red':"\u001b[31m", 'fg_green':"\u001b[32m", 'fg_yellow':"\u001b[33m", 'fg_blue':"\u001b[34m", 'fg_magenta':"\u001b[35m", 'fg_cyan':"\u001b[36m", 'fg_white':"\u001b[37m", 'bg_black':"\u001b[40m", 'bg_red':"\u001b[41m", 'bg_green':"\u001b[42m", 'bg_yellow':"\u001b[43m", 'bg_blue':"\u001b[44m", 'bg_magenta':"\u001b[45m", 'bg_cyan':"\u001b[46m", 'bg_white':"\u001b[47m", 'reset':"\u001b[0m", 'bold':"\u001b[1m", 'underline':"\u001b[4m" } def run_tests(test_list): for test in test_list: print("\n{:}Test step: {:}{:} ({:})".format(ANSI['bold'],test.__name__, ANSI['reset'], test.__doc__)) if not test(): print('error running test step ' + test.__name__) return False return True if __name__ == '__main__': last_a = 0 # Allow any of the settings in the file_locations structure to be overridden import argparse parser = argparse.ArgumentParser() for setting, value in file_locations.items(): parser.add_argument('--' + setting, help='Default setting: ' + value) args = parser.parse_args() for arg in args.__dict__: if args.__dict__[arg] is not None: print(arg, args.__dict__[arg]) file_locations[arg] = args.__dict__[arg] print(file_locations) print('\n\nTillitis TK-1 and TP-1 Production tests') while True: print('\n\n') options = [] print('=== Test sequences ===') i = 1 for name, tests in test_sequences.items(): print('{:}{:}. {:}{:}: {:}'.format(ANSI['bold'], i, name, ANSI['reset'], ', '.join([test.__name__ for test in tests]))) options.append(tests) i += 1 print('\n=== Manual tests ===') for test in manual_tests: print('{:}{:}. {:}{:}: {:}'.format(ANSI['bold'], i, test.__name__, ANSI['reset'], test.__doc__)) options.append([test]) i += 1 if(int(last_a) == 0): a = input('\n\n\nPlease type an option number and press return:') else: a = input('\n\n\nPress return to re-run test {:}, or type in a new option number and press return:'.format(last_a)) if a == '': a = last_a try: test_sequence = options[int(a)-1] except IndexError as e: print('Invalid input') continue except ValueError as e: print('Invalid input') continue try: result = run_tests(test_sequence) except Exception as e: print(e) result = False if not result: print(ANSI['bg_red'] + fail_msg + ANSI['reset']) try: disable_power() except AttributeError as e: pass except OSError as e: pass else: print(ANSI['bg_green'] + pass_msg + ANSI['reset']) last_a = a