Search code examples
pythondesign-patternspyserialorganization

Need suggestions for decoupling classes used in a script


I have a python program in which the main use case is interacting with it through a CLI to instruct it to send out byte packets over serial. The serial target adheres to a certain command protocol. The python program builds packets that adhere to this protocol based on user input on the CLI (specific command to send, arguments for the command, etc).

The module for this functionality consists of three classes: one which subclasses enum to create unique identifiers for each possible command, one which subclasses the cmd module to implement a CLI interface to the user (this class also does argument input sanitizing) and finally one class which takes in the desired command and arguments and builds the packet to send out over serial

The issue I'm having is that these classes are getting pretty coupled. Ideally I wanted the Master_Comm class to be kind of independent so it could be accessed from other modules to send out packets from a different origin (say a script file parser). As it is Serial_Interface has an instance of Master_Comm to access the sendcommand method, as well as implementing input sanitation (which may need to be done multiple places.

Any suggestions on organizing this better? Especially as the program grows (possibly implementing hundreds of commands).

import serial
from cmd import Cmd
from enum import Enum, unique

@unique
class Command_Names(Enum):
    CMD1 = 1
    CMD2 = 2

class Serial_Interface(Cmd):
    def __init__(self, port, baud,):
        Cmd.__init__(self)
        self.slave_comm = Master_Comm(port=port, baud=baud)
        # setup stuff

    def do_cmd1(self, args):
        try:
            assert 0 <= int(args) <= 25, "Argument out of 0-25 range"
            self.slave_comm.sendcommand(Command_Names.CMD1, args)
        except (AssertionError) as e:
            print(e)

    def do_cmd2(self, args):
        try:
            # No args for cmd2, so don't check anything
            self.slave_comm.sendcommand(Command_Names.CMD2, args)
        except (AssertionError) as e:
            print(e)


class Master_Comm(object):
    _SLAVE_COMMANDS = {Command_Names.CMD1:           (b'\x10', b'\x06', b'\x06'),
                       Command_Names.CMD2:           (b'\x10', b'\x07', b'\x07')}

    _SOURCE_ID = b'\xF0'
    _SEQ_NUM = b'\x00'

    def __init__(self, port, baud):
        self.ser = serial.Serial(port=None,
                                 baudrate=int(baud)
                                 )
        self.ser.port = port

    def open_serial(self):
        # do stuff

    def close_serial(self):
        # do stuff

    def sendcommand(self, command, args):
        try:
            txpacket = bytearray()
            txpacket.extend(self._SOURCE_ID)
            txpacket.extend(self._SEQ_NUM)
            txpacket.extend(self._SLAVE_COMMANDS[command][0])
            txpacket.extend(self._SLAVE_COMMANDS[command][1])
            txpacket.extend(self._SLAVE_COMMANDS[command][2])
            self.ser.write(tx_bytes)
        except Exception as e:
            print(e)

Solution

  • You can decouple the commands hardcoded in your Master_Comm class by passing them as an argument to its __init__() constructor. Below is simple implementation of this which creates and uses a new dictionary named CMDS which is defined separately.

    This table is passed as an argument to the Master_Comm class when an instance of it is created, then that is passed as an argument to the SerialInterface class (instead of it creating it's own).

    This same idea could be taken further by changing the format of the CMDS table so it included more information about the command and then using that in the SerialInterface class' implementation instead of the hardcoded stuff it still has in it (such as the length of the tuple of bytes associated with each one).

    Note that I also changed the names of your classes to they follow PEP 8 - Style Guide for Python Code recommendations for naming.

    import serial
    from cmd import Cmd
    from enum import Enum, unique
    
    class SerialInterface(Cmd):
        def __init__(self, mastercomm):
            Cmd.__init__(self)
            self.slave_comm = mastercomm
            # setup stuff
    
        def do_cmd1(self, args):
            try:
                assert 0 <= int(args) <= 25, "Argument out of 0-25 range"
                self.slave_comm.sendcommand(CommandNames.CMD1, args)
            except (AssertionError) as e:
                print(e)
    
        def do_cmd2(self, args):
            try:
                # No args for cmd2, so don't check anything
                self.slave_comm.sendcommand(CommandNames.CMD2, args)
            except (AssertionError) as e:
                print(e)
    
    
    class MasterComm:
        """ Customized serial communications class for communicating with serial
            port using the serial module and commands defined by the table "cmds".
        """
        def __init__(self, port, baud, cmds):
            self.ser = serial.Serial(port=None,
                                     baudrate=int(baud),)
            self.ser.port = port
            self.source_id = cmds['source_id']
            self.seq_num = cmds['seq_num']
            self.slave_commands = cmds['slave_commands']
    
        def sendcommand(self, command, args):
            try:
                txpacket = bytearray()
                txpacket.extend(self.source_id)
                txpacket.extend(self.seq_num)
                txpacket.extend(self.slave_commands[command][0])
                txpacket.extend(self.slave_commands[command][1])
                txpacket.extend(self.slave_commands[command][2])
                self.ser.write(txpacket)
            except Exception as e:
                print(e)
    
    
    @unique
    class CommandNames(Enum):
        CMD1 = 1
        CMD2 = 2
    
    CMDS = {
        'source_id': b'\xF0',
        'seq_num': b'\x00',
        'slave_commands' : {CommandNames.CMD1: (b'\x10', b'\x06', b'\x06'),
                            CommandNames.CMD2: (b'\x10', b'\x07', b'\x07')}
    }
    
    mastercomm = MasterComm(0x03b2, 8192, CMDS)
    serialinterface = SerialInterface(mastercomm)
    serialinterface.cmdloop()