1# Copyright (c) 2017 Linaro Limited.
2# Copyright (c) 2020 Gerson Fernando Budke <nandojve@gmail.com>
3#
4# SPDX-License-Identifier: Apache-2.0
5
6'''bossac-specific runner (flash only) for Atmel SAM microcontrollers.'''
7
8import os
9import pathlib
10import pickle
11import platform
12import subprocess
13import sys
14import time
15
16from runners.core import RunnerCaps, ZephyrBinaryRunner
17
18if platform.system() == 'Darwin':
19    DEFAULT_BOSSAC_PORT = None
20else:
21    DEFAULT_BOSSAC_PORT = '/dev/ttyACM0'
22DEFAULT_BOSSAC_SPEED = '115200'
23
24class BossacBinaryRunner(ZephyrBinaryRunner):
25    '''Runner front-end for bossac.'''
26
27    def __init__(self, cfg, bossac='bossac', port=DEFAULT_BOSSAC_PORT,
28                 speed=DEFAULT_BOSSAC_SPEED, boot_delay=0, erase=False):
29        super().__init__(cfg)
30        self.bossac = bossac
31        self.port = port
32        self.speed = speed
33        self.boot_delay = boot_delay
34        self.erase = erase
35
36    @classmethod
37    def name(cls):
38        return 'bossac'
39
40    @classmethod
41    def capabilities(cls):
42        return RunnerCaps(commands={'flash'}, erase=True)
43
44    @classmethod
45    def do_add_parser(cls, parser):
46        parser.add_argument('--bossac', default='bossac',
47                            help='path to bossac, default is bossac')
48        parser.add_argument('--bossac-port', default=DEFAULT_BOSSAC_PORT,
49                            help='serial port to use, default is ' +
50                            str(DEFAULT_BOSSAC_PORT))
51        parser.add_argument('--speed', default=DEFAULT_BOSSAC_SPEED,
52                            help='serial port speed to use, default is ' +
53                            DEFAULT_BOSSAC_SPEED)
54        parser.add_argument('--delay', default=0, type=float,
55                            help='''delay in seconds (may be a floating
56                            point number) to wait between putting the board
57                            into bootloader mode and running bossac;
58                            default is no delay''')
59
60    @classmethod
61    def do_create(cls, cfg, args):
62        return BossacBinaryRunner(cfg, bossac=args.bossac,
63                                  port=args.bossac_port, speed=args.speed,
64                                  boot_delay=args.delay, erase=args.erase)
65
66    def read_help(self):
67        """Run bossac --help and return the output as a list of lines"""
68        self.require(self.bossac)
69        try:
70            # BOSSA > 1.9.1 returns OK
71            out = self.check_output([self.bossac, '--help']).decode()
72        except subprocess.CalledProcessError as ex:
73            # BOSSA <= 1.9.1 returns an error
74            out = ex.output.decode()
75
76        return out.split('\n')
77
78    def supports(self, flag):
79        """Check if bossac supports a flag by searching the help"""
80        return any(flag in line for line in self.read_help())
81
82    def is_extended_samba_protocol(self):
83        ext_samba_versions = ['CONFIG_BOOTLOADER_BOSSA_ARDUINO',
84                              'CONFIG_BOOTLOADER_BOSSA_ADAFRUIT_UF2']
85
86        return any(self.build_conf.getboolean(x) for x in ext_samba_versions)
87
88    def is_partition_enabled(self):
89        return self.build_conf.getboolean('CONFIG_USE_DT_CODE_PARTITION')
90
91    def get_chosen_code_partition_node(self):
92        # Get the EDT Node corresponding to the zephyr,code-partition
93        # chosen DT node
94
95        # Ensure the build directory has a compiled DTS file
96        # where we expect it to be.
97        b = pathlib.Path(self.cfg.build_dir)
98        edt_pickle = b / 'zephyr' / 'edt.pickle'
99        if not edt_pickle.is_file():
100            error_msg = "can't load devicetree; expected to find:" + str(edt_pickle)
101
102            raise RuntimeError(error_msg)
103
104        # Load the devicetree.
105        try:
106            with open(edt_pickle, 'rb') as f:
107                edt = pickle.load(f)
108        except ModuleNotFoundError as err:
109            error_msg = "could not load devicetree, something may be wrong " \
110                    + "with the python environment"
111            raise RuntimeError(error_msg) from err
112
113        return edt.chosen_node('zephyr,code-partition')
114
115    def get_board_name(self):
116        if 'CONFIG_BOARD' not in self.build_conf:
117            return '<board>'
118
119        return self.build_conf['CONFIG_BOARD']
120
121    def get_dts_img_offset(self):
122        if self.build_conf.getboolean('CONFIG_BOOTLOADER_BOSSA_LEGACY'):
123            return 0
124
125        if self.build_conf.getboolean('CONFIG_HAS_FLASH_LOAD_OFFSET'):
126            return self.build_conf['CONFIG_FLASH_LOAD_OFFSET']
127
128        return 0
129
130    def get_image_offset(self, supports_offset):
131        """Validates and returns the flash offset"""
132
133        dts_img_offset = self.get_dts_img_offset()
134
135        if int(str(dts_img_offset), 16) > 0:
136            if not supports_offset:
137                old_sdk = 'This version of BOSSA does not support the' \
138                          ' --offset flag. Please upgrade to a newer Zephyr' \
139                          ' SDK version >= 0.12.0.'
140                raise RuntimeError(old_sdk)
141
142            return dts_img_offset
143
144        return None
145
146    def is_gnu_coreutils_stty(self):
147        try:
148            result = subprocess.run(
149                ['stty', '--version'], capture_output=True, text=True, check=True
150            )
151            return 'coreutils' in result.stdout
152        except subprocess.CalledProcessError:
153            return False
154
155    def set_serial_config(self):
156        if platform.system() == 'Linux' or platform.system() == 'Darwin':
157            self.require('stty')
158
159            # GNU coreutils uses a capital F flag for 'file'
160            flag = '-F' if self.is_gnu_coreutils_stty() else '-f'
161
162            if self.is_extended_samba_protocol():
163                self.speed = '1200'
164
165            cmd_stty = ['stty', flag, self.port, 'raw', 'ispeed', self.speed,
166                        'ospeed', self.speed, 'cs8', '-cstopb', 'ignpar',
167                        'eol', '255', 'eof', '255']
168            self.check_call(cmd_stty)
169            self.magic_delay()
170
171    def magic_delay(self):
172        '''There can be a time lag between the board resetting into
173        bootloader mode (done via stty above) and the OS enumerating
174        the USB device again. This function lets users tune a magic
175        delay for their system to handle this case. By default,
176        we don't wait.
177        '''
178
179        if self.boot_delay > 0:
180            time.sleep(self.boot_delay)
181
182    def make_bossac_cmd(self):
183        self.ensure_output('bin')
184        cmd_flash = [self.bossac, '-p', self.port, '-R', '-w', '-v',
185                     '-b', self.cfg.bin_file]
186
187        if self.erase:
188            cmd_flash += ['-e']
189
190        dt_chosen_code_partition_nd = self.get_chosen_code_partition_node()
191
192        if self.is_partition_enabled():
193            if dt_chosen_code_partition_nd is None:
194                error_msg = 'The device tree zephyr,code-partition chosen' \
195                            ' node must be defined.'
196
197                raise RuntimeError(error_msg)
198
199            offset = self.get_image_offset(self.supports('--offset'))
200
201            if offset is not None and int(str(offset), 16) > 0:
202                cmd_flash += ['-o', str(offset)]
203
204        elif dt_chosen_code_partition_nd is not None:
205            error_msg = 'There is no CONFIG_USE_DT_CODE_PARTITION Kconfig' \
206                        ' defined at ' + self.get_board_name() + \
207                        '_defconfig file.\n This means that' \
208                        ' zephyr,code-partition device tree node should not' \
209                        ' be defined. Check Zephyr SAM-BA documentation.'
210
211            raise RuntimeError(error_msg)
212
213        return cmd_flash
214
215    def get_darwin_serial_device_list(self):
216        """
217        Get a list of candidate serial ports on Darwin by querying the IOKit
218        registry.
219        """
220        import plistlib
221
222        ioreg_out = self.check_output(['ioreg', '-r', '-c', 'IOSerialBSDClient',
223                                       '-k', 'IOCalloutDevice', '-a'])
224        serial_ports = plistlib.loads(ioreg_out, fmt=plistlib.FMT_XML)
225
226        return [port["IOCalloutDevice"] for port in serial_ports]
227
228    def get_darwin_user_port_choice(self):
229        """
230        Ask the user to select the serial port from a set of candidate ports
231        retrieved from IOKit on Darwin.
232
233        Modelled on get_board_snr() in the nrfjprog runner.
234        """
235        devices = self.get_darwin_serial_device_list()
236
237        if len(devices) == 0:
238            raise RuntimeError('No candidate serial ports were found!')
239        elif len(devices) == 1:
240            print('Using only serial device on the system: ' + devices[0])
241            return devices[0]
242        elif not sys.stdin.isatty():
243            raise RuntimeError('Refusing to guess which serial port to use: '
244                               f'there are {len(devices)} available. '
245                               '(Interactive prompts disabled since standard '
246                               'input is not a terminal - please specify a '
247                               'port using --bossac-port instead)')
248
249        print('There are multiple serial ports available on this system:')
250
251        for i, device in enumerate(devices, 1):
252            print(f'    {i}. {device}')
253
254        p = f'Please select one (1-{len(devices)}, or EOF to exit): '
255
256        while True:
257            try:
258                value = input(p)
259            except EOFError:
260                sys.exit(0)
261            try:
262                value = int(value)
263            except ValueError:
264                continue
265            if 1 <= value <= len(devices):
266                break
267
268        return devices[value - 1]
269
270    def do_run(self, command, **kwargs):
271        if platform.system() == 'Linux':
272            if 'microsoft' in platform.uname().release.lower() or \
273                os.getenv('WSL_DISTRO_NAME') is not None or \
274                    os.getenv('WSL_INTEROP') is not None:
275                msg = 'CAUTION: BOSSAC runner not supported on WSL!'
276                raise RuntimeError(msg)
277        elif platform.system() == 'Darwin' and self.port is None:
278            self.port = self.get_darwin_user_port_choice()
279
280        self.require(self.bossac)
281        self.set_serial_config()
282        self.check_call(self.make_bossac_cmd())
283