1# Copyright (c) 2017 Linaro Limited.
2#
3# SPDX-License-Identifier: Apache-2.0
4
5'''Runner for flashing with dfu-util.'''
6
7import sys
8import time
9from collections import namedtuple
10
11from runners.core import BuildConfiguration, RunnerCaps, ZephyrBinaryRunner
12
13DfuSeConfig = namedtuple('DfuSeConfig', ['address', 'options'])
14
15
16class DfuUtilBinaryRunner(ZephyrBinaryRunner):
17    '''Runner front-end for dfu-util.'''
18
19    def __init__(self, cfg, dev_id, alt, img, exe='dfu-util',
20                 dfuse_config=None):
21
22        super().__init__(cfg)
23        self.dev_id = dev_id # Used only for error checking in do_run
24        self.alt = alt
25        self.img = img
26        self.cmd = [exe, f'-d,{dev_id}']
27        try:
28            self.list_pattern = f', alt={int(self.alt)},'
29        except ValueError:
30            self.list_pattern = f', name="{self.alt}",'
31
32        if dfuse_config is None:
33            self.dfuse = False
34        else:
35            self.dfuse = True
36        self.dfuse_config = dfuse_config
37        self.reset = False
38
39    @classmethod
40    def name(cls):
41        return 'dfu-util'
42
43    @classmethod
44    def capabilities(cls):
45        return RunnerCaps(commands={'flash'}, dev_id=True, flash_addr=True)
46
47    @classmethod
48    def dev_id_help(cls) -> str:
49        return 'USB VID:PID of the connected device.'
50
51    @classmethod
52    def do_add_parser(cls, parser):
53        parser.add_argument("--alt", required=True,
54                            help="interface alternate setting number or name")
55
56        # Optional:
57        parser.add_argument("--pid", dest='dev_id',
58                            help=cls.dev_id_help())
59        parser.add_argument("--img",
60                            help="binary to flash, default is --bin-file")
61        parser.add_argument("--dfuse", default=False, action='store_true',
62                            help='''use the DfuSe protocol extensions
63                                 supported by STMicroelectronics
64                                 devices (if given, the image flash
65                                 address respects
66                                 CONFIG_FLASH_BASE_ADDRESS and
67                                 CONFIG_FLASH_LOAD_OFFSET)''')
68        parser.add_argument("--dfuse-modifiers", default='leave',
69                            help='''colon-separated list of additional
70                                 DfuSe modifiers for dfu-util's -s
71                                 option (default is
72                                 "-s <flash-address>:leave", which starts
73                                 execution immediately); requires
74                                 --dfuse
75                                 ''')
76        parser.add_argument('--dfu-util', default='dfu-util',
77                            help='dfu-util executable; defaults to "dfu-util"')
78
79    @classmethod
80    def do_create(cls, cfg, args):
81        if args.img is None:
82            args.img = cfg.bin_file
83
84        if args.dfuse:
85            args.dt_flash = True  # --dfuse implies --dt-flash.
86            build_conf = BuildConfiguration(cfg.build_dir)
87            dcfg = DfuSeConfig(address=cls.get_flash_address(args, build_conf),
88                               options=args.dfuse_modifiers)
89        else:
90            dcfg = None
91
92        ret = DfuUtilBinaryRunner(cfg, args.dev_id, args.alt, args.img,
93                                  exe=args.dfu_util, dfuse_config=dcfg)
94        ret.ensure_device()
95        return ret
96
97    def ensure_device(self):
98        if not self.find_device():
99            self.reset = True
100            print('Please reset your board to switch to DFU mode...')
101            while not self.find_device():
102                time.sleep(0.1)
103
104    def find_device(self):
105        cmd = list(self.cmd) + ['-l']
106        output = self.check_output(cmd)
107        output = output.decode(sys.getdefaultencoding())
108        return self.list_pattern in output
109
110    def do_run(self, command, **kwargs):
111        if not self.dev_id:
112            raise RuntimeError('Please specify a USB VID:PID with the '
113                               '-i/--dev-id or --pid command-line switch.')
114        self.require(self.cmd[0])
115        self.ensure_output('bin')
116
117        if not self.find_device():
118            raise RuntimeError('device not found')
119
120        cmd = list(self.cmd)
121        if self.dfuse:
122            # http://dfu-util.sourceforge.net/dfuse.html
123            dcfg = self.dfuse_config
124            addr_opts = hex(dcfg.address) + ':' + dcfg.options
125            cmd.extend(['-s', addr_opts])
126        cmd.extend(['-a', self.alt, '-D', self.img])
127        self.check_call(cmd)
128
129        if self.dfuse and 'leave' in dcfg.options.split(':'):
130            # Normal DFU devices generally need to be reset to switch
131            # back to the flashed program.
132            #
133            # DfuSe targets do as well, except when 'leave' is given
134            # as an option.
135            self.reset = False
136        if self.reset:
137            print('Now reset your board again to switch back to runtime mode.')
138