From a6e61a7a5a24cf73d2866b9817e8b9a7e99852d0 Mon Sep 17 00:00:00 2001 From: Ivan Kravets Date: Fri, 10 Jun 2022 14:01:55 +0300 Subject: [PATCH] Restructure "device" module --- platformio/device/cli.py | 4 +- platformio/device/finder.py | 2 +- platformio/device/list/__init__.py | 13 ++ platformio/device/list/command.py | 99 ++++++++++ platformio/device/list/util.py | 154 +++++++++++++++ platformio/device/monitor/__init__.py | 13 ++ platformio/device/monitor/command.py | 184 ++++++++++++++++++ platformio/device/monitor/filters/__init__.py | 13 ++ platformio/device/monitor/filters/base.py | 100 ++++++++++ platformio/device/monitor/filters/hexlify.py | 38 ++++ platformio/device/monitor/filters/log2file.py | 45 +++++ .../device/monitor/filters/send_on_enter.py | 38 ++++ platformio/device/monitor/filters/time.py | 37 ++++ 13 files changed, 737 insertions(+), 3 deletions(-) create mode 100644 platformio/device/list/__init__.py create mode 100644 platformio/device/list/command.py create mode 100644 platformio/device/list/util.py create mode 100644 platformio/device/monitor/__init__.py create mode 100644 platformio/device/monitor/command.py create mode 100644 platformio/device/monitor/filters/__init__.py create mode 100644 platformio/device/monitor/filters/base.py create mode 100644 platformio/device/monitor/filters/hexlify.py create mode 100644 platformio/device/monitor/filters/log2file.py create mode 100644 platformio/device/monitor/filters/send_on_enter.py create mode 100644 platformio/device/monitor/filters/time.py diff --git a/platformio/device/cli.py b/platformio/device/cli.py index 5865c43e..13dec793 100644 --- a/platformio/device/cli.py +++ b/platformio/device/cli.py @@ -14,8 +14,8 @@ import click -from platformio.device.commands.list import device_list_cmd -from platformio.device.commands.monitor import device_monitor_cmd +from platformio.device.list.command import device_list_cmd +from platformio.device.monitor.command import device_monitor_cmd @click.group( diff --git a/platformio/device/finder.py b/platformio/device/finder.py index 0fe98baa..382fe000 100644 --- a/platformio/device/finder.py +++ b/platformio/device/finder.py @@ -18,7 +18,7 @@ from fnmatch import fnmatch import serial from platformio.compat import IS_WINDOWS -from platformio.device.list import list_logical_devices, list_serial_ports +from platformio.device.list.util import list_logical_devices, list_serial_ports def is_pattern_port(port): diff --git a/platformio/device/list/__init__.py b/platformio/device/list/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/platformio/device/list/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/platformio/device/list/command.py b/platformio/device/list/command.py new file mode 100644 index 00000000..078371cf --- /dev/null +++ b/platformio/device/list/command.py @@ -0,0 +1,99 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import click + +from platformio.device.list.util import ( + list_logical_devices, + list_mdns_services, + list_serial_ports, +) + + +@click.command("list", short_help="List devices") +@click.option("--serial", is_flag=True, help="List serial ports, default") +@click.option("--logical", is_flag=True, help="List logical devices") +@click.option("--mdns", is_flag=True, help="List multicast DNS services") +@click.option("--json-output", is_flag=True) +def device_list_cmd( # pylint: disable=too-many-branches + serial, logical, mdns, json_output +): + if not logical and not mdns: + serial = True + data = {} + if serial: + data["serial"] = list_serial_ports() + if logical: + data["logical"] = list_logical_devices() + if mdns: + data["mdns"] = list_mdns_services() + + single_key = list(data)[0] if len(list(data)) == 1 else None + + if json_output: + return click.echo(json.dumps(data[single_key] if single_key else data)) + + titles = { + "serial": "Serial Ports", + "logical": "Logical Devices", + "mdns": "Multicast DNS Services", + } + + for key, value in data.items(): + if not single_key: + click.secho(titles[key], bold=True) + click.echo("=" * len(titles[key])) + + if key == "serial": + for item in value: + click.secho(item["port"], fg="cyan") + click.echo("-" * len(item["port"])) + click.echo("Hardware ID: %s" % item["hwid"]) + click.echo("Description: %s" % item["description"]) + click.echo("") + + if key == "logical": + for item in value: + click.secho(item["path"], fg="cyan") + click.echo("-" * len(item["path"])) + click.echo("Name: %s" % item["name"]) + click.echo("") + + if key == "mdns": + for item in value: + click.secho(item["name"], fg="cyan") + click.echo("-" * len(item["name"])) + click.echo("Type: %s" % item["type"]) + click.echo("IP: %s" % item["ip"]) + click.echo("Port: %s" % item["port"]) + if item["properties"]: + click.echo( + "Properties: %s" + % ( + "; ".join( + [ + "%s=%s" % (k, v) + for k, v in item["properties"].items() + ] + ) + ) + ) + click.echo("") + + if single_key: + click.echo("") + + return True diff --git a/platformio/device/list/util.py b/platformio/device/list/util.py new file mode 100644 index 00000000..3695f760 --- /dev/null +++ b/platformio/device/list/util.py @@ -0,0 +1,154 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +import re +import time +from glob import glob + +import zeroconf + +from platformio import __version__, exception, proc +from platformio.compat import IS_MACOS, IS_WINDOWS + + +def list_serial_ports(filter_hwid=False): + try: + # pylint: disable=import-outside-toplevel + from serial.tools.list_ports import comports + except ImportError: + raise exception.GetSerialPortsError(os.name) + + result = [] + for p, d, h in comports(): + if not p: + continue + if not filter_hwid or "VID:PID" in h: + result.append({"port": p, "description": d, "hwid": h}) + + if filter_hwid: + return result + + # fix for PySerial + if not result and IS_MACOS: + for p in glob("/dev/tty.*"): + result.append({"port": p, "description": "n/a", "hwid": "n/a"}) + return result + + +def list_logical_devices(): + items = [] + if IS_WINDOWS: + try: + result = proc.exec_command( + ["wmic", "logicaldisk", "get", "name,VolumeName"] + ).get("out", "") + devicenamere = re.compile(r"^([A-Z]{1}\:)\s*(\S+)?") + for line in result.split("\n"): + match = devicenamere.match(line.strip()) + if not match: + continue + items.append({"path": match.group(1) + "\\", "name": match.group(2)}) + return items + except WindowsError: # pylint: disable=undefined-variable + pass + # try "fsutil" + result = proc.exec_command(["fsutil", "fsinfo", "drives"]).get("out", "") + for device in re.findall(r"[A-Z]:\\", result): + items.append({"path": device, "name": None}) + return items + + result = proc.exec_command(["df"]).get("out") + devicenamere = re.compile(r"^/.+\d+\%\s+([a-z\d\-_/]+)$", flags=re.I) + for line in result.split("\n"): + match = devicenamere.match(line.strip()) + if not match: + continue + items.append({"path": match.group(1), "name": os.path.basename(match.group(1))}) + return items + + +def list_mdns_services(): + class mDNSListener(object): + def __init__(self): + self._zc = zeroconf.Zeroconf(interfaces=zeroconf.InterfaceChoice.All) + self._found_types = [] + self._found_services = [] + + def __enter__(self): + zeroconf.ServiceBrowser( + self._zc, + [ + "_http._tcp.local.", + "_hap._tcp.local.", + "_services._dns-sd._udp.local.", + ], + self, + ) + return self + + def __exit__(self, etype, value, traceback): + self._zc.close() + + def add_service(self, zc, type_, name): + try: + assert zeroconf.service_type_name(name) + assert str(name) + except (AssertionError, UnicodeError, zeroconf.BadTypeInNameException): + return + if name not in self._found_types: + self._found_types.append(name) + zeroconf.ServiceBrowser(self._zc, name, self) + if type_ in self._found_types: + s = zc.get_service_info(type_, name) + if s: + self._found_services.append(s) + + def remove_service(self, zc, type_, name): + pass + + def update_service(self, zc, type_, name): + pass + + def get_services(self): + return self._found_services + + items = [] + with mDNSListener() as mdns: + time.sleep(3) + for service in mdns.get_services(): + properties = None + if service.properties: + try: + properties = { + k.decode("utf8"): v.decode("utf8") + if isinstance(v, bytes) + else v + for k, v in service.properties.items() + } + json.dumps(properties) + except UnicodeDecodeError: + properties = None + + items.append( + { + "type": service.type, + "name": service.name, + "ip": ", ".join(service.parsed_addresses()), + "port": service.port, + "properties": properties, + } + ) + return items diff --git a/platformio/device/monitor/__init__.py b/platformio/device/monitor/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/platformio/device/monitor/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/platformio/device/monitor/command.py b/platformio/device/monitor/command.py new file mode 100644 index 00000000..a36b1243 --- /dev/null +++ b/platformio/device/monitor/command.py @@ -0,0 +1,184 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +import click +from serial.tools import miniterm + +from platformio import exception, fs +from platformio.device.finder import find_serial_port +from platformio.device.monitor.filters.base import register_filters +from platformio.platform.factory import PlatformFactory +from platformio.project.config import ProjectConfig +from platformio.project.exception import NotPlatformIOProjectError +from platformio.project.options import ProjectOptions + + +@click.command("monitor", short_help="Monitor device (Serial/Socket)") +@click.option("--port", "-p", help="Port, a number or a device name") +@click.option( + "--baud", + "-b", + type=int, + help="Set baud rate, default=%d" % ProjectOptions["env.monitor_speed"].default, +) +@click.option( + "--parity", + default="N", + type=click.Choice(["N", "E", "O", "S", "M"]), + help="Set parity, default=N", +) +@click.option("--rtscts", is_flag=True, help="Enable RTS/CTS flow control, default=Off") +@click.option( + "--xonxoff", is_flag=True, help="Enable software flow control, default=Off" +) +@click.option( + "--rts", default=None, type=click.IntRange(0, 1), help="Set initial RTS line state" +) +@click.option( + "--dtr", default=None, type=click.IntRange(0, 1), help="Set initial DTR line state" +) +@click.option("--echo", is_flag=True, help="Enable local echo, default=Off") +@click.option( + "--encoding", + default="UTF-8", + help="Set the encoding for the serial port (e.g. hexlify, " + "Latin1, UTF-8), default: UTF-8", +) +@click.option("--filter", "-f", multiple=True, help="Add filters/text transformations") +@click.option( + "--eol", + default="CRLF", + type=click.Choice(["CR", "LF", "CRLF"]), + help="End of line mode, default=CRLF", +) +@click.option("--raw", is_flag=True, help="Do not apply any encodings/transformations") +@click.option( + "--exit-char", + type=int, + default=3, + help="ASCII code of special character that is used to exit " + "the application, default=3 (Ctrl+C)", +) +@click.option( + "--menu-char", + type=int, + default=20, + help="ASCII code of special character that is used to " + "control miniterm (menu), default=20 (DEC)", +) +@click.option( + "--quiet", + is_flag=True, + help="Diagnostics: suppress non-error messages, default=Off", +) +@click.option( + "-d", + "--project-dir", + default=os.getcwd, + type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), +) +@click.option( + "-e", + "--environment", + help="Load configuration from `platformio.ini` and specified environment", +) +def device_monitor_cmd(**kwargs): # pylint: disable=too-many-branches + project_options = {} + platform = None + with fs.cd(kwargs["project_dir"]): + try: + project_options = get_project_options(kwargs["environment"]) + kwargs = apply_project_monitor_options(kwargs, project_options) + if "platform" in project_options: + platform = PlatformFactory.new(project_options["platform"]) + except NotPlatformIOProjectError: + pass + register_filters(platform=platform, options=kwargs) + kwargs["port"] = find_serial_port( + initial_port=kwargs["port"], + board_config=platform.board_config(project_options.get("board")) + if platform and project_options.get("board") + else None, + upload_protocol=project_options.get("upload_port"), + ) + + # override system argv with patched options + sys.argv = ["monitor"] + project_options_to_monitor_argv( + kwargs, + project_options, + ignore=("port", "baud", "rts", "dtr", "environment", "project_dir"), + ) + + if not kwargs["quiet"]: + click.echo( + "--- Available filters and text transformations: %s" + % ", ".join(sorted(miniterm.TRANSFORMATIONS.keys())) + ) + click.echo("--- More details at https://bit.ly/pio-monitor-filters") + try: + miniterm.main( + default_port=kwargs["port"], + default_baudrate=kwargs["baud"] + or ProjectOptions["env.monitor_speed"].default, + default_rts=kwargs["rts"], + default_dtr=kwargs["dtr"], + ) + except Exception as e: + raise exception.MinitermException(e) + + +def get_project_options(environment=None): + config = ProjectConfig.get_instance() + config.validate(envs=[environment] if environment else None) + environment = environment or config.get_default_env() + return config.items(env=environment, as_dict=True) + + +def apply_project_monitor_options(cli_options, project_options): + for k in ("port", "speed", "rts", "dtr"): + k2 = "monitor_%s" % k + if k == "speed": + k = "baud" + if cli_options[k] is None and k2 in project_options: + cli_options[k] = project_options[k2] + if k != "port": + cli_options[k] = int(cli_options[k]) + return cli_options + + +def project_options_to_monitor_argv(cli_options, project_options, ignore=None): + confmon_flags = project_options.get("monitor_flags", []) + result = confmon_flags[::] + + for f in project_options.get("monitor_filters", []): + result.extend(["--filter", f]) + + for k, v in cli_options.items(): + if v is None or (ignore and k in ignore): + continue + k = "--" + k.replace("_", "-") + if k in confmon_flags: + continue + if isinstance(v, bool): + if v: + result.append(k) + elif isinstance(v, tuple): + for i in v: + result.extend([k, i]) + else: + result.extend([k, str(v)]) + return result diff --git a/platformio/device/monitor/filters/__init__.py b/platformio/device/monitor/filters/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/platformio/device/monitor/filters/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/platformio/device/monitor/filters/base.py b/platformio/device/monitor/filters/base.py new file mode 100644 index 00000000..e773ed65 --- /dev/null +++ b/platformio/device/monitor/filters/base.py @@ -0,0 +1,100 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import os + +from serial.tools import miniterm + +from platformio.compat import get_object_members, load_python_module +from platformio.package.manager.tool import ToolPackageManager +from platformio.project.config import ProjectConfig + + +class DeviceMonitorFilterBase(miniterm.Transform): + def __init__(self, options=None): + """Called by PlatformIO to pass context""" + miniterm.Transform.__init__(self) + + self.options = options or {} + self.project_dir = self.options.get("project_dir") + self.environment = self.options.get("environment") + + self.config = ProjectConfig.get_instance() + if not self.environment: + default_envs = self.config.default_envs() + if default_envs: + self.environment = default_envs[0] + elif self.config.envs(): + self.environment = self.config.envs()[0] + + def __call__(self): + """Called by the miniterm library when the filter is actually used""" + return self + + @property + def NAME(self): + raise NotImplementedError("Please declare NAME attribute for the filter class") + + +def register_filters(platform=None, options=None): + # project filters + load_monitor_filters( + ProjectConfig.get_instance().get("platformio", "monitor_dir"), + prefix="filter_", + options=options, + ) + # platform filters + if platform: + load_monitor_filters( + os.path.join(platform.get_dir(), "monitor"), + prefix="filter_", + options=options, + ) + # load package filters + pm = ToolPackageManager() + for pkg in pm.get_installed(): + load_monitor_filters( + os.path.join(pkg.path, "monitor"), prefix="filter_", options=options + ) + # default filters + load_monitor_filters(os.path.dirname(__file__), options=options) + + +def load_monitor_filters(monitor_dir, prefix=None, options=None): + if not os.path.isdir(monitor_dir): + return + for name in os.listdir(monitor_dir): + if (prefix and not name.startswith(prefix)) or not name.endswith(".py"): + continue + path = os.path.join(monitor_dir, name) + if not os.path.isfile(path): + continue + load_monitor_filter(path, options) + + +def load_monitor_filter(path, options=None): + name = os.path.basename(path) + name = name[: name.find(".")] + module = load_python_module("platformio.device.monitor.filters.%s" % name, path) + for cls in get_object_members(module).values(): + if ( + not inspect.isclass(cls) + or not issubclass(cls, DeviceMonitorFilterBase) + or cls == DeviceMonitorFilterBase + ): + continue + obj = cls(options) + miniterm.TRANSFORMATIONS[obj.NAME] = obj + return True diff --git a/platformio/device/monitor/filters/hexlify.py b/platformio/device/monitor/filters/hexlify.py new file mode 100644 index 00000000..28e83bfb --- /dev/null +++ b/platformio/device/monitor/filters/hexlify.py @@ -0,0 +1,38 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import serial + +from platformio.device.monitor.filters.base import DeviceMonitorFilterBase + + +class Hexlify(DeviceMonitorFilterBase): + NAME = "hexlify" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._counter = 0 + + def rx(self, text): + result = "" + for b in serial.iterbytes(text): + if (self._counter % 16) == 0: + result += "\n{:04X} | ".format(self._counter) + asciicode = ord(b) + if asciicode <= 255: + result += "{:02X} ".format(asciicode) + else: + result += "?? " + self._counter += 1 + return result diff --git a/platformio/device/monitor/filters/log2file.py b/platformio/device/monitor/filters/log2file.py new file mode 100644 index 00000000..bf97b551 --- /dev/null +++ b/platformio/device/monitor/filters/log2file.py @@ -0,0 +1,45 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os.path +from datetime import datetime + +from platformio.device.monitor.filters.base import DeviceMonitorFilterBase + + +class LogToFile(DeviceMonitorFilterBase): + NAME = "log2file" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._log_fp = None + + def __call__(self): + log_file_name = "platformio-device-monitor-%s.log" % datetime.now().strftime( + "%y%m%d-%H%M%S" + ) + print("--- Logging an output to %s" % os.path.abspath(log_file_name)) + # pylint: disable=consider-using-with + self._log_fp = io.open(log_file_name, "w", encoding="utf-8") + return self + + def __del__(self): + if self._log_fp: + self._log_fp.close() + + def rx(self, text): + self._log_fp.write(text) + self._log_fp.flush() + return text diff --git a/platformio/device/monitor/filters/send_on_enter.py b/platformio/device/monitor/filters/send_on_enter.py new file mode 100644 index 00000000..c28888cd --- /dev/null +++ b/platformio/device/monitor/filters/send_on_enter.py @@ -0,0 +1,38 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from platformio.device.monitor.filters.base import DeviceMonitorFilterBase + + +class SendOnEnter(DeviceMonitorFilterBase): + NAME = "send_on_enter" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._buffer = "" + + if self.options.get("eol") == "CR": + self._eol = "\r" + elif self.options.get("eol") == "LF": + self._eol = "\n" + else: + self._eol = "\r\n" + + def tx(self, text): + self._buffer += text + if self._buffer.endswith(self._eol): + text = self._buffer + self._buffer = "" + return text + return "" diff --git a/platformio/device/monitor/filters/time.py b/platformio/device/monitor/filters/time.py new file mode 100644 index 00000000..cde4e772 --- /dev/null +++ b/platformio/device/monitor/filters/time.py @@ -0,0 +1,37 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime + +from platformio.device.monitor.filters.base import DeviceMonitorFilterBase + + +class Timestamp(DeviceMonitorFilterBase): + NAME = "time" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._line_started = False + + def rx(self, text): + if self._line_started and "\n" not in text: + return text + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + if not self._line_started: + self._line_started = True + text = "%s > %s" % (timestamp, text) + if text.endswith("\n"): + self._line_started = False + return text[:-1].replace("\n", "\n%s > " % timestamp) + "\n" + return text.replace("\n", "\n%s > " % timestamp)