Compare commits

...

2 Commits

3 changed files with 128 additions and 25 deletions

View File

@ -1,25 +1,26 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import click import click
import serial.tools.list_ports
from helpers import CliContext, Logger, EepromProgrammer 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, @click.option('--device', '-d', default='/dev/ttyUSB0', show_default=True,
metavar='DEVICE', help="Set the serial device") metavar='DEVICE', help="Set the serial device")
@click.option('--baud', '-b', default=38400, show_default=True, @click.option('--baud', '-b', default=38400, show_default=True,
metavar='BAUDRATE', help="Set the baud rate of the serial device") metavar='BAUDRATE', help="Set the baud rate of the serial device")
@click.option('--verbose', '-v', is_flag=True, help='Print debug output') @click.option('--verbose', '-v', is_flag=True, help='Print debug output')
@click.pass_context @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 # Create dependencies
logger = Logger(verbose=verbose) logger = Logger(verbose=verbose)
eeprom_programmer = EepromProgrammer(logger=logger, device=device, baudrate=baud) eeprom_programmer = EepromProgrammer(logger=logger, device=device, baudrate=baud)
logger.debug("Creating CLI context (device: {}, bauds: {})".format(device, baud))
# Create CLI context # Create CLI context
logger.debug("Creating CLI context")
ctx.obj = CliContext( ctx.obj = CliContext(
logger=logger, logger=logger,
eeprom_programmer=eeprom_programmer, eeprom_programmer=eeprom_programmer,
@ -29,9 +30,9 @@ def eepprog(ctx: click.Context, device: str, baud: int, verbose: bool):
ctx.call_on_close(eeprom_programmer.close) 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 @click.pass_obj
def hello(context: CliContext): def hello(context: CliContext) -> None:
""" """
Say hello. :) Say hello. :)
@ -46,19 +47,36 @@ def hello(context: CliContext):
context.logger.success('yay!') 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 @click.pass_obj
def test(context: CliContext): def test(context: CliContext) -> None:
""" """
Send an 'INIT' command to the programmer and read the answer. Send an 'INIT' command to the programmer and read the answer.
Test command for debugging. Test command for debugging.
""" """
context.eeprom_programmer.test() context.eeprom_programmer.test_command()
@eepprog.command('list-devices', category='Helper commands', 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 # TODO shell: Run an interactive shell
if __name__ == '__main__': if __name__ == '__main__':
eepprog() eepprog(max_content_width=120)

View File

@ -1,5 +1,5 @@
from serial import Serial from serial import Serial
from typing import Optional from typing import Optional, Union
from . import Logger from . import Logger
@ -20,33 +20,49 @@ class EepromProgrammer:
self._device_file = device self._device_file = device
self._baudrate = baudrate self._baudrate = baudrate
# TODO def open(self) -> None:
def open(self):
""" """
Open and setup serial port. Open and setup serial port.
""" """
assert self.serial is None, 'Serial port is already opened!' 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) self.serial = Serial(self._device_file, self._baudrate)
def close(self): def close(self) -> None:
if self.serial is None: """
self.logger.debug("Serial port is already closed") Closes the serial port.
return """
if self.serial is not None:
self.logger.debug("Closing serial port") self.logger.debug("Closing serial port")
self.serial.close() 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 # TODO
def test(self): def test_command(self) -> None:
# Open serial port # Open serial port
if self.serial is None: if self.serial is None:
self.open() self.open()
# TODO where to do this? in open() or when needed?
self.serial.timeout = 1
# Write a test command # Write a test command
self.logger.info("Sending 'INIT' ...") self.logger.info("Sending INIT command ...")
self.serial.write(b"INIT\n") self.write(b'INIT BINARY\n')
# Just read some stuff # 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))

69
client/helpers/click.py Normal file
View File

@ -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)