Skip to content

Commit

Permalink
Automatically reconnect device monitor if a connection fails // Resolve
Browse files Browse the repository at this point in the history
  • Loading branch information
ivankravets committed Jun 11, 2022
1 parent c42fe32 commit 7f351bc
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 73 deletions.
8 changes: 7 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
~~~~~~~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion docs
99 changes: 34 additions & 65 deletions platformio/device/monitor/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,10 +29,11 @@
@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",
Expand All @@ -58,7 +58,9 @@
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",
Expand All @@ -78,13 +80,21 @@
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",
Expand All @@ -96,49 +106,32 @@
"--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 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"],
if options["menu_char"] == options["exit_char"]:
raise exception.UserSideException(
"--exit-char can not be the same as --menu-char"
)
except Exception as e:
raise exception.MinitermException(e)

start_terminal(options)


def get_project_options(environment=None):
Expand All @@ -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 changes: 174 additions & 0 deletions platformio/device/monitor/terminal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Copyright (c) 2014-present PlatformIO <[email protected]>
#
# 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
4 changes: 0 additions & 4 deletions platformio/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ class ReturnErrorCode(PlatformioException):
MESSAGE = "{0}"


class MinitermException(PlatformioException):
pass


class UserSideException(PlatformioException):
pass

Expand Down
Loading

0 comments on commit 7f351bc

Please sign in to comment.