Automatically reconnect device monitor if a connection fails // Resolve #3939

This commit is contained in:
Ivan Kravets
2022-06-11 20:55:52 +03:00
parent c42fe32972
commit 7f351bc7c8
7 changed files with 242 additions and 73 deletions

View File

@ -16,7 +16,13 @@ PlatformIO Core 6
6.0.3 (2022-??-??)
~~~~~~~~~~~~~~~~~~
- Fixed an issue when a custom `pio test --project-config <https://docs.platformio.org/en/latest/core/userguide/cmd_test.html#cmdoption-pio-test-c>`__ was not handled properly (`issue #4299 <https://github.com/platformio/platformio-core/issues/4299>`_)
* **Device Monitor**
- Automatically reconnect if a connection fails
- Added new `pio device monitor --no-reconnect <https://docs.platformio.org/en/latest/core/userguide/device/cmd_monitor.html#cmdoption-pio-device-monitor-reconnect-no-reconnect>`__ option to disable automatic reconnection
- Handle disconnects more gracefully (`issue #3939 <https://github.com/platformio/platformio-core/issues/3939>`_)
* Fixed an issue when a custom `pio test --project-config <https://docs.platformio.org/en/latest/core/userguide/cmd_test.html#cmdoption-pio-test-c>`__ was not handled properly (`issue #4299 <https://github.com/platformio/platformio-core/issues/4299>`_)
6.0.2 (2022-06-01)
~~~~~~~~~~~~~~~~~~

2
docs

Submodule docs updated: 300060ea08...c86b25dd81

View File

@ -13,14 +13,13 @@
# 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.device.monitor.terminal import start_terminal
from platformio.platform.factory import PlatformFactory
from platformio.project.config import ProjectConfig
from platformio.project.exception import NotPlatformIOProjectError
@ -30,10 +29,11 @@ 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",
"--baud",
type=int,
help="Set baud rate, default=%d" % ProjectOptions["env.monitor_speed"].default,
default=ProjectOptions["env.monitor_speed"].default,
help="Set baud/speed, default=%d" % ProjectOptions["env.monitor_speed"].default,
)
@click.option(
"--parity",
@ -58,7 +58,9 @@ from platformio.project.options import ProjectOptions
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(
"-f", "--filter", "filters", multiple=True, help="Add filters/text transformations"
)
@click.option(
"--eol",
default="CRLF",
@ -78,13 +80,21 @@ from platformio.project.options import ProjectOptions
type=int,
default=20,
help="ASCII code of special character that is used to "
"control miniterm (menu), default=20 (DEC)",
"control terminal (menu), default=20 (DEC)",
)
@click.option(
"--quiet",
is_flag=True,
help="Diagnostics: suppress non-error messages, default=Off",
)
@click.option(
"--reconnect/--no-reconnect",
default=True,
help=(
"If established connection fails, "
"silently retry on the same port, default=True"
),
)
@click.option(
"-d",
"--project-dir",
@ -96,49 +106,32 @@ from platformio.project.options import ProjectOptions
"--environment",
help="Load configuration from `platformio.ini` and specified environment",
)
def device_monitor_cmd(**kwargs): # pylint: disable=too-many-branches
project_options = {}
def device_monitor_cmd(**options):
platform = None
with fs.cd(kwargs["project_dir"]):
project_options = {}
with fs.cd(options["project_dir"]):
try:
project_options = get_project_options(kwargs["environment"])
kwargs = apply_project_monitor_options(kwargs, project_options)
project_options = get_project_options(options["environment"])
options = apply_project_monitor_options(options, 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"],
register_filters(platform=platform, options=options)
options["port"] = find_serial_port(
initial_port=options["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_protocol"),
)
# 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 options["menu_char"] == options["exit_char"]:
raise exception.UserSideException(
"--exit-char can not be the same as --menu-char"
)
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)
start_terminal(options)
def get_project_options(environment=None):
@ -148,37 +141,13 @@ def get_project_options(environment=None):
return config.items(env=environment, as_dict=True)
def apply_project_monitor_options(cli_options, project_options):
def apply_project_monitor_options(initial_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 initial_options[k] is None and k2 in project_options:
initial_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
initial_options[k] = int(initial_options[k])
return initial_options

View File

@ -0,0 +1,174 @@
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
#
# 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 signal
import threading
import click
import serial
from serial.tools import miniterm
from platformio.exception import UserSideException
class Terminal(miniterm.Miniterm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pio_unexpected_exception = None
def reader(self):
try:
super().reader()
except Exception as exc: # pylint: disable=broad-except
self.pio_unexpected_exception = exc
def writer(self):
try:
super().writer()
except Exception as exc: # pylint: disable=broad-except
self.pio_unexpected_exception = exc
def start_terminal(options):
retries = 0
is_port_valid = False
while True:
term = None
try:
term = new_terminal(options)
is_port_valid = True
options["port"] = term.serial.name
if retries:
click.echo("\t Connected!", err=True)
elif not options["quiet"]:
print_terminal_settings(term)
retries = 0 # reset
term.start()
try:
term.join(True)
except KeyboardInterrupt:
pass
term.join()
term.console.cleanup()
term.close()
if term.pio_unexpected_exception:
click.secho(
"Disconnected (%s)" % term.pio_unexpected_exception,
fg="red",
err=True,
)
if options["reconnect"]:
raise UserSideException(term.pio_unexpected_exception)
return
except UserSideException as exc:
if not is_port_valid:
raise exc
if not retries:
click.echo("Reconnecting to %s " % options["port"], err=True, nl=False)
signal.signal(signal.SIGINT, signal.SIG_DFL)
else:
click.echo(".", err=True, nl=False)
retries += 1
threading.Event().wait(retries / 2)
def new_terminal(options):
term = Terminal(
new_serial_instance(options),
echo=options["echo"],
eol=options["eol"].lower(),
filters=options["filters"] or ["default"],
)
term.exit_character = chr(options["exit_char"])
term.menu_character = chr(options["menu_char"])
term.raw = options["raw"]
term.set_rx_encoding(options["encoding"])
term.set_tx_encoding(options["encoding"])
return term
def print_terminal_settings(terminal):
click.echo(
"--- Terminal on {p.name} | "
"{p.baudrate} {p.bytesize}-{p.parity}-{p.stopbits}".format(p=terminal.serial)
)
click.echo(
"--- Available filters and text transformations: %s"
% ", ".join(sorted(miniterm.TRANSFORMATIONS.keys()))
)
click.echo("--- More details at https://bit.ly/pio-monitor-filters")
click.echo(
"--- Quit: {} | Menu: {} | Help: {} followed by {}".format(
miniterm.key_description(terminal.exit_character),
miniterm.key_description(terminal.menu_character),
miniterm.key_description(terminal.menu_character),
miniterm.key_description("\x08"),
)
)
def new_serial_instance(options): # pylint: disable=too-many-branches
serial_instance = None
port = options["port"]
while serial_instance is None:
# no port given on command line -> ask user now
if port is None or port == "-":
try:
port = miniterm.ask_for_port()
except KeyboardInterrupt:
click.echo("", err=True)
raise UserSideException("User aborted and port is not given")
else:
if not port:
raise UserSideException("Port is not given")
try:
serial_instance = serial.serial_for_url(
port,
options["baud"],
parity=options["parity"],
rtscts=options["rtscts"],
xonxoff=options["xonxoff"],
do_not_open=True,
)
if not hasattr(serial_instance, "cancel_read"):
# enable timeout for alive flag polling if cancel_read is not available
serial_instance.timeout = 1
if options["dtr"] is not None:
if not options["quiet"]:
click.echo(
"--- forcing DTR {}".format(
"active" if options["dtr"] else "inactive"
)
)
serial_instance.dtr = options["dtr"]
if options["rts"] is not None:
if not options["quiet"]:
click.echo(
"--- forcing RTS {}".format(
"active" if options["rts"] else "inactive"
)
)
serial_instance.rts = options["rts"]
if isinstance(serial_instance, serial.Serial):
serial_instance.exclusive = True
serial_instance.open()
except serial.SerialException as exc:
raise UserSideException(exc)
return serial_instance

View File

@ -30,10 +30,6 @@ class ReturnErrorCode(PlatformioException):
MESSAGE = "{0}"
class MinitermException(PlatformioException):
pass
class UserSideException(PlatformioException):
pass

View File

@ -28,7 +28,6 @@ from platformio.device.monitor.command import (
apply_project_monitor_options,
device_monitor_cmd,
get_project_options,
project_options_to_monitor_argv,
)
from platformio.package.manager.core import inject_contrib_pysite
from platformio.project.exception import NotPlatformIOProjectError
@ -360,6 +359,7 @@ def device_monitor(ctx, agents, **kwargs):
pass
kwargs["baud"] = kwargs["baud"] or ProjectOptions["env.monitor_speed"].default
kwargs["reconnect"] = False
def _tx_target(sock_dir):
subcmd_argv = ["remote"]
@ -387,3 +387,27 @@ def device_monitor(ctx, agents, **kwargs):
fs.rmtree(sock_dir)
return True
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

View File

@ -32,7 +32,7 @@ minimal_requirements = [
"colorama",
"marshmallow==%s" % ("3.*" if sys.version_info >= (3, 7) else "3.14.1"),
"pyelftools>=0.27,<1",
"pyserial==3.*",
"pyserial==3.5.*", # keep in sync "device/monitor/terminal.py"
"requests==2.*",
"requests==%s" % ("2.*" if sys.version_info >= (3, 7) else "2.27.1"),
"semantic_version==2.10.*",