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