def decode_packet(data, checksum=True, strict=True, server_packet=True): """ Decodes a packet from or send to the PCI. Returns a tuple, the packet that was parsed and the remainder that was unparsed (in the case of some special commands. If no packet was able to be parsed, the first element of the tuple will be None. However there may be some circumstances where there is still a remainder to be parsed (cancel request). """ data = data.strip() if data == '': return None, None # packets from clients have some special flags which we need to handle. if server_packet: if data[0] == '+': data = data[1:] return PowerOnPacket(), data elif data[0] == '!': # buffer is full / invalid checksum, some requests may be dropped. # serial interface guide s4.3.3 p28 data = data[1:] return PCIErrorPacket(), data if data[0] in CONFIRMATION_CODES: success = data[1] == '.' code = data[0] data = data[2:] return ConfirmationPacket(code, success), data else: if data == '~~~': # reset return ResetPacket(), None elif data == '|': # smart + connect shortcut return SmartConnectShortcutPacket(), None elif '?' in data: # discard data before the ?, and resubmit for processing. data = data.split('?')[-1] return None, data if data[0] == '\\': data = data[1:] if data[0] == '@': # this causes it to be once-off a "basic" mode command. data = data[1:] checksum = False if data[-1] not in HEX_CHARS: # then there is a confirmation code at the end. confirmation = data[-1] if confirmation not in CONFIRMATION_CODES: if strict: raise ValueError, "Confirmation code is not a lowercase letter in g - z" else: warnings.warn( 'Confirmation code is not a lowercase letter in g - z') data = data[:-1] else: confirmation = None for c in data: if c not in HEX_CHARS: raise ValueError, "Non-base16 input: %r in %r" % (c, data) # get the checksum, if it's there. if checksum: # check the checksum if not validate_cbus_checksum(data): real_checksum = get_real_cbus_checksum(data) if strict: raise ValueError, "C-Bus checksum incorrect (expected %r) and strict mode is enabled: %r." % ( real_checksum, data) else: warnings.warn( "C-Bus checksum incorrect (expected %r) in data %r" % (real_checksum, data), UserWarning) # strip checksum data = data[:-2] # base16 decode data = b16decode(data) # flags (serial interface guide s3.4) flags = ord(data[0]) destination_address_type = flags & 0x07 # "reserved", "must be set to 0" rc = (flags & 0x18) >> 3 dp = (flags & 0x20) == 0x20 # priority class priority_class = (flags & 0xC0) >> 6 # increment ourselves along data = data[1:] # handle source address if server_packet: source_addr = ord(data[0]) data = data[1:] else: source_addr = None if dp: # device management flag set! # this is used to set parameters of the PCI p = DeviceManagementPacket.decode_packet(data, checksum, flags, destination_address_type, rc, dp, priority_class) elif destination_address_type == DAT_PP: # decode as point-to-point packet p = PointToPointPacket.decode_packet(data, checksum, flags, destination_address_type, rc, dp, priority_class) #raise NotImplementedError, 'Point-to-point' elif destination_address_type == DAT_PM: # decode as point-to-multipoint packet p = PointToMultipointPacket.decode_packet(data, checksum, flags, destination_address_type, rc, dp, priority_class) elif destination_address_type == DAT_PPM: # decode as point-to-point-to-multipoint packet #return PointToPointToMultipointPacket.decode_packet(data, checksum, flags, destination_address_type, rc, dp, priority_class) raise NotImplementedError, 'Point-to-point-tomultipoint' if not server_packet and confirmation: p.confirmation = confirmation p.source_address = None elif source_addr: p.source_address = source_addr p.confirmation = None return p, None
def decode_packet( data: bytes, checksum: bool = True, strict: bool = True, from_pci: bool = True) \ -> Tuple[Union[BasePacket, AnyCAL, None], int]: """ Decodes a single C-Bus Serial Interface packet. The return value is a tuple: 0. The packet that was parsed, or None if there was no packet that could be parsed. 1. The buffer position that we parsed up to. This may be non-zero even if the packet was None (eg: Cancel request). Note: this decoder does not support unaddressed packets (such as Standard Format Status Replies). Note: Direct Command Access causes this method to return AnyCAL instead of a BasePacket. :param data: The data to parse, in encapsulated serial format. :param checksum: If True, requires a checksum for all packets :param strict: If True, returns InvalidPacket whenever checksum is incorrect. Otherwise, only emits a warning. :param from_pci: If True, parses the packet as if it were sent from/by a PCI -- if your software was sent packets by a PCI, this is what you want. If False, this parses the packet as if it were sent to a PCI; parsing messages that software expecting to communicate with a PCI sends. This could be used to build a fake PCI, or analyse the behaviour of other C-Bus software. """ confirmation = None consumed = 0 # Serial Interface User Guide s4.2.7 device_managment_cal = False if data == b'': return None, 0 # There are some special transport-layer flags that need to be handled # before parsing the rest of the message. if from_pci: if data.startswith(b'+'): # + return PowerOnPacket(), consumed + 1 elif data.startswith(b'!'): # ! # buffer is full / invalid checksum, some requests may be dropped. # serial interface guide s4.3.3 p28 return PCIErrorPacket(), consumed + 1 if len(data) < MIN_MESSAGE_SIZE: # Not enough data in the buffer to process yet. return None, 0 if data[0] in CONFIRMATION_CODES: success = indexbytes(data, 1) == 0x2e # . code = data[:1] return ConfirmationPacket(code, success), consumed + 2 end = data.find(END_RESPONSE) else: if data.startswith(b'~'): # Reset # Serial Interface Guide, s4.2.3 return ResetPacket(), consumed + 1 elif data.startswith(b'null'): # Toolkit is buggy, just ignore it. return None, consumed + 4 elif (data.startswith(b'|' + END_COMMAND) or data.startswith(b'||' + END_COMMAND)): # SMART + CONNECT shortcut consumed += data.find(END_COMMAND) + 1 return SmartConnectShortcutPacket(), consumed else: # Check if we need to discard a message # Serial interface guide, s4.2.4 nlp = data.find(END_COMMAND) qp = data.find(b'?') if -1 < qp < nlp: # Discard data before the "?", and continue return None, consumed + qp + 1 end = data.find(END_COMMAND) # Look for ending character(s). If there is none, break out now. if end == -1: return None, consumed # Make it so the end of the buffer is where the end of the command is, and # consume the command up to and including the ending byte(s). data = data[:end] if from_pci: consumed += end + len(END_RESPONSE) else: consumed += end + len(END_COMMAND) if not data: # Empty command, break out! return None, consumed if not from_pci: if data.startswith(b'@'): # Once-off BASIC mode command, Serial Interface Guide, s4.2.7 checksum = False device_managment_cal = True data = data[1:] elif data.startswith(b'\\'): data = data[1:] else: device_managment_cal = True if data[-1] not in HEX_CHARS: # then there is a confirmation code at the end. confirmation = int2byte(indexbytes(data, -1)) if confirmation not in CONFIRMATION_CODES: if strict: return InvalidPacket( payload=data, exception=ValueError( 'Confirmation code is not in range g..z') ), consumed else: warnings.warn('Confirmation code is not in range g..z') # strip confirmation byte data = data[:-1] for c in data: if c not in HEX_CHARS: return InvalidPacket( payload=data, exception=ValueError( f'Non-base16 input: {c:x} in {data}')), consumed # base16 decode data = b16decode(data) # get the checksum, if it's there. if checksum: # check the checksum if not validate_cbus_checksum(data): real_checksum = get_real_cbus_checksum(data) if strict: return InvalidPacket( payload=data, exception=ValueError( f'C-Bus checksum incorrect (expected 0x{real_checksum:x}) ' f'and strict mode is enabled: {data}')), consumed else: warnings.warn( f'C-Bus checksum incorrect (expected 0x{real_checksum:x}) ' f'in data {data}', UserWarning) # strip checksum data = data[:-1] # flags (serial interface guide s3.4) flags = byte2int(data) try: address_type = DestinationAddressType(flags & 0x07) # "reserved", "must be set to 0" # rc = (flags >> 3) & 0x03 dp = (flags & 0x20) == 0x20 # priority class priority_class = PriorityClass((flags >> 6) & 0x03) # increment ourselves along data = data[1:] # handle source address if from_pci: source_addr = byte2int(data) data = data[1:] else: source_addr = None if dp: # device management flag set! # this is used to set parameters of the PCI p = DeviceManagementPacket.decode_packet( data=data, checksum=checksum, priority_class=priority_class) elif device_managment_cal: cal, cal_len = PointToPointPacket.decode_cal(data) return cal, consumed + cal_len elif address_type == DestinationAddressType.POINT_TO_POINT: # decode as point-to-point packet p = PointToPointPacket.decode_packet(data=data, checksum=checksum, priority_class=priority_class) elif address_type == DestinationAddressType.POINT_TO_MULTIPOINT: # decode as point-to-multipoint packet p = PointToMultipointPacket.decode_packet( data=data, checksum=checksum, priority_class=priority_class) elif (address_type == DestinationAddressType.POINT_TO_POINT_TO_MULTIPOINT): # decode as point-to-point-to-multipoint packet # return PointToPointToMultipointPacket.decode_packet(data, checksum, # flags, destination_address_type, rc, dp, priority_class) raise NotImplementedError('Point-to-point-to-multipoint') else: raise NotImplementedError( f'Destination address type = 0x{address_type:x}') if not from_pci: p.confirmation = confirmation p.source_address = None elif source_addr: p.source_address = source_addr p.confirmation = None except Exception as e: p = InvalidPacket(payload=data, exception=e) return p, consumed
def decode_packet(data, checksum=True, strict=True, server_packet=True): """ Decodes a packet from or send to the PCI. Returns a tuple, the packet that was parsed and the remainder that was unparsed (in the case of some special commands. If no packet was able to be parsed, the first element of the tuple will be None. However there may be some circumstances where there is still a remainder to be parsed (cancel request). """ data = data.strip() if data == '': return None, None # packets from clients have some special flags which we need to handle. if server_packet: if data[0] == '+': data = data[1:] return PowerOnPacket(), data elif data[0] == '!': # buffer is full / invalid checksum, some requests may be dropped. # serial interface guide s4.3.3 p28 data = data[1:] return PCIErrorPacket(), data if data[0] in CONFIRMATION_CODES: success = data[1] == '.' code = data[0] data = data[2:] return ConfirmationPacket(code, success), data else: if data == '~~~': # reset return ResetPacket(), None elif data == '|': # smart + connect shortcut return SmartConnectShortcutPacket(), None elif '?' in data: # discard data before the ?, and resubmit for processing. data = data.split('?')[-1] return None, data if data[0] == '\\': data = data[1:] if data[0] == '@': # this causes it to be once-off a "basic" mode command. data = data[1:] checksum = False if data[-1] not in HEX_CHARS: # then there is a confirmation code at the end. confirmation = data[-1] if confirmation not in CONFIRMATION_CODES: if strict: raise ValueError, "Confirmation code is not a lowercase letter in g - z" else: warnings.warn('Confirmation code is not a lowercase letter in g - z') data = data[:-1] else: confirmation = None for c in data: if c not in HEX_CHARS: raise ValueError, "Non-base16 input: %r in %r" % (c, data) # get the checksum, if it's there. if checksum: # check the checksum if not validate_cbus_checksum(data): real_checksum = get_real_cbus_checksum(data) if strict: raise ValueError, "C-Bus checksum incorrect (expected %r) and strict mode is enabled: %r." % (real_checksum, data) else: warnings.warn("C-Bus checksum incorrect (expected %r) in data %r" % (real_checksum, data), UserWarning) # strip checksum data = data[:-2] # base16 decode data = b16decode(data) # flags (serial interface guide s3.4) flags = ord(data[0]) destination_address_type = flags & 0x07 # "reserved", "must be set to 0" rc = (flags & 0x18) >> 3 dp = (flags & 0x20) == 0x20 # priority class priority_class = (flags & 0xC0) >> 6 # increment ourselves along data = data[1:] # handle source address if server_packet: source_addr = ord(data[0]) data = data[1:] else: source_addr = None if dp: # device management flag set! # this is used to set parameters of the PCI p = DeviceManagementPacket.decode_packet(data, checksum, flags, destination_address_type, rc, dp, priority_class) elif destination_address_type == DAT_PP: # decode as point-to-point packet p = PointToPointPacket.decode_packet(data, checksum, flags, destination_address_type, rc, dp, priority_class) #raise NotImplementedError, 'Point-to-point' elif destination_address_type == DAT_PM: # decode as point-to-multipoint packet p = PointToMultipointPacket.decode_packet(data, checksum, flags, destination_address_type, rc, dp, priority_class) elif destination_address_type == DAT_PPM: # decode as point-to-point-to-multipoint packet #return PointToPointToMultipointPacket.decode_packet(data, checksum, flags, destination_address_type, rc, dp, priority_class) raise NotImplementedError, 'Point-to-point-tomultipoint' if not server_packet and confirmation: p.confirmation = confirmation p.source_address = None elif source_addr: p.source_address = source_addr p.confirmation = None return p, None