mirror of
https://github.com/platformio/platformio-core.git
synced 2025-07-29 17:47:14 +02:00
Automatically reconnect device monitor if a connection fails // Resolve #3939
This commit is contained in:
@ -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
2
docs
Submodule docs updated: 300060ea08...c86b25dd81
@ -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
|
||||
|
174
platformio/device/monitor/terminal.py
Normal file
174
platformio/device/monitor/terminal.py
Normal 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
|
@ -30,10 +30,6 @@ class ReturnErrorCode(PlatformioException):
|
||||
MESSAGE = "{0}"
|
||||
|
||||
|
||||
class MinitermException(PlatformioException):
|
||||
pass
|
||||
|
||||
|
||||
class UserSideException(PlatformioException):
|
||||
pass
|
||||
|
||||
|
@ -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
|
||||
|
2
setup.py
2
setup.py
@ -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.*",
|
||||
|
Reference in New Issue
Block a user