Embedding Guide#

pw_console: Multi-purpose pluggable interactive console for dev & manufacturing

Using embed()#

pw console is invoked by calling PwConsoleEmbed().embed() in your own Python script. For a complete example of an embedded device console script see pw_system/py/pw_system/console.py.

pw_console embed class.

class pw_console.embed.PwConsoleEmbed(
global_vars=None,
local_vars=None,
loggers: dict[str, Iterable[logging.Logger]] | Iterable | None = None,
test_mode=False,
repl_startup_message: str | None = None,
help_text: str | None = None,
app_title: str | None = None,
config_file_path: str | Path | None = None,
)#

Bases: object

Embed class for customizing the console before startup.

__init__(
global_vars=None,
local_vars=None,
loggers: dict[str, Iterable[logging.Logger]] | Iterable | None = None,
test_mode=False,
repl_startup_message: str | None = None,
help_text: str | None = None,
app_title: str | None = None,
config_file_path: str | Path | None = None,
) None#

Call this to embed pw console at the call point within your program.

Example usage:

import logging

from pw_console import PwConsoleEmbed

# Create the pw_console embed instance
console = PwConsoleEmbed(
    global_vars=globals(),
    local_vars=locals(),
    loggers={
        'Host Logs': [
            logging.getLogger(__package__),
            logging.getLogger(__name__),
        ],
        'Device Logs': [
            logging.getLogger('usb_gadget'),
        ],
    },
    app_title='My Awesome Console',
    config_file_path='/home/user/project/.pw_console.yaml',
)
# Optional: Add custom completions
console.add_sentence_completer(
    {
        'some_function', 'Function',
        'some_variable', 'Variable',
    }
)

# Setup Python loggers to output to a file instead of STDOUT.
console.setup_python_logging()

# Then run the console with:
console.embed()
Parameters:
  • global_vars – dictionary representing the desired global symbol table. Similar to what is returned by globals().

  • local_vars – dictionary representing the desired local symbol table. Similar to what is returned by locals().

  • loggers

    dict with keys of log window titles and values of either:

    1. List of logging.getLogger() instances.

    2. A single pw_console.log_store.LogStore instance.

  • app_title – Custom title text displayed in the user interface.

  • repl_startup_message – Custom text shown by default in the repl output pane.

  • help_text – Custom text shown at the top of the help window before keyboard shortcuts.

  • config_file_path – Path to a pw_console yaml config file.

add_bottom_toolbar(toolbar: WindowPaneToolbar) None#

Include a toolbar plugin to display at the bottom of the screen.

Bottom toolbars appear below all window panes and span the full width of the screen.

Parameters:

toolbar – Instance of the WindowPaneToolbar class.

add_floating_window_plugin(window_pane: FloatingWindowPane, **float_args) None#

Include a custom floating window pane plugin.

This adds a FloatingWindowPane class to the pw_console UI. The first argument should be the window to add and the remaining keyword arguments are passed to the prompt_toolkit Float() class. This allows positioning of the floating window. By default the floating window will be centered. To anchor the window to a side or corner of the screen set the left, right, top, or bottom keyword args.

For example:

from pw_console import PwConsoleEmbed

console = PwConsoleEmbed(...)
my_plugin = MyPlugin()
# Anchor this floating window 2 rows away from the top and 4 columns
# away from the left edge of the screen.
console.add_floating_window_plugin(my_plugin, top=2, left=4)

See all possible keyword args in the prompt_toolkit documentation: https://python-prompt-toolkit.readthedocs.io/en/stable/pages/reference.html#prompt_toolkit.layout.Float

Parameters:
  • window_pane – Any instance of the FloatingWindowPane class.

  • left – Distance to the left edge of the screen

  • right – Distance to the right edge of the screen

  • top – Distance to the top edge of the screen

  • bottom – Distance to the bottom edge of the screen

add_sentence_completer(word_meta_dict: dict[str, str], ignore_case=True) None#

Include a custom completer that matches on the entire repl input.

Parameters:

word_meta_dict – dictionary representing the sentence completions and descriptions. Keys are completion text, values are descriptions.

add_top_toolbar(toolbar: WindowPaneToolbar) None#

Include a toolbar plugin to display on the top of the screen.

Top toolbars appear above all window panes and just below the main menu bar. They span the full width of the screen.

Parameters:

toolbar – Instance of the WindowPaneToolbar class.

add_window_plugin(window_pane: WindowPane) None#

Include a custom window pane plugin.

Parameters:

window_pane – Any instance of the WindowPane class.

embed(override_window_config: dict | None = None) None#

Start the console.

hide_windows(*window_titles) None#

Hide window panes specified by title on console startup.

setup_python_logging(
last_resort_filename: str | None = None,
loggers_with_no_propagation: Iterable[Logger] | None = None,
) None#

Setup friendly logging for full-screen prompt_toolkit applications.

This function sets up Python log handlers to be friendly for full-screen prompt_toolkit applications. That is, logging to terminal STDOUT and STDERR is disabled so the terminal user interface can be drawn.

Specifically, all Python STDOUT and STDERR log handlers are disabled. It also sets log propagation to True. to ensure that all log messages are sent to the root logger.

Parameters:
  • last_resort_filename – If specified use this file as a fallback for unhandled Python logging messages. Normally Python will output any log messages with no handlers to STDERR as a fallback. If None, a temp file will be created instead. See Python documentation on logging.lastResort for more info.

  • loggers_with_no_propagation – List of logger instances to skip setting propagate = True. This is useful if you would like log messages from a particular source to not appear in the root logger.

class pw_console.log_store.LogStore(prefs: ConsolePrefs | None = None)#

Bases: Handler

Pigweed Console logging handler.

This is a Python logging.Handler class used to store logs for display in the pw_console user interface.

You may optionally add this as a handler to an existing logger instances. This will be required if logs need to be captured for display in the pw_console UI before the user interface is running.

Example usage:

import logging

from pw_console import PwConsoleEmbed, LogStore

_DEVICE_LOG = logging.getLogger('usb_gadget')

# Create a log store and add as a handler.
device_log_store = LogStore()
_DEVICE_LOG.addHander(device_log_store)

# Start communication with your device here, before embedding
# pw_console.

# Create the pw_console embed instance
console = PwConsoleEmbed(
    global_vars=globals(),
    local_vars=locals(),
    loggers={
        'Host Logs': [
            logging.getLogger(__package__),
            logging.getLogger(__name__),
        ],
        # Set the LogStore as the value of this logger window.
        'Device Logs': device_log_store,
    },
    app_title='My Awesome Console',
)

console.setup_python_logging()
console.embed()
__init__(prefs: ConsolePrefs | None = None)#

Initializes the LogStore instance.

Adding Plugins#

User plugin instances are created before starting-up and passed to the Pigweed Console embed instance. Typically, a console is started by creating a PwConsoleEmbed() instance, calling customization functions, then calling .embed() as shown in Using embed(). Adding plugins functions similarly by calling add_top_toolbar, add_bottom_toolbar, add_floating_window_plugin or add_window_plugin. For example:

# Create plugin instances
user_toolbar1 = DeviceStatusToolbar(device=client.client.channel(1))
user_toolbar2 = BandwithToolbar()
user_device_window = CustomWindowPlugin()

console = PwConsoleEmbed(
    global_vars=local_variables,
    loggers={
        'Device Logs': [logging.getLogger('rpc_device')],
        'Host Logs': [logging.getLogger()],
    },
    ...
)

# Add toolbar plugins
console.add_top_toolbar(user_toolbar1)
console.add_bottom_toolbar(user_toolbar2)

# Add Window plugins
console.add_window_plugin(user_device_window)

# Start the console
console.embed()

Adding Log Metadata#

pw_console can display log messages in a table with justified columns for metadata fields provided by pw_log_tokenized.

It is also possible to manually add values that should be displayed in columns using the extra keyword argument when logging from Python. See the Python’s logging documentation for how extra works. A dict of name, value pairs can be passed in as the extra_metadata_fields variable. For example, the following code will create a log message with two custom columns titled module and timestamp.

import logging

LOG = logging.getLogger('log_source_1')

LOG.info(
    'Hello there!',
    extra={
        'extra_metadata_fields': {
            'module': 'cool',
            'timestamp': 1.2345,
        }
    }
)

Debugging Serial Data#

pw_console is often used to communicate with devices using pySerial or pw_console.socket_client.SocketClient. To monitor the raw data flowing over the wire, pw_console provides simple wrappers for pySerial and socket client instances that log data for each read and write call.

Logging data with PySerial#

# Instead of 'import serial' use this import:
from pw_console.pyserial_wrapper import SerialWithLogging

serial_device = SerialWithLogging('/dev/ttyUSB0', 115200, timeout=1)

Logging data with sockets#

from pw_console.socket_client import SocketClientWithLogging

# Name resolution with explicit port
serial_device = SocketClientWithLogging('localhost:1234')
# Name resolution with default port.
serial_device = SocketClientWithLogging('pigweed.dev')
# Link-local IPv6 address with explicit port.
serial_device = SocketClientWithLogging('[fe80::100%enp1s0]:1234')
# Link-local IPv6 address with default port.
serial_device = SocketClientWithLogging('[fe80::100%enp1s0]')
# IPv4 address with port.
serial_device = SocketClientWithLogging('1.2.3.4:5678')

Tip

The SocketClient takes an optional callback called when a disconnect is detected. The pw_system console provides an example reconnect routine.

With the above examples each serial_device.read and write call will create a log message to the pw_console.serial_debug_logger Python logger. This logger can then be included as a log window pane in the PwConsoleEmbed() call.

import logging
from pw_console import PwConsoleEmbed

console = PwConsoleEmbed(
    global_vars=globals(),
    local_vars=locals(),
    loggers={
        'Host Logs': [
            # Root Python logger
            logging.getLogger(''),
            # Your current Python package logger.
            logging.getLogger(__package__)
        ],
        'Device Logs': [
            logging.getLogger('usb_gadget')
        ],
        'Serial Debug': [
            # New log window to display serial read and writes
            logging.getLogger('pw_console.serial_debug_logger')
        ],
    },
    app_title='CoolConsole',
)
# Then run the console with:
console.embed()
Serial debug pw_console screenshot.

Screenshot of issuing an Echo RPC with serial debug logging.#