1# Copyright (c) 2025 Core Devices LLC
2# Copyright (c) 2025 SiFli Technologies(Nanjing) Co., Ltd
3# SPDX-License-Identifier: Apache-2.0
4
5import argparse
6import shlex
7import subprocess
8
9from runners.core import RunnerCaps, RunnerConfig, ZephyrBinaryRunner
10
11
12class SftoolRunner(ZephyrBinaryRunner):
13    """Runner front-end for sftool CLI."""
14
15    def __init__(
16        self,
17        cfg: RunnerConfig,
18        chip: str,
19        port: str,
20        erase: bool,
21        dt_flash: bool,
22        tool_opt: list[str],
23        flash_files: list[str],
24    ) -> None:
25        super().__init__(cfg)
26
27        self._chip = chip
28        self._port = port
29        self._erase = erase
30        self._dt_flash = dt_flash
31        self._flash_files = flash_files
32
33        self._tool_opt: list[str] = []
34        for opts in [shlex.split(opt) for opt in tool_opt]:
35            self._tool_opt += opts
36
37    @classmethod
38    def name(cls):
39        return "sftool"
40
41    @classmethod
42    def capabilities(cls):
43        return RunnerCaps(commands={"flash"}, erase=True, flash_addr=True, file=True, tool_opt=True)
44
45    @classmethod
46    def do_add_parser(cls, parser):
47        parser.add_argument(
48            "--chip",
49            type=str,
50            required=True,
51            help="Target chip, e.g. SF32LB52",
52        )
53
54        parser.add_argument(
55            "--port",
56            type=str,
57            required=True,
58            help="Serial port device, e.g. /dev/ttyUSB0",
59        )
60        parser.add_argument(
61            "--flash-file",
62            dest="flash_files",
63            action="append",
64            default=[],
65            help="Additional file@address entries that must be flashed before the Zephyr image",
66        )
67
68    @classmethod
69    def do_create(cls, cfg: RunnerConfig, args: argparse.Namespace) -> "SftoolRunner":
70        return SftoolRunner(
71            cfg,
72            chip=args.chip,
73            port=args.port,
74            erase=args.erase,
75            dt_flash=args.dt_flash,
76            tool_opt=args.tool_opt,
77            flash_files=args.flash_files,
78        )
79
80    def do_run(self, command: str, **kwargs):
81        sftool = self.require("sftool")
82
83        cmd = [sftool, "--chip", self._chip, "--port", self._port]
84        cmd += self._tool_opt
85
86        flash_targets: list[str] = []
87        flash_targets.extend(self._flash_files)
88
89        if self.cfg.file:
90            flash_targets.append(self.cfg.file)
91        else:
92            if self.cfg.bin_file and self._dt_flash:
93                addr = self.flash_address_from_build_conf(self.build_conf)
94                flash_targets.append(f"{self.cfg.bin_file}@0x{addr:08x}")
95            elif self.cfg.hex_file:
96                flash_targets.append(self.cfg.hex_file)
97            else:
98                raise RuntimeError("No file available for flashing")
99
100        write_args = ["write_flash"]
101        if self._erase:
102            write_args.append("-e")
103
104        full_cmd = cmd + write_args + flash_targets
105        self._log_cmd(full_cmd)
106        if not self.dry_run:
107            subprocess.run(full_cmd, check=True)
108