Sep-19-2019, 08:32 AM
I am trying to create a library to encode commands sent over TCP to a controller box and decode the responses.
To makes things simple, each command corresponds to a hex code with a number of arguments, to which the controller box outputs response.
The aim of the code below is to implement the encoding/decoding of bytes exchanged with the controller with the following constraints:
The first two points are easy enough, however the 3rd point requires to write specific decoding methods for a majority of commands.
I have written 2 different approaches, which don't really satisfy me some various reasons. The first example would be the simplest if I did not have to implement specific decoding. The 2nd is my preferred method in this case, although I am not sure using subclassing like this is the way to go. Any thoughts or suggestions?
[Example 1: one class]
In this example, a Command is instanciated with one of the command names: the codes dictionary in the __init__ gives the correct format codes to use with struct.pack/unpack.
The main drawback is that all the specific decoding methods will have to be declared in the command class, likely resulting in a code that is harder to read and to maintain.
Here I am making a lot of subclasses, one per command type. In this case I can easily reimplement the encode and decode methods to suit my needs, and it's easier to add and remove commands.
To makes things simple, each command corresponds to a hex code with a number of arguments, to which the controller box outputs response.
The aim of the code below is to implement the encoding/decoding of bytes exchanged with the controller with the following constraints:
- Use easily identifiable command name instead of an obscure hex code
- Provide under the hood the correct format for encoding/decoding of bytes exchanged with the controller box
- Perform additional operations on the decoded bytes (formatting, convert, etc) if needed
The first two points are easy enough, however the 3rd point requires to write specific decoding methods for a majority of commands.
I have written 2 different approaches, which don't really satisfy me some various reasons. The first example would be the simplest if I did not have to implement specific decoding. The 2nd is my preferred method in this case, although I am not sure using subclassing like this is the way to go. Any thoughts or suggestions?
[Example 1: one class]
In this example, a Command is instanciated with one of the command names: the codes dictionary in the __init__ gives the correct format codes to use with struct.pack/unpack.
The main drawback is that all the specific decoding methods will have to be declared in the command class, likely resulting in a code that is harder to read and to maintain.
CLOSE_LNK = 0x25
GET_VERSION = 0x2A5
READ_NB_AXES = 0x34D
class Command:
def __init__(self, code):
codes = {CLOSE_LNK: {},
GET_VERSION: {'in_fmt': '<?', 'out_fmt': '< 4I'},
READ_NB_AXES: {'out_fmt': '< IBI', 'custom_decode': self.dec_0x34D}
# list goes on...
}
properties = codes[code]
self.command = code
self.in_fmt = properties.get('in_fmt')
self.out_fmt = properties.get('out_fmt')
self.c_encode = properties.get('custom_encode')
self.c_decode = properties.get('custom_decode')
def encode(self, *args):
# cls._check_argnb(*args)
if self.c_encode is not None:
return self.c_encode(*args)
if self.in_fmt is None:
return b''
return struct.pack(self.in_fmt, *args)
def decode(self, in_bytes):
if self.c_decode is not None:
return self.c_decode(in_bytes)
if self.out_fmt is None:
return b''
return struct.unpack(self.out_fmt, in_bytes)
# define custom encode/decode methods
def dec_0x34D(self, in_bytes):
return struct.unpack(self.out_fmt, in_bytes + b'\x00')
# .....
# .....
if __name__=="__main__":
a = Command(GET_VERSION)
print(a.encode(True))
print(a.decode(b'\x12\x2D\x00\x12\x2D\x00\x12\x2D\x00\x12\x2D\x00\x12\x2D\x00\x12'))[Example 2: subclassing]Here I am making a lot of subclasses, one per command type. In this case I can easily reimplement the encode and decode methods to suit my needs, and it's easier to add and remove commands.
class Command:
@classmethod
def encode(cls, *args):
return struct.pack(cls._in_fmt, *args)
@classmethod
def decode(cls, in_bytes):
return struct.unpack(cls._out_fmt, in_bytes)
@classmethod
def get_id(cls):
return cls._id
class CLOSE_LNK(Command):
_id = 0x25
@classmethod
def encode(cls):
return b''
@classmethod
def decode(cls, in_bytes):
return b''
class GET_VERSION(Command):
_id = 0x2A5
_in_fmt = '<?'
_out_fmt = '< 4I'
_argnb = 1
class READ_NB_AXES(Command):
_id = 0x34D
_out_fmt = '< IBI'
@classmethod
def encode(cls):
return b''
@classmethod
def decode(cls, in_bytes):
out = super().decode(in_bytes + b'\x00')
return out
if __name__=="__main__":
print(GET_VERSION.encode(True))
print(GET_VERSION.decode(b'\x12\x2D\x00\x12\x2D\x00\x12\x2D\x00\x12\x2D\x00\x12\x2D\x00\x12'))
