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