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
34build_elf_path = None
35baud_rate = None
36
37def cmd_check(cmd, cwd=None, stderr=subprocess.STDOUT):
38    return subprocess.check_output(cmd, cwd=cwd, stderr=stderr)
39
40
41def cmd_exec(cmd, cwd=None, shell=False):
42    return subprocess.check_call(cmd, cwd=cwd, shell=shell)
43
44
45def get_esp_serial_port(module_path):
46    try:
47        import serial.tools.list_ports
48        esptool_path = os.path.join(module_path, 'tools', 'esptool_py')
49        sys.path.insert(0, esptool_path)
50        import esptool
51        ports = list(sorted(p.device for p in serial.tools.list_ports.comports()))
52        # high baud rate could cause the failure of creation of the connection
53        esp = esptool.get_default_connected_device(serial_list=ports, port=None, connect_attempts=4,
54                                                   initial_baud=115200)
55        if esp is None:
56            log.die("No serial ports found. Connect a device, or use '-p PORT' "
57                    "option to set a specific port.")
58
59        serial_port = esp.serial_port
60        esp._port.close()
61
62        return serial_port
63    except Exception as e:
64        log.die("Error detecting ESP serial port: {}".format(e))
65        return None
66
67
68def parse_runners_yaml():
69    def runners_yaml_path(build_dir):
70        ret = Path(build_dir) / 'zephyr' / 'runners.yaml'
71        if not ret.is_file():
72            log.die("could not find build configuration")
73        return ret
74
75    def load_runners_yaml(path):
76        # Load runners.yaml and convert to Python object.
77
78        try:
79            with open(path, 'r') as f:
80                content = yaml.safe_load(f.read())
81        except Exception as e:
82            log.die("runners.yaml file open error: {}".format(e))
83
84        if not content.get('runners'):
85            log.wrn("no pre-configured runners in {}; "
86                    "this probably won't work".format(path))
87
88        return content
89
90    global build_elf_path
91    global baud_rate
92
93    build_dir = find_build_dir(None, True)
94    domain = load_domains(build_dir).get_default_domain()
95
96    # build dir differs when sysbuild is used
97    if domain.build_dir:
98        build_dir = domain.build_dir
99
100    yaml_path = runners_yaml_path(build_dir)
101    runners_yaml = load_runners_yaml(yaml_path)
102
103    # get build elf file path
104    build_elf_path = Path(build_dir) / 'zephyr' / runners_yaml['config']['elf_file']
105
106    # parse specific arguments for the tool
107    yaml_args = runners_yaml['args']['esp32']
108
109    # Iterate through the list to find default baud rate
110    for item in yaml_args:
111        if item.startswith('--esp-monitor-baud='):
112            baud_rate = item.split('=')[1]
113            break
114
115    # if specific default baud rate is not defined in runners.yaml, default to 115200
116    if not baud_rate:
117        baud_rate = "115200"
118
119class Tools(WestCommand):
120
121    def __init__(self):
122        super().__init__(
123            'espressif',
124            # Keep this in sync with the string in west-commands.yml.
125            'Espressif tools for west framework.',
126            dedent('''
127            This interface allows having esp-idf monitor support.'''),
128            accepts_unknown_args=False)
129
130    def do_add_parser(self, parser_adder):
131
132        parse_runners_yaml()
133
134        parser = parser_adder.add_parser(self.name,
135                                         help=self.help,
136                                         description=self.description)
137
138        parser.add_argument('command', choices=['monitor'],
139                            help='open serial port based on esp-idf monitor')
140
141        # monitor arguments
142        group = parser.add_argument_group('monitor optional arguments')
143        group.add_argument('-b', '--baud', default=baud_rate, help='Serial port baud rate')
144        group.add_argument('-p', '--port', help='Serial port address')
145        group.add_argument('-e', '--elf', help='ELF file')
146        group.add_argument('-n', '--eol', default='CRLF', help='EOL to use')
147        group.add_argument('-d', '--enable-address-decoding', action='store_true',
148                           help='Enable address decoding in the monitor')
149
150        return parser
151
152    def do_run(self, args, unknown_args):
153
154        module_path = (
155            Path(os.getenv("ZEPHYR_BASE")).absolute()
156            / r".."
157            / "modules"
158            / "hal"
159            / "espressif"
160        )
161
162        if not module_path.exists():
163            log.die('cannot find espressif hal in $ZEPHYR_BASE/../modules/hal/ path')
164
165        if args.command == "monitor":
166            self.monitor(module_path, args)
167
168    def monitor(self, module_path, args):
169
170        elf_path = args.elf
171        if elf_path:
172            elf_path = os.path.abspath(elf_path)
173        else:
174            elf_path = 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
184        cmd = [sys.executable, str(monitor_path), "-p", esp_port, "-b", args.baud, str(elf_path), "--eol", args.eol]
185
186        # Adding "-d" argument for idf_monitor.py disables the address decoding
187        if not args.enable_address_decoding:
188            cmd.append("-d")
189
190        cmd_exec(cmd, cwd=cmd_path)
191