diff --git a/scripts/west_commands/runners/blackmagicprobe.py b/scripts/west_commands/runners/blackmagicprobe.py index 5719fcbf415..54fc22a7eda 100644 --- a/scripts/west_commands/runners/blackmagicprobe.py +++ b/scripts/west_commands/runners/blackmagicprobe.py @@ -5,11 +5,106 @@ '''Runner for flashing with Black Magic Probe.''' # https://github.com/blacksphere/blackmagic/wiki +import glob +import os import signal +import sys from pathlib import Path from runners.core import ZephyrBinaryRunner, RunnerCaps +try: + import serial.tools.list_ports + MISSING_REQUIREMENTS = False +except ImportError: + MISSING_REQUIREMENTS = True + +# Default path for linux, based on the project udev.rules file. +DEFAULT_LINUX_BMP_PATH = '/dev/ttyBmpGdb' + +# Interface descriptor for the GDB port as defined in the BMP firmware. +BMP_GDB_INTERFACE = 'Black Magic GDB Server' + +# Product string as defined in the BMP firmware. +BMP_GDB_PRODUCT = "Black Magic Probe" + +# BMP vendor and product ID. +BMP_GDB_VID = 0x1d50 +BMP_GDB_PID = 0x6018 + +LINUX_SERIAL_GLOB = '/dev/ttyACM*' +DARWIN_SERIAL_GLOB = '/dev/cu.usbmodem*' + +def blackmagicprobe_gdb_serial_linux(): + '''Guess the GDB port on Linux platforms.''' + if os.path.exists(DEFAULT_LINUX_BMP_PATH): + return DEFAULT_LINUX_BMP_PATH + + if not MISSING_REQUIREMENTS: + for port in serial.tools.list_ports.comports(): + if port.interface == BMP_GDB_INTERFACE: + return port.device + + ports = glob.glob(LINUX_SERIAL_GLOB) + if not ports: + raise RuntimeError( + f'cannot find any valid port matching {LINUX_SERIAL_GLOB}') + return sorted(ports)[0] + +def blackmagicprobe_gdb_serial_darwin(): + '''Guess the GDB port on Darwin platforms.''' + if not MISSING_REQUIREMENTS: + bmp_ports = [] + for port in serial.tools.list_ports.comports(): + if port.description and port.description.startswith( + BMP_GDB_PRODUCT): + bmp_ports.append(port.device) + if bmp_ports: + return sorted(bmp_ports)[0] + + ports = glob.glob(DARWIN_SERIAL_GLOB) + if not ports: + raise RuntimeError( + f'cannot find any valid port matching {DARWIN_SERIAL_GLOB}') + return sorted(ports)[0] + +def blackmagicprobe_gdb_serial_win32(): + '''Guess the GDB port on Windows platforms.''' + if not MISSING_REQUIREMENTS: + bmp_ports = [] + for port in serial.tools.list_ports.comports(): + if port.vid == BMP_GDB_VID and port.pid == BMP_GDB_PID: + bmp_ports.append(port.device) + if bmp_ports: + return sorted(bmp_ports)[0] + + return 'COM1' + +def blackmagicprobe_gdb_serial(port): + '''Guess the GDB port for the probe. + + Return the port to use, in order of priority: + - the port specified manually + - the port in the BMP_GDB_SERIAL environment variable + - a guessed one depending on the host + ''' + if port: + return port + + if 'BMP_GDB_SERIAL' in os.environ: + return os.environ['BMP_GDB_SERIAL'] + + platform = sys.platform + if platform.startswith('linux'): + return blackmagicprobe_gdb_serial_linux() + elif platform.startswith('darwin'): + return blackmagicprobe_gdb_serial_darwin() + elif platform.startswith('win32'): + return blackmagicprobe_gdb_serial_win32() + else: + raise RuntimeError(f'unsupported platform: {platform}') + + class BlackMagicProbeRunner(ZephyrBinaryRunner): '''Runner front-end for Black Magic probe.''' @@ -21,7 +116,8 @@ class BlackMagicProbeRunner(ZephyrBinaryRunner): # # https://github.com/zephyrproject-rtos/zephyr/issues/50789 self.elf_file = Path(cfg.elf_file).as_posix() - self.gdb_serial = gdb_serial + self.gdb_serial = blackmagicprobe_gdb_serial(gdb_serial) + self.logger.info(f'using GDB serial: {self.gdb_serial}') if connect_rst: self.connect_rst_enable_arg = [ '-ex', "monitor connect_rst enable", @@ -49,8 +145,7 @@ class BlackMagicProbeRunner(ZephyrBinaryRunner): @classmethod def do_add_parser(cls, parser): - parser.add_argument('--gdb-serial', default='/dev/ttyACM0', - help='GDB serial port') + parser.add_argument('--gdb-serial', help='GDB serial port') parser.add_argument('--connect-rst', '--connect-srst', action='store_true', help='Assert SRST during connect? (default: no)') diff --git a/scripts/west_commands/tests/test_blackmagicprobe.py b/scripts/west_commands/tests/test_blackmagicprobe.py index abfb076a48d..992d3965ac5 100644 --- a/scripts/west_commands/tests/test_blackmagicprobe.py +++ b/scripts/west_commands/tests/test_blackmagicprobe.py @@ -5,11 +5,14 @@ import argparse from unittest.mock import patch, call +import os import pytest +from runners import blackmagicprobe from runners.blackmagicprobe import BlackMagicProbeRunner from conftest import RC_KERNEL_ELF, RC_GDB +import serial.tools.list_ports TEST_GDB_SERIAL = 'test-gdb-serial' @@ -88,3 +91,111 @@ def test_blackmagicprobe_connect_rst(cc, req, command, runner_config): runner.run(command) expected = EXPECTED_CONNECT_SRST_COMMAND[command] assert expected in cc.call_args_list[0][0][0] + +@pytest.mark.parametrize('arg, env, expected', [ + # Argument has priority + ('/dev/XXX', None, '/dev/XXX'), + ('/dev/XXX', '/dev/YYYY', '/dev/XXX'), + + # Then BMP_GDB_SERIAL env variable + (None, '/dev/XXX', '/dev/XXX'), + ]) +def test_blackmagicprobe_gdb_serial_generic(arg, env, expected): + if env: + os.environ['BMP_GDB_SERIAL'] = env + else: + if 'BMP_GDB_SERIAL' in os.environ: + os.environ.pop('BMP_GDB_SERIAL') + + ret = blackmagicprobe.blackmagicprobe_gdb_serial(arg) + assert expected == ret + +@pytest.mark.parametrize('known_path, comports, globs, expected', [ + (True, False, ['/dev/ttyACM0', '/dev/ttyACM1'], + blackmagicprobe.DEFAULT_LINUX_BMP_PATH), + (False, True, [], '/dev/ttyACM3'), + (False, False, ['/dev/ttyACM0', '/dev/ttyACM1'], '/dev/ttyACM0'), + (False, False, ['/dev/ttyACM1', '/dev/ttyACM0'], '/dev/ttyACM0'), + ]) +@patch('serial.tools.list_ports.comports') +@patch('os.path.exists') +@patch('glob.glob') +def test_blackmagicprobe_gdb_serial_linux(gg, ope, stlpc, known_path, comports, + globs, expected): + gg.return_value = globs + ope.return_value = known_path + if comports: + fake_comport1 = serial.tools.list_ports_common.ListPortInfo( + '/dev/ttyACM1') + fake_comport1.interface = 'something' + fake_comport2 = serial.tools.list_ports_common.ListPortInfo( + '/dev/ttyACM2') + fake_comport2.interface = None + fake_comport3 = serial.tools.list_ports_common.ListPortInfo( + '/dev/ttyACM3') + fake_comport3.interface = blackmagicprobe.BMP_GDB_INTERFACE + stlpc.return_value = [fake_comport1, fake_comport2, fake_comport3] + else: + stlpc.return_value = [] + + ret = blackmagicprobe.blackmagicprobe_gdb_serial_linux() + assert expected == ret + +@pytest.mark.parametrize('comports, globs, expected', [ + (True, [], '/dev/cu.usbmodem3'), + (False, ['/dev/cu.usbmodemAABBCC0', '/dev/cu.usbmodemAABBCC1'], + '/dev/cu.usbmodemAABBCC0'), + (False, ['/dev/cu.usbmodemAABBCC1', '/dev/cu.usbmodemAABBCC0'], + '/dev/cu.usbmodemAABBCC0'), + ]) +@patch('serial.tools.list_ports.comports') +@patch('glob.glob') +def test_blackmagicprobe_gdb_serial_darwin(gg, stlpc, comports, globs, expected): + gg.return_value = globs + if comports: + fake_comport1 = serial.tools.list_ports_common.ListPortInfo( + '/dev/cu.usbmodem1') + fake_comport1.description = 'unrelated' + fake_comport2 = serial.tools.list_ports_common.ListPortInfo( + '/dev/cu.usbmodem2') + fake_comport2.description = None + fake_comport3 = serial.tools.list_ports_common.ListPortInfo( + '/dev/cu.usbmodem3') + fake_comport3.description = f'{blackmagicprobe.BMP_GDB_PRODUCT} v1234' + fake_comport4 = serial.tools.list_ports_common.ListPortInfo( + '/dev/cu.usbmodem4') + fake_comport4.description = f'{blackmagicprobe.BMP_GDB_PRODUCT} v1234' + stlpc.return_value = [fake_comport1, fake_comport2, + fake_comport4, fake_comport3] + else: + stlpc.return_value = [] + + ret = blackmagicprobe.blackmagicprobe_gdb_serial_darwin() + assert expected == ret + +@pytest.mark.parametrize('comports, expected', [ + (True, 'COM4'), + (False, 'COM1'), + ]) +@patch('serial.tools.list_ports.comports') +def test_blackmagicprobe_gdb_serial_win32(stlpc, comports, expected): + if comports: + fake_comport1 = serial.tools.list_ports_common.ListPortInfo('COM2') + fake_comport1.vid = 123 + fake_comport1.pid = 456 + fake_comport2 = serial.tools.list_ports_common.ListPortInfo('COM3') + fake_comport2.vid = None + fake_comport2.pid = None + fake_comport3 = serial.tools.list_ports_common.ListPortInfo('COM4') + fake_comport3.vid = blackmagicprobe.BMP_GDB_VID + fake_comport3.pid = blackmagicprobe.BMP_GDB_PID + fake_comport4 = serial.tools.list_ports_common.ListPortInfo('COM5') + fake_comport4.vid = blackmagicprobe.BMP_GDB_VID + fake_comport4.pid = blackmagicprobe.BMP_GDB_PID + stlpc.return_value = [fake_comport1, fake_comport2, + fake_comport4, fake_comport3] + else: + stlpc.return_value = [] + + ret = blackmagicprobe.blackmagicprobe_gdb_serial_win32() + assert expected == ret