From d9a7cb401d8adac286101e54b060cf5fd3bb9b79 Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Sat, 12 Jun 2021 01:52:24 +0200 Subject: [PATCH 1/2] eepprog.py: Add list-devices command --- client/eepprog.py | 33 ++++++++++++++++------ client/helpers/EepromProgrammer.py | 44 ++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/client/eepprog.py b/client/eepprog.py index 58837d0..617fe3f 100755 --- a/client/eepprog.py +++ b/client/eepprog.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import click +import serial.tools.list_ports from helpers import CliContext, Logger, EepromProgrammer @@ -12,14 +13,13 @@ from helpers import CliContext, Logger, EepromProgrammer metavar='BAUDRATE', help="Set the baud rate of the serial device") @click.option('--verbose', '-v', is_flag=True, help='Print debug output') @click.pass_context -def eepprog(ctx: click.Context, device: str, baud: int, verbose: bool): +def eepprog(ctx: click.Context, device: str, baud: int, verbose: bool) -> None: # Create dependencies logger = Logger(verbose=verbose) eeprom_programmer = EepromProgrammer(logger=logger, device=device, baudrate=baud) - logger.debug("Creating CLI context (device: {}, bauds: {})".format(device, baud)) - # Create CLI context + logger.debug("Creating CLI context") ctx.obj = CliContext( logger=logger, eeprom_programmer=eeprom_programmer, @@ -31,7 +31,7 @@ def eepprog(ctx: click.Context, device: str, baud: int, verbose: bool): @eepprog.command('hello', short_help='Say hello. :)') @click.pass_obj -def hello(context: CliContext): +def hello(context: CliContext) -> None: """ Say hello. :) @@ -48,17 +48,34 @@ def hello(context: CliContext): @eepprog.command('test', short_help="Send INIT command to the programmer and read answer") @click.pass_obj -def test(context: CliContext): +def test(context: CliContext) -> None: """ Send an 'INIT' command to the programmer and read the answer. Test command for debugging. """ - context.eeprom_programmer.test() + context.eeprom_programmer.test_command() + + +@eepprog.command('list-devices', short_help="List available serial ports") +@click.pass_obj +def list_devices(context: CliContext) -> None: + """ + List serial ports that are available on the system. + + Internally uses 'serial.tools.list_ports' of pySerial. + """ + context.logger.debug("Getting list of serial ports") + ports = serial.tools.list_ports.comports() + + context.logger.debug() # Print empty line + click.echo("Found {} serial ports:".format(len(ports))) + for port in ports: + click.echo('' + str(port)) + -# TODO command: list-devices -> serial.tools.list_ports # TODO shell: Run an interactive shell if __name__ == '__main__': - eepprog() + eepprog(max_content_width=120) diff --git a/client/helpers/EepromProgrammer.py b/client/helpers/EepromProgrammer.py index d529ddc..19f55ec 100644 --- a/client/helpers/EepromProgrammer.py +++ b/client/helpers/EepromProgrammer.py @@ -1,5 +1,5 @@ from serial import Serial -from typing import Optional +from typing import Optional, Union from . import Logger @@ -20,33 +20,49 @@ class EepromProgrammer: self._device_file = device self._baudrate = baudrate - # TODO - def open(self): + def open(self) -> None: """ Open and setup serial port. """ assert self.serial is None, 'Serial port is already opened!' - self.logger.debug("Setting up serial device '{}' with baudrate {}".format(self._device_file, self._baudrate)) + self.logger.debug("Opening serial device '{}' with {} bauds".format(self._device_file, self._baudrate)) self.serial = Serial(self._device_file, self._baudrate) - def close(self): - if self.serial is None: - self.logger.debug("Serial port is already closed") - return + def close(self) -> None: + """ + Closes the serial port. + """ + if self.serial is not None: + self.logger.debug("Closing serial port") + self.serial.close() - self.logger.debug("Closing serial port") - self.serial.close() + def write(self, data: Union[bytes, bytearray]) -> int: + """ + Writes bytes to the serial port and writes debug log. + """ + self.logger.debug('Writing: {}'.format(str(data))) + return self.serial.write(data) # TODO - def test(self): + def test_command(self) -> None: # Open serial port if self.serial is None: self.open() + # TODO where to do this? in open() or when needed? + self.serial.timeout = 1 + # Write a test command - self.logger.info("Sending 'INIT' ...") - self.serial.write(b"INIT\n") + self.logger.info("Sending INIT command ...") + self.write(b'INIT BINARY\n') # Just read some stuff - self.logger.info("Read line: ", self.serial.readline(80)) + self.logger.info("Received line: ", self.serial.readline(80)) + + # Send a READ command + self.logger.info("Sending READ command ...") + self.write(b'READ 0000:0010\n') + + while True: + self.logger.info("Received line: ", self.serial.readline(80)) From d17d5341ceb48d7e4f0b7fe8260c947f30edc298 Mon Sep 17 00:00:00 2001 From: binaryDiv Date: Sat, 12 Jun 2021 02:02:32 +0200 Subject: [PATCH 2/2] eepprog.py: Implement Click CategorizedGroup --- client/eepprog.py | 9 +++--- client/helpers/click.py | 69 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 client/helpers/click.py diff --git a/client/eepprog.py b/client/eepprog.py index 617fe3f..2a9641f 100755 --- a/client/eepprog.py +++ b/client/eepprog.py @@ -4,9 +4,10 @@ import click import serial.tools.list_ports from helpers import CliContext, Logger, EepromProgrammer +from helpers.click import CategorizedGroup -@click.group() +@click.group(cls=CategorizedGroup) @click.option('--device', '-d', default='/dev/ttyUSB0', show_default=True, metavar='DEVICE', help="Set the serial device") @click.option('--baud', '-b', default=38400, show_default=True, @@ -29,7 +30,7 @@ def eepprog(ctx: click.Context, device: str, baud: int, verbose: bool) -> None: ctx.call_on_close(eeprom_programmer.close) -@eepprog.command('hello', short_help='Say hello. :)') +@eepprog.command('hello', category='Test commands', short_help='Say hello. :)') @click.pass_obj def hello(context: CliContext) -> None: """ @@ -46,7 +47,7 @@ def hello(context: CliContext) -> None: context.logger.success('yay!') -@eepprog.command('test', short_help="Send INIT command to the programmer and read answer") +@eepprog.command('test', category='Test commands', short_help="Send INIT command to the programmer and read answer") @click.pass_obj def test(context: CliContext) -> None: """ @@ -57,7 +58,7 @@ def test(context: CliContext) -> None: context.eeprom_programmer.test_command() -@eepprog.command('list-devices', short_help="List available serial ports") +@eepprog.command('list-devices', category='Helper commands', short_help="List available serial ports") @click.pass_obj def list_devices(context: CliContext) -> None: """ diff --git a/client/helpers/click.py b/client/helpers/click.py new file mode 100644 index 0000000..ff38a6a --- /dev/null +++ b/client/helpers/click.py @@ -0,0 +1,69 @@ +import click +import collections + + +class CategorizedGroup(click.Group): + """ + Click command group that categorizes subcommands in the help text by the additional 'category' attribute. + Commands that don't have a 'category' attribute are grouped under the default category "Commands" as usual. + + Example usage: + `@categorized_group.command('foobar', category='Debug commands')` + """ + default_category: str + + def __init__(self, name=None, commands=None, default_category: str = 'Commands', **attrs): + super().__init__(name, commands, **attrs) + self.commands = commands or collections.OrderedDict() + self.default_category = default_category + + def list_commands(self, ctx): + """ List commands in the order they were added to the group. """ + return self.commands + + def command(self, *args, **kwargs): + """ Extends the command decorator by setting a category attribute on the command. """ + category = kwargs.pop('category', None) + orig_decorator = super().command(*args, **kwargs) + + def decorator(f): + cmd = orig_decorator(f) + if category: + cmd.category = category + return cmd + + return decorator + + def format_commands(self, ctx, formatter): + """ + Prints commands in help messages. + This version groups them into categories and is based on the original Group.format_commands method. + """ + categorized_commands = {self.default_category: []} + + # Collect subcommands grouped by category + for subcommand in self.list_commands(ctx): + cmd = self.get_command(ctx, subcommand) + if cmd is None or cmd.hidden: + continue + + category = getattr(cmd, 'category', self.default_category) + if categorized_commands.get(category) is None: + categorized_commands[category] = [] + + categorized_commands[category].append((subcommand, cmd)) + + # Print help sections for each subcommand category + for category, subcommands in categorized_commands.items(): + if not len(subcommands): + continue + + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in subcommands) + + rows = [] + for subcommand, cmd in subcommands: + help_str = cmd.get_short_help_str(limit) + rows.append((subcommand, help_str)) + + with formatter.section(category): + formatter.write_dl(rows)