1# Copyright (c) 2020 Espressif Systems (Shanghai) Co., Ltd.
2#
3# SPDX-License-Identifier: Apache-2.0
4
5'''tools.py
6
7Espressif west extension for serial port monitor logging.'''
8
9import os
10import platform
11import subprocess
12import sys
13import yaml
14from pathlib import Path
15
16from textwrap import dedent
17from west.commands import WestCommand
18from west.configuration import config
19from west import log
20
21# This relies on this file being in hal_espressif/west/tools.py
22# If you move this file, you'll break it, so be careful.
23THIS_ZEPHYR = Path(__file__).parents[4] / 'zephyr'
24ZEPHYR_BASE = Path(os.environ.get('ZEPHYR_BASE', THIS_ZEPHYR))
25
26sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts", "west_commands"))
27
28from build_helpers import load_domains
29from build_helpers import is_zephyr_build, find_build_dir  # noqa: E402
30from runners.core import BuildConfiguration  # noqa: E402
31
32ESP_IDF_REMOTE = "https://github.com/zephyrproject-rtos/hal_espressif"
33
34
35def cmd_check(cmd, cwd=None, stderr=subprocess.STDOUT):
36    return subprocess.check_output(cmd, cwd=cwd, stderr=stderr)
37
38
39def cmd_exec(cmd, cwd=None, shell=False):
40    return subprocess.check_call(cmd, cwd=cwd, shell=shell)
41
42
43def get_esp_serial_port(module_path):
44    try:
45        import serial.tools.list_ports
46        esptool_path = os.path.join(module_path, 'tools', 'esptool_py')
47        sys.path.insert(0, esptool_path)
48        import esptool
49        ports = list(sorted(p.device for p in serial.tools.list_ports.comports()))
50        # high baud rate could cause the failure of creation of the connection
51        esp = esptool.get_default_connected_device(serial_list=ports, port=None, connect_attempts=4,
52                                                   initial_baud=115200)
53        if esp is None:
54            log.die("No serial ports found. Connect a device, or use '-p PORT' "
55                    "option to set a specific port.")
56
57        serial_port = esp.serial_port
58        esp._port.close()
59
60        return serial_port
61    except Exception as e:
62        log.die("Error detecting ESP serial port: {}".format(e))
63        return None
64
65
66def get_build_dir(path, die_if_none=True):
67    # Get the build directory for the given argument list and environment.
68
69    guess = config.get('build', 'guess-dir', fallback='never')
70    guess = guess == 'runners'
71    dir = find_build_dir(path, guess)
72
73    if dir and is_zephyr_build(dir):
74        return dir
75    elif die_if_none:
76        msg = 'could not find build directory and '
77        if dir:
78            msg = msg + 'neither {} nor {} are zephyr build directories.'
79        else:
80            msg = msg + ('{} is not a build directory and the default build '
81                         'directory cannot be determined. ')
82        log.die(msg.format(os.getcwd(), dir))
83    else:
84        return None
85
86
87def get_build_elf_path():
88    def runners_yaml_path(build_dir):
89        ret = Path(build_dir) / 'zephyr' / 'runners.yaml'
90        if not ret.is_file():
91            log.die("could not find build configuration")
92        return ret
93
94    def load_runners_yaml(path):
95        # Load runners.yaml and convert to Python object.
96
97        try:
98            with open(path, 'r') as f:
99                content = yaml.safe_load(f.read())
100        except Exception as e:
101            log.die("runners.yaml file open error: {}".format(e))
102
103        if not content.get('runners'):
104            log.wrn("no pre-configured runners in {}; "
105                    "this probably won't work".format(path))
106
107        return content
108
109    build_dir = get_build_dir(None)
110    domain = load_domains(build_dir).get_default_domain()
111
112    # build dir differs when sysbuild is used
113    if domain.name != 'app':
114        build_dir = Path(build_dir) / domain.name
115        build_dir = get_build_dir(build_dir)
116
117    yaml_path = runners_yaml_path(build_dir)
118    runners_yaml = load_runners_yaml(yaml_path)
119
120    return Path(build_dir) / 'zephyr' / runners_yaml['config']['elf_file']
121
122
123class Tools(WestCommand):
124
125    def __init__(self):
126        super().__init__(
127            'espressif',
128            # Keep this in sync with the string in west-commands.yml.
129            'Espressif tools for west framework.',
130            dedent('''
131            This interface allows having esp-idf monitor support.'''),
132            accepts_unknown_args=False)
133
134    def do_add_parser(self, parser_adder):
135        parser = parser_adder.add_parser(self.name,
136                                         help=self.help,
137                                         description=self.description)
138
139        parser.add_argument('command', choices=['monitor'],
140                            help='open serial port based on esp-idf monitor')
141
142        # monitor arguments
143        group = parser.add_argument_group('monitor optional arguments')
144        group.add_argument('-b', '--baud', default="115200", help='Serial port baud rate')
145        group.add_argument('-p', '--port', help='Serial port address')
146        group.add_argument('-e', '--elf', help='ELF file')
147        group.add_argument('-n', '--eol', default='CRLF', help='EOL to use')
148
149        return parser
150
151    def do_run(self, args, unknown_args):
152
153        module_path = (
154            Path(os.getenv("ZEPHYR_BASE")).absolute()
155            / r".."
156            / "modules"
157            / "hal"
158            / "espressif"
159        )
160
161        if not module_path.exists():
162            log.die('cannot find espressif hal in $ZEPHYR_BASE/../modules/hal/ path')
163
164        if args.command == "monitor":
165            self.monitor(module_path, args)
166
167    def monitor(self, module_path, args):
168
169        elf_path = args.elf
170        if elf_path:
171            elf_path = os.path.abspath(elf_path)
172        else:
173            # get build elf file path
174            elf_path = get_build_elf_path()
175
176        esp_port = args.port
177        if not esp_port:
178            # detect usb port using esptool
179            esp_port = get_esp_serial_port(module_path)
180
181        monitor_path = Path(module_path, "tools/idf_monitor/idf_monitor.py")
182        cmd_path = Path(os.getcwd())
183        if platform.system() == 'Windows':
184            cmd_exec(("python.exe", monitor_path, "-p", esp_port,
185                     "-b", args.baud, elf_path, "--eol", args.eol), cwd=cmd_path)
186        else:
187            cmd_exec((sys.executable, monitor_path, "-p", esp_port, "-b", args.baud,
188                      elf_path, "--eol", args.eol), cwd=cmd_path)
189