xref: /aosp_15_r20/external/pigweed/pw_arduino_build/py/pw_arduino_build/teensy_detector.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1#!/usr/bin/env python3
2# Copyright 2020 The Pigweed Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5# use this file except in compliance with the License. You may obtain a copy of
6# the License at
7#
8#     https://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations under
14# the License.
15"""Detects attached Teensy boards connected via usb."""
16
17import logging
18import re
19import subprocess
20import typing
21
22from pathlib import Path
23
24import pw_arduino_build.log
25
26_LOG = logging.getLogger('teensy_detector')
27
28
29class UnknownArduinoCore(Exception):
30    """Exception raised when a given core can not be found."""
31
32
33def log_subprocess_output(level, output):
34    """Logs subprocess output line-by-line."""
35
36    lines = output.decode('utf-8', errors='replace').splitlines()
37    for line in lines:
38        _LOG.log(level, line)
39
40
41class BoardInfo(typing.NamedTuple):
42    """Information about a connected dev board."""
43
44    dev_name: str
45    usb_device_path: str
46    protocol: str
47    label: str
48    arduino_upload_tool_name: str
49
50    def test_runner_args(self) -> list[str]:
51        return [
52            "--set-variable",
53            f"serial.port.protocol={self.protocol}",
54            "--set-variable",
55            f"serial.port={self.usb_device_path}",
56            "--set-variable",
57            f"serial.port.label={self.dev_name}",
58        ]
59
60
61def detect_boards(arduino_package_path=False) -> list:
62    """Detect attached boards, returning a list of Board objects."""
63
64    teensy_core = Path()
65    if arduino_package_path:
66        teensy_core = Path(arduino_package_path)
67    else:
68        teensy_core = Path("third_party/arduino/cores/teensy")
69        if not teensy_core.exists():
70            teensy_core = Path(
71                "third_party/pigweed/third_party/arduino/cores/teensy"
72            )
73
74    if not teensy_core.exists():
75        raise UnknownArduinoCore
76
77    teensy_device_line_regex = re.compile(
78        r"^(?P<address>[^ ]+) (?P<dev_name>[^ ]+) "
79        r"\((?P<label>[^)]+)\) ?(?P<rest>.*)$"
80    )
81
82    boards = []
83    detect_command = [
84        (teensy_core / "tools" / "teensy-tools" / "1.58.0" / "teensy_ports")
85        .absolute()
86        .as_posix(),
87        "-L",
88    ]
89
90    # TODO(tonymd): teensy_ports -L on windows does not return the right port
91    # string Example:
92    #
93    #   $ teensy_ports -L
94    #   Port_#0001.Hub_#0003 COM3 (Teensy 3.6) Serial
95    #
96    # So we get "-port=Port_#0001.Hub_#0003"
97    # But it should be "-port=usb:0/140000/0/1"
98
99    process = subprocess.run(
100        detect_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
101    )
102    if process.returncode != 0:
103        _LOG.error("Command failed with exit code %d.", process.returncode)
104        _LOG.error("Full command:")
105        _LOG.error("")
106        _LOG.error("  %s", " ".join(detect_command))
107        _LOG.error("")
108        _LOG.error("Process output:")
109        log_subprocess_output(logging.ERROR, process.stdout)
110        _LOG.error('')
111    for line in process.stdout.decode("utf-8", errors="replace").splitlines():
112        device_match_result = teensy_device_line_regex.match(line)
113        if device_match_result:
114            teensy_device = device_match_result.groupdict()
115            boards.append(
116                BoardInfo(
117                    dev_name=teensy_device["dev_name"],
118                    usb_device_path=teensy_device["address"],
119                    protocol="Teensy",
120                    label=teensy_device["label"],
121                    arduino_upload_tool_name="teensyloader",
122                )
123            )
124    return boards
125
126
127def main():
128    """This detects and then displays all attached discovery boards."""
129
130    pw_arduino_build.log.install(logging.INFO)
131
132    boards = detect_boards()
133    if not boards:
134        _LOG.info("No attached boards detected")
135    for idx, board in enumerate(boards):
136        _LOG.info("Board %d:", idx)
137        _LOG.info("  - Name: %s", board.label)
138        _LOG.info("  - Port: %s", board.dev_name)
139        _LOG.info("  - Address: %s", board.usb_device_path)
140        _LOG.info(
141            "  - Test runner args: %s", " ".join(board.test_runner_args())
142        )
143
144
145if __name__ == "__main__":
146    main()
147