1# Copyright (c) 2023, Antmicro <www.antmicro.com>
2#
3# Based on J-Link runner
4# Copyright (c) 2017 Linaro Limited.
5# SPDX-License-Identifier: Apache-2.0
6
7"""
8Runner that implements flashing with SiLabs Simplicity Commander binary tool.
9See SiLabs UG162: "Simplicity Commander Reference Guide" for more info.
10"""
11
12import ipaddress
13import os
14import re
15import shlex
16
17from runners.core import FileType, RunnerCaps, ZephyrBinaryRunner
18
19DEFAULT_APP = 'commander'
20
21def is_ip(ip):
22    if not ip:
23        return False
24    try:
25        ipaddress.ip_address(ip.split(':')[0])
26    except ValueError:
27        return False
28    return True
29
30class SiLabsCommanderBinaryRunner(ZephyrBinaryRunner):
31    def __init__(self, cfg, device, dev_id, commander, dt_flash, erase, speed, tool_opt,
32                 dev_id_type):
33        super().__init__(cfg)
34        self.file = cfg.file
35        self.file_type = cfg.file_type
36        self.hex_name = cfg.hex_file
37        self.bin_name = cfg.bin_file
38        self.elf_name = cfg.elf_file
39        self.device = device
40        self.dev_id = dev_id
41        self.commander = commander
42        self.dt_flash = dt_flash
43        self.erase = erase
44        self.speed = speed
45        self.dev_id_type = dev_id_type
46
47        self.tool_opt = []
48        for opts in [shlex.split(opt) for opt in tool_opt]:
49            self.tool_opt += opts
50
51    @staticmethod
52    def _detect_dev_id_type(dev_id):
53        """Detect device type based on dev_id pattern."""
54        if not dev_id:
55            return None
56
57        # Check if dev_id is numeric (serialno)
58        if re.match(r'^[0-9]+$', dev_id):
59            return 'serialno'
60
61        # Check if dev_id is an existing file (tty)
62        if os.path.exists(dev_id):
63            return 'tty'
64
65        # Check if dev_id is a valid IPv4
66        if is_ip(dev_id):
67            return 'ip'
68
69        # No match found
70        return None
71
72    @classmethod
73    def name(cls):
74        return 'silabs_commander'
75
76    @classmethod
77    def capabilities(cls):
78        return RunnerCaps(commands={'flash'},
79                          dev_id=True, flash_addr=True, erase=True,
80                          tool_opt=True, file=True)
81
82    @classmethod
83    def dev_id_help(cls) -> str:
84        return '''Device identifier. Can be either a serial number (for a USB connection), a path
85                  to a tty device, an IP address or a tunnel address. User can enforce the type of
86                  the identifier with --dev-id-type.
87                  If not specified, the first USB device detected is used. '''
88
89    @classmethod
90    def tool_opt_help(cls) -> str:
91        return "Additional options for Simplicity Commander, e.g. '--noreset'"
92
93    @classmethod
94    def do_add_parser(cls, parser):
95        # Required:
96        parser.add_argument('--device', required=True,
97                            help='device part number')
98
99        # Optional:
100        parser.add_argument('--commander', default=DEFAULT_APP,
101                            help='path to Simplicity Commander executable')
102        parser.add_argument('--speed', default=None,
103                            help='JTAG/SWD speed to use')
104        parser.add_argument('--dev-id-type', choices=['auto', 'serialno', 'tty', 'ip'],
105                            default='auto', help='Device type. "auto" (default) auto-detects '
106                            'the type, or specify explicitly')
107
108    @classmethod
109    def do_create(cls, cfg, args):
110        return SiLabsCommanderBinaryRunner(
111                cfg, args.device,
112                dev_id=args.dev_id,
113                commander=args.commander,
114                dt_flash=args.dt_flash,
115                erase=args.erase,
116                speed=args.speed,
117                tool_opt=args.tool_opt,
118                dev_id_type=args.dev_id_type)
119
120    def do_run(self, command, **kwargs):
121        self.require(self.commander)
122
123        opts = ['--device', self.device]
124        if self.erase:
125            opts.append('--masserase')
126        if self.dev_id:
127            # Determine dev_id_type
128            if self.dev_id_type == 'auto':
129                # Auto-detect device type
130                detected_type = self._detect_dev_id_type(self.dev_id)
131            else:
132                # Use manually specified device type
133                detected_type = self.dev_id_type
134
135            # Add appropriate option based on device type
136            if detected_type == 'serialno':
137                opts.extend(['--serialno', self.dev_id])
138            elif detected_type == 'tty':
139                opts.extend(['--identifybyserialport', self.dev_id])
140            elif detected_type == 'ip':
141                opts.extend(['--ip', self.dev_id])
142            else:
143                # Fallback to legacy behavior (serialno)
144                opts.extend(['--serialno', self.dev_id])
145                self.logger.warning(f'"{self.dev_id}" does not match any known pattern')
146
147        if self.speed is not None:
148            opts.extend(['--speed', self.speed])
149
150        # Get the build artifact to flash
151
152        if self.dt_flash:
153            flash_addr = self.flash_address_from_build_conf(self.build_conf)
154        else:
155            flash_addr = 0
156
157        if self.file is not None:
158            # use file provided by the user
159            if not os.path.isfile(self.file):
160                raise ValueError(f'Cannot flash; file ({self.file}) not found')
161
162            flash_file = self.file
163
164            if self.file_type == FileType.HEX:
165                flash_args = [flash_file]
166            elif self.file_type == FileType.BIN:
167                flash_args = ['--binary', '--address', f'0x{flash_addr:x}', flash_file]
168            else:
169                raise ValueError('Cannot flash; this runner only supports hex and bin files')
170
171        else:
172            # use hex or bin file provided by the buildsystem, preferring .hex over .bin
173            if self.hex_name is not None and os.path.isfile(self.hex_name):
174                flash_file = self.hex_name
175                flash_args = [flash_file]
176            elif self.bin_name is not None and os.path.isfile(self.bin_name):
177                flash_file = self.bin_name
178                flash_args = ['--binary', '--address', f'0x{flash_addr:x}', flash_file]
179            else:
180                raise ValueError(f'Cannot flash; no hex ({self.hex_name}) or '
181                                 f'bin ({self.bin_name}) files found.')
182
183        args = [self.commander, 'flash'] + opts + self.tool_opt + flash_args
184
185        self.logger.info(f'Flashing file: {flash_file}')
186        self.check_call(args)
187