Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions Documentation/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,61 @@ Options:

* ``--flash / --no-flash`` Flash image. Default: True.

``usb-suspend`` command
-----------------------

Check that a NuttX USB CDC/ACM link survives Linux USB runtime suspend.
Implemented by :class:`ntfc.lib.usb.suspend.UsbSuspendCheck`.

A device driver that reports ``CLASS_SUSPEND`` but never ``CLASS_RESUME``
leaves ``cdcacm_suspend()``'s ``uart_connected(false)`` latched, after which
every board-side ``open()`` and ``write()`` on the CDC port returns
``-ENOTCONN``. The device stays enumerated, so the failure reads as a random
USB wedge.

Each cycle forces a runtime suspend, resumes the device by opening the port,
and counts bytes over the read window and over its tail. The tail count is
what separates a working link from one that only flushed its stale CDC TX
buffer on resume.

.. code-block:: bash

python -m ntfc usb-suspend [OPTIONS]

Requires a Linux host, ``sudo`` for two sysfs power attributes, a closed port
(an open port pins runtime PM, so the host never suspends), and a board that
transmits unprompted. Takes no ``--confpath``: nothing here needs a NuttX
configuration.

Options:

* ``-d, --device PATH`` - CDC/ACM port of the board.
Default: autodetect, which requires exactly one CDC/ACM port.

* ``-n, --cycles INTEGER`` - Suspend/resume cycles to run. Default: 5.

* ``--delay-ms INTEGER`` - Autosuspend delay to force. Default: 1000.

* ``--baud INTEGER`` - Baud rate of the CDC/ACM port. Default: 115200.

* ``--read-secs FLOAT`` - Read window per cycle. Default: 2.0.

* ``--tail-secs FLOAT`` - Trailing part of the window that must carry data.
Default: 1.0.

* ``--min-bytes INTEGER`` - Bytes required in the tail window. Default: 2000.

* ``--console PATH`` - Board console, on a separate port, used to ask the
board itself whether its CDC port is writable after resume.

* ``--console-baud INTEGER`` - Baud rate of that console. Default: 115200.

* ``--cdc-path PATH`` - CDC path as the board sees it.
Default: ``/dev/ttyACM0``.

Exit codes: 0 the link recovered from every suspend, 1 it did not, 2 the host
never suspended the device so nothing was exercised.

Log Management
==============

Expand Down
4 changes: 4 additions & 0 deletions src/ntfc/cli/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ class DEnvironmentData:
modules: Optional[List[str]] = None
select_individual_tests: Optional[List[int]] = None

# usb suspend check
runusbsuspend: bool = False
usbsuspend: Optional[Any] = None

# multi-session
runmulti: bool = False
manifest: Optional[str] = None
Expand Down
36 changes: 32 additions & 4 deletions src/ntfc/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@
import pprint
import sys
from collections.abc import Mapping
from typing import Any, Dict, List, Tuple
from typing import Any, Dict, List, Optional, Tuple

import click
import yaml # type: ignore
from prettytable import PrettyTable

from ntfc.builder import BuilderConfigError, NuttXBuilder
from ntfc.cli.environment import Environment, pass_environment
from ntfc.lib.usb.suspend import UsbSuspendCheck, UsbSuspendError
from ntfc.log.logger import logger
from ntfc.multi import ManifestConfig, MultiSessionRunner
from ntfc.plugins_loader import commands_list
Expand Down Expand Up @@ -340,16 +341,43 @@ def multi_run(ctx: Environment) -> int:
return runner.run()


def usbsuspend_run(ctx: Environment) -> int:
"""Run the USB CDC/ACM suspend/resume check.

:param ctx: CLI environment carrying the check configuration.
:return: Exit code (0 = pass, 1 = fail, 2 = inconclusive).
"""
assert ctx.usbsuspend is not None
try:
return UsbSuspendCheck(ctx.usbsuspend).run()
except UsbSuspendError as exc:
raise click.ClickException(str(exc)) from exc


def standalone_run(ctx: Environment) -> Optional[int]:
"""Run a mode that needs no NuttX configuration of its own.

:param ctx: CLI environment.
:return: Exit code, or None when no such mode was selected.
"""
if ctx.runmulti:
return multi_run(ctx)

if ctx.runusbsuspend:
return usbsuspend_run(ctx)

return None


@pass_environment
def cli_on_close(ctx: Environment) -> bool:
"""Handle all work on Click close."""
if ctx.helpnow: # pragma: no cover
# do nothing if help was called
return True

# multi-session mode
if ctx.runmulti:
ret = multi_run(ctx)
ret = standalone_run(ctx)
if ret is not None:
if ret != 0:
exit(1)
return True
Expand Down
129 changes: 129 additions & 0 deletions src/ntfc/commands/cmd_usbsuspend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
############################################################################
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you 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.
#
############################################################################

"""Module containing NTFC usb-suspend command."""

from typing import Optional

import click

from ntfc.cli.environment import Environment, pass_environment
from ntfc.lib.usb.suspend import UsbSuspendConfig

###############################################################################
# Command: cmd_usbsuspend
###############################################################################


@click.command(name="usb-suspend")
@click.option(
"-d",
"--device",
default=None,
help="CDC/ACM port of the board. Default: autodetect",
)
@click.option(
"-n",
"--cycles",
type=int,
default=5,
help="Suspend/resume cycles to run. Default: 5",
)
@click.option(
"--delay-ms",
type=int,
default=1000,
help="Autosuspend delay to force. Default: 1000",
)
@click.option(
"--baud",
type=int,
default=115200,
help="Baud rate of the CDC/ACM port. Default: 115200",
)
@click.option(
"--read-secs",
type=float,
default=2.0,
help="Read window per cycle. Default: 2.0",
)
@click.option(
"--tail-secs",
type=float,
default=1.0,
help="Trailing part of the window that must carry data. Default: 1.0",
)
@click.option(
"--min-bytes",
type=int,
default=2000,
help="Bytes required in the tail window. Default: 2000",
)
@click.option(
"--console",
default=None,
help="Board console, to probe the port from the board after resume",
)
@click.option(
"--console-baud",
type=int,
default=115200,
help="Baud rate of that console. Default: 115200",
)
@click.option(
"--cdc-path",
default="/dev/ttyACM0",
help="CDC path as the board sees it. Default: /dev/ttyACM0",
)
@pass_environment
def cmd_usbsuspend(
ctx: Environment,
device: Optional[str],
cycles: int,
delay_ms: int,
baud: int,
read_secs: float,
tail_secs: float,
min_bytes: int,
console: Optional[str],
console_baud: int,
cdc_path: str,
) -> bool:
"""Check that a USB CDC/ACM link survives host runtime suspend.

Needs a Linux host, sudo for two sysfs power attributes, and a board
that transmits unprompted on the CDC port. Needs no NuttX
configuration, so it takes no --confpath.
"""
ctx.runusbsuspend = True
ctx.usbsuspend = UsbSuspendConfig(
device=device,
cycles=cycles,
delay_ms=delay_ms,
baud=baud,
read_secs=read_secs,
tail_secs=tail_secs,
min_bytes=min_bytes,
console=console,
console_baud=console_baud,
cdc_path=cdc_path,
)

return True
2 changes: 2 additions & 0 deletions src/ntfc/ext_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from ntfc.commands.cmd_build import cmd_build
from ntfc.commands.cmd_collect import cmd_collect
from ntfc.commands.cmd_test import cmd_test
from ntfc.commands.cmd_usbsuspend import cmd_usbsuspend

if TYPE_CHECKING:
import click
Expand All @@ -33,4 +34,5 @@
cmd_build,
cmd_collect,
cmd_test,
cmd_usbsuspend,
]
Loading