|
12 | 12 | # See the License for the specific language governing permissions and
|
13 | 13 | # limitations under the License.
|
14 | 14 |
|
| 15 | +import os |
| 16 | +import re |
| 17 | +import subprocess |
15 | 18 | import sys
|
16 | 19 | from fnmatch import fnmatch
|
17 |
| -from os import getcwd |
18 | 20 |
|
19 | 21 | import click
|
20 | 22 | from serial.tools import miniterm
|
|
23 | 25 | from platformio.compat import dump_json_to_unicode
|
24 | 26 | from platformio.managers.platform import PlatformFactory
|
25 | 27 | from platformio.project.config import ProjectConfig
|
26 |
| -from platformio.project.exception import NotPlatformIOProjectError |
| 28 | +from platformio.project.exception import PlatformioException, NotPlatformIOProjectError |
| 29 | +from platformio.project.helpers import load_project_ide_data |
27 | 30 |
|
28 | 31 |
|
29 | 32 | @click.group(short_help="Monitor device or list existing")
|
@@ -108,6 +111,93 @@ def device_list( # pylint: disable=too-many-branches
|
108 | 111 |
|
109 | 112 | return True
|
110 | 113 |
|
| 114 | +class EspExceptionDecoder(miniterm.Transform): |
| 115 | + NAME = "esp_exception_decoder" |
| 116 | + |
| 117 | + project_dir = None |
| 118 | + firmware_path = None |
| 119 | + addr2line_path = None |
| 120 | + |
| 121 | + @classmethod |
| 122 | + def setup_paths(cls, project_dir, environment): |
| 123 | + config = ProjectConfig.get_instance() |
| 124 | + if not environment: |
| 125 | + default_envs = config.default_envs() |
| 126 | + if default_envs: |
| 127 | + environment = default_envs[0] |
| 128 | + else: |
| 129 | + environment = config.envs()[0] |
| 130 | + |
| 131 | + build_dir = config.get_optional_dir("build") |
| 132 | + cls.project_dir = project_dir |
| 133 | + if not cls.project_dir.endswith(os.path.sep): |
| 134 | + cls.project_dir += os.path.sep |
| 135 | + |
| 136 | + try: |
| 137 | + data = load_project_ide_data(project_dir, environment) |
| 138 | + cls.firmware_path = os.path.join(build_dir, environment, "firmware.elf") |
| 139 | + if not os.path.isfile(cls.firmware_path): |
| 140 | + sys.stderr.write("%s: firmware at %s does not exist, rebuild the project?\n" % |
| 141 | + (cls.__name__, cls.firmware_path)) |
| 142 | + return False |
| 143 | + |
| 144 | + cc_path = data.get("cc_path", "") |
| 145 | + if "-gcc" in cc_path: |
| 146 | + path = cc_path.replace("-gcc", "-addr2line") |
| 147 | + if os.path.isfile(path): |
| 148 | + cls.addr2line_path = path |
| 149 | + return True |
| 150 | + except PlatformioException as e: |
| 151 | + sys.stderr.write("%s: disabling, exception while looking for addr2line: %s\n" % |
| 152 | + (cls.__name__, e)) |
| 153 | + return False |
| 154 | + sys.stderr.write("%s: disabling, failed to find addr2line.\n" % cls.__name__) |
| 155 | + return False |
| 156 | + |
| 157 | + def __init__(self, *args, **kwargs): |
| 158 | + super(EspExceptionDecoder, self).__init__(*args, **kwargs) |
| 159 | + |
| 160 | + self.buffer = "" |
| 161 | + self.backtrace_re = re.compile(r"^Backtrace: ((0x[0-9a-f]+:0x[0-9a-f]+ ?)+)\s*$") |
| 162 | + |
| 163 | + def rx(self, text): |
| 164 | + if self.addr2line_path is None: |
| 165 | + return text |
| 166 | + |
| 167 | + last = 0 |
| 168 | + while True: |
| 169 | + idx = text.find("\n", last) |
| 170 | + if idx == -1: |
| 171 | + self.buffer += text[last:] |
| 172 | + break |
| 173 | + |
| 174 | + line = text[last:idx] |
| 175 | + if self.buffer: |
| 176 | + line = self.buffer + line |
| 177 | + self.buffer = "" |
| 178 | + last = idx+1 |
| 179 | + |
| 180 | + m = self.backtrace_re.match(line) |
| 181 | + if m is None: |
| 182 | + continue |
| 183 | + |
| 184 | + trace = self.get_backtrace(m) |
| 185 | + if len(trace) != "": |
| 186 | + text = text[:idx+1] + trace + text[idx+1:] |
| 187 | + last += len(trace) |
| 188 | + return text |
| 189 | + |
| 190 | + def get_backtrace(self, match): |
| 191 | + trace = "" |
| 192 | + try: |
| 193 | + args = [ self.addr2line_path, "-fipC", "-e", self.firmware_path ] |
| 194 | + for i, addr in enumerate(match.group(1).split()): |
| 195 | + output = subprocess.check_output(args + [ addr ]).decode("utf-8") |
| 196 | + trace += " #%d in %s\n" % (i, output.strip().replace(self.project_dir, "")) |
| 197 | + except subprocess.CalledProcessError as e: |
| 198 | + sys.stderr.write("%s: failed to call %s: %s\n" % |
| 199 | + (self.__class__.__name__, self.addr2line_path, e)) |
| 200 | + return trace |
111 | 201 |
|
112 | 202 | @cli.command("monitor", short_help="Monitor device (Serial)")
|
113 | 203 | @click.option("--port", "-p", help="Port, a number or a device name")
|
@@ -165,7 +255,7 @@ def device_list( # pylint: disable=too-many-branches
|
165 | 255 | @click.option(
|
166 | 256 | "-d",
|
167 | 257 | "--project-dir",
|
168 |
| - default=getcwd, |
| 258 | + default=os.getcwd, |
169 | 259 | type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
170 | 260 | )
|
171 | 261 | @click.option(
|
@@ -217,6 +307,10 @@ def device_monitor(**kwargs): # pylint: disable=too-many-branches
|
217 | 307 | ignore=("port", "baud", "rts", "dtr", "environment", "project_dir"),
|
218 | 308 | )
|
219 | 309 |
|
| 310 | + if any((EspExceptionDecoder.NAME in a for a in sys.argv)): |
| 311 | + EspExceptionDecoder.setup_paths(kwargs["project_dir"], kwargs["environment"]) |
| 312 | + miniterm.TRANSFORMATIONS[EspExceptionDecoder.NAME] = EspExceptionDecoder |
| 313 | + |
220 | 314 | try:
|
221 | 315 | miniterm.main(
|
222 | 316 | default_port=kwargs["port"],
|
|
0 commit comments