parent
							
								
									48523ced91
								
							
						
					
					
						commit
						5086abb60d
					
				@ -0,0 +1,367 @@ | 
				
			||||
class TF_Msg: | 
				
			||||
    def __init__(self): | 
				
			||||
        self.data = bytearray() | 
				
			||||
        self.len = 0 | 
				
			||||
        self.type = 0 | 
				
			||||
        self.id = 0 | 
				
			||||
 | 
				
			||||
    def __str__(self): | 
				
			||||
        return f"ID {self.id:X}h, type {self.type:X}h, len {self.len:d}, body: {self.data}" | 
				
			||||
 | 
				
			||||
class TF: | 
				
			||||
    STAY = 'STAY' | 
				
			||||
    RENEW = 'RENEW' | 
				
			||||
    CLOSE = 'CLOSE' | 
				
			||||
    NEXT = 'NEXT' | 
				
			||||
 | 
				
			||||
class TinyFrame: | 
				
			||||
    def __init__(self, peer=1): | 
				
			||||
        self.write = None # the writer function should be attached here | 
				
			||||
 | 
				
			||||
        self.id_listeners = {} | 
				
			||||
        self.type_listeners = {} | 
				
			||||
        self.fallback_listener = None | 
				
			||||
        self.peer = peer # the peer bit | 
				
			||||
 | 
				
			||||
        # ----------------------------- FRAME FORMAT --------------------------------- | 
				
			||||
        #  The format can be adjusted to fit your particular application needs | 
				
			||||
 | 
				
			||||
        #  If the connection is reliable, you can disable the SOF byte and checksums. | 
				
			||||
        #  That can save up to 9 bytes of overhead. | 
				
			||||
 | 
				
			||||
        #  ,-----+----+-----+------+------------+- - - -+------------, | 
				
			||||
        #  | SOF | ID | LEN | TYPE | HEAD_CKSUM | DATA  | PLD_CKSUM  | | 
				
			||||
        #  | 1   | ?  | ?   | ?    | ?          | ...   | ?          | <- size (bytes) | 
				
			||||
        #  '-----+----+-----+------+------------+- - - -+------------' | 
				
			||||
 | 
				
			||||
        #  !!! BOTH SIDES MUST USE THE SAME SETTINGS !!! | 
				
			||||
 | 
				
			||||
        #  Adjust sizes as desired (1,2,4) | 
				
			||||
        self.ID_BYTES = 2 | 
				
			||||
        self.LEN_BYTES = 2 | 
				
			||||
        self.TYPE_BYTES = 1 | 
				
			||||
 | 
				
			||||
        #  Checksum type | 
				
			||||
        # ('none', 'xor', 'crc16, 'crc32' | 
				
			||||
        self.CKSUM_TYPE = 'xor' | 
				
			||||
 | 
				
			||||
        #  Use a SOF byte to mark the start of a frame | 
				
			||||
        self.USE_SOF_BYTE = True | 
				
			||||
        #  Value of the SOF byte (if TF_USE_SOF_BYTE == 1) | 
				
			||||
        self.SOF_BYTE = 0x01 | 
				
			||||
 | 
				
			||||
        self.next_frame_id = 0 | 
				
			||||
 | 
				
			||||
        self.reset_parser() | 
				
			||||
 | 
				
			||||
    def reset_parser(self): | 
				
			||||
        # parser state: SOF, ID, LEN, TYPE, HCK, PLD, PCK | 
				
			||||
        self.ps = 'SOF' | 
				
			||||
        # buffer for receiving bytes | 
				
			||||
        self.rbuf = None | 
				
			||||
        # expected number of bytes to receive | 
				
			||||
        self.rlen = 0 | 
				
			||||
        # buffer for payload or checksum | 
				
			||||
        self.rpayload = None | 
				
			||||
        # received frame | 
				
			||||
        self.rf = TF_Msg() | 
				
			||||
 | 
				
			||||
    @property | 
				
			||||
    def _CKSUM_BYTES(self): | 
				
			||||
        if self.CKSUM_TYPE == 'none' or self.CKSUM_TYPE is None: | 
				
			||||
            return 0 | 
				
			||||
        elif self.CKSUM_TYPE == 'xor': | 
				
			||||
            return 1 | 
				
			||||
        elif self.CKSUM_TYPE == 'crc16': | 
				
			||||
            return 2 | 
				
			||||
        elif self.CKSUM_TYPE == 'crc32': | 
				
			||||
            return 2 | 
				
			||||
        else: | 
				
			||||
            raise Exception("Bad cksum type!") | 
				
			||||
 | 
				
			||||
    def _cksum(self, buffer): | 
				
			||||
        if self.CKSUM_TYPE == 'none' or self.CKSUM_TYPE is None: | 
				
			||||
            return 0 | 
				
			||||
 | 
				
			||||
        elif self.CKSUM_TYPE == 'xor': | 
				
			||||
            acc = 0 | 
				
			||||
            for b in buffer: | 
				
			||||
                acc ^= b | 
				
			||||
            return (~acc) & ((1<<(self._CKSUM_BYTES*8))-1) | 
				
			||||
 | 
				
			||||
        elif self.CKSUM_TYPE == 'crc16': | 
				
			||||
            raise Exception("CRC16 not implemented!") | 
				
			||||
 | 
				
			||||
        elif self.CKSUM_TYPE == 'crc32': | 
				
			||||
            raise Exception("CRC32 not implemented!") | 
				
			||||
 | 
				
			||||
        else: | 
				
			||||
            raise Exception("Bad cksum type!") | 
				
			||||
 | 
				
			||||
    @property | 
				
			||||
    def _SOF_BYTES(self): | 
				
			||||
        return 1 if self.USE_SOF_BYTE else 0 | 
				
			||||
 | 
				
			||||
    def _gen_frame_id(self): | 
				
			||||
        """ | 
				
			||||
        Get a new frame ID | 
				
			||||
        """ | 
				
			||||
 | 
				
			||||
        frame_id = self.next_frame_id | 
				
			||||
 | 
				
			||||
        self.next_frame_id += 1 | 
				
			||||
        if self.next_frame_id > ((1<<(8*self.ID_BYTES-1))-1): | 
				
			||||
            self.next_frame_id = 0 | 
				
			||||
 | 
				
			||||
        if self.peer == 1: | 
				
			||||
            frame_id |= 1<<(8*self.ID_BYTES-1) | 
				
			||||
 | 
				
			||||
        return frame_id | 
				
			||||
 | 
				
			||||
    def _pack(self, num, bytes): | 
				
			||||
        """ Pack a number for a TF field """ | 
				
			||||
        return num.to_bytes(bytes, byteorder='big', signed=False) | 
				
			||||
 | 
				
			||||
    def _unpack(self, buf): | 
				
			||||
        """ Unpack a number from a TF field """ | 
				
			||||
        return int.from_bytes(buf, byteorder='big', signed=False) | 
				
			||||
 | 
				
			||||
    def query(self, type, listener, pld=None, id=None): | 
				
			||||
        """ Send a query """ | 
				
			||||
        (id, buf) = self._compose(type=type, pld=pld, id=id) | 
				
			||||
 | 
				
			||||
        if listener is not None: | 
				
			||||
            self.add_id_listener(id, listener) | 
				
			||||
 | 
				
			||||
        self.write(buf) | 
				
			||||
 | 
				
			||||
    def send(self, type, pld=None, id=None): | 
				
			||||
        """ Like query, but with no listener """ | 
				
			||||
        self.query(type=type, pld=pld, id=id, listener=None) | 
				
			||||
 | 
				
			||||
    def _compose(self, type, pld=None, id=None): | 
				
			||||
        """ | 
				
			||||
        Compose a frame. | 
				
			||||
 | 
				
			||||
        frame_id can be an ID of an existing session, None for a new session. | 
				
			||||
        """ | 
				
			||||
 | 
				
			||||
        if pld is None: | 
				
			||||
            pld = b'' | 
				
			||||
 | 
				
			||||
        if id is None: | 
				
			||||
            id = self._gen_frame_id() | 
				
			||||
 | 
				
			||||
        buf = bytearray() | 
				
			||||
        if self.USE_SOF_BYTE: | 
				
			||||
            buf.extend(self._pack(self.SOF_BYTE, 1)) | 
				
			||||
 | 
				
			||||
        buf.extend(self._pack(id, self.ID_BYTES)) | 
				
			||||
        buf.extend(self._pack(len(pld), self.LEN_BYTES)) | 
				
			||||
        buf.extend(self._pack(type, self.TYPE_BYTES)) | 
				
			||||
 | 
				
			||||
        if self._CKSUM_BYTES > 0: | 
				
			||||
            buf.extend(self._pack(self._cksum(buf), self._CKSUM_BYTES)) | 
				
			||||
 | 
				
			||||
        if len(pld) > 0: | 
				
			||||
            buf.extend(pld) | 
				
			||||
            buf.extend(self._pack(self._cksum(pld), self._CKSUM_BYTES)) | 
				
			||||
 | 
				
			||||
        return (id, buf) | 
				
			||||
 | 
				
			||||
    def accept(self, bytes): | 
				
			||||
        """ | 
				
			||||
        Parse bytes received on the serial port | 
				
			||||
        """ | 
				
			||||
        for b in bytes: | 
				
			||||
            self.accept_byte(b) | 
				
			||||
 | 
				
			||||
    def accept_byte(self, b): | 
				
			||||
        if self.ps == 'SOF': | 
				
			||||
            if self.USE_SOF_BYTE: | 
				
			||||
                if b != self.SOF_BYTE: | 
				
			||||
                    return | 
				
			||||
 | 
				
			||||
                self.rpayload = bytearray() | 
				
			||||
                self.rpayload.append(b) | 
				
			||||
 | 
				
			||||
            self.ps = 'ID' | 
				
			||||
            self.rlen = self.ID_BYTES | 
				
			||||
            self.rbuf = bytearray() | 
				
			||||
 | 
				
			||||
            if self.USE_SOF_BYTE: | 
				
			||||
                return | 
				
			||||
 | 
				
			||||
        if self.ps == 'ID': | 
				
			||||
            self.rpayload.append(b) | 
				
			||||
            self.rbuf.append(b) | 
				
			||||
 | 
				
			||||
            if len(self.rbuf) == self.rlen: | 
				
			||||
                self.rf.id = self._unpack(self.rbuf) | 
				
			||||
 | 
				
			||||
                self.ps = 'LEN' | 
				
			||||
                self.rlen = self.LEN_BYTES | 
				
			||||
                self.rbuf = bytearray() | 
				
			||||
            return | 
				
			||||
 | 
				
			||||
        if self.ps == 'LEN': | 
				
			||||
            self.rpayload.append(b) | 
				
			||||
            self.rbuf.append(b) | 
				
			||||
 | 
				
			||||
            if len(self.rbuf) == self.rlen: | 
				
			||||
                self.rf.len = self._unpack(self.rbuf) | 
				
			||||
 | 
				
			||||
                self.ps = 'TYPE' | 
				
			||||
                self.rlen = self.TYPE_BYTES | 
				
			||||
                self.rbuf = bytearray() | 
				
			||||
            return | 
				
			||||
 | 
				
			||||
        if self.ps == 'TYPE': | 
				
			||||
            self.rpayload.append(b) | 
				
			||||
            self.rbuf.append(b) | 
				
			||||
 | 
				
			||||
            if len(self.rbuf) == self.rlen: | 
				
			||||
                self.rf.type = self._unpack(self.rbuf) | 
				
			||||
 | 
				
			||||
                if self._CKSUM_BYTES > 0: | 
				
			||||
                    self.ps = 'HCK' | 
				
			||||
                    self.rlen = self._CKSUM_BYTES | 
				
			||||
                    self.rbuf = bytearray() | 
				
			||||
                else: | 
				
			||||
                    self.ps = 'PLD' | 
				
			||||
                    self.rlen = self.rf.len | 
				
			||||
                    self.rbuf = bytearray() | 
				
			||||
            return | 
				
			||||
 | 
				
			||||
        if self.ps == 'HCK': | 
				
			||||
            self.rbuf.append(b) | 
				
			||||
 | 
				
			||||
            if len(self.rbuf) == self.rlen: | 
				
			||||
                hck = self._unpack(self.rbuf) | 
				
			||||
                actual = self._cksum(self.rpayload) | 
				
			||||
 | 
				
			||||
                if hck != actual: | 
				
			||||
                    self.reset_parser() | 
				
			||||
                else: | 
				
			||||
                    if self.rf.len == 0: | 
				
			||||
                        self.handle_rx_frame() | 
				
			||||
                        self.reset_parser() | 
				
			||||
                    else: | 
				
			||||
                        self.ps = 'PLD' | 
				
			||||
                        self.rlen = self.rf.len | 
				
			||||
                        self.rbuf = bytearray() | 
				
			||||
                        self.rpayload = bytearray() | 
				
			||||
            return | 
				
			||||
 | 
				
			||||
        if self.ps == 'PLD': | 
				
			||||
            self.rpayload.append(b) | 
				
			||||
            self.rbuf.append(b) | 
				
			||||
 | 
				
			||||
            if len(self.rbuf) == self.rlen: | 
				
			||||
                self.rf.data = self.rpayload | 
				
			||||
 | 
				
			||||
                if self._CKSUM_BYTES > 0: | 
				
			||||
                    self.ps = 'PCK' | 
				
			||||
                    self.rlen = self._CKSUM_BYTES | 
				
			||||
                    self.rbuf = bytearray() | 
				
			||||
                else: | 
				
			||||
                    self.handle_rx_frame() | 
				
			||||
                    self.reset_parser() | 
				
			||||
            return | 
				
			||||
 | 
				
			||||
        if self.ps == 'PCK': | 
				
			||||
            self.rbuf.append(b) | 
				
			||||
 | 
				
			||||
            if len(self.rbuf) == self.rlen: | 
				
			||||
                pck = self._unpack(self.rbuf) | 
				
			||||
                actual = self._cksum(self.rpayload) | 
				
			||||
 | 
				
			||||
                if pck != actual: | 
				
			||||
                    self.reset_parser() | 
				
			||||
                else: | 
				
			||||
                    self.handle_rx_frame() | 
				
			||||
                    self.reset_parser() | 
				
			||||
            return | 
				
			||||
 | 
				
			||||
    def handle_rx_frame(self): | 
				
			||||
        frame = self.rf | 
				
			||||
 | 
				
			||||
        if frame.id in self.id_listeners and self.id_listeners[frame.id] is not None: | 
				
			||||
            lst = self.id_listeners[frame.id] | 
				
			||||
            rv = lst['fn'](self, frame) | 
				
			||||
            if rv == TF.CLOSE: | 
				
			||||
                self.id_listeners[frame.id] = None | 
				
			||||
                return | 
				
			||||
            elif rv == TF.RENEW: | 
				
			||||
                lst.age = 0 | 
				
			||||
                return | 
				
			||||
            elif rv == TF.STAY: | 
				
			||||
                return | 
				
			||||
            # TF.NEXT lets another handler process it | 
				
			||||
 | 
				
			||||
        if frame.type in self.type_listeners and self.type_listeners[frame.type] is not None: | 
				
			||||
            lst = self.type_listeners[frame.type] | 
				
			||||
            rv = lst['fn'](self, frame) | 
				
			||||
            if rv == TF.CLOSE: | 
				
			||||
                self.type_listeners[frame.type] = None | 
				
			||||
                return | 
				
			||||
            elif rv != TF.NEXT: | 
				
			||||
                return | 
				
			||||
 | 
				
			||||
        if self.fallback_listener is not None: | 
				
			||||
            lst = self.fallback_listener | 
				
			||||
            rv = lst['fn'](self, frame) | 
				
			||||
            if rv == TF.CLOSE: | 
				
			||||
                self.fallback_listener = None | 
				
			||||
 | 
				
			||||
    def add_id_listener(self, id, lst, lifetime=None): | 
				
			||||
        """ | 
				
			||||
        Add a ID listener that expires in "lifetime" ticks | 
				
			||||
 | 
				
			||||
        listener function takes two arguments: | 
				
			||||
        tinyframe instance and a msg object | 
				
			||||
        """ | 
				
			||||
        self.id_listeners[id] = { | 
				
			||||
            'fn': lst, | 
				
			||||
            'lifetime': lifetime, | 
				
			||||
            'age': 0, | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
    def add_type_listener(self, type, lst): | 
				
			||||
        """ | 
				
			||||
        Add a type listener | 
				
			||||
 | 
				
			||||
        listener function takes two arguments: | 
				
			||||
        tinyframe instance and a msg object | 
				
			||||
        """ | 
				
			||||
        self.type_listeners[type] = { | 
				
			||||
            'fn': lst, | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
    def add_fallback_listener(self, lst): | 
				
			||||
        """ | 
				
			||||
        Add a fallback listener | 
				
			||||
 | 
				
			||||
        listener function takes two arguments: | 
				
			||||
        tinyframe instance and a msg object | 
				
			||||
        """ | 
				
			||||
        self.fallback_listener = { | 
				
			||||
            'fn': lst, | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
					Loading…
					
					
				
		Reference in new issue