From 48f55e0b7398b5c561b3bacd7479304ebd45aba6 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 6 Mar 2018 09:17:18 -0500 Subject: [PATCH 01/50] add the packet number back in --- py34/bacpypes/analysis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/py34/bacpypes/analysis.py b/py34/bacpypes/analysis.py index fe21cc39..386bb123 100755 --- a/py34/bacpypes/analysis.py +++ b/py34/bacpypes/analysis.py @@ -358,13 +358,13 @@ def decode_file(fname): # create a pcap object p = pcap.pcap(fname) - for timestamp, data in p: + for i, (timestamp, data) in enumerate(p): pkt = decode_packet(data) if not pkt: continue - # save the index and timestamp in the packet - # pkt._index = i + # save the packet number (as viewed in Wireshark) and timestamp + pkt._number = i + 1 pkt._timestamp = timestamp yield pkt From 2fb24ae22e456c1d814701e76a0655225c93f931 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 6 Mar 2018 13:51:50 -0500 Subject: [PATCH 02/50] sync the py34 changes with py27 #173 --- py27/bacpypes/analysis.py | 9 +++++---- py34/bacpypes/analysis.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/py27/bacpypes/analysis.py b/py27/bacpypes/analysis.py index 388360a4..7e7d1d5c 100755 --- a/py27/bacpypes/analysis.py +++ b/py27/bacpypes/analysis.py @@ -355,16 +355,17 @@ def decode_file(fname): if not pcap: raise RuntimeError("failed to import pcap") - # create a pcap object + # create a pcap object, reading from the file p = pcap.pcap(fname) - for timestamp, data in p: + # loop through the packets + for i, (timestamp, data) in enumerate(p): pkt = decode_packet(data) if not pkt: continue - # save the index and timestamp in the packet - # pkt._index = i + # save the packet number (as viewed in Wireshark) and timestamp + pkt._number = i + 1 pkt._timestamp = timestamp yield pkt diff --git a/py34/bacpypes/analysis.py b/py34/bacpypes/analysis.py index 386bb123..0d980f29 100755 --- a/py34/bacpypes/analysis.py +++ b/py34/bacpypes/analysis.py @@ -355,9 +355,10 @@ def decode_file(fname): if not pcap: raise RuntimeError("failed to import pcap") - # create a pcap object + # create a pcap object, reading from the file p = pcap.pcap(fname) + # loop through the packets for i, (timestamp, data) in enumerate(p): pkt = decode_packet(data) if not pkt: From b5a866e723055dcf3699930fea8bc5fe19a42b0a Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Wed, 28 Mar 2018 23:12:31 -0400 Subject: [PATCH 03/50] first crack at BIPNAT for #178 --- py25/bacpypes/bvllservice.py | 304 +++++++++++++++++++++++++++++++++++ py27/bacpypes/bvllservice.py | 302 ++++++++++++++++++++++++++++++++++ py34/bacpypes/bvllservice.py | 303 ++++++++++++++++++++++++++++++++++ 3 files changed, 909 insertions(+) diff --git a/py25/bacpypes/bvllservice.py b/py25/bacpypes/bvllservice.py index 65ae6143..2a46cf33 100755 --- a/py25/bacpypes/bvllservice.py +++ b/py25/bacpypes/bvllservice.py @@ -552,6 +552,7 @@ def confirmation(self, pdu): # send it upstream self.response(xpdu) + return # check the BBMD registration status, we may not be registered if self.registrationStatus != 0: @@ -988,6 +989,309 @@ def delete_peer(self, addr): bacpypes_debugging(BIPBBMD) +# +# BIPNAT +# + +class BIPNAT(BIPSAP, Client, Server, RecurringTask, DebugContents): + + _debug_contents = ('bbmdAddress', 'bbmdBDT+', 'bbmdFDT+') + + def __init__(self, addr, sapID=None, cid=None, sid=None): + """A BBMD node that is the destination for NATed traffic.""" + if _debug: BIPNAT._debug("__init__ %r sapID=%r cid=%r sid=%r", addr, sapID, cid, sid) + BIPSAP.__init__(self, sapID) + Client.__init__(self, cid) + Server.__init__(self, sid) + RecurringTask.__init__(self, 1000.0) + + self.bbmdAddress = addr + self.bbmdBDT = [] + self.bbmdFDT = [] + + # install so process_task runs + self.install_task() + + def indication(self, pdu): + if _debug: BIPNAT._debug("indication %r", pdu) + + # check for local stations + if pdu.pduDestination.addrType == Address.localStationAddr: + ###TODO the destination should be a peer or a registered foreign device + + # make an original unicast PDU + xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduDestination + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + # check for broadcasts + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + # make a forwarded PDU + xpdu = ForwardedNPDU(self.bbmdAddress, pdu, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) + + # send it to the peers, all of them have all F's mask + for bdte in self.bbmdBDT: + if bdte != self.bbmdAddress: + xpdu.pduDestination = Address((bdte.addrIP, bdte.addrPort)) + BIPNAT._debug(" - sending to peer: %r", xpdu.pduDestination) + self.request(xpdu) + + # send it to the registered foreign devices + for fdte in self.bbmdFDT: + xpdu.pduDestination = fdte.fdAddress + if _debug: BIPNAT._debug(" - sending to foreign device: %r", xpdu.pduDestination) + self.request(xpdu) + + else: + BIPNAT._warning("invalid destination address: %r", pdu.pduDestination) + + def confirmation(self, pdu): + if _debug: BIPNAT._debug("confirmation %r", pdu) + + # some kind of response to a request + if isinstance(pdu, Result): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, WriteBroadcastDistributionTable): + ###TODO verify this is from a management network/address + + # build a response + xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduSource + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadBroadcastDistributionTable): + ###TODO verify this is from a management network/address + + # build a response + xpdu = ReadBroadcastDistributionTableAck(self.bbmdBDT, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduSource + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ForwardedNPDU): + ###TODO verify this is from a peer + + # build a PDU with the source from the real source + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) + + # send it upstream + self.response(xpdu) + + # build a forwarded NPDU to send out + xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) + + # send it to the registered foreign devices + for fdte in self.bbmdFDT: + xpdu.pduDestination = fdte.fdAddress + if _debug: BIPNAT._debug(" - sending to foreign device: %r", xpdu.pduDestination) + self.request(xpdu) + + elif isinstance(pdu, RegisterForeignDevice): + ###TODO verify this is from an acceptable address + + # process the request + stat = self.register_foreign_device(pdu.pduSource, pdu.bvlciTimeToLive) + + # build a response + xpdu = Result(code=stat, destination=pdu.pduSource, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadForeignDeviceTable): + ###TODO verify this is from a management network/address + + # build a response + xpdu = ReadForeignDeviceTableAck(self.bbmdFDT, destination=pdu.pduSource, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, DeleteForeignDeviceTableEntry): + ###TODO verify this is from a management network/address + + # process the request + stat = self.delete_foreign_device_table_entry(pdu.bvlciAddress) + + # build a response + xpdu = Result(code=stat, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduSource + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, DistributeBroadcastToNetwork): + ###TODO verify this is from a registered foreign device + + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) + + # send it upstream + self.response(xpdu) + + # build a forwarded NPDU to send out + xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) + + # send it to the peers + for bdte in self.bbmdBDT: + if bdte == self.bbmdAddress: + if _debug: BIPNAT._debug(" - no local broadcast") + else: + xpdu.pduDestination = Address((bdte.addrIP, bdte.addrPort)) + if _debug: BIPNAT._debug(" - sending to peer: %r", xpdu.pduDestination) + self.request(xpdu) + + # send it to the other registered foreign devices + for fdte in self.bbmdFDT: + if fdte.fdAddress != pdu.pduSource: + xpdu.pduDestination = fdte.fdAddress + if _debug: BIPNAT._debug(" - sending to foreign device: %r", xpdu.pduDestination) + self.request(xpdu) + + elif isinstance(pdu, OriginalUnicastNPDU): + ###TODO verify this is from a peer + + # build a vanilla PDU + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) + + # send it upstream + self.response(xpdu) + + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPNAT._debug(" - original broadcast dropped") + + else: + BIPNAT._warning("invalid pdu type: %s", type(pdu)) + + def register_foreign_device(self, addr, ttl): + """Add a foreign device to the FDT.""" + if _debug: BIPNAT._debug("register_foreign_device %r %r", addr, ttl) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation( addr ) + else: + raise TypeError("addr must be a string or an Address") + + for fdte in self.bbmdFDT: + if addr == fdte.fdAddress: + break + else: + fdte = FDTEntry() + fdte.fdAddress = addr + self.bbmdFDT.append( fdte ) + + fdte.fdTTL = ttl + fdte.fdRemain = ttl + 5 + + # return success + return 0 + + def delete_foreign_device_table_entry(self, addr): + if _debug: BIPNAT._debug("delete_foreign_device_table_entry %r", addr) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation( addr ) + else: + raise TypeError("addr must be a string or an Address") + + # find it and delete it + stat = 0 + for i in range(len(self.bbmdFDT)-1, -1, -1): + if addr == self.bbmdFDT[i].fdAddress: + del self.bbmdFDT[i] + break + else: + stat = 99 ### entry not found + + # return status + return stat + + def process_task(self): + # look for foreign device registrations that have expired + for i in range(len(self.bbmdFDT)-1, -1, -1): + fdte = self.bbmdFDT[i] + fdte.fdRemain -= 1 + + # delete it if it expired + if fdte.fdRemain <= 0: + if _debug: BIPNAT._debug("foreign device expired: %r", fdte.fdAddress) + del self.bbmdFDT[i] + + def add_peer(self, addr): + if _debug: BIPNAT._debug("add_peer %r", addr) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation(addr) + else: + raise TypeError("addr must be a string or an Address") + + # if it's this BBMD, make it the first one + if self.bbmdBDT and (addr == self.bbmdAddress): + raise RuntimeError("add self to BDT as first address") + + # see if it's already there + for bdte in self.bbmdBDT: + if addr == bdte: + break + else: + self.bbmdBDT.append(addr) + + def delete_peer(self, addr): + if _debug: BIPNAT._debug("delete_peer %r", addr) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation(addr) + else: + raise TypeError("addr must be a string or an Address") + + # look for the peer address + for i in range(len(self.bbmdBDT)-1, -1, -1): + if addr == self.bbmdBDT[i]: + del self.bbmdBDT[i] + break + else: + pass + +bacpypes_debugging(BIPNAT) + # # BVLLServiceElement # diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index 4c2cc182..f687d365 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -982,6 +982,308 @@ def delete_peer(self, addr): else: pass +# +# BIPNAT +# + +@bacpypes_debugging +class BIPNAT(BIPSAP, Client, Server, RecurringTask, DebugContents): + + _debug_contents = ('bbmdAddress', 'bbmdBDT+', 'bbmdFDT+') + + def __init__(self, addr, sapID=None, cid=None, sid=None): + """A BBMD node that is the destination for NATed traffic.""" + if _debug: BIPNAT._debug("__init__ %r sapID=%r cid=%r sid=%r", addr, sapID, cid, sid) + BIPSAP.__init__(self, sapID) + Client.__init__(self, cid) + Server.__init__(self, sid) + RecurringTask.__init__(self, 1000.0) + + self.bbmdAddress = addr + self.bbmdBDT = [] + self.bbmdFDT = [] + + # install so process_task runs + self.install_task() + + def indication(self, pdu): + if _debug: BIPNAT._debug("indication %r", pdu) + + # check for local stations + if pdu.pduDestination.addrType == Address.localStationAddr: + ###TODO the destination should be a peer or a registered foreign device + + # make an original unicast PDU + xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduDestination + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + # check for broadcasts + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + # make a forwarded PDU + xpdu = ForwardedNPDU(self.bbmdAddress, pdu, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) + + # send it to the peers, all of them have all F's mask + for bdte in self.bbmdBDT: + if bdte != self.bbmdAddress: + xpdu.pduDestination = Address((bdte.addrIP, bdte.addrPort)) + BIPNAT._debug(" - sending to peer: %r", xpdu.pduDestination) + self.request(xpdu) + + # send it to the registered foreign devices + for fdte in self.bbmdFDT: + xpdu.pduDestination = fdte.fdAddress + if _debug: BIPNAT._debug(" - sending to foreign device: %r", xpdu.pduDestination) + self.request(xpdu) + + else: + BIPNAT._warning("invalid destination address: %r", pdu.pduDestination) + + def confirmation(self, pdu): + if _debug: BIPNAT._debug("confirmation %r", pdu) + + # some kind of response to a request + if isinstance(pdu, Result): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, WriteBroadcastDistributionTable): + ###TODO verify this is from a management network/address + + # build a response + xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduSource + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadBroadcastDistributionTable): + ###TODO verify this is from a management network/address + + # build a response + xpdu = ReadBroadcastDistributionTableAck(self.bbmdBDT, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduSource + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ForwardedNPDU): + ###TODO verify this is from a peer + + # build a PDU with the source from the real source + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) + + # send it upstream + self.response(xpdu) + + # build a forwarded NPDU to send out + xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) + + # send it to the registered foreign devices + for fdte in self.bbmdFDT: + xpdu.pduDestination = fdte.fdAddress + if _debug: BIPNAT._debug(" - sending to foreign device: %r", xpdu.pduDestination) + self.request(xpdu) + + elif isinstance(pdu, RegisterForeignDevice): + ###TODO verify this is from an acceptable address + + # process the request + stat = self.register_foreign_device(pdu.pduSource, pdu.bvlciTimeToLive) + + # build a response + xpdu = Result(code=stat, destination=pdu.pduSource, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadForeignDeviceTable): + ###TODO verify this is from a management network/address + + # build a response + xpdu = ReadForeignDeviceTableAck(self.bbmdFDT, destination=pdu.pduSource, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, DeleteForeignDeviceTableEntry): + ###TODO verify this is from a management network/address + + # process the request + stat = self.delete_foreign_device_table_entry(pdu.bvlciAddress) + + # build a response + xpdu = Result(code=stat, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduSource + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, DistributeBroadcastToNetwork): + ###TODO verify this is from a registered foreign device + + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) + + # send it upstream + self.response(xpdu) + + # build a forwarded NPDU to send out + xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) + + # send it to the peers + for bdte in self.bbmdBDT: + if bdte == self.bbmdAddress: + if _debug: BIPNAT._debug(" - no local broadcast") + else: + xpdu.pduDestination = Address((bdte.addrIP, bdte.addrPort)) + if _debug: BIPNAT._debug(" - sending to peer: %r", xpdu.pduDestination) + self.request(xpdu) + + # send it to the other registered foreign devices + for fdte in self.bbmdFDT: + if fdte.fdAddress != pdu.pduSource: + xpdu.pduDestination = fdte.fdAddress + if _debug: BIPNAT._debug(" - sending to foreign device: %r", xpdu.pduDestination) + self.request(xpdu) + + elif isinstance(pdu, OriginalUnicastNPDU): + ###TODO verify this is from a peer + + # build a vanilla PDU + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) + + # send it upstream + self.response(xpdu) + + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPNAT._debug(" - original broadcast dropped") + + else: + BIPNAT._warning("invalid pdu type: %s", type(pdu)) + + def register_foreign_device(self, addr, ttl): + """Add a foreign device to the FDT.""" + if _debug: BIPNAT._debug("register_foreign_device %r %r", addr, ttl) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation( addr ) + else: + raise TypeError("addr must be a string or an Address") + + for fdte in self.bbmdFDT: + if addr == fdte.fdAddress: + break + else: + fdte = FDTEntry() + fdte.fdAddress = addr + self.bbmdFDT.append( fdte ) + + fdte.fdTTL = ttl + fdte.fdRemain = ttl + 5 + + # return success + return 0 + + def delete_foreign_device_table_entry(self, addr): + if _debug: BIPNAT._debug("delete_foreign_device_table_entry %r", addr) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation( addr ) + else: + raise TypeError("addr must be a string or an Address") + + # find it and delete it + stat = 0 + for i in range(len(self.bbmdFDT)-1, -1, -1): + if addr == self.bbmdFDT[i].fdAddress: + del self.bbmdFDT[i] + break + else: + stat = 99 ### entry not found + + # return status + return stat + + def process_task(self): + # look for foreign device registrations that have expired + for i in range(len(self.bbmdFDT)-1, -1, -1): + fdte = self.bbmdFDT[i] + fdte.fdRemain -= 1 + + # delete it if it expired + if fdte.fdRemain <= 0: + if _debug: BIPNAT._debug("foreign device expired: %r", fdte.fdAddress) + del self.bbmdFDT[i] + + def add_peer(self, addr): + if _debug: BIPNAT._debug("add_peer %r", addr) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation(addr) + else: + raise TypeError("addr must be a string or an Address") + + # if it's this BBMD, make it the first one + if self.bbmdBDT and (addr == self.bbmdAddress): + raise RuntimeError("add self to BDT as first address") + + # see if it's already there + for bdte in self.bbmdBDT: + if addr == bdte: + break + else: + self.bbmdBDT.append(addr) + + def delete_peer(self, addr): + if _debug: BIPNAT._debug("delete_peer %r", addr) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation(addr) + else: + raise TypeError("addr must be a string or an Address") + + # look for the peer address + for i in range(len(self.bbmdBDT)-1, -1, -1): + if addr == self.bbmdBDT[i]: + del self.bbmdBDT[i] + break + else: + pass + # # BVLLServiceElement # diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index dddb2d01..0477bf0d 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -547,6 +547,7 @@ def confirmation(self, pdu): # send it upstream self.response(xpdu) + return # check the BBMD registration status, we may not be registered if self.registrationStatus != 0: @@ -980,6 +981,308 @@ def delete_peer(self, addr): else: pass +# +# BIPNAT +# + +@bacpypes_debugging +class BIPNAT(BIPSAP, Client, Server, RecurringTask, DebugContents): + + _debug_contents = ('bbmdAddress', 'bbmdBDT+', 'bbmdFDT+') + + def __init__(self, addr, sapID=None, cid=None, sid=None): + """A BBMD node that is the destination for NATed traffic.""" + if _debug: BIPNAT._debug("__init__ %r sapID=%r cid=%r sid=%r", addr, sapID, cid, sid) + BIPSAP.__init__(self, sapID) + Client.__init__(self, cid) + Server.__init__(self, sid) + RecurringTask.__init__(self, 1000.0) + + self.bbmdAddress = addr + self.bbmdBDT = [] + self.bbmdFDT = [] + + # install so process_task runs + self.install_task() + + def indication(self, pdu): + if _debug: BIPNAT._debug("indication %r", pdu) + + # check for local stations + if pdu.pduDestination.addrType == Address.localStationAddr: + ###TODO the destination should be a peer or a registered foreign device + + # make an original unicast PDU + xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduDestination + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + # check for broadcasts + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + # make a forwarded PDU + xpdu = ForwardedNPDU(self.bbmdAddress, pdu, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) + + # send it to the peers, all of them have all F's mask + for bdte in self.bbmdBDT: + if bdte != self.bbmdAddress: + xpdu.pduDestination = Address((bdte.addrIP, bdte.addrPort)) + BIPNAT._debug(" - sending to peer: %r", xpdu.pduDestination) + self.request(xpdu) + + # send it to the registered foreign devices + for fdte in self.bbmdFDT: + xpdu.pduDestination = fdte.fdAddress + if _debug: BIPNAT._debug(" - sending to foreign device: %r", xpdu.pduDestination) + self.request(xpdu) + + else: + BIPNAT._warning("invalid destination address: %r", pdu.pduDestination) + + def confirmation(self, pdu): + if _debug: BIPNAT._debug("confirmation %r", pdu) + + # some kind of response to a request + if isinstance(pdu, Result): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, WriteBroadcastDistributionTable): + ###TODO verify this is from a management network/address + + # build a response + xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduSource + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadBroadcastDistributionTable): + ###TODO verify this is from a management network/address + + # build a response + xpdu = ReadBroadcastDistributionTableAck(self.bbmdBDT, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduSource + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ForwardedNPDU): + ###TODO verify this is from a peer + + # build a PDU with the source from the real source + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) + + # send it upstream + self.response(xpdu) + + # build a forwarded NPDU to send out + xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) + + # send it to the registered foreign devices + for fdte in self.bbmdFDT: + xpdu.pduDestination = fdte.fdAddress + if _debug: BIPNAT._debug(" - sending to foreign device: %r", xpdu.pduDestination) + self.request(xpdu) + + elif isinstance(pdu, RegisterForeignDevice): + ###TODO verify this is from an acceptable address + + # process the request + stat = self.register_foreign_device(pdu.pduSource, pdu.bvlciTimeToLive) + + # build a response + xpdu = Result(code=stat, destination=pdu.pduSource, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadForeignDeviceTable): + ###TODO verify this is from a management network/address + + # build a response + xpdu = ReadForeignDeviceTableAck(self.bbmdFDT, destination=pdu.pduSource, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, DeleteForeignDeviceTableEntry): + ###TODO verify this is from a management network/address + + # process the request + stat = self.delete_foreign_device_table_entry(pdu.bvlciAddress) + + # build a response + xpdu = Result(code=stat, user_data=pdu.pduUserData) + xpdu.pduDestination = pdu.pduSource + if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) + + # send it downstream + self.request(xpdu) + + elif isinstance(pdu, DistributeBroadcastToNetwork): + ###TODO verify this is from a registered foreign device + + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) + + # send it upstream + self.response(xpdu) + + # build a forwarded NPDU to send out + xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) + + # send it to the peers + for bdte in self.bbmdBDT: + if bdte == self.bbmdAddress: + if _debug: BIPNAT._debug(" - no local broadcast") + else: + xpdu.pduDestination = Address((bdte.addrIP, bdte.addrPort)) + if _debug: BIPNAT._debug(" - sending to peer: %r", xpdu.pduDestination) + self.request(xpdu) + + # send it to the other registered foreign devices + for fdte in self.bbmdFDT: + if fdte.fdAddress != pdu.pduSource: + xpdu.pduDestination = fdte.fdAddress + if _debug: BIPNAT._debug(" - sending to foreign device: %r", xpdu.pduDestination) + self.request(xpdu) + + elif isinstance(pdu, OriginalUnicastNPDU): + ###TODO verify this is from a peer + + # build a vanilla PDU + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) + + # send it upstream + self.response(xpdu) + + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPNAT._debug(" - original broadcast dropped") + + else: + BIPNAT._warning("invalid pdu type: %s", type(pdu)) + + def register_foreign_device(self, addr, ttl): + """Add a foreign device to the FDT.""" + if _debug: BIPNAT._debug("register_foreign_device %r %r", addr, ttl) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation( addr ) + else: + raise TypeError("addr must be a string or an Address") + + for fdte in self.bbmdFDT: + if addr == fdte.fdAddress: + break + else: + fdte = FDTEntry() + fdte.fdAddress = addr + self.bbmdFDT.append( fdte ) + + fdte.fdTTL = ttl + fdte.fdRemain = ttl + 5 + + # return success + return 0 + + def delete_foreign_device_table_entry(self, addr): + if _debug: BIPNAT._debug("delete_foreign_device_table_entry %r", addr) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation( addr ) + else: + raise TypeError("addr must be a string or an Address") + + # find it and delete it + stat = 0 + for i in range(len(self.bbmdFDT)-1, -1, -1): + if addr == self.bbmdFDT[i].fdAddress: + del self.bbmdFDT[i] + break + else: + stat = 99 ### entry not found + + # return status + return stat + + def process_task(self): + # look for foreign device registrations that have expired + for i in range(len(self.bbmdFDT)-1, -1, -1): + fdte = self.bbmdFDT[i] + fdte.fdRemain -= 1 + + # delete it if it expired + if fdte.fdRemain <= 0: + if _debug: BIPNAT._debug("foreign device expired: %r", fdte.fdAddress) + del self.bbmdFDT[i] + + def add_peer(self, addr): + if _debug: BIPNAT._debug("add_peer %r", addr) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation(addr) + else: + raise TypeError("addr must be a string or an Address") + + # if it's this BBMD, make it the first one + if self.bbmdBDT and (addr == self.bbmdAddress): + raise RuntimeError("add self to BDT as first address") + + # see if it's already there + for bdte in self.bbmdBDT: + if addr == bdte: + break + else: + self.bbmdBDT.append(addr) + + def delete_peer(self, addr): + if _debug: BIPNAT._debug("delete_peer %r", addr) + + # see if it is an address or make it one + if isinstance(addr, Address): + pass + elif isinstance(addr, str): + addr = LocalStation(addr) + else: + raise TypeError("addr must be a string or an Address") + + # look for the peer address + for i in range(len(self.bbmdBDT)-1, -1, -1): + if addr == self.bbmdBDT[i]: + del self.bbmdBDT[i] + break + else: + pass + # # BVLLServiceElement # From 552660e9dba43218ebd5bc0a572bd71a4668dcbe Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 29 Mar 2018 00:12:37 -0400 Subject: [PATCH 04/50] sample NAT router (no peers) --- samples/NATRouter.py | 139 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100755 samples/NATRouter.py diff --git a/samples/NATRouter.py b/samples/NATRouter.py new file mode 100755 index 00000000..fa9b70c1 --- /dev/null +++ b/samples/NATRouter.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python + +""" +This sample application presents itself as a router between an "inside" network +that sits behind a NAT and a "global" network of other NAT router peers. + +$ python NATRouter.py addr1 port1 net1 addr2 port2 net2 + + addr1 - local address like 192.168.1.10/24 + port1 - local port + net1 - local network number + addr2 - global address like 201.1.1.1:47809 + port2 - local mapped port + net2 - global network number + +The sample addresses are like running BR1 from Figure J-8, Clause J.7.5. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run +from bacpypes.comm import bind + +from bacpypes.pdu import Address +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.bvllservice import BIPBBMD, BIPNAT, AnnexJCodec, UDPMultiplexer + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# NATRouter +# + +@bacpypes_debugging +class NATRouter: + + def __init__(self, addr1, port1, net1, addr2, port2, net2): + if _debug: NATRouter._debug("__init__ %r %r %r %r %r %r", addr1, port1, net1, addr2, port2, net2) + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + #== First stack + + # local address + local_addr = Address("{}:{}".format(addr1, port1)) + + # create a BBMD stack + self.s1_bip = BIPBBMD(local_addr) + self.s1_annexj = AnnexJCodec() + self.s1_mux = UDPMultiplexer(local_addr) + + # bind the bottom layers + bind(self.s1_bip, self.s1_annexj, self.s1_mux.annexJ) + + # bind the BIP stack to the local network + self.nsap.bind(self.s1_bip, net1, addr1) + + #== Second stack + + # global address + global_addr = Address(addr2) + nat_addr = Address("{}:{}".format(addr1, port2)) + + # create a NAT stack + self.s2_bip = BIPNAT(global_addr) + self.s2_annexj = AnnexJCodec() + self.s2_mux = UDPMultiplexer(nat_addr) + + # bind the bottom layers + bind(self.s2_bip, self.s2_annexj, self.s2_mux.annexJ) + + # bind the BIP stack to the global network + self.nsap.bind(self.s2_bip, net2) + +# +# __main__ +# + +def main(): + # parse the command line arguments + parser = ArgumentParser(description=__doc__) + + # add an argument for local address + parser.add_argument('addr1', type=str, + help='address of first network', + ) + + # add an argument for local port + parser.add_argument('port1', type=int, + help='port number of local network', + ) + + # add an argument for interval + parser.add_argument('net1', type=int, + help='network number of local network', + ) + + # add an argument for interval + parser.add_argument('addr2', type=str, + help='address of global network (outside NAT)', + ) + + # add an argument for local port + parser.add_argument('port2', type=int, + help='port number of global forwarded port', + ) + + # add an argument for interval + parser.add_argument('net2', type=int, + help='network number of global network', + ) + + # now parse the arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # create the router + router = NATRouter(args.addr1, args.port1, args.net1, args.addr2, args.port2, args.net2) + if _debug: _log.debug(" - router: %r", router) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() From a8336be47969f08afa1da0286fb948af35c82938 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 10 Apr 2018 10:10:44 -0400 Subject: [PATCH 05/50] stage ready for the next release --- py25/bacpypes/__init__.py | 2 +- py27/bacpypes/__init__.py | 2 +- py34/bacpypes/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index 70e98038..3adf9c1c 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.0' +__version__ = '0.17.1' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 70e98038..3adf9c1c 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.0' +__version__ = '0.17.1' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index a77e8be9..084ca281 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.0' +__version__ = '0.17.1' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' From 9483bbda9646db086307f90497c0e23afae4aa57 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Sat, 21 Apr 2018 23:06:59 -0400 Subject: [PATCH 06/50] missed updating a sample application --- samples/COVServer.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/samples/COVServer.py b/samples/COVServer.py index ad2558a4..99782969 100755 --- a/samples/COVServer.py +++ b/samples/COVServer.py @@ -402,13 +402,6 @@ def main(): # add it to the device test_application.add_object(test_bv) - # get the services supported - services_supported = test_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - test_device.protocolServicesSupported = services_supported.value - # make a console if args.console: test_console = COVConsoleCmd() From 92e851416a3066c9d0e52e3df4e59c4e107a39fa Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Sat, 21 Apr 2018 23:07:55 -0400 Subject: [PATCH 07/50] additional sample application for Marco on gitter --- samples/COVClientApp.py | 195 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100755 samples/COVClientApp.py diff --git a/samples/COVClientApp.py b/samples/COVClientApp.py new file mode 100755 index 00000000..b2274c93 --- /dev/null +++ b/samples/COVClientApp.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python + +""" +Configured with a subscription context object which is passed to the +application, it sends a SubscribeCOVRequest and listens for confirmed or +unconfirmed COV notifications, lines them up with the context, and passes the +APDU to the context to print out. + +Making multiple subscription contexts and keeping them active based on their +lifetime is left as an exercise for the reader. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run, deferred +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.apdu import SubscribeCOVRequest, SimpleAckPDU +from bacpypes.errors import ExecutionError + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None + +subscription_contexts = {} +next_proc_id = 1 + +# +# SubscriptionContext +# + +@bacpypes_debugging +class SubscriptionContext: + + def __init__(self, address, objid, confirmed=None, lifetime=None): + if _debug: SubscriptionContext._debug("__init__ %r %r confirmed=%r lifetime=%r", address, objid, confirmed, lifetime) + global subscription_contexts, next_proc_id + + # destination for subscription requests + self.address = address + + # assign a unique process identifer and keep track of it + self.subscriberProcessIdentifier = next_proc_id + next_proc_id += 1 + subscription_contexts[self.subscriberProcessIdentifier] = self + + self.monitoredObjectIdentifier = objid + self.issueConfirmedNotifications = confirmed + self.lifetime = lifetime + + def cov_notification(self, apdu): + if _debug: SubscriptionContext._debug("cov_notification %r", apdu) + + # make a rash assumption that the property value is going to be + # a single application encoded tag + print("{} {} changed\n {}".format( + apdu.pduSource, + apdu.monitoredObjectIdentifier, + ",\n ".join("{} = {}".format( + element.propertyIdentifier, + str(element.value.tagList[0].app_to_object().value), + ) for element in apdu.listOfValues), + )) + +# +# SubscribeCOVApplication +# + +@bacpypes_debugging +class SubscribeCOVApplication(BIPSimpleApplication): + + def __init__(self, *args): + if _debug: SubscribeCOVApplication._debug("__init__ %r", args) + BIPSimpleApplication.__init__(self, *args) + + def send_subscription(self, context): + if _debug: SubscribeCOVApplication._debug("send_subscription %r", context) + + # build a request + request = SubscribeCOVRequest( + subscriberProcessIdentifier=context.subscriberProcessIdentifier, + monitoredObjectIdentifier=context.monitoredObjectIdentifier, + ) + request.pduDestination = context.address + + # optional parameters + if context.issueConfirmedNotifications is not None: + request.issueConfirmedNotifications = context.issueConfirmedNotifications + if context.lifetime is not None: + request.lifetime = context.lifetime + + # make an IOCB + iocb = IOCB(request) + if _debug: SubscribeCOVApplication._debug(" - iocb: %r", iocb) + + # callback when it is acknowledged + iocb.add_callback(self.subscription_acknowledged) + + # give it to the application + this_application.request_io(iocb) + + def subscription_acknowledged(self, iocb): + if _debug: SubscribeCOVApplication._debug("subscription_acknowledged %r", iocb) + + # do something for success + if iocb.ioResponse: + if _debug: SubscribeCOVApplication._debug(" - response: %r", iocb.ioResponse) + + # do something for error/reject/abort + if iocb.ioError: + if _debug: SubscribeCOVApplication._debug(" - error: %r", iocb.ioError) + + def cov_notification(self, apdu): + if _debug: SubscribeCOVApplication._debug("cov_notification %r", apdu) + + def do_ConfirmedCOVNotificationRequest(self, apdu): + if _debug: SubscribeCOVApplication._debug("do_ConfirmedCOVNotificationRequest %r", apdu) + + # look up the process identifier + context = subscription_contexts.get(apdu.subscriberProcessIdentifier, None) + if not context or apdu.pduSource != context.address: + if _debug: SubscribeCOVApplication._debug(" - no context") + + # this is turned into an ErrorPDU and sent back to the client + raise ExecutionError('services', 'unknownSubscription') + + # now tell the context object + context.cov_notification(apdu) + + # success + response = SimpleAckPDU(context=apdu) + if _debug: SubscribeCOVApplication._debug(" - simple_ack: %r", response) + + # return the result + self.response(response) + + def do_UnconfirmedCOVNotificationRequest(self, apdu): + if _debug: SubscribeCOVApplication._debug("do_UnconfirmedCOVNotificationRequest %r", apdu) + + # look up the process identifier + context = subscription_contexts.get(apdu.subscriberProcessIdentifier, None) + if not context or apdu.pduSource != context.address: + if _debug: SubscribeCOVApplication._debug(" - no context") + return + + # now tell the context object + context.cov_notification(apdu) + +# +# __main__ +# + +def main(): + global this_application + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject( + objectName=args.ini.objectname, + objectIdentifier=int(args.ini.objectidentifier), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # make a simple application + this_application = SubscribeCOVApplication(this_device, args.ini.address) + + # make a subscription context + context = SubscriptionContext(Address("10.0.1.31"), ('analogValue', 1), False, 60) + + # send the subscription when the stack is ready + deferred(this_application.send_subscription, context) + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() From a879abc5405c982e73757e68e5241d7cd51ad9d2 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Sat, 21 Apr 2018 23:10:30 -0400 Subject: [PATCH 08/50] obsolete function definition --- samples/COVClientApp.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/samples/COVClientApp.py b/samples/COVClientApp.py index b2274c93..3709b80f 100755 --- a/samples/COVClientApp.py +++ b/samples/COVClientApp.py @@ -118,9 +118,6 @@ def subscription_acknowledged(self, iocb): if iocb.ioError: if _debug: SubscribeCOVApplication._debug(" - error: %r", iocb.ioError) - def cov_notification(self, apdu): - if _debug: SubscribeCOVApplication._debug("cov_notification %r", apdu) - def do_ConfirmedCOVNotificationRequest(self, apdu): if _debug: SubscribeCOVApplication._debug("do_ConfirmedCOVNotificationRequest %r", apdu) From 68bcfc869d62071bb5f46e66a510116948bb87bf Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 3 May 2018 22:14:03 -0400 Subject: [PATCH 09/50] apply patch #185 incorrect optional parameters of LifeSafetyOperationRequest and RemoveListElementRequest --- py25/bacpypes/apdu.py | 4 ++-- py27/bacpypes/apdu.py | 4 ++-- py34/bacpypes/apdu.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index ae3659cf..3aa29f28 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -1307,7 +1307,7 @@ class LifeSafetyOperationRequest(ConfirmedRequestSequence): [ Element('requestingProcessIdentifier', Unsigned, 0) , Element('requestingSource', CharacterString, 1) , Element('request', LifeSafetyOperation, 2) - , Element('objectIdentifier', ObjectIdentifier, 3) + , Element('objectIdentifier', ObjectIdentifier, 3, True) ] register_confirmed_request_type(LifeSafetyOperationRequest) @@ -1491,7 +1491,7 @@ class RemoveListElementRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('objectIdentifier', ObjectIdentifier, 0) , Element('propertyIdentifier', PropertyIdentifier, 1) - , Element('propertyArrayIndex', Unsigned, 2) + , Element('propertyArrayIndex', Unsigned, 2, True) , Element('listOfElements', Any, 3) ] diff --git a/py27/bacpypes/apdu.py b/py27/bacpypes/apdu.py index 10706aa2..b0fd8e8f 100755 --- a/py27/bacpypes/apdu.py +++ b/py27/bacpypes/apdu.py @@ -1301,7 +1301,7 @@ class LifeSafetyOperationRequest(ConfirmedRequestSequence): [ Element('requestingProcessIdentifier', Unsigned, 0) , Element('requestingSource', CharacterString, 1) , Element('request', LifeSafetyOperation, 2) - , Element('objectIdentifier', ObjectIdentifier, 3) + , Element('objectIdentifier', ObjectIdentifier, 3, True) ] register_confirmed_request_type(LifeSafetyOperationRequest) @@ -1485,7 +1485,7 @@ class RemoveListElementRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('objectIdentifier', ObjectIdentifier, 0) , Element('propertyIdentifier', PropertyIdentifier, 1) - , Element('propertyArrayIndex', Unsigned, 2) + , Element('propertyArrayIndex', Unsigned, 2, True) , Element('listOfElements', Any, 3) ] diff --git a/py34/bacpypes/apdu.py b/py34/bacpypes/apdu.py index 10706aa2..b0fd8e8f 100755 --- a/py34/bacpypes/apdu.py +++ b/py34/bacpypes/apdu.py @@ -1301,7 +1301,7 @@ class LifeSafetyOperationRequest(ConfirmedRequestSequence): [ Element('requestingProcessIdentifier', Unsigned, 0) , Element('requestingSource', CharacterString, 1) , Element('request', LifeSafetyOperation, 2) - , Element('objectIdentifier', ObjectIdentifier, 3) + , Element('objectIdentifier', ObjectIdentifier, 3, True) ] register_confirmed_request_type(LifeSafetyOperationRequest) @@ -1485,7 +1485,7 @@ class RemoveListElementRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('objectIdentifier', ObjectIdentifier, 0) , Element('propertyIdentifier', PropertyIdentifier, 1) - , Element('propertyArrayIndex', Unsigned, 2) + , Element('propertyArrayIndex', Unsigned, 2, True) , Element('listOfElements', Any, 3) ] From 3ee0cbd0dd24806871d6c1da18fdcd0d707cde24 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 3 May 2018 22:26:50 -0400 Subject: [PATCH 10/50] merge #181 with additional tests --- py25/bacpypes/bvllservice.py | 122 ++++++------ py25/bacpypes/vlan.py | 9 +- py27/bacpypes/bvllservice.py | 123 ++++++------ py27/bacpypes/vlan.py | 9 +- py34/bacpypes/bvllservice.py | 122 ++++++------ py34/bacpypes/vlan.py | 9 +- samples/NATRouter.py | 139 ++++++++++++++ tests/test_bvll/helpers.py | 294 ++++++++++++++++++++++++++--- tests/test_bvll/test_bbmd.py | 303 +++++++++++++++++++++++++++++- tests/test_bvll/test_foreign.py | 175 +++++++++-------- tests/test_bvll/test_simple.py | 27 ++- tests/test_network/helpers.py | 32 ++-- tests/test_network/test_net_1.py | 10 +- tests/test_network/test_net_2.py | 10 +- tests/test_network/test_net_3.py | 8 +- tests/test_vlan/test_ipnetwork.py | 24 +++ 16 files changed, 1089 insertions(+), 327 deletions(-) create mode 100644 samples/NATRouter.py diff --git a/py25/bacpypes/bvllservice.py b/py25/bacpypes/bvllservice.py index 65ae6143..ecfb5014 100755 --- a/py25/bacpypes/bvllservice.py +++ b/py25/bacpypes/bvllservice.py @@ -546,33 +546,38 @@ def confirmation(self, pdu): return - elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - - # send it upstream - self.response(xpdu) - # check the BBMD registration status, we may not be registered if self.registrationStatus != 0: if _debug: BIPForeign._debug(" - packet dropped, unregistered") return - if isinstance(pdu, ReadBroadcastDistributionTableAck): - # send this to the service access point - self.sap_response(pdu) + if isinstance(pdu, OriginalUnicastNPDU): + # build a vanilla PDU + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - elif isinstance(pdu, ReadForeignDeviceTableAck): - # send this to the service access point - self.sap_response(pdu) + # send it upstream + self.response(xpdu) elif isinstance(pdu, ForwardedNPDU): + # make sure the forwarded PDU from the bbmd + if pdu.pduSource != self.bbmdAddress: + if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") + return + # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) # send it upstream self.response(xpdu) + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response xpdu = Result(code=0x0010, user_data=pdu.pduUserData) @@ -696,7 +701,7 @@ def indication(self, pdu): # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduDestination - if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) + if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -719,13 +724,13 @@ def indication(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -741,8 +746,9 @@ def confirmation(self, pdu): elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response - xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu = Result(code=0x0010, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduSource + if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -761,27 +767,38 @@ def confirmation(self, pdu): self.sap_response(pdu) elif isinstance(pdu, ForwardedNPDU): - # build a PDU with the source from the real source - xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) - # look for self as first entry in the BDT - if self.bbmdBDT and (self.bbmdBDT[0] == self.bbmdAddress): - xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") - self.request(xpdu) + # if this was unicast to us, do next hop + if pdu.pduDestination.addrType == Address.localStationAddr: + if _debug: BIPBBMD._debug(" - unicast message") + + # if this BBMD is listed in its BDT, send a local broadcast + if self.bbmdAddress in self.bbmdBDT: + xpdu.pduDestination = LocalBroadcast() + if _debug: BIPBBMD._debug(" - local broadcast") + self.request(xpdu) + + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + if _debug: BIPBBMD._debug(" - directed broadcast message") + + else: + BIPBBMD._warning("invalid destination address: %r", pdu.pduDestination) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, RegisterForeignDevice): @@ -820,12 +837,13 @@ def confirmation(self, pdu): self.request(xpdu) elif isinstance(pdu, DistributeBroadcastToNetwork): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -835,35 +853,37 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte == self.bbmdAddress: xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") + if _debug: BIPBBMD._debug(" - local broadcast") self.request(xpdu) else: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the other registered foreign devices for fdte in self.bbmdFDT: if fdte.fdAddress != pdu.pduSource: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) elif isinstance(pdu, OriginalBroadcastNPDU): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # make a forwarded PDU xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -873,13 +893,13 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -929,7 +949,7 @@ def delete_foreign_device_table_entry(self, addr): del self.bbmdFDT[i] break else: - stat = 99 ### entry not found + stat = 0x0050 ### entry not found # return status return stat @@ -956,10 +976,6 @@ def add_peer(self, addr): else: raise TypeError("addr must be a string or an Address") - # if it's this BBMD, make it the first one - if self.bbmdBDT and (addr == self.bbmdAddress): - raise RuntimeError("add self to BDT as first address") - # see if it's already there for bdte in self.bbmdBDT: if addr == bdte: diff --git a/py25/bacpypes/vlan.py b/py25/bacpypes/vlan.py index 4d716964..e8e9fc29 100755 --- a/py25/bacpypes/vlan.py +++ b/py25/bacpypes/vlan.py @@ -161,9 +161,9 @@ class IPNetwork(Network): ('1.2.3.255', 5) and the other nodes must have the same tuple. """ - def __init__(self): + def __init__(self, name=''): if _debug: IPNetwork._debug("__init__") - Network.__init__(self) + Network.__init__(self, name=name) def add_node(self, node): if _debug: IPNetwork._debug("add_node %r", node) @@ -213,7 +213,7 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): class IPRouterNode(Client): - def __init__(self, router, addr, lan=None): + def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) # save the reference to the router @@ -238,6 +238,9 @@ def process_pdu(self, pdu): # pass it downstream self.request(pdu) + def __repr__(self): + return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + bacpypes_debugging(IPRouterNode) # diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index 4c2cc182..f3d4276c 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -542,34 +542,38 @@ def confirmation(self, pdu): return - elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - - # send it upstream - self.response(xpdu) - return - # check the BBMD registration status, we may not be registered if self.registrationStatus != 0: if _debug: BIPForeign._debug(" - packet dropped, unregistered") return - if isinstance(pdu, ReadBroadcastDistributionTableAck): - # send this to the service access point - self.sap_response(pdu) + if isinstance(pdu, OriginalUnicastNPDU): + # build a vanilla PDU + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - elif isinstance(pdu, ReadForeignDeviceTableAck): - # send this to the service access point - self.sap_response(pdu) + # send it upstream + self.response(xpdu) elif isinstance(pdu, ForwardedNPDU): + # make sure the forwarded PDU from the bbmd + if pdu.pduSource != self.bbmdAddress: + if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") + return + # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) # send it upstream self.response(xpdu) + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response xpdu = Result(code=0x0010, user_data=pdu.pduUserData) @@ -692,7 +696,7 @@ def indication(self, pdu): # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduDestination - if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) + if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -715,13 +719,13 @@ def indication(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -737,8 +741,9 @@ def confirmation(self, pdu): elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response - xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu = Result(code=0x0010, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduSource + if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -757,27 +762,38 @@ def confirmation(self, pdu): self.sap_response(pdu) elif isinstance(pdu, ForwardedNPDU): - # build a PDU with the source from the real source - xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) - # look for self as first entry in the BDT - if self.bbmdBDT and (self.bbmdBDT[0] == self.bbmdAddress): - xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") - self.request(xpdu) + # if this was unicast to us, do next hop + if pdu.pduDestination.addrType == Address.localStationAddr: + if _debug: BIPBBMD._debug(" - unicast message") + + # if this BBMD is listed in its BDT, send a local broadcast + if self.bbmdAddress in self.bbmdBDT: + xpdu.pduDestination = LocalBroadcast() + if _debug: BIPBBMD._debug(" - local broadcast") + self.request(xpdu) + + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + if _debug: BIPBBMD._debug(" - directed broadcast message") + + else: + BIPBBMD._warning("invalid destination address: %r", pdu.pduDestination) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, RegisterForeignDevice): @@ -816,12 +832,13 @@ def confirmation(self, pdu): self.request(xpdu) elif isinstance(pdu, DistributeBroadcastToNetwork): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -831,35 +848,37 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte == self.bbmdAddress: xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") + if _debug: BIPBBMD._debug(" - local broadcast") self.request(xpdu) else: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the other registered foreign devices for fdte in self.bbmdFDT: if fdte.fdAddress != pdu.pduSource: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) elif isinstance(pdu, OriginalBroadcastNPDU): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # make a forwarded PDU xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -869,13 +888,13 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -925,7 +944,7 @@ def delete_foreign_device_table_entry(self, addr): del self.bbmdFDT[i] break else: - stat = 99 ### entry not found + stat = 0x0050 ### entry not found # return status return stat @@ -952,10 +971,6 @@ def add_peer(self, addr): else: raise TypeError("addr must be a string or an Address") - # if it's this BBMD, make it the first one - if self.bbmdBDT and (addr == self.bbmdAddress): - raise RuntimeError("add self to BDT as first address") - # see if it's already there for bdte in self.bbmdBDT: if addr == bdte: diff --git a/py27/bacpypes/vlan.py b/py27/bacpypes/vlan.py index ead2d03a..ea839239 100755 --- a/py27/bacpypes/vlan.py +++ b/py27/bacpypes/vlan.py @@ -161,9 +161,9 @@ class IPNetwork(Network): ('1.2.3.255', 5) and the other nodes must have the same tuple. """ - def __init__(self): + def __init__(self, name=''): if _debug: IPNetwork._debug("__init__") - Network.__init__(self) + Network.__init__(self, name=name) def add_node(self, node): if _debug: IPNetwork._debug("add_node %r", node) @@ -213,7 +213,7 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): @bacpypes_debugging class IPRouterNode(Client): - def __init__(self, router, addr, lan=None): + def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) # save the reference to the router @@ -238,6 +238,9 @@ def process_pdu(self, pdu): # pass it downstream self.request(pdu) + def __repr__(self): + return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + # # IPRouter # diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index dddb2d01..c3823879 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -541,33 +541,38 @@ def confirmation(self, pdu): return - elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - - # send it upstream - self.response(xpdu) - # check the BBMD registration status, we may not be registered if self.registrationStatus != 0: if _debug: BIPForeign._debug(" - packet dropped, unregistered") return - if isinstance(pdu, ReadBroadcastDistributionTableAck): - # send this to the service access point - self.sap_response(pdu) + if isinstance(pdu, OriginalUnicastNPDU): + # build a vanilla PDU + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - elif isinstance(pdu, ReadForeignDeviceTableAck): - # send this to the service access point - self.sap_response(pdu) + # send it upstream + self.response(xpdu) elif isinstance(pdu, ForwardedNPDU): + # make sure the forwarded PDU from the bbmd + if pdu.pduSource != self.bbmdAddress: + if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") + return + # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) # send it upstream self.response(xpdu) + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response xpdu = Result(code=0x0010, user_data=pdu.pduUserData) @@ -690,7 +695,7 @@ def indication(self, pdu): # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduDestination - if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) + if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -713,13 +718,13 @@ def indication(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -735,8 +740,9 @@ def confirmation(self, pdu): elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response - xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu = Result(code=0x0010, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduSource + if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -755,27 +761,38 @@ def confirmation(self, pdu): self.sap_response(pdu) elif isinstance(pdu, ForwardedNPDU): - # build a PDU with the source from the real source - xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) - # look for self as first entry in the BDT - if self.bbmdBDT and (self.bbmdBDT[0] == self.bbmdAddress): - xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") - self.request(xpdu) + # if this was unicast to us, do next hop + if pdu.pduDestination.addrType == Address.localStationAddr: + if _debug: BIPBBMD._debug(" - unicast message") + + # if this BBMD is listed in its BDT, send a local broadcast + if self.bbmdAddress in self.bbmdBDT: + xpdu.pduDestination = LocalBroadcast() + if _debug: BIPBBMD._debug(" - local broadcast") + self.request(xpdu) + + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + if _debug: BIPBBMD._debug(" - directed broadcast message") + + else: + BIPBBMD._warning("invalid destination address: %r", pdu.pduDestination) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, RegisterForeignDevice): @@ -814,12 +831,13 @@ def confirmation(self, pdu): self.request(xpdu) elif isinstance(pdu, DistributeBroadcastToNetwork): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -829,35 +847,37 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte == self.bbmdAddress: xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") + if _debug: BIPBBMD._debug(" - local broadcast") self.request(xpdu) else: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the other registered foreign devices for fdte in self.bbmdFDT: if fdte.fdAddress != pdu.pduSource: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) elif isinstance(pdu, OriginalBroadcastNPDU): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # make a forwarded PDU xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -867,13 +887,13 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -923,7 +943,7 @@ def delete_foreign_device_table_entry(self, addr): del self.bbmdFDT[i] break else: - stat = 99 ### entry not found + stat = 0x0050 ### entry not found # return status return stat @@ -950,10 +970,6 @@ def add_peer(self, addr): else: raise TypeError("addr must be a string or an Address") - # if it's this BBMD, make it the first one - if self.bbmdBDT and (addr == self.bbmdAddress): - raise RuntimeError("add self to BDT as first address") - # see if it's already there for bdte in self.bbmdBDT: if addr == bdte: diff --git a/py34/bacpypes/vlan.py b/py34/bacpypes/vlan.py index ead2d03a..ea839239 100755 --- a/py34/bacpypes/vlan.py +++ b/py34/bacpypes/vlan.py @@ -161,9 +161,9 @@ class IPNetwork(Network): ('1.2.3.255', 5) and the other nodes must have the same tuple. """ - def __init__(self): + def __init__(self, name=''): if _debug: IPNetwork._debug("__init__") - Network.__init__(self) + Network.__init__(self, name=name) def add_node(self, node): if _debug: IPNetwork._debug("add_node %r", node) @@ -213,7 +213,7 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): @bacpypes_debugging class IPRouterNode(Client): - def __init__(self, router, addr, lan=None): + def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) # save the reference to the router @@ -238,6 +238,9 @@ def process_pdu(self, pdu): # pass it downstream self.request(pdu) + def __repr__(self): + return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + # # IPRouter # diff --git a/samples/NATRouter.py b/samples/NATRouter.py new file mode 100644 index 00000000..fa9b70c1 --- /dev/null +++ b/samples/NATRouter.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python + +""" +This sample application presents itself as a router between an "inside" network +that sits behind a NAT and a "global" network of other NAT router peers. + +$ python NATRouter.py addr1 port1 net1 addr2 port2 net2 + + addr1 - local address like 192.168.1.10/24 + port1 - local port + net1 - local network number + addr2 - global address like 201.1.1.1:47809 + port2 - local mapped port + net2 - global network number + +The sample addresses are like running BR1 from Figure J-8, Clause J.7.5. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run +from bacpypes.comm import bind + +from bacpypes.pdu import Address +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.bvllservice import BIPBBMD, BIPNAT, AnnexJCodec, UDPMultiplexer + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# NATRouter +# + +@bacpypes_debugging +class NATRouter: + + def __init__(self, addr1, port1, net1, addr2, port2, net2): + if _debug: NATRouter._debug("__init__ %r %r %r %r %r %r", addr1, port1, net1, addr2, port2, net2) + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + #== First stack + + # local address + local_addr = Address("{}:{}".format(addr1, port1)) + + # create a BBMD stack + self.s1_bip = BIPBBMD(local_addr) + self.s1_annexj = AnnexJCodec() + self.s1_mux = UDPMultiplexer(local_addr) + + # bind the bottom layers + bind(self.s1_bip, self.s1_annexj, self.s1_mux.annexJ) + + # bind the BIP stack to the local network + self.nsap.bind(self.s1_bip, net1, addr1) + + #== Second stack + + # global address + global_addr = Address(addr2) + nat_addr = Address("{}:{}".format(addr1, port2)) + + # create a NAT stack + self.s2_bip = BIPNAT(global_addr) + self.s2_annexj = AnnexJCodec() + self.s2_mux = UDPMultiplexer(nat_addr) + + # bind the bottom layers + bind(self.s2_bip, self.s2_annexj, self.s2_mux.annexJ) + + # bind the BIP stack to the global network + self.nsap.bind(self.s2_bip, net2) + +# +# __main__ +# + +def main(): + # parse the command line arguments + parser = ArgumentParser(description=__doc__) + + # add an argument for local address + parser.add_argument('addr1', type=str, + help='address of first network', + ) + + # add an argument for local port + parser.add_argument('port1', type=int, + help='port number of local network', + ) + + # add an argument for interval + parser.add_argument('net1', type=int, + help='network number of local network', + ) + + # add an argument for interval + parser.add_argument('addr2', type=str, + help='address of global network (outside NAT)', + ) + + # add an argument for local port + parser.add_argument('port2', type=int, + help='port number of global forwarded port', + ) + + # add an argument for interval + parser.add_argument('net2', type=int, + help='network number of global network', + ) + + # now parse the arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # create the router + router = NATRouter(args.addr1, args.port1, args.net1, args.addr2, args.port2, args.net2) + if _debug: _log.debug(" - router: %r", router) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/tests/test_bvll/helpers.py b/tests/test_bvll/helpers.py index 5146319f..c09ee505 100644 --- a/tests/test_bvll/helpers.py +++ b/tests/test_bvll/helpers.py @@ -6,10 +6,19 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger -from bacpypes.comm import Client, Server, bind +from bacpypes.comm import Client, Server, ApplicationServiceElement, bind from bacpypes.pdu import Address, LocalBroadcast, PDU, unpack_ip_addr from bacpypes.vlan import IPNode +from bacpypes.app import DeviceInfoCache, Application +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement + +from bacpypes.object import register_object_type +from bacpypes.local.device import LocalDeviceObject +from bacpypes.service.device import WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices + from ..state_machine import ClientStateMachine from bacpypes.bvllservice import BIPSimple, BIPForeign, BIPBBMD, AnnexJCodec @@ -26,6 +35,12 @@ @bacpypes_debugging class FauxMultiplexer(Client, Server): + """This class is a placeholder for UDPMultiplexer without the code that + determines if the upstream packets are Annex-H or Annex-J packets, it + assumes they are all Annex-J. It creates and binds itself to an IPNode + which is added to an IPNetwork. + """ + def __init__(self, addr, network=None, cid=None, sid=None): if _debug: FauxMultiplexer._debug("__init__") @@ -81,37 +96,54 @@ def confirmation(self, pdu): self.response(PDU(pdu, source=src, destination=dest)) # -# SnifferNode +# SnifferStateMachine # @bacpypes_debugging -class SnifferNode(ClientStateMachine): +class SnifferStateMachine(ClientStateMachine): + + """This class acts as a sniffer for BVLL messages. The client state + machine sits above an Annex-J codec so the send and receive PDUs are + BVLL PDUs. + """ def __init__(self, address, vlan): - if _debug: SnifferNode._debug("__init__ %r %r", address, vlan) + if _debug: SnifferStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address self.name = address self.address = Address(address) - # create a promiscuous node, added to the network - self.node = IPNode(self.address, vlan, promiscuous=True) - if _debug: SnifferNode._debug(" - node: %r", self.node) + # BACnet/IP interpreter + self.annexj = AnnexJCodec() - # bind this to the node - bind(self, self.node) + # fake multiplexer has a VLAN node in it + self.mux = FauxMultiplexer(self.address, vlan) + + # might receive all packets and allow spoofing + self.mux.node.promiscuous = True + self.mux.node.spoofing = True + + # bind the stack together + bind(self, self.annexj, self.mux) # -# CodecNode +# BIPStateMachine # @bacpypes_debugging -class CodecNode(ClientStateMachine): +class BIPStateMachine(ClientStateMachine): + + """This class is an application layer for BVLL messages that has no BVLL + processing like the 'simple', 'foreign', or 'bbmd' versions. The client + state machine sits above and Annex-J codec so the send and receive PDUs are + BVLL PDUs. + """ def __init__(self, address, vlan): - if _debug: CodecNode._debug("__init__ %r %r", address, vlan) + if _debug: BIPStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -129,14 +161,18 @@ def __init__(self, address, vlan): # -# SimpleNode +# BIPSimpleStateMachine # @bacpypes_debugging -class SimpleNode(ClientStateMachine): +class BIPSimpleStateMachine(ClientStateMachine): + + """This class sits on a BIPSimple instance, the send() and receive() + parameters are NPDUs. + """ def __init__(self, address, vlan): - if _debug: SimpleNode._debug("__init__ %r %r", address, vlan) + if _debug: BIPSimpleStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -155,14 +191,18 @@ def __init__(self, address, vlan): # -# ForeignNode +# BIPForeignStateMachine # @bacpypes_debugging -class ForeignNode(ClientStateMachine): +class BIPForeignStateMachine(ClientStateMachine): + + """This class sits on a BIPForeign instance, the send() and receive() + parameters are NPDUs. + """ def __init__(self, address, vlan): - if _debug: ForeignNode._debug("__init__ %r %r", address, vlan) + if _debug: BIPForeignStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -180,14 +220,18 @@ def __init__(self, address, vlan): bind(self, self.bip, self.annexj, self.mux) # -# BBMDNode +# BIPBBMDStateMachine # @bacpypes_debugging -class BBMDNode(ClientStateMachine): +class BIPBBMDStateMachine(ClientStateMachine): + + """This class sits on a BIPBBMD instance, the send() and receive() + parameters are NPDUs. + """ def __init__(self, address, vlan): - if _debug: BBMDNode._debug("__init__ %r %r", address, vlan) + if _debug: BIPBBMDStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -200,7 +244,7 @@ def __init__(self, address, vlan): # build an address, full mask bdt_address = "%s/32:%d" % self.address.addrTuple - if _debug: BBMDNode._debug(" - bdt_address: %r", bdt_address) + if _debug: BIPBBMDStateMachine._debug(" - bdt_address: %r", bdt_address) # add itself as the first entry in the BDT self.bip.add_peer(Address(bdt_address)) @@ -211,3 +255,209 @@ def __init__(self, address, vlan): # bind the stack together bind(self, self.bip, self.annexj, self.mux) +# +# BIPSimpleNode +# + +@bacpypes_debugging +class BIPSimpleNode: + + """This class is a BIPSimple instance that is not bound to a state machine.""" + + def __init__(self, address, vlan): + if _debug: BIPSimpleNode._debug("__init__ %r %r", address, vlan) + + # save the name and address + self.name = address + self.address = Address(address) + + # BACnet/IP interpreter + self.bip = BIPSimple() + self.annexj = AnnexJCodec() + + # fake multiplexer has a VLAN node in it + self.mux = FauxMultiplexer(self.address, vlan) + + # bind the stack together + bind(self.bip, self.annexj, self.mux) + +# +# BIPBBMDNode +# + +@bacpypes_debugging +class BIPBBMDNode: + + """This class is a BIPBBMD instance that is not bound to a state machine.""" + + def __init__(self, address, vlan): + if _debug: BIPBBMDNode._debug("__init__ %r %r", address, vlan) + + # save the name and address + self.name = address + self.address = Address(address) + if _debug: BIPBBMDNode._debug(" - address: %r", self.address) + + # BACnet/IP interpreter + self.bip = BIPBBMD(self.address) + self.annexj = AnnexJCodec() + + # build an address, full mask + bdt_address = "%s/32:%d" % self.address.addrTuple + if _debug: BIPBBMDNode._debug(" - bdt_address: %r", bdt_address) + + # add itself as the first entry in the BDT + self.bip.add_peer(Address(bdt_address)) + + # fake multiplexer has a VLAN node in it + self.mux = FauxMultiplexer(self.address, vlan) + + # bind the stack together + bind(self.bip, self.annexj, self.mux) + + +# +# TestDeviceObject +# + +@register_object_type(vendor_id=999) +class TestDeviceObject(LocalDeviceObject): + + pass + +# +# BIPSimpleApplicationLayerStateMachine +# + +@bacpypes_debugging +class BIPSimpleApplicationLayerStateMachine(ApplicationServiceElement, ClientStateMachine): + + def __init__(self, address, vlan): + if _debug: BIPSimpleApplicationLayerStateMachine._debug("__init__ %r %r", address, vlan) + + # build a name, save the address + self.name = "app @ %s" % (address,) + self.address = Address(address) + + # build a local device object + local_device = TestDeviceObject( + objectName=self.name, + objectIdentifier=('device', 998), + vendorIdentifier=999, + ) + + # build an address and save it + self.address = Address(address) + if _debug: BIPSimpleApplicationLayerStateMachine._debug(" - address: %r", self.address) + + # continue with initialization + ApplicationServiceElement.__init__(self) + ClientStateMachine.__init__(self, name=local_device.objectName) + + # include a application decoder + self.asap = ApplicationServiceAccessPoint() + + # pass the device object to the state machine access point so it + # can know if it should support segmentation + self.smap = StateMachineAccessPoint(local_device) + + # the segmentation state machines need access to some device + # information cache, usually shared with the application + self.smap.deviceInfoCache = DeviceInfoCache() + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # BACnet/IP interpreter + self.bip = BIPSimple() + self.annexj = AnnexJCodec() + + # fake multiplexer has a VLAN node in it + self.mux = FauxMultiplexer(self.address, vlan) + + # bind the stack together + bind(self.bip, self.annexj, self.mux) + + # bind the stack to the local network + self.nsap.bind(self.bip) + + def indication(self, apdu): + if _debug: BIPSimpleApplicationLayerStateMachine._debug("indication %r", apdu) + self.receive(apdu) + + def confirmation(self, apdu): + if _debug: BIPSimpleApplicationLayerStateMachine._debug("confirmation %r %r", apdu) + self.receive(apdu) + +# +# BIPBBMDApplication +# + +class BIPBBMDApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): + + def __init__(self, address, vlan): + if _debug: BIPBBMDApplication._debug("__init__ %r %r", address, vlan) + + # build a name, save the address + self.name = "app @ %s" % (address,) + self.address = Address(address) + if _debug: BIPBBMDApplication._debug(" - address: %r", self.address) + + # build a local device object + local_device = TestDeviceObject( + objectName=self.name, + objectIdentifier=('device', 999), + vendorIdentifier=999, + ) + + # continue with initialization + Application.__init__(self, local_device, self.address) + + # include a application decoder + self.asap = ApplicationServiceAccessPoint() + + # pass the device object to the state machine access point so it + # can know if it should support segmentation + self.smap = StateMachineAccessPoint(local_device) + + # the segmentation state machines need access to the same device + # information cache as the application + self.smap.deviceInfoCache = self.deviceInfoCache + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # BACnet/IP interpreter + self.bip = BIPBBMD(self.address) + self.annexj = AnnexJCodec() + + # build an address, full mask + bdt_address = "%s/32:%d" % self.address.addrTuple + if _debug: BIPBBMDNode._debug(" - bdt_address: %r", bdt_address) + + # add itself as the first entry in the BDT + self.bip.add_peer(Address(bdt_address)) + + # fake multiplexer has a VLAN node in it + self.mux = FauxMultiplexer(self.address, vlan) + + # bind the stack together + bind(self.bip, self.annexj, self.mux) + + # bind the stack to the local network + self.nsap.bind(self.bip) + diff --git a/tests/test_bvll/test_bbmd.py b/tests/test_bvll/test_bbmd.py index fdffa2a0..d78798af 100644 --- a/tests/test_bvll/test_bbmd.py +++ b/tests/test_bvll/test_bbmd.py @@ -1 +1,302 @@ -# placeholder +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test BBMD +--------- +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.pdu import Address, PDU, LocalBroadcast +from bacpypes.vlan import IPNetwork, IPRouter +from bacpypes.bvll import ( + Result, + WriteBroadcastDistributionTable, + ReadBroadcastDistributionTable, ReadBroadcastDistributionTableAck, + ForwardedNPDU, + RegisterForeignDevice, + ReadForeignDeviceTable, ReadForeignDeviceTableAck, + DeleteForeignDeviceTableEntry, + DistributeBroadcastToNetwork, + OriginalUnicastNPDU, + OriginalBroadcastNPDU, + ) + +from bacpypes.apdu import ( + WhoIsRequest, IAmRequest, + ReadPropertyRequest, ReadPropertyACK, + AbortPDU, + ) + +from ..state_machine import StateMachineGroup, TrafficLog +from ..time_machine import reset_time_machine, run_time_machine + +from .helpers import ( + SnifferStateMachine, BIPStateMachine, BIPSimpleStateMachine, + BIPForeignStateMachine, BIPBBMDStateMachine, + BIPSimpleNode, BIPBBMDNode, + BIPSimpleApplicationLayerStateMachine, + BIPBBMDApplication, + ) + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# TNetwork +# + +@bacpypes_debugging +class TNetwork(StateMachineGroup): + + def __init__(self, count): + if _debug: TNetwork._debug("__init__ %r", count) + StateMachineGroup.__init__(self) + + # reset the time machine + reset_time_machine() + if _debug: TNetwork._debug(" - time machine reset") + + # create a traffic log + self.traffic_log = TrafficLog() + + # make a router + self.router = IPRouter() + + # make the networks + self.vlan = [] + for net in range(1, count + 1): + # make a network and set the traffic log + ip_network = IPNetwork("192.168.{}.0/24".format(net)) + ip_network.traffic_log = self.traffic_log + + # make a router + router_address = Address("192.168.{}.1/24".format(net)) + self.router.add_network(router_address, ip_network) + + self.vlan.append(ip_network) + + def run(self, time_limit=60.0): + if _debug: TNetwork._debug("run %r", time_limit) + + # run the group + super(TNetwork, self).run() + + # run it for some time + run_time_machine(time_limit) + if _debug: TNetwork._debug(" - time machine finished") + + # check for success + all_success, some_failed = super(TNetwork, self).check_for_success() + if _debug: + TNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) + for state_machine in self.state_machines: + if state_machine.running: + TNetwork._debug(" %r (running)", state_machine) + elif not state_machine.current_state: + TNetwork._debug(" %r (not started)", state_machine) + else: + TNetwork._debug(" %r", state_machine) + for direction, pdu in state_machine.transaction_log: + TNetwork._debug(" %s %r", direction, pdu) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(TNetwork._debug) + + assert all_success + + +@bacpypes_debugging +class TestNonBBMD(unittest.TestCase): + + def setup_method(self, method): + """This function is called before each test method is called and is + given a reference to the test method.""" + if _debug: TestNonBBMD._debug("setup_method %r", method) + + # create a network + self.tnet = TNetwork(1) + + # test device + self.td = BIPStateMachine("192.168.1.2/24", self.tnet.vlan[0]) + self.tnet.append(self.td) + + # implementation under test + self.iut = BIPSimpleNode("192.168.1.3/24", self.tnet.vlan[0]) + + def test_write_bdt_fail(self): + """Test writing a BDT.""" + if _debug: TestNonBBMD._debug("test_write_bdt_fail") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-1-0") \ + .send(WriteBroadcastDistributionTable(destination=self.iut.address)).doc("1-1-1") \ + .receive(Result, bvlciResultCode=0x0010).doc("1-1-2") \ + .success() + + # run the group + self.tnet.run() + + def test_read_bdt_fail(self): + """Test reading a BDT.""" + if _debug: TestNonBBMD._debug("test_read_bdt_fail") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-2-0") \ + .send(ReadBroadcastDistributionTable(destination=self.iut.address)).doc("1-2-1") \ + .receive(Result, bvlciResultCode=0x0020).doc("1-2-2") \ + .success() + + # run the group + self.tnet.run() + + def test_register_fail(self): + """Test registering as a foreign device to a non-BBMD.""" + if _debug: TestNonBBMD._debug("test_read_fdt_success") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-3-0") \ + .send(RegisterForeignDevice(10, destination=self.iut.address)).doc("1-3-1") \ + .receive(Result, bvlciResultCode=0x0030).doc("1-3-2") \ + .success() + + # run the group + self.tnet.run() + + def test_read_fdt_fail(self): + """Test reading an FDT from a non-BBMD.""" + if _debug: TestNonBBMD._debug("test_read_fdt_success") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-4-0") \ + .send(ReadForeignDeviceTable(destination=self.iut.address)).doc("1-4-1") \ + .receive(Result, bvlciResultCode=0x0040).doc("1-4-2") \ + .success() + + # run the group + self.tnet.run() + + def test_delete_fail(self): + """Test deleting an FDT entry from a non-BBMD.""" + if _debug: TestNonBBMD._debug("test_delete_fail") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-5-0") \ + .send(DeleteForeignDeviceTableEntry(Address("1.2.3.4"), destination=self.iut.address)).doc("1-5-1") \ + .receive(Result, bvlciResultCode=0x0050).doc("1-5-2") \ + .success() + + # run the group + self.tnet.run() + + def test_distribute_fail(self): + """Test asking a non-BBMD to distribute a broadcast.""" + if _debug: TestNonBBMD._debug("test_delete_fail") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-6-0") \ + .send(DistributeBroadcastToNetwork(xtob('deadbeef'), destination=self.iut.address)).doc("1-6-1") \ + .receive(Result, bvlciResultCode=0x0060).doc("1-6-2") \ + .success() + + # run the group + self.tnet.run() + + +@bacpypes_debugging +class TestBBMD(unittest.TestCase): + + def test_14_2_1_1(self): + """14.2.1.1 Execute Forwarded-NPDU (One-hop Distribution).""" + if _debug: TestBBMD._debug("test_14_2_1_1") + + # create a network + tnet = TNetwork(2) + + # implementation under test + iut = BIPBBMDApplication("192.168.1.2/24", tnet.vlan[0]) + if _debug: TestBBMD._debug(" - iut.bip: %r", iut.bip) + + # BBMD on net 2 + bbmd1 = BIPBBMDNode("192.168.2.2/24", tnet.vlan[1]) + + # add the IUT as a one-hop peer + bbmd1.bip.add_peer(Address("192.168.1.2/24")) + if _debug: TestBBMD._debug(" - bbmd1.bip: %r", bbmd1.bip) + + # test device + td = BIPSimpleApplicationLayerStateMachine("192.168.2.3/24", tnet.vlan[1]) + tnet.append(td) + + # listener looks for extra traffic + listener = BIPStateMachine("192.168.1.3/24", tnet.vlan[0]) + listener.mux.node.promiscuous = True + tnet.append(listener) + + # broadcast a forwarded NPDU + td.start_state.doc("2-1-0") \ + .send(WhoIsRequest(destination=LocalBroadcast())).doc("2-1-1") \ + .receive(IAmRequest).doc("2-1-2") \ + .success() + + # listen for the directed broadcast, then the original unicast, + # then fail if there's anything else + listener.start_state.doc("2-2-0") \ + .receive(ForwardedNPDU).doc("2-2-1") \ + .receive(OriginalUnicastNPDU).doc("2-2-2") \ + .timeout(3).doc("2-2-3") \ + .success() + + # run the group + tnet.run() + + def test_14_2_1_2(self): + """14.2.1.1 Execute Forwarded-NPDU (Two-hop Distribution).""" + if _debug: TestBBMD._debug("test_14_2_1_2") + + # create a network + tnet = TNetwork(2) + + # implementation under test + iut = BIPBBMDApplication("192.168.1.2/24", tnet.vlan[0]) + if _debug: TestBBMD._debug(" - iut.bip: %r", iut.bip) + + # BBMD on net 2 + bbmd1 = BIPBBMDNode("192.168.2.2/24", tnet.vlan[1]) + + # add the IUT as a two-hop peer + bbmd1.bip.add_peer(Address("192.168.1.2/32")) + if _debug: TestBBMD._debug(" - bbmd1.bip: %r", bbmd1.bip) + + # test device + td = BIPSimpleApplicationLayerStateMachine("192.168.2.3/24", tnet.vlan[1]) + tnet.append(td) + + # listener looks for extra traffic + listener = BIPStateMachine("192.168.1.3/24", tnet.vlan[0]) + listener.mux.node.promiscuous = True + tnet.append(listener) + + # broadcast a forwarded NPDU + td.start_state.doc("2-3-0") \ + .send(WhoIsRequest(destination=LocalBroadcast())).doc("2-3-1") \ + .receive(IAmRequest).doc("2-3-2") \ + .success() + + # listen for the forwarded NPDU, then the re-braodcast on the local LAN + # then the original unicast going back, then fail if there's anything else + listener.start_state.doc("2-4-0") \ + .receive(ForwardedNPDU).doc("2-4-1") \ + .receive(ForwardedNPDU).doc("2-4-2") \ + .receive(OriginalUnicastNPDU).doc("2-4-3") \ + .timeout(3).doc("2-4-4") \ + .success() + + # run the group + tnet.run() + diff --git a/tests/test_bvll/test_foreign.py b/tests/test_bvll/test_foreign.py index 521b527f..0fa157b7 100644 --- a/tests/test_bvll/test_foreign.py +++ b/tests/test_bvll/test_foreign.py @@ -12,12 +12,20 @@ from bacpypes.pdu import Address, PDU, LocalBroadcast from bacpypes.vlan import IPNetwork, IPRouter -from bacpypes.bvll import ReadForeignDeviceTable, ReadForeignDeviceTableAck - -from ..state_machine import StateMachineGroup +from bacpypes.bvll import ( + Result, RegisterForeignDevice, + ReadForeignDeviceTable, ReadForeignDeviceTableAck, + DistributeBroadcastToNetwork, ForwardedNPDU, + OriginalUnicastNPDU, OriginalBroadcastNPDU, + ) + +from ..state_machine import StateMachineGroup, TrafficLog from ..time_machine import reset_time_machine, run_time_machine -from .helpers import SnifferNode, CodecNode, SimpleNode, ForeignNode, BBMDNode +from .helpers import ( + SnifferStateMachine, BIPStateMachine, + BIPSimpleStateMachine, BIPForeignStateMachine, BIPBBMDStateMachine, + ) # some debugging _debug = 0 @@ -39,23 +47,28 @@ def __init__(self): reset_time_machine() if _debug: TNetwork._debug(" - time machine reset") + # create a traffic log + self.traffic_log = TrafficLog() + # make a router self.router = IPRouter() # make a home LAN - self.home_vlan = IPNetwork() - self.router.add_network(Address("192.168.5.1/24"), self.home_vlan) + self.vlan_5 = IPNetwork("192.168.5.0/24") + self.vlan_5.traffic_log = self.traffic_log + self.router.add_network(Address("192.168.5.1/24"), self.vlan_5) # make a remote LAN - self.remote_vlan = IPNetwork() - self.router.add_network(Address("192.168.6.1/24"), self.remote_vlan) + self.vlan_6 = IPNetwork("192.168.6.0/24") + self.vlan_6.traffic_log = self.traffic_log + self.router.add_network(Address("192.168.6.1/24"), self.vlan_6) # the foreign device - self.fd = ForeignNode("192.168.6.2/24", self.remote_vlan) + self.fd = BIPForeignStateMachine("192.168.6.2/24", self.vlan_6) self.append(self.fd) # bbmd - self.bbmd = BBMDNode("192.168.5.3/24", self.home_vlan) + self.bbmd = BIPBBMDStateMachine("192.168.5.3/24", self.vlan_5) self.append(self.bbmd) def run(self, time_limit=60.0): @@ -70,6 +83,22 @@ def run(self, time_limit=60.0): # check for success all_success, some_failed = super(TNetwork, self).check_for_success() + + if _debug: + TNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) + for state_machine in self.state_machines: + if state_machine.running: + TNetwork._debug(" %r (running)", state_machine) + elif not state_machine.current_state: + TNetwork._debug(" %r (not started)", state_machine) + else: + TNetwork._debug(" %r", state_machine) + for direction, pdu in state_machine.transaction_log: + TNetwork._debug(" %s %s", direction, str(pdu)) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(TNetwork._debug) + assert all_success @@ -97,58 +126,46 @@ def test_registration(self): # create a network tnet = TNetwork() - # add an addition codec node to the home vlan - cnode = CodecNode("192.168.5.2/24", tnet.home_vlan) - tnet.append(cnode) - - # home sniffer node - home_sniffer = SnifferNode("192.168.5.254/24", tnet.home_vlan) - tnet.append(home_sniffer) - - # remote sniffer node - remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) - tnet.append(remote_sniffer) - # tell the B/IP layer of the foreign device to register tnet.fd.start_state \ .call(tnet.fd.bip.register, tnet.bbmd.address, 30) \ .success() - # sniffer pieces - registration_request = xtob('81.05.0006' # bvlci - '001e' # time-to-live - ) - registration_ack = xtob('81.00.0006.0000') # simple ack + # remote sniffer node + remote_sniffer = SnifferStateMachine("192.168.6.254/24", tnet.vlan_6) + tnet.append(remote_sniffer) - # remote sniffer sees registration + # sniffer traffic remote_sniffer.start_state.doc("1-1-0") \ - .receive(PDU, pduData=registration_request).doc("1-1-1") \ - .receive(PDU, pduData=registration_ack).doc("1-1-2") \ + .receive(RegisterForeignDevice).doc("1-1-1") \ + .receive(Result).doc("1-1-2") \ .set_event('fd-registered').doc("1-1-3") \ .success() # the bbmd is idle tnet.bbmd.start_state.success() - # read the FDT - cnode.start_state.doc("1-2-0") \ + # home snooper node + home_snooper = BIPStateMachine("192.168.5.2/24", tnet.vlan_5) + tnet.append(home_snooper) + + # snooper will read the foreign device table + home_snooper.start_state.doc("1-2-0") \ .wait_event('fd-registered').doc("1-2-1") \ .send(ReadForeignDeviceTable(destination=tnet.bbmd.address)).doc("1-2-2") \ .receive(ReadForeignDeviceTableAck).doc("1-2-3") \ .success() - # the tnode reads the registration table - read_fdt_request = xtob('81.06.0004') # bvlci - read_fdt_ack = xtob('81.07.000e' # read-ack - 'c0.a8.06.02.ba.c0 001e 0023' # address, ttl, remaining - ) + # home sniffer node + home_sniffer = SnifferStateMachine("192.168.5.254/24", tnet.vlan_5) + tnet.append(home_sniffer) - # home sniffer sees registration + # sniffer traffic home_sniffer.start_state.doc("1-3-0") \ - .receive(PDU, pduData=registration_request).doc("1-3-1") \ - .receive(PDU, pduData=registration_ack).doc("1-3-2") \ - .receive(PDU, pduData=read_fdt_request).doc("1-3-3") \ - .receive(PDU, pduData=read_fdt_ack).doc("1-3-4") \ + .receive(RegisterForeignDevice).doc("1-3-1") \ + .receive(Result).doc("1-3-2") \ + .receive(ReadForeignDeviceTable).doc("1-3-3") \ + .receive(ReadForeignDeviceTableAck).doc("1-3-4") \ .success() # run the group @@ -170,21 +187,15 @@ def test_refresh_registration(self): tnet.bbmd.start_state.success() # remote sniffer node - remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + remote_sniffer = SnifferStateMachine("192.168.6.254/24", tnet.vlan_6) tnet.append(remote_sniffer) - # sniffer pieces - registration_request = xtob('81.05.0006' # bvlci - '000a' # time-to-live - ) - registration_ack = xtob('81.00.0006.0000') # simple ack - - # remote sniffer sees registration + # sniffer traffic remote_sniffer.start_state.doc("2-1-0") \ - .receive(PDU, pduData=registration_request).doc("2-1-1") \ - .receive(PDU, pduData=registration_ack).doc("2-1-2") \ - .receive(PDU, pduData=registration_request).doc("2-1-3") \ - .receive(PDU, pduData=registration_ack).doc("2-1-4") \ + .receive(RegisterForeignDevice).doc("2-1-1") \ + .receive(Result).doc("2-1-2") \ + .receive(RegisterForeignDevice).doc("2-1-3") \ + .receive(Result).doc("2-1-4") \ .success() # run the group @@ -205,7 +216,7 @@ def test_unicast(self): # register, wait for ack, send some beef tnet.fd.start_state.doc("3-1-0") \ .call(tnet.fd.bip.register, tnet.bbmd.address, 60).doc("3-1-1") \ - .wait_event('fd-registered').doc("3-1-2") \ + .wait_event('3-registered').doc("3-1-2") \ .send(pdu).doc("3-1-3") \ .success() @@ -215,24 +226,15 @@ def test_unicast(self): .success() # remote sniffer node - remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + remote_sniffer = SnifferStateMachine("192.168.6.254/24", tnet.vlan_6) tnet.append(remote_sniffer) - # sniffer pieces - registration_request = xtob('81.05.0006' # bvlci - '003c' # time-to-live (60) - ) - registration_ack = xtob('81.00.0006.0000') # simple ack - unicast_pdu = xtob('81.0a.0008' # original unicast bvlci - 'dead.beef' # PDU being unicast - ) - - # remote sniffer sees registration + # sniffer traffic remote_sniffer.start_state.doc("3-2-0") \ - .receive(PDU, pduData=registration_request).doc("3-2-1") \ - .receive(PDU, pduData=registration_ack).doc("3-2-2") \ - .set_event('fd-registered').doc("3-2-3") \ - .receive(PDU, pduData=unicast_pdu).doc("3-2-4") \ + .receive(RegisterForeignDevice).doc("3-2-1") \ + .receive(Result).doc("3-2-2") \ + .set_event('3-registered').doc("3-2-3") \ + .receive(OriginalUnicastNPDU).doc("3-2-4") \ .success() # run the group @@ -258,38 +260,29 @@ def test_broadcast(self): .success() # the bbmd is happy when it gets the pdu - tnet.bbmd.start_state \ - .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data) \ + tnet.bbmd.start_state.doc("4-2-0") \ + .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data).doc("4-2-1") \ .success() - # home sniffer node - home_node = SimpleNode("192.168.5.254/24", tnet.home_vlan) + # home simple node + home_node = BIPSimpleStateMachine("192.168.5.254/24", tnet.vlan_5) tnet.append(home_node) # home node happy when getting the pdu, broadcast by the bbmd - home_node.start_state.doc("4-2-0") \ - .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data).doc("4-2-1") \ + home_node.start_state.doc("4-3-0") \ + .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data).doc("4-3-1") \ .success() # remote sniffer node - remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + remote_sniffer = SnifferStateMachine("192.168.6.254/24", tnet.vlan_6) tnet.append(remote_sniffer) - # sniffer pieces - registration_request = xtob('81.05.0006' # bvlci - '003c' # time-to-live (60) - ) - registration_ack = xtob('81.00.0006.0000') # simple ack - distribute_pdu = xtob('81.09.0008' # bvlci - 'deadbeef' # PDU to broadcast - ) - - # remote sniffer sees registration - remote_sniffer.start_state.doc("4-3-0") \ - .receive(PDU, pduData=registration_request).doc("4-3-1") \ - .receive(PDU, pduData=registration_ack).doc("4-3-2") \ + # remote traffic + remote_sniffer.start_state.doc("4-4-0") \ + .receive(RegisterForeignDevice).doc("4-4-1") \ + .receive(Result).doc("4-4-2") \ .set_event('4-registered') \ - .receive(PDU, pduData=distribute_pdu).doc("4-3-3") \ + .receive(DistributeBroadcastToNetwork).doc("4-4-3") \ .success() # run the group diff --git a/tests/test_bvll/test_simple.py b/tests/test_bvll/test_simple.py index fccf8cf0..729b9d0e 100644 --- a/tests/test_bvll/test_simple.py +++ b/tests/test_bvll/test_simple.py @@ -11,12 +11,15 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob from bacpypes.pdu import PDU, LocalBroadcast +from bacpypes.bvll import OriginalUnicastNPDU, OriginalBroadcastNPDU from bacpypes.vlan import IPNetwork from ..state_machine import match_pdu, StateMachineGroup from ..time_machine import reset_time_machine, run_time_machine -from .helpers import SnifferNode, SimpleNode +from .helpers import ( + SnifferStateMachine, BIPSimpleStateMachine, + ) # some debugging _debug = 0 @@ -42,15 +45,15 @@ def __init__(self): self.vlan = IPNetwork() # test device - self.td = SimpleNode("192.168.4.1/24", self.vlan) + self.td = BIPSimpleStateMachine("192.168.4.1/24", self.vlan) self.append(self.td) # implementation under test - self.iut = SimpleNode("192.168.4.2/24", self.vlan) + self.iut = BIPSimpleStateMachine("192.168.4.2/24", self.vlan) self.append(self.iut) # sniffer node - self.sniffer = SnifferNode("192.168.4.254/24", self.vlan) + self.sniffer = SnifferStateMachine("192.168.4.254/24", self.vlan) self.append(self.sniffer) @@ -109,12 +112,10 @@ def test_unicast(self): tnet.iut.start_state.receive(PDU, pduSource=tnet.td.address).success() # sniffer sees message on the wire - tnet.sniffer.start_state.receive(PDU, + tnet.sniffer.start_state.receive(OriginalUnicastNPDU, pduSource=tnet.td.address.addrTuple, pduDestination=tnet.iut.address.addrTuple, - pduData=xtob('81.0a.0008' # original unicast bvlci - 'deadbeef' # PDU being unicast - ), + pduData=pdu_data, ).timeout(1.0).success() # run the group @@ -137,12 +138,10 @@ def test_broadcast(self): tnet.iut.start_state.receive(PDU, pduSource=tnet.td.address).success() # sniffer sees message on the wire - tnet.sniffer.start_state.receive(PDU, - pduSource=tnet.td.address.addrTuple, - pduDestination=('192.168.4.255', 47808), - pduData=xtob('81.0b.0008' # original broadcast bvlci - 'deadbeef' # PDU being unicast - ), + tnet.sniffer.start_state.receive(OriginalBroadcastNPDU, + pduSource=tnet.td.address.addrTuple, +# pduDestination=('192.168.4.255', 47808), + pduData=pdu_data, ).timeout(1.0).success() # run the group diff --git a/tests/test_network/helpers.py b/tests/test_network/helpers.py index 1c039e41..a409ffb6 100644 --- a/tests/test_network/helpers.py +++ b/tests/test_network/helpers.py @@ -71,14 +71,14 @@ def confirmation(self, pdu): # -# SnifferNode +# SnifferStateMachine # @bacpypes_debugging -class SnifferNode(ClientStateMachine): +class SnifferStateMachine(ClientStateMachine): def __init__(self, address, vlan): - if _debug: SnifferNode._debug("__init__ %r %r", address, vlan) + if _debug: SnifferStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -87,20 +87,20 @@ def __init__(self, address, vlan): # create a promiscuous node, added to the network self.node = Node(self.address, vlan, promiscuous=True) - if _debug: SnifferNode._debug(" - node: %r", self.node) + if _debug: SnifferStateMachine._debug(" - node: %r", self.node) # bind this to the node bind(self, self.node) # -# NetworkLayerNode +# NetworkLayerStateMachine # @bacpypes_debugging -class NetworkLayerNode(ClientStateMachine): +class NetworkLayerStateMachine(ClientStateMachine): def __init__(self, address, vlan): - if _debug: NetworkLayerNode._debug("__init__ %r %r", address, vlan) + if _debug: NetworkLayerStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -109,11 +109,11 @@ def __init__(self, address, vlan): # create a network layer encoder/decoder self.codec = NPDUCodec() - if _debug: SnifferNode._debug(" - codec: %r", self.codec) + if _debug: SnifferStateMachine._debug(" - codec: %r", self.codec) # create a node, added to the network self.node = Node(self.address, vlan) - if _debug: SnifferNode._debug(" - node: %r", self.node) + if _debug: SnifferStateMachine._debug(" - node: %r", self.node) # bind this to the node bind(self, self.codec, self.node) @@ -158,14 +158,14 @@ class TestDeviceObject(LocalDeviceObject): pass # -# ApplicationLayerNode +# ApplicationLayerStateMachine # @bacpypes_debugging -class ApplicationLayerNode(ApplicationServiceElement, ClientStateMachine): +class ApplicationLayerStateMachine(ApplicationServiceElement, ClientStateMachine): def __init__(self, address, vlan): - if _debug: ApplicationLayerNode._debug("__init__ %r %r", address, vlan) + if _debug: ApplicationLayerStateMachine._debug("__init__ %r %r", address, vlan) # build a name, save the address self.name = "app @ %s" % (address,) @@ -180,7 +180,7 @@ def __init__(self, address, vlan): # build an address and save it self.address = Address(address) - if _debug: ApplicationLayerNode._debug(" - address: %r", self.address) + if _debug: ApplicationLayerStateMachine._debug(" - address: %r", self.address) # continue with initialization ApplicationServiceElement.__init__(self) @@ -209,17 +209,17 @@ def __init__(self, address, vlan): # create a node, added to the network self.node = Node(self.address, vlan) - if _debug: ApplicationLayerNode._debug(" - node: %r", self.node) + if _debug: ApplicationLayerStateMachine._debug(" - node: %r", self.node) # bind the stack to the local network self.nsap.bind(self.node) def indication(self, apdu): - if _debug: ApplicationLayerNode._debug("indication %r", apdu) + if _debug: ApplicationLayerStateMachine._debug("indication %r", apdu) self.receive(apdu) def confirmation(self, apdu): - if _debug: ApplicationLayerNode._debug("confirmation %r %r", apdu) + if _debug: ApplicationLayerStateMachine._debug("confirmation %r %r", apdu) self.receive(apdu) # diff --git a/tests/test_network/test_net_1.py b/tests/test_network/test_net_1.py index 3ed474d3..f77d1d04 100644 --- a/tests/test_network/test_net_1.py +++ b/tests/test_network/test_net_1.py @@ -29,7 +29,7 @@ from ..state_machine import match_pdu, StateMachineGroup from ..time_machine import reset_time_machine, run_time_machine -from .helpers import SnifferNode, NetworkLayerNode, RouterNode +from .helpers import SnifferStateMachine, NetworkLayerStateMachine, RouterNode # some debugging _debug = 0 @@ -58,11 +58,11 @@ def __init__(self): self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) # test device - self.td = NetworkLayerNode("1", self.vlan1) + self.td = NetworkLayerStateMachine("1", self.vlan1) self.append(self.td) # sniffer node - self.sniffer1 = SnifferNode("2", self.vlan1) + self.sniffer1 = SnifferStateMachine("2", self.vlan1) self.append(self.sniffer1) # add the network @@ -72,7 +72,7 @@ def __init__(self): self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) # sniffer node - self.sniffer2 = SnifferNode("4", self.vlan2) + self.sniffer2 = SnifferStateMachine("4", self.vlan2) self.append(self.sniffer2) # add the network @@ -82,7 +82,7 @@ def __init__(self): self.vlan3 = Network(name="vlan3", broadcast_address=LocalBroadcast()) # sniffer node - self.sniffer3 = SnifferNode("6", self.vlan3) + self.sniffer3 = SnifferStateMachine("6", self.vlan3) self.append(self.sniffer3) # add the network diff --git a/tests/test_network/test_net_2.py b/tests/test_network/test_net_2.py index 10f554fc..73d65396 100644 --- a/tests/test_network/test_net_2.py +++ b/tests/test_network/test_net_2.py @@ -30,7 +30,7 @@ from ..state_machine import match_pdu, StateMachineGroup, TrafficLog from ..time_machine import reset_time_machine, run_time_machine -from .helpers import SnifferNode, NetworkLayerNode, RouterNode +from .helpers import SnifferStateMachine, NetworkLayerStateMachine, RouterNode # some debugging _debug = 0 @@ -64,11 +64,11 @@ def __init__(self): self.vlan1.traffic_log = self.traffic_log # test device - self.td = NetworkLayerNode("1", self.vlan1) + self.td = NetworkLayerStateMachine("1", self.vlan1) self.append(self.td) # sniffer node - self.sniffer1 = SnifferNode("2", self.vlan1) + self.sniffer1 = SnifferStateMachine("2", self.vlan1) self.append(self.sniffer1) # connect vlan1 to iut1 @@ -79,7 +79,7 @@ def __init__(self): self.vlan2.traffic_log = self.traffic_log # sniffer node - self.sniffer2 = SnifferNode("4", self.vlan2) + self.sniffer2 = SnifferStateMachine("4", self.vlan2) self.append(self.sniffer2) # connect vlan2 to both routers @@ -91,7 +91,7 @@ def __init__(self): self.vlan3.traffic_log = self.traffic_log # sniffer node - self.sniffer3 = SnifferNode("7", self.vlan3) + self.sniffer3 = SnifferStateMachine("7", self.vlan3) self.append(self.sniffer3) # connect vlan3 to the second router diff --git a/tests/test_network/test_net_3.py b/tests/test_network/test_net_3.py index 1b8bfb78..0bf434c1 100644 --- a/tests/test_network/test_net_3.py +++ b/tests/test_network/test_net_3.py @@ -38,7 +38,7 @@ from ..time_machine import reset_time_machine, run_time_machine from .helpers import ( - SnifferNode, NetworkLayerNode, RouterNode, ApplicationLayerNode, + SnifferStateMachine, NetworkLayerStateMachine, RouterNode, ApplicationLayerStateMachine, ApplicationNode, ) @@ -69,11 +69,11 @@ def __init__(self): self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) # test device - self.td = ApplicationLayerNode("1", self.vlan1) + self.td = ApplicationLayerStateMachine("1", self.vlan1) self.append(self.td) # sniffer node - self.sniffer1 = SnifferNode("2", self.vlan1) + self.sniffer1 = SnifferStateMachine("2", self.vlan1) self.append(self.sniffer1) # add the network @@ -86,7 +86,7 @@ def __init__(self): self.app2 = ApplicationNode("4", self.vlan2) # sniffer node - self.sniffer2 = SnifferNode("5", self.vlan2) + self.sniffer2 = SnifferStateMachine("5", self.vlan2) self.append(self.sniffer2) # add the network diff --git a/tests/test_vlan/test_ipnetwork.py b/tests/test_vlan/test_ipnetwork.py index a825f98e..82f92eb9 100644 --- a/tests/test_vlan/test_ipnetwork.py +++ b/tests/test_vlan/test_ipnetwork.py @@ -346,6 +346,30 @@ def test_send_receive(self): csm_10_3.start_state.timeout(1).success() csm_20_2.start_state.timeout(1).success() + def test_local_broadcast(self): + """Test that a node can send a message to all of the other nodes on + the same network. + """ + if _debug: TestRouter._debug("test_local_broadcast") + + # unpack the state machines + csm_10_2, csm_10_3, csm_20_2, csm_20_3 = self.smg.state_machines + + # make a broadcast PDU from network 10 node 1 + pdu = PDU(b'data', + source=('192.168.10.2', 47808), + destination=('192.168.10.255', 47808), + ) + if _debug: TestVLAN._debug(" - pdu: %r", pdu) + + # node 10-2 sends the pdu, node 10-3 gets pdu, nodes 20-2 and 20-3 dont + csm_10_2.start_state.send(pdu).success() + csm_10_3.start_state.receive(PDU, + pduSource=('192.168.10.2', 47808), + ).success() + csm_20_2.start_state.timeout(1).success() + csm_20_3.start_state.timeout(1).success() + def test_remote_broadcast(self): """Test that a node can send a message to all of the other nodes on a different network. From c97450c26b572b390290b1b4925d72659a1812f4 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 3 May 2018 22:41:39 -0400 Subject: [PATCH 11/50] last remnants of #151 --- py25/bacpypes/object.py | 2 +- py27/bacpypes/object.py | 2 +- py34/bacpypes/object.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index 12b8d819..c5ce0cae 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -1440,7 +1440,7 @@ class EventEnrollmentObject(Object): , ReadableProperty('eventTimeStamps', ArrayOf(TimeStamp)) , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString)) , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString)) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference) , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index dc09e736..ac39cc05 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -1422,7 +1422,7 @@ class EventEnrollmentObject(Object): , ReadableProperty('eventTimeStamps', ArrayOf(TimeStamp)) , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString)) , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString)) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference) , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index 97b58992..9f2a4e99 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -1422,7 +1422,7 @@ class EventEnrollmentObject(Object): , ReadableProperty('eventTimeStamps', ArrayOf(TimeStamp)) , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString)) , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString)) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference) , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) From 8a49bedd6059debca78111bb12d11bec781c6972 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Fri, 4 May 2018 18:14:03 -0400 Subject: [PATCH 12/50] merging #188 before starting #187 --- py25/bacpypes/appservice.py | 31 +++++++++++++----------- py27/bacpypes/appservice.py | 31 +++++++++++++----------- py34/bacpypes/appservice.py | 31 +++++++++++++----------- tests/test_service/helpers.py | 21 +++++++++-------- tests/test_service/test_device.py | 39 +++++++++++++++++++++++++++++-- 5 files changed, 99 insertions(+), 54 deletions(-) diff --git a/py25/bacpypes/appservice.py b/py25/bacpypes/appservice.py index 942424e4..cfae8f47 100755 --- a/py25/bacpypes/appservice.py +++ b/py25/bacpypes/appservice.py @@ -17,7 +17,7 @@ SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \ unconfirmed_request_types, confirmed_request_types, complex_ack_types, \ error_types -from .errors import RejectException, AbortException +from .errors import RejectException, AbortException, UnrecognizedService # some debugging _debug = 0 @@ -1330,23 +1330,26 @@ def indication(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu) if isinstance(apdu, ConfirmedRequestPDU): + # assume no errors found + error_found = None + + # look up the class associated with the service atype = confirmed_request_types.get(apdu.apduService) if not atype: if _debug: ApplicationServiceAccessPoint._debug(" - no confirmed request decoder") - return - - # assume no errors found - error_found = None + error_found = UnrecognizedService() - try: - xpdu = atype() - xpdu.decode(apdu) - except RejectException, err: - ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) - error_found = err - except AbortException, err: - ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) - error_found = err + # no error so far, keep going + if not error_found: + try: + xpdu = atype() + xpdu.decode(apdu) + except RejectException as err: + ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) + error_found = err + except AbortException as err: + ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) + error_found = err # no error so far, keep going if not error_found: diff --git a/py27/bacpypes/appservice.py b/py27/bacpypes/appservice.py index 4a3b65da..1ebfc891 100755 --- a/py27/bacpypes/appservice.py +++ b/py27/bacpypes/appservice.py @@ -17,7 +17,7 @@ SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \ unconfirmed_request_types, confirmed_request_types, complex_ack_types, \ error_types -from .errors import RejectException, AbortException +from .errors import RejectException, AbortException, UnrecognizedService # some debugging _debug = 0 @@ -1327,23 +1327,26 @@ def indication(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu) if isinstance(apdu, ConfirmedRequestPDU): + # assume no errors found + error_found = None + + # look up the class associated with the service atype = confirmed_request_types.get(apdu.apduService) if not atype: if _debug: ApplicationServiceAccessPoint._debug(" - no confirmed request decoder") - return + error_found = UnrecognizedService() - # assume no errors found - error_found = None - - try: - xpdu = atype() - xpdu.decode(apdu) - except RejectException as err: - ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) - error_found = err - except AbortException as err: - ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) - error_found = err + # no error so far, keep going + if not error_found: + try: + xpdu = atype() + xpdu.decode(apdu) + except RejectException as err: + ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) + error_found = err + except AbortException as err: + ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) + error_found = err # no error so far, keep going if not error_found: diff --git a/py34/bacpypes/appservice.py b/py34/bacpypes/appservice.py index 4a3b65da..1ebfc891 100755 --- a/py34/bacpypes/appservice.py +++ b/py34/bacpypes/appservice.py @@ -17,7 +17,7 @@ SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \ unconfirmed_request_types, confirmed_request_types, complex_ack_types, \ error_types -from .errors import RejectException, AbortException +from .errors import RejectException, AbortException, UnrecognizedService # some debugging _debug = 0 @@ -1327,23 +1327,26 @@ def indication(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu) if isinstance(apdu, ConfirmedRequestPDU): + # assume no errors found + error_found = None + + # look up the class associated with the service atype = confirmed_request_types.get(apdu.apduService) if not atype: if _debug: ApplicationServiceAccessPoint._debug(" - no confirmed request decoder") - return + error_found = UnrecognizedService() - # assume no errors found - error_found = None - - try: - xpdu = atype() - xpdu.decode(apdu) - except RejectException as err: - ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) - error_found = err - except AbortException as err: - ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) - error_found = err + # no error so far, keep going + if not error_found: + try: + xpdu = atype() + xpdu.decode(apdu) + except RejectException as err: + ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) + error_found = err + except AbortException as err: + ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) + error_found = err # no error so far, keep going if not error_found: diff --git a/tests/test_service/helpers.py b/tests/test_service/helpers.py index 0e1768c4..5aca62d6 100644 --- a/tests/test_service/helpers.py +++ b/tests/test_service/helpers.py @@ -52,7 +52,7 @@ def __init__(self): ) # test device - self.td = ApplicationNode(self.td_device_object, self.vlan) + self.td = ApplicationStateMachine(self.td_device_object, self.vlan) self.append(self.td) # implementation under test device object @@ -65,7 +65,7 @@ def __init__(self): ) # implementation under test - self.iut = ApplicationNode(self.iut_device_object, self.vlan) + self.iut = ApplicationStateMachine(self.iut_device_object, self.vlan) self.append(self.iut) def run(self, time_limit=60.0): @@ -86,6 +86,7 @@ def run(self, time_limit=60.0): # check for success all_success, some_failed = super(ApplicationNetwork, self).check_for_success() + ApplicationNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) assert all_success @@ -126,18 +127,18 @@ def confirmation(self, pdu): # -# ApplicationNode +# ApplicationStateMachine # @bacpypes_debugging -class ApplicationNode(Application, StateMachine): +class ApplicationStateMachine(Application, StateMachine): def __init__(self, localDevice, vlan): - if _debug: ApplicationNode._debug("__init__ %r %r", localDevice, vlan) + if _debug: ApplicationStateMachine._debug("__init__ %r %r", localDevice, vlan) # build an address and save it self.address = Address(localDevice.objectIdentifier[1]) - if _debug: ApplicationNode._debug(" - address: %r", self.address) + if _debug: ApplicationStateMachine._debug(" - address: %r", self.address) # continue with initialization Application.__init__(self, localDevice, self.address) @@ -171,22 +172,22 @@ def __init__(self, localDevice, vlan): self.nsap.bind(self.node) def send(self, apdu): - if _debug: ApplicationNode._debug("send(%s) %r", self.name, apdu) + if _debug: ApplicationStateMachine._debug("send(%s) %r", self.name, apdu) # send the apdu down the stack self.request(apdu) def indication(self, apdu): - if _debug: ApplicationNode._debug("indication(%s) %r", self.name, apdu) + if _debug: ApplicationStateMachine._debug("indication(%s) %r", self.name, apdu) # let the state machine know the request was received self.receive(apdu) # allow the application to process it - super(ApplicationNode, self).indication(apdu) + super(ApplicationStateMachine, self).indication(apdu) def confirmation(self, apdu): - if _debug: ApplicationNode._debug("confirmation(%s) %r", self.name, apdu) + if _debug: ApplicationStateMachine._debug("confirmation(%s) %r", self.name, apdu) # forward the confirmation to the state machine self.receive(apdu) diff --git a/tests/test_service/test_device.py b/tests/test_service/test_device.py index aa7fdac5..677aa503 100644 --- a/tests/test_service/test_device.py +++ b/tests/test_service/test_device.py @@ -11,11 +11,13 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob from bacpypes.pdu import Address, LocalBroadcast, PDU +from bacpypes.basetypes import PropertyReference from bacpypes.apdu import ( + ConfirmedRequestSequence, SequenceOf, Element, WhoIsRequest, IAmRequest, WhoHasRequest, WhoHasLimits, WhoHasObject, IHaveRequest, DeviceCommunicationControlRequest, - SimpleAckPDU, Error, + SimpleAckPDU, Error, RejectPDU, ) from bacpypes.service.device import ( @@ -23,7 +25,7 @@ DeviceCommunicationControlServices, ) -from .helpers import ApplicationNetwork, ApplicationNode +from .helpers import ApplicationNetwork # some debugging _debug = 0 @@ -391,3 +393,36 @@ def test_incorrect_password(self): # run the group anet.run() + +@bacpypes_debugging +class TestUnrecognizedService(unittest.TestCase): + + class ReadPropertyConditionalRequest(ConfirmedRequestSequence): + serviceChoice = 13 + sequenceElements = [ + # Element('objectSelectionCriteria', ObjectSelectionCriteria, 1), + Element('listOfPropertyReferences', SequenceOf(PropertyReference), 1), + ] + + def test_9_39_1(self): + """9.39.1 Unsupported Confirmed Services Test""" + if _debug: TestUnrecognizedService._debug("test_9_39_1") + + # create a network + anet = ApplicationNetwork() + + # send the request, get it rejected + anet.td.start_state.doc("7-6-0") \ + .send(TestUnrecognizedService.ReadPropertyConditionalRequest( + destination=anet.iut.address, + listOfPropertyReferences=[], + )).doc("7-6-1") \ + .receive(RejectPDU, pduSource=anet.iut.address, apduAbortRejectReason=9).doc("7-6-2") \ + .success() + + # no IUT application layer matching + anet.iut.start_state.success() + + # run the group + anet.run() + From fc2510cd5d2ed7a38764b065b4d0a43bfd5c4abd Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 10 May 2018 20:28:36 -0400 Subject: [PATCH 13/50] new sample --- samples/ReadObjectList.py | 244 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100755 samples/ReadObjectList.py diff --git a/samples/ReadObjectList.py b/samples/ReadObjectList.py new file mode 100755 index 00000000..63756496 --- /dev/null +++ b/samples/ReadObjectList.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python + +""" +This application is given the device instance number of a device and its +address read the object list, then for each object, read the object name. +""" + +from collections import deque + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run, deferred, stop +from bacpypes.iocb import IOCB + +from bacpypes.primitivedata import ObjectIdentifier, CharacterString +from bacpypes.constructeddata import ArrayOf + +from bacpypes.pdu import Address +from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_device = None +this_application = None + +# convenience definition +ArrayOfObjectIdentifier = ArrayOf(ObjectIdentifier) + +# +# ObjectListContext +# + +class ObjectListContext: + + def __init__(self, device_id, device_addr): + self.device_id = device_id + self.device_addr = device_addr + + self.object_list = [] + self.object_names = [] + + self._object_list_queue = None + + def completed(self, had_error=None): + if had_error: + print("had error: %r" % (had_error,)) + else: + for objid, objname in zip(self.object_list, self.object_names): + print("%s: %s" % (objid, objname)) + + stop() + +# +# ReadObjectListApplication +# + +@bacpypes_debugging +class ReadObjectListApplication(BIPSimpleApplication): + + def __init__(self, *args): + if _debug: ReadObjectListApplication._debug("__init__ %r", args) + BIPSimpleApplication.__init__(self, *args) + + def read_object_list(self, device_id, device_addr): + if _debug: ReadObjectListApplication._debug("read_object_list %r %r", device_id, device_addr) + + # create a context to hold the results + context = ObjectListContext(device_id, device_addr) + + # build a request for the object name + request = ReadPropertyRequest( + destination=context.device_addr, + objectIdentifier=context.device_id, + propertyIdentifier='objectList', + ) + if _debug: ReadObjectListApplication._debug(" - request: %r", request) + + # make an IOCB, reference the context + iocb = IOCB(request) + iocb.context = context + if _debug: ReadObjectListApplication._debug(" - iocb: %r", iocb) + + # let us know when its complete + iocb.add_callback(self.object_list_results) + + # give it to the application + self.request_io(iocb) + + def object_list_results(self, iocb): + if _debug: ReadObjectListApplication._debug("object_list_results %r", iocb) + + # extract the context + context = iocb.context + + # do something for error/reject/abort + if iocb.ioError: + context.completed(iocb.ioError) + return + + # do something for success + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: ReadObjectListApplication._debug(" - not an ack") + context.completed(RuntimeError("read property ack expected")) + return + + # pull out the content + object_list = apdu.propertyValue.cast_out(ArrayOfObjectIdentifier) + if _debug: ReadObjectListApplication._debug(" - object_list: %r", object_list) + + # store it in the context + context.object_list = object_list + + # make a queue of the identifiers to read, start reading them + context._object_list_queue = deque(object_list) + deferred(self.read_next_object, context) + + def read_next_object(self, context): + if _debug: ReadObjectListApplication._debug("read_next_object %r", context) + + # if there's nothing more to do, we're done + if not context._object_list_queue: + if _debug: ReadObjectListApplication._debug(" - all done") + context.completed() + return + + # pop off the next object identifier + object_id = context._object_list_queue.popleft() + if _debug: ReadObjectListApplication._debug(" - object_id: %r", object_id) + + # build a request for the object name + request = ReadPropertyRequest( + destination=context.device_addr, + objectIdentifier=object_id, + propertyIdentifier='objectName', + ) + if _debug: ReadObjectListApplication._debug(" - request: %r", request) + + # make an IOCB, reference the context + iocb = IOCB(request) + iocb.context = context + if _debug: ReadObjectListApplication._debug(" - iocb: %r", iocb) + + # let us know when its complete + iocb.add_callback(self.object_name_results) + + # give it to the application + self.request_io(iocb) + + def object_name_results(self, iocb): + if _debug: ReadObjectListApplication._debug("object_name_results %r", iocb) + + # extract the context + context = iocb.context + + # do something for error/reject/abort + if iocb.ioError: + context.completed(iocb.ioError) + return + + # do something for success + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: ReadObjectListApplication._debug(" - not an ack") + context.completed(RuntimeError("read property ack expected")) + return + + # pull out the name + object_name = apdu.propertyValue.cast_out(CharacterString) + if _debug: ReadObjectListApplication._debug(" - object_name: %r", object_name) + + # store it in the context + context.object_names.append(object_name) + + # read the next one + deferred(self.read_next_object, context) + +# +# __main__ +# + +def main(): + global this_device + global this_application + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + + # add an argument for interval + parser.add_argument('device_id', type=int, + help='device identifier', + ) + + # add an argument for interval + parser.add_argument('device_addr', type=str, + help='device address', + ) + + # parse the args + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject( + objectName=args.ini.objectname, + objectIdentifier=int(args.ini.objectidentifier), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # make a simple application + this_application = ReadObjectListApplication(this_device, args.ini.address) + + # build a device object identifier + device_id = ('device', args.device_id) + + # translate the address + device_addr = Address(args.device_addr) + + # kick off the process after the core is up and running + deferred(this_application.read_object_list, device_id, device_addr) + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() From d5fa8768d57771fa8ed10335d96be3f9c70a904a Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 24 May 2018 00:12:39 -0400 Subject: [PATCH 14/50] merging #187 for release --- py25/bacpypes/apdu.py | 46 ++- py25/bacpypes/app.py | 170 ++++++----- py25/bacpypes/appservice.py | 374 +++++++++++++---------- py25/bacpypes/task.py | 12 +- py25/bacpypes/vlan.py | 3 +- py27/bacpypes/apdu.py | 39 ++- py27/bacpypes/app.py | 173 ++++++----- py27/bacpypes/appservice.py | 370 ++++++++++++---------- py27/bacpypes/task.py | 12 +- py27/bacpypes/vlan.py | 5 +- py34/bacpypes/apdu.py | 39 ++- py34/bacpypes/app.py | 173 ++++++----- py34/bacpypes/appservice.py | 368 ++++++++++++---------- py34/bacpypes/task.py | 12 +- py34/bacpypes/vlan.py | 5 +- samples/AccumulatorObject.py | 4 + samples/COVClient.py | 4 + samples/COVClientApp.py | 4 + samples/COVServer.py | 4 + samples/CommandableMixin.py | 4 + samples/DeviceCommunicationControl.py | 4 + samples/DeviceDiscovery.py | 4 + samples/DeviceDiscoveryForeign.py | 4 + samples/HTTPServer.py | 4 + samples/LocalScheduleObject.py | 4 + samples/MultiStateValueObject.py | 4 + samples/MultipleReadProperty.py | 4 + samples/MultipleReadPropertyHammer.py | 4 + samples/MultipleReadPropertyThreaded.py | 4 + samples/RandomAnalogValueSleep.py | 4 + samples/ReadAllProperties.py | 4 + samples/ReadProperty.py | 8 + samples/ReadProperty25.py | 4 + samples/ReadPropertyAny.py | 4 + samples/ReadPropertyMultiple.py | 30 +- samples/ReadPropertyMultiple25.py | 4 + samples/ReadPropertyMultipleServer.py | 4 + samples/ReadPropertyMultipleServer25.py | 4 + samples/ReadRange.py | 4 + samples/ReadWriteEventMessageTexts.py | 4 + samples/ReadWriteFile.py | 4 + samples/ReadWriteFileServer.py | 4 + samples/ReadWriteProperty.py | 4 + samples/RecurringMultipleReadProperty.py | 4 + samples/ThreadedReadProperty.py | 4 + samples/VendorAVObject.py | 4 + samples/VendorReadWriteProperty.py | 4 + samples/WhoIsIAm.py | 5 +- samples/WhoIsIAmForeign.py | 5 +- samples/WritePropertyTCPServer.py | 4 + samples/WriteSomething.py | 4 + tests/__init__.py | 1 + tests/state_machine.py | 8 +- tests/test_bvll/test_bbmd.py | 25 +- tests/test_network/test_net_3.py | 16 +- tests/test_segmentation/__init__.py | 7 + tests/test_segmentation/test_1.py | 345 +++++++++++++++++++++ tests/test_service/test_device.py | 92 +++++- tests/time_machine.py | 6 +- 59 files changed, 1669 insertions(+), 808 deletions(-) create mode 100644 tests/test_segmentation/__init__.py create mode 100644 tests/test_segmentation/test_1.py diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index 3aa29f28..118b4ab0 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -56,38 +56,51 @@ def register_error_type(klass): # encode_max_segments_accepted/decode_max_segments_accepted # +_max_segments_accepted_encoding = [ + None, 2, 4, 8, 16, 32, 64, None, + ] + def encode_max_segments_accepted(arg): """Encode the maximum number of segments the device will accept, Section - 20.1.2.4""" - w = 0 - while (arg and not arg & 1): - w += 1 - arg = (arg >> 1) - return w + 20.1.2.4, and if the device says it can only accept one segment it shouldn't + say that it supports segmentation!""" + # unspecified + if not arg: + return 0 + + if arg > 64: + return 7 + + # the largest number not greater than the arg + for i in range(6, 0, -1): + if _max_segments_accepted_encoding[i] <= arg: + return i + + raise ValueError("invalid max max segments accepted: {0}".format(arg)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section 20.1.2.4""" - return arg and (1 << arg) or None + return _max_segments_accepted_encoding[arg] # # encode_max_apdu_length_accepted/decode_max_apdu_length_accepted # -_max_apdu_response_encoding = [50, 128, 206, 480, 1024, 1476, None, None, +_max_apdu_length_encoding = [50, 128, 206, 480, 1024, 1476, None, None, None, None, None, None, None, None, None, None] def encode_max_apdu_length_accepted(arg): """Return the encoding of the highest encodable value less than the value of the arg.""" for i in range(5, -1, -1): - if (arg >= _max_apdu_response_encoding[i]): + if (arg >= _max_apdu_length_encoding[i]): return i raise ValueError("invalid max APDU length accepted: {0}".format(arg)) def decode_max_apdu_length_accepted(arg): - v = _max_apdu_response_encoding[arg] + v = _max_apdu_length_encoding[arg] if not v: raise ValueError("invalid max APDU length accepted: {0}".format(arg)) @@ -173,7 +186,7 @@ def encode(self, pdu): if self.apduSA: buff += 0x02 pdu.put(buff) - pdu.put((encode_max_segments_accepted(self.apduMaxSegs) << 4) + encode_max_apdu_length_accepted(self.apduMaxResp)) + pdu.put((self.apduMaxSegs << 4) + self.apduMaxResp) pdu.put(self.apduInvokeID) if self.apduSeg: pdu.put(self.apduSeq) @@ -254,8 +267,8 @@ def decode(self, pdu): self.apduMor = ((buff & 0x04) != 0) self.apduSA = ((buff & 0x02) != 0) buff = pdu.get() - self.apduMaxSegs = decode_max_segments_accepted( (buff >> 4) & 0x07 ) - self.apduMaxResp = decode_max_apdu_length_accepted( buff & 0x0F ) + self.apduMaxSegs = (buff >> 4) & 0x07 + self.apduMaxResp = buff & 0x0F self.apduInvokeID = pdu.get() if self.apduSeg: self.apduSeq = pdu.get() @@ -693,6 +706,7 @@ def decode(self, apdu): # create a tag list and decode the rest of the data self._tag_list = TagList() self._tag_list.decode(apdu) + if _debug: APCISequence._debug(" - tag list: %r", self._tag_list) # pass the taglist to the Sequence for additional decoding Sequence.decode(self, self._tag_list) @@ -1501,9 +1515,9 @@ class RemoveListElementRequest(ConfirmedRequestSequence): class DeviceCommunicationControlRequestEnableDisable(Enumerated): enumerations = \ - { 'enable':0 - , 'disable':1 - , 'disableInitiation':2 + { 'enable': 0 + , 'disable': 1 + , 'disableInitiation': 2 } class DeviceCommunicationControlRequest(ConfirmedRequestSequence): diff --git a/py25/bacpypes/app.py b/py25/bacpypes/app.py index fa5f9ca5..895fadad 100755 --- a/py25/bacpypes/app.py +++ b/py25/bacpypes/app.py @@ -26,7 +26,8 @@ # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ - ConfirmedServiceChoice, UnconfirmedServiceChoice + ConfirmedServiceChoice, UnconfirmedServiceChoice, \ + IAmRequest from .basetypes import ServicesSupported # basic services @@ -53,16 +54,16 @@ class DeviceInfo(DebugContents): 'maxSegmentsAccepted', ) - def __init__(self): + def __init__(self, device_identifier, address): # this information is from an IAmRequest - self.deviceIdentifier = None # device identifier - self.address = None # LocalStation or RemoteStation + self.deviceIdentifier = device_identifier + self.address = address + self.maxApduLengthAccepted = 1024 # maximum APDU device will accept self.segmentationSupported = 'noSegmentation' # normally no segmentation + self.maxSegmentsAccepted = None # None iff no segmentation self.vendorID = None # vendor identifier - - self.maxNpduLength = 1497 # maximum we can send in transit - self.maxSegmentsAccepted = None # value for proposed/actual window size + self.maxNpduLength = None # maximum we can send in transit (see 19.4) bacpypes_debugging(DeviceInfo) @@ -72,127 +73,135 @@ def __init__(self): class DeviceInfoCache: - def __init__(self): + def __init__(self, device_info_class=DeviceInfo): if _debug: DeviceInfoCache._debug("__init__") + # a little error checking + if not issubclass(device_info_class, DeviceInfo): + raise ValueError("not a DeviceInfo subclass: %r" % (device_info_class,)) + # empty cache self.cache = {} + # class for new records + self.device_info_class = device_info_class + def has_device_info(self, key): """Return true iff cache has information about the device.""" if _debug: DeviceInfoCache._debug("has_device_info %r", key) return key in self.cache - def add_device_info(self, apdu): + def iam_device_info(self, apdu): """Create a device information record based on the contents of an IAmRequest and put it in the cache.""" - if _debug: DeviceInfoCache._debug("add_device_info %r", apdu) + if _debug: DeviceInfoCache._debug("iam_device_info %r", apdu) - # get the existing cache record by identifier - info = self.get_device_info(apdu.iAmDeviceIdentifier[1]) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # make sure the apdu is an I-Am + if not isinstance(apdu, IAmRequest): + raise ValueError("not an IAmRequest: %r" % (apdu,)) - # update existing record - if info: - if (info.address == apdu.pduSource): - return + # get the device instance + device_instance = apdu.iAmDeviceIdentifier[1] - info.address = apdu.pduSource - else: - # get the existing record by address (creates a new record) - info = self.get_device_info(apdu.pduSource) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # get the existing cache record if it exists + device_info = self.cache.get(device_instance, None) + + # maybe there is a record for this address + if not device_info: + device_info = self.cache.get(apdu.pduSource, None) - info.deviceIdentifier = apdu.iAmDeviceIdentifier[1] + # make a new one using the class provided + if not device_info: + device_info = self.device_info_class(device_instance, apdu.pduSource) - # update the rest of the values - info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted - info.segmentationSupported = apdu.segmentationSupported - info.vendorID = apdu.vendorID + # jam in the correct values + device_info.deviceIdentifier = device_instance + device_info.address = apdu.pduSource + device_info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted + device_info.segmentationSupported = apdu.segmentationSupported + device_info.vendorID = apdu.vendorID - # say this is an updated record - self.update_device_info(info) + # tell the cache this is an updated record + self.update_device_info(device_info) def get_device_info(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" if _debug: DeviceInfoCache._debug("get_device_info %r", key) - if isinstance(key, int): - current_info = self.cache.get(key, None) - - elif not isinstance(key, Address): - raise TypeError("key must be integer or an address") + # get the info if it's there + device_info = self.cache.get(key, None) + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): - raise TypeError("address must be a local or remote station") + return device_info - else: - current_info = self.cache.get(key, None) - if not current_info: - current_info = DeviceInfo() - current_info.address = key - current_info._cache_keys = (None, key) - current_info._ref_count = 1 - - self.cache[key] = current_info - else: - if _debug: DeviceInfoCache._debug(" - reference bump") - current_info._ref_count += 1 - - if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) - - return current_info - - def update_device_info(self, info): + def update_device_info(self, device_info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" - if _debug: DeviceInfoCache._debug("update_device_info %r", info) + if _debug: DeviceInfoCache._debug("update_device_info %r", device_info) - cache_id, cache_address = info._cache_keys + # give this a reference count if it doesn't have one + if not hasattr(device_info, '_ref_count'): + device_info._ref_count = 0 - if (cache_id is not None) and (info.deviceIdentifier != cache_id): + # get the current keys + cache_id, cache_address = getattr(device_info, '_cache_keys', (None, None)) + + if (cache_id is not None) and (device_info.deviceIdentifier != cache_id): if _debug: DeviceInfoCache._debug(" - device identifier updated") # remove the old reference, add the new one del self.cache[cache_id] - self.cache[info.deviceIdentifier] = info - - cache_id = info.deviceIdentifier + self.cache[device_info.deviceIdentifier] = device_info - if (cache_address is not None) and (info.address != cache_address): + if (cache_address is not None) and (device_info.address != cache_address): if _debug: DeviceInfoCache._debug(" - device address updated") # remove the old reference, add the new one del self.cache[cache_address] - self.cache[info.address] = info - - cache_address = info.address + self.cache[device_info.address] = device_info # update the keys - info._cache_keys = (cache_id, cache_address) + device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) + + def acquire(self, key): + """Return the known information about the device. If the key is the + address of an unknown device, build a generic device information record + add put it in the cache.""" + if _debug: DeviceInfoCache._debug("acquire %r", key) + + if isinstance(key, int): + device_info = self.cache.get(key, None) + + elif not isinstance(key, Address): + raise TypeError("key must be integer or an address") + + elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): + raise TypeError("address must be a local or remote station") + + else: + device_info = self.cache.get(key, None) + + if device_info: + if _debug: DeviceInfoCache._debug(" - reference bump") + device_info._ref_count += 1 + + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - def release_device_info(self, info): + return device_info + + def release(self, device_info): """This function is called by the segmentation state machine when it has finished with the device information.""" - if _debug: DeviceInfoCache._debug("release_device_info %r", info) + if _debug: DeviceInfoCache._debug("release %r", device_info) # this information record might be used by more than one SSM - if info._ref_count > 1: - if _debug: DeviceInfoCache._debug(" - multiple references") - info._ref_count -= 1 - return + if device_info._ref_count == 0: + raise RuntimeError("reference count") - cache_id, cache_address = info._cache_keys - if cache_id is not None: - del self.cache[cache_id] - if cache_address is not None: - del self.cache[cache_address] - if _debug: DeviceInfoCache._debug(" - released") + # decrement the reference count + device_info._ref_count -= 1 bacpypes_debugging(DeviceInfoCache) @@ -619,3 +628,4 @@ def __init__(self, localAddress, eID=None): self.nsap.bind(self.bip) bacpypes_debugging(BIPNetworkApplication) + diff --git a/py25/bacpypes/appservice.py b/py25/bacpypes/appservice.py index cfae8f47..b30a7e8a 100755 --- a/py25/bacpypes/appservice.py +++ b/py25/bacpypes/appservice.py @@ -12,7 +12,9 @@ from .task import OneShotTask from .pdu import Address -from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \ +from .apdu import encode_max_segments_accepted, decode_max_segments_accepted, \ + encode_max_apdu_length_accepted, decode_max_apdu_length_accepted, \ + AbortPDU, AbortReason, ComplexAckPDU, \ ConfirmedRequestPDU, Error, ErrorPDU, RejectPDU, SegmentAckPDU, \ SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \ unconfirmed_request_types, confirmed_request_types, complex_ack_types, \ @@ -44,19 +46,23 @@ class SSM(OneShotTask, DebugContents): , 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED' ] - _debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID' + _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' ) - def __init__(self, sap, remoteDevice): + def __init__(self, sap, pdu_address): """Common parts for client and server segmentation.""" - if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice) + if _debug: SSM._debug("__init__ %r %r", sap, pdu_address) OneShotTask.__init__(self) self.ssmSAP = sap # service access point - self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance + + # save the address and get the device information + self.pdu_address = pdu_address + self.device_info = sap.deviceInfoCache.get_device_info(pdu_address) + self.invokeID = None # invoke ID self.state = IDLE # initial state @@ -70,11 +76,17 @@ def __init__(self, sap, remoteDevice): self.lastSequenceNumber = None self.initialSequenceNumber = None self.actualWindowSize = None - self.proposedWindowSize = None - # the maximum number of segments starts out being what's in the SAP - # which is the defaults or values from the local device. - self.maxSegmentsAccepted = self.ssmSAP.maxSegmentsAccepted + # local device object provides these or SAP provides defaults, make + # copies here so they are consistent throughout the transaction but + # they could change from one transaction to the next + self.numberOfApduRetries = getattr(sap.localDevice, 'numberOfApduRetries', sap.numberOfApduRetries) + self.apduTimeout = getattr(sap.localDevice, 'apduTimeout', sap.apduTimeout) + + self.segmentationSupported = getattr(sap.localDevice, 'segmentationSupported', sap.segmentationSupported) + self.segmentTimeout = getattr(sap.localDevice, 'segmentTimeout', sap.segmentTimeout) + self.maxSegmentsAccepted = getattr(sap.localDevice, 'maxSegmentsAccepted', sap.maxSegmentsAccepted) + self.maxApduLengthAccepted = getattr(sap.localDevice, 'maxApduLengthAccepted', sap.maxApduLengthAccepted) def start_timer(self, msecs): if _debug: SSM._debug("start_timer %r", msecs) @@ -145,19 +157,19 @@ def get_segment(self, indx): # check for invalid segment number if indx >= self.segmentCount: - raise RuntimeError("invalid segment number %r, APDU has %r segments" % (indx, self.segmentCount)) + raise RuntimeError("invalid segment number {0}, APDU has {1} segments".format(indx, self.segmentCount)) if self.segmentAPDU.apduType == ConfirmedRequestPDU.pduType: if _debug: SSM._debug(" - confirmed request context") segAPDU = ConfirmedRequestPDU(self.segmentAPDU.apduService) - segAPDU.apduMaxSegs = self.maxSegmentsAccepted - segAPDU.apduMaxResp = self.ssmSAP.maxApduLengthAccepted - segAPDU.apduInvokeID = self.invokeID; + segAPDU.apduMaxSegs = encode_max_segments_accepted(self.maxSegmentsAccepted) + segAPDU.apduMaxResp = encode_max_apdu_length_accepted(self.maxApduLengthAccepted) + segAPDU.apduInvokeID = self.invokeID # segmented response accepted? - segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth') + segAPDU.apduSA = self.segmentationSupported in ('segmentedReceive', 'segmentedBoth') if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA) elif self.segmentAPDU.apduType == ComplexAckPDU.pduType: @@ -171,14 +183,21 @@ def get_segment(self, indx): segAPDU.pduUserData = self.segmentAPDU.pduUserData # make sure the destination is set - segAPDU.pduDestination = self.remoteDevice.address + segAPDU.pduDestination = self.pdu_address # segmented message? if (self.segmentCount != 1): segAPDU.apduSeg = True segAPDU.apduMor = (indx < (self.segmentCount - 1)) # more follows segAPDU.apduSeq = indx % 256 # sequence number - segAPDU.apduWin = self.proposedWindowSize # window size + + # first segment sends proposed window size, rest get actual + if indx == 0: + if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) + segAPDU.apduWin = self.proposedWindowSize + else: + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) + segAPDU.apduWin = self.actualWindowSize else: segAPDU.apduSeg = False segAPDU.apduMor = False @@ -210,10 +229,11 @@ def in_window(self, seqA, seqB): return rslt - def FillWindow(self, seqNum): + def fill_window(self, seqNum): """This function sends all of the packets necessary to fill out the segmentation window.""" - if _debug: SSM._debug("FillWindow %r", seqNum) + if _debug: SSM._debug("fill_window %r", seqNum) + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) for ix in range(self.actualWindowSize): apdu = self.get_segment(seqNum + ix) @@ -234,13 +254,18 @@ def FillWindow(self, seqNum): class ClientSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ClientSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) # initialize the retry count self.retryCount = 0 + # acquire the device info + if self.device_info: + if _debug: ClientSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) + def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" if _debug: ClientSSM._debug("set_state %r (%s) timer=%r", newState, SSM.transactionLabels[newState], timer) @@ -253,8 +278,10 @@ def set_state(self, newState, timer=0): if _debug: ClientSSM._debug(" - remove from active transactions") self.ssmSAP.clientTransactions.remove(self) - if _debug: ClientSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by client transaction functions when it wants @@ -263,7 +290,7 @@ def request(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -280,26 +307,27 @@ def indication(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit, - # the maximum conveyable by the internetwork to the remote device, and - # the maximum APDU size accepted by the remote device. - self.segmentSize = min( - self.ssmSAP.maxApduLengthAccepted, - self.remoteDevice.maxNpduLength, - self.remoteDevice.maxApduLengthAccepted, - ) - if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) + # if the max apdu length of the server isn't known, assume that it + # is the same size as our own and will be the segment size + if (not self.device_info) or (self.device_info.maxApduLengthAccepted is None): + self.segmentSize = self.maxApduLengthAccepted + + # if the max npdu length of the server isn't known, assume that it + # is the same as the max apdu length accepted + elif self.device_info.maxNpduLength is None: + self.segmentSize = self.device_info.maxApduLengthAccepted - # the maximum number of segments acceptable in the reply - if apdu.apduMaxSegs is not None: - # this request overrides the default - self.maxSegmentsAccepted = apdu.apduMaxSegs + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the server and the largest it can accept + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.device_info.maxApduLengthAccepted) + if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) # save the invoke ID self.invokeID = apdu.apduInvokeID if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -312,34 +340,49 @@ def indication(self, apdu): # make sure we support segmented transmit if we need to if self.segmentCount > 1: - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't send segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): - if _debug: ClientSSM._debug(" - remote device can't receive segmented requests") + + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for segmentation support") + + elif self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if _debug: ClientSSM._debug(" - server can't receive segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our request + # that the server said it was willing to accept + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for maximum number of segments") + + elif not self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server doesn't say maximum number of segments") + + elif self.segmentCount > self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # send out the first segment (or the whole thing) if self.segmentCount == 1: - # SendConfirmedUnsegmented + # unsegmented self.sentAllSegments = True self.retryCount = 0 - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) else: - # SendConfirmedSegmented + # segmented self.sentAllSegments = False self.retryCount = 0 self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.actualWindowSize = None # segment ack will set value + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # deliver to the device self.request(self.get_segment(0)) @@ -350,7 +393,7 @@ def response(self, apdu): if _debug: ClientSSM._debug("response %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it to the application @@ -407,29 +450,31 @@ def segmented_request(self, apdu): and receives an apdu.""" if _debug: ClientSSM._debug("segmented_request %r", apdu) - # client is ready for the next segment + # server is ready for the next segment if apdu.apduType == SegmentAckPDU.pduType: if _debug: ClientSSM._debug(" - segment ack") + # actual window size is provided by server + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ClientSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: if _debug: ClientSSM._debug(" - all done sending request") - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) # more segments to send else: if _debug: ClientSSM._debug(" - more segments to send") self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 - self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # simple ack elif (apdu.apduType == SimpleAckPDU.pduType): @@ -452,6 +497,7 @@ def segmented_request(self, apdu): self.response(abort) # send it to the application elif not apdu.apduSeg: + # ack is not segmented self.set_state(COMPLETED) self.response(apdu) @@ -459,10 +505,11 @@ def segmented_request(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # minimum of what the server is proposing and this client proposes + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # some kind of problem elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType): @@ -479,12 +526,12 @@ def segmented_request_timeout(self): if _debug: ClientSSM._debug("segmented_request_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: if _debug: ClientSSM._debug(" - retry segmented request") self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: if _debug: ClientSSM._debug(" - abort, no response from the device") @@ -516,7 +563,7 @@ def await_confirmation(self, apdu): self.set_state(COMPLETED) self.response(apdu) - elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + elif self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't receive segmented messages") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) @@ -527,10 +574,10 @@ def await_confirmation(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + self.actualWindowSize = apdu.apduWin self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # send back a segment ack segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) @@ -546,7 +593,7 @@ def await_confirmation(self, apdu): elif (apdu.apduType == SegmentAckPDU.pduType): if _debug: ClientSSM._debug(" - segment ack(!?)") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) else: raise RuntimeError("invalid APDU (3)") @@ -554,9 +601,9 @@ def await_confirmation(self, apdu): def await_confirmation_timeout(self): if _debug: ClientSSM._debug("await_confirmation_timeout") - self.retryCount += 1 - if self.retryCount < self.ssmSAP.retryCount: - if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.ssmSAP.retryCount) + if self.retryCount < self.numberOfApduRetries: + if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.numberOfApduRetries) + self.retryCount += 1 # save the retry count, indication acts like the request is coming # from the application so the retryCount gets re-initialized. @@ -594,8 +641,8 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - segment %s received out of order, should be %s", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) return @@ -610,7 +657,7 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - no more follows") # send a final ack - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) self.set_state(COMPLETED) @@ -620,15 +667,15 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) else: # wait for more segments if _debug: ClientSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_confirmation_timeout(self): if _debug: ClientSSM._debug("segmented_confirmation_timeout") @@ -644,9 +691,14 @@ def segmented_confirmation_timeout(self): class ServerSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ServerSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) + + # acquire the device info + if self.device_info: + if _debug: ServerSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" @@ -660,8 +712,10 @@ def set_state(self, newState, timer=0): if _debug: ServerSSM._debug(" - remove from active transactions") self.ssmSAP.serverTransactions.remove(self) - if _debug: ServerSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by transaction functions to send @@ -669,7 +723,7 @@ def request(self, apdu): if _debug: ServerSSM._debug("request %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it via the device @@ -698,7 +752,7 @@ def response(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -740,14 +794,15 @@ def confirmation(self, apdu): # save the response and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit - # (assumed to have no local buffer limitations), the maximum conveyable - # by the internetwork to the remote device, and the maximum APDU size - # accepted by the remote device. - self.segmentSize = min(self.remoteDevice.maxNpduLength, self.remoteDevice.maxApduLengthAccepted) + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the client and the largest it can accept + if (not self.device_info) or (self.device_info.maxNpduLength is None): + self.segmentSize = self.maxApduLengthAccepted + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.maxApduLengthAccepted) if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -763,26 +818,31 @@ def confirmation(self, apdu): if _debug: ServerSSM._debug(" - segmentation required, %d segments", self.segmentCount) # make sure we support segmented transmit - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ServerSSM._debug(" - server can't send segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # make sure client supports segmented receive - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ServerSSM._debug(" - client can't receive segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our response + # that the device said it was willing to accept in the request + if self.segmentCount > self.maxSegmentsAccepted: + if _debug: ServerSSM._debug(" - client can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # initialize the state self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 + self.actualWindowSize = None # send out the first segment (or the whole thing) if self.segmentCount == 1: @@ -790,7 +850,7 @@ def confirmation(self, apdu): self.set_state(COMPLETED) else: self.response(self.get_segment(0)) - self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_RESPONSE, self.segmentTimeout) else: raise RuntimeError("invalid APDU (4)") @@ -838,38 +898,48 @@ def idle(self, apdu): self.invokeID = apdu.apduInvokeID if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID) - # make sure the device information is synced with the request if apdu.apduSA: - if self.remoteDevice.segmentationSupported == 'noSegmentation': + if not self.device_info: + if _debug: ServerSSM._debug(" - no client device info") + + elif self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") - self.remoteDevice.segmentationSupported = 'segmentedReceive' + self.device_info.segmentationSupported = 'segmentedReceive' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedTransmit': + elif self.device_info.segmentationSupported == 'segmentedTransmit': if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive") - self.remoteDevice.segmentationSupported = 'segmentedBoth' + self.device_info.segmentationSupported = 'segmentedBoth' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedReceive': + elif self.device_info.segmentationSupported == 'segmentedReceive': pass - elif self.remoteDevice.segmentationSupported == 'segmentedBoth': + elif self.device_info.segmentationSupported == 'segmentedBoth': pass else: raise RuntimeError("invalid segmentation supported in device info") - if apdu.apduMaxSegs != self.remoteDevice.maxSegmentsAccepted: - if _debug: ServerSSM._debug(" - update maximum segments accepted?") - if apdu.apduMaxResp != self.remoteDevice.maxApduLengthAccepted: - if _debug: ServerSSM._debug(" - update maximum max APDU length accepted?") + # decode the maximum that the client can receive in one APDU, and if + # there is a value in the device information then use that one because + # it came from reading device object property value or from an I-Am + # message that was received + self.maxApduLengthAccepted = decode_max_apdu_length_accepted(apdu.apduMaxResp) + if self.device_info and self.device_info.maxApduLengthAccepted is not None: + if self.device_info.maxApduLengthAccepted < self.maxApduLengthAccepted: + if _debug: ServerSSM._debug(" - apduMaxResp encoding error") + else: + self.maxApduLengthAccepted = self.device_info.maxApduLengthAccepted + if _debug: ServerSSM._debug(" - maxApduLengthAccepted: %r", self.maxApduLengthAccepted) - # save the number of segments the client is willing to accept in the ack - self.maxSegmentsAccepted = apdu.apduMaxSegs + # save the number of segments the client is willing to accept in the ack, + # if this is None then the value is unknown or more than 64 + self.maxSegmentsAccepted = decode_max_segments_accepted(apdu.apduMaxSegs) # unsegmented request if not apdu.apduSeg: @@ -878,7 +948,7 @@ def idle(self, apdu): return # make sure we support segmented requests - if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return @@ -886,17 +956,18 @@ def idle(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the window size is the minimum of what I'm willing to receive and - # what the device has said it would like to send - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # the window size is the minimum of what I would propose and what the + # device has proposed + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + if _debug: ServerSSM._debug(" - actualWindowSize? min(%r, %r) -> %r", apdu.apduWin, self.proposedWindowSize, self.actualWindowSize) # initialize the state self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) if _debug: ServerSSM._debug(" - segAck: %r", segack) self.response(segack) @@ -929,10 +1000,10 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - segment %d received out of order, should be %d", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) return @@ -948,7 +1019,7 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - no more follows") # send back a final segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.response(segack) # forward the whole thing to the application @@ -959,17 +1030,17 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) else: # wait for more segments if _debug: ServerSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_request_timeout(self): if _debug: ServerSSM._debug("segmented_request_timeout") @@ -1009,10 +1080,13 @@ def segmented_response(self, apdu): if (apdu.apduType == SegmentAckPDU.pduType): if _debug: ServerSSM._debug(" - segment ack") + # actual window size is provided by client + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ServerSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: @@ -1025,8 +1099,8 @@ def segmented_response(self, apdu): self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # some kind of problem elif (apdu.apduType == AbortPDU.pduType): @@ -1040,10 +1114,10 @@ def segmented_response_timeout(self): if _debug: ServerSSM._debug("segmented_response_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: # give up self.set_state(ABORTED) @@ -1064,6 +1138,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): ServiceAccessPoint.__init__(self, sap) # save a reference to the device information cache + self.localDevice = localDevice self.deviceInfoCache = deviceInfoCache # client settings @@ -1074,27 +1149,19 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): self.serverTransactions = [] # confirmed request defaults - self.retryCount = 3 - self.retryTimeout = 3000 + self.numberOfApduRetries = 3 + self.apduTimeout = 3000 self.maxApduLengthAccepted = 1024 # segmentation defaults self.segmentationSupported = 'noSegmentation' self.segmentTimeout = 1500 - self.maxSegmentsAccepted = 8 + self.maxSegmentsAccepted = 2 + self.proposedWindowSize = 2 # device communication control self.dccEnableDisable = 'enable' - # local device object provides these - if localDevice: - self.retryCount = localDevice.numberOfApduRetries - self.retryTimeout = localDevice.apduTimeout - self.segmentationSupported = localDevice.segmentationSupported - self.segmentTimeout = localDevice.apduSegmentTimeout - self.maxSegmentsAccepted = localDevice.maxSegmentsAccepted - self.maxApduLengthAccepted = localDevice.maxApduLengthAccepted - # how long the state machine is willing to wait for the application # layer to form a response and send it self.applicationTimeout = 3000 @@ -1113,7 +1180,7 @@ def get_next_invoke_id(self, addr): raise RuntimeError("no available invoke ID") for tr in self.clientTransactions: - if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address): + if (invokeID == tr.invokeID) and (addr == tr.pdu_address): break else: break @@ -1154,14 +1221,11 @@ def confirmation(self, pdu): if isinstance(apdu, ConfirmedRequestPDU): # find duplicates of this request for tr in self.serverTransactions: - if (apdu.pduSource == tr.remoteDevice.address) and (apdu.apduInvokeID == tr.invokeID): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource) - # build a server transaction - tr = ServerSSM(self, remoteDevice) + tr = ServerSSM(self, apdu.pduSource) # add it to our transactions to track it self.serverTransactions.append(tr) @@ -1180,7 +1244,7 @@ def confirmation(self, pdu): # find the client transaction this is acking for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1192,7 +1256,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1201,7 +1265,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1213,7 +1277,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1222,7 +1286,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1266,19 +1330,15 @@ def sap_indication(self, apdu): else: # verify the invoke ID isn't already being used for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): raise RuntimeError("invoke ID in use") # warning for bogus requests if (apdu.pduDestination.addrType != Address.localStationAddr) and (apdu.pduDestination.addrType != Address.remoteStationAddr): StateMachineAccessPoint._warning("%s is not a local or remote station", apdu.pduDestination) - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduDestination) - if _debug: StateMachineAccessPoint._debug(" - remoteDevice: %r", remoteDevice) - # create a client transaction state machine - tr = ClientSSM(self, remoteDevice) + tr = ClientSSM(self, apdu.pduDestination) if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr) # add it to our transactions to track it @@ -1302,7 +1362,7 @@ def sap_confirmation(self, apdu): or isinstance(apdu, AbortPDU): # find the appropriate server transaction for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): break else: return @@ -1344,10 +1404,10 @@ def indication(self, apdu): try: xpdu = atype() xpdu.decode(apdu) - except RejectException as err: + except RejectException, err: ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) error_found = err - except AbortException as err: + except AbortException, err: ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) error_found = err diff --git a/py25/bacpypes/task.py b/py25/bacpypes/task.py index a761da4c..04096bbb 100755 --- a/py25/bacpypes/task.py +++ b/py25/bacpypes/task.py @@ -8,6 +8,7 @@ from time import time as _time from heapq import heapify, heappush, heappop +import itertools from .singleton import SingletonLogging from .debugging import DebugContents, Logging, ModuleLogger, bacpypes_debugging @@ -280,6 +281,9 @@ def __init__(self): # task manager is this instance _task_manager = self + # unique sequence counter for tasks scheduled at the same time + self.counter = itertools.count() + # there may be tasks created that couldn't be scheduled # because a task manager wasn't created yet. if _unscheduled_tasks: @@ -304,7 +308,7 @@ def install_task(self, task): self.suspend_task(task) # save this in the task list - heappush( self.tasks, (task.taskTime, task) ) + heappush( self.tasks, (task.taskTime, next(self.counter), task) ) if _debug: TaskManager._debug(" - tasks: %r", self.tasks) task.isScheduled = True @@ -317,7 +321,7 @@ def suspend_task(self, task): if _debug: TaskManager._debug("suspend_task %r", task) # remove this guy - for i, (when, curtask) in enumerate(self.tasks): + for i, (when, n, curtask) in enumerate(self.tasks): if task is curtask: if _debug: TaskManager._debug(" - task found") del self.tasks[i] @@ -352,7 +356,7 @@ def get_next_task(self): if self.tasks: # look at the first task - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] if when <= now: # pull it off the list and mark that it's no longer scheduled heappop(self.tasks) @@ -360,7 +364,7 @@ def get_next_task(self): task.isScheduled = False if self.tasks: - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] # peek at the next task, return how long to wait delta = max(when - now, 0.0) else: diff --git a/py25/bacpypes/vlan.py b/py25/bacpypes/vlan.py index e8e9fc29..fef75358 100755 --- a/py25/bacpypes/vlan.py +++ b/py25/bacpypes/vlan.py @@ -216,8 +216,9 @@ class IPRouterNode(Client): def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) - # save the reference to the router + # save the references to the router for packets and the lan for debugging self.router = router + self.lan = lan # make ourselves an IPNode and bind to it self.node = IPNode(addr, lan=lan, promiscuous=True, spoofing=True) diff --git a/py27/bacpypes/apdu.py b/py27/bacpypes/apdu.py index b0fd8e8f..c19a811e 100755 --- a/py27/bacpypes/apdu.py +++ b/py27/bacpypes/apdu.py @@ -56,38 +56,51 @@ def register_error_type(klass): # encode_max_segments_accepted/decode_max_segments_accepted # +_max_segments_accepted_encoding = [ + None, 2, 4, 8, 16, 32, 64, None, + ] + def encode_max_segments_accepted(arg): """Encode the maximum number of segments the device will accept, Section - 20.1.2.4""" - w = 0 - while (arg and not arg & 1): - w += 1 - arg = (arg >> 1) - return w + 20.1.2.4, and if the device says it can only accept one segment it shouldn't + say that it supports segmentation!""" + # unspecified + if not arg: + return 0 + + if arg > 64: + return 7 + + # the largest number not greater than the arg + for i in range(6, 0, -1): + if _max_segments_accepted_encoding[i] <= arg: + return i + + raise ValueError("invalid max max segments accepted: {0}".format(arg)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section 20.1.2.4""" - return arg and (1 << arg) or None + return _max_segments_accepted_encoding[arg] # # encode_max_apdu_length_accepted/decode_max_apdu_length_accepted # -_max_apdu_response_encoding = [50, 128, 206, 480, 1024, 1476, None, None, +_max_apdu_length_encoding = [50, 128, 206, 480, 1024, 1476, None, None, None, None, None, None, None, None, None, None] def encode_max_apdu_length_accepted(arg): """Return the encoding of the highest encodable value less than the value of the arg.""" for i in range(5, -1, -1): - if (arg >= _max_apdu_response_encoding[i]): + if (arg >= _max_apdu_length_encoding[i]): return i raise ValueError("invalid max APDU length accepted: {0}".format(arg)) def decode_max_apdu_length_accepted(arg): - v = _max_apdu_response_encoding[arg] + v = _max_apdu_length_encoding[arg] if not v: raise ValueError("invalid max APDU length accepted: {0}".format(arg)) @@ -174,7 +187,7 @@ def encode(self, pdu): if self.apduSA: buff += 0x02 pdu.put(buff) - pdu.put((encode_max_segments_accepted(self.apduMaxSegs) << 4) + encode_max_apdu_length_accepted(self.apduMaxResp)) + pdu.put((self.apduMaxSegs << 4) + self.apduMaxResp) pdu.put(self.apduInvokeID) if self.apduSeg: pdu.put(self.apduSeq) @@ -255,8 +268,8 @@ def decode(self, pdu): self.apduMor = ((buff & 0x04) != 0) self.apduSA = ((buff & 0x02) != 0) buff = pdu.get() - self.apduMaxSegs = decode_max_segments_accepted( (buff >> 4) & 0x07 ) - self.apduMaxResp = decode_max_apdu_length_accepted( buff & 0x0F ) + self.apduMaxSegs = (buff >> 4) & 0x07 + self.apduMaxResp = buff & 0x0F self.apduInvokeID = pdu.get() if self.apduSeg: self.apduSeq = pdu.get() diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index a1081497..a66dffd7 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -26,7 +26,8 @@ # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ - ConfirmedServiceChoice, UnconfirmedServiceChoice + ConfirmedServiceChoice, UnconfirmedServiceChoice, \ + IAmRequest from .basetypes import ServicesSupported # basic services @@ -54,16 +55,16 @@ class DeviceInfo(DebugContents): 'maxSegmentsAccepted', ) - def __init__(self): + def __init__(self, device_identifier, address): # this information is from an IAmRequest - self.deviceIdentifier = None # device identifier - self.address = None # LocalStation or RemoteStation + self.deviceIdentifier = device_identifier + self.address = address + self.maxApduLengthAccepted = 1024 # maximum APDU device will accept self.segmentationSupported = 'noSegmentation' # normally no segmentation + self.maxSegmentsAccepted = None # None iff no segmentation self.vendorID = None # vendor identifier - - self.maxNpduLength = 1497 # maximum we can send in transit - self.maxSegmentsAccepted = None # value for proposed/actual window size + self.maxNpduLength = None # maximum we can send in transit (see 19.4) # # DeviceInfoCache @@ -72,127 +73,135 @@ def __init__(self): @bacpypes_debugging class DeviceInfoCache: - def __init__(self): + def __init__(self, device_info_class=DeviceInfo): if _debug: DeviceInfoCache._debug("__init__") + # a little error checking + if not issubclass(device_info_class, DeviceInfo): + raise ValueError("not a DeviceInfo subclass: %r" % (device_info_class,)) + # empty cache self.cache = {} + # class for new records + self.device_info_class = device_info_class + def has_device_info(self, key): """Return true iff cache has information about the device.""" if _debug: DeviceInfoCache._debug("has_device_info %r", key) return key in self.cache - def add_device_info(self, apdu): + def iam_device_info(self, apdu): """Create a device information record based on the contents of an IAmRequest and put it in the cache.""" - if _debug: DeviceInfoCache._debug("add_device_info %r", apdu) + if _debug: DeviceInfoCache._debug("iam_device_info %r", apdu) - # get the existing cache record by identifier - info = self.get_device_info(apdu.iAmDeviceIdentifier[1]) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # make sure the apdu is an I-Am + if not isinstance(apdu, IAmRequest): + raise ValueError("not an IAmRequest: %r" % (apdu,)) - # update existing record - if info: - if (info.address == apdu.pduSource): - return + # get the device instance + device_instance = apdu.iAmDeviceIdentifier[1] - info.address = apdu.pduSource - else: - # get the existing record by address (creates a new record) - info = self.get_device_info(apdu.pduSource) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # get the existing cache record if it exists + device_info = self.cache.get(device_instance, None) + + # maybe there is a record for this address + if not device_info: + device_info = self.cache.get(apdu.pduSource, None) - info.deviceIdentifier = apdu.iAmDeviceIdentifier[1] + # make a new one using the class provided + if not device_info: + device_info = self.device_info_class(device_instance, apdu.pduSource) - # update the rest of the values - info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted - info.segmentationSupported = apdu.segmentationSupported - info.vendorID = apdu.vendorID + # jam in the correct values + device_info.deviceIdentifier = device_instance + device_info.address = apdu.pduSource + device_info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted + device_info.segmentationSupported = apdu.segmentationSupported + device_info.vendorID = apdu.vendorID - # say this is an updated record - self.update_device_info(info) + # tell the cache this is an updated record + self.update_device_info(device_info) def get_device_info(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" if _debug: DeviceInfoCache._debug("get_device_info %r", key) - if isinstance(key, int): - current_info = self.cache.get(key, None) - - elif not isinstance(key, Address): - raise TypeError("key must be integer or an address") + # get the info if it's there + device_info = self.cache.get(key, None) + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): - raise TypeError("address must be a local or remote station") + return device_info - else: - current_info = self.cache.get(key, None) - if not current_info: - current_info = DeviceInfo() - current_info.address = key - current_info._cache_keys = (None, key) - current_info._ref_count = 1 - - self.cache[key] = current_info - else: - if _debug: DeviceInfoCache._debug(" - reference bump") - current_info._ref_count += 1 - - if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) - - return current_info - - def update_device_info(self, info): + def update_device_info(self, device_info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" - if _debug: DeviceInfoCache._debug("update_device_info %r", info) + if _debug: DeviceInfoCache._debug("update_device_info %r", device_info) - cache_id, cache_address = info._cache_keys + # give this a reference count if it doesn't have one + if not hasattr(device_info, '_ref_count'): + device_info._ref_count = 0 - if (cache_id is not None) and (info.deviceIdentifier != cache_id): + # get the current keys + cache_id, cache_address = getattr(device_info, '_cache_keys', (None, None)) + + if (cache_id is not None) and (device_info.deviceIdentifier != cache_id): if _debug: DeviceInfoCache._debug(" - device identifier updated") # remove the old reference, add the new one del self.cache[cache_id] - self.cache[info.deviceIdentifier] = info - - cache_id = info.deviceIdentifier + self.cache[device_info.deviceIdentifier] = device_info - if (cache_address is not None) and (info.address != cache_address): + if (cache_address is not None) and (device_info.address != cache_address): if _debug: DeviceInfoCache._debug(" - device address updated") # remove the old reference, add the new one del self.cache[cache_address] - self.cache[info.address] = info - - cache_address = info.address + self.cache[device_info.address] = device_info # update the keys - info._cache_keys = (cache_id, cache_address) + device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) + + def acquire(self, key): + """Return the known information about the device. If the key is the + address of an unknown device, build a generic device information record + add put it in the cache.""" + if _debug: DeviceInfoCache._debug("acquire %r", key) - def release_device_info(self, info): + if isinstance(key, int): + device_info = self.cache.get(key, None) + + elif not isinstance(key, Address): + raise TypeError("key must be integer or an address") + + elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): + raise TypeError("address must be a local or remote station") + + else: + device_info = self.cache.get(key, None) + + if device_info: + if _debug: DeviceInfoCache._debug(" - reference bump") + device_info._ref_count += 1 + + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) + + return device_info + + def release(self, device_info): """This function is called by the segmentation state machine when it has finished with the device information.""" - if _debug: DeviceInfoCache._debug("release_device_info %r", info) + if _debug: DeviceInfoCache._debug("release %r", device_info) # this information record might be used by more than one SSM - if info._ref_count > 1: - if _debug: DeviceInfoCache._debug(" - multiple references") - info._ref_count -= 1 - return + if device_info._ref_count == 0: + raise RuntimeError("reference count") - cache_id, cache_address = info._cache_keys - if cache_id is not None: - del self.cache[cache_id] - if cache_address is not None: - del self.cache[cache_address] - if _debug: DeviceInfoCache._debug(" - released") + # decrement the reference count + device_info._ref_count -= 1 # # Application @@ -260,9 +269,9 @@ def add_object(self, obj): # make sure it hasn't already been defined if object_name in self.objectName: - raise RuntimeError("already an object with name {0!r}".format(object_name)) + raise RuntimeError("already an object with name %r" % (object_name,)) if object_identifier in self.objectIdentifier: - raise RuntimeError("already an object with identifier {0!r}".format(object_identifier)) + raise RuntimeError("already an object with identifier %r" % (object_identifier,)) # now put it in local dictionaries self.objectName[object_name] = obj diff --git a/py27/bacpypes/appservice.py b/py27/bacpypes/appservice.py index 1ebfc891..641e6554 100755 --- a/py27/bacpypes/appservice.py +++ b/py27/bacpypes/appservice.py @@ -12,7 +12,9 @@ from .task import OneShotTask from .pdu import Address -from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \ +from .apdu import encode_max_segments_accepted, decode_max_segments_accepted, \ + encode_max_apdu_length_accepted, decode_max_apdu_length_accepted, \ + AbortPDU, AbortReason, ComplexAckPDU, \ ConfirmedRequestPDU, Error, ErrorPDU, RejectPDU, SegmentAckPDU, \ SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \ unconfirmed_request_types, confirmed_request_types, complex_ack_types, \ @@ -45,19 +47,23 @@ class SSM(OneShotTask, DebugContents): , 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED' ] - _debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID' + _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' ) - def __init__(self, sap, remoteDevice): + def __init__(self, sap, pdu_address): """Common parts for client and server segmentation.""" - if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice) + if _debug: SSM._debug("__init__ %r %r", sap, pdu_address) OneShotTask.__init__(self) self.ssmSAP = sap # service access point - self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance + + # save the address and get the device information + self.pdu_address = pdu_address + self.device_info = sap.deviceInfoCache.get_device_info(pdu_address) + self.invokeID = None # invoke ID self.state = IDLE # initial state @@ -71,11 +77,17 @@ def __init__(self, sap, remoteDevice): self.lastSequenceNumber = None self.initialSequenceNumber = None self.actualWindowSize = None - self.proposedWindowSize = None - # the maximum number of segments starts out being what's in the SAP - # which is the defaults or values from the local device. - self.maxSegmentsAccepted = self.ssmSAP.maxSegmentsAccepted + # local device object provides these or SAP provides defaults, make + # copies here so they are consistent throughout the transaction but + # they could change from one transaction to the next + self.numberOfApduRetries = getattr(sap.localDevice, 'numberOfApduRetries', sap.numberOfApduRetries) + self.apduTimeout = getattr(sap.localDevice, 'apduTimeout', sap.apduTimeout) + + self.segmentationSupported = getattr(sap.localDevice, 'segmentationSupported', sap.segmentationSupported) + self.segmentTimeout = getattr(sap.localDevice, 'segmentTimeout', sap.segmentTimeout) + self.maxSegmentsAccepted = getattr(sap.localDevice, 'maxSegmentsAccepted', sap.maxSegmentsAccepted) + self.maxApduLengthAccepted = getattr(sap.localDevice, 'maxApduLengthAccepted', sap.maxApduLengthAccepted) def start_timer(self, msecs): if _debug: SSM._debug("start_timer %r", msecs) @@ -146,19 +158,19 @@ def get_segment(self, indx): # check for invalid segment number if indx >= self.segmentCount: - raise RuntimeError("invalid segment number {0}, APDU has {1} segments".format(indx, self.segmentCount)) + raise RuntimeError("invalid segment number %r, APDU has %r segments" % (indx, self.segmentCount)) if self.segmentAPDU.apduType == ConfirmedRequestPDU.pduType: if _debug: SSM._debug(" - confirmed request context") segAPDU = ConfirmedRequestPDU(self.segmentAPDU.apduService) - segAPDU.apduMaxSegs = self.maxSegmentsAccepted - segAPDU.apduMaxResp = self.ssmSAP.maxApduLengthAccepted - segAPDU.apduInvokeID = self.invokeID; + segAPDU.apduMaxSegs = encode_max_segments_accepted(self.maxSegmentsAccepted) + segAPDU.apduMaxResp = encode_max_apdu_length_accepted(self.maxApduLengthAccepted) + segAPDU.apduInvokeID = self.invokeID # segmented response accepted? - segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth') + segAPDU.apduSA = self.segmentationSupported in ('segmentedReceive', 'segmentedBoth') if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA) elif self.segmentAPDU.apduType == ComplexAckPDU.pduType: @@ -172,14 +184,21 @@ def get_segment(self, indx): segAPDU.pduUserData = self.segmentAPDU.pduUserData # make sure the destination is set - segAPDU.pduDestination = self.remoteDevice.address + segAPDU.pduDestination = self.pdu_address # segmented message? if (self.segmentCount != 1): segAPDU.apduSeg = True segAPDU.apduMor = (indx < (self.segmentCount - 1)) # more follows segAPDU.apduSeq = indx % 256 # sequence number - segAPDU.apduWin = self.proposedWindowSize # window size + + # first segment sends proposed window size, rest get actual + if indx == 0: + if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) + segAPDU.apduWin = self.proposedWindowSize + else: + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) + segAPDU.apduWin = self.actualWindowSize else: segAPDU.apduSeg = False segAPDU.apduMor = False @@ -211,10 +230,11 @@ def in_window(self, seqA, seqB): return rslt - def FillWindow(self, seqNum): + def fill_window(self, seqNum): """This function sends all of the packets necessary to fill out the segmentation window.""" - if _debug: SSM._debug("FillWindow %r", seqNum) + if _debug: SSM._debug("fill_window %r", seqNum) + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) for ix in range(self.actualWindowSize): apdu = self.get_segment(seqNum + ix) @@ -234,13 +254,18 @@ def FillWindow(self, seqNum): @bacpypes_debugging class ClientSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ClientSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) # initialize the retry count self.retryCount = 0 + # acquire the device info + if self.device_info: + if _debug: ClientSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) + def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" if _debug: ClientSSM._debug("set_state %r (%s) timer=%r", newState, SSM.transactionLabels[newState], timer) @@ -253,8 +278,10 @@ def set_state(self, newState, timer=0): if _debug: ClientSSM._debug(" - remove from active transactions") self.ssmSAP.clientTransactions.remove(self) - if _debug: ClientSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by client transaction functions when it wants @@ -263,7 +290,7 @@ def request(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -280,26 +307,27 @@ def indication(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit, - # the maximum conveyable by the internetwork to the remote device, and - # the maximum APDU size accepted by the remote device. - self.segmentSize = min( - self.ssmSAP.maxApduLengthAccepted, - self.remoteDevice.maxNpduLength, - self.remoteDevice.maxApduLengthAccepted, - ) - if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) + # if the max apdu length of the server isn't known, assume that it + # is the same size as our own and will be the segment size + if (not self.device_info) or (self.device_info.maxApduLengthAccepted is None): + self.segmentSize = self.maxApduLengthAccepted - # the maximum number of segments acceptable in the reply - if apdu.apduMaxSegs is not None: - # this request overrides the default - self.maxSegmentsAccepted = apdu.apduMaxSegs + # if the max npdu length of the server isn't known, assume that it + # is the same as the max apdu length accepted + elif self.device_info.maxNpduLength is None: + self.segmentSize = self.device_info.maxApduLengthAccepted + + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the server and the largest it can accept + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.device_info.maxApduLengthAccepted) + if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) # save the invoke ID self.invokeID = apdu.apduInvokeID if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -312,34 +340,49 @@ def indication(self, apdu): # make sure we support segmented transmit if we need to if self.segmentCount > 1: - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't send segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): - if _debug: ClientSSM._debug(" - remote device can't receive segmented requests") + + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for segmentation support") + + elif self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if _debug: ClientSSM._debug(" - server can't receive segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our request + # that the server said it was willing to accept + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for maximum number of segments") + + elif not self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server doesn't say maximum number of segments") + + elif self.segmentCount > self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # send out the first segment (or the whole thing) if self.segmentCount == 1: - # SendConfirmedUnsegmented + # unsegmented self.sentAllSegments = True self.retryCount = 0 - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) else: - # SendConfirmedSegmented + # segmented self.sentAllSegments = False self.retryCount = 0 self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.actualWindowSize = None # segment ack will set value + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # deliver to the device self.request(self.get_segment(0)) @@ -350,7 +393,7 @@ def response(self, apdu): if _debug: ClientSSM._debug("response %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it to the application @@ -407,29 +450,31 @@ def segmented_request(self, apdu): and receives an apdu.""" if _debug: ClientSSM._debug("segmented_request %r", apdu) - # client is ready for the next segment + # server is ready for the next segment if apdu.apduType == SegmentAckPDU.pduType: if _debug: ClientSSM._debug(" - segment ack") + # actual window size is provided by server + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ClientSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: if _debug: ClientSSM._debug(" - all done sending request") - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) # more segments to send else: if _debug: ClientSSM._debug(" - more segments to send") self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 - self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # simple ack elif (apdu.apduType == SimpleAckPDU.pduType): @@ -452,6 +497,7 @@ def segmented_request(self, apdu): self.response(abort) # send it to the application elif not apdu.apduSeg: + # ack is not segmented self.set_state(COMPLETED) self.response(apdu) @@ -459,10 +505,11 @@ def segmented_request(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # minimum of what the server is proposing and this client proposes + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # some kind of problem elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType): @@ -479,12 +526,12 @@ def segmented_request_timeout(self): if _debug: ClientSSM._debug("segmented_request_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: if _debug: ClientSSM._debug(" - retry segmented request") self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: if _debug: ClientSSM._debug(" - abort, no response from the device") @@ -516,7 +563,7 @@ def await_confirmation(self, apdu): self.set_state(COMPLETED) self.response(apdu) - elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + elif self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't receive segmented messages") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) @@ -527,10 +574,10 @@ def await_confirmation(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + self.actualWindowSize = apdu.apduWin self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # send back a segment ack segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) @@ -546,7 +593,7 @@ def await_confirmation(self, apdu): elif (apdu.apduType == SegmentAckPDU.pduType): if _debug: ClientSSM._debug(" - segment ack(!?)") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) else: raise RuntimeError("invalid APDU (3)") @@ -554,9 +601,9 @@ def await_confirmation(self, apdu): def await_confirmation_timeout(self): if _debug: ClientSSM._debug("await_confirmation_timeout") - self.retryCount += 1 - if self.retryCount < self.ssmSAP.retryCount: - if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.ssmSAP.retryCount) + if self.retryCount < self.numberOfApduRetries: + if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.numberOfApduRetries) + self.retryCount += 1 # save the retry count, indication acts like the request is coming # from the application so the retryCount gets re-initialized. @@ -594,8 +641,8 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - segment %s received out of order, should be %s", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) return @@ -610,7 +657,7 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - no more follows") # send a final ack - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) self.set_state(COMPLETED) @@ -620,15 +667,15 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) else: # wait for more segments if _debug: ClientSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_confirmation_timeout(self): if _debug: ClientSSM._debug("segmented_confirmation_timeout") @@ -643,9 +690,14 @@ def segmented_confirmation_timeout(self): @bacpypes_debugging class ServerSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ServerSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) + + # acquire the device info + if self.device_info: + if _debug: ServerSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" @@ -659,8 +711,10 @@ def set_state(self, newState, timer=0): if _debug: ServerSSM._debug(" - remove from active transactions") self.ssmSAP.serverTransactions.remove(self) - if _debug: ServerSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by transaction functions to send @@ -668,7 +722,7 @@ def request(self, apdu): if _debug: ServerSSM._debug("request %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it via the device @@ -697,7 +751,7 @@ def response(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -739,14 +793,15 @@ def confirmation(self, apdu): # save the response and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit - # (assumed to have no local buffer limitations), the maximum conveyable - # by the internetwork to the remote device, and the maximum APDU size - # accepted by the remote device. - self.segmentSize = min(self.remoteDevice.maxNpduLength, self.remoteDevice.maxApduLengthAccepted) + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the client and the largest it can accept + if (not self.device_info) or (self.device_info.maxNpduLength is None): + self.segmentSize = self.maxApduLengthAccepted + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.maxApduLengthAccepted) if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -762,26 +817,31 @@ def confirmation(self, apdu): if _debug: ServerSSM._debug(" - segmentation required, %d segments", self.segmentCount) # make sure we support segmented transmit - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ServerSSM._debug(" - server can't send segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # make sure client supports segmented receive - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ServerSSM._debug(" - client can't receive segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our response + # that the device said it was willing to accept in the request + if self.segmentCount > self.maxSegmentsAccepted: + if _debug: ServerSSM._debug(" - client can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # initialize the state self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 + self.actualWindowSize = None # send out the first segment (or the whole thing) if self.segmentCount == 1: @@ -789,7 +849,7 @@ def confirmation(self, apdu): self.set_state(COMPLETED) else: self.response(self.get_segment(0)) - self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_RESPONSE, self.segmentTimeout) else: raise RuntimeError("invalid APDU (4)") @@ -837,38 +897,48 @@ def idle(self, apdu): self.invokeID = apdu.apduInvokeID if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID) - # make sure the device information is synced with the request if apdu.apduSA: - if self.remoteDevice.segmentationSupported == 'noSegmentation': + if not self.device_info: + if _debug: ServerSSM._debug(" - no client device info") + + elif self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") - self.remoteDevice.segmentationSupported = 'segmentedReceive' + self.device_info.segmentationSupported = 'segmentedReceive' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedTransmit': + elif self.device_info.segmentationSupported == 'segmentedTransmit': if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive") - self.remoteDevice.segmentationSupported = 'segmentedBoth' + self.device_info.segmentationSupported = 'segmentedBoth' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedReceive': + elif self.device_info.segmentationSupported == 'segmentedReceive': pass - elif self.remoteDevice.segmentationSupported == 'segmentedBoth': + elif self.device_info.segmentationSupported == 'segmentedBoth': pass else: raise RuntimeError("invalid segmentation supported in device info") - if apdu.apduMaxSegs != self.remoteDevice.maxSegmentsAccepted: - if _debug: ServerSSM._debug(" - update maximum segments accepted?") - if apdu.apduMaxResp != self.remoteDevice.maxApduLengthAccepted: - if _debug: ServerSSM._debug(" - update maximum max APDU length accepted?") + # decode the maximum that the client can receive in one APDU, and if + # there is a value in the device information then use that one because + # it came from reading device object property value or from an I-Am + # message that was received + self.maxApduLengthAccepted = decode_max_apdu_length_accepted(apdu.apduMaxResp) + if self.device_info and self.device_info.maxApduLengthAccepted is not None: + if self.device_info.maxApduLengthAccepted < self.maxApduLengthAccepted: + if _debug: ServerSSM._debug(" - apduMaxResp encoding error") + else: + self.maxApduLengthAccepted = self.device_info.maxApduLengthAccepted + if _debug: ServerSSM._debug(" - maxApduLengthAccepted: %r", self.maxApduLengthAccepted) - # save the number of segments the client is willing to accept in the ack - self.maxSegmentsAccepted = apdu.apduMaxSegs + # save the number of segments the client is willing to accept in the ack, + # if this is None then the value is unknown or more than 64 + self.maxSegmentsAccepted = decode_max_segments_accepted(apdu.apduMaxSegs) # unsegmented request if not apdu.apduSeg: @@ -877,7 +947,7 @@ def idle(self, apdu): return # make sure we support segmented requests - if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return @@ -885,17 +955,18 @@ def idle(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the window size is the minimum of what I'm willing to receive and - # what the device has said it would like to send - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # the window size is the minimum of what I would propose and what the + # device has proposed + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + if _debug: ServerSSM._debug(" - actualWindowSize? min(%r, %r) -> %r", apdu.apduWin, self.proposedWindowSize, self.actualWindowSize) # initialize the state self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) if _debug: ServerSSM._debug(" - segAck: %r", segack) self.response(segack) @@ -928,10 +999,10 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - segment %d received out of order, should be %d", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) return @@ -947,7 +1018,7 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - no more follows") # send back a final segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.response(segack) # forward the whole thing to the application @@ -958,17 +1029,17 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) else: # wait for more segments if _debug: ServerSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_request_timeout(self): if _debug: ServerSSM._debug("segmented_request_timeout") @@ -1008,10 +1079,13 @@ def segmented_response(self, apdu): if (apdu.apduType == SegmentAckPDU.pduType): if _debug: ServerSSM._debug(" - segment ack") + # actual window size is provided by client + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ServerSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: @@ -1024,8 +1098,8 @@ def segmented_response(self, apdu): self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # some kind of problem elif (apdu.apduType == AbortPDU.pduType): @@ -1039,10 +1113,10 @@ def segmented_response_timeout(self): if _debug: ServerSSM._debug("segmented_response_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: # give up self.set_state(ABORTED) @@ -1062,6 +1136,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): ServiceAccessPoint.__init__(self, sap) # save a reference to the device information cache + self.localDevice = localDevice self.deviceInfoCache = deviceInfoCache # client settings @@ -1072,27 +1147,19 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): self.serverTransactions = [] # confirmed request defaults - self.retryCount = 3 - self.retryTimeout = 3000 + self.numberOfApduRetries = 3 + self.apduTimeout = 3000 self.maxApduLengthAccepted = 1024 # segmentation defaults self.segmentationSupported = 'noSegmentation' self.segmentTimeout = 1500 - self.maxSegmentsAccepted = 8 + self.maxSegmentsAccepted = 2 + self.proposedWindowSize = 2 # device communication control self.dccEnableDisable = 'enable' - # local device object provides these - if localDevice: - self.retryCount = localDevice.numberOfApduRetries - self.retryTimeout = localDevice.apduTimeout - self.segmentationSupported = localDevice.segmentationSupported - self.segmentTimeout = localDevice.apduSegmentTimeout - self.maxSegmentsAccepted = localDevice.maxSegmentsAccepted - self.maxApduLengthAccepted = localDevice.maxApduLengthAccepted - # how long the state machine is willing to wait for the application # layer to form a response and send it self.applicationTimeout = 3000 @@ -1111,7 +1178,7 @@ def get_next_invoke_id(self, addr): raise RuntimeError("no available invoke ID") for tr in self.clientTransactions: - if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address): + if (invokeID == tr.invokeID) and (addr == tr.pdu_address): break else: break @@ -1152,14 +1219,11 @@ def confirmation(self, pdu): if isinstance(apdu, ConfirmedRequestPDU): # find duplicates of this request for tr in self.serverTransactions: - if (apdu.pduSource == tr.remoteDevice.address) and (apdu.apduInvokeID == tr.invokeID): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource) - # build a server transaction - tr = ServerSSM(self, remoteDevice) + tr = ServerSSM(self, apdu.pduSource) # add it to our transactions to track it self.serverTransactions.append(tr) @@ -1178,7 +1242,7 @@ def confirmation(self, pdu): # find the client transaction this is acking for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1190,7 +1254,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1199,7 +1263,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1211,7 +1275,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1220,7 +1284,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1264,19 +1328,15 @@ def sap_indication(self, apdu): else: # verify the invoke ID isn't already being used for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): raise RuntimeError("invoke ID in use") # warning for bogus requests if (apdu.pduDestination.addrType != Address.localStationAddr) and (apdu.pduDestination.addrType != Address.remoteStationAddr): StateMachineAccessPoint._warning("%s is not a local or remote station", apdu.pduDestination) - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduDestination) - if _debug: StateMachineAccessPoint._debug(" - remoteDevice: %r", remoteDevice) - # create a client transaction state machine - tr = ClientSSM(self, remoteDevice) + tr = ClientSSM(self, apdu.pduDestination) if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr) # add it to our transactions to track it @@ -1300,7 +1360,7 @@ def sap_confirmation(self, apdu): or isinstance(apdu, AbortPDU): # find the appropriate server transaction for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): break else: return diff --git a/py27/bacpypes/task.py b/py27/bacpypes/task.py index c4fec155..e3f405ce 100755 --- a/py27/bacpypes/task.py +++ b/py27/bacpypes/task.py @@ -8,6 +8,7 @@ from time import time as _time from heapq import heapify, heappush, heappop +import itertools from .singleton import SingletonLogging from .debugging import DebugContents, Logging, ModuleLogger, bacpypes_debugging @@ -276,6 +277,9 @@ def __init__(self): # task manager is this instance _task_manager = self + # unique sequence counter for tasks scheduled at the same time + self.counter = itertools.count() + # there may be tasks created that couldn't be scheduled # because a task manager wasn't created yet. if _unscheduled_tasks: @@ -300,7 +304,7 @@ def install_task(self, task): self.suspend_task(task) # save this in the task list - heappush( self.tasks, (task.taskTime, task) ) + heappush( self.tasks, (task.taskTime, next(self.counter), task) ) if _debug: TaskManager._debug(" - tasks: %r", self.tasks) task.isScheduled = True @@ -313,7 +317,7 @@ def suspend_task(self, task): if _debug: TaskManager._debug("suspend_task %r", task) # remove this guy - for i, (when, curtask) in enumerate(self.tasks): + for i, (when, n, curtask) in enumerate(self.tasks): if task is curtask: if _debug: TaskManager._debug(" - task found") del self.tasks[i] @@ -348,7 +352,7 @@ def get_next_task(self): if self.tasks: # look at the first task - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] if when <= now: # pull it off the list and mark that it's no longer scheduled heappop(self.tasks) @@ -356,7 +360,7 @@ def get_next_task(self): task.isScheduled = False if self.tasks: - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] # peek at the next task, return how long to wait delta = max(when - now, 0.0) else: diff --git a/py27/bacpypes/vlan.py b/py27/bacpypes/vlan.py index ea839239..5908e898 100755 --- a/py27/bacpypes/vlan.py +++ b/py27/bacpypes/vlan.py @@ -32,6 +32,7 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.name = name self.nodes = [] + self.broadcast_address = broadcast_address self.drop_percent = drop_percent @@ -216,8 +217,9 @@ class IPRouterNode(Client): def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) - # save the reference to the router + # save the references to the router for packets and the lan for debugging self.router = router + self.lan = lan # make ourselves an IPNode and bind to it self.node = IPNode(addr, lan=lan, promiscuous=True, spoofing=True) @@ -241,6 +243,7 @@ def process_pdu(self, pdu): def __repr__(self): return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + # # IPRouter # diff --git a/py34/bacpypes/apdu.py b/py34/bacpypes/apdu.py index b0fd8e8f..c19a811e 100755 --- a/py34/bacpypes/apdu.py +++ b/py34/bacpypes/apdu.py @@ -56,38 +56,51 @@ def register_error_type(klass): # encode_max_segments_accepted/decode_max_segments_accepted # +_max_segments_accepted_encoding = [ + None, 2, 4, 8, 16, 32, 64, None, + ] + def encode_max_segments_accepted(arg): """Encode the maximum number of segments the device will accept, Section - 20.1.2.4""" - w = 0 - while (arg and not arg & 1): - w += 1 - arg = (arg >> 1) - return w + 20.1.2.4, and if the device says it can only accept one segment it shouldn't + say that it supports segmentation!""" + # unspecified + if not arg: + return 0 + + if arg > 64: + return 7 + + # the largest number not greater than the arg + for i in range(6, 0, -1): + if _max_segments_accepted_encoding[i] <= arg: + return i + + raise ValueError("invalid max max segments accepted: {0}".format(arg)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section 20.1.2.4""" - return arg and (1 << arg) or None + return _max_segments_accepted_encoding[arg] # # encode_max_apdu_length_accepted/decode_max_apdu_length_accepted # -_max_apdu_response_encoding = [50, 128, 206, 480, 1024, 1476, None, None, +_max_apdu_length_encoding = [50, 128, 206, 480, 1024, 1476, None, None, None, None, None, None, None, None, None, None] def encode_max_apdu_length_accepted(arg): """Return the encoding of the highest encodable value less than the value of the arg.""" for i in range(5, -1, -1): - if (arg >= _max_apdu_response_encoding[i]): + if (arg >= _max_apdu_length_encoding[i]): return i raise ValueError("invalid max APDU length accepted: {0}".format(arg)) def decode_max_apdu_length_accepted(arg): - v = _max_apdu_response_encoding[arg] + v = _max_apdu_length_encoding[arg] if not v: raise ValueError("invalid max APDU length accepted: {0}".format(arg)) @@ -174,7 +187,7 @@ def encode(self, pdu): if self.apduSA: buff += 0x02 pdu.put(buff) - pdu.put((encode_max_segments_accepted(self.apduMaxSegs) << 4) + encode_max_apdu_length_accepted(self.apduMaxResp)) + pdu.put((self.apduMaxSegs << 4) + self.apduMaxResp) pdu.put(self.apduInvokeID) if self.apduSeg: pdu.put(self.apduSeq) @@ -255,8 +268,8 @@ def decode(self, pdu): self.apduMor = ((buff & 0x04) != 0) self.apduSA = ((buff & 0x02) != 0) buff = pdu.get() - self.apduMaxSegs = decode_max_segments_accepted( (buff >> 4) & 0x07 ) - self.apduMaxResp = decode_max_apdu_length_accepted( buff & 0x0F ) + self.apduMaxSegs = (buff >> 4) & 0x07 + self.apduMaxResp = buff & 0x0F self.apduInvokeID = pdu.get() if self.apduSeg: self.apduSeq = pdu.get() diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index a1081497..a66dffd7 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -26,7 +26,8 @@ # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ - ConfirmedServiceChoice, UnconfirmedServiceChoice + ConfirmedServiceChoice, UnconfirmedServiceChoice, \ + IAmRequest from .basetypes import ServicesSupported # basic services @@ -54,16 +55,16 @@ class DeviceInfo(DebugContents): 'maxSegmentsAccepted', ) - def __init__(self): + def __init__(self, device_identifier, address): # this information is from an IAmRequest - self.deviceIdentifier = None # device identifier - self.address = None # LocalStation or RemoteStation + self.deviceIdentifier = device_identifier + self.address = address + self.maxApduLengthAccepted = 1024 # maximum APDU device will accept self.segmentationSupported = 'noSegmentation' # normally no segmentation + self.maxSegmentsAccepted = None # None iff no segmentation self.vendorID = None # vendor identifier - - self.maxNpduLength = 1497 # maximum we can send in transit - self.maxSegmentsAccepted = None # value for proposed/actual window size + self.maxNpduLength = None # maximum we can send in transit (see 19.4) # # DeviceInfoCache @@ -72,127 +73,135 @@ def __init__(self): @bacpypes_debugging class DeviceInfoCache: - def __init__(self): + def __init__(self, device_info_class=DeviceInfo): if _debug: DeviceInfoCache._debug("__init__") + # a little error checking + if not issubclass(device_info_class, DeviceInfo): + raise ValueError("not a DeviceInfo subclass: %r" % (device_info_class,)) + # empty cache self.cache = {} + # class for new records + self.device_info_class = device_info_class + def has_device_info(self, key): """Return true iff cache has information about the device.""" if _debug: DeviceInfoCache._debug("has_device_info %r", key) return key in self.cache - def add_device_info(self, apdu): + def iam_device_info(self, apdu): """Create a device information record based on the contents of an IAmRequest and put it in the cache.""" - if _debug: DeviceInfoCache._debug("add_device_info %r", apdu) + if _debug: DeviceInfoCache._debug("iam_device_info %r", apdu) - # get the existing cache record by identifier - info = self.get_device_info(apdu.iAmDeviceIdentifier[1]) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # make sure the apdu is an I-Am + if not isinstance(apdu, IAmRequest): + raise ValueError("not an IAmRequest: %r" % (apdu,)) - # update existing record - if info: - if (info.address == apdu.pduSource): - return + # get the device instance + device_instance = apdu.iAmDeviceIdentifier[1] - info.address = apdu.pduSource - else: - # get the existing record by address (creates a new record) - info = self.get_device_info(apdu.pduSource) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # get the existing cache record if it exists + device_info = self.cache.get(device_instance, None) + + # maybe there is a record for this address + if not device_info: + device_info = self.cache.get(apdu.pduSource, None) - info.deviceIdentifier = apdu.iAmDeviceIdentifier[1] + # make a new one using the class provided + if not device_info: + device_info = self.device_info_class(device_instance, apdu.pduSource) - # update the rest of the values - info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted - info.segmentationSupported = apdu.segmentationSupported - info.vendorID = apdu.vendorID + # jam in the correct values + device_info.deviceIdentifier = device_instance + device_info.address = apdu.pduSource + device_info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted + device_info.segmentationSupported = apdu.segmentationSupported + device_info.vendorID = apdu.vendorID - # say this is an updated record - self.update_device_info(info) + # tell the cache this is an updated record + self.update_device_info(device_info) def get_device_info(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" if _debug: DeviceInfoCache._debug("get_device_info %r", key) - if isinstance(key, int): - current_info = self.cache.get(key, None) - - elif not isinstance(key, Address): - raise TypeError("key must be integer or an address") + # get the info if it's there + device_info = self.cache.get(key, None) + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): - raise TypeError("address must be a local or remote station") + return device_info - else: - current_info = self.cache.get(key, None) - if not current_info: - current_info = DeviceInfo() - current_info.address = key - current_info._cache_keys = (None, key) - current_info._ref_count = 1 - - self.cache[key] = current_info - else: - if _debug: DeviceInfoCache._debug(" - reference bump") - current_info._ref_count += 1 - - if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) - - return current_info - - def update_device_info(self, info): + def update_device_info(self, device_info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" - if _debug: DeviceInfoCache._debug("update_device_info %r", info) + if _debug: DeviceInfoCache._debug("update_device_info %r", device_info) - cache_id, cache_address = info._cache_keys + # give this a reference count if it doesn't have one + if not hasattr(device_info, '_ref_count'): + device_info._ref_count = 0 - if (cache_id is not None) and (info.deviceIdentifier != cache_id): + # get the current keys + cache_id, cache_address = getattr(device_info, '_cache_keys', (None, None)) + + if (cache_id is not None) and (device_info.deviceIdentifier != cache_id): if _debug: DeviceInfoCache._debug(" - device identifier updated") # remove the old reference, add the new one del self.cache[cache_id] - self.cache[info.deviceIdentifier] = info - - cache_id = info.deviceIdentifier + self.cache[device_info.deviceIdentifier] = device_info - if (cache_address is not None) and (info.address != cache_address): + if (cache_address is not None) and (device_info.address != cache_address): if _debug: DeviceInfoCache._debug(" - device address updated") # remove the old reference, add the new one del self.cache[cache_address] - self.cache[info.address] = info - - cache_address = info.address + self.cache[device_info.address] = device_info # update the keys - info._cache_keys = (cache_id, cache_address) + device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) + + def acquire(self, key): + """Return the known information about the device. If the key is the + address of an unknown device, build a generic device information record + add put it in the cache.""" + if _debug: DeviceInfoCache._debug("acquire %r", key) - def release_device_info(self, info): + if isinstance(key, int): + device_info = self.cache.get(key, None) + + elif not isinstance(key, Address): + raise TypeError("key must be integer or an address") + + elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): + raise TypeError("address must be a local or remote station") + + else: + device_info = self.cache.get(key, None) + + if device_info: + if _debug: DeviceInfoCache._debug(" - reference bump") + device_info._ref_count += 1 + + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) + + return device_info + + def release(self, device_info): """This function is called by the segmentation state machine when it has finished with the device information.""" - if _debug: DeviceInfoCache._debug("release_device_info %r", info) + if _debug: DeviceInfoCache._debug("release %r", device_info) # this information record might be used by more than one SSM - if info._ref_count > 1: - if _debug: DeviceInfoCache._debug(" - multiple references") - info._ref_count -= 1 - return + if device_info._ref_count == 0: + raise RuntimeError("reference count") - cache_id, cache_address = info._cache_keys - if cache_id is not None: - del self.cache[cache_id] - if cache_address is not None: - del self.cache[cache_address] - if _debug: DeviceInfoCache._debug(" - released") + # decrement the reference count + device_info._ref_count -= 1 # # Application @@ -260,9 +269,9 @@ def add_object(self, obj): # make sure it hasn't already been defined if object_name in self.objectName: - raise RuntimeError("already an object with name {0!r}".format(object_name)) + raise RuntimeError("already an object with name %r" % (object_name,)) if object_identifier in self.objectIdentifier: - raise RuntimeError("already an object with identifier {0!r}".format(object_identifier)) + raise RuntimeError("already an object with identifier %r" % (object_identifier,)) # now put it in local dictionaries self.objectName[object_name] = obj diff --git a/py34/bacpypes/appservice.py b/py34/bacpypes/appservice.py index 1ebfc891..43316f13 100755 --- a/py34/bacpypes/appservice.py +++ b/py34/bacpypes/appservice.py @@ -12,7 +12,9 @@ from .task import OneShotTask from .pdu import Address -from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \ +from .apdu import encode_max_segments_accepted, decode_max_segments_accepted, \ + encode_max_apdu_length_accepted, decode_max_apdu_length_accepted, \ + AbortPDU, AbortReason, ComplexAckPDU, \ ConfirmedRequestPDU, Error, ErrorPDU, RejectPDU, SegmentAckPDU, \ SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \ unconfirmed_request_types, confirmed_request_types, complex_ack_types, \ @@ -45,19 +47,23 @@ class SSM(OneShotTask, DebugContents): , 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED' ] - _debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID' + _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' ) - def __init__(self, sap, remoteDevice): + def __init__(self, sap, pdu_address): """Common parts for client and server segmentation.""" - if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice) + if _debug: SSM._debug("__init__ %r %r", sap, pdu_address) OneShotTask.__init__(self) self.ssmSAP = sap # service access point - self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance + + # save the address and get the device information + self.pdu_address = pdu_address + self.device_info = sap.deviceInfoCache.get_device_info(pdu_address) + self.invokeID = None # invoke ID self.state = IDLE # initial state @@ -71,11 +77,17 @@ def __init__(self, sap, remoteDevice): self.lastSequenceNumber = None self.initialSequenceNumber = None self.actualWindowSize = None - self.proposedWindowSize = None - # the maximum number of segments starts out being what's in the SAP - # which is the defaults or values from the local device. - self.maxSegmentsAccepted = self.ssmSAP.maxSegmentsAccepted + # local device object provides these or SAP provides defaults, make + # copies here so they are consistent throughout the transaction but + # they could change from one transaction to the next + self.numberOfApduRetries = getattr(sap.localDevice, 'numberOfApduRetries', sap.numberOfApduRetries) + self.apduTimeout = getattr(sap.localDevice, 'apduTimeout', sap.apduTimeout) + + self.segmentationSupported = getattr(sap.localDevice, 'segmentationSupported', sap.segmentationSupported) + self.segmentTimeout = getattr(sap.localDevice, 'segmentTimeout', sap.segmentTimeout) + self.maxSegmentsAccepted = getattr(sap.localDevice, 'maxSegmentsAccepted', sap.maxSegmentsAccepted) + self.maxApduLengthAccepted = getattr(sap.localDevice, 'maxApduLengthAccepted', sap.maxApduLengthAccepted) def start_timer(self, msecs): if _debug: SSM._debug("start_timer %r", msecs) @@ -153,12 +165,12 @@ def get_segment(self, indx): segAPDU = ConfirmedRequestPDU(self.segmentAPDU.apduService) - segAPDU.apduMaxSegs = self.maxSegmentsAccepted - segAPDU.apduMaxResp = self.ssmSAP.maxApduLengthAccepted - segAPDU.apduInvokeID = self.invokeID; + segAPDU.apduMaxSegs = encode_max_segments_accepted(self.maxSegmentsAccepted) + segAPDU.apduMaxResp = encode_max_apdu_length_accepted(self.maxApduLengthAccepted) + segAPDU.apduInvokeID = self.invokeID # segmented response accepted? - segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth') + segAPDU.apduSA = self.segmentationSupported in ('segmentedReceive', 'segmentedBoth') if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA) elif self.segmentAPDU.apduType == ComplexAckPDU.pduType: @@ -172,14 +184,21 @@ def get_segment(self, indx): segAPDU.pduUserData = self.segmentAPDU.pduUserData # make sure the destination is set - segAPDU.pduDestination = self.remoteDevice.address + segAPDU.pduDestination = self.pdu_address # segmented message? if (self.segmentCount != 1): segAPDU.apduSeg = True segAPDU.apduMor = (indx < (self.segmentCount - 1)) # more follows segAPDU.apduSeq = indx % 256 # sequence number - segAPDU.apduWin = self.proposedWindowSize # window size + + # first segment sends proposed window size, rest get actual + if indx == 0: + if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) + segAPDU.apduWin = self.proposedWindowSize + else: + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) + segAPDU.apduWin = self.actualWindowSize else: segAPDU.apduSeg = False segAPDU.apduMor = False @@ -211,10 +230,11 @@ def in_window(self, seqA, seqB): return rslt - def FillWindow(self, seqNum): + def fill_window(self, seqNum): """This function sends all of the packets necessary to fill out the segmentation window.""" - if _debug: SSM._debug("FillWindow %r", seqNum) + if _debug: SSM._debug("fill_window %r", seqNum) + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) for ix in range(self.actualWindowSize): apdu = self.get_segment(seqNum + ix) @@ -234,13 +254,18 @@ def FillWindow(self, seqNum): @bacpypes_debugging class ClientSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ClientSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) # initialize the retry count self.retryCount = 0 + # acquire the device info + if self.device_info: + if _debug: ClientSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) + def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" if _debug: ClientSSM._debug("set_state %r (%s) timer=%r", newState, SSM.transactionLabels[newState], timer) @@ -253,8 +278,10 @@ def set_state(self, newState, timer=0): if _debug: ClientSSM._debug(" - remove from active transactions") self.ssmSAP.clientTransactions.remove(self) - if _debug: ClientSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by client transaction functions when it wants @@ -263,7 +290,7 @@ def request(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -280,26 +307,27 @@ def indication(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit, - # the maximum conveyable by the internetwork to the remote device, and - # the maximum APDU size accepted by the remote device. - self.segmentSize = min( - self.ssmSAP.maxApduLengthAccepted, - self.remoteDevice.maxNpduLength, - self.remoteDevice.maxApduLengthAccepted, - ) - if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) + # if the max apdu length of the server isn't known, assume that it + # is the same size as our own and will be the segment size + if (not self.device_info) or (self.device_info.maxApduLengthAccepted is None): + self.segmentSize = self.maxApduLengthAccepted - # the maximum number of segments acceptable in the reply - if apdu.apduMaxSegs is not None: - # this request overrides the default - self.maxSegmentsAccepted = apdu.apduMaxSegs + # if the max npdu length of the server isn't known, assume that it + # is the same as the max apdu length accepted + elif self.device_info.maxNpduLength is None: + self.segmentSize = self.device_info.maxApduLengthAccepted + + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the server and the largest it can accept + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.device_info.maxApduLengthAccepted) + if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) # save the invoke ID self.invokeID = apdu.apduInvokeID if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -312,34 +340,49 @@ def indication(self, apdu): # make sure we support segmented transmit if we need to if self.segmentCount > 1: - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't send segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): - if _debug: ClientSSM._debug(" - remote device can't receive segmented requests") + + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for segmentation support") + + elif self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if _debug: ClientSSM._debug(" - server can't receive segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our request + # that the server said it was willing to accept + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for maximum number of segments") + + elif not self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server doesn't say maximum number of segments") + + elif self.segmentCount > self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # send out the first segment (or the whole thing) if self.segmentCount == 1: - # SendConfirmedUnsegmented + # unsegmented self.sentAllSegments = True self.retryCount = 0 - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) else: - # SendConfirmedSegmented + # segmented self.sentAllSegments = False self.retryCount = 0 self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.actualWindowSize = None # segment ack will set value + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # deliver to the device self.request(self.get_segment(0)) @@ -350,7 +393,7 @@ def response(self, apdu): if _debug: ClientSSM._debug("response %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it to the application @@ -407,29 +450,31 @@ def segmented_request(self, apdu): and receives an apdu.""" if _debug: ClientSSM._debug("segmented_request %r", apdu) - # client is ready for the next segment + # server is ready for the next segment if apdu.apduType == SegmentAckPDU.pduType: if _debug: ClientSSM._debug(" - segment ack") + # actual window size is provided by server + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ClientSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: if _debug: ClientSSM._debug(" - all done sending request") - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) # more segments to send else: if _debug: ClientSSM._debug(" - more segments to send") self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 - self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # simple ack elif (apdu.apduType == SimpleAckPDU.pduType): @@ -452,6 +497,7 @@ def segmented_request(self, apdu): self.response(abort) # send it to the application elif not apdu.apduSeg: + # ack is not segmented self.set_state(COMPLETED) self.response(apdu) @@ -459,10 +505,11 @@ def segmented_request(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # minimum of what the server is proposing and this client proposes + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # some kind of problem elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType): @@ -479,12 +526,12 @@ def segmented_request_timeout(self): if _debug: ClientSSM._debug("segmented_request_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: if _debug: ClientSSM._debug(" - retry segmented request") self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: if _debug: ClientSSM._debug(" - abort, no response from the device") @@ -516,7 +563,7 @@ def await_confirmation(self, apdu): self.set_state(COMPLETED) self.response(apdu) - elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + elif self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't receive segmented messages") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) @@ -527,10 +574,10 @@ def await_confirmation(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + self.actualWindowSize = apdu.apduWin self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # send back a segment ack segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) @@ -546,7 +593,7 @@ def await_confirmation(self, apdu): elif (apdu.apduType == SegmentAckPDU.pduType): if _debug: ClientSSM._debug(" - segment ack(!?)") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) else: raise RuntimeError("invalid APDU (3)") @@ -554,9 +601,9 @@ def await_confirmation(self, apdu): def await_confirmation_timeout(self): if _debug: ClientSSM._debug("await_confirmation_timeout") - self.retryCount += 1 - if self.retryCount < self.ssmSAP.retryCount: - if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.ssmSAP.retryCount) + if self.retryCount < self.numberOfApduRetries: + if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.numberOfApduRetries) + self.retryCount += 1 # save the retry count, indication acts like the request is coming # from the application so the retryCount gets re-initialized. @@ -594,8 +641,8 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - segment %s received out of order, should be %s", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) return @@ -610,7 +657,7 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - no more follows") # send a final ack - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) self.set_state(COMPLETED) @@ -620,15 +667,15 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) else: # wait for more segments if _debug: ClientSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_confirmation_timeout(self): if _debug: ClientSSM._debug("segmented_confirmation_timeout") @@ -643,9 +690,14 @@ def segmented_confirmation_timeout(self): @bacpypes_debugging class ServerSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ServerSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) + + # acquire the device info + if self.device_info: + if _debug: ServerSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" @@ -659,8 +711,10 @@ def set_state(self, newState, timer=0): if _debug: ServerSSM._debug(" - remove from active transactions") self.ssmSAP.serverTransactions.remove(self) - if _debug: ServerSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by transaction functions to send @@ -668,7 +722,7 @@ def request(self, apdu): if _debug: ServerSSM._debug("request %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it via the device @@ -697,7 +751,7 @@ def response(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -739,14 +793,15 @@ def confirmation(self, apdu): # save the response and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit - # (assumed to have no local buffer limitations), the maximum conveyable - # by the internetwork to the remote device, and the maximum APDU size - # accepted by the remote device. - self.segmentSize = min(self.remoteDevice.maxNpduLength, self.remoteDevice.maxApduLengthAccepted) + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the client and the largest it can accept + if (not self.device_info) or (self.device_info.maxNpduLength is None): + self.segmentSize = self.maxApduLengthAccepted + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.maxApduLengthAccepted) if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -762,26 +817,31 @@ def confirmation(self, apdu): if _debug: ServerSSM._debug(" - segmentation required, %d segments", self.segmentCount) # make sure we support segmented transmit - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ServerSSM._debug(" - server can't send segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # make sure client supports segmented receive - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ServerSSM._debug(" - client can't receive segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our response + # that the device said it was willing to accept in the request + if self.segmentCount > self.maxSegmentsAccepted: + if _debug: ServerSSM._debug(" - client can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # initialize the state self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 + self.actualWindowSize = None # send out the first segment (or the whole thing) if self.segmentCount == 1: @@ -789,7 +849,7 @@ def confirmation(self, apdu): self.set_state(COMPLETED) else: self.response(self.get_segment(0)) - self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_RESPONSE, self.segmentTimeout) else: raise RuntimeError("invalid APDU (4)") @@ -837,38 +897,48 @@ def idle(self, apdu): self.invokeID = apdu.apduInvokeID if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID) - # make sure the device information is synced with the request if apdu.apduSA: - if self.remoteDevice.segmentationSupported == 'noSegmentation': + if not self.device_info: + if _debug: ServerSSM._debug(" - no client device info") + + elif self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") - self.remoteDevice.segmentationSupported = 'segmentedReceive' + self.device_info.segmentationSupported = 'segmentedReceive' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedTransmit': + elif self.device_info.segmentationSupported == 'segmentedTransmit': if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive") - self.remoteDevice.segmentationSupported = 'segmentedBoth' + self.device_info.segmentationSupported = 'segmentedBoth' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedReceive': + elif self.device_info.segmentationSupported == 'segmentedReceive': pass - elif self.remoteDevice.segmentationSupported == 'segmentedBoth': + elif self.device_info.segmentationSupported == 'segmentedBoth': pass else: raise RuntimeError("invalid segmentation supported in device info") - if apdu.apduMaxSegs != self.remoteDevice.maxSegmentsAccepted: - if _debug: ServerSSM._debug(" - update maximum segments accepted?") - if apdu.apduMaxResp != self.remoteDevice.maxApduLengthAccepted: - if _debug: ServerSSM._debug(" - update maximum max APDU length accepted?") + # decode the maximum that the client can receive in one APDU, and if + # there is a value in the device information then use that one because + # it came from reading device object property value or from an I-Am + # message that was received + self.maxApduLengthAccepted = decode_max_apdu_length_accepted(apdu.apduMaxResp) + if self.device_info and self.device_info.maxApduLengthAccepted is not None: + if self.device_info.maxApduLengthAccepted < self.maxApduLengthAccepted: + if _debug: ServerSSM._debug(" - apduMaxResp encoding error") + else: + self.maxApduLengthAccepted = self.device_info.maxApduLengthAccepted + if _debug: ServerSSM._debug(" - maxApduLengthAccepted: %r", self.maxApduLengthAccepted) - # save the number of segments the client is willing to accept in the ack - self.maxSegmentsAccepted = apdu.apduMaxSegs + # save the number of segments the client is willing to accept in the ack, + # if this is None then the value is unknown or more than 64 + self.maxSegmentsAccepted = decode_max_segments_accepted(apdu.apduMaxSegs) # unsegmented request if not apdu.apduSeg: @@ -877,7 +947,7 @@ def idle(self, apdu): return # make sure we support segmented requests - if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return @@ -885,17 +955,18 @@ def idle(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the window size is the minimum of what I'm willing to receive and - # what the device has said it would like to send - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # the window size is the minimum of what I would propose and what the + # device has proposed + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + if _debug: ServerSSM._debug(" - actualWindowSize? min(%r, %r) -> %r", apdu.apduWin, self.proposedWindowSize, self.actualWindowSize) # initialize the state self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) if _debug: ServerSSM._debug(" - segAck: %r", segack) self.response(segack) @@ -928,10 +999,10 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - segment %d received out of order, should be %d", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) return @@ -947,7 +1018,7 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - no more follows") # send back a final segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.response(segack) # forward the whole thing to the application @@ -958,17 +1029,17 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) else: # wait for more segments if _debug: ServerSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_request_timeout(self): if _debug: ServerSSM._debug("segmented_request_timeout") @@ -1008,10 +1079,13 @@ def segmented_response(self, apdu): if (apdu.apduType == SegmentAckPDU.pduType): if _debug: ServerSSM._debug(" - segment ack") + # actual window size is provided by client + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ServerSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: @@ -1024,8 +1098,8 @@ def segmented_response(self, apdu): self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # some kind of problem elif (apdu.apduType == AbortPDU.pduType): @@ -1039,10 +1113,10 @@ def segmented_response_timeout(self): if _debug: ServerSSM._debug("segmented_response_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: # give up self.set_state(ABORTED) @@ -1062,6 +1136,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): ServiceAccessPoint.__init__(self, sap) # save a reference to the device information cache + self.localDevice = localDevice self.deviceInfoCache = deviceInfoCache # client settings @@ -1072,27 +1147,19 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): self.serverTransactions = [] # confirmed request defaults - self.retryCount = 3 - self.retryTimeout = 3000 + self.numberOfApduRetries = 3 + self.apduTimeout = 3000 self.maxApduLengthAccepted = 1024 # segmentation defaults self.segmentationSupported = 'noSegmentation' self.segmentTimeout = 1500 - self.maxSegmentsAccepted = 8 + self.maxSegmentsAccepted = 2 + self.proposedWindowSize = 2 # device communication control self.dccEnableDisable = 'enable' - # local device object provides these - if localDevice: - self.retryCount = localDevice.numberOfApduRetries - self.retryTimeout = localDevice.apduTimeout - self.segmentationSupported = localDevice.segmentationSupported - self.segmentTimeout = localDevice.apduSegmentTimeout - self.maxSegmentsAccepted = localDevice.maxSegmentsAccepted - self.maxApduLengthAccepted = localDevice.maxApduLengthAccepted - # how long the state machine is willing to wait for the application # layer to form a response and send it self.applicationTimeout = 3000 @@ -1111,7 +1178,7 @@ def get_next_invoke_id(self, addr): raise RuntimeError("no available invoke ID") for tr in self.clientTransactions: - if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address): + if (invokeID == tr.invokeID) and (addr == tr.pdu_address): break else: break @@ -1152,14 +1219,11 @@ def confirmation(self, pdu): if isinstance(apdu, ConfirmedRequestPDU): # find duplicates of this request for tr in self.serverTransactions: - if (apdu.pduSource == tr.remoteDevice.address) and (apdu.apduInvokeID == tr.invokeID): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource) - # build a server transaction - tr = ServerSSM(self, remoteDevice) + tr = ServerSSM(self, apdu.pduSource) # add it to our transactions to track it self.serverTransactions.append(tr) @@ -1178,7 +1242,7 @@ def confirmation(self, pdu): # find the client transaction this is acking for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1190,7 +1254,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1199,7 +1263,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1211,7 +1275,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1220,7 +1284,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1264,19 +1328,15 @@ def sap_indication(self, apdu): else: # verify the invoke ID isn't already being used for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): raise RuntimeError("invoke ID in use") # warning for bogus requests if (apdu.pduDestination.addrType != Address.localStationAddr) and (apdu.pduDestination.addrType != Address.remoteStationAddr): StateMachineAccessPoint._warning("%s is not a local or remote station", apdu.pduDestination) - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduDestination) - if _debug: StateMachineAccessPoint._debug(" - remoteDevice: %r", remoteDevice) - # create a client transaction state machine - tr = ClientSSM(self, remoteDevice) + tr = ClientSSM(self, apdu.pduDestination) if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr) # add it to our transactions to track it @@ -1300,7 +1360,7 @@ def sap_confirmation(self, apdu): or isinstance(apdu, AbortPDU): # find the appropriate server transaction for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): break else: return diff --git a/py34/bacpypes/task.py b/py34/bacpypes/task.py index 1c89a4f7..0c25c33a 100755 --- a/py34/bacpypes/task.py +++ b/py34/bacpypes/task.py @@ -8,6 +8,7 @@ from time import time as _time from heapq import heapify, heappush, heappop +import itertools from .singleton import SingletonLogging from .debugging import DebugContents, Logging, ModuleLogger, bacpypes_debugging @@ -276,6 +277,9 @@ def __init__(self): # task manager is this instance _task_manager = self + # unique sequence counter for tasks scheduled at the same time + self.counter = itertools.count() + # there may be tasks created that couldn't be scheduled # because a task manager wasn't created yet. if _unscheduled_tasks: @@ -300,7 +304,7 @@ def install_task(self, task): self.suspend_task(task) # save this in the task list - heappush( self.tasks, (task.taskTime, task) ) + heappush( self.tasks, (task.taskTime, next(self.counter), task) ) if _debug: TaskManager._debug(" - tasks: %r", self.tasks) task.isScheduled = True @@ -313,7 +317,7 @@ def suspend_task(self, task): if _debug: TaskManager._debug("suspend_task %r", task) # remove this guy - for i, (when, curtask) in enumerate(self.tasks): + for i, (when, n, curtask) in enumerate(self.tasks): if task is curtask: if _debug: TaskManager._debug(" - task found") del self.tasks[i] @@ -348,7 +352,7 @@ def get_next_task(self): if self.tasks: # look at the first task - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] if when <= now: # pull it off the list and mark that it's no longer scheduled heappop(self.tasks) @@ -356,7 +360,7 @@ def get_next_task(self): task.isScheduled = False if self.tasks: - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] # peek at the next task, return how long to wait delta = max(when - now, 0.0) else: diff --git a/py34/bacpypes/vlan.py b/py34/bacpypes/vlan.py index ea839239..5908e898 100755 --- a/py34/bacpypes/vlan.py +++ b/py34/bacpypes/vlan.py @@ -32,6 +32,7 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.name = name self.nodes = [] + self.broadcast_address = broadcast_address self.drop_percent = drop_percent @@ -216,8 +217,9 @@ class IPRouterNode(Client): def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) - # save the reference to the router + # save the references to the router for packets and the lan for debugging self.router = router + self.lan = lan # make ourselves an IPNode and bind to it self.node = IPNode(addr, lan=lan, promiscuous=True, spoofing=True) @@ -241,6 +243,7 @@ def process_pdu(self, pdu): def __repr__(self): return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + # # IPRouter # diff --git a/samples/AccumulatorObject.py b/samples/AccumulatorObject.py index 01130394..a0b8a0c9 100755 --- a/samples/AccumulatorObject.py +++ b/samples/AccumulatorObject.py @@ -78,6 +78,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/COVClient.py b/samples/COVClient.py index e79ba964..a904e027 100755 --- a/samples/COVClient.py +++ b/samples/COVClient.py @@ -225,6 +225,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = SubscribeCOVApplication(this_device, args.ini.address) diff --git a/samples/COVClientApp.py b/samples/COVClientApp.py index 3709b80f..b28e760c 100755 --- a/samples/COVClientApp.py +++ b/samples/COVClientApp.py @@ -173,6 +173,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = SubscribeCOVApplication(this_device, args.ini.address) diff --git a/samples/COVServer.py b/samples/COVServer.py index 99782969..18c53d9c 100755 --- a/samples/COVServer.py +++ b/samples/COVServer.py @@ -373,6 +373,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application test_application = SubscribeCOVApplication(test_device, args.ini.address) diff --git a/samples/CommandableMixin.py b/samples/CommandableMixin.py index 41b8998c..11242d27 100644 --- a/samples/CommandableMixin.py +++ b/samples/CommandableMixin.py @@ -387,6 +387,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/DeviceCommunicationControl.py b/samples/DeviceCommunicationControl.py index e4ce6d63..58b8c925 100755 --- a/samples/DeviceCommunicationControl.py +++ b/samples/DeviceCommunicationControl.py @@ -131,6 +131,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/DeviceDiscovery.py b/samples/DeviceDiscovery.py index e0f7276b..d68fd63c 100755 --- a/samples/DeviceDiscovery.py +++ b/samples/DeviceDiscovery.py @@ -208,6 +208,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = DiscoveryApplication(this_device, args.ini.address) diff --git a/samples/DeviceDiscoveryForeign.py b/samples/DeviceDiscoveryForeign.py index 1e7b977e..b75011a9 100755 --- a/samples/DeviceDiscoveryForeign.py +++ b/samples/DeviceDiscoveryForeign.py @@ -208,6 +208,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = DiscoveryApplication( this_device, args.ini.address, diff --git a/samples/HTTPServer.py b/samples/HTTPServer.py index 61d1c21e..a7ceca27 100755 --- a/samples/HTTPServer.py +++ b/samples/HTTPServer.py @@ -187,6 +187,10 @@ class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/LocalScheduleObject.py b/samples/LocalScheduleObject.py index 20e6c48b..2a4c1d9c 100644 --- a/samples/LocalScheduleObject.py +++ b/samples/LocalScheduleObject.py @@ -82,6 +82,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/MultiStateValueObject.py b/samples/MultiStateValueObject.py index b3c9b903..7abf2309 100755 --- a/samples/MultiStateValueObject.py +++ b/samples/MultiStateValueObject.py @@ -41,6 +41,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/MultipleReadProperty.py b/samples/MultipleReadProperty.py index 4bf2dde5..45d2e9b4 100755 --- a/samples/MultipleReadProperty.py +++ b/samples/MultipleReadProperty.py @@ -139,6 +139,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = ReadPointListApplication(point_list, this_device, args.ini.address) diff --git a/samples/MultipleReadPropertyHammer.py b/samples/MultipleReadPropertyHammer.py index fbac9694..e7812bc4 100755 --- a/samples/MultipleReadPropertyHammer.py +++ b/samples/MultipleReadPropertyHammer.py @@ -209,6 +209,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = ReadPointListApplication(this_device, args.ini.address) diff --git a/samples/MultipleReadPropertyThreaded.py b/samples/MultipleReadPropertyThreaded.py index 652057ae..dfe85898 100755 --- a/samples/MultipleReadPropertyThreaded.py +++ b/samples/MultipleReadPropertyThreaded.py @@ -131,6 +131,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/RandomAnalogValueSleep.py b/samples/RandomAnalogValueSleep.py index ef944b25..0022d6b5 100644 --- a/samples/RandomAnalogValueSleep.py +++ b/samples/RandomAnalogValueSleep.py @@ -116,6 +116,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadAllProperties.py b/samples/ReadAllProperties.py index 8cd81627..448da749 100755 --- a/samples/ReadAllProperties.py +++ b/samples/ReadAllProperties.py @@ -165,6 +165,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = ReadPropertyApplication(this_device, args.ini.address) diff --git a/samples/ReadProperty.py b/samples/ReadProperty.py index f0f7dfe2..19ac78dd 100755 --- a/samples/ReadProperty.py +++ b/samples/ReadProperty.py @@ -159,6 +159,14 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadProperty25.py b/samples/ReadProperty25.py index abfa136c..76e05276 100755 --- a/samples/ReadProperty25.py +++ b/samples/ReadProperty25.py @@ -161,6 +161,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyAny.py b/samples/ReadPropertyAny.py index 6b9be041..ac082b7c 100755 --- a/samples/ReadPropertyAny.py +++ b/samples/ReadPropertyAny.py @@ -132,6 +132,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) if _debug: _log.debug(" - this_application: %r", this_application) diff --git a/samples/ReadPropertyMultiple.py b/samples/ReadPropertyMultiple.py index ca1df025..dc814a38 100755 --- a/samples/ReadPropertyMultiple.py +++ b/samples/ReadPropertyMultiple.py @@ -2,8 +2,8 @@ """ This application presents a 'console' prompt to the user asking for read commands -which create ReadPropertyRequest PDUs, then lines up the coorresponding ReadPropertyACK -and prints the value. +which create ReadPropertyMultipleRequest PDUs, then lines up the coorresponding +ReadPropertyMultipleACK and prints the value. """ import sys @@ -151,7 +151,7 @@ def do_read(self, args): # here is the read result readResult = element.readResult - sys.stdout.write(propertyIdentifier) + sys.stdout.write(str(propertyIdentifier)) if propertyArrayIndex is not None: sys.stdout.write("[" + str(propertyArrayIndex) + "]") @@ -167,17 +167,17 @@ def do_read(self, args): datatype = get_datatype(objectIdentifier[0], propertyIdentifier) if _debug: ReadPropertyMultipleConsoleCmd._debug(" - datatype: %r", datatype) if not datatype: - raise TypeError("unknown datatype") - - # special case for array parts, others are managed by cast_out - if issubclass(datatype, Array) and (propertyArrayIndex is not None): - if propertyArrayIndex == 0: - value = propertyValue.cast_out(Unsigned) - else: - value = propertyValue.cast_out(datatype.subtype) + value = '?' else: - value = propertyValue.cast_out(datatype) - if _debug: ReadPropertyMultipleConsoleCmd._debug(" - value: %r", value) + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (propertyArrayIndex is not None): + if propertyArrayIndex == 0: + value = propertyValue.cast_out(Unsigned) + else: + value = propertyValue.cast_out(datatype.subtype) + else: + value = propertyValue.cast_out(datatype) + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - value: %r", value) sys.stdout.write(" = " + str(value) + '\n') sys.stdout.flush() @@ -211,6 +211,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultiple25.py b/samples/ReadPropertyMultiple25.py index 41a857b7..d0cb8139 100755 --- a/samples/ReadPropertyMultiple25.py +++ b/samples/ReadPropertyMultiple25.py @@ -217,6 +217,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultipleServer.py b/samples/ReadPropertyMultipleServer.py index c186599b..2f2a1058 100755 --- a/samples/ReadPropertyMultipleServer.py +++ b/samples/ReadPropertyMultipleServer.py @@ -104,6 +104,10 @@ def main(): _dcc_password="xyzzy", ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application this_application = ReadPropertyMultipleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultipleServer25.py b/samples/ReadPropertyMultipleServer25.py index b483e2fc..05d60455 100755 --- a/samples/ReadPropertyMultipleServer25.py +++ b/samples/ReadPropertyMultipleServer25.py @@ -103,6 +103,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application this_application = ReadPropertyMultipleApplication(this_device, args.ini.address) diff --git a/samples/ReadRange.py b/samples/ReadRange.py index a328dd10..cca08201 100755 --- a/samples/ReadRange.py +++ b/samples/ReadRange.py @@ -129,6 +129,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteEventMessageTexts.py b/samples/ReadWriteEventMessageTexts.py index 67b24e5e..5b47a220 100644 --- a/samples/ReadWriteEventMessageTexts.py +++ b/samples/ReadWriteEventMessageTexts.py @@ -238,6 +238,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteFile.py b/samples/ReadWriteFile.py index 16fd36fa..f3eae017 100755 --- a/samples/ReadWriteFile.py +++ b/samples/ReadWriteFile.py @@ -319,6 +319,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteFileServer.py b/samples/ReadWriteFileServer.py index 407aa093..6ddeddd3 100755 --- a/samples/ReadWriteFileServer.py +++ b/samples/ReadWriteFileServer.py @@ -182,6 +182,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteProperty.py b/samples/ReadWriteProperty.py index 70e85dea..18dfe055 100755 --- a/samples/ReadWriteProperty.py +++ b/samples/ReadWriteProperty.py @@ -279,6 +279,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/RecurringMultipleReadProperty.py b/samples/RecurringMultipleReadProperty.py index bb1826ce..298521f6 100755 --- a/samples/RecurringMultipleReadProperty.py +++ b/samples/RecurringMultipleReadProperty.py @@ -172,6 +172,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a dog this_application = PrairieDog(args.interval, this_device, args.ini.address) if _debug: _log.debug(" - this_application: %r", this_application) diff --git a/samples/ThreadedReadProperty.py b/samples/ThreadedReadProperty.py index 34f34607..2fcdcc66 100755 --- a/samples/ThreadedReadProperty.py +++ b/samples/ThreadedReadProperty.py @@ -169,6 +169,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/VendorAVObject.py b/samples/VendorAVObject.py index b2fc5ae6..85ab1253 100755 --- a/samples/VendorAVObject.py +++ b/samples/VendorAVObject.py @@ -103,6 +103,10 @@ def main(): vendorIdentifier=vendor_id, ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/VendorReadWriteProperty.py b/samples/VendorReadWriteProperty.py index 9687f248..fd1f5e3a 100755 --- a/samples/VendorReadWriteProperty.py +++ b/samples/VendorReadWriteProperty.py @@ -251,6 +251,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/WhoIsIAm.py b/samples/WhoIsIAm.py index 1583446e..7ed9b65d 100755 --- a/samples/WhoIsIAm.py +++ b/samples/WhoIsIAm.py @@ -187,7 +187,10 @@ def main(): segmentationSupported=args.ini.segmentationsupported, vendorIdentifier=int(args.ini.vendoridentifier), ) - if _debug: _log.debug(" - this_device: %r", this_device) + + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) # make a simple application this_application = WhoIsIAmApplication( diff --git a/samples/WhoIsIAmForeign.py b/samples/WhoIsIAmForeign.py index 2371341c..59956f8e 100755 --- a/samples/WhoIsIAmForeign.py +++ b/samples/WhoIsIAmForeign.py @@ -197,7 +197,10 @@ def main(): segmentationSupported=args.ini.segmentationsupported, vendorIdentifier=int(args.ini.vendoridentifier), ) - if _debug: _log.debug(" - this_device: %r", this_device) + + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) # make a simple application this_application = WhoIsIAmApplication( diff --git a/samples/WritePropertyTCPServer.py b/samples/WritePropertyTCPServer.py index 9b9a005d..1a52623d 100755 --- a/samples/WritePropertyTCPServer.py +++ b/samples/WritePropertyTCPServer.py @@ -234,6 +234,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/WriteSomething.py b/samples/WriteSomething.py index 5aaa57bb..003001d7 100755 --- a/samples/WriteSomething.py +++ b/samples/WriteSomething.py @@ -126,6 +126,10 @@ def main(): vendorIdentifier=int(args.ini.vendoridentifier), ) + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) if _debug: _log.debug(" - this_application: %r", this_application) diff --git a/tests/__init__.py b/tests/__init__.py index 2935d939..d06f887e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -27,4 +27,5 @@ from . import test_network from . import test_service from . import test_local +from . import test_segmentation diff --git a/tests/state_machine.py b/tests/state_machine.py index b7fa564d..9e4e2ddc 100755 --- a/tests/state_machine.py +++ b/tests/state_machine.py @@ -88,18 +88,18 @@ def match_pdu(pdu, pdu_type=None, **pdu_attrs): # check the type if pdu_type and not isinstance(pdu, pdu_type): - if _debug: match_pdu._debug(" - wrong type") + if _debug: match_pdu._debug(" - failed match, wrong type") return False # check for matching attribute values for attr_name, attr_value in pdu_attrs.items(): if not hasattr(pdu, attr_name): - if _debug: match_pdu._debug(" - missing attr: %r", attr_name) + if _debug: match_pdu._debug(" - failed match, missing attr: %r", attr_name) return False if getattr(pdu, attr_name) != attr_value: - if _debug: StateMachine._debug(" - attr value: %r, %r", attr_name, attr_value) + if _debug: StateMachine._debug(" - failed match, attr value: %r, %r", attr_name, attr_value) return False - if _debug: match_pdu._debug(" - successful_match") + if _debug: match_pdu._debug(" - successful match") return True diff --git a/tests/test_bvll/test_bbmd.py b/tests/test_bvll/test_bbmd.py index d78798af..602df2a7 100644 --- a/tests/test_bvll/test_bbmd.py +++ b/tests/test_bvll/test_bbmd.py @@ -288,15 +288,26 @@ def test_14_2_1_2(self): .receive(IAmRequest).doc("2-3-2") \ .success() - # listen for the forwarded NPDU, then the re-braodcast on the local LAN - # then the original unicast going back, then fail if there's anything else - listener.start_state.doc("2-4-0") \ - .receive(ForwardedNPDU).doc("2-4-1") \ - .receive(ForwardedNPDU).doc("2-4-2") \ - .receive(OriginalUnicastNPDU).doc("2-4-3") \ - .timeout(3).doc("2-4-4") \ + # listen for the forwarded NPDU. The packet will be sent upstream which + # will generate the original unicast going back, then it will be + # re-broadcast on the local LAN. Fail if there's anything after that. + s241 = listener.start_state.doc("2-4-0") \ + .receive(ForwardedNPDU).doc("2-4-1") + + # look for the original unicast going back, followed by the rebroadcast + # of the forwarded NPDU on the local LAN + both = s241 \ + .receive(OriginalUnicastNPDU).doc("2-4-1-a") \ + .receive(ForwardedNPDU).doc("2-4-1-b") + + # fail if anything is received after both packets + both.timeout(3).doc("2-4-4") \ .success() + # allow the two packets in either order + s241.receive(ForwardedNPDU).doc("2-4-2-a") \ + .receive(OriginalUnicastNPDU, next_state=both).doc("2-4-2-b") + # run the group tnet.run() diff --git a/tests/test_network/test_net_3.py b/tests/test_network/test_net_3.py index 0bf434c1..21b4e715 100644 --- a/tests/test_network/test_net_3.py +++ b/tests/test_network/test_net_3.py @@ -35,7 +35,7 @@ ) from ..state_machine import match_pdu, StateMachineGroup -from ..time_machine import reset_time_machine, run_time_machine +from ..time_machine import reset_time_machine, run_time_machine, current_time from .helpers import ( SnifferStateMachine, NetworkLayerStateMachine, RouterNode, ApplicationLayerStateMachine, @@ -94,12 +94,16 @@ def __init__(self): def run(self, time_limit=60.0): if _debug: TNetwork._debug("run %r", time_limit) + if _debug: TNetwork._debug(" - current_time: %r", current_time()) # run the group super(TNetwork, self).run() + if _debug: TNetwork._debug(" - current_time: %r", current_time()) # run it for some time run_time_machine(time_limit) + if _debug: TNetwork._debug(" - current_time: %r", current_time()) + if _debug: TNetwork._debug(" - time machine finished") for state_machine in self.state_machines: @@ -335,27 +339,27 @@ def test_remote_read_2(self): ) ).doc("5-2-2") \ .receive(PDU, - pduData=xtob('01.24.00.02.01.04.ff' + pduData=xtob('01.24.00.02.01.04.ff' # request '02.44.01.0c.0c.02.00.00.04.19.78' ) ).doc("5-2-3") \ .receive(PDU, - pduData=xtob('01.08.00.02.01.04' + pduData=xtob('01.08.00.02.01.04' # ack '30.01.0c.0c.02.00.00.04.19.78.3e.22.03.e7.3f' ) ).doc("5-2-4") \ .timeout(3).doc("5-2-5") \ .success() - # network 2 sees local broadcast request and unicast response + # network 2 sees routed request and unicast response tnet.sniffer2.start_state.doc('5-3-0') \ .receive(PDU, - pduData=xtob('01.0c.00.01.01.01' + pduData=xtob('01.0c.00.01.01.01' # request '02.44.01.0c.0c.02.00.00.04.19.78' ) ).doc("5-3-1") \ .receive(PDU, - pduData=xtob('01.20.00.01.01.01.ff' + pduData=xtob('01.20.00.01.01.01.ff' # ack '30.01.0c.0c.02.00.00.04.19.78.3e.22.03.e7.3f' ) ).doc("5-3-2") \ diff --git a/tests/test_segmentation/__init__.py b/tests/test_segmentation/__init__.py new file mode 100644 index 00000000..c3e903a9 --- /dev/null +++ b/tests/test_segmentation/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/python + +""" +Test Segmentation +""" + +from . import test_1 diff --git a/tests/test_segmentation/test_1.py b/tests/test_segmentation/test_1.py new file mode 100644 index 00000000..ed42de67 --- /dev/null +++ b/tests/test_segmentation/test_1.py @@ -0,0 +1,345 @@ +#!/urs/bin/python3 + +""" +Test Segmentation +""" + +import random +import string +import unittest + +from bacpypes.debugging import ModuleLogger, bacpypes_debugging, btox, xtob +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.primitivedata import CharacterString +from bacpypes.constructeddata import Any + +from bacpypes.comm import Client, bind +from bacpypes.pdu import Address, LocalBroadcast +from bacpypes.vlan import Network, Node + +from bacpypes.npdu import NPDU, npdu_types +from bacpypes.apdu import APDU, apdu_types, \ + confirmed_request_types, unconfirmed_request_types, complex_ack_types, error_types, \ + ConfirmedRequestPDU, UnconfirmedRequestPDU, \ + SimpleAckPDU, ComplexAckPDU, SegmentAckPDU, ErrorPDU, RejectPDU, AbortPDU + +from bacpypes.apdu import APDU, ErrorPDU, RejectPDU, AbortPDU, \ + ConfirmedPrivateTransferRequest, ConfirmedPrivateTransferACK + +from bacpypes.app import Application +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.local.device import LocalDeviceObject + +from ..state_machine import StateMachine, StateMachineGroup, TrafficLog +from ..time_machine import reset_time_machine, run_time_machine + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# ApplicationNetwork +# + +@bacpypes_debugging +class ApplicationNetwork(StateMachineGroup): + + def __init__(self, td_device_object, iut_device_object): + if _debug: ApplicationNetwork._debug("__init__ %r %r", td_device_object, iut_device_object) + StateMachineGroup.__init__(self) + + # reset the time machine + reset_time_machine() + if _debug: ApplicationNetwork._debug(" - time machine reset") + + # create a traffic log + self.traffic_log = TrafficLog() + + # make a little LAN + self.vlan = Network(broadcast_address=LocalBroadcast()) + self.vlan.traffic_log = self.traffic_log + + # sniffer + self.sniffer = SnifferNode(self.vlan) + + # test device + self.td = ApplicationStateMachine(td_device_object, self.vlan) + self.append(self.td) + + # implementation under test + self.iut = ApplicationStateMachine(iut_device_object, self.vlan) + self.append(self.iut) + + def run(self, time_limit=60.0): + if _debug: ApplicationNetwork._debug("run %r", time_limit) + + # run the group + super(ApplicationNetwork, self).run() + if _debug: ApplicationNetwork._debug(" - group running") + + # run it for some time + run_time_machine(time_limit) + if _debug: + ApplicationNetwork._debug(" - time machine finished") + for state_machine in self.state_machines: + ApplicationNetwork._debug(" - machine: %r", state_machine) + for direction, pdu in state_machine.transaction_log: + ApplicationNetwork._debug(" %s %s", direction, str(pdu)) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(ApplicationNetwork._debug) + + # check for success + all_success, some_failed = super(ApplicationNetwork, self).check_for_success() + ApplicationNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) + assert all_success + + +# +# SnifferNode +# + +@bacpypes_debugging +class SnifferNode(Client): ### , StateMachine): + + def __init__(self, vlan): + if _debug: SnifferNode._debug("__init__ %r", vlan) + + # save the name and give it a blank address + self.name = "sniffer" + self.address = Address() + + # continue with initialization + Client.__init__(self) + ### StateMachine.__init__(self) + + # create a promiscuous node, added to the network + self.node = Node(self.address, vlan, promiscuous=True) + if _debug: SnifferNode._debug(" - node: %r", self.node) + + # bind this to the node + bind(self, self.node) + + def send(self, pdu): + if _debug: SnifferNode._debug("send(%s) %r", self.name, pdu) + raise RuntimeError("sniffers don't send") + + def confirmation(self, pdu): + if _debug: SnifferNode._debug("confirmation(%s) %r", self.name, pdu) + + # it's an NPDU + npdu = NPDU() + npdu.decode(pdu) + + # decode as a generic APDU + apdu = APDU() + apdu.decode(npdu) + + # "lift" the source and destination address + if npdu.npduSADR: + apdu.pduSource = npdu.npduSADR + else: + apdu.pduSource = npdu.pduSource + if npdu.npduDADR: + apdu.pduDestination = npdu.npduDADR + else: + apdu.pduDestination = npdu.pduDestination + + # make a more focused interpretation + atype = apdu_types.get(apdu.apduType) + if _debug: SnifferNode._debug(" - atype: %r", atype) + + xpdu = apdu + apdu = atype() + apdu.decode(xpdu) + + print(repr(apdu)) + apdu.debug_contents() + print("") + +# +# ApplicationStateMachine +# + +@bacpypes_debugging +class ApplicationStateMachine(Application, StateMachine): + + def __init__(self, localDevice, vlan): + if _debug: ApplicationStateMachine._debug("__init__ %r %r", localDevice, vlan) + + # build an address and save it + self.address = Address(localDevice.objectIdentifier[1]) + if _debug: ApplicationStateMachine._debug(" - address: %r", self.address) + + # continue with initialization + Application.__init__(self, localDevice, self.address) + StateMachine.__init__(self, name=localDevice.objectName) + + # include a application decoder + self.asap = ApplicationServiceAccessPoint() + + # pass the device object to the state machine access point so it + # can know if it should support segmentation + self.smap = StateMachineAccessPoint(localDevice) + + # the segmentation state machines need access to the same device + # information cache as the application + self.smap.deviceInfoCache = self.deviceInfoCache + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # create a node, added to the network + self.node = Node(self.address, vlan) + + # bind the network service to the node, no network number + self.nsap.bind(self.node) + + # placeholder for the result block + self.confirmed_private_result = None + + def send(self, apdu): + if _debug: ApplicationStateMachine._debug("send(%s) %r", self.name, apdu) + + # send the apdu down the stack + self.request(apdu) + + def indication(self, apdu): + if _debug: ApplicationStateMachine._debug("indication(%s) %r", self.name, apdu) + + # let the state machine know the request was received + self.receive(apdu) + + # allow the application to process it + super(ApplicationStateMachine, self).indication(apdu) + + def confirmation(self, apdu): + if _debug: ApplicationStateMachine._debug("confirmation(%s) %r", self.name, apdu) + + # forward the confirmation to the state machine + self.receive(apdu) + + def do_ConfirmedPrivateTransferRequest(self, apdu): + if _debug: ApplicationStateMachine._debug("do_ConfirmedPrivateTransferRequest(%s) %r", self.name, apdu) + + # simple ack + xapdu = ConfirmedPrivateTransferACK(context=apdu) + xapdu.vendorID = 999 + xapdu.serviceNumber = 1 + xapdu.resultBlock = self.confirmed_private_result + + if _debug: ApplicationStateMachine._debug(" - xapdu: %r", xapdu) + + # send the response back + self.response(xapdu) + + +@bacpypes_debugging +class TestSegmentation(unittest.TestCase): + + def test_1(self): + """9.39.1 Unsupported Confirmed Services Test""" + if _debug: TestSegmentation._debug("test_1") + + # client device object + td_device_object = LocalDeviceObject( + objectName="td", + objectIdentifier=("device", 10), + maxApduLengthAccepted=206, + segmentationSupported='segmentedBoth', + maxSegmentsAccepted=4, + vendorIdentifier=999, + ) + + # server device object + iut_device_object = LocalDeviceObject( + objectName="iut", + objectIdentifier=("device", 20), + maxApduLengthAccepted=206, + segmentationSupported='segmentedBoth', + maxSegmentsAccepted=99, + vendorIdentifier=999, + ) + + # create a network + anet = ApplicationNetwork(td_device_object, iut_device_object) + + # client settings + c_ndpu_len = 50 + c_len = 0 + + # server settings + s_ndpu_len = 50 + s_len = 0 + + # tell the device info cache of the client about the server + if 0: + iut_device_info = anet.td.deviceInfoCache.get_device_info(anet.iut.address) + + # update the rest of the values + iut_device_info.maxApduLengthAccepted = iut_device_object.maxApduLengthAccepted + iut_device_info.segmentationSupported = iut_device_object.segmentationSupported + iut_device_info.vendorID = iut_device_object.vendorIdentifier + iut_device_info.maxSegmentsAccepted = iut_device_object.maxSegmentsAccepted + iut_device_info.maxNpduLength = s_ndpu_len + + # tell the device info cache of the server device about the client + if 0: + td_device_info = anet.iut.deviceInfoCache.get_device_info(anet.td.address) + + # update the rest of the values + td_device_info.maxApduLengthAccepted = td_device_object.maxApduLengthAccepted + td_device_info.segmentationSupported = td_device_object.segmentationSupported + td_device_info.vendorID = td_device_object.vendorIdentifier + td_device_info.maxSegmentsAccepted = td_device_object.maxSegmentsAccepted + td_device_info.maxNpduLength = c_ndpu_len + + # build a request string + if c_len: + request_string = Any( + CharacterString( + ''.join(random.choice(string.lowercase) for _ in range(c_len)) + ) + ) + else: + request_string = None + + # response string is stuffed into the server + if s_len: + anet.iut.confirmed_private_result = Any( + CharacterString( + ''.join(random.choice(string.lowercase) for _ in range(s_len)) + ) + ) + else: + anet.iut.confirmed_private_result = None + + # send the request, get it rejected + s761 = anet.td.start_state.doc("7-6-0") \ + .send(ConfirmedPrivateTransferRequest( + vendorID=999, serviceNumber=1, + serviceParameters=request_string, + destination=anet.iut.address, + )).doc("7-6-1") + + s761.receive(ConfirmedPrivateTransferACK).doc("7-6-2") \ + .success() + s761.receive(AbortPDU, apduAbortRejectReason=11).doc("7-6-3") \ + .success() + + # no IUT application layer matching + anet.iut.start_state.success() + + # run the group + anet.run() + diff --git a/tests/test_service/test_device.py b/tests/test_service/test_device.py index 677aa503..995b9e8d 100644 --- a/tests/test_service/test_device.py +++ b/tests/test_service/test_device.py @@ -8,16 +8,16 @@ import unittest -from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob +from bacpypes.debugging import bacpypes_debugging, ModuleLogger -from bacpypes.pdu import Address, LocalBroadcast, PDU +from bacpypes.pdu import Address, PDU from bacpypes.basetypes import PropertyReference from bacpypes.apdu import ( ConfirmedRequestSequence, SequenceOf, Element, WhoIsRequest, IAmRequest, - WhoHasRequest, WhoHasLimits, WhoHasObject, IHaveRequest, - DeviceCommunicationControlRequest, - SimpleAckPDU, Error, RejectPDU, + WhoHasRequest, WhoHasObject, IHaveRequest, + DeviceCommunicationControlRequest, ReadPropertyRequest, + SimpleAckPDU, Error, RejectPDU, AbortPDU, ) from bacpypes.service.device import ( @@ -25,7 +25,7 @@ DeviceCommunicationControlServices, ) -from .helpers import ApplicationNetwork +from .helpers import ApplicationNetwork, SnifferNode # some debugging _debug = 0 @@ -426,3 +426,83 @@ def test_9_39_1(self): # run the group anet.run() + +@bacpypes_debugging +class TestAPDURetryTimeout(unittest.TestCase): + + def test_apdu_retry_default(self): + """Confirmed Request - No Reply""" + if _debug: TestAPDURetryTimeout._debug("test_apdu_retry") + + # create a network + anet = ApplicationNetwork() + + # adjust test if default retries changes + assert anet.iut_device_object.numberOfApduRetries == 3 + + # add a sniffer to see requests without doing anything + sniffer = SnifferNode(anet.vlan) + anet.append(sniffer) + + # no TD application layer matching + anet.td.start_state.success() + + # send a request to a non-existent device, get it rejected + anet.iut.start_state.doc("7-7-0") \ + .send(ReadPropertyRequest( + objectIdentifier=('analogValue', 1), + propertyIdentifier='presentValue', + destination=Address(99), + )).doc("7-7-1") \ + .receive(AbortPDU, apduAbortRejectReason=65).doc("7-7-2") \ + .success() + + # see the attempts and nothing else + sniffer.start_state.doc("7-8-0") \ + .receive(PDU).doc("7-8-1") \ + .receive(PDU).doc("7-8-2") \ + .receive(PDU).doc("7-8-3") \ + .receive(PDU).doc("7-8-4") \ + .timeout(10).doc("7-8-5") \ + .success() + + # run the group + anet.run() + + def test_apdu_retry_1(self): + """Confirmed Request - No Reply""" + if _debug: TestAPDURetryTimeout._debug("test_apdu_retry_1") + + # create a network + anet = ApplicationNetwork() + + # change the retry count in the device properties + anet.iut_device_object.numberOfApduRetries = 1 + + # add a sniffer to see requests without doing anything + sniffer = SnifferNode(anet.vlan) + anet.append(sniffer) + + # no TD application layer matching + anet.td.start_state.success() + + # send a request to a non-existent device, get it rejected + anet.iut.start_state.doc("7-9-0") \ + .send(ReadPropertyRequest( + objectIdentifier=('analogValue', 1), + propertyIdentifier='presentValue', + destination=Address(99), + )).doc("7-9-1") \ + .receive(AbortPDU, apduAbortRejectReason=65).doc("7-9-2") \ + .success() + + # see the attempts and nothing else + sniffer.start_state.doc("7-10-0") \ + .receive(PDU).doc("7-10-1") \ + .receive(PDU).doc("7-10-2") \ + .timeout(10).doc("7-10-3") \ + .success() + + # run the group + anet.run() + diff --git a/tests/time_machine.py b/tests/time_machine.py index f6a93238..6ea853ae 100755 --- a/tests/time_machine.py +++ b/tests/time_machine.py @@ -90,7 +90,7 @@ def more_to_do(self): return False # peek at the next task and see when it is supposed to run - when, task = self.tasks[0] + when, n, task = self.tasks[0] if when >= self.time_limit: if _debug: TimeMachine._debug(" - next task at or exceeds time limit") return False @@ -119,7 +119,7 @@ def get_next_task(self): else: # peek at the next task and see when it is supposed to run - when, _ = self.tasks[0] + when, n, _ = self.tasks[0] if when >= self.time_limit: if _debug: TimeMachine._debug(" - time limit reached") @@ -128,7 +128,7 @@ def get_next_task(self): else: # pull it off the list - when, task = heappop(self.tasks) + when, n, task = heappop(self.tasks) if _debug: TimeMachine._debug(" - when, task: %r, %s", when, task) # mark that it is no longer scheduled From 8b1d3650a782af9db325fd2a6430bade6eb53775 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 24 May 2018 01:04:22 -0400 Subject: [PATCH 15/50] shuffle the registration check #177 --- py25/bacpypes/bvllservice.py | 13 ++++++++----- py27/bacpypes/bvllservice.py | 13 ++++++++----- py34/bacpypes/bvllservice.py | 13 ++++++++----- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/py25/bacpypes/bvllservice.py b/py25/bacpypes/bvllservice.py index ecfb5014..2588f4c0 100755 --- a/py25/bacpypes/bvllservice.py +++ b/py25/bacpypes/bvllservice.py @@ -546,11 +546,6 @@ def confirmation(self, pdu): return - # check the BBMD registration status, we may not be registered - if self.registrationStatus != 0: - if _debug: BIPForeign._debug(" - packet dropped, unregistered") - return - if isinstance(pdu, OriginalUnicastNPDU): # build a vanilla PDU xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) @@ -559,6 +554,11 @@ def confirmation(self, pdu): self.response(xpdu) elif isinstance(pdu, ForwardedNPDU): + # check the BBMD registration status, we may not be registered + if self.registrationStatus != 0: + if _debug: BIPForeign._debug(" - packet dropped, unregistered") + return + # make sure the forwarded PDU from the bbmd if pdu.pduSource != self.bbmdAddress: if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") @@ -626,6 +626,9 @@ def confirmation(self, pdu): # send it downstream self.request(xpdu) + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPForeign._debug(" - packet dropped") + else: BIPForeign._warning("invalid pdu type: %s", type(pdu)) diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index f3d4276c..a1147149 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -542,11 +542,6 @@ def confirmation(self, pdu): return - # check the BBMD registration status, we may not be registered - if self.registrationStatus != 0: - if _debug: BIPForeign._debug(" - packet dropped, unregistered") - return - if isinstance(pdu, OriginalUnicastNPDU): # build a vanilla PDU xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) @@ -555,6 +550,11 @@ def confirmation(self, pdu): self.response(xpdu) elif isinstance(pdu, ForwardedNPDU): + # check the BBMD registration status, we may not be registered + if self.registrationStatus != 0: + if _debug: BIPForeign._debug(" - packet dropped, unregistered") + return + # make sure the forwarded PDU from the bbmd if pdu.pduSource != self.bbmdAddress: if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") @@ -622,6 +622,9 @@ def confirmation(self, pdu): # send it downstream self.request(xpdu) + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPForeign._debug(" - packet dropped") + else: BIPForeign._warning("invalid pdu type: %s", type(pdu)) diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index c3823879..577deb72 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -541,11 +541,6 @@ def confirmation(self, pdu): return - # check the BBMD registration status, we may not be registered - if self.registrationStatus != 0: - if _debug: BIPForeign._debug(" - packet dropped, unregistered") - return - if isinstance(pdu, OriginalUnicastNPDU): # build a vanilla PDU xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) @@ -554,6 +549,11 @@ def confirmation(self, pdu): self.response(xpdu) elif isinstance(pdu, ForwardedNPDU): + # check the BBMD registration status, we may not be registered + if self.registrationStatus != 0: + if _debug: BIPForeign._debug(" - packet dropped, unregistered") + return + # make sure the forwarded PDU from the bbmd if pdu.pduSource != self.bbmdAddress: if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") @@ -621,6 +621,9 @@ def confirmation(self, pdu): # send it downstream self.request(xpdu) + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPForeign._debug(" - packet dropped") + else: BIPForeign._warning("invalid pdu type: %s", type(pdu)) From 6170b75fd4a79efcb441fbe139447f31284e7634 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 24 May 2018 16:33:48 -0400 Subject: [PATCH 16/50] make it easier to initialize local device objects from the INI file, clean up samples --- py25/bacpypes/local/device.py | 142 ++++++++++++++++++---- py27/bacpypes/local/device.py | 144 ++++++++++++++++++----- py34/bacpypes/local/device.py | 142 ++++++++++++++++++---- samples/AccumulatorObject.py | 13 +- samples/COVClient.py | 13 +- samples/COVClientApp.py | 13 +- samples/COVServer.py | 17 +-- samples/CommandableMixin.py | 13 +- samples/DeviceCommunicationControl.py | 13 +- samples/DeviceDiscovery.py | 19 +-- samples/DeviceDiscoveryForeign.py | 19 +-- samples/HTTPServer.py | 13 +- samples/LocalScheduleObject.py | 13 +- samples/MultiStateValueObject.py | 13 +- samples/MultipleReadProperty.py | 15 +-- samples/MultipleReadPropertyHammer.py | 13 +- samples/MultipleReadPropertyThreaded.py | 13 +- samples/RandomAnalogValueSleep.py | 13 +- samples/ReadAllProperties.py | 18 +-- samples/ReadObjectList.py | 9 +- samples/ReadProperty.py | 17 +-- samples/ReadProperty25.py | 13 +- samples/ReadPropertyAny.py | 13 +- samples/ReadPropertyMultiple.py | 13 +- samples/ReadPropertyMultiple25.py | 13 +- samples/ReadPropertyMultipleServer.py | 16 +-- samples/ReadPropertyMultipleServer25.py | 13 +- samples/ReadRange.py | 13 +- samples/ReadWriteEventMessageTexts.py | 14 +-- samples/ReadWriteFile.py | 15 +-- samples/ReadWriteFileServer.py | 15 +-- samples/ReadWriteProperty.py | 13 +- samples/RecurringMultipleReadProperty.py | 15 +-- samples/ThreadedReadProperty.py | 13 +- samples/VendorAVObject.py | 13 +- samples/VendorReadWriteProperty.py | 16 +-- samples/WhoIsIAm.py | 14 +-- samples/WhoIsIAmForeign.py | 13 +- samples/WritePropertyTCPServer.py | 13 +- samples/WriteSomething.py | 13 +- 40 files changed, 439 insertions(+), 507 deletions(-) diff --git a/py25/bacpypes/local/device.py b/py25/bacpypes/local/device.py index 7af52a7d..bab2bdd3 100644 --- a/py25/bacpypes/local/device.py +++ b/py25/bacpypes/local/device.py @@ -2,7 +2,9 @@ from ..debugging import bacpypes_debugging, ModuleLogger -from ..primitivedata import Date, Time, ObjectIdentifier +from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ + OctetString, CharacterString, BitString, Enumerated, Date, Time, \ + ObjectIdentifier from ..constructeddata import ArrayOf from ..basetypes import ServicesSupported @@ -107,21 +109,113 @@ class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): def __init__(self, **kwargs): if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] + # start with an empty dictionary of device object properties + init_args = {} + ini_arg = kwargs.get('ini', None) + if _debug: LocalDeviceObject._debug(" - ini_arg: %r", dir(ini_arg)) - # check for registration + # check for registration as a keyword parameter or in the INI file if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: + if _debug: LocalDeviceObject._debug(" - unregistered") + + vendor_identifier = kwargs.get('vendorIdentifier', None) + if _debug: LocalDeviceObject._debug(" - keyword vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + vendor_identifier = getattr(ini_arg, 'vendoridentifier', None) + if _debug: LocalDeviceObject._debug(" - INI vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + register_object_type(self.__class__, vendor_id=vendor_identifier) + + # look for properties, fill in values from the keyword arguments or + # the INI parameter (converted to a proper value) if it was provided + for propid, prop in self._properties.items(): + # special processing for object identifier + if propid == 'objectIdentifier': + continue + + # use keyword argument if it was provided + if propid in kwargs: + prop_value = kwargs[propid] + else: + prop_value = getattr(ini_arg, propid.lower(), None) + if prop_value is None: + continue + + prop_datatype = prop.datatype + + if issubclass(prop_datatype, Null): + if prop_value != "Null": + raise ValueError("invalid null property value: %r" % (propid,)) + prop_value = None + + elif issubclass(prop_datatype, Boolean): + prop_value = prop_value.lower() + if prop_value not in ('true', 'false', 'set', 'reset'): + raise ValueError("invalid boolean property value: %r" % (propid,)) + prop_value = prop_value in ('true', 'set') + + elif issubclass(prop_datatype, (Unsigned, Integer)): + try: + prop_value = int(prop_value) + except ValueError: + raise ValueError("invalid unsigned or integer property value: %r" % (propid,)) + + elif issubclass(prop_datatype, (Real, Double)): + try: + prop_value = float(prop_value) + except ValueError: + raise ValueError("invalid real or double property value: %r" % (propid,)) + + elif issubclass(prop_datatype, OctetString): + try: + prop_value = xtob(prop_value) + except: + raise ValueError("invalid octet string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, CharacterString): + pass + + elif issubclass(prop_datatype, BitString): + try: + bstr, prop_value = prop_value, [] + for b in bstr: + if b not in ('0', '1'): + raise ValueError + prop_value.append(int(b)) + except: + raise ValueError("invalid bit string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, Enumerated): + pass + + else: + raise ValueError("cannot interpret %r INI paramter" % (propid,)) + if _debug: LocalDeviceObject._debug(" - property %r: %r", propid, prop_value) + + # at long last + init_args[propid] = prop_value + + # check for object identifier as a keyword parameter or in the INI file, + # and it might be just an int, so make it a tuple if necessary + if 'objectIdentifier' in kwargs: + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + elif hasattr(ini_arg, 'objectidentifier'): + object_identifier = ('device', int(getattr(ini_arg, 'objectidentifier'))) + else: + raise RuntimeError("objectIdentifier is required") + init_args['objectIdentifier'] = object_identifier + if _debug: LocalDeviceObject._debug(" - object identifier: %r", object_identifier) + + # fill in default property values not in init_args + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in init_args: + init_args[attr] = value # check for properties this class implements if 'localDate' in kwargs: @@ -131,29 +225,25 @@ def __init__(self, **kwargs): if 'protocolServicesSupported' in kwargs: raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - # the object list is provided if 'objectList' in kwargs: raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + init_args['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: + if init_args['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + if _debug: LocalDeviceObject._debug(" - init_args: %r", init_args) # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) + super(LocalDeviceObject, self).__init__(**init_args) + + # pass along special property values that are not BACnet properties + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) bacpypes_debugging(LocalDeviceObject) diff --git a/py27/bacpypes/local/device.py b/py27/bacpypes/local/device.py index f1aef729..5256c28f 100644 --- a/py27/bacpypes/local/device.py +++ b/py27/bacpypes/local/device.py @@ -1,8 +1,10 @@ #!/usr/bin/env python -from ..debugging import bacpypes_debugging, ModuleLogger +from ..debugging import bacpypes_debugging, ModuleLogger, xtob -from ..primitivedata import Date, Time, ObjectIdentifier +from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ + OctetString, CharacterString, BitString, Enumerated, Date, Time, \ + ObjectIdentifier from ..constructeddata import ArrayOf from ..basetypes import ServicesSupported @@ -107,21 +109,113 @@ class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): def __init__(self, **kwargs): if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] + # start with an empty dictionary of device object properties + init_args = {} + ini_arg = kwargs.get('ini', None) + if _debug: LocalDeviceObject._debug(" - ini_arg: %r", dir(ini_arg)) - # check for registration + # check for registration as a keyword parameter or in the INI file if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: + if _debug: LocalDeviceObject._debug(" - unregistered") + + vendor_identifier = kwargs.get('vendorIdentifier', None) + if _debug: LocalDeviceObject._debug(" - keyword vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + vendor_identifier = getattr(ini_arg, 'vendoridentifier', None) + if _debug: LocalDeviceObject._debug(" - INI vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + register_object_type(self.__class__, vendor_id=vendor_identifier) + + # look for properties, fill in values from the keyword arguments or + # the INI parameter (converted to a proper value) if it was provided + for propid, prop in self._properties.items(): + # special processing for object identifier + if propid == 'objectIdentifier': + continue + + # use keyword argument if it was provided + if propid in kwargs: + prop_value = kwargs[propid] + else: + prop_value = getattr(ini_arg, propid.lower(), None) + if prop_value is None: + continue + + prop_datatype = prop.datatype + + if issubclass(prop_datatype, Null): + if prop_value != "Null": + raise ValueError("invalid null property value: %r" % (propid,)) + prop_value = None + + elif issubclass(prop_datatype, Boolean): + prop_value = prop_value.lower() + if prop_value not in ('true', 'false', 'set', 'reset'): + raise ValueError("invalid boolean property value: %r" % (propid,)) + prop_value = prop_value in ('true', 'set') + + elif issubclass(prop_datatype, (Unsigned, Integer)): + try: + prop_value = int(prop_value) + except ValueError: + raise ValueError("invalid unsigned or integer property value: %r" % (propid,)) + + elif issubclass(prop_datatype, (Real, Double)): + try: + prop_value = float(prop_value) + except ValueError: + raise ValueError("invalid real or double property value: %r" % (propid,)) + + elif issubclass(prop_datatype, OctetString): + try: + prop_value = xtob(prop_value) + except: + raise ValueError("invalid octet string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, CharacterString): + pass + + elif issubclass(prop_datatype, BitString): + try: + bstr, prop_value = prop_value, [] + for b in bstr: + if b not in ('0', '1'): + raise ValueError + prop_value.append(int(b)) + except: + raise ValueError("invalid bit string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, Enumerated): + pass + + else: + raise ValueError("cannot interpret %r INI paramter" % (propid,)) + if _debug: LocalDeviceObject._debug(" - property %r: %r", propid, prop_value) + + # at long last + init_args[propid] = prop_value + + # check for object identifier as a keyword parameter or in the INI file, + # and it might be just an int, so make it a tuple if necessary + if 'objectIdentifier' in kwargs: + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + elif hasattr(ini_arg, 'objectidentifier'): + object_identifier = ('device', int(getattr(ini_arg, 'objectidentifier'))) + else: + raise RuntimeError("objectIdentifier is required") + init_args['objectIdentifier'] = object_identifier + if _debug: LocalDeviceObject._debug(" - object identifier: %r", object_identifier) + + # fill in default property values not in init_args + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in init_args: + init_args[attr] = value # check for properties this class implements if 'localDate' in kwargs: @@ -131,27 +225,23 @@ def __init__(self, **kwargs): if 'protocolServicesSupported' in kwargs: raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - # the object list is provided if 'objectList' in kwargs: raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + init_args['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: + if init_args['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + if _debug: LocalDeviceObject._debug(" - init_args: %r", init_args) # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) + super(LocalDeviceObject, self).__init__(**init_args) + + # pass along special property values that are not BACnet properties + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) diff --git a/py34/bacpypes/local/device.py b/py34/bacpypes/local/device.py index c823b897..88d7ba99 100644 --- a/py34/bacpypes/local/device.py +++ b/py34/bacpypes/local/device.py @@ -2,7 +2,9 @@ from ..debugging import bacpypes_debugging, ModuleLogger -from ..primitivedata import Date, Time, ObjectIdentifier +from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ + OctetString, CharacterString, BitString, Enumerated, Date, Time, \ + ObjectIdentifier from ..constructeddata import ArrayOf from ..basetypes import ServicesSupported @@ -107,21 +109,113 @@ class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): def __init__(self, **kwargs): if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] + # start with an empty dictionary of device object properties + init_args = {} + ini_arg = kwargs.get('ini', None) + if _debug: LocalDeviceObject._debug(" - ini_arg: %r", dir(ini_arg)) - # check for registration + # check for registration as a keyword parameter or in the INI file if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: + if _debug: LocalDeviceObject._debug(" - unregistered") + + vendor_identifier = kwargs.get('vendorIdentifier', None) + if _debug: LocalDeviceObject._debug(" - keyword vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + vendor_identifier = getattr(ini_arg, 'vendoridentifier', None) + if _debug: LocalDeviceObject._debug(" - INI vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + register_object_type(self.__class__, vendor_id=vendor_identifier) + + # look for properties, fill in values from the keyword arguments or + # the INI parameter (converted to a proper value) if it was provided + for propid, prop in self._properties.items(): + # special processing for object identifier + if propid == 'objectIdentifier': + continue + + # use keyword argument if it was provided + if propid in kwargs: + prop_value = kwargs[propid] + else: + prop_value = getattr(ini_arg, propid.lower(), None) + if prop_value is None: + continue + + prop_datatype = prop.datatype + + if issubclass(prop_datatype, Null): + if prop_value != "Null": + raise ValueError("invalid null property value: %r" % (propid,)) + prop_value = None + + elif issubclass(prop_datatype, Boolean): + prop_value = prop_value.lower() + if prop_value not in ('true', 'false', 'set', 'reset'): + raise ValueError("invalid boolean property value: %r" % (propid,)) + prop_value = prop_value in ('true', 'set') + + elif issubclass(prop_datatype, (Unsigned, Integer)): + try: + prop_value = int(prop_value) + except ValueError: + raise ValueError("invalid unsigned or integer property value: %r" % (propid,)) + + elif issubclass(prop_datatype, (Real, Double)): + try: + prop_value = float(prop_value) + except ValueError: + raise ValueError("invalid real or double property value: %r" % (propid,)) + + elif issubclass(prop_datatype, OctetString): + try: + prop_value = xtob(prop_value) + except: + raise ValueError("invalid octet string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, CharacterString): + pass + + elif issubclass(prop_datatype, BitString): + try: + bstr, prop_value = prop_value, [] + for b in bstr: + if b not in ('0', '1'): + raise ValueError + prop_value.append(int(b)) + except: + raise ValueError("invalid bit string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, Enumerated): + pass + + else: + raise ValueError("cannot interpret %r INI paramter" % (propid,)) + if _debug: LocalDeviceObject._debug(" - property %r: %r", propid, prop_value) + + # at long last + init_args[propid] = prop_value + + # check for object identifier as a keyword parameter or in the INI file, + # and it might be just an int, so make it a tuple if necessary + if 'objectIdentifier' in kwargs: + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, int): + object_identifier = ('device', object_identifier) + elif hasattr(ini_arg, 'objectidentifier'): + object_identifier = ('device', int(getattr(ini_arg, 'objectidentifier'))) + else: + raise RuntimeError("objectIdentifier is required") + init_args['objectIdentifier'] = object_identifier + if _debug: LocalDeviceObject._debug(" - object identifier: %r", object_identifier) + + # fill in default property values not in init_args + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in init_args: + init_args[attr] = value # check for properties this class implements if 'localDate' in kwargs: @@ -131,27 +225,23 @@ def __init__(self, **kwargs): if 'protocolServicesSupported' in kwargs: raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, int): - object_identifier = ('device', object_identifier) - # the object list is provided if 'objectList' in kwargs: raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + init_args['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: + if init_args['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + if _debug: LocalDeviceObject._debug(" - init_args: %r", init_args) # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) + super(LocalDeviceObject, self).__init__(**init_args) + + # pass along special property values that are not BACnet properties + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) diff --git a/samples/AccumulatorObject.py b/samples/AccumulatorObject.py index a0b8a0c9..d317d418 100755 --- a/samples/AccumulatorObject.py +++ b/samples/AccumulatorObject.py @@ -70,17 +70,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=('device', int(args.ini.objectidentifier)), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/COVClient.py b/samples/COVClient.py index a904e027..dd6f8aa0 100755 --- a/samples/COVClient.py +++ b/samples/COVClient.py @@ -217,17 +217,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = SubscribeCOVApplication(this_device, args.ini.address) diff --git a/samples/COVClientApp.py b/samples/COVClientApp.py index b28e760c..41c601f0 100755 --- a/samples/COVClientApp.py +++ b/samples/COVClientApp.py @@ -165,17 +165,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = SubscribeCOVApplication(this_device, args.ini.address) diff --git a/samples/COVServer.py b/samples/COVServer.py index 18c53d9c..4c32dcda 100755 --- a/samples/COVServer.py +++ b/samples/COVServer.py @@ -365,20 +365,11 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - test_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application - test_application = SubscribeCOVApplication(test_device, args.ini.address) + test_application = SubscribeCOVApplication(this_device, args.ini.address) # make an analog value object test_av = AnalogValueObject( @@ -392,7 +383,7 @@ def main(): # add it to the device test_application.add_object(test_av) - _log.debug(" - object list: %r", test_device.objectList) + _log.debug(" - object list: %r", this_device.objectList) # make a binary value object test_bv = BinaryValueObject( diff --git a/samples/CommandableMixin.py b/samples/CommandableMixin.py index 11242d27..2a268e09 100644 --- a/samples/CommandableMixin.py +++ b/samples/CommandableMixin.py @@ -379,17 +379,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/DeviceCommunicationControl.py b/samples/DeviceCommunicationControl.py index 58b8c925..d679a097 100755 --- a/samples/DeviceCommunicationControl.py +++ b/samples/DeviceCommunicationControl.py @@ -123,17 +123,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/DeviceDiscovery.py b/samples/DeviceDiscovery.py index d68fd63c..11b0c1af 100755 --- a/samples/DeviceDiscovery.py +++ b/samples/DeviceDiscovery.py @@ -16,10 +16,9 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.apdu import WhoIsRequest, IAmRequest, ReadPropertyRequest, ReadPropertyACK +from bacpypes.apdu import WhoIsRequest, ReadPropertyRequest, ReadPropertyACK from bacpypes.primitivedata import CharacterString -from bacpypes.basetypes import ServicesSupported -from bacpypes.errors import DecodingError +from bacpypes.errors import MissingRequiredParameter from bacpypes.app import BIPSimpleApplication from bacpypes.local.device import LocalDeviceObject @@ -149,7 +148,6 @@ def do_whois(self, args): try: # gather the parameters - request = WhoIsRequest() if (len(args) == 1) or (len(args) == 3): addr = Address(args[0]) del args[0] @@ -200,17 +198,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = DiscoveryApplication(this_device, args.ini.address) diff --git a/samples/DeviceDiscoveryForeign.py b/samples/DeviceDiscoveryForeign.py index b75011a9..8634552b 100755 --- a/samples/DeviceDiscoveryForeign.py +++ b/samples/DeviceDiscoveryForeign.py @@ -16,10 +16,9 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.apdu import WhoIsRequest, IAmRequest, ReadPropertyRequest, ReadPropertyACK +from bacpypes.apdu import WhoIsRequest, ReadPropertyRequest, ReadPropertyACK from bacpypes.primitivedata import CharacterString -from bacpypes.basetypes import ServicesSupported -from bacpypes.errors import DecodingError +from bacpypes.errors import MissingRequiredParameter from bacpypes.app import BIPForeignApplication from bacpypes.local.device import LocalDeviceObject @@ -149,7 +148,6 @@ def do_whois(self, args): try: # gather the parameters - request = WhoIsRequest() if (len(args) == 1) or (len(args) == 3): addr = Address(args[0]) del args[0] @@ -200,17 +198,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = DiscoveryApplication( diff --git a/samples/HTTPServer.py b/samples/HTTPServer.py index a7ceca27..64681e63 100755 --- a/samples/HTTPServer.py +++ b/samples/HTTPServer.py @@ -179,17 +179,8 @@ class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/LocalScheduleObject.py b/samples/LocalScheduleObject.py index 2a4c1d9c..83776c85 100644 --- a/samples/LocalScheduleObject.py +++ b/samples/LocalScheduleObject.py @@ -74,17 +74,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=('device', int(args.ini.objectidentifier)), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/MultiStateValueObject.py b/samples/MultiStateValueObject.py index 7abf2309..cd3d201f 100755 --- a/samples/MultiStateValueObject.py +++ b/samples/MultiStateValueObject.py @@ -33,17 +33,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/MultipleReadProperty.py b/samples/MultipleReadProperty.py index 45d2e9b4..64064683 100755 --- a/samples/MultipleReadProperty.py +++ b/samples/MultipleReadProperty.py @@ -18,7 +18,7 @@ from bacpypes.pdu import Address from bacpypes.object import get_datatype -from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK +from bacpypes.apdu import ReadPropertyRequest from bacpypes.primitivedata import Unsigned from bacpypes.constructeddata import Array @@ -131,17 +131,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = ReadPointListApplication(point_list, this_device, args.ini.address) diff --git a/samples/MultipleReadPropertyHammer.py b/samples/MultipleReadPropertyHammer.py index e7812bc4..4a641aaa 100755 --- a/samples/MultipleReadPropertyHammer.py +++ b/samples/MultipleReadPropertyHammer.py @@ -201,17 +201,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = ReadPointListApplication(this_device, args.ini.address) diff --git a/samples/MultipleReadPropertyThreaded.py b/samples/MultipleReadPropertyThreaded.py index dfe85898..4ad8c0d7 100755 --- a/samples/MultipleReadPropertyThreaded.py +++ b/samples/MultipleReadPropertyThreaded.py @@ -123,17 +123,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/RandomAnalogValueSleep.py b/samples/RandomAnalogValueSleep.py index 0022d6b5..0f9cf3de 100644 --- a/samples/RandomAnalogValueSleep.py +++ b/samples/RandomAnalogValueSleep.py @@ -108,17 +108,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=('device', int(args.ini.objectidentifier)), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadAllProperties.py b/samples/ReadAllProperties.py index 448da749..c80bf373 100755 --- a/samples/ReadAllProperties.py +++ b/samples/ReadAllProperties.py @@ -11,15 +11,12 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger from bacpypes.consolelogging import ConfigArgumentParser -from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, stop, deferred from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK -from bacpypes.primitivedata import Unsigned -from bacpypes.constructeddata import Array +from bacpypes.apdu import ReadPropertyRequest from bacpypes.app import BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype @@ -157,17 +154,8 @@ def main(): if _debug: _log.debug(" - property_list: %r", property_list) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = ReadPropertyApplication(this_device, args.ini.address) diff --git a/samples/ReadObjectList.py b/samples/ReadObjectList.py index 63756496..f51240ac 100755 --- a/samples/ReadObjectList.py +++ b/samples/ReadObjectList.py @@ -214,13 +214,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = ReadObjectListApplication(this_device, args.ini.address) diff --git a/samples/ReadProperty.py b/samples/ReadProperty.py index 19ac78dd..d8551e4c 100755 --- a/samples/ReadProperty.py +++ b/samples/ReadProperty.py @@ -151,21 +151,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadProperty25.py b/samples/ReadProperty25.py index 76e05276..9885b53a 100755 --- a/samples/ReadProperty25.py +++ b/samples/ReadProperty25.py @@ -153,17 +153,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyAny.py b/samples/ReadPropertyAny.py index ac082b7c..ff1c042f 100755 --- a/samples/ReadPropertyAny.py +++ b/samples/ReadPropertyAny.py @@ -17,9 +17,9 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.object import get_datatype, get_object_class +from bacpypes.object import get_object_class -from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK +from bacpypes.apdu import ReadPropertyRequest from bacpypes.primitivedata import Tag from bacpypes.app import BIPSimpleApplication @@ -124,13 +124,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # provide max segments accepted if any kind of segmentation supported if args.ini.segmentationsupported != 'noSegmentation': diff --git a/samples/ReadPropertyMultiple.py b/samples/ReadPropertyMultiple.py index dc814a38..35456b78 100755 --- a/samples/ReadPropertyMultiple.py +++ b/samples/ReadPropertyMultiple.py @@ -203,17 +203,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultiple25.py b/samples/ReadPropertyMultiple25.py index d0cb8139..ab712e6d 100755 --- a/samples/ReadPropertyMultiple25.py +++ b/samples/ReadPropertyMultiple25.py @@ -209,17 +209,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultipleServer.py b/samples/ReadPropertyMultipleServer.py index 2f2a1058..13c9d7df 100755 --- a/samples/ReadPropertyMultipleServer.py +++ b/samples/ReadPropertyMultipleServer.py @@ -20,6 +20,8 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.service.device import DeviceCommunicationControlServices from bacpypes.service.object import ReadWritePropertyMultipleServices +from bacpypes.local.device import LocalDeviceObject + # some debugging _debug = 0 @@ -95,18 +97,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - _dcc_password="xyzzy", - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = ReadPropertyMultipleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultipleServer25.py b/samples/ReadPropertyMultipleServer25.py index 05d60455..29f3e8a3 100755 --- a/samples/ReadPropertyMultipleServer25.py +++ b/samples/ReadPropertyMultipleServer25.py @@ -95,17 +95,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = ReadPropertyMultipleApplication(this_device, args.ini.address) diff --git a/samples/ReadRange.py b/samples/ReadRange.py index cca08201..53f77aa8 100755 --- a/samples/ReadRange.py +++ b/samples/ReadRange.py @@ -121,17 +121,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteEventMessageTexts.py b/samples/ReadWriteEventMessageTexts.py index 5b47a220..5effd7f9 100644 --- a/samples/ReadWriteEventMessageTexts.py +++ b/samples/ReadWriteEventMessageTexts.py @@ -228,19 +228,9 @@ def main(): context = args.address, args.objtype, args.objinst if _debug: _log.debug(" - context: %r", context) - # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteFile.py b/samples/ReadWriteFile.py index f3eae017..a1a47109 100755 --- a/samples/ReadWriteFile.py +++ b/samples/ReadWriteFile.py @@ -18,7 +18,7 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.apdu import Error, AbortPDU, \ +from bacpypes.apdu import \ AtomicReadFileRequest, \ AtomicReadFileRequestAccessMethodChoice, \ AtomicReadFileRequestAccessMethodChoiceRecordAccess, \ @@ -311,17 +311,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteFileServer.py b/samples/ReadWriteFileServer.py index 6ddeddd3..00042237 100755 --- a/samples/ReadWriteFileServer.py +++ b/samples/ReadWriteFileServer.py @@ -16,7 +16,7 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.local.device import LocalDeviceObject -from bacpypes.local.file import FileServices, LocalRecordAccessFileObject, LocalStreamAccessFileObject +from bacpypes.local.file import LocalRecordAccessFileObject, LocalStreamAccessFileObject from bacpypes.service.file import FileServices # some debugging @@ -174,17 +174,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteProperty.py b/samples/ReadWriteProperty.py index 18dfe055..d77ff09a 100755 --- a/samples/ReadWriteProperty.py +++ b/samples/ReadWriteProperty.py @@ -271,17 +271,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/RecurringMultipleReadProperty.py b/samples/RecurringMultipleReadProperty.py index 298521f6..a479089e 100755 --- a/samples/RecurringMultipleReadProperty.py +++ b/samples/RecurringMultipleReadProperty.py @@ -19,7 +19,7 @@ from bacpypes.pdu import Address from bacpypes.object import get_datatype -from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK +from bacpypes.apdu import ReadPropertyRequest from bacpypes.primitivedata import Unsigned from bacpypes.constructeddata import Array @@ -164,17 +164,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a dog this_application = PrairieDog(args.interval, this_device, args.ini.address) diff --git a/samples/ThreadedReadProperty.py b/samples/ThreadedReadProperty.py index 2fcdcc66..6f79d717 100755 --- a/samples/ThreadedReadProperty.py +++ b/samples/ThreadedReadProperty.py @@ -161,17 +161,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/VendorAVObject.py b/samples/VendorAVObject.py index 85ab1253..f173e7d0 100755 --- a/samples/VendorAVObject.py +++ b/samples/VendorAVObject.py @@ -95,17 +95,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=vendor_id, - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/VendorReadWriteProperty.py b/samples/VendorReadWriteProperty.py index fd1f5e3a..2d64493f 100755 --- a/samples/VendorReadWriteProperty.py +++ b/samples/VendorReadWriteProperty.py @@ -20,8 +20,7 @@ from bacpypes.pdu import Address from bacpypes.object import get_object_class, get_datatype -from bacpypes.apdu import Error, AbortPDU, SimpleAckPDU, \ - ReadPropertyRequest, ReadPropertyACK, WritePropertyRequest +from bacpypes.apdu import ReadPropertyRequest, WritePropertyRequest from bacpypes.primitivedata import Tag, Null, Atomic, Integer, Unsigned, Real from bacpypes.constructeddata import Array, Any @@ -243,17 +242,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/WhoIsIAm.py b/samples/WhoIsIAm.py index 7ed9b65d..331cf265 100755 --- a/samples/WhoIsIAm.py +++ b/samples/WhoIsIAm.py @@ -17,7 +17,6 @@ from bacpypes.pdu import Address, GlobalBroadcast from bacpypes.apdu import WhoIsRequest, IAmRequest -from bacpypes.basetypes import ServicesSupported from bacpypes.errors import DecodingError from bacpypes.app import BIPSimpleApplication @@ -180,17 +179,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = WhoIsIAmApplication( diff --git a/samples/WhoIsIAmForeign.py b/samples/WhoIsIAmForeign.py index 59956f8e..eeaa88e2 100755 --- a/samples/WhoIsIAmForeign.py +++ b/samples/WhoIsIAmForeign.py @@ -190,17 +190,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = WhoIsIAmApplication( diff --git a/samples/WritePropertyTCPServer.py b/samples/WritePropertyTCPServer.py index 1a52623d..8a4f613b 100755 --- a/samples/WritePropertyTCPServer.py +++ b/samples/WritePropertyTCPServer.py @@ -226,17 +226,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/WriteSomething.py b/samples/WriteSomething.py index 003001d7..529399eb 100755 --- a/samples/WriteSomething.py +++ b/samples/WriteSomething.py @@ -118,17 +118,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # provide max segments accepted if any kind of segmentation supported - if args.ini.segmentationsupported != 'noSegmentation': - this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) From 6d1c019aefa1af019473ffea7c39741ca73e14af Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 24 May 2018 23:29:19 -0400 Subject: [PATCH 17/50] doc string update --- py25/bacpypes/app.py | 5 ++--- py27/bacpypes/app.py | 5 ++--- py34/bacpypes/app.py | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/py25/bacpypes/app.py b/py25/bacpypes/app.py index 895fadad..9be79fb7 100755 --- a/py25/bacpypes/app.py +++ b/py25/bacpypes/app.py @@ -166,9 +166,8 @@ def update_device_info(self, device_info): device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) def acquire(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" + """Return the known information about the device and mark the record + as being used by a segmenation state machine.""" if _debug: DeviceInfoCache._debug("acquire %r", key) if isinstance(key, int): diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index a66dffd7..1548a3b4 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -166,9 +166,8 @@ def update_device_info(self, device_info): device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) def acquire(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" + """Return the known information about the device and mark the record + as being used by a segmenation state machine.""" if _debug: DeviceInfoCache._debug("acquire %r", key) if isinstance(key, int): diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index a66dffd7..1548a3b4 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -166,9 +166,8 @@ def update_device_info(self, device_info): device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) def acquire(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" + """Return the known information about the device and mark the record + as being used by a segmenation state machine.""" if _debug: DeviceInfoCache._debug("acquire %r", key) if isinstance(key, int): From 4a593540155f2ee058c0c472fe0122c0381496af Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 31 May 2018 22:37:11 -0400 Subject: [PATCH 18/50] prep stage for the next minor release --- py25/bacpypes/__init__.py | 2 +- py27/bacpypes/__init__.py | 2 +- py34/bacpypes/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index 3adf9c1c..8e201b5b 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.1' +__version__ = '0.17.2' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 3adf9c1c..8e201b5b 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.1' +__version__ = '0.17.2' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index 084ca281..f556be27 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.1' +__version__ = '0.17.2' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' From 76fd2e7b0008945ce7e2cc970a76b2b564bb2acc Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Sun, 3 Jun 2018 22:35:59 -0400 Subject: [PATCH 19/50] bring the branch up to date --- py25/bacpypes/__init__.py | 4 +- py25/bacpypes/analysis.py | 8 +- py25/bacpypes/apdu.py | 50 +- py25/bacpypes/app.py | 169 ++-- py25/bacpypes/appservice.py | 401 ++++++---- py25/bacpypes/bvllservice.py | 125 +-- py25/bacpypes/constructeddata.py | 38 +- py25/bacpypes/local/__init__.py | 11 + py25/bacpypes/local/device.py | 249 ++++++ py25/bacpypes/local/file.py | 91 +++ py25/bacpypes/local/object.py | 69 ++ py25/bacpypes/local/schedule.py | 561 ++++++++++++++ py25/bacpypes/netservice.py | 601 +++++++++------ py25/bacpypes/npdu.py | 22 +- py25/bacpypes/object.py | 10 +- py25/bacpypes/primitivedata.py | 81 +- py25/bacpypes/service/device.py | 123 +-- py25/bacpypes/service/object.py | 53 -- py25/bacpypes/task.py | 12 +- py25/bacpypes/vlan.py | 19 +- py27/bacpypes/__init__.py | 4 +- py27/bacpypes/analysis.py | 8 +- py27/bacpypes/apdu.py | 43 +- py27/bacpypes/app.py | 172 +++-- py27/bacpypes/appservice.py | 401 ++++++---- py27/bacpypes/bvllservice.py | 126 +-- py27/bacpypes/constructeddata.py | 40 +- py27/bacpypes/local/__init__.py | 11 + py27/bacpypes/local/device.py | 247 ++++++ py27/bacpypes/local/file.py | 89 +++ py27/bacpypes/local/object.py | 68 ++ py27/bacpypes/local/schedule.py | 558 ++++++++++++++ py27/bacpypes/netservice.py | 597 ++++++++------ py27/bacpypes/npdu.py | 28 +- py27/bacpypes/object.py | 9 +- py27/bacpypes/primitivedata.py | 81 +- py27/bacpypes/service/device.py | 122 +-- py27/bacpypes/service/object.py | 55 +- py27/bacpypes/task.py | 12 +- py27/bacpypes/vlan.py | 21 +- py34/bacpypes/__init__.py | 4 +- py34/bacpypes/analysis.py | 8 +- py34/bacpypes/apdu.py | 43 +- py34/bacpypes/app.py | 172 +++-- py34/bacpypes/appservice.py | 399 ++++++---- py34/bacpypes/bvllservice.py | 125 +-- py34/bacpypes/constructeddata.py | 38 +- py34/bacpypes/local/__init__.py | 11 + py34/bacpypes/local/device.py | 247 ++++++ py34/bacpypes/local/file.py | 89 +++ py34/bacpypes/local/object.py | 68 ++ py34/bacpypes/local/schedule.py | 558 ++++++++++++++ py34/bacpypes/netservice.py | 597 ++++++++------ py34/bacpypes/npdu.py | 28 +- py34/bacpypes/object.py | 10 +- py34/bacpypes/primitivedata.py | 59 +- py34/bacpypes/service/device.py | 122 +-- py34/bacpypes/service/object.py | 51 -- py34/bacpypes/task.py | 12 +- py34/bacpypes/vlan.py | 21 +- samples/AccumulatorObject.py | 18 +- samples/BBMD2VLANRouter.py | 3 +- samples/COVClient.py | 18 +- samples/COVClientApp.py | 187 +++++ samples/COVServer.py | 22 +- samples/CommandableMixin.py | 13 +- samples/DeviceCommunicationControl.py | 18 +- samples/DeviceDiscovery.py | 24 +- samples/DeviceDiscoveryForeign.py | 24 +- samples/HTTPServer.py | 18 +- .../HandsOnLab/Sample1_SimpleApplication.py | 9 +- .../HandsOnLab/Sample2_WhoIsIAmApplication.py | 9 +- .../Sample3_WhoHasIHaveApplication.py | 9 +- .../Sample4_RandomAnalogValueObject.py | 9 +- samples/IP2VLANRouter.py | 261 +++++++ samples/LocalScheduleObject.py | 327 ++++++++ samples/MultiStateValueObject.py | 18 +- samples/MultipleReadProperty.py | 20 +- samples/MultipleReadPropertyHammer.py | 19 +- samples/MultipleReadPropertyThreaded.py | 18 +- samples/RandomAnalogValueSleep.py | 18 +- samples/ReadAllProperties.py | 23 +- samples/ReadObjectList.py | 239 ++++++ samples/ReadProperty.py | 18 +- samples/ReadProperty25.py | 18 +- samples/ReadPropertyAny.py | 26 +- samples/ReadPropertyMultiple.py | 44 +- samples/ReadPropertyMultiple25.py | 18 +- samples/ReadPropertyMultipleServer.py | 21 +- samples/ReadPropertyMultipleServer25.py | 20 +- samples/ReadRange.py | 18 +- samples/ReadWriteEventMessageTexts.py | 19 +- samples/ReadWriteFile.py | 20 +- samples/ReadWriteFileServer.py | 22 +- samples/ReadWriteProperty.py | 44 +- samples/RecurringMultipleReadProperty.py | 20 +- samples/ThreadedReadProperty.py | 18 +- samples/Tutorial/SampleConsoleCmd-Answer.py | 9 +- samples/Tutorial/SampleConsoleCmd.py | 9 +- samples/Tutorial/WhoIsIAm.py | 19 +- samples/VendorAVObject.py | 18 +- samples/VendorReadWriteProperty.py | 21 +- samples/WhoIsIAm.py | 28 +- samples/WhoIsIAmForeign.py | 28 +- samples/WhoIsRouter.py | 6 +- samples/WritePropertyTCPServer.py | 262 +++++++ samples/WriteSomething.py | 18 +- sandbox/explore_types.py | 74 ++ sandbox/local_schedule_object_t1.py | 682 ++++++++++++++++ sandbox/local_schedule_object_t2.py | 728 ++++++++++++++++++ sandbox/mutable_schedule_object.py | 71 ++ sandbox/vlan_to_vlan.py | 338 ++++++++ setup.py | 1 + tests/__init__.py | 4 + tests/state_machine.py | 44 +- tests/test_bvll/helpers.py | 313 +++++++- tests/test_bvll/test_bbmd.py | 314 +++++++- tests/test_bvll/test_foreign.py | 175 ++--- tests/test_bvll/test_simple.py | 27 +- tests/test_local/__init__.py | 9 + tests/test_local/test_local_schedule_1.py | 27 + tests/test_local/test_local_schedule_2.py | 110 +++ tests/test_network/__init__.py | 9 + tests/test_network/helpers.py | 279 +++++++ tests/test_network/test_net_1.py | 245 ++++++ tests/test_network/test_net_2.py | 271 +++++++ tests/test_network/test_net_3.py | 411 ++++++++++ tests/test_npdu/__init__.py | 8 + tests/test_npdu/helpers.py | 59 ++ tests/test_npdu/test_codec.py | 361 +++++++++ tests/test_primitive_data/test_date.py | 15 +- tests/test_primitive_data/test_time.py | 6 +- tests/test_segmentation/__init__.py | 7 + tests/test_segmentation/test_1.py | 345 +++++++++ tests/test_service/helpers.py | 27 +- tests/test_service/test_device.py | 127 ++- tests/test_service/test_object.py | 6 +- tests/test_utilities/test_time_machine.py | 69 +- tests/test_vlan/test_ipnetwork.py | 24 + tests/time_machine.py | 152 +++- 140 files changed, 12351 insertions(+), 3106 deletions(-) create mode 100644 py25/bacpypes/local/__init__.py create mode 100644 py25/bacpypes/local/device.py create mode 100644 py25/bacpypes/local/file.py create mode 100644 py25/bacpypes/local/object.py create mode 100644 py25/bacpypes/local/schedule.py create mode 100644 py27/bacpypes/local/__init__.py create mode 100644 py27/bacpypes/local/device.py create mode 100644 py27/bacpypes/local/file.py create mode 100644 py27/bacpypes/local/object.py create mode 100644 py27/bacpypes/local/schedule.py create mode 100644 py34/bacpypes/local/__init__.py create mode 100644 py34/bacpypes/local/device.py create mode 100644 py34/bacpypes/local/file.py create mode 100644 py34/bacpypes/local/object.py create mode 100644 py34/bacpypes/local/schedule.py create mode 100755 samples/COVClientApp.py create mode 100755 samples/IP2VLANRouter.py create mode 100644 samples/LocalScheduleObject.py create mode 100755 samples/ReadObjectList.py create mode 100755 samples/WritePropertyTCPServer.py create mode 100644 sandbox/explore_types.py create mode 100644 sandbox/local_schedule_object_t1.py create mode 100644 sandbox/local_schedule_object_t2.py create mode 100644 sandbox/mutable_schedule_object.py create mode 100755 sandbox/vlan_to_vlan.py create mode 100644 tests/test_local/__init__.py create mode 100644 tests/test_local/test_local_schedule_1.py create mode 100644 tests/test_local/test_local_schedule_2.py create mode 100644 tests/test_network/__init__.py create mode 100644 tests/test_network/helpers.py create mode 100644 tests/test_network/test_net_1.py create mode 100644 tests/test_network/test_net_2.py create mode 100644 tests/test_network/test_net_3.py create mode 100644 tests/test_npdu/__init__.py create mode 100644 tests/test_npdu/helpers.py create mode 100644 tests/test_npdu/test_codec.py create mode 100644 tests/test_segmentation/__init__.py create mode 100644 tests/test_segmentation/test_1.py diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index 90604514..8e201b5b 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.0' +__version__ = '0.17.2' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' @@ -69,6 +69,8 @@ from . import app from . import appservice + +from . import local from . import service # diff --git a/py25/bacpypes/analysis.py b/py25/bacpypes/analysis.py index 95ab2697..1a10b494 100755 --- a/py25/bacpypes/analysis.py +++ b/py25/bacpypes/analysis.py @@ -23,7 +23,7 @@ except: pass -from .debugging import ModuleLogger, DebugContents, bacpypes_debugging, btox +from .debugging import ModuleLogger, bacpypes_debugging, btox from .pdu import PDU, Address from .bvll import BVLPDU, bvl_pdu_types, ForwardedNPDU, \ @@ -147,6 +147,8 @@ def decode_packet(data): # assume it is ethernet for now d = decode_ethernet(data) + pduSource = Address(d['source_address']) + pduDestination = Address(d['destination_address']) data = d['data'] # there could be a VLAN header @@ -177,10 +179,8 @@ def decode_packet(data): decode_packet._debug(" - pduDestination: %r", pduDestination) else: if _debug: decode_packet._debug(" - not a UDP packet") - return None else: if _debug: decode_packet._debug(" - not an IP packet") - return None # check for empty if not data: @@ -364,7 +364,7 @@ def decode_file(fname): # Tracer # -class Tracer(DebugContents): +class Tracer: def __init__(self, initial_state=None): if _debug: Tracer._debug("__init__ initial_state=%r", initial_state) diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index ae3659cf..118b4ab0 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -56,38 +56,51 @@ def register_error_type(klass): # encode_max_segments_accepted/decode_max_segments_accepted # +_max_segments_accepted_encoding = [ + None, 2, 4, 8, 16, 32, 64, None, + ] + def encode_max_segments_accepted(arg): """Encode the maximum number of segments the device will accept, Section - 20.1.2.4""" - w = 0 - while (arg and not arg & 1): - w += 1 - arg = (arg >> 1) - return w + 20.1.2.4, and if the device says it can only accept one segment it shouldn't + say that it supports segmentation!""" + # unspecified + if not arg: + return 0 + + if arg > 64: + return 7 + + # the largest number not greater than the arg + for i in range(6, 0, -1): + if _max_segments_accepted_encoding[i] <= arg: + return i + + raise ValueError("invalid max max segments accepted: {0}".format(arg)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section 20.1.2.4""" - return arg and (1 << arg) or None + return _max_segments_accepted_encoding[arg] # # encode_max_apdu_length_accepted/decode_max_apdu_length_accepted # -_max_apdu_response_encoding = [50, 128, 206, 480, 1024, 1476, None, None, +_max_apdu_length_encoding = [50, 128, 206, 480, 1024, 1476, None, None, None, None, None, None, None, None, None, None] def encode_max_apdu_length_accepted(arg): """Return the encoding of the highest encodable value less than the value of the arg.""" for i in range(5, -1, -1): - if (arg >= _max_apdu_response_encoding[i]): + if (arg >= _max_apdu_length_encoding[i]): return i raise ValueError("invalid max APDU length accepted: {0}".format(arg)) def decode_max_apdu_length_accepted(arg): - v = _max_apdu_response_encoding[arg] + v = _max_apdu_length_encoding[arg] if not v: raise ValueError("invalid max APDU length accepted: {0}".format(arg)) @@ -173,7 +186,7 @@ def encode(self, pdu): if self.apduSA: buff += 0x02 pdu.put(buff) - pdu.put((encode_max_segments_accepted(self.apduMaxSegs) << 4) + encode_max_apdu_length_accepted(self.apduMaxResp)) + pdu.put((self.apduMaxSegs << 4) + self.apduMaxResp) pdu.put(self.apduInvokeID) if self.apduSeg: pdu.put(self.apduSeq) @@ -254,8 +267,8 @@ def decode(self, pdu): self.apduMor = ((buff & 0x04) != 0) self.apduSA = ((buff & 0x02) != 0) buff = pdu.get() - self.apduMaxSegs = decode_max_segments_accepted( (buff >> 4) & 0x07 ) - self.apduMaxResp = decode_max_apdu_length_accepted( buff & 0x0F ) + self.apduMaxSegs = (buff >> 4) & 0x07 + self.apduMaxResp = buff & 0x0F self.apduInvokeID = pdu.get() if self.apduSeg: self.apduSeq = pdu.get() @@ -693,6 +706,7 @@ def decode(self, apdu): # create a tag list and decode the rest of the data self._tag_list = TagList() self._tag_list.decode(apdu) + if _debug: APCISequence._debug(" - tag list: %r", self._tag_list) # pass the taglist to the Sequence for additional decoding Sequence.decode(self, self._tag_list) @@ -1307,7 +1321,7 @@ class LifeSafetyOperationRequest(ConfirmedRequestSequence): [ Element('requestingProcessIdentifier', Unsigned, 0) , Element('requestingSource', CharacterString, 1) , Element('request', LifeSafetyOperation, 2) - , Element('objectIdentifier', ObjectIdentifier, 3) + , Element('objectIdentifier', ObjectIdentifier, 3, True) ] register_confirmed_request_type(LifeSafetyOperationRequest) @@ -1491,7 +1505,7 @@ class RemoveListElementRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('objectIdentifier', ObjectIdentifier, 0) , Element('propertyIdentifier', PropertyIdentifier, 1) - , Element('propertyArrayIndex', Unsigned, 2) + , Element('propertyArrayIndex', Unsigned, 2, True) , Element('listOfElements', Any, 3) ] @@ -1501,9 +1515,9 @@ class RemoveListElementRequest(ConfirmedRequestSequence): class DeviceCommunicationControlRequestEnableDisable(Enumerated): enumerations = \ - { 'enable':0 - , 'disable':1 - , 'disableInitiation':2 + { 'enable': 0 + , 'disable': 1 + , 'disableInitiation': 2 } class DeviceCommunicationControlRequest(ConfirmedRequestSequence): diff --git a/py25/bacpypes/app.py b/py25/bacpypes/app.py index fa5f9ca5..9be79fb7 100755 --- a/py25/bacpypes/app.py +++ b/py25/bacpypes/app.py @@ -26,7 +26,8 @@ # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ - ConfirmedServiceChoice, UnconfirmedServiceChoice + ConfirmedServiceChoice, UnconfirmedServiceChoice, \ + IAmRequest from .basetypes import ServicesSupported # basic services @@ -53,16 +54,16 @@ class DeviceInfo(DebugContents): 'maxSegmentsAccepted', ) - def __init__(self): + def __init__(self, device_identifier, address): # this information is from an IAmRequest - self.deviceIdentifier = None # device identifier - self.address = None # LocalStation or RemoteStation + self.deviceIdentifier = device_identifier + self.address = address + self.maxApduLengthAccepted = 1024 # maximum APDU device will accept self.segmentationSupported = 'noSegmentation' # normally no segmentation + self.maxSegmentsAccepted = None # None iff no segmentation self.vendorID = None # vendor identifier - - self.maxNpduLength = 1497 # maximum we can send in transit - self.maxSegmentsAccepted = None # value for proposed/actual window size + self.maxNpduLength = None # maximum we can send in transit (see 19.4) bacpypes_debugging(DeviceInfo) @@ -72,127 +73,134 @@ def __init__(self): class DeviceInfoCache: - def __init__(self): + def __init__(self, device_info_class=DeviceInfo): if _debug: DeviceInfoCache._debug("__init__") + # a little error checking + if not issubclass(device_info_class, DeviceInfo): + raise ValueError("not a DeviceInfo subclass: %r" % (device_info_class,)) + # empty cache self.cache = {} + # class for new records + self.device_info_class = device_info_class + def has_device_info(self, key): """Return true iff cache has information about the device.""" if _debug: DeviceInfoCache._debug("has_device_info %r", key) return key in self.cache - def add_device_info(self, apdu): + def iam_device_info(self, apdu): """Create a device information record based on the contents of an IAmRequest and put it in the cache.""" - if _debug: DeviceInfoCache._debug("add_device_info %r", apdu) + if _debug: DeviceInfoCache._debug("iam_device_info %r", apdu) - # get the existing cache record by identifier - info = self.get_device_info(apdu.iAmDeviceIdentifier[1]) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # make sure the apdu is an I-Am + if not isinstance(apdu, IAmRequest): + raise ValueError("not an IAmRequest: %r" % (apdu,)) - # update existing record - if info: - if (info.address == apdu.pduSource): - return + # get the device instance + device_instance = apdu.iAmDeviceIdentifier[1] - info.address = apdu.pduSource - else: - # get the existing record by address (creates a new record) - info = self.get_device_info(apdu.pduSource) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # get the existing cache record if it exists + device_info = self.cache.get(device_instance, None) - info.deviceIdentifier = apdu.iAmDeviceIdentifier[1] + # maybe there is a record for this address + if not device_info: + device_info = self.cache.get(apdu.pduSource, None) - # update the rest of the values - info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted - info.segmentationSupported = apdu.segmentationSupported - info.vendorID = apdu.vendorID + # make a new one using the class provided + if not device_info: + device_info = self.device_info_class(device_instance, apdu.pduSource) - # say this is an updated record - self.update_device_info(info) + # jam in the correct values + device_info.deviceIdentifier = device_instance + device_info.address = apdu.pduSource + device_info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted + device_info.segmentationSupported = apdu.segmentationSupported + device_info.vendorID = apdu.vendorID + + # tell the cache this is an updated record + self.update_device_info(device_info) def get_device_info(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" if _debug: DeviceInfoCache._debug("get_device_info %r", key) - if isinstance(key, int): - current_info = self.cache.get(key, None) + # get the info if it's there + device_info = self.cache.get(key, None) + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - elif not isinstance(key, Address): - raise TypeError("key must be integer or an address") + return device_info - elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): - raise TypeError("address must be a local or remote station") - - else: - current_info = self.cache.get(key, None) - if not current_info: - current_info = DeviceInfo() - current_info.address = key - current_info._cache_keys = (None, key) - current_info._ref_count = 1 - - self.cache[key] = current_info - else: - if _debug: DeviceInfoCache._debug(" - reference bump") - current_info._ref_count += 1 - - if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) - - return current_info - - def update_device_info(self, info): + def update_device_info(self, device_info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" - if _debug: DeviceInfoCache._debug("update_device_info %r", info) + if _debug: DeviceInfoCache._debug("update_device_info %r", device_info) - cache_id, cache_address = info._cache_keys + # give this a reference count if it doesn't have one + if not hasattr(device_info, '_ref_count'): + device_info._ref_count = 0 - if (cache_id is not None) and (info.deviceIdentifier != cache_id): + # get the current keys + cache_id, cache_address = getattr(device_info, '_cache_keys', (None, None)) + + if (cache_id is not None) and (device_info.deviceIdentifier != cache_id): if _debug: DeviceInfoCache._debug(" - device identifier updated") # remove the old reference, add the new one del self.cache[cache_id] - self.cache[info.deviceIdentifier] = info - - cache_id = info.deviceIdentifier + self.cache[device_info.deviceIdentifier] = device_info - if (cache_address is not None) and (info.address != cache_address): + if (cache_address is not None) and (device_info.address != cache_address): if _debug: DeviceInfoCache._debug(" - device address updated") # remove the old reference, add the new one del self.cache[cache_address] - self.cache[info.address] = info - - cache_address = info.address + self.cache[device_info.address] = device_info # update the keys - info._cache_keys = (cache_id, cache_address) + device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) + + def acquire(self, key): + """Return the known information about the device and mark the record + as being used by a segmenation state machine.""" + if _debug: DeviceInfoCache._debug("acquire %r", key) + + if isinstance(key, int): + device_info = self.cache.get(key, None) + + elif not isinstance(key, Address): + raise TypeError("key must be integer or an address") + + elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): + raise TypeError("address must be a local or remote station") - def release_device_info(self, info): + else: + device_info = self.cache.get(key, None) + + if device_info: + if _debug: DeviceInfoCache._debug(" - reference bump") + device_info._ref_count += 1 + + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) + + return device_info + + def release(self, device_info): """This function is called by the segmentation state machine when it has finished with the device information.""" - if _debug: DeviceInfoCache._debug("release_device_info %r", info) + if _debug: DeviceInfoCache._debug("release %r", device_info) # this information record might be used by more than one SSM - if info._ref_count > 1: - if _debug: DeviceInfoCache._debug(" - multiple references") - info._ref_count -= 1 - return + if device_info._ref_count == 0: + raise RuntimeError("reference count") - cache_id, cache_address = info._cache_keys - if cache_id is not None: - del self.cache[cache_id] - if cache_address is not None: - del self.cache[cache_address] - if _debug: DeviceInfoCache._debug(" - released") + # decrement the reference count + device_info._ref_count -= 1 bacpypes_debugging(DeviceInfoCache) @@ -619,3 +627,4 @@ def __init__(self, localAddress, eID=None): self.nsap.bind(self.bip) bacpypes_debugging(BIPNetworkApplication) + diff --git a/py25/bacpypes/appservice.py b/py25/bacpypes/appservice.py index 942424e4..b30a7e8a 100755 --- a/py25/bacpypes/appservice.py +++ b/py25/bacpypes/appservice.py @@ -12,12 +12,14 @@ from .task import OneShotTask from .pdu import Address -from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \ +from .apdu import encode_max_segments_accepted, decode_max_segments_accepted, \ + encode_max_apdu_length_accepted, decode_max_apdu_length_accepted, \ + AbortPDU, AbortReason, ComplexAckPDU, \ ConfirmedRequestPDU, Error, ErrorPDU, RejectPDU, SegmentAckPDU, \ SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \ unconfirmed_request_types, confirmed_request_types, complex_ack_types, \ error_types -from .errors import RejectException, AbortException +from .errors import RejectException, AbortException, UnrecognizedService # some debugging _debug = 0 @@ -44,19 +46,23 @@ class SSM(OneShotTask, DebugContents): , 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED' ] - _debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID' + _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' ) - def __init__(self, sap, remoteDevice): + def __init__(self, sap, pdu_address): """Common parts for client and server segmentation.""" - if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice) + if _debug: SSM._debug("__init__ %r %r", sap, pdu_address) OneShotTask.__init__(self) self.ssmSAP = sap # service access point - self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance + + # save the address and get the device information + self.pdu_address = pdu_address + self.device_info = sap.deviceInfoCache.get_device_info(pdu_address) + self.invokeID = None # invoke ID self.state = IDLE # initial state @@ -70,11 +76,17 @@ def __init__(self, sap, remoteDevice): self.lastSequenceNumber = None self.initialSequenceNumber = None self.actualWindowSize = None - self.proposedWindowSize = None - # the maximum number of segments starts out being what's in the SAP - # which is the defaults or values from the local device. - self.maxSegmentsAccepted = self.ssmSAP.maxSegmentsAccepted + # local device object provides these or SAP provides defaults, make + # copies here so they are consistent throughout the transaction but + # they could change from one transaction to the next + self.numberOfApduRetries = getattr(sap.localDevice, 'numberOfApduRetries', sap.numberOfApduRetries) + self.apduTimeout = getattr(sap.localDevice, 'apduTimeout', sap.apduTimeout) + + self.segmentationSupported = getattr(sap.localDevice, 'segmentationSupported', sap.segmentationSupported) + self.segmentTimeout = getattr(sap.localDevice, 'segmentTimeout', sap.segmentTimeout) + self.maxSegmentsAccepted = getattr(sap.localDevice, 'maxSegmentsAccepted', sap.maxSegmentsAccepted) + self.maxApduLengthAccepted = getattr(sap.localDevice, 'maxApduLengthAccepted', sap.maxApduLengthAccepted) def start_timer(self, msecs): if _debug: SSM._debug("start_timer %r", msecs) @@ -145,19 +157,19 @@ def get_segment(self, indx): # check for invalid segment number if indx >= self.segmentCount: - raise RuntimeError("invalid segment number %r, APDU has %r segments" % (indx, self.segmentCount)) + raise RuntimeError("invalid segment number {0}, APDU has {1} segments".format(indx, self.segmentCount)) if self.segmentAPDU.apduType == ConfirmedRequestPDU.pduType: if _debug: SSM._debug(" - confirmed request context") segAPDU = ConfirmedRequestPDU(self.segmentAPDU.apduService) - segAPDU.apduMaxSegs = self.maxSegmentsAccepted - segAPDU.apduMaxResp = self.ssmSAP.maxApduLengthAccepted - segAPDU.apduInvokeID = self.invokeID; + segAPDU.apduMaxSegs = encode_max_segments_accepted(self.maxSegmentsAccepted) + segAPDU.apduMaxResp = encode_max_apdu_length_accepted(self.maxApduLengthAccepted) + segAPDU.apduInvokeID = self.invokeID # segmented response accepted? - segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth') + segAPDU.apduSA = self.segmentationSupported in ('segmentedReceive', 'segmentedBoth') if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA) elif self.segmentAPDU.apduType == ComplexAckPDU.pduType: @@ -171,14 +183,21 @@ def get_segment(self, indx): segAPDU.pduUserData = self.segmentAPDU.pduUserData # make sure the destination is set - segAPDU.pduDestination = self.remoteDevice.address + segAPDU.pduDestination = self.pdu_address # segmented message? if (self.segmentCount != 1): segAPDU.apduSeg = True segAPDU.apduMor = (indx < (self.segmentCount - 1)) # more follows segAPDU.apduSeq = indx % 256 # sequence number - segAPDU.apduWin = self.proposedWindowSize # window size + + # first segment sends proposed window size, rest get actual + if indx == 0: + if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) + segAPDU.apduWin = self.proposedWindowSize + else: + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) + segAPDU.apduWin = self.actualWindowSize else: segAPDU.apduSeg = False segAPDU.apduMor = False @@ -210,10 +229,11 @@ def in_window(self, seqA, seqB): return rslt - def FillWindow(self, seqNum): + def fill_window(self, seqNum): """This function sends all of the packets necessary to fill out the segmentation window.""" - if _debug: SSM._debug("FillWindow %r", seqNum) + if _debug: SSM._debug("fill_window %r", seqNum) + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) for ix in range(self.actualWindowSize): apdu = self.get_segment(seqNum + ix) @@ -234,13 +254,18 @@ def FillWindow(self, seqNum): class ClientSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ClientSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) # initialize the retry count self.retryCount = 0 + # acquire the device info + if self.device_info: + if _debug: ClientSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) + def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" if _debug: ClientSSM._debug("set_state %r (%s) timer=%r", newState, SSM.transactionLabels[newState], timer) @@ -253,8 +278,10 @@ def set_state(self, newState, timer=0): if _debug: ClientSSM._debug(" - remove from active transactions") self.ssmSAP.clientTransactions.remove(self) - if _debug: ClientSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by client transaction functions when it wants @@ -263,7 +290,7 @@ def request(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -280,26 +307,27 @@ def indication(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit, - # the maximum conveyable by the internetwork to the remote device, and - # the maximum APDU size accepted by the remote device. - self.segmentSize = min( - self.ssmSAP.maxApduLengthAccepted, - self.remoteDevice.maxNpduLength, - self.remoteDevice.maxApduLengthAccepted, - ) - if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) + # if the max apdu length of the server isn't known, assume that it + # is the same size as our own and will be the segment size + if (not self.device_info) or (self.device_info.maxApduLengthAccepted is None): + self.segmentSize = self.maxApduLengthAccepted - # the maximum number of segments acceptable in the reply - if apdu.apduMaxSegs is not None: - # this request overrides the default - self.maxSegmentsAccepted = apdu.apduMaxSegs + # if the max npdu length of the server isn't known, assume that it + # is the same as the max apdu length accepted + elif self.device_info.maxNpduLength is None: + self.segmentSize = self.device_info.maxApduLengthAccepted + + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the server and the largest it can accept + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.device_info.maxApduLengthAccepted) + if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) # save the invoke ID self.invokeID = apdu.apduInvokeID if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -312,34 +340,49 @@ def indication(self, apdu): # make sure we support segmented transmit if we need to if self.segmentCount > 1: - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't send segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): - if _debug: ClientSSM._debug(" - remote device can't receive segmented requests") + + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for segmentation support") + + elif self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if _debug: ClientSSM._debug(" - server can't receive segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our request + # that the server said it was willing to accept + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for maximum number of segments") + + elif not self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server doesn't say maximum number of segments") + + elif self.segmentCount > self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # send out the first segment (or the whole thing) if self.segmentCount == 1: - # SendConfirmedUnsegmented + # unsegmented self.sentAllSegments = True self.retryCount = 0 - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) else: - # SendConfirmedSegmented + # segmented self.sentAllSegments = False self.retryCount = 0 self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.actualWindowSize = None # segment ack will set value + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # deliver to the device self.request(self.get_segment(0)) @@ -350,7 +393,7 @@ def response(self, apdu): if _debug: ClientSSM._debug("response %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it to the application @@ -407,29 +450,31 @@ def segmented_request(self, apdu): and receives an apdu.""" if _debug: ClientSSM._debug("segmented_request %r", apdu) - # client is ready for the next segment + # server is ready for the next segment if apdu.apduType == SegmentAckPDU.pduType: if _debug: ClientSSM._debug(" - segment ack") + # actual window size is provided by server + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ClientSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: if _debug: ClientSSM._debug(" - all done sending request") - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) # more segments to send else: if _debug: ClientSSM._debug(" - more segments to send") self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 - self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # simple ack elif (apdu.apduType == SimpleAckPDU.pduType): @@ -452,6 +497,7 @@ def segmented_request(self, apdu): self.response(abort) # send it to the application elif not apdu.apduSeg: + # ack is not segmented self.set_state(COMPLETED) self.response(apdu) @@ -459,10 +505,11 @@ def segmented_request(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # minimum of what the server is proposing and this client proposes + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # some kind of problem elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType): @@ -479,12 +526,12 @@ def segmented_request_timeout(self): if _debug: ClientSSM._debug("segmented_request_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: if _debug: ClientSSM._debug(" - retry segmented request") self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: if _debug: ClientSSM._debug(" - abort, no response from the device") @@ -516,7 +563,7 @@ def await_confirmation(self, apdu): self.set_state(COMPLETED) self.response(apdu) - elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + elif self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't receive segmented messages") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) @@ -527,10 +574,10 @@ def await_confirmation(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + self.actualWindowSize = apdu.apduWin self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # send back a segment ack segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) @@ -546,7 +593,7 @@ def await_confirmation(self, apdu): elif (apdu.apduType == SegmentAckPDU.pduType): if _debug: ClientSSM._debug(" - segment ack(!?)") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) else: raise RuntimeError("invalid APDU (3)") @@ -554,9 +601,9 @@ def await_confirmation(self, apdu): def await_confirmation_timeout(self): if _debug: ClientSSM._debug("await_confirmation_timeout") - self.retryCount += 1 - if self.retryCount < self.ssmSAP.retryCount: - if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.ssmSAP.retryCount) + if self.retryCount < self.numberOfApduRetries: + if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.numberOfApduRetries) + self.retryCount += 1 # save the retry count, indication acts like the request is coming # from the application so the retryCount gets re-initialized. @@ -594,8 +641,8 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - segment %s received out of order, should be %s", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) return @@ -610,7 +657,7 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - no more follows") # send a final ack - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) self.set_state(COMPLETED) @@ -620,15 +667,15 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) else: # wait for more segments if _debug: ClientSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_confirmation_timeout(self): if _debug: ClientSSM._debug("segmented_confirmation_timeout") @@ -644,9 +691,14 @@ def segmented_confirmation_timeout(self): class ServerSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ServerSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) + + # acquire the device info + if self.device_info: + if _debug: ServerSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" @@ -660,8 +712,10 @@ def set_state(self, newState, timer=0): if _debug: ServerSSM._debug(" - remove from active transactions") self.ssmSAP.serverTransactions.remove(self) - if _debug: ServerSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by transaction functions to send @@ -669,7 +723,7 @@ def request(self, apdu): if _debug: ServerSSM._debug("request %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it via the device @@ -698,7 +752,7 @@ def response(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -740,14 +794,15 @@ def confirmation(self, apdu): # save the response and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit - # (assumed to have no local buffer limitations), the maximum conveyable - # by the internetwork to the remote device, and the maximum APDU size - # accepted by the remote device. - self.segmentSize = min(self.remoteDevice.maxNpduLength, self.remoteDevice.maxApduLengthAccepted) + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the client and the largest it can accept + if (not self.device_info) or (self.device_info.maxNpduLength is None): + self.segmentSize = self.maxApduLengthAccepted + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.maxApduLengthAccepted) if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -763,26 +818,31 @@ def confirmation(self, apdu): if _debug: ServerSSM._debug(" - segmentation required, %d segments", self.segmentCount) # make sure we support segmented transmit - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ServerSSM._debug(" - server can't send segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # make sure client supports segmented receive - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ServerSSM._debug(" - client can't receive segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our response + # that the device said it was willing to accept in the request + if self.segmentCount > self.maxSegmentsAccepted: + if _debug: ServerSSM._debug(" - client can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # initialize the state self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 + self.actualWindowSize = None # send out the first segment (or the whole thing) if self.segmentCount == 1: @@ -790,7 +850,7 @@ def confirmation(self, apdu): self.set_state(COMPLETED) else: self.response(self.get_segment(0)) - self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_RESPONSE, self.segmentTimeout) else: raise RuntimeError("invalid APDU (4)") @@ -838,38 +898,48 @@ def idle(self, apdu): self.invokeID = apdu.apduInvokeID if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID) - # make sure the device information is synced with the request if apdu.apduSA: - if self.remoteDevice.segmentationSupported == 'noSegmentation': + if not self.device_info: + if _debug: ServerSSM._debug(" - no client device info") + + elif self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") - self.remoteDevice.segmentationSupported = 'segmentedReceive' + self.device_info.segmentationSupported = 'segmentedReceive' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedTransmit': + elif self.device_info.segmentationSupported == 'segmentedTransmit': if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive") - self.remoteDevice.segmentationSupported = 'segmentedBoth' + self.device_info.segmentationSupported = 'segmentedBoth' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedReceive': + elif self.device_info.segmentationSupported == 'segmentedReceive': pass - elif self.remoteDevice.segmentationSupported == 'segmentedBoth': + elif self.device_info.segmentationSupported == 'segmentedBoth': pass else: raise RuntimeError("invalid segmentation supported in device info") - if apdu.apduMaxSegs != self.remoteDevice.maxSegmentsAccepted: - if _debug: ServerSSM._debug(" - update maximum segments accepted?") - if apdu.apduMaxResp != self.remoteDevice.maxApduLengthAccepted: - if _debug: ServerSSM._debug(" - update maximum max APDU length accepted?") + # decode the maximum that the client can receive in one APDU, and if + # there is a value in the device information then use that one because + # it came from reading device object property value or from an I-Am + # message that was received + self.maxApduLengthAccepted = decode_max_apdu_length_accepted(apdu.apduMaxResp) + if self.device_info and self.device_info.maxApduLengthAccepted is not None: + if self.device_info.maxApduLengthAccepted < self.maxApduLengthAccepted: + if _debug: ServerSSM._debug(" - apduMaxResp encoding error") + else: + self.maxApduLengthAccepted = self.device_info.maxApduLengthAccepted + if _debug: ServerSSM._debug(" - maxApduLengthAccepted: %r", self.maxApduLengthAccepted) - # save the number of segments the client is willing to accept in the ack - self.maxSegmentsAccepted = apdu.apduMaxSegs + # save the number of segments the client is willing to accept in the ack, + # if this is None then the value is unknown or more than 64 + self.maxSegmentsAccepted = decode_max_segments_accepted(apdu.apduMaxSegs) # unsegmented request if not apdu.apduSeg: @@ -878,7 +948,7 @@ def idle(self, apdu): return # make sure we support segmented requests - if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return @@ -886,17 +956,18 @@ def idle(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the window size is the minimum of what I'm willing to receive and - # what the device has said it would like to send - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # the window size is the minimum of what I would propose and what the + # device has proposed + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + if _debug: ServerSSM._debug(" - actualWindowSize? min(%r, %r) -> %r", apdu.apduWin, self.proposedWindowSize, self.actualWindowSize) # initialize the state self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) if _debug: ServerSSM._debug(" - segAck: %r", segack) self.response(segack) @@ -929,10 +1000,10 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - segment %d received out of order, should be %d", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) return @@ -948,7 +1019,7 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - no more follows") # send back a final segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.response(segack) # forward the whole thing to the application @@ -959,17 +1030,17 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) else: # wait for more segments if _debug: ServerSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_request_timeout(self): if _debug: ServerSSM._debug("segmented_request_timeout") @@ -1009,10 +1080,13 @@ def segmented_response(self, apdu): if (apdu.apduType == SegmentAckPDU.pduType): if _debug: ServerSSM._debug(" - segment ack") + # actual window size is provided by client + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ServerSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: @@ -1025,8 +1099,8 @@ def segmented_response(self, apdu): self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # some kind of problem elif (apdu.apduType == AbortPDU.pduType): @@ -1040,10 +1114,10 @@ def segmented_response_timeout(self): if _debug: ServerSSM._debug("segmented_response_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: # give up self.set_state(ABORTED) @@ -1064,6 +1138,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): ServiceAccessPoint.__init__(self, sap) # save a reference to the device information cache + self.localDevice = localDevice self.deviceInfoCache = deviceInfoCache # client settings @@ -1074,27 +1149,19 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): self.serverTransactions = [] # confirmed request defaults - self.retryCount = 3 - self.retryTimeout = 3000 + self.numberOfApduRetries = 3 + self.apduTimeout = 3000 self.maxApduLengthAccepted = 1024 # segmentation defaults self.segmentationSupported = 'noSegmentation' self.segmentTimeout = 1500 - self.maxSegmentsAccepted = 8 + self.maxSegmentsAccepted = 2 + self.proposedWindowSize = 2 # device communication control self.dccEnableDisable = 'enable' - # local device object provides these - if localDevice: - self.retryCount = localDevice.numberOfApduRetries - self.retryTimeout = localDevice.apduTimeout - self.segmentationSupported = localDevice.segmentationSupported - self.segmentTimeout = localDevice.apduSegmentTimeout - self.maxSegmentsAccepted = localDevice.maxSegmentsAccepted - self.maxApduLengthAccepted = localDevice.maxApduLengthAccepted - # how long the state machine is willing to wait for the application # layer to form a response and send it self.applicationTimeout = 3000 @@ -1113,7 +1180,7 @@ def get_next_invoke_id(self, addr): raise RuntimeError("no available invoke ID") for tr in self.clientTransactions: - if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address): + if (invokeID == tr.invokeID) and (addr == tr.pdu_address): break else: break @@ -1154,14 +1221,11 @@ def confirmation(self, pdu): if isinstance(apdu, ConfirmedRequestPDU): # find duplicates of this request for tr in self.serverTransactions: - if (apdu.pduSource == tr.remoteDevice.address) and (apdu.apduInvokeID == tr.invokeID): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource) - # build a server transaction - tr = ServerSSM(self, remoteDevice) + tr = ServerSSM(self, apdu.pduSource) # add it to our transactions to track it self.serverTransactions.append(tr) @@ -1180,7 +1244,7 @@ def confirmation(self, pdu): # find the client transaction this is acking for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1192,7 +1256,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1201,7 +1265,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1213,7 +1277,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1222,7 +1286,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1266,19 +1330,15 @@ def sap_indication(self, apdu): else: # verify the invoke ID isn't already being used for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): raise RuntimeError("invoke ID in use") # warning for bogus requests if (apdu.pduDestination.addrType != Address.localStationAddr) and (apdu.pduDestination.addrType != Address.remoteStationAddr): StateMachineAccessPoint._warning("%s is not a local or remote station", apdu.pduDestination) - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduDestination) - if _debug: StateMachineAccessPoint._debug(" - remoteDevice: %r", remoteDevice) - # create a client transaction state machine - tr = ClientSSM(self, remoteDevice) + tr = ClientSSM(self, apdu.pduDestination) if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr) # add it to our transactions to track it @@ -1302,7 +1362,7 @@ def sap_confirmation(self, apdu): or isinstance(apdu, AbortPDU): # find the appropriate server transaction for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): break else: return @@ -1330,23 +1390,26 @@ def indication(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu) if isinstance(apdu, ConfirmedRequestPDU): + # assume no errors found + error_found = None + + # look up the class associated with the service atype = confirmed_request_types.get(apdu.apduService) if not atype: if _debug: ApplicationServiceAccessPoint._debug(" - no confirmed request decoder") - return - - # assume no errors found - error_found = None + error_found = UnrecognizedService() - try: - xpdu = atype() - xpdu.decode(apdu) - except RejectException, err: - ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) - error_found = err - except AbortException, err: - ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) - error_found = err + # no error so far, keep going + if not error_found: + try: + xpdu = atype() + xpdu.decode(apdu) + except RejectException, err: + ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) + error_found = err + except AbortException, err: + ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) + error_found = err # no error so far, keep going if not error_found: diff --git a/py25/bacpypes/bvllservice.py b/py25/bacpypes/bvllservice.py index 2a46cf33..90208f15 100755 --- a/py25/bacpypes/bvllservice.py +++ b/py25/bacpypes/bvllservice.py @@ -546,7 +546,7 @@ def confirmation(self, pdu): return - elif isinstance(pdu, OriginalUnicastNPDU): + if isinstance(pdu, OriginalUnicastNPDU): # build a vanilla PDU xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) @@ -554,26 +554,31 @@ def confirmation(self, pdu): self.response(xpdu) return - # check the BBMD registration status, we may not be registered - if self.registrationStatus != 0: - if _debug: BIPForeign._debug(" - packet dropped, unregistered") - return - - if isinstance(pdu, ReadBroadcastDistributionTableAck): - # send this to the service access point - self.sap_response(pdu) + elif isinstance(pdu, ForwardedNPDU): + # check the BBMD registration status, we may not be registered + if self.registrationStatus != 0: + if _debug: BIPForeign._debug(" - packet dropped, unregistered") + return - elif isinstance(pdu, ReadForeignDeviceTableAck): - # send this to the service access point - self.sap_response(pdu) + # make sure the forwarded PDU from the bbmd + if pdu.pduSource != self.bbmdAddress: + if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") + return - elif isinstance(pdu, ForwardedNPDU): # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) # send it upstream self.response(xpdu) + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response xpdu = Result(code=0x0010, user_data=pdu.pduUserData) @@ -622,6 +627,9 @@ def confirmation(self, pdu): # send it downstream self.request(xpdu) + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPForeign._debug(" - packet dropped") + else: BIPForeign._warning("invalid pdu type: %s", type(pdu)) @@ -697,7 +705,7 @@ def indication(self, pdu): # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduDestination - if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) + if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -720,13 +728,13 @@ def indication(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -742,8 +750,9 @@ def confirmation(self, pdu): elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response - xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu = Result(code=0x0010, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduSource + if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -762,27 +771,38 @@ def confirmation(self, pdu): self.sap_response(pdu) elif isinstance(pdu, ForwardedNPDU): - # build a PDU with the source from the real source - xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) - # look for self as first entry in the BDT - if self.bbmdBDT and (self.bbmdBDT[0] == self.bbmdAddress): - xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") - self.request(xpdu) + # if this was unicast to us, do next hop + if pdu.pduDestination.addrType == Address.localStationAddr: + if _debug: BIPBBMD._debug(" - unicast message") + + # if this BBMD is listed in its BDT, send a local broadcast + if self.bbmdAddress in self.bbmdBDT: + xpdu.pduDestination = LocalBroadcast() + if _debug: BIPBBMD._debug(" - local broadcast") + self.request(xpdu) + + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + if _debug: BIPBBMD._debug(" - directed broadcast message") + + else: + BIPBBMD._warning("invalid destination address: %r", pdu.pduDestination) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, RegisterForeignDevice): @@ -821,12 +841,13 @@ def confirmation(self, pdu): self.request(xpdu) elif isinstance(pdu, DistributeBroadcastToNetwork): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -836,35 +857,37 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte == self.bbmdAddress: xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") + if _debug: BIPBBMD._debug(" - local broadcast") self.request(xpdu) else: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the other registered foreign devices for fdte in self.bbmdFDT: if fdte.fdAddress != pdu.pduSource: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) elif isinstance(pdu, OriginalBroadcastNPDU): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # make a forwarded PDU xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -874,13 +897,13 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -930,7 +953,7 @@ def delete_foreign_device_table_entry(self, addr): del self.bbmdFDT[i] break else: - stat = 99 ### entry not found + stat = 0x0050 ### entry not found # return status return stat @@ -957,10 +980,6 @@ def add_peer(self, addr): else: raise TypeError("addr must be a string or an Address") - # if it's this BBMD, make it the first one - if self.bbmdBDT and (addr == self.bbmdAddress): - raise RuntimeError("add self to BDT as first address") - # see if it's already there for bdte in self.bbmdBDT: if addr == bdte: diff --git a/py25/bacpypes/constructeddata.py b/py25/bacpypes/constructeddata.py index 1ececeeb..cdc096ec 100755 --- a/py25/bacpypes/constructeddata.py +++ b/py25/bacpypes/constructeddata.py @@ -144,6 +144,7 @@ def decode(self, taglist): for element in self.sequenceElements: tag = taglist.Peek() + if _debug: Sequence._debug(" - element, tag: %r, %r", element, tag) # no more elements if tag is None: @@ -190,7 +191,29 @@ def decode(self, taglist): if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) - # check for an atomic element + # check for an any atomic element + elif issubclass(element.klass, AnyAtomic): + # convert it to application encoding + if element.context is not None: + raise InvalidTag("%s any atomic with context tag %d" % (element.name, element.context)) + + if tag.tagClass != Tag.applicationTagClass: + if not element.optional: + raise InvalidParameterDatatype("%s expected any atomic application tag" % (element.name,)) + else: + setattr(self, element.name, None) + continue + + # consume the tag + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = element.klass(tag) + + # now save the value + setattr(self, element.name, helper.value) + + # check for specific kind of atomic element, or the context says what kind elif issubclass(element.klass, Atomic): # convert it to application encoding if element.context is not None: @@ -409,6 +432,9 @@ def __len__(self): def __getitem__(self, item): return self.value[item] + def __iter__(self): + return iter(self.value) + def encode(self, taglist): if _debug: _SequenceOf._debug("(%r)encode %r", self.__class__.__name__, taglist) for value in self.value: @@ -753,6 +779,9 @@ def __delitem__(self, item): del self.value[item] self.value[0] -= 1 + def __iter__(self): + return iter(self.value[1:]) + def index(self, value): # only search through values for i in range(1, self.value[0] + 1): @@ -1349,8 +1378,13 @@ def decode(self, tag): # get the data self.value = tag.app_to_object() + @classmethod + def is_valid(cls, arg): + """Return True if arg is valid value for the class.""" + return isinstance(arg, Atomic) and not isinstance(arg, AnyAtomic) + def __str__(self): - return "AnyAtomic(%s)" % (str(self.value), ) + return "%s(%s)" % (self.__class__.__name__, str(self.value)) def __repr__(self): desc = self.__module__ + '.' + self.__class__.__name__ diff --git a/py25/bacpypes/local/__init__.py b/py25/bacpypes/local/__init__.py new file mode 100644 index 00000000..277c3c76 --- /dev/null +++ b/py25/bacpypes/local/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Local Object Subpackage +""" + +from . import object +from . import device +from . import file +from . import schedule + diff --git a/py25/bacpypes/local/device.py b/py25/bacpypes/local/device.py new file mode 100644 index 00000000..bab2bdd3 --- /dev/null +++ b/py25/bacpypes/local/device.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ + OctetString, CharacterString, BitString, Enumerated, Date, Time, \ + ObjectIdentifier +from ..constructeddata import ArrayOf +from ..basetypes import ServicesSupported + +from ..errors import ExecutionError +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentLocalDate +# + +class CurrentLocalDate(Property): + + def __init__(self): + Property.__init__(self, 'localDate', Date, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentLocalTime +# + +class CurrentLocalTime(Property): + + def __init__(self): + Property.__init__(self, 'localTime', Time, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentProtocolServicesSupported +# + +class CurrentProtocolServicesSupported(Property): + + def __init__(self): + if _debug: CurrentProtocolServicesSupported._debug("__init__") + Property.__init__(self, 'protocolServicesSupported', ServicesSupported, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentProtocolServicesSupported._debug("ReadProperty %r %r", obj, arrayIndex) + + # not an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return what the application says + return obj._app.get_services_supported() + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +bacpypes_debugging(CurrentProtocolServicesSupported) + +# +# LocalDeviceObject +# + +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): + + properties = [ + CurrentLocalTime(), + CurrentLocalDate(), + CurrentProtocolServicesSupported(), + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # start with an empty dictionary of device object properties + init_args = {} + ini_arg = kwargs.get('ini', None) + if _debug: LocalDeviceObject._debug(" - ini_arg: %r", dir(ini_arg)) + + # check for registration as a keyword parameter or in the INI file + if self.__class__ not in registered_object_types.values(): + if _debug: LocalDeviceObject._debug(" - unregistered") + + vendor_identifier = kwargs.get('vendorIdentifier', None) + if _debug: LocalDeviceObject._debug(" - keyword vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + vendor_identifier = getattr(ini_arg, 'vendoridentifier', None) + if _debug: LocalDeviceObject._debug(" - INI vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + + register_object_type(self.__class__, vendor_id=vendor_identifier) + + # look for properties, fill in values from the keyword arguments or + # the INI parameter (converted to a proper value) if it was provided + for propid, prop in self._properties.items(): + # special processing for object identifier + if propid == 'objectIdentifier': + continue + + # use keyword argument if it was provided + if propid in kwargs: + prop_value = kwargs[propid] + else: + prop_value = getattr(ini_arg, propid.lower(), None) + if prop_value is None: + continue + + prop_datatype = prop.datatype + + if issubclass(prop_datatype, Null): + if prop_value != "Null": + raise ValueError("invalid null property value: %r" % (propid,)) + prop_value = None + + elif issubclass(prop_datatype, Boolean): + prop_value = prop_value.lower() + if prop_value not in ('true', 'false', 'set', 'reset'): + raise ValueError("invalid boolean property value: %r" % (propid,)) + prop_value = prop_value in ('true', 'set') + + elif issubclass(prop_datatype, (Unsigned, Integer)): + try: + prop_value = int(prop_value) + except ValueError: + raise ValueError("invalid unsigned or integer property value: %r" % (propid,)) + + elif issubclass(prop_datatype, (Real, Double)): + try: + prop_value = float(prop_value) + except ValueError: + raise ValueError("invalid real or double property value: %r" % (propid,)) + + elif issubclass(prop_datatype, OctetString): + try: + prop_value = xtob(prop_value) + except: + raise ValueError("invalid octet string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, CharacterString): + pass + + elif issubclass(prop_datatype, BitString): + try: + bstr, prop_value = prop_value, [] + for b in bstr: + if b not in ('0', '1'): + raise ValueError + prop_value.append(int(b)) + except: + raise ValueError("invalid bit string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, Enumerated): + pass + + else: + raise ValueError("cannot interpret %r INI paramter" % (propid,)) + if _debug: LocalDeviceObject._debug(" - property %r: %r", propid, prop_value) + + # at long last + init_args[propid] = prop_value + + # check for object identifier as a keyword parameter or in the INI file, + # and it might be just an int, so make it a tuple if necessary + if 'objectIdentifier' in kwargs: + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + elif hasattr(ini_arg, 'objectidentifier'): + object_identifier = ('device', int(getattr(ini_arg, 'objectidentifier'))) + else: + raise RuntimeError("objectIdentifier is required") + init_args['objectIdentifier'] = object_identifier + if _debug: LocalDeviceObject._debug(" - object identifier: %r", object_identifier) + + # fill in default property values not in init_args + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in init_args: + init_args[attr] = value + + # check for properties this class implements + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + if 'protocolServicesSupported' in kwargs: + raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + init_args['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + + # check for a minimum value + if init_args['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - init_args: %r", init_args) + + # proceed as usual + super(LocalDeviceObject, self).__init__(**init_args) + + # pass along special property values that are not BACnet properties + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) + +bacpypes_debugging(LocalDeviceObject) + diff --git a/py25/bacpypes/local/file.py b/py25/bacpypes/local/file.py new file mode 100644 index 00000000..3792526a --- /dev/null +++ b/py25/bacpypes/local/file.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +bacpypes_debugging(LocalRecordAccessFileObject) + +# +# Local Stream Access File Object Type +# + +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + +bacpypes_debugging(LocalStreamAccessFileObject) + diff --git a/py25/bacpypes/local/object.py b/py25/bacpypes/local/object.py new file mode 100644 index 00000000..5b5be5d3 --- /dev/null +++ b/py25/bacpypes/local/object.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..basetypes import PropertyIdentifier +from ..constructeddata import ArrayOf + +from ..errors import ExecutionError +from ..object import Property, Object + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +bacpypes_debugging(CurrentPropertyList) + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + diff --git a/py25/bacpypes/local/schedule.py b/py25/bacpypes/local/schedule.py new file mode 100644 index 00000000..a6f368be --- /dev/null +++ b/py25/bacpypes/local/schedule.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..core import deferred +from ..task import OneShotTask + +from ..primitivedata import Atomic, Null, Unsigned, Date, Time +from ..constructeddata import Array +from ..object import get_datatype, ScheduleObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# match_date +# + +def match_date(date, date_pattern): + """ + Match a specific date, a four-tuple with no special values, with a date + pattern, four-tuple possibly having special values. + """ + # unpack the date and pattern + year, month, day, day_of_week = date + year_p, month_p, day_p, day_of_week_p = date_pattern + + # check the year + if year_p == 255: + # any year + pass + elif year != year_p: + # specific year + return False + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the day + if day_p == 255: + # any day + pass + elif day_p == 32: + # last day of the month + last_day = calendar.monthrange(year + 1900, month)[1] + if day != last_day: + return False + elif day_p == 33: + # odd days of the month + if (day % 2) == 0: + return False + elif day_p == 34: + # even days of the month + if (day % 2) == 1: + return False + elif day != day_p: + # specific day + return False + + # check the day of week + if day_of_week_p == 255: + # any day of the week + pass + elif day_of_week != day_of_week_p: + # specific day of the week + return False + + # all tests pass + return True + +# +# match_date_range +# + +def match_date_range(date, date_range): + """ + Match a specific date, a four-tuple with no special values, with a DateRange + object which as a start date and end date. + """ + return (date[:3] >= date_range.startDate[:3]) \ + and (date[:3] <= date_range.endDate[:3]) + +# +# match_weeknday +# + +def match_weeknday(date, weeknday): + """ + Match a specific date, a four-tuple with no special values, with a + BACnetWeekNDay, an octet string with three (unsigned) octets. + """ + # unpack the date + year, month, day, day_of_week = date + last_day = calendar.monthrange(year + 1900, month)[1] + + # unpack the date pattern octet string + weeknday_unpacked = [ord(c) for c in weeknday] + month_p, week_of_month_p, day_of_week_p = weeknday_unpacked + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the week of the month + if week_of_month_p == 255: + # any week + pass + elif week_of_month_p == 1: + # days numbered 1-7 + if (day > 7): + return False + elif week_of_month_p == 2: + # days numbered 8-14 + if (day < 8) or (day > 14): + return False + elif week_of_month_p == 3: + # days numbered 15-21 + if (day < 15) or (day > 21): + return False + elif week_of_month_p == 4: + # days numbered 22-28 + if (day < 22) or (day > 28): + return False + elif week_of_month_p == 5: + # days numbered 29-31 + if (day < 29) or (day > 31): + return False + elif week_of_month_p == 6: + # last 7 days of this month + if (day < last_day - 6): + return False + elif week_of_month_p == 7: + # any of the 7 days prior to the last 7 days of this month + if (day < last_day - 13) or (day > last_day - 7): + return False + elif week_of_month_p == 8: + # any of the 7 days prior to the last 14 days of this month + if (day < last_day - 20) or (day > last_day - 14): + return False + elif week_of_month_p == 9: + # any of the 7 days prior to the last 21 days of this month + if (day < last_day - 27) or (day > last_day - 21): + return False + + # check the day + if day_of_week_p == 255: + # any day + pass + elif day_of_week != day_of_week_p: + # specific day + return False + + # all tests pass + return True + +# +# date_in_calendar_entry +# + +def date_in_calendar_entry(date, calendar_entry): + if _debug: date_in_calendar_entry._debug("date_in_calendar_entry %r %r", date, calendar_entry) + + match = False + if calendar_entry.date: + match = match_date(date, calendar_entry.date) + elif calendar_entry.dateRange: + match = match_date_range(date, calendar_entry.dateRange) + elif calendar_entry.weekNDay: + match = match_weeknday(date, calendar_entry.weekNDay) + else: + raise RuntimeError("") + if _debug: date_in_calendar_entry._debug(" - match: %r", match) + + return match + +bacpypes_debugging(date_in_calendar_entry) + +# +# datetime_to_time +# + +def datetime_to_time(date, time): + """Take the date and time 4-tuples and return the time in seconds since + the epoch as a floating point number.""" + if (255 in date) or (255 in time): + raise RuntimeError("specific date and time required") + + time_tuple = ( + date[0]+1900, date[1], date[2], + time[0], time[1], time[2], + 0, 0, -1, + ) + return _mktime(time_tuple) + +# +# LocalScheduleObject +# + +class LocalScheduleObject(CurrentPropertyListMixIn, ScheduleObject): + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + + # make sure present value was provided + if 'presentValue' not in kwargs: + raise RuntimeError("presentValue required") + if not isinstance(kwargs['presentValue'], Atomic): + raise TypeError("presentValue must be an Atomic value") + + # continue initialization + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors to check the reliability if these change + for prop in ('weeklySchedule', 'exceptionSchedule', 'scheduleDefault'): + self._property_monitors[prop].append(self._check_reliability) + + # check it now + self._check_reliability() + + def _check_reliability(self, old_value=None, new_value=None): + """This function is called when the object is created and after + one of its configuration properties has changed. The new and old value + parameters are ignored, this is called after the property has been + changed and this is only concerned with the current value.""" + if _debug: LocalScheduleObject._debug("_check_reliability %r %r", old_value, new_value) + + try: + schedule_default = self.scheduleDefault + + if schedule_default is None: + raise ValueError("scheduleDefault expected") + if not isinstance(schedule_default, Atomic): + raise TypeError("scheduleDefault must be an instance of an atomic type") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + # check the weekly schedule values + if self.weeklySchedule: + for daily_schedule in self.weeklySchedule: + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleObject._debug(" - daily time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + elif 255 in time_value.time: + if _debug: LocalScheduleObject._debug(" - wildcard in time") + raise ValueError("must be a specific time") + + # check the exception schedule values + if self.exceptionSchedule: + for special_event in self.exceptionSchedule: + for time_value in special_event.listOfTimeValues: + if _debug: LocalScheduleObject._debug(" - special event time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + + # check list of object property references + obj_prop_refs = self.listOfObjectPropertyReferences + if obj_prop_refs: + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + raise RuntimeError("no external references") + + # get the datatype of the property to be written + obj_type = obj_prop_ref.objectIdentifier[0] + datatype = get_datatype(obj_type, obj_prop_ref.propertyIdentifier) + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if issubclass(datatype, Array) and (obj_prop_ref.propertyArrayIndex is not None): + if obj_prop_ref.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if datatype is not schedule_datatype: + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + datatype, + schedule_datatype, + ) + raise TypeError("wrong type") + + # all good + self.reliability = 'noFaultDetected' + if _debug: LocalScheduleObject._debug(" - no fault detected") + + except Exception as err: + if _debug: LocalScheduleObject._debug(" - exception: %r", err) + self.reliability = 'configurationError' + +bacpypes_debugging(LocalScheduleObject) + +# +# LocalScheduleInterpreter +# + +class LocalScheduleInterpreter(OneShotTask): + + def __init__(self, sched_obj): + if _debug: LocalScheduleInterpreter._debug("__init__ %r", sched_obj) + OneShotTask.__init__(self) + + # reference the schedule object to update + self.sched_obj = sched_obj + + # add a monitor for the present value + sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + + # call to interpret the schedule + deferred(self.process_task) + + def present_value_changed(self, old_value, new_value): + """This function is called when the presentValue of the local schedule + object has changed, both internally by this interpreter, or externally + by some client using WriteProperty.""" + if _debug: LocalScheduleInterpreter._debug("present_value_changed %s %s", old_value, new_value) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # process the list of [device] object property [array index] references + obj_prop_refs = self.sched_obj.listOfObjectPropertyReferences + if not obj_prop_refs: + if _debug: LocalScheduleInterpreter._debug(" - no writes defined") + return + + # primitive values just set the value part + new_value = new_value.value + + # loop through the writes + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + if _debug: LocalScheduleInterpreter._debug(" - no externals") + continue + + # get the object from the application + obj = self.sched_obj._app.get_object_id(obj_prop_ref.objectIdentifier) + if not obj: + if _debug: LocalScheduleInterpreter._debug(" - no object") + continue + + # try to change the value + try: + obj.WriteProperty( + obj_prop_ref.propertyIdentifier, + new_value, + arrayIndex=obj_prop_ref.propertyArrayIndex, + priority=self.sched_obj.priorityForWriting, + ) + if _debug: LocalScheduleInterpreter._debug(" - success") + except Exception as err: + if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + + def process_task(self): + if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) + + # check for a valid configuration + if self.sched_obj.reliability != 'noFaultDetected': + if _debug: LocalScheduleInterpreter._debug(" - fault detected") + return + + # get the date and time from the device object in case it provides + # some custom functionality + if self.sched_obj._app and self.sched_obj._app.localDevice: + current_date = self.sched_obj._app.localDevice.localDate + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = self.sched_obj._app.localDevice.localTime + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + else: + # get the current date and time, as provided by the task manager + current_date = Date().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = Time().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + + # evaluate the time + current_value, next_transition = self.eval(current_date, current_time) + if _debug: LocalScheduleInterpreter._debug(" - current_value, next_transition: %r, %r", current_value, next_transition) + + ### set the present value + self.sched_obj.presentValue = current_value + + # compute the time of the next transition + transition_time = datetime_to_time(current_date, next_transition) + + # install this to run again + self.install_task(transition_time) + + def eval(self, edate, etime): + """Evaluate the schedule according to the provided date and time and + return the appropriate present value, or None if not in the effective + period.""" + if _debug: LocalScheduleInterpreter._debug("eval %r %r", edate, etime) + + # reference the schedule object + sched_obj = self.sched_obj + if _debug: LocalScheduleInterpreter._debug(" sched_obj: %r", sched_obj) + + # verify the date falls in the effective period + if not match_date_range(edate, sched_obj.effectivePeriod): + return None + + # the event priority is a list of values that are in effect for + # exception schedules with the special event priority, see 135.1-2013 + # clause 7.3.2.23.10.3.8, Revision 4 Event Priority Test + event_priority = [None] * 16 + + next_day = (24, 0, 0, 0) + next_transition_time = [None] * 16 + + # check the exception schedule values + if sched_obj.exceptionSchedule: + for special_event in sched_obj.exceptionSchedule: + if _debug: LocalScheduleInterpreter._debug(" - special_event: %r", special_event) + + # check the special event period + special_event_period = special_event.period + if special_event_period is None: + raise RuntimeError("special event period required") + + match = False + calendar_entry = special_event_period.calendarEntry + if calendar_entry: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + else: + # get the calendar object from the application + calendar_object = sched_obj._app.get_object_id(special_event_period.calendarReference) + if not calendar_object: + raise RuntimeError("invalid calendar object reference") + if _debug: LocalScheduleInterpreter._debug(" - calendar_object: %r", calendar_object) + + for calendar_entry in calendar_object.dateList: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + if match: + break + + # didn't match the period, try the next special event + if not match: + if _debug: LocalScheduleInterpreter._debug(" - no matching calendar entry") + continue + + # event priority array index + priority = special_event.eventPriority - 1 + if _debug: LocalScheduleInterpreter._debug(" - priority: %r", priority) + + # look for all of the possible times + for time_value in special_event.listOfTimeValues: + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - relinquish exception @ %r", tval) + event_priority[priority] = None + next_transition_time[priority] = None + else: + if _debug: LocalScheduleInterpreter._debug(" - consider exception @ %r", tval) + event_priority[priority] = time_value.value + next_transition_time[priority] = next_day + else: + next_transition_time[priority] = tval + break + + # assume the next transition will be at the start of the next day + earliest_transition = next_day + + # check if any of the special events came up with something + for priority_value, next_transition in zip(event_priority, next_transition_time): + if next_transition is not None: + earliest_transition = min(earliest_transition, next_transition) + if priority_value is not None: + if _debug: LocalScheduleInterpreter._debug(" - priority_value: %r", priority_value) + return priority_value, earliest_transition + + # start out with the default + daily_value = sched_obj.scheduleDefault + + # check the daily schedule + if sched_obj.weeklySchedule: + daily_schedule = sched_obj.weeklySchedule[edate[3]] + if _debug: LocalScheduleInterpreter._debug(" - daily_schedule: %r", daily_schedule) + + # look for all of the possible times + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleInterpreter._debug(" - time_value: %r", time_value) + + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - back to normal @ %r", tval) + daily_value = sched_obj.scheduleDefault + else: + if _debug: LocalScheduleInterpreter._debug(" - new value @ %r", tval) + daily_value = time_value.value + else: + earliest_transition = min(earliest_transition, tval) + break + + # return what was matched, if anything + return daily_value, earliest_transition + +bacpypes_debugging(LocalScheduleInterpreter) + diff --git a/py25/bacpypes/netservice.py b/py25/bacpypes/netservice.py index e564c111..76c5e021 100755 --- a/py25/bacpypes/netservice.py +++ b/py25/bacpypes/netservice.py @@ -4,7 +4,7 @@ Network Service """ -from copy import copy as _deepcopy +from copy import deepcopy as _deepcopy from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .errors import ConfigurationError @@ -27,32 +27,135 @@ ROUTER_UNREACHABLE = 3 # cannot route # -# NetworkReference +# RouterInfo # -class NetworkReference: - """These objects map a network to a router.""" +class RouterInfo(DebugContents): + """These objects are routing information records that map router + addresses with destination networks.""" - def __init__(self, net, router, status): - self.network = net - self.router = router - self.status = status + _debug_contents = ('snet', 'address', 'dnets', 'status') + + def __init__(self, snet, address, dnets, status=ROUTER_AVAILABLE): + self.snet = snet # source network + self.address = address # address of the router + self.dnets = dnets # list of reachable networks through this router + self.status = status # router status # -# RouterReference +# RouterInfoCache # -class RouterReference(DebugContents): - """These objects map a router; the adapter to talk to it, - its address, and a list of networks that it routes to.""" +class RouterInfoCache: + + def __init__(self): + if _debug: RouterInfoCache._debug("__init__") + + self.routers = {} # (snet, address) -> RouterInfo + self.networks = {} # network -> RouterInfo + + def get_router_info(self, dnet): + if _debug: RouterInfoCache._debug("get_router_info %r", dnet) + + # check to see if we know about it + if dnet not in self.networks: + if _debug: RouterInfoCache._debug(" - no route") + return None + + # return the network and address + router_info = self.networks[dnet] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # return the network, address, and status + return (router_info.snet, router_info.address, router_info.status) + + def update_router_info(self, snet, address, dnets): + if _debug: RouterInfoCache._debug("update_router_info %r %r %r", snet, address, dnets) + + # look up the router reference, make a new record if necessary + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - new router") + router_info = self.routers[key] = RouterInfo(snet, address, list()) + else: + router_info = self.routers[key] + + # add (or move) the destination networks + for dnet in dnets: + if dnet in self.networks: + other_router = self.networks[dnet] + if other_router is router_info: + if _debug: RouterInfoCache._debug(" - existing router, match") + continue + elif dnet not in other_router.dnets: + if _debug: RouterInfoCache._debug(" - where did it go?") + else: + other_router.dnets.remove(dnet) + if not other_router.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[(snet, other_router.address)] + + # add a reference to the router + self.networks[dnet] = router_info + if _debug: RouterInfoCache._debug(" - reference added") + + # maybe update the list of networks for this router + if dnet not in router_info.dnets: + router_info.dnets.append(dnet) + if _debug: RouterInfoCache._debug(" - dnet added, now: %r", router_info.dnets) + + def update_router_status(self, snet, address, status): + if _debug: RouterInfoCache._debug("update_router_status %r %r %r", snet, address, status) + + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - not a router we care about") + return + + router_info = self.routers[key] + router_info.status = status + if _debug: RouterInfoCache._debug(" - status updated") + + def delete_router_info(self, snet, address=None, dnets=None): + if _debug: RouterInfoCache._debug("delete_router_info %r %r %r", dnets) + + # if address is None, remove all the routers for the network + if address is None: + for rnet, raddress in self.routers.keys(): + if snet == rnet: + if _debug: RouterInfoCache._debug(" - going down") + self.delete_router_info(snet, raddress) + if _debug: RouterInfoCache._debug(" - back topside") + return + + # look up the router reference + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - unknown router") + return + + router_info = self.routers[key] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # if dnets is None, remove all the networks for the router + if dnets is None: + dnets = router_info.dnets + + # loop through the list of networks to be deleted + for dnet in dnets: + if dnet in self.networks: + del self.networks[dnet] + if _debug: RouterInfoCache._debug(" - removed from networks: %r", dnet) + if dnet in router_info.dnets: + router_info.dnets.remove(dnet) + if _debug: RouterInfoCache._debug(" - removed from router_info: %r", dnet) - _debug_contents = ('adapter-', 'address', 'networks', 'status') + # see if we still care + if not router_info.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[key] - def __init__(self, adapter, addr, nets, status): - self.adapter = adapter - self.address = addr # local station relative to the adapter - self.networks = nets # list of remote networks - self.status = status # status as presented by the router +bacpypes_debugging(RouterInfoCache) # # NetworkAdapter @@ -63,14 +166,11 @@ class NetworkAdapter(Client, DebugContents): _debug_contents = ('adapterSAP-', 'adapterNet') def __init__(self, sap, net, cid=None): - if _debug: NetworkAdapter._debug("__init__ %r (net=%r) cid=%r", sap, net, cid) + if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) Client.__init__(self, cid) self.adapterSAP = sap self.adapterNet = net - # add this to the list of adapters for the network - sap.adapters.append(self) - def confirmation(self, pdu): """Decode upstream PDUs and pass them up to the service access point.""" if _debug: NetworkAdapter._debug("confirmation %r (net=%r)", pdu, self.adapterNet) @@ -105,117 +205,73 @@ class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): , 'localAdapter-', 'localAddress' ) - def __init__(self, sap=None, sid=None): + def __init__(self, routerInfoCache=None, sap=None, sid=None): if _debug: NetworkServiceAccessPoint._debug("__init__ sap=%r sid=%r", sap, sid) ServiceAccessPoint.__init__(self, sap) Server.__init__(self, sid) - self.adapters = [] # list of adapters - self.routers = {} # (adapter, address) -> RouterReference - self.networks = {} # network -> RouterReference + # map of directly connected networks + self.adapters = {} # net -> NetworkAdapter + + # use the provided cache or make a default one + self.router_info_cache = routerInfoCache or RouterInfoCache() + + # map to a list of application layer packets waiting for a path + self.pending_nets = {} - self.localAdapter = None # which one is local - self.localAddress = None # what is the local address + # these are set when bind() is called + self.local_adapter = None + self.local_address = None def bind(self, server, net=None, address=None): """Create a network adapter object and bind.""" if _debug: NetworkServiceAccessPoint._debug("bind %r net=%r address=%r", server, net, address) - if (net is None) and self.adapters: + # make sure this hasn't already been called with this network + if net in self.adapters: raise RuntimeError("already bound") - # create an adapter object + # when binding to an adapter and there is more than one, then they + # must all have network numbers and one of them will be the default + if (net is not None) and (None in self.adapters): + raise RuntimeError("default adapter bound") + + # create an adapter object, add it to our map adapter = NetworkAdapter(self, net) + self.adapters[net] = adapter + if _debug: NetworkServiceAccessPoint._debug(" - adapters[%r]: %r", net, adapter) # if the address was given, make it the "local" one if address: - self.localAdapter = adapter - self.localAddress = address + self.local_adapter = adapter + self.local_address = address # bind to the server bind(adapter, server) #----- - def add_router_references(self, adapter, address, netlist): + def add_router_references(self, snet, address, dnets): """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", adapter, address, netlist) - - # make a key for the router reference - rkey = (adapter, address) - - for snet in netlist: - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] - - if rref.adapter == adapter and rref.address == address: - pass # matches current entry - else: - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] - - # remove the network - del self.networks[snet] + if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) - ### check to see if it is OK to add the new entry + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - # get the router reference for this router - rref = self.routers.get(rkey, None) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # pass this along to the cache + self.router_info_cache.update_router_info(snet, address, dnets) - # reference the snet - self.networks[snet] = rref - else: - # new reference - rref = RouterReference( adapter, address, [snet], 0) - self.routers[rkey] = rref + def delete_router_references(self, snet, address=None, dnets=None): + """Delete references to routers/networks.""" + if _debug: NetworkServiceAccessPoint._debug("delete_router_references %r %r %r", snet, address, dnets) - # reference the snet - self.networks[snet] = rref + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - def remove_router_references(self, adapter, address=None): - """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("remove_router_references %r %r", adapter, address) - - delrlist = [] - delnlist = [] - # scan through the dictionary of router references - for rkey in self.routers.keys(): - # rip apart the key - radapter, raddress = rkey - - # pick all references on the adapter, optionally limited to a specific address - match = radapter is adapter - if match and address is not None: - match = (raddress == address) - if not match: - continue - - # save it for deletion - delrlist.append(rkey) - delnlist.extend(self.routers[rkey].networks) - if _debug: - NetworkServiceAccessPoint._debug(" - delrlist: %r", delrlist) - NetworkServiceAccessPoint._debug(" - delnlist: %r", delnlist) - - # delete the entries - for rkey in delrlist: - try: - del self.routers[rkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - rkey not in self.routers: %r", rkey) - for nkey in delnlist: - try: - del self.networks[nkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - nkey not in self.networks: %r", rkey) + # pass this along to the cache + self.router_info_cache.delete_router_info(snet, address, dnets) #----- @@ -227,11 +283,12 @@ def indication(self, pdu): raise ConfigurationError("no adapters") # might be able to relax this restriction - if (len(self.adapters) > 1) and (not self.localAdapter): + if (len(self.adapters) > 1) and (not self.local_adapter): raise ConfigurationError("local adapter must be set") # get the local adapter - adapter = self.localAdapter or self.adapters[0] + adapter = self.local_adapter or self.adapters[None] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # build a generic APDU apdu = _APDU(user_data=pdu.pduUserData) @@ -263,7 +320,7 @@ def indication(self, pdu): npdu.npduDADR = apdu.pduDestination # send it to all of connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): xadapter.process_npdu(npdu) return @@ -279,32 +336,53 @@ def indication(self, pdu): ### when it's a directly connected network raise RuntimeError("addressing problem") - # check for an available path - if dnet in self.networks: - rref = self.networks[dnet] - adapter = rref.adapter + # get it ready to send when the path is found + npdu.pduDestination = None + npdu.npduDADR = apdu.pduDestination + + # we might already be waiting for a path for this network + if dnet in self.pending_nets: + if _debug: NetworkServiceAccessPoint._debug(" - already waiting for path") + self.pending_nets[dnet].append(npdu) + return + + # check cache for an available path + path_info = self.router_info_cache.get_router_info(dnet) - ### make sure the direct connect is OK, may need to connect + # if there is info, we have a path + if path_info: + snet, address, status = path_info + if _debug: NetworkServiceAccessPoint._debug(" - path found: %r, %r, %r", snet, address, status) - ### make sure the peer router is OK, may need to connect + # check for an adapter + if snet not in self.adapters: + raise RuntimeError("network found but not connected: %r", snet) + adapter = self.adapters[snet] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # fix the destination - npdu.pduDestination = rref.address - npdu.npduDADR = apdu.pduDestination + npdu.pduDestination = address # send it along adapter.process_npdu(npdu) return - if _debug: NetworkServiceAccessPoint._debug(" - no known path to network, broadcast to discover it") + if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") - # set the destination - npdu.pduDestination = LocalBroadcast() - npdu.npduDADR = apdu.pduDestination + # add it to the list of packets waiting for the network + net_list = self.pending_nets.get(dnet, None) + if net_list is None: + net_list = self.pending_nets[dnet] = [] + net_list.append(npdu) + + # build a request for the network and send it to all of the adapters + xnpdu = WhoIsRouterToNetwork(dnet) + xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: - xadapter.process_npdu(npdu) + for adapter in self.adapters.values(): + ### make sure the adapter is OK + self.sap_indication(adapter, xnpdu) def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("process_npdu %r %r", adapter, npdu) @@ -312,83 +390,68 @@ def process_npdu(self, adapter, npdu): # make sure our configuration is OK if (not self.adapters): raise ConfigurationError("no adapters") - if (len(self.adapters) > 1) and (not self.localAdapter): - raise ConfigurationError("local adapter must be set") # check for source routing if npdu.npduSADR and (npdu.npduSADR.addrType != Address.nullAddr): + if _debug: NetworkServiceAccessPoint._debug(" - check source path") + # see if this is attempting to spoof a directly connected network snet = npdu.npduSADR.addrNet - for xadapter in self.adapters: - if (xadapter is not adapter) and (snet == xadapter.adapterNet): - NetworkServiceAccessPoint._warning("spoof?") - ### log this - return - - # make a key for the router reference - rkey = (adapter, npdu.pduSource) - - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] - if rref.adapter == adapter and rref.address == npdu.pduSource: - pass # matches current entry - else: - if _debug: NetworkServiceAccessPoint._debug(" - replaces entry") - - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] + if snet in self.adapters: + NetworkServiceAccessPoint._warning(" - path error (1)") + return - # remove the network - del self.networks[snet] + # see if there is routing information for this source network + router_info = self.router_info_cache.get_router_info(snet) + if router_info: + router_snet, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug(" - router_address, router_status: %r, %r", router_address, router_status) - # get the router reference for this router - rref = self.routers.get(rkey) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # see if the router has changed + if not (router_address == npdu.pduSource): + if _debug: NetworkServiceAccessPoint._debug(" - replacing path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) else: - # new reference - rref = RouterReference( adapter, npdu.pduSource, [snet], 0) - self.routers[rkey] = rref + if _debug: NetworkServiceAccessPoint._debug(" - new path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) # check for destination routing if (not npdu.npduDADR) or (npdu.npduDADR.addrType == Address.nullAddr): - processLocally = (not self.localAdapter) or (adapter is self.localAdapter) or (npdu.npduNetMessage is not None) + if _debug: NetworkServiceAccessPoint._debug(" - no DADR") + + processLocally = (not self.local_adapter) or (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) forwardMessage = False elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote broadcast") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (2)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) forwardMessage = True elif npdu.npduDADR.addrType == Address.remoteStationAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote station") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (3)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) \ - and (npdu.npduDADR.addrAddr == self.localAddress.addrAddr) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ + and (npdu.npduDADR.addrAddr == self.local_address.addrAddr) forwardMessage = not processLocally elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - DADR is global broadcast") + processLocally = True forwardMessage = True @@ -402,6 +465,8 @@ def process_npdu(self, adapter, npdu): # application or network layer message if npdu.npduNetMessage is None: + if _debug: NetworkServiceAccessPoint._debug(" - application layer message") + if processLocally and self.serverPeer: if _debug: NetworkServiceAccessPoint._debug(" - processing APDU locally") @@ -411,7 +476,7 @@ def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug(" - apdu: %r", apdu) # see if it needs to look routed - if (len(self.adapters) > 1) and (adapter != self.localAdapter): + if (len(self.adapters) > 1) and (adapter != self.local_adapter): # combine the source address if not npdu.npduSADR: apdu.pduSource = RemoteStation( adapter.adapterNet, npdu.pduSource.addrAddr ) @@ -420,7 +485,7 @@ def process_npdu(self, adapter, npdu): # map the destination if not npdu.npduDADR: - apdu.pduDestination = self.localAddress + apdu.pduDestination = self.local_address elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: apdu.pduDestination = npdu.npduDADR elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: @@ -447,6 +512,8 @@ def process_npdu(self, adapter, npdu): self.response(apdu) else: + if _debug: NetworkServiceAccessPoint._debug(" - network layer message") + if processLocally: if npdu.npduNetMessage not in npdu_types: if _debug: NetworkServiceAccessPoint._debug(" - unknown npdu type: %r", npdu.npduNetMessage) @@ -461,18 +528,19 @@ def process_npdu(self, adapter, npdu): # pass to the service element self.sap_request(adapter, xpdu) - # maybe local processing only + # might not need to forward this to other devices if not forwardMessage: if _debug: NetworkServiceAccessPoint._debug(" - no forwarding") return # make sure we're really a router if (len(self.adapters) == 1): - if _debug: NetworkServiceAccessPoint._debug(" - not really a router") + if _debug: NetworkServiceAccessPoint._debug(" - not a router") return # make sure it hasn't looped if (npdu.npduHopCount == 0): + if _debug: NetworkServiceAccessPoint._debug(" - no more hops") return # build a new NPDU to send to other adapters @@ -493,9 +561,10 @@ def process_npdu(self, adapter, npdu): # if this is a broadcast it goes everywhere if npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - global broadcasting") newpdu.pduDestination = LocalBroadcast() - for xadapter in self.adapters: + for xadapter in self.adapters.values(): if (xadapter is not adapter): xadapter.process_npdu(_deepcopy(newpdu)) return @@ -503,38 +572,54 @@ def process_npdu(self, adapter, npdu): if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr) \ or (npdu.npduDADR.addrType == Address.remoteStationAddr): dnet = npdu.npduDADR.addrNet + if _debug: NetworkServiceAccessPoint._debug(" - remote station/broadcast") - # see if this should go to one of our directly connected adapters - for xadapter in self.adapters: - if dnet == xadapter.adapterNet: - if _debug: NetworkServiceAccessPoint._debug(" - found direct connect via %r", xadapter) - if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): - newpdu.pduDestination = LocalBroadcast() - else: - newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) + # see if this a locally connected network + if dnet in self.adapters: + xadapter = self.adapters[dnet] + if xadapter is adapter: + if _debug: NetworkServiceAccessPoint._debug(" - path error (4)") + return + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - # last leg in routing - newpdu.npduDADR = None + # if this was a remote broadcast, it's now a local one + if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): + newpdu.pduDestination = LocalBroadcast() + else: + newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) - # send the packet downstream - xadapter.process_npdu(_deepcopy(newpdu)) - return + # last leg in routing + newpdu.npduDADR = None - # see if we know how to get there - if dnet in self.networks: - rref = self.networks[dnet] - newpdu.pduDestination = rref.address + # send the packet downstream + xadapter.process_npdu(_deepcopy(newpdu)) + return - ### check to make sure the router is OK + # see if there is routing information for this destination network + router_info = self.router_info_cache.get_router_info(dnet) + if router_info: + router_net, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + + if router_net not in self.adapters: + if _debug: NetworkServiceAccessPoint._debug(" - path error (5)") + return - ### check to make sure the network is OK, may need to connect + xadapter = self.adapters[router_net] + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - if _debug: NetworkServiceAccessPoint._debug(" - newpdu: %r", newpdu) + # the destination is the address of the router + newpdu.pduDestination = router_address # send the packet downstream - rref.adapter.process_npdu(_deepcopy(newpdu)) + xadapter.process_npdu(_deepcopy(newpdu)) return + if _debug: NetworkServiceAccessPoint._debug(" - no router info found") + ### queue this message for reprocessing when the response comes back # try to find a path to the network @@ -542,16 +627,17 @@ def process_npdu(self, adapter, npdu): xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): # skip the horse it rode in on if (xadapter is adapter): continue - ### make sure the adapter is OK + # pass this along as if it came from the NSE self.sap_indication(xadapter, xnpdu) - ### log this, what to do? - return + return + + if _debug: NetworkServiceAccessPoint._debug(" - bad DADR: %r", npdu.npduDADR) def sap_indication(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("sap_indication %r %r", adapter, npdu) @@ -624,17 +710,15 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # build a list of reachable networks netlist = [] - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter): - netlist.append(xadapter.adapterNet) + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is adapter): + continue - # build a list of other available networks - for net, rref in sap.networks.items(): - if rref.adapter is not adapter: - ### skip those marked unreachable - ### skip those that are not available - netlist.append(net) + # add the direct network + netlist.append(xadapter.adapterNet) + + ### add the other reachable if netlist: if _debug: NetworkServiceElement._debug(" - found these: %r", netlist) @@ -649,42 +733,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): else: # requesting a specific network if _debug: NetworkServiceElement._debug(" - requesting specific network: %r", npdu.wirtnNetwork) + dnet = npdu.wirtnNetwork - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter) and (npdu.wirtnNetwork == xadapter.adapterNet): - if _debug: NetworkServiceElement._debug(" - found it directly connected") + # check the directly connected networks + if dnet in sap.adapters: + if _debug: NetworkServiceElement._debug(" - directly connected") - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) - break else: - # check for networks I know about - if npdu.wirtnNetwork in sap.networks: - rref = sap.networks[npdu.wirtnNetwork] - if rref.adapter is adapter: - if _debug: NetworkServiceElement._debug(" - same net as request") + # see if there is routing information for this source network + router_info = sap.router_info_cache.get_router_info(dnet) + if router_info: + if _debug: NetworkServiceElement._debug(" - router found") + + router_net, router_address, router_status = router_info + if _debug: NetworkServiceElement._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + if router_net not in sap.adapters: + if _debug: NetworkServiceElement._debug(" - path error (6)") + return - else: - if _debug: NetworkServiceElement._debug(" - found on adapter: %r", rref.adapter) - - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) else: if _debug: NetworkServiceElement._debug(" - forwarding request to other adapters") # build a request - whoisrtn = WhoIsRouterToNetwork(npdu.wirtnNetwork, user_data=npdu.pduUserData) + whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) whoisrtn.pduDestination = LocalBroadcast() # if the request had a source, forward it along @@ -695,7 +783,7 @@ def WhoIsRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) # send it to all of the (other) adapters - for xadapter in sap.adapters: + for xadapter in sap.adapters.values(): if xadapter is not adapter: if _debug: NetworkServiceElement._debug(" - sending on adapter: %r", xadapter) self.request(xadapter, whoisrtn) @@ -703,8 +791,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("IAmRouterToNetwork %r %r", adapter, npdu) + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + # pass along to the service access point - self.elementService.add_router_references(adapter, npdu.pduSource, npdu.iartnNetworkList) + sap.add_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) + + # skip if this is not a router + if len(sap.adapters) > 1: + # build a broadcast annoucement + iamrtn = IAmRouterToNetwork(npdu.iartnNetworkList, user_data=npdu.pduUserData) + iamrtn.pduDestination = LocalBroadcast() + + # send it to all of the connected adapters + for xadapter in sap.adapters.values(): + # skip the horse it rode in on + if (xadapter is adapter): + continue + + # request this + self.request(xadapter, iamrtn) + + # look for pending NPDUs for the networks + for dnet in npdu.iartnNetworkList: + pending_npdus = sap.pending_nets.get(dnet, None) + if pending_npdus is not None: + if _debug: NetworkServiceElement._debug(" - %d pending to %r", len(pending_npdus), dnet) + + # delete the references + del sap.pending_nets[dnet] + + # now reprocess them + for pending_npdu in pending_npdus: + if _debug: NetworkServiceElement._debug(" - sending %s", repr(pending_npdu)) + + # the destination is the address of the router + pending_npdu.pduDestination = npdu.pduSource + + # send the packet downstream + adapter.process_npdu(pending_npdu) def ICouldBeRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("ICouldBeRouterToNetwork %r %r", adapter, npdu) @@ -755,3 +881,4 @@ def DisconnectConnectionToNetwork(self, adapter, npdu): # sap = self.elementService bacpypes_debugging(NetworkServiceElement) + diff --git a/py25/bacpypes/npdu.py b/py25/bacpypes/npdu.py index 8e2ebaa8..69033ed8 100755 --- a/py25/bacpypes/npdu.py +++ b/py25/bacpypes/npdu.py @@ -479,7 +479,7 @@ def __init__(self, netList=[], *args, **kwargs): def encode(self, npdu): NPCI.update(npdu, self) - for net in self.ratnNetworkList: + for net in self.rbtnNetworkList: npdu.put_short(net) def decode(self, npdu): @@ -546,6 +546,12 @@ def __init__(self, dnet=None, portID=None, portInfo=None): self.rtPortID = portID self.rtPortInfo = portInfo + def __eq__(self, other): + """Return true iff entries are identical.""" + return (self.rtDNET == other.rtDNET) and \ + (self.rtPortID == other.rtPortID) and \ + (self.rtPortInfo == other.rtPortInfo) + def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" # make/extend the dictionary of content @@ -738,6 +744,11 @@ class WhatIsNetworkNumber(NPDU): messageType = 0x12 + def __init__(self, *args, **kwargs): + super(WhatIsNetworkNumber, self).__init__(*args, **kwargs) + + self.npduNetMessage = WhatIsNetworkNumber.messageType + def encode(self, npdu): NPCI.update(npdu, self) @@ -758,10 +769,17 @@ def npdu_contents(self, use_dict=None, as_class=dict): class NetworkNumberIs(NPDU): - _debug_contents = ('nniNET', 'nniFlag',) + _debug_contents = ('nniNet', 'nniFlag',) messageType = 0x13 + def __init__(self, net=None, flag=None, *args, **kwargs): + super(NetworkNumberIs, self).__init__(*args, **kwargs) + + self.npduNetMessage = NetworkNumberIs.messageType + self.nniNet = net + self.nniFlag = flag + def encode(self, npdu): NPCI.update(npdu, self) npdu.put_short( self.nniNET ) diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index 503625e5..c5ce0cae 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -81,6 +81,7 @@ def _register(xcls): # build a property dictionary by going through the class and all its parents _properties = {} for c in cls.__mro__: + if _debug: register_object_type._debug(" - c: %r", c) for prop in getattr(c, 'properties', []): if prop.identifier not in _properties: _properties[prop.identifier] = prop @@ -214,6 +215,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + elif issubclass(self.datatype, AnyAtomic): + if _debug: Property._debug(" - property is any atomic, checking value") + if not isinstance(value, Atomic): + raise InvalidParameterDatatype("%s must be an atomic instance" % ( + self.identifier, + )) + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): @@ -1432,7 +1440,7 @@ class EventEnrollmentObject(Object): , ReadableProperty('eventTimeStamps', ArrayOf(TimeStamp)) , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString)) , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString)) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference) , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index 15fa21db..5367ed05 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -14,6 +14,9 @@ from .errors import DecodingError, InvalidTag, InvalidParameterDatatype from .pdu import PDUData +# import the task manager to get the "current" date and time +from .task import TaskManager as _TaskManager + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -449,6 +452,8 @@ class Atomic(object): _app_tag = None def __cmp__(self, other): + # sys.stderr.write("__cmp__ %r %r\n" % (self, other)) + # hoop jump it if not isinstance(other, self.__class__): other = self.__class__(other) @@ -461,6 +466,26 @@ def __cmp__(self, other): else: return 0 + def __lt__(self, other): + # sys.stderr.write("__lt__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return (self.value < other.value) + + def __eq__(self, other): + # sys.stderr.write("__eq__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return self.value == other.value + @classmethod def coerce(cls, arg): """Given an arg, return the appropriate value given the class.""" @@ -1338,6 +1363,9 @@ def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): elif isinstance(arg, Date): self.value = arg.value + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") @@ -1366,11 +1394,31 @@ def CalcDayOfWeek(self): # put it back together self.value = (year, month, day, day_of_week) - def now(self): - tup = time.localtime() + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) + self.value = (tup[0]-1900, tup[1], tup[2], tup[6] + 1) + return self + def __float__(self): + """Convert to seconds since the epoch.""" + # rip apart the value + year, month, day, day_of_week = self.value + + # check for special values + if (year == 255) or (month in _special_mon_inv) or (day in _special_day_inv): + raise ValueError("no wildcard values") + + # convert to time.time() value + return time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.dateAppTag, ''.join(chr(i) for i in self.value)) @@ -1450,19 +1498,40 @@ def __init__(self, arg=None, hour=255, minute=255, second=255, hundredth=255): tup_list[3] = tup_list[3] * 10 self.value = tuple(tup_list) + elif isinstance(arg, Time): self.value = arg.value + + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") - def now(self): - now = time.time() - tup = time.localtime(now) + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) - self.value = (tup[3], tup[4], tup[5], int((now - int(now)) * 100)) + self.value = (tup[3], tup[4], tup[5], int((when - int(when)) * 100)) return self + def __float__(self): + """Return the current value as an offset from midnight.""" + if 255 in self.value: + raise ValueError("no wildcard values") + + # rip it apart + hour, minute, second, hundredth = self.value + + # put it together + return (hour * 3600.0) + (minute * 60.0) + second + (hundredth / 100.0) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.timeAppTag, ''.join(chr(c) for c in self.value)) diff --git a/py25/bacpypes/service/device.py b/py25/bacpypes/service/device.py index 3c0e9bac..af3d29ae 100644 --- a/py25/bacpypes/service/device.py +++ b/py25/bacpypes/service/device.py @@ -4,137 +4,16 @@ from ..capability import Capability from ..pdu import GlobalBroadcast -from ..primitivedata import Date, Time, ObjectIdentifier -from ..constructeddata import ArrayOf -from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU, Error +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU from ..errors import ExecutionError, InconsistentParameters, \ MissingRequiredParameter, ParameterOutOfRange -from ..object import register_object_type, registered_object_types, \ - Property, DeviceObject from ..task import FunctionTask -from .object import CurrentPropertyListMixIn - # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for local time - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - - # the object list is provided - if 'objectList' in kwargs: - raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) - -bacpypes_debugging(LocalDeviceObject) - # # Who-Is I-Am Services # diff --git a/py25/bacpypes/service/object.py b/py25/bacpypes/service/object.py index 0ce857a7..a7fe647a 100755 --- a/py25/bacpypes/service/object.py +++ b/py25/bacpypes/service/object.py @@ -20,59 +20,6 @@ # handy reference ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) -# -# CurrentPropertyList -# - -class CurrentPropertyList(Property): - - def __init__(self): - if _debug: CurrentPropertyList._debug("__init__") - Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) - - # make a list of the properties that have values - property_list = [k for k, v in obj._values.items() - if v is not None - and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') - ] - if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) - - # sort the list so it's stable - property_list.sort() - - # asking for the whole thing - if arrayIndex is None: - return ArrayOfPropertyIdentifier(property_list) - - # asking for the length - if arrayIndex == 0: - return len(property_list) - - # asking for an index - if arrayIndex > len(property_list): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return property_list[arrayIndex - 1] - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -bacpypes_debugging(CurrentPropertyList) - -# -# CurrentPropertyListMixIn -# - -class CurrentPropertyListMixIn(Object): - - properties = [ - CurrentPropertyList(), - ] - -bacpypes_debugging(CurrentPropertyListMixIn) - # # ReadProperty and WriteProperty Services # diff --git a/py25/bacpypes/task.py b/py25/bacpypes/task.py index a761da4c..04096bbb 100755 --- a/py25/bacpypes/task.py +++ b/py25/bacpypes/task.py @@ -8,6 +8,7 @@ from time import time as _time from heapq import heapify, heappush, heappop +import itertools from .singleton import SingletonLogging from .debugging import DebugContents, Logging, ModuleLogger, bacpypes_debugging @@ -280,6 +281,9 @@ def __init__(self): # task manager is this instance _task_manager = self + # unique sequence counter for tasks scheduled at the same time + self.counter = itertools.count() + # there may be tasks created that couldn't be scheduled # because a task manager wasn't created yet. if _unscheduled_tasks: @@ -304,7 +308,7 @@ def install_task(self, task): self.suspend_task(task) # save this in the task list - heappush( self.tasks, (task.taskTime, task) ) + heappush( self.tasks, (task.taskTime, next(self.counter), task) ) if _debug: TaskManager._debug(" - tasks: %r", self.tasks) task.isScheduled = True @@ -317,7 +321,7 @@ def suspend_task(self, task): if _debug: TaskManager._debug("suspend_task %r", task) # remove this guy - for i, (when, curtask) in enumerate(self.tasks): + for i, (when, n, curtask) in enumerate(self.tasks): if task is curtask: if _debug: TaskManager._debug(" - task found") del self.tasks[i] @@ -352,7 +356,7 @@ def get_next_task(self): if self.tasks: # look at the first task - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] if when <= now: # pull it off the list and mark that it's no longer scheduled heappop(self.tasks) @@ -360,7 +364,7 @@ def get_next_task(self): task.isScheduled = False if self.tasks: - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] # peek at the next task, return how long to wait delta = max(when - now, 0.0) else: diff --git a/py25/bacpypes/vlan.py b/py25/bacpypes/vlan.py index e3d94069..fef75358 100755 --- a/py25/bacpypes/vlan.py +++ b/py25/bacpypes/vlan.py @@ -34,6 +34,9 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.broadcast_address = broadcast_address self.drop_percent = drop_percent + # point to a TrafficLog instance + self.traffic_log = None + def add_node(self, node): """ Add a node to this network, let the node know which network it's on. """ if _debug: Network._debug("add_node %r", node) @@ -58,6 +61,10 @@ def process_pdu(self, pdu): """ if _debug: Network._debug("process_pdu(%s) %r", self.name, pdu) + # if there is a traffic log, call it with the network name and pdu + if self.traffic_log: + self.traffic_log(self.name, pdu) + # randomly drop a packet if self.drop_percent != 0.0: if (random.random() * 100.0) < self.drop_percent: @@ -154,9 +161,9 @@ class IPNetwork(Network): ('1.2.3.255', 5) and the other nodes must have the same tuple. """ - def __init__(self): + def __init__(self, name=''): if _debug: IPNetwork._debug("__init__") - Network.__init__(self) + Network.__init__(self, name=name) def add_node(self, node): if _debug: IPNetwork._debug("add_node %r", node) @@ -206,11 +213,12 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): class IPRouterNode(Client): - def __init__(self, router, addr, lan=None): + def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) - # save the reference to the router + # save the references to the router for packets and the lan for debugging self.router = router + self.lan = lan # make ourselves an IPNode and bind to it self.node = IPNode(addr, lan=lan, promiscuous=True, spoofing=True) @@ -231,6 +239,9 @@ def process_pdu(self, pdu): # pass it downstream self.request(pdu) + def __repr__(self): + return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + bacpypes_debugging(IPRouterNode) # diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 90604514..8e201b5b 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.0' +__version__ = '0.17.2' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' @@ -69,6 +69,8 @@ from . import app from . import appservice + +from . import local from . import service # diff --git a/py27/bacpypes/analysis.py b/py27/bacpypes/analysis.py index 388360a4..0840c6ac 100755 --- a/py27/bacpypes/analysis.py +++ b/py27/bacpypes/analysis.py @@ -23,7 +23,7 @@ except: pass -from .debugging import ModuleLogger, DebugContents, bacpypes_debugging, btox +from .debugging import ModuleLogger, bacpypes_debugging, btox from .pdu import PDU, Address from .bvll import BVLPDU, bvl_pdu_types, ForwardedNPDU, \ @@ -144,6 +144,8 @@ def decode_packet(data): # assume it is ethernet for now d = decode_ethernet(data) + pduSource = Address(d['source_address']) + pduDestination = Address(d['destination_address']) data = d['data'] # there could be a VLAN header @@ -174,10 +176,8 @@ def decode_packet(data): decode_packet._debug(" - pduDestination: %r", pduDestination) else: if _debug: decode_packet._debug(" - not a UDP packet") - return None else: if _debug: decode_packet._debug(" - not an IP packet") - return None # check for empty if not data: @@ -374,7 +374,7 @@ def decode_file(fname): # @bacpypes_debugging -class Tracer(DebugContents): +class Tracer: def __init__(self, initial_state=None): if _debug: Tracer._debug("__init__ initial_state=%r", initial_state) diff --git a/py27/bacpypes/apdu.py b/py27/bacpypes/apdu.py index 10706aa2..c19a811e 100755 --- a/py27/bacpypes/apdu.py +++ b/py27/bacpypes/apdu.py @@ -56,38 +56,51 @@ def register_error_type(klass): # encode_max_segments_accepted/decode_max_segments_accepted # +_max_segments_accepted_encoding = [ + None, 2, 4, 8, 16, 32, 64, None, + ] + def encode_max_segments_accepted(arg): """Encode the maximum number of segments the device will accept, Section - 20.1.2.4""" - w = 0 - while (arg and not arg & 1): - w += 1 - arg = (arg >> 1) - return w + 20.1.2.4, and if the device says it can only accept one segment it shouldn't + say that it supports segmentation!""" + # unspecified + if not arg: + return 0 + + if arg > 64: + return 7 + + # the largest number not greater than the arg + for i in range(6, 0, -1): + if _max_segments_accepted_encoding[i] <= arg: + return i + + raise ValueError("invalid max max segments accepted: {0}".format(arg)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section 20.1.2.4""" - return arg and (1 << arg) or None + return _max_segments_accepted_encoding[arg] # # encode_max_apdu_length_accepted/decode_max_apdu_length_accepted # -_max_apdu_response_encoding = [50, 128, 206, 480, 1024, 1476, None, None, +_max_apdu_length_encoding = [50, 128, 206, 480, 1024, 1476, None, None, None, None, None, None, None, None, None, None] def encode_max_apdu_length_accepted(arg): """Return the encoding of the highest encodable value less than the value of the arg.""" for i in range(5, -1, -1): - if (arg >= _max_apdu_response_encoding[i]): + if (arg >= _max_apdu_length_encoding[i]): return i raise ValueError("invalid max APDU length accepted: {0}".format(arg)) def decode_max_apdu_length_accepted(arg): - v = _max_apdu_response_encoding[arg] + v = _max_apdu_length_encoding[arg] if not v: raise ValueError("invalid max APDU length accepted: {0}".format(arg)) @@ -174,7 +187,7 @@ def encode(self, pdu): if self.apduSA: buff += 0x02 pdu.put(buff) - pdu.put((encode_max_segments_accepted(self.apduMaxSegs) << 4) + encode_max_apdu_length_accepted(self.apduMaxResp)) + pdu.put((self.apduMaxSegs << 4) + self.apduMaxResp) pdu.put(self.apduInvokeID) if self.apduSeg: pdu.put(self.apduSeq) @@ -255,8 +268,8 @@ def decode(self, pdu): self.apduMor = ((buff & 0x04) != 0) self.apduSA = ((buff & 0x02) != 0) buff = pdu.get() - self.apduMaxSegs = decode_max_segments_accepted( (buff >> 4) & 0x07 ) - self.apduMaxResp = decode_max_apdu_length_accepted( buff & 0x0F ) + self.apduMaxSegs = (buff >> 4) & 0x07 + self.apduMaxResp = buff & 0x0F self.apduInvokeID = pdu.get() if self.apduSeg: self.apduSeq = pdu.get() @@ -1301,7 +1314,7 @@ class LifeSafetyOperationRequest(ConfirmedRequestSequence): [ Element('requestingProcessIdentifier', Unsigned, 0) , Element('requestingSource', CharacterString, 1) , Element('request', LifeSafetyOperation, 2) - , Element('objectIdentifier', ObjectIdentifier, 3) + , Element('objectIdentifier', ObjectIdentifier, 3, True) ] register_confirmed_request_type(LifeSafetyOperationRequest) @@ -1485,7 +1498,7 @@ class RemoveListElementRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('objectIdentifier', ObjectIdentifier, 0) , Element('propertyIdentifier', PropertyIdentifier, 1) - , Element('propertyArrayIndex', Unsigned, 2) + , Element('propertyArrayIndex', Unsigned, 2, True) , Element('listOfElements', Any, 3) ] diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index a1081497..1548a3b4 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -26,7 +26,8 @@ # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ - ConfirmedServiceChoice, UnconfirmedServiceChoice + ConfirmedServiceChoice, UnconfirmedServiceChoice, \ + IAmRequest from .basetypes import ServicesSupported # basic services @@ -54,16 +55,16 @@ class DeviceInfo(DebugContents): 'maxSegmentsAccepted', ) - def __init__(self): + def __init__(self, device_identifier, address): # this information is from an IAmRequest - self.deviceIdentifier = None # device identifier - self.address = None # LocalStation or RemoteStation + self.deviceIdentifier = device_identifier + self.address = address + self.maxApduLengthAccepted = 1024 # maximum APDU device will accept self.segmentationSupported = 'noSegmentation' # normally no segmentation + self.maxSegmentsAccepted = None # None iff no segmentation self.vendorID = None # vendor identifier - - self.maxNpduLength = 1497 # maximum we can send in transit - self.maxSegmentsAccepted = None # value for proposed/actual window size + self.maxNpduLength = None # maximum we can send in transit (see 19.4) # # DeviceInfoCache @@ -72,127 +73,134 @@ def __init__(self): @bacpypes_debugging class DeviceInfoCache: - def __init__(self): + def __init__(self, device_info_class=DeviceInfo): if _debug: DeviceInfoCache._debug("__init__") + # a little error checking + if not issubclass(device_info_class, DeviceInfo): + raise ValueError("not a DeviceInfo subclass: %r" % (device_info_class,)) + # empty cache self.cache = {} + # class for new records + self.device_info_class = device_info_class + def has_device_info(self, key): """Return true iff cache has information about the device.""" if _debug: DeviceInfoCache._debug("has_device_info %r", key) return key in self.cache - def add_device_info(self, apdu): + def iam_device_info(self, apdu): """Create a device information record based on the contents of an IAmRequest and put it in the cache.""" - if _debug: DeviceInfoCache._debug("add_device_info %r", apdu) + if _debug: DeviceInfoCache._debug("iam_device_info %r", apdu) - # get the existing cache record by identifier - info = self.get_device_info(apdu.iAmDeviceIdentifier[1]) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # make sure the apdu is an I-Am + if not isinstance(apdu, IAmRequest): + raise ValueError("not an IAmRequest: %r" % (apdu,)) - # update existing record - if info: - if (info.address == apdu.pduSource): - return + # get the device instance + device_instance = apdu.iAmDeviceIdentifier[1] - info.address = apdu.pduSource - else: - # get the existing record by address (creates a new record) - info = self.get_device_info(apdu.pduSource) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # get the existing cache record if it exists + device_info = self.cache.get(device_instance, None) - info.deviceIdentifier = apdu.iAmDeviceIdentifier[1] + # maybe there is a record for this address + if not device_info: + device_info = self.cache.get(apdu.pduSource, None) - # update the rest of the values - info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted - info.segmentationSupported = apdu.segmentationSupported - info.vendorID = apdu.vendorID + # make a new one using the class provided + if not device_info: + device_info = self.device_info_class(device_instance, apdu.pduSource) - # say this is an updated record - self.update_device_info(info) + # jam in the correct values + device_info.deviceIdentifier = device_instance + device_info.address = apdu.pduSource + device_info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted + device_info.segmentationSupported = apdu.segmentationSupported + device_info.vendorID = apdu.vendorID + + # tell the cache this is an updated record + self.update_device_info(device_info) def get_device_info(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" if _debug: DeviceInfoCache._debug("get_device_info %r", key) - if isinstance(key, int): - current_info = self.cache.get(key, None) + # get the info if it's there + device_info = self.cache.get(key, None) + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - elif not isinstance(key, Address): - raise TypeError("key must be integer or an address") + return device_info - elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): - raise TypeError("address must be a local or remote station") - - else: - current_info = self.cache.get(key, None) - if not current_info: - current_info = DeviceInfo() - current_info.address = key - current_info._cache_keys = (None, key) - current_info._ref_count = 1 - - self.cache[key] = current_info - else: - if _debug: DeviceInfoCache._debug(" - reference bump") - current_info._ref_count += 1 - - if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) - - return current_info - - def update_device_info(self, info): + def update_device_info(self, device_info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" - if _debug: DeviceInfoCache._debug("update_device_info %r", info) + if _debug: DeviceInfoCache._debug("update_device_info %r", device_info) - cache_id, cache_address = info._cache_keys + # give this a reference count if it doesn't have one + if not hasattr(device_info, '_ref_count'): + device_info._ref_count = 0 - if (cache_id is not None) and (info.deviceIdentifier != cache_id): + # get the current keys + cache_id, cache_address = getattr(device_info, '_cache_keys', (None, None)) + + if (cache_id is not None) and (device_info.deviceIdentifier != cache_id): if _debug: DeviceInfoCache._debug(" - device identifier updated") # remove the old reference, add the new one del self.cache[cache_id] - self.cache[info.deviceIdentifier] = info - - cache_id = info.deviceIdentifier + self.cache[device_info.deviceIdentifier] = device_info - if (cache_address is not None) and (info.address != cache_address): + if (cache_address is not None) and (device_info.address != cache_address): if _debug: DeviceInfoCache._debug(" - device address updated") # remove the old reference, add the new one del self.cache[cache_address] - self.cache[info.address] = info - - cache_address = info.address + self.cache[device_info.address] = device_info # update the keys - info._cache_keys = (cache_id, cache_address) + device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) + + def acquire(self, key): + """Return the known information about the device and mark the record + as being used by a segmenation state machine.""" + if _debug: DeviceInfoCache._debug("acquire %r", key) + + if isinstance(key, int): + device_info = self.cache.get(key, None) + + elif not isinstance(key, Address): + raise TypeError("key must be integer or an address") + + elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): + raise TypeError("address must be a local or remote station") + + else: + device_info = self.cache.get(key, None) + + if device_info: + if _debug: DeviceInfoCache._debug(" - reference bump") + device_info._ref_count += 1 + + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - def release_device_info(self, info): + return device_info + + def release(self, device_info): """This function is called by the segmentation state machine when it has finished with the device information.""" - if _debug: DeviceInfoCache._debug("release_device_info %r", info) + if _debug: DeviceInfoCache._debug("release %r", device_info) # this information record might be used by more than one SSM - if info._ref_count > 1: - if _debug: DeviceInfoCache._debug(" - multiple references") - info._ref_count -= 1 - return + if device_info._ref_count == 0: + raise RuntimeError("reference count") - cache_id, cache_address = info._cache_keys - if cache_id is not None: - del self.cache[cache_id] - if cache_address is not None: - del self.cache[cache_address] - if _debug: DeviceInfoCache._debug(" - released") + # decrement the reference count + device_info._ref_count -= 1 # # Application @@ -260,9 +268,9 @@ def add_object(self, obj): # make sure it hasn't already been defined if object_name in self.objectName: - raise RuntimeError("already an object with name {0!r}".format(object_name)) + raise RuntimeError("already an object with name %r" % (object_name,)) if object_identifier in self.objectIdentifier: - raise RuntimeError("already an object with identifier {0!r}".format(object_identifier)) + raise RuntimeError("already an object with identifier %r" % (object_identifier,)) # now put it in local dictionaries self.objectName[object_name] = obj diff --git a/py27/bacpypes/appservice.py b/py27/bacpypes/appservice.py index 4a3b65da..641e6554 100755 --- a/py27/bacpypes/appservice.py +++ b/py27/bacpypes/appservice.py @@ -12,12 +12,14 @@ from .task import OneShotTask from .pdu import Address -from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \ +from .apdu import encode_max_segments_accepted, decode_max_segments_accepted, \ + encode_max_apdu_length_accepted, decode_max_apdu_length_accepted, \ + AbortPDU, AbortReason, ComplexAckPDU, \ ConfirmedRequestPDU, Error, ErrorPDU, RejectPDU, SegmentAckPDU, \ SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \ unconfirmed_request_types, confirmed_request_types, complex_ack_types, \ error_types -from .errors import RejectException, AbortException +from .errors import RejectException, AbortException, UnrecognizedService # some debugging _debug = 0 @@ -45,19 +47,23 @@ class SSM(OneShotTask, DebugContents): , 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED' ] - _debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID' + _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' ) - def __init__(self, sap, remoteDevice): + def __init__(self, sap, pdu_address): """Common parts for client and server segmentation.""" - if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice) + if _debug: SSM._debug("__init__ %r %r", sap, pdu_address) OneShotTask.__init__(self) self.ssmSAP = sap # service access point - self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance + + # save the address and get the device information + self.pdu_address = pdu_address + self.device_info = sap.deviceInfoCache.get_device_info(pdu_address) + self.invokeID = None # invoke ID self.state = IDLE # initial state @@ -71,11 +77,17 @@ def __init__(self, sap, remoteDevice): self.lastSequenceNumber = None self.initialSequenceNumber = None self.actualWindowSize = None - self.proposedWindowSize = None - # the maximum number of segments starts out being what's in the SAP - # which is the defaults or values from the local device. - self.maxSegmentsAccepted = self.ssmSAP.maxSegmentsAccepted + # local device object provides these or SAP provides defaults, make + # copies here so they are consistent throughout the transaction but + # they could change from one transaction to the next + self.numberOfApduRetries = getattr(sap.localDevice, 'numberOfApduRetries', sap.numberOfApduRetries) + self.apduTimeout = getattr(sap.localDevice, 'apduTimeout', sap.apduTimeout) + + self.segmentationSupported = getattr(sap.localDevice, 'segmentationSupported', sap.segmentationSupported) + self.segmentTimeout = getattr(sap.localDevice, 'segmentTimeout', sap.segmentTimeout) + self.maxSegmentsAccepted = getattr(sap.localDevice, 'maxSegmentsAccepted', sap.maxSegmentsAccepted) + self.maxApduLengthAccepted = getattr(sap.localDevice, 'maxApduLengthAccepted', sap.maxApduLengthAccepted) def start_timer(self, msecs): if _debug: SSM._debug("start_timer %r", msecs) @@ -146,19 +158,19 @@ def get_segment(self, indx): # check for invalid segment number if indx >= self.segmentCount: - raise RuntimeError("invalid segment number {0}, APDU has {1} segments".format(indx, self.segmentCount)) + raise RuntimeError("invalid segment number %r, APDU has %r segments" % (indx, self.segmentCount)) if self.segmentAPDU.apduType == ConfirmedRequestPDU.pduType: if _debug: SSM._debug(" - confirmed request context") segAPDU = ConfirmedRequestPDU(self.segmentAPDU.apduService) - segAPDU.apduMaxSegs = self.maxSegmentsAccepted - segAPDU.apduMaxResp = self.ssmSAP.maxApduLengthAccepted - segAPDU.apduInvokeID = self.invokeID; + segAPDU.apduMaxSegs = encode_max_segments_accepted(self.maxSegmentsAccepted) + segAPDU.apduMaxResp = encode_max_apdu_length_accepted(self.maxApduLengthAccepted) + segAPDU.apduInvokeID = self.invokeID # segmented response accepted? - segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth') + segAPDU.apduSA = self.segmentationSupported in ('segmentedReceive', 'segmentedBoth') if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA) elif self.segmentAPDU.apduType == ComplexAckPDU.pduType: @@ -172,14 +184,21 @@ def get_segment(self, indx): segAPDU.pduUserData = self.segmentAPDU.pduUserData # make sure the destination is set - segAPDU.pduDestination = self.remoteDevice.address + segAPDU.pduDestination = self.pdu_address # segmented message? if (self.segmentCount != 1): segAPDU.apduSeg = True segAPDU.apduMor = (indx < (self.segmentCount - 1)) # more follows segAPDU.apduSeq = indx % 256 # sequence number - segAPDU.apduWin = self.proposedWindowSize # window size + + # first segment sends proposed window size, rest get actual + if indx == 0: + if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) + segAPDU.apduWin = self.proposedWindowSize + else: + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) + segAPDU.apduWin = self.actualWindowSize else: segAPDU.apduSeg = False segAPDU.apduMor = False @@ -211,10 +230,11 @@ def in_window(self, seqA, seqB): return rslt - def FillWindow(self, seqNum): + def fill_window(self, seqNum): """This function sends all of the packets necessary to fill out the segmentation window.""" - if _debug: SSM._debug("FillWindow %r", seqNum) + if _debug: SSM._debug("fill_window %r", seqNum) + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) for ix in range(self.actualWindowSize): apdu = self.get_segment(seqNum + ix) @@ -234,13 +254,18 @@ def FillWindow(self, seqNum): @bacpypes_debugging class ClientSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ClientSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) # initialize the retry count self.retryCount = 0 + # acquire the device info + if self.device_info: + if _debug: ClientSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) + def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" if _debug: ClientSSM._debug("set_state %r (%s) timer=%r", newState, SSM.transactionLabels[newState], timer) @@ -253,8 +278,10 @@ def set_state(self, newState, timer=0): if _debug: ClientSSM._debug(" - remove from active transactions") self.ssmSAP.clientTransactions.remove(self) - if _debug: ClientSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by client transaction functions when it wants @@ -263,7 +290,7 @@ def request(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -280,26 +307,27 @@ def indication(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit, - # the maximum conveyable by the internetwork to the remote device, and - # the maximum APDU size accepted by the remote device. - self.segmentSize = min( - self.ssmSAP.maxApduLengthAccepted, - self.remoteDevice.maxNpduLength, - self.remoteDevice.maxApduLengthAccepted, - ) - if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) + # if the max apdu length of the server isn't known, assume that it + # is the same size as our own and will be the segment size + if (not self.device_info) or (self.device_info.maxApduLengthAccepted is None): + self.segmentSize = self.maxApduLengthAccepted - # the maximum number of segments acceptable in the reply - if apdu.apduMaxSegs is not None: - # this request overrides the default - self.maxSegmentsAccepted = apdu.apduMaxSegs + # if the max npdu length of the server isn't known, assume that it + # is the same as the max apdu length accepted + elif self.device_info.maxNpduLength is None: + self.segmentSize = self.device_info.maxApduLengthAccepted + + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the server and the largest it can accept + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.device_info.maxApduLengthAccepted) + if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) # save the invoke ID self.invokeID = apdu.apduInvokeID if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -312,34 +340,49 @@ def indication(self, apdu): # make sure we support segmented transmit if we need to if self.segmentCount > 1: - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't send segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): - if _debug: ClientSSM._debug(" - remote device can't receive segmented requests") + + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for segmentation support") + + elif self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if _debug: ClientSSM._debug(" - server can't receive segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our request + # that the server said it was willing to accept + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for maximum number of segments") + + elif not self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server doesn't say maximum number of segments") + + elif self.segmentCount > self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # send out the first segment (or the whole thing) if self.segmentCount == 1: - # SendConfirmedUnsegmented + # unsegmented self.sentAllSegments = True self.retryCount = 0 - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) else: - # SendConfirmedSegmented + # segmented self.sentAllSegments = False self.retryCount = 0 self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.actualWindowSize = None # segment ack will set value + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # deliver to the device self.request(self.get_segment(0)) @@ -350,7 +393,7 @@ def response(self, apdu): if _debug: ClientSSM._debug("response %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it to the application @@ -407,29 +450,31 @@ def segmented_request(self, apdu): and receives an apdu.""" if _debug: ClientSSM._debug("segmented_request %r", apdu) - # client is ready for the next segment + # server is ready for the next segment if apdu.apduType == SegmentAckPDU.pduType: if _debug: ClientSSM._debug(" - segment ack") + # actual window size is provided by server + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ClientSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: if _debug: ClientSSM._debug(" - all done sending request") - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) # more segments to send else: if _debug: ClientSSM._debug(" - more segments to send") self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 - self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # simple ack elif (apdu.apduType == SimpleAckPDU.pduType): @@ -452,6 +497,7 @@ def segmented_request(self, apdu): self.response(abort) # send it to the application elif not apdu.apduSeg: + # ack is not segmented self.set_state(COMPLETED) self.response(apdu) @@ -459,10 +505,11 @@ def segmented_request(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # minimum of what the server is proposing and this client proposes + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # some kind of problem elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType): @@ -479,12 +526,12 @@ def segmented_request_timeout(self): if _debug: ClientSSM._debug("segmented_request_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: if _debug: ClientSSM._debug(" - retry segmented request") self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: if _debug: ClientSSM._debug(" - abort, no response from the device") @@ -516,7 +563,7 @@ def await_confirmation(self, apdu): self.set_state(COMPLETED) self.response(apdu) - elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + elif self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't receive segmented messages") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) @@ -527,10 +574,10 @@ def await_confirmation(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + self.actualWindowSize = apdu.apduWin self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # send back a segment ack segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) @@ -546,7 +593,7 @@ def await_confirmation(self, apdu): elif (apdu.apduType == SegmentAckPDU.pduType): if _debug: ClientSSM._debug(" - segment ack(!?)") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) else: raise RuntimeError("invalid APDU (3)") @@ -554,9 +601,9 @@ def await_confirmation(self, apdu): def await_confirmation_timeout(self): if _debug: ClientSSM._debug("await_confirmation_timeout") - self.retryCount += 1 - if self.retryCount < self.ssmSAP.retryCount: - if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.ssmSAP.retryCount) + if self.retryCount < self.numberOfApduRetries: + if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.numberOfApduRetries) + self.retryCount += 1 # save the retry count, indication acts like the request is coming # from the application so the retryCount gets re-initialized. @@ -594,8 +641,8 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - segment %s received out of order, should be %s", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) return @@ -610,7 +657,7 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - no more follows") # send a final ack - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) self.set_state(COMPLETED) @@ -620,15 +667,15 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) else: # wait for more segments if _debug: ClientSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_confirmation_timeout(self): if _debug: ClientSSM._debug("segmented_confirmation_timeout") @@ -643,9 +690,14 @@ def segmented_confirmation_timeout(self): @bacpypes_debugging class ServerSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ServerSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) + + # acquire the device info + if self.device_info: + if _debug: ServerSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" @@ -659,8 +711,10 @@ def set_state(self, newState, timer=0): if _debug: ServerSSM._debug(" - remove from active transactions") self.ssmSAP.serverTransactions.remove(self) - if _debug: ServerSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by transaction functions to send @@ -668,7 +722,7 @@ def request(self, apdu): if _debug: ServerSSM._debug("request %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it via the device @@ -697,7 +751,7 @@ def response(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -739,14 +793,15 @@ def confirmation(self, apdu): # save the response and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit - # (assumed to have no local buffer limitations), the maximum conveyable - # by the internetwork to the remote device, and the maximum APDU size - # accepted by the remote device. - self.segmentSize = min(self.remoteDevice.maxNpduLength, self.remoteDevice.maxApduLengthAccepted) + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the client and the largest it can accept + if (not self.device_info) or (self.device_info.maxNpduLength is None): + self.segmentSize = self.maxApduLengthAccepted + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.maxApduLengthAccepted) if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -762,26 +817,31 @@ def confirmation(self, apdu): if _debug: ServerSSM._debug(" - segmentation required, %d segments", self.segmentCount) # make sure we support segmented transmit - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ServerSSM._debug(" - server can't send segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # make sure client supports segmented receive - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ServerSSM._debug(" - client can't receive segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our response + # that the device said it was willing to accept in the request + if self.segmentCount > self.maxSegmentsAccepted: + if _debug: ServerSSM._debug(" - client can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # initialize the state self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 + self.actualWindowSize = None # send out the first segment (or the whole thing) if self.segmentCount == 1: @@ -789,7 +849,7 @@ def confirmation(self, apdu): self.set_state(COMPLETED) else: self.response(self.get_segment(0)) - self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_RESPONSE, self.segmentTimeout) else: raise RuntimeError("invalid APDU (4)") @@ -837,38 +897,48 @@ def idle(self, apdu): self.invokeID = apdu.apduInvokeID if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID) - # make sure the device information is synced with the request if apdu.apduSA: - if self.remoteDevice.segmentationSupported == 'noSegmentation': + if not self.device_info: + if _debug: ServerSSM._debug(" - no client device info") + + elif self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") - self.remoteDevice.segmentationSupported = 'segmentedReceive' + self.device_info.segmentationSupported = 'segmentedReceive' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedTransmit': + elif self.device_info.segmentationSupported == 'segmentedTransmit': if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive") - self.remoteDevice.segmentationSupported = 'segmentedBoth' + self.device_info.segmentationSupported = 'segmentedBoth' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedReceive': + elif self.device_info.segmentationSupported == 'segmentedReceive': pass - elif self.remoteDevice.segmentationSupported == 'segmentedBoth': + elif self.device_info.segmentationSupported == 'segmentedBoth': pass else: raise RuntimeError("invalid segmentation supported in device info") - if apdu.apduMaxSegs != self.remoteDevice.maxSegmentsAccepted: - if _debug: ServerSSM._debug(" - update maximum segments accepted?") - if apdu.apduMaxResp != self.remoteDevice.maxApduLengthAccepted: - if _debug: ServerSSM._debug(" - update maximum max APDU length accepted?") + # decode the maximum that the client can receive in one APDU, and if + # there is a value in the device information then use that one because + # it came from reading device object property value or from an I-Am + # message that was received + self.maxApduLengthAccepted = decode_max_apdu_length_accepted(apdu.apduMaxResp) + if self.device_info and self.device_info.maxApduLengthAccepted is not None: + if self.device_info.maxApduLengthAccepted < self.maxApduLengthAccepted: + if _debug: ServerSSM._debug(" - apduMaxResp encoding error") + else: + self.maxApduLengthAccepted = self.device_info.maxApduLengthAccepted + if _debug: ServerSSM._debug(" - maxApduLengthAccepted: %r", self.maxApduLengthAccepted) - # save the number of segments the client is willing to accept in the ack - self.maxSegmentsAccepted = apdu.apduMaxSegs + # save the number of segments the client is willing to accept in the ack, + # if this is None then the value is unknown or more than 64 + self.maxSegmentsAccepted = decode_max_segments_accepted(apdu.apduMaxSegs) # unsegmented request if not apdu.apduSeg: @@ -877,7 +947,7 @@ def idle(self, apdu): return # make sure we support segmented requests - if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return @@ -885,17 +955,18 @@ def idle(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the window size is the minimum of what I'm willing to receive and - # what the device has said it would like to send - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # the window size is the minimum of what I would propose and what the + # device has proposed + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + if _debug: ServerSSM._debug(" - actualWindowSize? min(%r, %r) -> %r", apdu.apduWin, self.proposedWindowSize, self.actualWindowSize) # initialize the state self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) if _debug: ServerSSM._debug(" - segAck: %r", segack) self.response(segack) @@ -928,10 +999,10 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - segment %d received out of order, should be %d", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) return @@ -947,7 +1018,7 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - no more follows") # send back a final segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.response(segack) # forward the whole thing to the application @@ -958,17 +1029,17 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) else: # wait for more segments if _debug: ServerSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_request_timeout(self): if _debug: ServerSSM._debug("segmented_request_timeout") @@ -1008,10 +1079,13 @@ def segmented_response(self, apdu): if (apdu.apduType == SegmentAckPDU.pduType): if _debug: ServerSSM._debug(" - segment ack") + # actual window size is provided by client + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ServerSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: @@ -1024,8 +1098,8 @@ def segmented_response(self, apdu): self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # some kind of problem elif (apdu.apduType == AbortPDU.pduType): @@ -1039,10 +1113,10 @@ def segmented_response_timeout(self): if _debug: ServerSSM._debug("segmented_response_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: # give up self.set_state(ABORTED) @@ -1062,6 +1136,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): ServiceAccessPoint.__init__(self, sap) # save a reference to the device information cache + self.localDevice = localDevice self.deviceInfoCache = deviceInfoCache # client settings @@ -1072,27 +1147,19 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): self.serverTransactions = [] # confirmed request defaults - self.retryCount = 3 - self.retryTimeout = 3000 + self.numberOfApduRetries = 3 + self.apduTimeout = 3000 self.maxApduLengthAccepted = 1024 # segmentation defaults self.segmentationSupported = 'noSegmentation' self.segmentTimeout = 1500 - self.maxSegmentsAccepted = 8 + self.maxSegmentsAccepted = 2 + self.proposedWindowSize = 2 # device communication control self.dccEnableDisable = 'enable' - # local device object provides these - if localDevice: - self.retryCount = localDevice.numberOfApduRetries - self.retryTimeout = localDevice.apduTimeout - self.segmentationSupported = localDevice.segmentationSupported - self.segmentTimeout = localDevice.apduSegmentTimeout - self.maxSegmentsAccepted = localDevice.maxSegmentsAccepted - self.maxApduLengthAccepted = localDevice.maxApduLengthAccepted - # how long the state machine is willing to wait for the application # layer to form a response and send it self.applicationTimeout = 3000 @@ -1111,7 +1178,7 @@ def get_next_invoke_id(self, addr): raise RuntimeError("no available invoke ID") for tr in self.clientTransactions: - if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address): + if (invokeID == tr.invokeID) and (addr == tr.pdu_address): break else: break @@ -1152,14 +1219,11 @@ def confirmation(self, pdu): if isinstance(apdu, ConfirmedRequestPDU): # find duplicates of this request for tr in self.serverTransactions: - if (apdu.pduSource == tr.remoteDevice.address) and (apdu.apduInvokeID == tr.invokeID): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource) - # build a server transaction - tr = ServerSSM(self, remoteDevice) + tr = ServerSSM(self, apdu.pduSource) # add it to our transactions to track it self.serverTransactions.append(tr) @@ -1178,7 +1242,7 @@ def confirmation(self, pdu): # find the client transaction this is acking for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1190,7 +1254,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1199,7 +1263,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1211,7 +1275,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1220,7 +1284,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1264,19 +1328,15 @@ def sap_indication(self, apdu): else: # verify the invoke ID isn't already being used for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): raise RuntimeError("invoke ID in use") # warning for bogus requests if (apdu.pduDestination.addrType != Address.localStationAddr) and (apdu.pduDestination.addrType != Address.remoteStationAddr): StateMachineAccessPoint._warning("%s is not a local or remote station", apdu.pduDestination) - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduDestination) - if _debug: StateMachineAccessPoint._debug(" - remoteDevice: %r", remoteDevice) - # create a client transaction state machine - tr = ClientSSM(self, remoteDevice) + tr = ClientSSM(self, apdu.pduDestination) if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr) # add it to our transactions to track it @@ -1300,7 +1360,7 @@ def sap_confirmation(self, apdu): or isinstance(apdu, AbortPDU): # find the appropriate server transaction for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): break else: return @@ -1327,23 +1387,26 @@ def indication(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu) if isinstance(apdu, ConfirmedRequestPDU): + # assume no errors found + error_found = None + + # look up the class associated with the service atype = confirmed_request_types.get(apdu.apduService) if not atype: if _debug: ApplicationServiceAccessPoint._debug(" - no confirmed request decoder") - return - - # assume no errors found - error_found = None + error_found = UnrecognizedService() - try: - xpdu = atype() - xpdu.decode(apdu) - except RejectException as err: - ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) - error_found = err - except AbortException as err: - ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) - error_found = err + # no error so far, keep going + if not error_found: + try: + xpdu = atype() + xpdu.decode(apdu) + except RejectException as err: + ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) + error_found = err + except AbortException as err: + ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) + error_found = err # no error so far, keep going if not error_found: diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index f687d365..8cc4ab88 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -542,34 +542,38 @@ def confirmation(self, pdu): return - elif isinstance(pdu, OriginalUnicastNPDU): + if isinstance(pdu, OriginalUnicastNPDU): # build a vanilla PDU xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) # send it upstream self.response(xpdu) - return - - # check the BBMD registration status, we may not be registered - if self.registrationStatus != 0: - if _debug: BIPForeign._debug(" - packet dropped, unregistered") - return - if isinstance(pdu, ReadBroadcastDistributionTableAck): - # send this to the service access point - self.sap_response(pdu) + elif isinstance(pdu, ForwardedNPDU): + # check the BBMD registration status, we may not be registered + if self.registrationStatus != 0: + if _debug: BIPForeign._debug(" - packet dropped, unregistered") + return - elif isinstance(pdu, ReadForeignDeviceTableAck): - # send this to the service access point - self.sap_response(pdu) + # make sure the forwarded PDU from the bbmd + if pdu.pduSource != self.bbmdAddress: + if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") + return - elif isinstance(pdu, ForwardedNPDU): # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) # send it upstream self.response(xpdu) + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response xpdu = Result(code=0x0010, user_data=pdu.pduUserData) @@ -618,6 +622,9 @@ def confirmation(self, pdu): # send it downstream self.request(xpdu) + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPForeign._debug(" - packet dropped") + else: BIPForeign._warning("invalid pdu type: %s", type(pdu)) @@ -692,7 +699,7 @@ def indication(self, pdu): # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduDestination - if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) + if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -715,13 +722,13 @@ def indication(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -737,8 +744,9 @@ def confirmation(self, pdu): elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response - xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu = Result(code=0x0010, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduSource + if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -757,27 +765,38 @@ def confirmation(self, pdu): self.sap_response(pdu) elif isinstance(pdu, ForwardedNPDU): - # build a PDU with the source from the real source - xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) - # look for self as first entry in the BDT - if self.bbmdBDT and (self.bbmdBDT[0] == self.bbmdAddress): - xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") - self.request(xpdu) + # if this was unicast to us, do next hop + if pdu.pduDestination.addrType == Address.localStationAddr: + if _debug: BIPBBMD._debug(" - unicast message") + + # if this BBMD is listed in its BDT, send a local broadcast + if self.bbmdAddress in self.bbmdBDT: + xpdu.pduDestination = LocalBroadcast() + if _debug: BIPBBMD._debug(" - local broadcast") + self.request(xpdu) + + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + if _debug: BIPBBMD._debug(" - directed broadcast message") + + else: + BIPBBMD._warning("invalid destination address: %r", pdu.pduDestination) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, RegisterForeignDevice): @@ -816,12 +835,13 @@ def confirmation(self, pdu): self.request(xpdu) elif isinstance(pdu, DistributeBroadcastToNetwork): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -831,35 +851,37 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte == self.bbmdAddress: xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") + if _debug: BIPBBMD._debug(" - local broadcast") self.request(xpdu) else: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the other registered foreign devices for fdte in self.bbmdFDT: if fdte.fdAddress != pdu.pduSource: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) elif isinstance(pdu, OriginalBroadcastNPDU): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # make a forwarded PDU xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -869,13 +891,13 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -925,7 +947,7 @@ def delete_foreign_device_table_entry(self, addr): del self.bbmdFDT[i] break else: - stat = 99 ### entry not found + stat = 0x0050 ### entry not found # return status return stat @@ -952,10 +974,6 @@ def add_peer(self, addr): else: raise TypeError("addr must be a string or an Address") - # if it's this BBMD, make it the first one - if self.bbmdBDT and (addr == self.bbmdAddress): - raise RuntimeError("add self to BDT as first address") - # see if it's already there for bdte in self.bbmdBDT: if addr == bdte: diff --git a/py27/bacpypes/constructeddata.py b/py27/bacpypes/constructeddata.py index c8eb755b..bad87d0d 100755 --- a/py27/bacpypes/constructeddata.py +++ b/py27/bacpypes/constructeddata.py @@ -145,6 +145,7 @@ def decode(self, taglist): for element in self.sequenceElements: tag = taglist.Peek() + if _debug: Sequence._debug(" - element, tag: %r, %r", element, tag) # no more elements if tag is None: @@ -191,7 +192,29 @@ def decode(self, taglist): if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) - # check for an atomic element + # check for an any atomic element + elif issubclass(element.klass, AnyAtomic): + # convert it to application encoding + if element.context is not None: + raise InvalidTag("%s any atomic with context tag %d" % (element.name, element.context)) + + if tag.tagClass != Tag.applicationTagClass: + if not element.optional: + raise InvalidParameterDatatype("%s expected any atomic application tag" % (element.name,)) + else: + setattr(self, element.name, None) + continue + + # consume the tag + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = element.klass(tag) + + # now save the value + setattr(self, element.name, helper.value) + + # check for specific kind of atomic element, or the context says what kind elif issubclass(element.klass, Atomic): # convert it to application encoding if element.context is not None: @@ -410,6 +433,9 @@ def __len__(self): def __getitem__(self, item): return self.value[item] + def __iter__(self): + return iter(self.value) + def encode(self, taglist): if _debug: _SequenceOf._debug("(%r)encode %r", self.__class__.__name__, taglist) for value in self.value: @@ -749,6 +775,9 @@ def __delitem__(self, item): del self.value[item] self.value[0] -= 1 + def __iter__(self): + return iter(self.value[1:]) + def index(self, value): # only search through values for i in range(1, self.value[0] + 1): @@ -1311,7 +1340,7 @@ def dict_contents(self, use_dict=None, as_class=dict): # @bacpypes_debugging -class AnyAtomic: +class AnyAtomic(Atomic): def __init__(self, arg=None): if _debug: AnyAtomic._debug("__init__ %r", arg) @@ -1342,8 +1371,13 @@ def decode(self, tag): # get the data self.value = tag.app_to_object() + @classmethod + def is_valid(cls, arg): + """Return True if arg is valid value for the class.""" + return isinstance(arg, Atomic) and not isinstance(arg, AnyAtomic) + def __str__(self): - return "AnyAtomic(%s)" % (str(self.value), ) + return "%s(%s)" % (self.__class__.__name__, str(self.value)) def __repr__(self): desc = self.__module__ + '.' + self.__class__.__name__ diff --git a/py27/bacpypes/local/__init__.py b/py27/bacpypes/local/__init__.py new file mode 100644 index 00000000..277c3c76 --- /dev/null +++ b/py27/bacpypes/local/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Local Object Subpackage +""" + +from . import object +from . import device +from . import file +from . import schedule + diff --git a/py27/bacpypes/local/device.py b/py27/bacpypes/local/device.py new file mode 100644 index 00000000..5256c28f --- /dev/null +++ b/py27/bacpypes/local/device.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger, xtob + +from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ + OctetString, CharacterString, BitString, Enumerated, Date, Time, \ + ObjectIdentifier +from ..constructeddata import ArrayOf +from ..basetypes import ServicesSupported + +from ..errors import ExecutionError +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentLocalDate +# + +class CurrentLocalDate(Property): + + def __init__(self): + Property.__init__(self, 'localDate', Date, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentLocalTime +# + +class CurrentLocalTime(Property): + + def __init__(self): + Property.__init__(self, 'localTime', Time, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentProtocolServicesSupported +# + +@bacpypes_debugging +class CurrentProtocolServicesSupported(Property): + + def __init__(self): + if _debug: CurrentProtocolServicesSupported._debug("__init__") + Property.__init__(self, 'protocolServicesSupported', ServicesSupported, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentProtocolServicesSupported._debug("ReadProperty %r %r", obj, arrayIndex) + + # not an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return what the application says + return obj._app.get_services_supported() + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# LocalDeviceObject +# + +@bacpypes_debugging +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): + + properties = [ + CurrentLocalTime(), + CurrentLocalDate(), + CurrentProtocolServicesSupported(), + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # start with an empty dictionary of device object properties + init_args = {} + ini_arg = kwargs.get('ini', None) + if _debug: LocalDeviceObject._debug(" - ini_arg: %r", dir(ini_arg)) + + # check for registration as a keyword parameter or in the INI file + if self.__class__ not in registered_object_types.values(): + if _debug: LocalDeviceObject._debug(" - unregistered") + + vendor_identifier = kwargs.get('vendorIdentifier', None) + if _debug: LocalDeviceObject._debug(" - keyword vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + vendor_identifier = getattr(ini_arg, 'vendoridentifier', None) + if _debug: LocalDeviceObject._debug(" - INI vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + + register_object_type(self.__class__, vendor_id=vendor_identifier) + + # look for properties, fill in values from the keyword arguments or + # the INI parameter (converted to a proper value) if it was provided + for propid, prop in self._properties.items(): + # special processing for object identifier + if propid == 'objectIdentifier': + continue + + # use keyword argument if it was provided + if propid in kwargs: + prop_value = kwargs[propid] + else: + prop_value = getattr(ini_arg, propid.lower(), None) + if prop_value is None: + continue + + prop_datatype = prop.datatype + + if issubclass(prop_datatype, Null): + if prop_value != "Null": + raise ValueError("invalid null property value: %r" % (propid,)) + prop_value = None + + elif issubclass(prop_datatype, Boolean): + prop_value = prop_value.lower() + if prop_value not in ('true', 'false', 'set', 'reset'): + raise ValueError("invalid boolean property value: %r" % (propid,)) + prop_value = prop_value in ('true', 'set') + + elif issubclass(prop_datatype, (Unsigned, Integer)): + try: + prop_value = int(prop_value) + except ValueError: + raise ValueError("invalid unsigned or integer property value: %r" % (propid,)) + + elif issubclass(prop_datatype, (Real, Double)): + try: + prop_value = float(prop_value) + except ValueError: + raise ValueError("invalid real or double property value: %r" % (propid,)) + + elif issubclass(prop_datatype, OctetString): + try: + prop_value = xtob(prop_value) + except: + raise ValueError("invalid octet string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, CharacterString): + pass + + elif issubclass(prop_datatype, BitString): + try: + bstr, prop_value = prop_value, [] + for b in bstr: + if b not in ('0', '1'): + raise ValueError + prop_value.append(int(b)) + except: + raise ValueError("invalid bit string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, Enumerated): + pass + + else: + raise ValueError("cannot interpret %r INI paramter" % (propid,)) + if _debug: LocalDeviceObject._debug(" - property %r: %r", propid, prop_value) + + # at long last + init_args[propid] = prop_value + + # check for object identifier as a keyword parameter or in the INI file, + # and it might be just an int, so make it a tuple if necessary + if 'objectIdentifier' in kwargs: + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + elif hasattr(ini_arg, 'objectidentifier'): + object_identifier = ('device', int(getattr(ini_arg, 'objectidentifier'))) + else: + raise RuntimeError("objectIdentifier is required") + init_args['objectIdentifier'] = object_identifier + if _debug: LocalDeviceObject._debug(" - object identifier: %r", object_identifier) + + # fill in default property values not in init_args + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in init_args: + init_args[attr] = value + + # check for properties this class implements + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + if 'protocolServicesSupported' in kwargs: + raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + init_args['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + + # check for a minimum value + if init_args['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - init_args: %r", init_args) + + # proceed as usual + super(LocalDeviceObject, self).__init__(**init_args) + + # pass along special property values that are not BACnet properties + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) + diff --git a/py27/bacpypes/local/file.py b/py27/bacpypes/local/file.py new file mode 100644 index 00000000..8007bef4 --- /dev/null +++ b/py27/bacpypes/local/file.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +@bacpypes_debugging +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +# +# Local Stream Access File Object Type +# + +@bacpypes_debugging +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + diff --git a/py27/bacpypes/local/object.py b/py27/bacpypes/local/object.py new file mode 100644 index 00000000..3078b9c6 --- /dev/null +++ b/py27/bacpypes/local/object.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..basetypes import PropertyIdentifier +from ..constructeddata import ArrayOf + +from ..errors import ExecutionError +from ..object import Property, Object + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +@bacpypes_debugging +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + diff --git a/py27/bacpypes/local/schedule.py b/py27/bacpypes/local/schedule.py new file mode 100644 index 00000000..91cbee62 --- /dev/null +++ b/py27/bacpypes/local/schedule.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..core import deferred +from ..task import OneShotTask + +from ..primitivedata import Atomic, Null, Unsigned, Date, Time +from ..constructeddata import Array +from ..object import get_datatype, ScheduleObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# match_date +# + +def match_date(date, date_pattern): + """ + Match a specific date, a four-tuple with no special values, with a date + pattern, four-tuple possibly having special values. + """ + # unpack the date and pattern + year, month, day, day_of_week = date + year_p, month_p, day_p, day_of_week_p = date_pattern + + # check the year + if year_p == 255: + # any year + pass + elif year != year_p: + # specific year + return False + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the day + if day_p == 255: + # any day + pass + elif day_p == 32: + # last day of the month + last_day = calendar.monthrange(year + 1900, month)[1] + if day != last_day: + return False + elif day_p == 33: + # odd days of the month + if (day % 2) == 0: + return False + elif day_p == 34: + # even days of the month + if (day % 2) == 1: + return False + elif day != day_p: + # specific day + return False + + # check the day of week + if day_of_week_p == 255: + # any day of the week + pass + elif day_of_week != day_of_week_p: + # specific day of the week + return False + + # all tests pass + return True + +# +# match_date_range +# + +def match_date_range(date, date_range): + """ + Match a specific date, a four-tuple with no special values, with a DateRange + object which as a start date and end date. + """ + return (date[:3] >= date_range.startDate[:3]) \ + and (date[:3] <= date_range.endDate[:3]) + +# +# match_weeknday +# + +def match_weeknday(date, weeknday): + """ + Match a specific date, a four-tuple with no special values, with a + BACnetWeekNDay, an octet string with three (unsigned) octets. + """ + # unpack the date + year, month, day, day_of_week = date + last_day = calendar.monthrange(year + 1900, month)[1] + + # unpack the date pattern octet string + weeknday_unpacked = [ord(c) for c in weeknday] + month_p, week_of_month_p, day_of_week_p = weeknday_unpacked + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the week of the month + if week_of_month_p == 255: + # any week + pass + elif week_of_month_p == 1: + # days numbered 1-7 + if (day > 7): + return False + elif week_of_month_p == 2: + # days numbered 8-14 + if (day < 8) or (day > 14): + return False + elif week_of_month_p == 3: + # days numbered 15-21 + if (day < 15) or (day > 21): + return False + elif week_of_month_p == 4: + # days numbered 22-28 + if (day < 22) or (day > 28): + return False + elif week_of_month_p == 5: + # days numbered 29-31 + if (day < 29) or (day > 31): + return False + elif week_of_month_p == 6: + # last 7 days of this month + if (day < last_day - 6): + return False + elif week_of_month_p == 7: + # any of the 7 days prior to the last 7 days of this month + if (day < last_day - 13) or (day > last_day - 7): + return False + elif week_of_month_p == 8: + # any of the 7 days prior to the last 14 days of this month + if (day < last_day - 20) or (day > last_day - 14): + return False + elif week_of_month_p == 9: + # any of the 7 days prior to the last 21 days of this month + if (day < last_day - 27) or (day > last_day - 21): + return False + + # check the day + if day_of_week_p == 255: + # any day + pass + elif day_of_week != day_of_week_p: + # specific day + return False + + # all tests pass + return True + +# +# date_in_calendar_entry +# + +@bacpypes_debugging +def date_in_calendar_entry(date, calendar_entry): + if _debug: date_in_calendar_entry._debug("date_in_calendar_entry %r %r", date, calendar_entry) + + match = False + if calendar_entry.date: + match = match_date(date, calendar_entry.date) + elif calendar_entry.dateRange: + match = match_date_range(date, calendar_entry.dateRange) + elif calendar_entry.weekNDay: + match = match_weeknday(date, calendar_entry.weekNDay) + else: + raise RuntimeError("") + if _debug: date_in_calendar_entry._debug(" - match: %r", match) + + return match + +# +# datetime_to_time +# + +def datetime_to_time(date, time): + """Take the date and time 4-tuples and return the time in seconds since + the epoch as a floating point number.""" + if (255 in date) or (255 in time): + raise RuntimeError("specific date and time required") + + time_tuple = ( + date[0]+1900, date[1], date[2], + time[0], time[1], time[2], + 0, 0, -1, + ) + return _mktime(time_tuple) + +# +# LocalScheduleObject +# + +@bacpypes_debugging +class LocalScheduleObject(CurrentPropertyListMixIn, ScheduleObject): + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + + # make sure present value was provided + if 'presentValue' not in kwargs: + raise RuntimeError("presentValue required") + if not isinstance(kwargs['presentValue'], Atomic): + raise TypeError("presentValue must be an Atomic value") + + # continue initialization + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors to check the reliability if these change + for prop in ('weeklySchedule', 'exceptionSchedule', 'scheduleDefault'): + self._property_monitors[prop].append(self._check_reliability) + + # check it now + self._check_reliability() + + def _check_reliability(self, old_value=None, new_value=None): + """This function is called when the object is created and after + one of its configuration properties has changed. The new and old value + parameters are ignored, this is called after the property has been + changed and this is only concerned with the current value.""" + if _debug: LocalScheduleObject._debug("_check_reliability %r %r", old_value, new_value) + + try: + schedule_default = self.scheduleDefault + + if schedule_default is None: + raise ValueError("scheduleDefault expected") + if not isinstance(schedule_default, Atomic): + raise TypeError("scheduleDefault must be an instance of an atomic type") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + # check the weekly schedule values + if self.weeklySchedule: + for daily_schedule in self.weeklySchedule: + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleObject._debug(" - daily time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + elif 255 in time_value.time: + if _debug: LocalScheduleObject._debug(" - wildcard in time") + raise ValueError("must be a specific time") + + # check the exception schedule values + if self.exceptionSchedule: + for special_event in self.exceptionSchedule: + for time_value in special_event.listOfTimeValues: + if _debug: LocalScheduleObject._debug(" - special event time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + + # check list of object property references + obj_prop_refs = self.listOfObjectPropertyReferences + if obj_prop_refs: + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + raise RuntimeError("no external references") + + # get the datatype of the property to be written + obj_type = obj_prop_ref.objectIdentifier[0] + datatype = get_datatype(obj_type, obj_prop_ref.propertyIdentifier) + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if issubclass(datatype, Array) and (obj_prop_ref.propertyArrayIndex is not None): + if obj_prop_ref.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if datatype is not schedule_datatype: + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + datatype, + schedule_datatype, + ) + raise TypeError("wrong type") + + # all good + self.reliability = 'noFaultDetected' + if _debug: LocalScheduleObject._debug(" - no fault detected") + + except Exception as err: + if _debug: LocalScheduleObject._debug(" - exception: %r", err) + self.reliability = 'configurationError' + +# +# LocalScheduleInterpreter +# + +@bacpypes_debugging +class LocalScheduleInterpreter(OneShotTask): + + def __init__(self, sched_obj): + if _debug: LocalScheduleInterpreter._debug("__init__ %r", sched_obj) + OneShotTask.__init__(self) + + # reference the schedule object to update + self.sched_obj = sched_obj + + # add a monitor for the present value + sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + + # call to interpret the schedule + deferred(self.process_task) + + def present_value_changed(self, old_value, new_value): + """This function is called when the presentValue of the local schedule + object has changed, both internally by this interpreter, or externally + by some client using WriteProperty.""" + if _debug: LocalScheduleInterpreter._debug("present_value_changed %s %s", old_value, new_value) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # process the list of [device] object property [array index] references + obj_prop_refs = self.sched_obj.listOfObjectPropertyReferences + if not obj_prop_refs: + if _debug: LocalScheduleInterpreter._debug(" - no writes defined") + return + + # primitive values just set the value part + new_value = new_value.value + + # loop through the writes + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + if _debug: LocalScheduleInterpreter._debug(" - no externals") + continue + + # get the object from the application + obj = self.sched_obj._app.get_object_id(obj_prop_ref.objectIdentifier) + if not obj: + if _debug: LocalScheduleInterpreter._debug(" - no object") + continue + + # try to change the value + try: + obj.WriteProperty( + obj_prop_ref.propertyIdentifier, + new_value, + arrayIndex=obj_prop_ref.propertyArrayIndex, + priority=self.sched_obj.priorityForWriting, + ) + if _debug: LocalScheduleInterpreter._debug(" - success") + except Exception as err: + if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + + def process_task(self): + if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) + + # check for a valid configuration + if self.sched_obj.reliability != 'noFaultDetected': + if _debug: LocalScheduleInterpreter._debug(" - fault detected") + return + + # get the date and time from the device object in case it provides + # some custom functionality + if self.sched_obj._app and self.sched_obj._app.localDevice: + current_date = self.sched_obj._app.localDevice.localDate + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = self.sched_obj._app.localDevice.localTime + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + else: + # get the current date and time, as provided by the task manager + current_date = Date().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = Time().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + + # evaluate the time + current_value, next_transition = self.eval(current_date, current_time) + if _debug: LocalScheduleInterpreter._debug(" - current_value, next_transition: %r, %r", current_value, next_transition) + + ### set the present value + self.sched_obj.presentValue = current_value + + # compute the time of the next transition + transition_time = datetime_to_time(current_date, next_transition) + + # install this to run again + self.install_task(transition_time) + + def eval(self, edate, etime): + """Evaluate the schedule according to the provided date and time and + return the appropriate present value, or None if not in the effective + period.""" + if _debug: LocalScheduleInterpreter._debug("eval %r %r", edate, etime) + + # reference the schedule object + sched_obj = self.sched_obj + if _debug: LocalScheduleInterpreter._debug(" sched_obj: %r", sched_obj) + + # verify the date falls in the effective period + if not match_date_range(edate, sched_obj.effectivePeriod): + return None + + # the event priority is a list of values that are in effect for + # exception schedules with the special event priority, see 135.1-2013 + # clause 7.3.2.23.10.3.8, Revision 4 Event Priority Test + event_priority = [None] * 16 + + next_day = (24, 0, 0, 0) + next_transition_time = [None] * 16 + + # check the exception schedule values + if sched_obj.exceptionSchedule: + for special_event in sched_obj.exceptionSchedule: + if _debug: LocalScheduleInterpreter._debug(" - special_event: %r", special_event) + + # check the special event period + special_event_period = special_event.period + if special_event_period is None: + raise RuntimeError("special event period required") + + match = False + calendar_entry = special_event_period.calendarEntry + if calendar_entry: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + else: + # get the calendar object from the application + calendar_object = sched_obj._app.get_object_id(special_event_period.calendarReference) + if not calendar_object: + raise RuntimeError("invalid calendar object reference") + if _debug: LocalScheduleInterpreter._debug(" - calendar_object: %r", calendar_object) + + for calendar_entry in calendar_object.dateList: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + if match: + break + + # didn't match the period, try the next special event + if not match: + if _debug: LocalScheduleInterpreter._debug(" - no matching calendar entry") + continue + + # event priority array index + priority = special_event.eventPriority - 1 + if _debug: LocalScheduleInterpreter._debug(" - priority: %r", priority) + + # look for all of the possible times + for time_value in special_event.listOfTimeValues: + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - relinquish exception @ %r", tval) + event_priority[priority] = None + next_transition_time[priority] = None + else: + if _debug: LocalScheduleInterpreter._debug(" - consider exception @ %r", tval) + event_priority[priority] = time_value.value + next_transition_time[priority] = next_day + else: + next_transition_time[priority] = tval + break + + # assume the next transition will be at the start of the next day + earliest_transition = next_day + + # check if any of the special events came up with something + for priority_value, next_transition in zip(event_priority, next_transition_time): + if next_transition is not None: + earliest_transition = min(earliest_transition, next_transition) + if priority_value is not None: + if _debug: LocalScheduleInterpreter._debug(" - priority_value: %r", priority_value) + return priority_value, earliest_transition + + # start out with the default + daily_value = sched_obj.scheduleDefault + + # check the daily schedule + if sched_obj.weeklySchedule: + daily_schedule = sched_obj.weeklySchedule[edate[3]] + if _debug: LocalScheduleInterpreter._debug(" - daily_schedule: %r", daily_schedule) + + # look for all of the possible times + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleInterpreter._debug(" - time_value: %r", time_value) + + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - back to normal @ %r", tval) + daily_value = sched_obj.scheduleDefault + else: + if _debug: LocalScheduleInterpreter._debug(" - new value @ %r", tval) + daily_value = time_value.value + else: + earliest_transition = min(earliest_transition, tval) + break + + # return what was matched, if anything + return daily_value, earliest_transition + diff --git a/py27/bacpypes/netservice.py b/py27/bacpypes/netservice.py index 859be42c..391abea7 100755 --- a/py27/bacpypes/netservice.py +++ b/py27/bacpypes/netservice.py @@ -27,32 +27,134 @@ ROUTER_UNREACHABLE = 3 # cannot route # -# NetworkReference +# RouterInfo # -class NetworkReference: - """These objects map a network to a router.""" +class RouterInfo(DebugContents): + """These objects are routing information records that map router + addresses with destination networks.""" - def __init__(self, net, router, status): - self.network = net - self.router = router - self.status = status + _debug_contents = ('snet', 'address', 'dnets', 'status') + + def __init__(self, snet, address, dnets, status=ROUTER_AVAILABLE): + self.snet = snet # source network + self.address = address # address of the router + self.dnets = dnets # list of reachable networks through this router + self.status = status # router status # -# RouterReference +# RouterInfoCache # -class RouterReference(DebugContents): - """These objects map a router; the adapter to talk to it, - its address, and a list of networks that it routes to.""" +@bacpypes_debugging +class RouterInfoCache: + + def __init__(self): + if _debug: RouterInfoCache._debug("__init__") + + self.routers = {} # (snet, address) -> RouterInfo + self.networks = {} # network -> RouterInfo + + def get_router_info(self, dnet): + if _debug: RouterInfoCache._debug("get_router_info %r", dnet) + + # check to see if we know about it + if dnet not in self.networks: + if _debug: RouterInfoCache._debug(" - no route") + return None + + # return the network and address + router_info = self.networks[dnet] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # return the network, address, and status + return (router_info.snet, router_info.address, router_info.status) + + def update_router_info(self, snet, address, dnets): + if _debug: RouterInfoCache._debug("update_router_info %r %r %r", snet, address, dnets) + + # look up the router reference, make a new record if necessary + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - new router") + router_info = self.routers[key] = RouterInfo(snet, address, list()) + else: + router_info = self.routers[key] + + # add (or move) the destination networks + for dnet in dnets: + if dnet in self.networks: + other_router = self.networks[dnet] + if other_router is router_info: + if _debug: RouterInfoCache._debug(" - existing router, match") + continue + elif dnet not in other_router.dnets: + if _debug: RouterInfoCache._debug(" - where did it go?") + else: + other_router.dnets.remove(dnet) + if not other_router.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[(snet, other_router.address)] + + # add a reference to the router + self.networks[dnet] = router_info + if _debug: RouterInfoCache._debug(" - reference added") + + # maybe update the list of networks for this router + if dnet not in router_info.dnets: + router_info.dnets.append(dnet) + if _debug: RouterInfoCache._debug(" - dnet added, now: %r", router_info.dnets) + + def update_router_status(self, snet, address, status): + if _debug: RouterInfoCache._debug("update_router_status %r %r %r", snet, address, status) + + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - not a router we care about") + return + + router_info = self.routers[key] + router_info.status = status + if _debug: RouterInfoCache._debug(" - status updated") + + def delete_router_info(self, snet, address=None, dnets=None): + if _debug: RouterInfoCache._debug("delete_router_info %r %r %r", dnets) + + # if address is None, remove all the routers for the network + if address is None: + for rnet, raddress in self.routers.keys(): + if snet == rnet: + if _debug: RouterInfoCache._debug(" - going down") + self.delete_router_info(snet, raddress) + if _debug: RouterInfoCache._debug(" - back topside") + return + + # look up the router reference + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - unknown router") + return + + router_info = self.routers[key] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) - _debug_contents = ('adapter-', 'address', 'networks', 'status') + # if dnets is None, remove all the networks for the router + if dnets is None: + dnets = router_info.dnets + + # loop through the list of networks to be deleted + for dnet in dnets: + if dnet in self.networks: + del self.networks[dnet] + if _debug: RouterInfoCache._debug(" - removed from networks: %r", dnet) + if dnet in router_info.dnets: + router_info.dnets.remove(dnet) + if _debug: RouterInfoCache._debug(" - removed from router_info: %r", dnet) - def __init__(self, adapter, addr, nets, status): - self.adapter = adapter - self.address = addr # local station relative to the adapter - self.networks = nets # list of remote networks - self.status = status # status as presented by the router + # see if we still care + if not router_info.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[key] # # NetworkAdapter @@ -64,14 +166,11 @@ class NetworkAdapter(Client, DebugContents): _debug_contents = ('adapterSAP-', 'adapterNet') def __init__(self, sap, net, cid=None): - if _debug: NetworkAdapter._debug("__init__ %r (net=%r) cid=%r", sap, net, cid) + if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) Client.__init__(self, cid) self.adapterSAP = sap self.adapterNet = net - # add this to the list of adapters for the network - sap.adapters.append(self) - def confirmation(self, pdu): """Decode upstream PDUs and pass them up to the service access point.""" if _debug: NetworkAdapter._debug("confirmation %r (net=%r)", pdu, self.adapterNet) @@ -105,117 +204,73 @@ class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): , 'localAdapter-', 'localAddress' ) - def __init__(self, sap=None, sid=None): + def __init__(self, routerInfoCache=None, sap=None, sid=None): if _debug: NetworkServiceAccessPoint._debug("__init__ sap=%r sid=%r", sap, sid) ServiceAccessPoint.__init__(self, sap) Server.__init__(self, sid) - self.adapters = [] # list of adapters - self.routers = {} # (adapter, address) -> RouterReference - self.networks = {} # network -> RouterReference + # map of directly connected networks + self.adapters = {} # net -> NetworkAdapter + + # use the provided cache or make a default one + self.router_info_cache = routerInfoCache or RouterInfoCache() + + # map to a list of application layer packets waiting for a path + self.pending_nets = {} - self.localAdapter = None # which one is local - self.localAddress = None # what is the local address + # these are set when bind() is called + self.local_adapter = None + self.local_address = None def bind(self, server, net=None, address=None): """Create a network adapter object and bind.""" if _debug: NetworkServiceAccessPoint._debug("bind %r net=%r address=%r", server, net, address) - if (net is None) and self.adapters: + # make sure this hasn't already been called with this network + if net in self.adapters: raise RuntimeError("already bound") - # create an adapter object + # when binding to an adapter and there is more than one, then they + # must all have network numbers and one of them will be the default + if (net is not None) and (None in self.adapters): + raise RuntimeError("default adapter bound") + + # create an adapter object, add it to our map adapter = NetworkAdapter(self, net) + self.adapters[net] = adapter + if _debug: NetworkServiceAccessPoint._debug(" - adapters[%r]: %r", net, adapter) # if the address was given, make it the "local" one if address: - self.localAdapter = adapter - self.localAddress = address + self.local_adapter = adapter + self.local_address = address # bind to the server bind(adapter, server) #----- - def add_router_references(self, adapter, address, netlist): + def add_router_references(self, snet, address, dnets): """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", adapter, address, netlist) - - # make a key for the router reference - rkey = (adapter, address) - - for snet in netlist: - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] - - if rref.adapter == adapter and rref.address == address: - pass # matches current entry - else: - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] - - # remove the network - del self.networks[snet] + if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) - ### check to see if it is OK to add the new entry + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - # get the router reference for this router - rref = self.routers.get(rkey, None) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # pass this along to the cache + self.router_info_cache.update_router_info(snet, address, dnets) - # reference the snet - self.networks[snet] = rref - else: - # new reference - rref = RouterReference( adapter, address, [snet], 0) - self.routers[rkey] = rref + def delete_router_references(self, snet, address=None, dnets=None): + """Delete references to routers/networks.""" + if _debug: NetworkServiceAccessPoint._debug("delete_router_references %r %r %r", snet, address, dnets) - # reference the snet - self.networks[snet] = rref + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - def remove_router_references(self, adapter, address=None): - """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("remove_router_references %r %r", adapter, address) - - delrlist = [] - delnlist = [] - # scan through the dictionary of router references - for rkey in self.routers.keys(): - # rip apart the key - radapter, raddress = rkey - - # pick all references on the adapter, optionally limited to a specific address - match = radapter is adapter - if match and address is not None: - match = (raddress == address) - if not match: - continue - - # save it for deletion - delrlist.append(rkey) - delnlist.extend(self.routers[rkey].networks) - if _debug: - NetworkServiceAccessPoint._debug(" - delrlist: %r", delrlist) - NetworkServiceAccessPoint._debug(" - delnlist: %r", delnlist) - - # delete the entries - for rkey in delrlist: - try: - del self.routers[rkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - rkey not in self.routers: %r", rkey) - for nkey in delnlist: - try: - del self.networks[nkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - nkey not in self.networks: %r", rkey) + # pass this along to the cache + self.router_info_cache.delete_router_info(snet, address, dnets) #----- @@ -227,11 +282,12 @@ def indication(self, pdu): raise ConfigurationError("no adapters") # might be able to relax this restriction - if (len(self.adapters) > 1) and (not self.localAdapter): + if (len(self.adapters) > 1) and (not self.local_adapter): raise ConfigurationError("local adapter must be set") # get the local adapter - adapter = self.localAdapter or self.adapters[0] + adapter = self.local_adapter or self.adapters[None] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # build a generic APDU apdu = _APDU(user_data=pdu.pduUserData) @@ -263,7 +319,7 @@ def indication(self, pdu): npdu.npduDADR = apdu.pduDestination # send it to all of connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): xadapter.process_npdu(npdu) return @@ -279,32 +335,53 @@ def indication(self, pdu): ### when it's a directly connected network raise RuntimeError("addressing problem") - # check for an available path - if dnet in self.networks: - rref = self.networks[dnet] - adapter = rref.adapter + # get it ready to send when the path is found + npdu.pduDestination = None + npdu.npduDADR = apdu.pduDestination + + # we might already be waiting for a path for this network + if dnet in self.pending_nets: + if _debug: NetworkServiceAccessPoint._debug(" - already waiting for path") + self.pending_nets[dnet].append(npdu) + return + + # check cache for an available path + path_info = self.router_info_cache.get_router_info(dnet) - ### make sure the direct connect is OK, may need to connect + # if there is info, we have a path + if path_info: + snet, address, status = path_info + if _debug: NetworkServiceAccessPoint._debug(" - path found: %r, %r, %r", snet, address, status) - ### make sure the peer router is OK, may need to connect + # check for an adapter + if snet not in self.adapters: + raise RuntimeError("network found but not connected: %r", snet) + adapter = self.adapters[snet] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # fix the destination - npdu.pduDestination = rref.address - npdu.npduDADR = apdu.pduDestination + npdu.pduDestination = address # send it along adapter.process_npdu(npdu) return - if _debug: NetworkServiceAccessPoint._debug(" - no known path to network, broadcast to discover it") + if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") - # set the destination - npdu.pduDestination = LocalBroadcast() - npdu.npduDADR = apdu.pduDestination + # add it to the list of packets waiting for the network + net_list = self.pending_nets.get(dnet, None) + if net_list is None: + net_list = self.pending_nets[dnet] = [] + net_list.append(npdu) + + # build a request for the network and send it to all of the adapters + xnpdu = WhoIsRouterToNetwork(dnet) + xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: - xadapter.process_npdu(npdu) + for adapter in self.adapters.values(): + ### make sure the adapter is OK + self.sap_indication(adapter, xnpdu) def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("process_npdu %r %r", adapter, npdu) @@ -312,83 +389,68 @@ def process_npdu(self, adapter, npdu): # make sure our configuration is OK if (not self.adapters): raise ConfigurationError("no adapters") - if (len(self.adapters) > 1) and (not self.localAdapter): - raise ConfigurationError("local adapter must be set") # check for source routing if npdu.npduSADR and (npdu.npduSADR.addrType != Address.nullAddr): + if _debug: NetworkServiceAccessPoint._debug(" - check source path") + # see if this is attempting to spoof a directly connected network snet = npdu.npduSADR.addrNet - for xadapter in self.adapters: - if (xadapter is not adapter) and (snet == xadapter.adapterNet): - NetworkServiceAccessPoint._warning("spoof?") - ### log this - return - - # make a key for the router reference - rkey = (adapter, npdu.pduSource) - - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] - if rref.adapter == adapter and rref.address == npdu.pduSource: - pass # matches current entry - else: - if _debug: NetworkServiceAccessPoint._debug(" - replaces entry") - - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] + if snet in self.adapters: + NetworkServiceAccessPoint._warning(" - path error (1)") + return - # remove the network - del self.networks[snet] + # see if there is routing information for this source network + router_info = self.router_info_cache.get_router_info(snet) + if router_info: + router_snet, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug(" - router_address, router_status: %r, %r", router_address, router_status) - # get the router reference for this router - rref = self.routers.get(rkey) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # see if the router has changed + if not (router_address == npdu.pduSource): + if _debug: NetworkServiceAccessPoint._debug(" - replacing path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) else: - # new reference - rref = RouterReference( adapter, npdu.pduSource, [snet], 0) - self.routers[rkey] = rref + if _debug: NetworkServiceAccessPoint._debug(" - new path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) # check for destination routing if (not npdu.npduDADR) or (npdu.npduDADR.addrType == Address.nullAddr): - processLocally = (not self.localAdapter) or (adapter is self.localAdapter) or (npdu.npduNetMessage is not None) + if _debug: NetworkServiceAccessPoint._debug(" - no DADR") + + processLocally = (not self.local_adapter) or (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) forwardMessage = False elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote broadcast") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (2)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) forwardMessage = True elif npdu.npduDADR.addrType == Address.remoteStationAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote station") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (3)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) \ - and (npdu.npduDADR.addrAddr == self.localAddress.addrAddr) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ + and (npdu.npduDADR.addrAddr == self.local_address.addrAddr) forwardMessage = not processLocally elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - DADR is global broadcast") + processLocally = True forwardMessage = True @@ -402,6 +464,8 @@ def process_npdu(self, adapter, npdu): # application or network layer message if npdu.npduNetMessage is None: + if _debug: NetworkServiceAccessPoint._debug(" - application layer message") + if processLocally and self.serverPeer: if _debug: NetworkServiceAccessPoint._debug(" - processing APDU locally") @@ -411,7 +475,7 @@ def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug(" - apdu: %r", apdu) # see if it needs to look routed - if (len(self.adapters) > 1) and (adapter != self.localAdapter): + if (len(self.adapters) > 1) and (adapter != self.local_adapter): # combine the source address if not npdu.npduSADR: apdu.pduSource = RemoteStation( adapter.adapterNet, npdu.pduSource.addrAddr ) @@ -420,7 +484,7 @@ def process_npdu(self, adapter, npdu): # map the destination if not npdu.npduDADR: - apdu.pduDestination = self.localAddress + apdu.pduDestination = self.local_address elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: apdu.pduDestination = npdu.npduDADR elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: @@ -447,6 +511,8 @@ def process_npdu(self, adapter, npdu): self.response(apdu) else: + if _debug: NetworkServiceAccessPoint._debug(" - network layer message") + if processLocally: if npdu.npduNetMessage not in npdu_types: if _debug: NetworkServiceAccessPoint._debug(" - unknown npdu type: %r", npdu.npduNetMessage) @@ -461,18 +527,19 @@ def process_npdu(self, adapter, npdu): # pass to the service element self.sap_request(adapter, xpdu) - # maybe local processing only + # might not need to forward this to other devices if not forwardMessage: if _debug: NetworkServiceAccessPoint._debug(" - no forwarding") return # make sure we're really a router if (len(self.adapters) == 1): - if _debug: NetworkServiceAccessPoint._debug(" - not really a router") + if _debug: NetworkServiceAccessPoint._debug(" - not a router") return # make sure it hasn't looped if (npdu.npduHopCount == 0): + if _debug: NetworkServiceAccessPoint._debug(" - no more hops") return # build a new NPDU to send to other adapters @@ -493,9 +560,10 @@ def process_npdu(self, adapter, npdu): # if this is a broadcast it goes everywhere if npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - global broadcasting") newpdu.pduDestination = LocalBroadcast() - for xadapter in self.adapters: + for xadapter in self.adapters.values(): if (xadapter is not adapter): xadapter.process_npdu(_deepcopy(newpdu)) return @@ -503,36 +571,54 @@ def process_npdu(self, adapter, npdu): if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr) \ or (npdu.npduDADR.addrType == Address.remoteStationAddr): dnet = npdu.npduDADR.addrNet + if _debug: NetworkServiceAccessPoint._debug(" - remote station/broadcast") - # see if this should go to one of our directly connected adapters - for xadapter in self.adapters: - if dnet == xadapter.adapterNet: - if _debug: NetworkServiceAccessPoint._debug(" - found direct connect via %r", xadapter) - if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): - newpdu.pduDestination = LocalBroadcast() - else: - newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) + # see if this a locally connected network + if dnet in self.adapters: + xadapter = self.adapters[dnet] + if xadapter is adapter: + if _debug: NetworkServiceAccessPoint._debug(" - path error (4)") + return + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - # last leg in routing - newpdu.npduDADR = None + # if this was a remote broadcast, it's now a local one + if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): + newpdu.pduDestination = LocalBroadcast() + else: + newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) - # send the packet downstream - xadapter.process_npdu(_deepcopy(newpdu)) - return + # last leg in routing + newpdu.npduDADR = None - # see if we know how to get there - if dnet in self.networks: - rref = self.networks[dnet] - newpdu.pduDestination = rref.address + # send the packet downstream + xadapter.process_npdu(_deepcopy(newpdu)) + return + + # see if there is routing information for this destination network + router_info = self.router_info_cache.get_router_info(dnet) + if router_info: + router_net, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + + if router_net not in self.adapters: + if _debug: NetworkServiceAccessPoint._debug(" - path error (5)") + return - ### check to make sure the router is OK + xadapter = self.adapters[router_net] + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - ### check to make sure the network is OK, may need to connect + # the destination is the address of the router + newpdu.pduDestination = router_address # send the packet downstream - rref.adapter.process_npdu(_deepcopy(newpdu)) + xadapter.process_npdu(_deepcopy(newpdu)) return + if _debug: NetworkServiceAccessPoint._debug(" - no router info found") + ### queue this message for reprocessing when the response comes back # try to find a path to the network @@ -540,16 +626,17 @@ def process_npdu(self, adapter, npdu): xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): # skip the horse it rode in on if (xadapter is adapter): continue - ### make sure the adapter is OK + # pass this along as if it came from the NSE self.sap_indication(xadapter, xnpdu) - ### log this, what to do? - return + return + + if _debug: NetworkServiceAccessPoint._debug(" - bad DADR: %r", npdu.npduDADR) def sap_indication(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("sap_indication %r %r", adapter, npdu) @@ -621,17 +708,15 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # build a list of reachable networks netlist = [] - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter): - netlist.append(xadapter.adapterNet) + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is adapter): + continue + + # add the direct network + netlist.append(xadapter.adapterNet) - # build a list of other available networks - for net, rref in sap.networks.items(): - if rref.adapter is not adapter: - ### skip those marked unreachable - ### skip those that are not available - netlist.append(net) + ### add the other reachable if netlist: if _debug: NetworkServiceElement._debug(" - found these: %r", netlist) @@ -646,42 +731,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): else: # requesting a specific network if _debug: NetworkServiceElement._debug(" - requesting specific network: %r", npdu.wirtnNetwork) + dnet = npdu.wirtnNetwork - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter) and (npdu.wirtnNetwork == xadapter.adapterNet): - if _debug: NetworkServiceElement._debug(" - found it directly connected") + # check the directly connected networks + if dnet in sap.adapters: + if _debug: NetworkServiceElement._debug(" - directly connected") - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) - break else: - # check for networks I know about - if npdu.wirtnNetwork in sap.networks: - rref = sap.networks[npdu.wirtnNetwork] - if rref.adapter is adapter: - if _debug: NetworkServiceElement._debug(" - same net as request") - - else: - if _debug: NetworkServiceElement._debug(" - found on adapter: %r", rref.adapter) + # see if there is routing information for this source network + router_info = sap.router_info_cache.get_router_info(dnet) + if router_info: + if _debug: NetworkServiceElement._debug(" - router found") + + router_net, router_address, router_status = router_info + if _debug: NetworkServiceElement._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + if router_net not in sap.adapters: + if _debug: NetworkServiceElement._debug(" - path error (6)") + return - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) else: if _debug: NetworkServiceElement._debug(" - forwarding request to other adapters") # build a request - whoisrtn = WhoIsRouterToNetwork(npdu.wirtnNetwork, user_data=npdu.pduUserData) + whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) whoisrtn.pduDestination = LocalBroadcast() # if the request had a source, forward it along @@ -692,7 +781,7 @@ def WhoIsRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) # send it to all of the (other) adapters - for xadapter in sap.adapters: + for xadapter in sap.adapters.values(): if xadapter is not adapter: if _debug: NetworkServiceElement._debug(" - sending on adapter: %r", xadapter) self.request(xadapter, whoisrtn) @@ -700,8 +789,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("IAmRouterToNetwork %r %r", adapter, npdu) + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + # pass along to the service access point - self.elementService.add_router_references(adapter, npdu.pduSource, npdu.iartnNetworkList) + sap.add_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) + + # skip if this is not a router + if len(sap.adapters) > 1: + # build a broadcast annoucement + iamrtn = IAmRouterToNetwork(npdu.iartnNetworkList, user_data=npdu.pduUserData) + iamrtn.pduDestination = LocalBroadcast() + + # send it to all of the connected adapters + for xadapter in sap.adapters.values(): + # skip the horse it rode in on + if (xadapter is adapter): + continue + + # request this + self.request(xadapter, iamrtn) + + # look for pending NPDUs for the networks + for dnet in npdu.iartnNetworkList: + pending_npdus = sap.pending_nets.get(dnet, None) + if pending_npdus is not None: + if _debug: NetworkServiceElement._debug(" - %d pending to %r", len(pending_npdus), dnet) + + # delete the references + del sap.pending_nets[dnet] + + # now reprocess them + for pending_npdu in pending_npdus: + if _debug: NetworkServiceElement._debug(" - sending %s", repr(pending_npdu)) + + # the destination is the address of the router + pending_npdu.pduDestination = npdu.pduSource + + # send the packet downstream + adapter.process_npdu(pending_npdu) def ICouldBeRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("ICouldBeRouterToNetwork %r %r", adapter, npdu) diff --git a/py27/bacpypes/npdu.py b/py27/bacpypes/npdu.py index c6ad9d97..436f6bb8 100755 --- a/py27/bacpypes/npdu.py +++ b/py27/bacpypes/npdu.py @@ -476,7 +476,7 @@ def __init__(self, netList=[], *args, **kwargs): def encode(self, npdu): NPCI.update(npdu, self) - for net in self.ratnNetworkList: + for net in self.rbtnNetworkList: npdu.put_short(net) def decode(self, npdu): @@ -543,6 +543,12 @@ def __init__(self, dnet=None, portID=None, portInfo=None): self.rtPortID = portID self.rtPortInfo = portInfo + def __eq__(self, other): + """Return true iff entries are identical.""" + return (self.rtDNET == other.rtDNET) and \ + (self.rtPortID == other.rtPortID) and \ + (self.rtPortInfo == other.rtPortInfo) + def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" # make/extend the dictionary of content @@ -735,6 +741,11 @@ class WhatIsNetworkNumber(NPDU): messageType = 0x12 + def __init__(self, *args, **kwargs): + super(WhatIsNetworkNumber, self).__init__(*args, **kwargs) + + self.npduNetMessage = WhatIsNetworkNumber.messageType + def encode(self, npdu): NPCI.update(npdu, self) @@ -755,25 +766,32 @@ def npdu_contents(self, use_dict=None, as_class=dict): class NetworkNumberIs(NPDU): - _debug_contents = ('nniNET', 'nniFlag',) + _debug_contents = ('nniNet', 'nniFlag',) messageType = 0x13 + def __init__(self, net=None, flag=None, *args, **kwargs): + super(NetworkNumberIs, self).__init__(*args, **kwargs) + + self.npduNetMessage = NetworkNumberIs.messageType + self.nniNet = net + self.nniFlag = flag + def encode(self, npdu): NPCI.update(npdu, self) - npdu.put_short( self.nniNET ) + npdu.put_short( self.nniNet ) npdu.put( self.nniFlag ) def decode(self, npdu): NPCI.update(self, npdu) - self.nniNET = npdu.get_short() + self.nniNet = npdu.get_short() self.nniFlag = npdu.get() def npdu_contents(self, use_dict=None, as_class=dict): return key_value_contents(use_dict=use_dict, as_class=as_class, key_values=( ('function', 'NetorkNumberIs'), - ('net', self.nniNET), + ('net', self.nniNet), ('flag', self.nniFlag), )) diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 523285cc..ac39cc05 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -216,6 +216,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + elif issubclass(self.datatype, AnyAtomic): + if _debug: Property._debug(" - property is any atomic, checking value") + if not isinstance(value, Atomic): + raise InvalidParameterDatatype("%s must be an atomic instance" % ( + self.identifier, + )) + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): @@ -1415,7 +1422,7 @@ class EventEnrollmentObject(Object): , ReadableProperty('eventTimeStamps', ArrayOf(TimeStamp)) , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString)) , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString)) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference) , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index 2c9f55ce..ccc0d688 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -14,6 +14,9 @@ from .errors import DecodingError, InvalidTag, InvalidParameterDatatype from .pdu import PDUData +# import the task manager to get the "current" date and time +from .task import TaskManager as _TaskManager + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -453,6 +456,8 @@ class Atomic(object): _app_tag = None def __cmp__(self, other): + # sys.stderr.write("__cmp__ %r %r\n" % (self, other)) + # hoop jump it if not isinstance(other, self.__class__): other = self.__class__(other) @@ -465,6 +470,26 @@ def __cmp__(self, other): else: return 0 + def __lt__(self, other): + # sys.stderr.write("__lt__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return (self.value < other.value) + + def __eq__(self, other): + # sys.stderr.write("__eq__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return self.value == other.value + @classmethod def coerce(cls, arg): """Given an arg, return the appropriate value given the class.""" @@ -1344,6 +1369,9 @@ def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): elif isinstance(arg, Date): self.value = arg.value + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") @@ -1372,11 +1400,31 @@ def CalcDayOfWeek(self): # put it back together self.value = (year, month, day, day_of_week) - def now(self): - tup = time.localtime() + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) + self.value = (tup[0]-1900, tup[1], tup[2], tup[6] + 1) + return self + def __float__(self): + """Convert to seconds since the epoch.""" + # rip apart the value + year, month, day, day_of_week = self.value + + # check for special values + if (year == 255) or (month in _special_mon_inv) or (day in _special_day_inv): + raise ValueError("no wildcard values") + + # convert to time.time() value + return time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.dateAppTag, bytearray(self.value)) @@ -1456,19 +1504,40 @@ def __init__(self, arg=None, hour=255, minute=255, second=255, hundredth=255): tup_list[3] = tup_list[3] * 10 self.value = tuple(tup_list) + elif isinstance(arg, Time): self.value = arg.value + + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") - def now(self): - now = time.time() - tup = time.localtime(now) + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) - self.value = (tup[3], tup[4], tup[5], int((now - int(now)) * 100)) + self.value = (tup[3], tup[4], tup[5], int((when - int(when)) * 100)) return self + def __float__(self): + """Return the current value as an offset from midnight.""" + if 255 in self.value: + raise ValueError("no wildcard values") + + # rip it apart + hour, minute, second, hundredth = self.value + + # put it together + return (hour * 3600.0) + (minute * 60.0) + second + (hundredth / 100.0) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.timeAppTag, bytearray(self.value)) diff --git a/py27/bacpypes/service/device.py b/py27/bacpypes/service/device.py index 25283a4d..11be52b0 100644 --- a/py27/bacpypes/service/device.py +++ b/py27/bacpypes/service/device.py @@ -4,136 +4,16 @@ from ..capability import Capability from ..pdu import GlobalBroadcast -from ..primitivedata import Date, Time, ObjectIdentifier -from ..constructeddata import ArrayOf -from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU, Error +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU from ..errors import ExecutionError, InconsistentParameters, \ MissingRequiredParameter, ParameterOutOfRange -from ..object import register_object_type, registered_object_types, \ - Property, DeviceObject from ..task import FunctionTask -from .object import CurrentPropertyListMixIn - # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -@bacpypes_debugging -class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for properties this class implements - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - - # the object list is provided - if 'objectList' in kwargs: - raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) - # # Who-Is I-Am Services # diff --git a/py27/bacpypes/service/object.py b/py27/bacpypes/service/object.py index ca8d3fe9..5c4c993f 100644 --- a/py27/bacpypes/service/object.py +++ b/py27/bacpypes/service/object.py @@ -7,11 +7,11 @@ from ..primitivedata import Atomic, Null, Unsigned from ..constructeddata import Any, Array, ArrayOf -from ..apdu import Error, \ +from ..apdu import \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import Property, Object, PropertyError +from ..object import PropertyError # some debugging _debug = 0 @@ -20,57 +20,6 @@ # handy reference ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) -# -# CurrentPropertyList -# - -@bacpypes_debugging -class CurrentPropertyList(Property): - - def __init__(self): - if _debug: CurrentPropertyList._debug("__init__") - Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) - - # make a list of the properties that have values - property_list = [k for k, v in obj._values.items() - if v is not None - and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') - ] - if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) - - # sort the list so it's stable - property_list.sort() - - # asking for the whole thing - if arrayIndex is None: - return ArrayOfPropertyIdentifier(property_list) - - # asking for the length - if arrayIndex == 0: - return len(property_list) - - # asking for an index - if arrayIndex > len(property_list): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return property_list[arrayIndex - 1] - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentPropertyListMixIn -# - -@bacpypes_debugging -class CurrentPropertyListMixIn(Object): - - properties = [ - CurrentPropertyList(), - ] - # # ReadProperty and WriteProperty Services # diff --git a/py27/bacpypes/task.py b/py27/bacpypes/task.py index c4fec155..e3f405ce 100755 --- a/py27/bacpypes/task.py +++ b/py27/bacpypes/task.py @@ -8,6 +8,7 @@ from time import time as _time from heapq import heapify, heappush, heappop +import itertools from .singleton import SingletonLogging from .debugging import DebugContents, Logging, ModuleLogger, bacpypes_debugging @@ -276,6 +277,9 @@ def __init__(self): # task manager is this instance _task_manager = self + # unique sequence counter for tasks scheduled at the same time + self.counter = itertools.count() + # there may be tasks created that couldn't be scheduled # because a task manager wasn't created yet. if _unscheduled_tasks: @@ -300,7 +304,7 @@ def install_task(self, task): self.suspend_task(task) # save this in the task list - heappush( self.tasks, (task.taskTime, task) ) + heappush( self.tasks, (task.taskTime, next(self.counter), task) ) if _debug: TaskManager._debug(" - tasks: %r", self.tasks) task.isScheduled = True @@ -313,7 +317,7 @@ def suspend_task(self, task): if _debug: TaskManager._debug("suspend_task %r", task) # remove this guy - for i, (when, curtask) in enumerate(self.tasks): + for i, (when, n, curtask) in enumerate(self.tasks): if task is curtask: if _debug: TaskManager._debug(" - task found") del self.tasks[i] @@ -348,7 +352,7 @@ def get_next_task(self): if self.tasks: # look at the first task - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] if when <= now: # pull it off the list and mark that it's no longer scheduled heappop(self.tasks) @@ -356,7 +360,7 @@ def get_next_task(self): task.isScheduled = False if self.tasks: - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] # peek at the next task, return how long to wait delta = max(when - now, 0.0) else: diff --git a/py27/bacpypes/vlan.py b/py27/bacpypes/vlan.py index 953e266e..5908e898 100755 --- a/py27/bacpypes/vlan.py +++ b/py27/bacpypes/vlan.py @@ -32,9 +32,13 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.name = name self.nodes = [] + self.broadcast_address = broadcast_address self.drop_percent = drop_percent + # point to a TrafficLog instance + self.traffic_log = None + def add_node(self, node): """ Add a node to this network, let the node know which network it's on. """ if _debug: Network._debug("add_node %r", node) @@ -59,6 +63,10 @@ def process_pdu(self, pdu): """ if _debug: Network._debug("process_pdu(%s) %r", self.name, pdu) + # if there is a traffic log, call it with the network name and pdu + if self.traffic_log: + self.traffic_log(self.name, pdu) + # randomly drop a packet if self.drop_percent != 0.0: if (random.random() * 100.0) < self.drop_percent: @@ -154,9 +162,9 @@ class IPNetwork(Network): ('1.2.3.255', 5) and the other nodes must have the same tuple. """ - def __init__(self): + def __init__(self, name=''): if _debug: IPNetwork._debug("__init__") - Network.__init__(self) + Network.__init__(self, name=name) def add_node(self, node): if _debug: IPNetwork._debug("add_node %r", node) @@ -206,11 +214,12 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): @bacpypes_debugging class IPRouterNode(Client): - def __init__(self, router, addr, lan=None): + def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) - # save the reference to the router + # save the references to the router for packets and the lan for debugging self.router = router + self.lan = lan # make ourselves an IPNode and bind to it self.node = IPNode(addr, lan=lan, promiscuous=True, spoofing=True) @@ -231,6 +240,10 @@ def process_pdu(self, pdu): # pass it downstream self.request(pdu) + def __repr__(self): + return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + + # # IPRouter # diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index 5ea5be2f..f556be27 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.0' +__version__ = '0.17.2' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' @@ -69,6 +69,8 @@ from . import app from . import appservice + +from . import local from . import service # diff --git a/py34/bacpypes/analysis.py b/py34/bacpypes/analysis.py index fe21cc39..7b64a3f0 100755 --- a/py34/bacpypes/analysis.py +++ b/py34/bacpypes/analysis.py @@ -23,7 +23,7 @@ except: pass -from .debugging import ModuleLogger, DebugContents, bacpypes_debugging, btox +from .debugging import ModuleLogger, bacpypes_debugging, btox, xtob from .pdu import PDU, Address from .bvll import BVLPDU, bvl_pdu_types, ForwardedNPDU, \ @@ -144,6 +144,8 @@ def decode_packet(data): # assume it is ethernet for now d = decode_ethernet(data) + pduSource = Address(d['source_address']) + pduDestination = Address(d['destination_address']) data = d['data'] # there could be a VLAN header @@ -174,10 +176,8 @@ def decode_packet(data): decode_packet._debug(" - pduDestination: %r", pduDestination) else: if _debug: decode_packet._debug(" - not a UDP packet") - return None else: if _debug: decode_packet._debug(" - not an IP packet") - return None # check for empty if not data: @@ -374,7 +374,7 @@ def decode_file(fname): # @bacpypes_debugging -class Tracer(DebugContents): +class Tracer: def __init__(self, initial_state=None): if _debug: Tracer._debug("__init__ initial_state=%r", initial_state) diff --git a/py34/bacpypes/apdu.py b/py34/bacpypes/apdu.py index 10706aa2..c19a811e 100755 --- a/py34/bacpypes/apdu.py +++ b/py34/bacpypes/apdu.py @@ -56,38 +56,51 @@ def register_error_type(klass): # encode_max_segments_accepted/decode_max_segments_accepted # +_max_segments_accepted_encoding = [ + None, 2, 4, 8, 16, 32, 64, None, + ] + def encode_max_segments_accepted(arg): """Encode the maximum number of segments the device will accept, Section - 20.1.2.4""" - w = 0 - while (arg and not arg & 1): - w += 1 - arg = (arg >> 1) - return w + 20.1.2.4, and if the device says it can only accept one segment it shouldn't + say that it supports segmentation!""" + # unspecified + if not arg: + return 0 + + if arg > 64: + return 7 + + # the largest number not greater than the arg + for i in range(6, 0, -1): + if _max_segments_accepted_encoding[i] <= arg: + return i + + raise ValueError("invalid max max segments accepted: {0}".format(arg)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section 20.1.2.4""" - return arg and (1 << arg) or None + return _max_segments_accepted_encoding[arg] # # encode_max_apdu_length_accepted/decode_max_apdu_length_accepted # -_max_apdu_response_encoding = [50, 128, 206, 480, 1024, 1476, None, None, +_max_apdu_length_encoding = [50, 128, 206, 480, 1024, 1476, None, None, None, None, None, None, None, None, None, None] def encode_max_apdu_length_accepted(arg): """Return the encoding of the highest encodable value less than the value of the arg.""" for i in range(5, -1, -1): - if (arg >= _max_apdu_response_encoding[i]): + if (arg >= _max_apdu_length_encoding[i]): return i raise ValueError("invalid max APDU length accepted: {0}".format(arg)) def decode_max_apdu_length_accepted(arg): - v = _max_apdu_response_encoding[arg] + v = _max_apdu_length_encoding[arg] if not v: raise ValueError("invalid max APDU length accepted: {0}".format(arg)) @@ -174,7 +187,7 @@ def encode(self, pdu): if self.apduSA: buff += 0x02 pdu.put(buff) - pdu.put((encode_max_segments_accepted(self.apduMaxSegs) << 4) + encode_max_apdu_length_accepted(self.apduMaxResp)) + pdu.put((self.apduMaxSegs << 4) + self.apduMaxResp) pdu.put(self.apduInvokeID) if self.apduSeg: pdu.put(self.apduSeq) @@ -255,8 +268,8 @@ def decode(self, pdu): self.apduMor = ((buff & 0x04) != 0) self.apduSA = ((buff & 0x02) != 0) buff = pdu.get() - self.apduMaxSegs = decode_max_segments_accepted( (buff >> 4) & 0x07 ) - self.apduMaxResp = decode_max_apdu_length_accepted( buff & 0x0F ) + self.apduMaxSegs = (buff >> 4) & 0x07 + self.apduMaxResp = buff & 0x0F self.apduInvokeID = pdu.get() if self.apduSeg: self.apduSeq = pdu.get() @@ -1301,7 +1314,7 @@ class LifeSafetyOperationRequest(ConfirmedRequestSequence): [ Element('requestingProcessIdentifier', Unsigned, 0) , Element('requestingSource', CharacterString, 1) , Element('request', LifeSafetyOperation, 2) - , Element('objectIdentifier', ObjectIdentifier, 3) + , Element('objectIdentifier', ObjectIdentifier, 3, True) ] register_confirmed_request_type(LifeSafetyOperationRequest) @@ -1485,7 +1498,7 @@ class RemoveListElementRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('objectIdentifier', ObjectIdentifier, 0) , Element('propertyIdentifier', PropertyIdentifier, 1) - , Element('propertyArrayIndex', Unsigned, 2) + , Element('propertyArrayIndex', Unsigned, 2, True) , Element('listOfElements', Any, 3) ] diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index a1081497..1548a3b4 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -26,7 +26,8 @@ # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ - ConfirmedServiceChoice, UnconfirmedServiceChoice + ConfirmedServiceChoice, UnconfirmedServiceChoice, \ + IAmRequest from .basetypes import ServicesSupported # basic services @@ -54,16 +55,16 @@ class DeviceInfo(DebugContents): 'maxSegmentsAccepted', ) - def __init__(self): + def __init__(self, device_identifier, address): # this information is from an IAmRequest - self.deviceIdentifier = None # device identifier - self.address = None # LocalStation or RemoteStation + self.deviceIdentifier = device_identifier + self.address = address + self.maxApduLengthAccepted = 1024 # maximum APDU device will accept self.segmentationSupported = 'noSegmentation' # normally no segmentation + self.maxSegmentsAccepted = None # None iff no segmentation self.vendorID = None # vendor identifier - - self.maxNpduLength = 1497 # maximum we can send in transit - self.maxSegmentsAccepted = None # value for proposed/actual window size + self.maxNpduLength = None # maximum we can send in transit (see 19.4) # # DeviceInfoCache @@ -72,127 +73,134 @@ def __init__(self): @bacpypes_debugging class DeviceInfoCache: - def __init__(self): + def __init__(self, device_info_class=DeviceInfo): if _debug: DeviceInfoCache._debug("__init__") + # a little error checking + if not issubclass(device_info_class, DeviceInfo): + raise ValueError("not a DeviceInfo subclass: %r" % (device_info_class,)) + # empty cache self.cache = {} + # class for new records + self.device_info_class = device_info_class + def has_device_info(self, key): """Return true iff cache has information about the device.""" if _debug: DeviceInfoCache._debug("has_device_info %r", key) return key in self.cache - def add_device_info(self, apdu): + def iam_device_info(self, apdu): """Create a device information record based on the contents of an IAmRequest and put it in the cache.""" - if _debug: DeviceInfoCache._debug("add_device_info %r", apdu) + if _debug: DeviceInfoCache._debug("iam_device_info %r", apdu) - # get the existing cache record by identifier - info = self.get_device_info(apdu.iAmDeviceIdentifier[1]) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # make sure the apdu is an I-Am + if not isinstance(apdu, IAmRequest): + raise ValueError("not an IAmRequest: %r" % (apdu,)) - # update existing record - if info: - if (info.address == apdu.pduSource): - return + # get the device instance + device_instance = apdu.iAmDeviceIdentifier[1] - info.address = apdu.pduSource - else: - # get the existing record by address (creates a new record) - info = self.get_device_info(apdu.pduSource) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # get the existing cache record if it exists + device_info = self.cache.get(device_instance, None) - info.deviceIdentifier = apdu.iAmDeviceIdentifier[1] + # maybe there is a record for this address + if not device_info: + device_info = self.cache.get(apdu.pduSource, None) - # update the rest of the values - info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted - info.segmentationSupported = apdu.segmentationSupported - info.vendorID = apdu.vendorID + # make a new one using the class provided + if not device_info: + device_info = self.device_info_class(device_instance, apdu.pduSource) - # say this is an updated record - self.update_device_info(info) + # jam in the correct values + device_info.deviceIdentifier = device_instance + device_info.address = apdu.pduSource + device_info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted + device_info.segmentationSupported = apdu.segmentationSupported + device_info.vendorID = apdu.vendorID + + # tell the cache this is an updated record + self.update_device_info(device_info) def get_device_info(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" if _debug: DeviceInfoCache._debug("get_device_info %r", key) - if isinstance(key, int): - current_info = self.cache.get(key, None) + # get the info if it's there + device_info = self.cache.get(key, None) + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - elif not isinstance(key, Address): - raise TypeError("key must be integer or an address") + return device_info - elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): - raise TypeError("address must be a local or remote station") - - else: - current_info = self.cache.get(key, None) - if not current_info: - current_info = DeviceInfo() - current_info.address = key - current_info._cache_keys = (None, key) - current_info._ref_count = 1 - - self.cache[key] = current_info - else: - if _debug: DeviceInfoCache._debug(" - reference bump") - current_info._ref_count += 1 - - if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) - - return current_info - - def update_device_info(self, info): + def update_device_info(self, device_info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" - if _debug: DeviceInfoCache._debug("update_device_info %r", info) + if _debug: DeviceInfoCache._debug("update_device_info %r", device_info) - cache_id, cache_address = info._cache_keys + # give this a reference count if it doesn't have one + if not hasattr(device_info, '_ref_count'): + device_info._ref_count = 0 - if (cache_id is not None) and (info.deviceIdentifier != cache_id): + # get the current keys + cache_id, cache_address = getattr(device_info, '_cache_keys', (None, None)) + + if (cache_id is not None) and (device_info.deviceIdentifier != cache_id): if _debug: DeviceInfoCache._debug(" - device identifier updated") # remove the old reference, add the new one del self.cache[cache_id] - self.cache[info.deviceIdentifier] = info - - cache_id = info.deviceIdentifier + self.cache[device_info.deviceIdentifier] = device_info - if (cache_address is not None) and (info.address != cache_address): + if (cache_address is not None) and (device_info.address != cache_address): if _debug: DeviceInfoCache._debug(" - device address updated") # remove the old reference, add the new one del self.cache[cache_address] - self.cache[info.address] = info - - cache_address = info.address + self.cache[device_info.address] = device_info # update the keys - info._cache_keys = (cache_id, cache_address) + device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) + + def acquire(self, key): + """Return the known information about the device and mark the record + as being used by a segmenation state machine.""" + if _debug: DeviceInfoCache._debug("acquire %r", key) + + if isinstance(key, int): + device_info = self.cache.get(key, None) + + elif not isinstance(key, Address): + raise TypeError("key must be integer or an address") + + elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): + raise TypeError("address must be a local or remote station") + + else: + device_info = self.cache.get(key, None) + + if device_info: + if _debug: DeviceInfoCache._debug(" - reference bump") + device_info._ref_count += 1 + + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - def release_device_info(self, info): + return device_info + + def release(self, device_info): """This function is called by the segmentation state machine when it has finished with the device information.""" - if _debug: DeviceInfoCache._debug("release_device_info %r", info) + if _debug: DeviceInfoCache._debug("release %r", device_info) # this information record might be used by more than one SSM - if info._ref_count > 1: - if _debug: DeviceInfoCache._debug(" - multiple references") - info._ref_count -= 1 - return + if device_info._ref_count == 0: + raise RuntimeError("reference count") - cache_id, cache_address = info._cache_keys - if cache_id is not None: - del self.cache[cache_id] - if cache_address is not None: - del self.cache[cache_address] - if _debug: DeviceInfoCache._debug(" - released") + # decrement the reference count + device_info._ref_count -= 1 # # Application @@ -260,9 +268,9 @@ def add_object(self, obj): # make sure it hasn't already been defined if object_name in self.objectName: - raise RuntimeError("already an object with name {0!r}".format(object_name)) + raise RuntimeError("already an object with name %r" % (object_name,)) if object_identifier in self.objectIdentifier: - raise RuntimeError("already an object with identifier {0!r}".format(object_identifier)) + raise RuntimeError("already an object with identifier %r" % (object_identifier,)) # now put it in local dictionaries self.objectName[object_name] = obj diff --git a/py34/bacpypes/appservice.py b/py34/bacpypes/appservice.py index 4a3b65da..43316f13 100755 --- a/py34/bacpypes/appservice.py +++ b/py34/bacpypes/appservice.py @@ -12,12 +12,14 @@ from .task import OneShotTask from .pdu import Address -from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \ +from .apdu import encode_max_segments_accepted, decode_max_segments_accepted, \ + encode_max_apdu_length_accepted, decode_max_apdu_length_accepted, \ + AbortPDU, AbortReason, ComplexAckPDU, \ ConfirmedRequestPDU, Error, ErrorPDU, RejectPDU, SegmentAckPDU, \ SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \ unconfirmed_request_types, confirmed_request_types, complex_ack_types, \ error_types -from .errors import RejectException, AbortException +from .errors import RejectException, AbortException, UnrecognizedService # some debugging _debug = 0 @@ -45,19 +47,23 @@ class SSM(OneShotTask, DebugContents): , 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED' ] - _debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID' + _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' ) - def __init__(self, sap, remoteDevice): + def __init__(self, sap, pdu_address): """Common parts for client and server segmentation.""" - if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice) + if _debug: SSM._debug("__init__ %r %r", sap, pdu_address) OneShotTask.__init__(self) self.ssmSAP = sap # service access point - self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance + + # save the address and get the device information + self.pdu_address = pdu_address + self.device_info = sap.deviceInfoCache.get_device_info(pdu_address) + self.invokeID = None # invoke ID self.state = IDLE # initial state @@ -71,11 +77,17 @@ def __init__(self, sap, remoteDevice): self.lastSequenceNumber = None self.initialSequenceNumber = None self.actualWindowSize = None - self.proposedWindowSize = None - # the maximum number of segments starts out being what's in the SAP - # which is the defaults or values from the local device. - self.maxSegmentsAccepted = self.ssmSAP.maxSegmentsAccepted + # local device object provides these or SAP provides defaults, make + # copies here so they are consistent throughout the transaction but + # they could change from one transaction to the next + self.numberOfApduRetries = getattr(sap.localDevice, 'numberOfApduRetries', sap.numberOfApduRetries) + self.apduTimeout = getattr(sap.localDevice, 'apduTimeout', sap.apduTimeout) + + self.segmentationSupported = getattr(sap.localDevice, 'segmentationSupported', sap.segmentationSupported) + self.segmentTimeout = getattr(sap.localDevice, 'segmentTimeout', sap.segmentTimeout) + self.maxSegmentsAccepted = getattr(sap.localDevice, 'maxSegmentsAccepted', sap.maxSegmentsAccepted) + self.maxApduLengthAccepted = getattr(sap.localDevice, 'maxApduLengthAccepted', sap.maxApduLengthAccepted) def start_timer(self, msecs): if _debug: SSM._debug("start_timer %r", msecs) @@ -153,12 +165,12 @@ def get_segment(self, indx): segAPDU = ConfirmedRequestPDU(self.segmentAPDU.apduService) - segAPDU.apduMaxSegs = self.maxSegmentsAccepted - segAPDU.apduMaxResp = self.ssmSAP.maxApduLengthAccepted - segAPDU.apduInvokeID = self.invokeID; + segAPDU.apduMaxSegs = encode_max_segments_accepted(self.maxSegmentsAccepted) + segAPDU.apduMaxResp = encode_max_apdu_length_accepted(self.maxApduLengthAccepted) + segAPDU.apduInvokeID = self.invokeID # segmented response accepted? - segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth') + segAPDU.apduSA = self.segmentationSupported in ('segmentedReceive', 'segmentedBoth') if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA) elif self.segmentAPDU.apduType == ComplexAckPDU.pduType: @@ -172,14 +184,21 @@ def get_segment(self, indx): segAPDU.pduUserData = self.segmentAPDU.pduUserData # make sure the destination is set - segAPDU.pduDestination = self.remoteDevice.address + segAPDU.pduDestination = self.pdu_address # segmented message? if (self.segmentCount != 1): segAPDU.apduSeg = True segAPDU.apduMor = (indx < (self.segmentCount - 1)) # more follows segAPDU.apduSeq = indx % 256 # sequence number - segAPDU.apduWin = self.proposedWindowSize # window size + + # first segment sends proposed window size, rest get actual + if indx == 0: + if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) + segAPDU.apduWin = self.proposedWindowSize + else: + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) + segAPDU.apduWin = self.actualWindowSize else: segAPDU.apduSeg = False segAPDU.apduMor = False @@ -211,10 +230,11 @@ def in_window(self, seqA, seqB): return rslt - def FillWindow(self, seqNum): + def fill_window(self, seqNum): """This function sends all of the packets necessary to fill out the segmentation window.""" - if _debug: SSM._debug("FillWindow %r", seqNum) + if _debug: SSM._debug("fill_window %r", seqNum) + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) for ix in range(self.actualWindowSize): apdu = self.get_segment(seqNum + ix) @@ -234,13 +254,18 @@ def FillWindow(self, seqNum): @bacpypes_debugging class ClientSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ClientSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) # initialize the retry count self.retryCount = 0 + # acquire the device info + if self.device_info: + if _debug: ClientSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) + def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" if _debug: ClientSSM._debug("set_state %r (%s) timer=%r", newState, SSM.transactionLabels[newState], timer) @@ -253,8 +278,10 @@ def set_state(self, newState, timer=0): if _debug: ClientSSM._debug(" - remove from active transactions") self.ssmSAP.clientTransactions.remove(self) - if _debug: ClientSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by client transaction functions when it wants @@ -263,7 +290,7 @@ def request(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -280,26 +307,27 @@ def indication(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit, - # the maximum conveyable by the internetwork to the remote device, and - # the maximum APDU size accepted by the remote device. - self.segmentSize = min( - self.ssmSAP.maxApduLengthAccepted, - self.remoteDevice.maxNpduLength, - self.remoteDevice.maxApduLengthAccepted, - ) - if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) + # if the max apdu length of the server isn't known, assume that it + # is the same size as our own and will be the segment size + if (not self.device_info) or (self.device_info.maxApduLengthAccepted is None): + self.segmentSize = self.maxApduLengthAccepted - # the maximum number of segments acceptable in the reply - if apdu.apduMaxSegs is not None: - # this request overrides the default - self.maxSegmentsAccepted = apdu.apduMaxSegs + # if the max npdu length of the server isn't known, assume that it + # is the same as the max apdu length accepted + elif self.device_info.maxNpduLength is None: + self.segmentSize = self.device_info.maxApduLengthAccepted + + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the server and the largest it can accept + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.device_info.maxApduLengthAccepted) + if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) # save the invoke ID self.invokeID = apdu.apduInvokeID if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -312,34 +340,49 @@ def indication(self, apdu): # make sure we support segmented transmit if we need to if self.segmentCount > 1: - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't send segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): - if _debug: ClientSSM._debug(" - remote device can't receive segmented requests") + + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for segmentation support") + + elif self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if _debug: ClientSSM._debug(" - server can't receive segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our request + # that the server said it was willing to accept + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for maximum number of segments") + + elif not self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server doesn't say maximum number of segments") + + elif self.segmentCount > self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # send out the first segment (or the whole thing) if self.segmentCount == 1: - # SendConfirmedUnsegmented + # unsegmented self.sentAllSegments = True self.retryCount = 0 - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) else: - # SendConfirmedSegmented + # segmented self.sentAllSegments = False self.retryCount = 0 self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.actualWindowSize = None # segment ack will set value + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # deliver to the device self.request(self.get_segment(0)) @@ -350,7 +393,7 @@ def response(self, apdu): if _debug: ClientSSM._debug("response %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it to the application @@ -407,29 +450,31 @@ def segmented_request(self, apdu): and receives an apdu.""" if _debug: ClientSSM._debug("segmented_request %r", apdu) - # client is ready for the next segment + # server is ready for the next segment if apdu.apduType == SegmentAckPDU.pduType: if _debug: ClientSSM._debug(" - segment ack") + # actual window size is provided by server + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ClientSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: if _debug: ClientSSM._debug(" - all done sending request") - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) # more segments to send else: if _debug: ClientSSM._debug(" - more segments to send") self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 - self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # simple ack elif (apdu.apduType == SimpleAckPDU.pduType): @@ -452,6 +497,7 @@ def segmented_request(self, apdu): self.response(abort) # send it to the application elif not apdu.apduSeg: + # ack is not segmented self.set_state(COMPLETED) self.response(apdu) @@ -459,10 +505,11 @@ def segmented_request(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # minimum of what the server is proposing and this client proposes + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # some kind of problem elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType): @@ -479,12 +526,12 @@ def segmented_request_timeout(self): if _debug: ClientSSM._debug("segmented_request_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: if _debug: ClientSSM._debug(" - retry segmented request") self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: if _debug: ClientSSM._debug(" - abort, no response from the device") @@ -516,7 +563,7 @@ def await_confirmation(self, apdu): self.set_state(COMPLETED) self.response(apdu) - elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + elif self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't receive segmented messages") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) @@ -527,10 +574,10 @@ def await_confirmation(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + self.actualWindowSize = apdu.apduWin self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # send back a segment ack segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) @@ -546,7 +593,7 @@ def await_confirmation(self, apdu): elif (apdu.apduType == SegmentAckPDU.pduType): if _debug: ClientSSM._debug(" - segment ack(!?)") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) else: raise RuntimeError("invalid APDU (3)") @@ -554,9 +601,9 @@ def await_confirmation(self, apdu): def await_confirmation_timeout(self): if _debug: ClientSSM._debug("await_confirmation_timeout") - self.retryCount += 1 - if self.retryCount < self.ssmSAP.retryCount: - if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.ssmSAP.retryCount) + if self.retryCount < self.numberOfApduRetries: + if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.numberOfApduRetries) + self.retryCount += 1 # save the retry count, indication acts like the request is coming # from the application so the retryCount gets re-initialized. @@ -594,8 +641,8 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - segment %s received out of order, should be %s", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) return @@ -610,7 +657,7 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - no more follows") # send a final ack - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) self.set_state(COMPLETED) @@ -620,15 +667,15 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + self.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) else: # wait for more segments if _debug: ClientSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_confirmation_timeout(self): if _debug: ClientSSM._debug("segmented_confirmation_timeout") @@ -643,9 +690,14 @@ def segmented_confirmation_timeout(self): @bacpypes_debugging class ServerSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ServerSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) + + # acquire the device info + if self.device_info: + if _debug: ServerSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" @@ -659,8 +711,10 @@ def set_state(self, newState, timer=0): if _debug: ServerSSM._debug(" - remove from active transactions") self.ssmSAP.serverTransactions.remove(self) - if _debug: ServerSSM._debug(" - release device information") - self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by transaction functions to send @@ -668,7 +722,7 @@ def request(self, apdu): if _debug: ServerSSM._debug("request %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it via the device @@ -697,7 +751,7 @@ def response(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -739,14 +793,15 @@ def confirmation(self, apdu): # save the response and set the segmentation context self.set_segmentation_context(apdu) - # the segment size is the minimum of the maximum size I can transmit - # (assumed to have no local buffer limitations), the maximum conveyable - # by the internetwork to the remote device, and the maximum APDU size - # accepted by the remote device. - self.segmentSize = min(self.remoteDevice.maxNpduLength, self.remoteDevice.maxApduLengthAccepted) + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the client and the largest it can accept + if (not self.device_info) or (self.device_info.maxNpduLength is None): + self.segmentSize = self.maxApduLengthAccepted + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.maxApduLengthAccepted) if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -762,26 +817,31 @@ def confirmation(self, apdu): if _debug: ServerSSM._debug(" - segmentation required, %d segments", self.segmentCount) # make sure we support segmented transmit - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ServerSSM._debug(" - server can't send segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # make sure client supports segmented receive - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ServerSSM._debug(" - client can't receive segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our response + # that the device said it was willing to accept in the request + if self.segmentCount > self.maxSegmentsAccepted: + if _debug: ServerSSM._debug(" - client can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # initialize the state self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 + self.actualWindowSize = None # send out the first segment (or the whole thing) if self.segmentCount == 1: @@ -789,7 +849,7 @@ def confirmation(self, apdu): self.set_state(COMPLETED) else: self.response(self.get_segment(0)) - self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_RESPONSE, self.segmentTimeout) else: raise RuntimeError("invalid APDU (4)") @@ -837,38 +897,48 @@ def idle(self, apdu): self.invokeID = apdu.apduInvokeID if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID) - # make sure the device information is synced with the request if apdu.apduSA: - if self.remoteDevice.segmentationSupported == 'noSegmentation': + if not self.device_info: + if _debug: ServerSSM._debug(" - no client device info") + + elif self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") - self.remoteDevice.segmentationSupported = 'segmentedReceive' + self.device_info.segmentationSupported = 'segmentedReceive' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedTransmit': + elif self.device_info.segmentationSupported == 'segmentedTransmit': if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive") - self.remoteDevice.segmentationSupported = 'segmentedBoth' + self.device_info.segmentationSupported = 'segmentedBoth' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedReceive': + elif self.device_info.segmentationSupported == 'segmentedReceive': pass - elif self.remoteDevice.segmentationSupported == 'segmentedBoth': + elif self.device_info.segmentationSupported == 'segmentedBoth': pass else: raise RuntimeError("invalid segmentation supported in device info") - if apdu.apduMaxSegs != self.remoteDevice.maxSegmentsAccepted: - if _debug: ServerSSM._debug(" - update maximum segments accepted?") - if apdu.apduMaxResp != self.remoteDevice.maxApduLengthAccepted: - if _debug: ServerSSM._debug(" - update maximum max APDU length accepted?") + # decode the maximum that the client can receive in one APDU, and if + # there is a value in the device information then use that one because + # it came from reading device object property value or from an I-Am + # message that was received + self.maxApduLengthAccepted = decode_max_apdu_length_accepted(apdu.apduMaxResp) + if self.device_info and self.device_info.maxApduLengthAccepted is not None: + if self.device_info.maxApduLengthAccepted < self.maxApduLengthAccepted: + if _debug: ServerSSM._debug(" - apduMaxResp encoding error") + else: + self.maxApduLengthAccepted = self.device_info.maxApduLengthAccepted + if _debug: ServerSSM._debug(" - maxApduLengthAccepted: %r", self.maxApduLengthAccepted) - # save the number of segments the client is willing to accept in the ack - self.maxSegmentsAccepted = apdu.apduMaxSegs + # save the number of segments the client is willing to accept in the ack, + # if this is None then the value is unknown or more than 64 + self.maxSegmentsAccepted = decode_max_segments_accepted(apdu.apduMaxSegs) # unsegmented request if not apdu.apduSeg: @@ -877,7 +947,7 @@ def idle(self, apdu): return # make sure we support segmented requests - if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return @@ -885,17 +955,18 @@ def idle(self, apdu): # save the request and set the segmentation context self.set_segmentation_context(apdu) - # the window size is the minimum of what I'm willing to receive and - # what the device has said it would like to send - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # the window size is the minimum of what I would propose and what the + # device has proposed + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + if _debug: ServerSSM._debug(" - actualWindowSize? min(%r, %r) -> %r", apdu.apduWin, self.proposedWindowSize, self.actualWindowSize) # initialize the state self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) if _debug: ServerSSM._debug(" - segAck: %r", segack) self.response(segack) @@ -928,10 +999,10 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - segment %d received out of order, should be %d", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256) # segment received out of order - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) return @@ -947,7 +1018,7 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - no more follows") # send back a final segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.response(segack) # forward the whole thing to the application @@ -958,17 +1029,17 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) else: # wait for more segments if _debug: ServerSSM._debug(" - wait for more segments") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) def segmented_request_timeout(self): if _debug: ServerSSM._debug("segmented_request_timeout") @@ -1008,10 +1079,13 @@ def segmented_response(self, apdu): if (apdu.apduType == SegmentAckPDU.pduType): if _debug: ServerSSM._debug(" - segment ack") + # actual window size is provided by client + self.actualWindowSize = apdu.apduWin + # duplicate ack received? if not self.in_window(apdu.apduSeq, self.initialSequenceNumber): if _debug: ServerSSM._debug(" - not in window") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: @@ -1024,8 +1098,8 @@ def segmented_response(self, apdu): self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # some kind of problem elif (apdu.apduType == AbortPDU.pduType): @@ -1039,10 +1113,10 @@ def segmented_response_timeout(self): if _debug: ServerSSM._debug("segmented_response_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: # give up self.set_state(ABORTED) @@ -1062,6 +1136,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): ServiceAccessPoint.__init__(self, sap) # save a reference to the device information cache + self.localDevice = localDevice self.deviceInfoCache = deviceInfoCache # client settings @@ -1072,27 +1147,19 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): self.serverTransactions = [] # confirmed request defaults - self.retryCount = 3 - self.retryTimeout = 3000 + self.numberOfApduRetries = 3 + self.apduTimeout = 3000 self.maxApduLengthAccepted = 1024 # segmentation defaults self.segmentationSupported = 'noSegmentation' self.segmentTimeout = 1500 - self.maxSegmentsAccepted = 8 + self.maxSegmentsAccepted = 2 + self.proposedWindowSize = 2 # device communication control self.dccEnableDisable = 'enable' - # local device object provides these - if localDevice: - self.retryCount = localDevice.numberOfApduRetries - self.retryTimeout = localDevice.apduTimeout - self.segmentationSupported = localDevice.segmentationSupported - self.segmentTimeout = localDevice.apduSegmentTimeout - self.maxSegmentsAccepted = localDevice.maxSegmentsAccepted - self.maxApduLengthAccepted = localDevice.maxApduLengthAccepted - # how long the state machine is willing to wait for the application # layer to form a response and send it self.applicationTimeout = 3000 @@ -1111,7 +1178,7 @@ def get_next_invoke_id(self, addr): raise RuntimeError("no available invoke ID") for tr in self.clientTransactions: - if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address): + if (invokeID == tr.invokeID) and (addr == tr.pdu_address): break else: break @@ -1152,14 +1219,11 @@ def confirmation(self, pdu): if isinstance(apdu, ConfirmedRequestPDU): # find duplicates of this request for tr in self.serverTransactions: - if (apdu.pduSource == tr.remoteDevice.address) and (apdu.apduInvokeID == tr.invokeID): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource) - # build a server transaction - tr = ServerSSM(self, remoteDevice) + tr = ServerSSM(self, apdu.pduSource) # add it to our transactions to track it self.serverTransactions.append(tr) @@ -1178,7 +1242,7 @@ def confirmation(self, pdu): # find the client transaction this is acking for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1190,7 +1254,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1199,7 +1263,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1211,7 +1275,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1220,7 +1284,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1264,19 +1328,15 @@ def sap_indication(self, apdu): else: # verify the invoke ID isn't already being used for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): raise RuntimeError("invoke ID in use") # warning for bogus requests if (apdu.pduDestination.addrType != Address.localStationAddr) and (apdu.pduDestination.addrType != Address.remoteStationAddr): StateMachineAccessPoint._warning("%s is not a local or remote station", apdu.pduDestination) - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduDestination) - if _debug: StateMachineAccessPoint._debug(" - remoteDevice: %r", remoteDevice) - # create a client transaction state machine - tr = ClientSSM(self, remoteDevice) + tr = ClientSSM(self, apdu.pduDestination) if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr) # add it to our transactions to track it @@ -1300,7 +1360,7 @@ def sap_confirmation(self, apdu): or isinstance(apdu, AbortPDU): # find the appropriate server transaction for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): break else: return @@ -1327,23 +1387,26 @@ def indication(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu) if isinstance(apdu, ConfirmedRequestPDU): + # assume no errors found + error_found = None + + # look up the class associated with the service atype = confirmed_request_types.get(apdu.apduService) if not atype: if _debug: ApplicationServiceAccessPoint._debug(" - no confirmed request decoder") - return - - # assume no errors found - error_found = None + error_found = UnrecognizedService() - try: - xpdu = atype() - xpdu.decode(apdu) - except RejectException as err: - ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) - error_found = err - except AbortException as err: - ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) - error_found = err + # no error so far, keep going + if not error_found: + try: + xpdu = atype() + xpdu.decode(apdu) + except RejectException as err: + ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) + error_found = err + except AbortException as err: + ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) + error_found = err # no error so far, keep going if not error_found: diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index 0477bf0d..0815c01e 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -541,7 +541,7 @@ def confirmation(self, pdu): return - elif isinstance(pdu, OriginalUnicastNPDU): + if isinstance(pdu, OriginalUnicastNPDU): # build a vanilla PDU xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) @@ -549,26 +549,31 @@ def confirmation(self, pdu): self.response(xpdu) return - # check the BBMD registration status, we may not be registered - if self.registrationStatus != 0: - if _debug: BIPForeign._debug(" - packet dropped, unregistered") - return - - if isinstance(pdu, ReadBroadcastDistributionTableAck): - # send this to the service access point - self.sap_response(pdu) + elif isinstance(pdu, ForwardedNPDU): + # check the BBMD registration status, we may not be registered + if self.registrationStatus != 0: + if _debug: BIPForeign._debug(" - packet dropped, unregistered") + return - elif isinstance(pdu, ReadForeignDeviceTableAck): - # send this to the service access point - self.sap_response(pdu) + # make sure the forwarded PDU from the bbmd + if pdu.pduSource != self.bbmdAddress: + if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") + return - elif isinstance(pdu, ForwardedNPDU): # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) # send it upstream self.response(xpdu) + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response xpdu = Result(code=0x0010, user_data=pdu.pduUserData) @@ -617,6 +622,9 @@ def confirmation(self, pdu): # send it downstream self.request(xpdu) + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPForeign._debug(" - packet dropped") + else: BIPForeign._warning("invalid pdu type: %s", type(pdu)) @@ -691,7 +699,7 @@ def indication(self, pdu): # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduDestination - if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) + if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -714,13 +722,13 @@ def indication(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -736,8 +744,9 @@ def confirmation(self, pdu): elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response - xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu = Result(code=0x0010, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduSource + if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -756,27 +765,38 @@ def confirmation(self, pdu): self.sap_response(pdu) elif isinstance(pdu, ForwardedNPDU): - # build a PDU with the source from the real source - xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) - # look for self as first entry in the BDT - if self.bbmdBDT and (self.bbmdBDT[0] == self.bbmdAddress): - xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") - self.request(xpdu) + # if this was unicast to us, do next hop + if pdu.pduDestination.addrType == Address.localStationAddr: + if _debug: BIPBBMD._debug(" - unicast message") + + # if this BBMD is listed in its BDT, send a local broadcast + if self.bbmdAddress in self.bbmdBDT: + xpdu.pduDestination = LocalBroadcast() + if _debug: BIPBBMD._debug(" - local broadcast") + self.request(xpdu) + + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + if _debug: BIPBBMD._debug(" - directed broadcast message") + + else: + BIPBBMD._warning("invalid destination address: %r", pdu.pduDestination) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, RegisterForeignDevice): @@ -815,12 +835,13 @@ def confirmation(self, pdu): self.request(xpdu) elif isinstance(pdu, DistributeBroadcastToNetwork): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -830,35 +851,37 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte == self.bbmdAddress: xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") + if _debug: BIPBBMD._debug(" - local broadcast") self.request(xpdu) else: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the other registered foreign devices for fdte in self.bbmdFDT: if fdte.fdAddress != pdu.pduSource: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) elif isinstance(pdu, OriginalBroadcastNPDU): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # make a forwarded PDU xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -868,13 +891,13 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -924,7 +947,7 @@ def delete_foreign_device_table_entry(self, addr): del self.bbmdFDT[i] break else: - stat = 99 ### entry not found + stat = 0x0050 ### entry not found # return status return stat @@ -951,10 +974,6 @@ def add_peer(self, addr): else: raise TypeError("addr must be a string or an Address") - # if it's this BBMD, make it the first one - if self.bbmdBDT and (addr == self.bbmdAddress): - raise RuntimeError("add self to BDT as first address") - # see if it's already there for bdte in self.bbmdBDT: if addr == bdte: diff --git a/py34/bacpypes/constructeddata.py b/py34/bacpypes/constructeddata.py index c8eb755b..574901ae 100755 --- a/py34/bacpypes/constructeddata.py +++ b/py34/bacpypes/constructeddata.py @@ -145,6 +145,7 @@ def decode(self, taglist): for element in self.sequenceElements: tag = taglist.Peek() + if _debug: Sequence._debug(" - element, tag: %r, %r", element, tag) # no more elements if tag is None: @@ -191,7 +192,29 @@ def decode(self, taglist): if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) - # check for an atomic element + # check for an any atomic element + elif issubclass(element.klass, AnyAtomic): + # convert it to application encoding + if element.context is not None: + raise InvalidTag("%s any atomic with context tag %d" % (element.name, element.context)) + + if tag.tagClass != Tag.applicationTagClass: + if not element.optional: + raise InvalidParameterDatatype("%s expected any atomic application tag" % (element.name,)) + else: + setattr(self, element.name, None) + continue + + # consume the tag + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = element.klass(tag) + + # now save the value + setattr(self, element.name, helper.value) + + # check for specific kind of atomic element, or the context says what kind elif issubclass(element.klass, Atomic): # convert it to application encoding if element.context is not None: @@ -410,6 +433,9 @@ def __len__(self): def __getitem__(self, item): return self.value[item] + def __iter__(self): + return iter(self.value) + def encode(self, taglist): if _debug: _SequenceOf._debug("(%r)encode %r", self.__class__.__name__, taglist) for value in self.value: @@ -749,6 +775,9 @@ def __delitem__(self, item): del self.value[item] self.value[0] -= 1 + def __iter__(self): + return iter(self.value[1:]) + def index(self, value): # only search through values for i in range(1, self.value[0] + 1): @@ -1342,8 +1371,13 @@ def decode(self, tag): # get the data self.value = tag.app_to_object() + @classmethod + def is_valid(cls, arg): + """Return True if arg is valid value for the class.""" + return isinstance(arg, Atomic) and not isinstance(arg, AnyAtomic) + def __str__(self): - return "AnyAtomic(%s)" % (str(self.value), ) + return "%s(%s)" % (self.__class__.__name__, str(self.value)) def __repr__(self): desc = self.__module__ + '.' + self.__class__.__name__ diff --git a/py34/bacpypes/local/__init__.py b/py34/bacpypes/local/__init__.py new file mode 100644 index 00000000..277c3c76 --- /dev/null +++ b/py34/bacpypes/local/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Local Object Subpackage +""" + +from . import object +from . import device +from . import file +from . import schedule + diff --git a/py34/bacpypes/local/device.py b/py34/bacpypes/local/device.py new file mode 100644 index 00000000..88d7ba99 --- /dev/null +++ b/py34/bacpypes/local/device.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ + OctetString, CharacterString, BitString, Enumerated, Date, Time, \ + ObjectIdentifier +from ..constructeddata import ArrayOf +from ..basetypes import ServicesSupported + +from ..errors import ExecutionError +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentLocalDate +# + +class CurrentLocalDate(Property): + + def __init__(self): + Property.__init__(self, 'localDate', Date, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentLocalTime +# + +class CurrentLocalTime(Property): + + def __init__(self): + Property.__init__(self, 'localTime', Time, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentProtocolServicesSupported +# + +@bacpypes_debugging +class CurrentProtocolServicesSupported(Property): + + def __init__(self): + if _debug: CurrentProtocolServicesSupported._debug("__init__") + Property.__init__(self, 'protocolServicesSupported', ServicesSupported, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentProtocolServicesSupported._debug("ReadProperty %r %r", obj, arrayIndex) + + # not an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return what the application says + return obj._app.get_services_supported() + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# LocalDeviceObject +# + +@bacpypes_debugging +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): + + properties = [ + CurrentLocalTime(), + CurrentLocalDate(), + CurrentProtocolServicesSupported(), + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # start with an empty dictionary of device object properties + init_args = {} + ini_arg = kwargs.get('ini', None) + if _debug: LocalDeviceObject._debug(" - ini_arg: %r", dir(ini_arg)) + + # check for registration as a keyword parameter or in the INI file + if self.__class__ not in registered_object_types.values(): + if _debug: LocalDeviceObject._debug(" - unregistered") + + vendor_identifier = kwargs.get('vendorIdentifier', None) + if _debug: LocalDeviceObject._debug(" - keyword vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + vendor_identifier = getattr(ini_arg, 'vendoridentifier', None) + if _debug: LocalDeviceObject._debug(" - INI vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + + register_object_type(self.__class__, vendor_id=vendor_identifier) + + # look for properties, fill in values from the keyword arguments or + # the INI parameter (converted to a proper value) if it was provided + for propid, prop in self._properties.items(): + # special processing for object identifier + if propid == 'objectIdentifier': + continue + + # use keyword argument if it was provided + if propid in kwargs: + prop_value = kwargs[propid] + else: + prop_value = getattr(ini_arg, propid.lower(), None) + if prop_value is None: + continue + + prop_datatype = prop.datatype + + if issubclass(prop_datatype, Null): + if prop_value != "Null": + raise ValueError("invalid null property value: %r" % (propid,)) + prop_value = None + + elif issubclass(prop_datatype, Boolean): + prop_value = prop_value.lower() + if prop_value not in ('true', 'false', 'set', 'reset'): + raise ValueError("invalid boolean property value: %r" % (propid,)) + prop_value = prop_value in ('true', 'set') + + elif issubclass(prop_datatype, (Unsigned, Integer)): + try: + prop_value = int(prop_value) + except ValueError: + raise ValueError("invalid unsigned or integer property value: %r" % (propid,)) + + elif issubclass(prop_datatype, (Real, Double)): + try: + prop_value = float(prop_value) + except ValueError: + raise ValueError("invalid real or double property value: %r" % (propid,)) + + elif issubclass(prop_datatype, OctetString): + try: + prop_value = xtob(prop_value) + except: + raise ValueError("invalid octet string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, CharacterString): + pass + + elif issubclass(prop_datatype, BitString): + try: + bstr, prop_value = prop_value, [] + for b in bstr: + if b not in ('0', '1'): + raise ValueError + prop_value.append(int(b)) + except: + raise ValueError("invalid bit string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, Enumerated): + pass + + else: + raise ValueError("cannot interpret %r INI paramter" % (propid,)) + if _debug: LocalDeviceObject._debug(" - property %r: %r", propid, prop_value) + + # at long last + init_args[propid] = prop_value + + # check for object identifier as a keyword parameter or in the INI file, + # and it might be just an int, so make it a tuple if necessary + if 'objectIdentifier' in kwargs: + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, int): + object_identifier = ('device', object_identifier) + elif hasattr(ini_arg, 'objectidentifier'): + object_identifier = ('device', int(getattr(ini_arg, 'objectidentifier'))) + else: + raise RuntimeError("objectIdentifier is required") + init_args['objectIdentifier'] = object_identifier + if _debug: LocalDeviceObject._debug(" - object identifier: %r", object_identifier) + + # fill in default property values not in init_args + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in init_args: + init_args[attr] = value + + # check for properties this class implements + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + if 'protocolServicesSupported' in kwargs: + raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + init_args['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + + # check for a minimum value + if init_args['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - init_args: %r", init_args) + + # proceed as usual + super(LocalDeviceObject, self).__init__(**init_args) + + # pass along special property values that are not BACnet properties + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) + diff --git a/py34/bacpypes/local/file.py b/py34/bacpypes/local/file.py new file mode 100644 index 00000000..8007bef4 --- /dev/null +++ b/py34/bacpypes/local/file.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +@bacpypes_debugging +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +# +# Local Stream Access File Object Type +# + +@bacpypes_debugging +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + diff --git a/py34/bacpypes/local/object.py b/py34/bacpypes/local/object.py new file mode 100644 index 00000000..3078b9c6 --- /dev/null +++ b/py34/bacpypes/local/object.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..basetypes import PropertyIdentifier +from ..constructeddata import ArrayOf + +from ..errors import ExecutionError +from ..object import Property, Object + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +@bacpypes_debugging +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + diff --git a/py34/bacpypes/local/schedule.py b/py34/bacpypes/local/schedule.py new file mode 100644 index 00000000..d22911a3 --- /dev/null +++ b/py34/bacpypes/local/schedule.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..core import deferred +from ..task import OneShotTask + +from ..primitivedata import Atomic, Null, Unsigned, Date, Time +from ..constructeddata import Array +from ..object import get_datatype, ScheduleObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# match_date +# + +def match_date(date, date_pattern): + """ + Match a specific date, a four-tuple with no special values, with a date + pattern, four-tuple possibly having special values. + """ + # unpack the date and pattern + year, month, day, day_of_week = date + year_p, month_p, day_p, day_of_week_p = date_pattern + + # check the year + if year_p == 255: + # any year + pass + elif year != year_p: + # specific year + return False + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the day + if day_p == 255: + # any day + pass + elif day_p == 32: + # last day of the month + last_day = calendar.monthrange(year + 1900, month)[1] + if day != last_day: + return False + elif day_p == 33: + # odd days of the month + if (day % 2) == 0: + return False + elif day_p == 34: + # even days of the month + if (day % 2) == 1: + return False + elif day != day_p: + # specific day + return False + + # check the day of week + if day_of_week_p == 255: + # any day of the week + pass + elif day_of_week != day_of_week_p: + # specific day of the week + return False + + # all tests pass + return True + +# +# match_date_range +# + +def match_date_range(date, date_range): + """ + Match a specific date, a four-tuple with no special values, with a DateRange + object which as a start date and end date. + """ + return (date[:3] >= date_range.startDate[:3]) \ + and (date[:3] <= date_range.endDate[:3]) + +# +# match_weeknday +# + +def match_weeknday(date, weeknday): + """ + Match a specific date, a four-tuple with no special values, with a + BACnetWeekNDay, an octet string with three (unsigned) octets. + """ + # unpack the date + year, month, day, day_of_week = date + last_day = calendar.monthrange(year + 1900, month)[1] + + # unpack the date pattern octet string + weeknday_unpacked = [c for c in weeknday] + month_p, week_of_month_p, day_of_week_p = weeknday_unpacked + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the week of the month + if week_of_month_p == 255: + # any week + pass + elif week_of_month_p == 1: + # days numbered 1-7 + if (day > 7): + return False + elif week_of_month_p == 2: + # days numbered 8-14 + if (day < 8) or (day > 14): + return False + elif week_of_month_p == 3: + # days numbered 15-21 + if (day < 15) or (day > 21): + return False + elif week_of_month_p == 4: + # days numbered 22-28 + if (day < 22) or (day > 28): + return False + elif week_of_month_p == 5: + # days numbered 29-31 + if (day < 29) or (day > 31): + return False + elif week_of_month_p == 6: + # last 7 days of this month + if (day < last_day - 6): + return False + elif week_of_month_p == 7: + # any of the 7 days prior to the last 7 days of this month + if (day < last_day - 13) or (day > last_day - 7): + return False + elif week_of_month_p == 8: + # any of the 7 days prior to the last 14 days of this month + if (day < last_day - 20) or (day > last_day - 14): + return False + elif week_of_month_p == 9: + # any of the 7 days prior to the last 21 days of this month + if (day < last_day - 27) or (day > last_day - 21): + return False + + # check the day + if day_of_week_p == 255: + # any day + pass + elif day_of_week != day_of_week_p: + # specific day + return False + + # all tests pass + return True + +# +# date_in_calendar_entry +# + +@bacpypes_debugging +def date_in_calendar_entry(date, calendar_entry): + if _debug: date_in_calendar_entry._debug("date_in_calendar_entry %r %r", date, calendar_entry) + + match = False + if calendar_entry.date: + match = match_date(date, calendar_entry.date) + elif calendar_entry.dateRange: + match = match_date_range(date, calendar_entry.dateRange) + elif calendar_entry.weekNDay: + match = match_weeknday(date, calendar_entry.weekNDay) + else: + raise RuntimeError("") + if _debug: date_in_calendar_entry._debug(" - match: %r", match) + + return match + +# +# datetime_to_time +# + +def datetime_to_time(date, time): + """Take the date and time 4-tuples and return the time in seconds since + the epoch as a floating point number.""" + if (255 in date) or (255 in time): + raise RuntimeError("specific date and time required") + + time_tuple = ( + date[0]+1900, date[1], date[2], + time[0], time[1], time[2], + 0, 0, -1, + ) + return _mktime(time_tuple) + +# +# LocalScheduleObject +# + +@bacpypes_debugging +class LocalScheduleObject(CurrentPropertyListMixIn, ScheduleObject): + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + + # make sure present value was provided + if 'presentValue' not in kwargs: + raise RuntimeError("presentValue required") + if not isinstance(kwargs['presentValue'], Atomic): + raise TypeError("presentValue must be an Atomic value") + + # continue initialization + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors to check the reliability if these change + for prop in ('weeklySchedule', 'exceptionSchedule', 'scheduleDefault'): + self._property_monitors[prop].append(self._check_reliability) + + # check it now + self._check_reliability() + + def _check_reliability(self, old_value=None, new_value=None): + """This function is called when the object is created and after + one of its configuration properties has changed. The new and old value + parameters are ignored, this is called after the property has been + changed and this is only concerned with the current value.""" + if _debug: LocalScheduleObject._debug("_check_reliability %r %r", old_value, new_value) + + try: + schedule_default = self.scheduleDefault + + if schedule_default is None: + raise ValueError("scheduleDefault expected") + if not isinstance(schedule_default, Atomic): + raise TypeError("scheduleDefault must be an instance of an atomic type") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + # check the weekly schedule values + if self.weeklySchedule: + for daily_schedule in self.weeklySchedule: + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleObject._debug(" - daily time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + elif 255 in time_value.time: + if _debug: LocalScheduleObject._debug(" - wildcard in time") + raise ValueError("must be a specific time") + + # check the exception schedule values + if self.exceptionSchedule: + for special_event in self.exceptionSchedule: + for time_value in special_event.listOfTimeValues: + if _debug: LocalScheduleObject._debug(" - special event time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + + # check list of object property references + obj_prop_refs = self.listOfObjectPropertyReferences + if obj_prop_refs: + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + raise RuntimeError("no external references") + + # get the datatype of the property to be written + obj_type = obj_prop_ref.objectIdentifier[0] + datatype = get_datatype(obj_type, obj_prop_ref.propertyIdentifier) + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if issubclass(datatype, Array) and (obj_prop_ref.propertyArrayIndex is not None): + if obj_prop_ref.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if datatype is not schedule_datatype: + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + datatype, + schedule_datatype, + ) + raise TypeError("wrong type") + + # all good + self.reliability = 'noFaultDetected' + if _debug: LocalScheduleObject._debug(" - no fault detected") + + except Exception as err: + if _debug: LocalScheduleObject._debug(" - exception: %r", err) + self.reliability = 'configurationError' + +# +# LocalScheduleInterpreter +# + +@bacpypes_debugging +class LocalScheduleInterpreter(OneShotTask): + + def __init__(self, sched_obj): + if _debug: LocalScheduleInterpreter._debug("__init__ %r", sched_obj) + OneShotTask.__init__(self) + + # reference the schedule object to update + self.sched_obj = sched_obj + + # add a monitor for the present value + sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + + # call to interpret the schedule + deferred(self.process_task) + + def present_value_changed(self, old_value, new_value): + """This function is called when the presentValue of the local schedule + object has changed, both internally by this interpreter, or externally + by some client using WriteProperty.""" + if _debug: LocalScheduleInterpreter._debug("present_value_changed %s %s", old_value, new_value) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # process the list of [device] object property [array index] references + obj_prop_refs = self.sched_obj.listOfObjectPropertyReferences + if not obj_prop_refs: + if _debug: LocalScheduleInterpreter._debug(" - no writes defined") + return + + # primitive values just set the value part + new_value = new_value.value + + # loop through the writes + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + if _debug: LocalScheduleInterpreter._debug(" - no externals") + continue + + # get the object from the application + obj = self.sched_obj._app.get_object_id(obj_prop_ref.objectIdentifier) + if not obj: + if _debug: LocalScheduleInterpreter._debug(" - no object") + continue + + # try to change the value + try: + obj.WriteProperty( + obj_prop_ref.propertyIdentifier, + new_value, + arrayIndex=obj_prop_ref.propertyArrayIndex, + priority=self.sched_obj.priorityForWriting, + ) + if _debug: LocalScheduleInterpreter._debug(" - success") + except Exception as err: + if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + + def process_task(self): + if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) + + # check for a valid configuration + if self.sched_obj.reliability != 'noFaultDetected': + if _debug: LocalScheduleInterpreter._debug(" - fault detected") + return + + # get the date and time from the device object in case it provides + # some custom functionality + if self.sched_obj._app and self.sched_obj._app.localDevice: + current_date = self.sched_obj._app.localDevice.localDate + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = self.sched_obj._app.localDevice.localTime + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + else: + # get the current date and time, as provided by the task manager + current_date = Date().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = Time().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + + # evaluate the time + current_value, next_transition = self.eval(current_date, current_time) + if _debug: LocalScheduleInterpreter._debug(" - current_value, next_transition: %r, %r", current_value, next_transition) + + ### set the present value + self.sched_obj.presentValue = current_value + + # compute the time of the next transition + transition_time = datetime_to_time(current_date, next_transition) + + # install this to run again + self.install_task(transition_time) + + def eval(self, edate, etime): + """Evaluate the schedule according to the provided date and time and + return the appropriate present value, or None if not in the effective + period.""" + if _debug: LocalScheduleInterpreter._debug("eval %r %r", edate, etime) + + # reference the schedule object + sched_obj = self.sched_obj + if _debug: LocalScheduleInterpreter._debug(" sched_obj: %r", sched_obj) + + # verify the date falls in the effective period + if not match_date_range(edate, sched_obj.effectivePeriod): + return None + + # the event priority is a list of values that are in effect for + # exception schedules with the special event priority, see 135.1-2013 + # clause 7.3.2.23.10.3.8, Revision 4 Event Priority Test + event_priority = [None] * 16 + + next_day = (24, 0, 0, 0) + next_transition_time = [None] * 16 + + # check the exception schedule values + if sched_obj.exceptionSchedule: + for special_event in sched_obj.exceptionSchedule: + if _debug: LocalScheduleInterpreter._debug(" - special_event: %r", special_event) + + # check the special event period + special_event_period = special_event.period + if special_event_period is None: + raise RuntimeError("special event period required") + + match = False + calendar_entry = special_event_period.calendarEntry + if calendar_entry: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + else: + # get the calendar object from the application + calendar_object = sched_obj._app.get_object_id(special_event_period.calendarReference) + if not calendar_object: + raise RuntimeError("invalid calendar object reference") + if _debug: LocalScheduleInterpreter._debug(" - calendar_object: %r", calendar_object) + + for calendar_entry in calendar_object.dateList: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + if match: + break + + # didn't match the period, try the next special event + if not match: + if _debug: LocalScheduleInterpreter._debug(" - no matching calendar entry") + continue + + # event priority array index + priority = special_event.eventPriority - 1 + if _debug: LocalScheduleInterpreter._debug(" - priority: %r", priority) + + # look for all of the possible times + for time_value in special_event.listOfTimeValues: + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - relinquish exception @ %r", tval) + event_priority[priority] = None + next_transition_time[priority] = None + else: + if _debug: LocalScheduleInterpreter._debug(" - consider exception @ %r", tval) + event_priority[priority] = time_value.value + next_transition_time[priority] = next_day + else: + next_transition_time[priority] = tval + break + + # assume the next transition will be at the start of the next day + earliest_transition = next_day + + # check if any of the special events came up with something + for priority_value, next_transition in zip(event_priority, next_transition_time): + if next_transition is not None: + earliest_transition = min(earliest_transition, next_transition) + if priority_value is not None: + if _debug: LocalScheduleInterpreter._debug(" - priority_value: %r", priority_value) + return priority_value, earliest_transition + + # start out with the default + daily_value = sched_obj.scheduleDefault + + # check the daily schedule + if sched_obj.weeklySchedule: + daily_schedule = sched_obj.weeklySchedule[edate[3]] + if _debug: LocalScheduleInterpreter._debug(" - daily_schedule: %r", daily_schedule) + + # look for all of the possible times + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleInterpreter._debug(" - time_value: %r", time_value) + + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - back to normal @ %r", tval) + daily_value = sched_obj.scheduleDefault + else: + if _debug: LocalScheduleInterpreter._debug(" - new value @ %r", tval) + daily_value = time_value.value + else: + earliest_transition = min(earliest_transition, tval) + break + + # return what was matched, if anything + return daily_value, earliest_transition + diff --git a/py34/bacpypes/netservice.py b/py34/bacpypes/netservice.py index 859be42c..391abea7 100755 --- a/py34/bacpypes/netservice.py +++ b/py34/bacpypes/netservice.py @@ -27,32 +27,134 @@ ROUTER_UNREACHABLE = 3 # cannot route # -# NetworkReference +# RouterInfo # -class NetworkReference: - """These objects map a network to a router.""" +class RouterInfo(DebugContents): + """These objects are routing information records that map router + addresses with destination networks.""" - def __init__(self, net, router, status): - self.network = net - self.router = router - self.status = status + _debug_contents = ('snet', 'address', 'dnets', 'status') + + def __init__(self, snet, address, dnets, status=ROUTER_AVAILABLE): + self.snet = snet # source network + self.address = address # address of the router + self.dnets = dnets # list of reachable networks through this router + self.status = status # router status # -# RouterReference +# RouterInfoCache # -class RouterReference(DebugContents): - """These objects map a router; the adapter to talk to it, - its address, and a list of networks that it routes to.""" +@bacpypes_debugging +class RouterInfoCache: + + def __init__(self): + if _debug: RouterInfoCache._debug("__init__") + + self.routers = {} # (snet, address) -> RouterInfo + self.networks = {} # network -> RouterInfo + + def get_router_info(self, dnet): + if _debug: RouterInfoCache._debug("get_router_info %r", dnet) + + # check to see if we know about it + if dnet not in self.networks: + if _debug: RouterInfoCache._debug(" - no route") + return None + + # return the network and address + router_info = self.networks[dnet] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # return the network, address, and status + return (router_info.snet, router_info.address, router_info.status) + + def update_router_info(self, snet, address, dnets): + if _debug: RouterInfoCache._debug("update_router_info %r %r %r", snet, address, dnets) + + # look up the router reference, make a new record if necessary + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - new router") + router_info = self.routers[key] = RouterInfo(snet, address, list()) + else: + router_info = self.routers[key] + + # add (or move) the destination networks + for dnet in dnets: + if dnet in self.networks: + other_router = self.networks[dnet] + if other_router is router_info: + if _debug: RouterInfoCache._debug(" - existing router, match") + continue + elif dnet not in other_router.dnets: + if _debug: RouterInfoCache._debug(" - where did it go?") + else: + other_router.dnets.remove(dnet) + if not other_router.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[(snet, other_router.address)] + + # add a reference to the router + self.networks[dnet] = router_info + if _debug: RouterInfoCache._debug(" - reference added") + + # maybe update the list of networks for this router + if dnet not in router_info.dnets: + router_info.dnets.append(dnet) + if _debug: RouterInfoCache._debug(" - dnet added, now: %r", router_info.dnets) + + def update_router_status(self, snet, address, status): + if _debug: RouterInfoCache._debug("update_router_status %r %r %r", snet, address, status) + + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - not a router we care about") + return + + router_info = self.routers[key] + router_info.status = status + if _debug: RouterInfoCache._debug(" - status updated") + + def delete_router_info(self, snet, address=None, dnets=None): + if _debug: RouterInfoCache._debug("delete_router_info %r %r %r", dnets) + + # if address is None, remove all the routers for the network + if address is None: + for rnet, raddress in self.routers.keys(): + if snet == rnet: + if _debug: RouterInfoCache._debug(" - going down") + self.delete_router_info(snet, raddress) + if _debug: RouterInfoCache._debug(" - back topside") + return + + # look up the router reference + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - unknown router") + return + + router_info = self.routers[key] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) - _debug_contents = ('adapter-', 'address', 'networks', 'status') + # if dnets is None, remove all the networks for the router + if dnets is None: + dnets = router_info.dnets + + # loop through the list of networks to be deleted + for dnet in dnets: + if dnet in self.networks: + del self.networks[dnet] + if _debug: RouterInfoCache._debug(" - removed from networks: %r", dnet) + if dnet in router_info.dnets: + router_info.dnets.remove(dnet) + if _debug: RouterInfoCache._debug(" - removed from router_info: %r", dnet) - def __init__(self, adapter, addr, nets, status): - self.adapter = adapter - self.address = addr # local station relative to the adapter - self.networks = nets # list of remote networks - self.status = status # status as presented by the router + # see if we still care + if not router_info.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[key] # # NetworkAdapter @@ -64,14 +166,11 @@ class NetworkAdapter(Client, DebugContents): _debug_contents = ('adapterSAP-', 'adapterNet') def __init__(self, sap, net, cid=None): - if _debug: NetworkAdapter._debug("__init__ %r (net=%r) cid=%r", sap, net, cid) + if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) Client.__init__(self, cid) self.adapterSAP = sap self.adapterNet = net - # add this to the list of adapters for the network - sap.adapters.append(self) - def confirmation(self, pdu): """Decode upstream PDUs and pass them up to the service access point.""" if _debug: NetworkAdapter._debug("confirmation %r (net=%r)", pdu, self.adapterNet) @@ -105,117 +204,73 @@ class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): , 'localAdapter-', 'localAddress' ) - def __init__(self, sap=None, sid=None): + def __init__(self, routerInfoCache=None, sap=None, sid=None): if _debug: NetworkServiceAccessPoint._debug("__init__ sap=%r sid=%r", sap, sid) ServiceAccessPoint.__init__(self, sap) Server.__init__(self, sid) - self.adapters = [] # list of adapters - self.routers = {} # (adapter, address) -> RouterReference - self.networks = {} # network -> RouterReference + # map of directly connected networks + self.adapters = {} # net -> NetworkAdapter + + # use the provided cache or make a default one + self.router_info_cache = routerInfoCache or RouterInfoCache() + + # map to a list of application layer packets waiting for a path + self.pending_nets = {} - self.localAdapter = None # which one is local - self.localAddress = None # what is the local address + # these are set when bind() is called + self.local_adapter = None + self.local_address = None def bind(self, server, net=None, address=None): """Create a network adapter object and bind.""" if _debug: NetworkServiceAccessPoint._debug("bind %r net=%r address=%r", server, net, address) - if (net is None) and self.adapters: + # make sure this hasn't already been called with this network + if net in self.adapters: raise RuntimeError("already bound") - # create an adapter object + # when binding to an adapter and there is more than one, then they + # must all have network numbers and one of them will be the default + if (net is not None) and (None in self.adapters): + raise RuntimeError("default adapter bound") + + # create an adapter object, add it to our map adapter = NetworkAdapter(self, net) + self.adapters[net] = adapter + if _debug: NetworkServiceAccessPoint._debug(" - adapters[%r]: %r", net, adapter) # if the address was given, make it the "local" one if address: - self.localAdapter = adapter - self.localAddress = address + self.local_adapter = adapter + self.local_address = address # bind to the server bind(adapter, server) #----- - def add_router_references(self, adapter, address, netlist): + def add_router_references(self, snet, address, dnets): """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", adapter, address, netlist) - - # make a key for the router reference - rkey = (adapter, address) - - for snet in netlist: - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] - - if rref.adapter == adapter and rref.address == address: - pass # matches current entry - else: - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] - - # remove the network - del self.networks[snet] + if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) - ### check to see if it is OK to add the new entry + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - # get the router reference for this router - rref = self.routers.get(rkey, None) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # pass this along to the cache + self.router_info_cache.update_router_info(snet, address, dnets) - # reference the snet - self.networks[snet] = rref - else: - # new reference - rref = RouterReference( adapter, address, [snet], 0) - self.routers[rkey] = rref + def delete_router_references(self, snet, address=None, dnets=None): + """Delete references to routers/networks.""" + if _debug: NetworkServiceAccessPoint._debug("delete_router_references %r %r %r", snet, address, dnets) - # reference the snet - self.networks[snet] = rref + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - def remove_router_references(self, adapter, address=None): - """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("remove_router_references %r %r", adapter, address) - - delrlist = [] - delnlist = [] - # scan through the dictionary of router references - for rkey in self.routers.keys(): - # rip apart the key - radapter, raddress = rkey - - # pick all references on the adapter, optionally limited to a specific address - match = radapter is adapter - if match and address is not None: - match = (raddress == address) - if not match: - continue - - # save it for deletion - delrlist.append(rkey) - delnlist.extend(self.routers[rkey].networks) - if _debug: - NetworkServiceAccessPoint._debug(" - delrlist: %r", delrlist) - NetworkServiceAccessPoint._debug(" - delnlist: %r", delnlist) - - # delete the entries - for rkey in delrlist: - try: - del self.routers[rkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - rkey not in self.routers: %r", rkey) - for nkey in delnlist: - try: - del self.networks[nkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - nkey not in self.networks: %r", rkey) + # pass this along to the cache + self.router_info_cache.delete_router_info(snet, address, dnets) #----- @@ -227,11 +282,12 @@ def indication(self, pdu): raise ConfigurationError("no adapters") # might be able to relax this restriction - if (len(self.adapters) > 1) and (not self.localAdapter): + if (len(self.adapters) > 1) and (not self.local_adapter): raise ConfigurationError("local adapter must be set") # get the local adapter - adapter = self.localAdapter or self.adapters[0] + adapter = self.local_adapter or self.adapters[None] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # build a generic APDU apdu = _APDU(user_data=pdu.pduUserData) @@ -263,7 +319,7 @@ def indication(self, pdu): npdu.npduDADR = apdu.pduDestination # send it to all of connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): xadapter.process_npdu(npdu) return @@ -279,32 +335,53 @@ def indication(self, pdu): ### when it's a directly connected network raise RuntimeError("addressing problem") - # check for an available path - if dnet in self.networks: - rref = self.networks[dnet] - adapter = rref.adapter + # get it ready to send when the path is found + npdu.pduDestination = None + npdu.npduDADR = apdu.pduDestination + + # we might already be waiting for a path for this network + if dnet in self.pending_nets: + if _debug: NetworkServiceAccessPoint._debug(" - already waiting for path") + self.pending_nets[dnet].append(npdu) + return + + # check cache for an available path + path_info = self.router_info_cache.get_router_info(dnet) - ### make sure the direct connect is OK, may need to connect + # if there is info, we have a path + if path_info: + snet, address, status = path_info + if _debug: NetworkServiceAccessPoint._debug(" - path found: %r, %r, %r", snet, address, status) - ### make sure the peer router is OK, may need to connect + # check for an adapter + if snet not in self.adapters: + raise RuntimeError("network found but not connected: %r", snet) + adapter = self.adapters[snet] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # fix the destination - npdu.pduDestination = rref.address - npdu.npduDADR = apdu.pduDestination + npdu.pduDestination = address # send it along adapter.process_npdu(npdu) return - if _debug: NetworkServiceAccessPoint._debug(" - no known path to network, broadcast to discover it") + if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") - # set the destination - npdu.pduDestination = LocalBroadcast() - npdu.npduDADR = apdu.pduDestination + # add it to the list of packets waiting for the network + net_list = self.pending_nets.get(dnet, None) + if net_list is None: + net_list = self.pending_nets[dnet] = [] + net_list.append(npdu) + + # build a request for the network and send it to all of the adapters + xnpdu = WhoIsRouterToNetwork(dnet) + xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: - xadapter.process_npdu(npdu) + for adapter in self.adapters.values(): + ### make sure the adapter is OK + self.sap_indication(adapter, xnpdu) def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("process_npdu %r %r", adapter, npdu) @@ -312,83 +389,68 @@ def process_npdu(self, adapter, npdu): # make sure our configuration is OK if (not self.adapters): raise ConfigurationError("no adapters") - if (len(self.adapters) > 1) and (not self.localAdapter): - raise ConfigurationError("local adapter must be set") # check for source routing if npdu.npduSADR and (npdu.npduSADR.addrType != Address.nullAddr): + if _debug: NetworkServiceAccessPoint._debug(" - check source path") + # see if this is attempting to spoof a directly connected network snet = npdu.npduSADR.addrNet - for xadapter in self.adapters: - if (xadapter is not adapter) and (snet == xadapter.adapterNet): - NetworkServiceAccessPoint._warning("spoof?") - ### log this - return - - # make a key for the router reference - rkey = (adapter, npdu.pduSource) - - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] - if rref.adapter == adapter and rref.address == npdu.pduSource: - pass # matches current entry - else: - if _debug: NetworkServiceAccessPoint._debug(" - replaces entry") - - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] + if snet in self.adapters: + NetworkServiceAccessPoint._warning(" - path error (1)") + return - # remove the network - del self.networks[snet] + # see if there is routing information for this source network + router_info = self.router_info_cache.get_router_info(snet) + if router_info: + router_snet, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug(" - router_address, router_status: %r, %r", router_address, router_status) - # get the router reference for this router - rref = self.routers.get(rkey) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # see if the router has changed + if not (router_address == npdu.pduSource): + if _debug: NetworkServiceAccessPoint._debug(" - replacing path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) else: - # new reference - rref = RouterReference( adapter, npdu.pduSource, [snet], 0) - self.routers[rkey] = rref + if _debug: NetworkServiceAccessPoint._debug(" - new path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) # check for destination routing if (not npdu.npduDADR) or (npdu.npduDADR.addrType == Address.nullAddr): - processLocally = (not self.localAdapter) or (adapter is self.localAdapter) or (npdu.npduNetMessage is not None) + if _debug: NetworkServiceAccessPoint._debug(" - no DADR") + + processLocally = (not self.local_adapter) or (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) forwardMessage = False elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote broadcast") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (2)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) forwardMessage = True elif npdu.npduDADR.addrType == Address.remoteStationAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote station") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (3)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) \ - and (npdu.npduDADR.addrAddr == self.localAddress.addrAddr) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ + and (npdu.npduDADR.addrAddr == self.local_address.addrAddr) forwardMessage = not processLocally elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - DADR is global broadcast") + processLocally = True forwardMessage = True @@ -402,6 +464,8 @@ def process_npdu(self, adapter, npdu): # application or network layer message if npdu.npduNetMessage is None: + if _debug: NetworkServiceAccessPoint._debug(" - application layer message") + if processLocally and self.serverPeer: if _debug: NetworkServiceAccessPoint._debug(" - processing APDU locally") @@ -411,7 +475,7 @@ def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug(" - apdu: %r", apdu) # see if it needs to look routed - if (len(self.adapters) > 1) and (adapter != self.localAdapter): + if (len(self.adapters) > 1) and (adapter != self.local_adapter): # combine the source address if not npdu.npduSADR: apdu.pduSource = RemoteStation( adapter.adapterNet, npdu.pduSource.addrAddr ) @@ -420,7 +484,7 @@ def process_npdu(self, adapter, npdu): # map the destination if not npdu.npduDADR: - apdu.pduDestination = self.localAddress + apdu.pduDestination = self.local_address elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: apdu.pduDestination = npdu.npduDADR elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: @@ -447,6 +511,8 @@ def process_npdu(self, adapter, npdu): self.response(apdu) else: + if _debug: NetworkServiceAccessPoint._debug(" - network layer message") + if processLocally: if npdu.npduNetMessage not in npdu_types: if _debug: NetworkServiceAccessPoint._debug(" - unknown npdu type: %r", npdu.npduNetMessage) @@ -461,18 +527,19 @@ def process_npdu(self, adapter, npdu): # pass to the service element self.sap_request(adapter, xpdu) - # maybe local processing only + # might not need to forward this to other devices if not forwardMessage: if _debug: NetworkServiceAccessPoint._debug(" - no forwarding") return # make sure we're really a router if (len(self.adapters) == 1): - if _debug: NetworkServiceAccessPoint._debug(" - not really a router") + if _debug: NetworkServiceAccessPoint._debug(" - not a router") return # make sure it hasn't looped if (npdu.npduHopCount == 0): + if _debug: NetworkServiceAccessPoint._debug(" - no more hops") return # build a new NPDU to send to other adapters @@ -493,9 +560,10 @@ def process_npdu(self, adapter, npdu): # if this is a broadcast it goes everywhere if npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - global broadcasting") newpdu.pduDestination = LocalBroadcast() - for xadapter in self.adapters: + for xadapter in self.adapters.values(): if (xadapter is not adapter): xadapter.process_npdu(_deepcopy(newpdu)) return @@ -503,36 +571,54 @@ def process_npdu(self, adapter, npdu): if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr) \ or (npdu.npduDADR.addrType == Address.remoteStationAddr): dnet = npdu.npduDADR.addrNet + if _debug: NetworkServiceAccessPoint._debug(" - remote station/broadcast") - # see if this should go to one of our directly connected adapters - for xadapter in self.adapters: - if dnet == xadapter.adapterNet: - if _debug: NetworkServiceAccessPoint._debug(" - found direct connect via %r", xadapter) - if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): - newpdu.pduDestination = LocalBroadcast() - else: - newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) + # see if this a locally connected network + if dnet in self.adapters: + xadapter = self.adapters[dnet] + if xadapter is adapter: + if _debug: NetworkServiceAccessPoint._debug(" - path error (4)") + return + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - # last leg in routing - newpdu.npduDADR = None + # if this was a remote broadcast, it's now a local one + if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): + newpdu.pduDestination = LocalBroadcast() + else: + newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) - # send the packet downstream - xadapter.process_npdu(_deepcopy(newpdu)) - return + # last leg in routing + newpdu.npduDADR = None - # see if we know how to get there - if dnet in self.networks: - rref = self.networks[dnet] - newpdu.pduDestination = rref.address + # send the packet downstream + xadapter.process_npdu(_deepcopy(newpdu)) + return + + # see if there is routing information for this destination network + router_info = self.router_info_cache.get_router_info(dnet) + if router_info: + router_net, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + + if router_net not in self.adapters: + if _debug: NetworkServiceAccessPoint._debug(" - path error (5)") + return - ### check to make sure the router is OK + xadapter = self.adapters[router_net] + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - ### check to make sure the network is OK, may need to connect + # the destination is the address of the router + newpdu.pduDestination = router_address # send the packet downstream - rref.adapter.process_npdu(_deepcopy(newpdu)) + xadapter.process_npdu(_deepcopy(newpdu)) return + if _debug: NetworkServiceAccessPoint._debug(" - no router info found") + ### queue this message for reprocessing when the response comes back # try to find a path to the network @@ -540,16 +626,17 @@ def process_npdu(self, adapter, npdu): xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): # skip the horse it rode in on if (xadapter is adapter): continue - ### make sure the adapter is OK + # pass this along as if it came from the NSE self.sap_indication(xadapter, xnpdu) - ### log this, what to do? - return + return + + if _debug: NetworkServiceAccessPoint._debug(" - bad DADR: %r", npdu.npduDADR) def sap_indication(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("sap_indication %r %r", adapter, npdu) @@ -621,17 +708,15 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # build a list of reachable networks netlist = [] - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter): - netlist.append(xadapter.adapterNet) + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is adapter): + continue + + # add the direct network + netlist.append(xadapter.adapterNet) - # build a list of other available networks - for net, rref in sap.networks.items(): - if rref.adapter is not adapter: - ### skip those marked unreachable - ### skip those that are not available - netlist.append(net) + ### add the other reachable if netlist: if _debug: NetworkServiceElement._debug(" - found these: %r", netlist) @@ -646,42 +731,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): else: # requesting a specific network if _debug: NetworkServiceElement._debug(" - requesting specific network: %r", npdu.wirtnNetwork) + dnet = npdu.wirtnNetwork - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter) and (npdu.wirtnNetwork == xadapter.adapterNet): - if _debug: NetworkServiceElement._debug(" - found it directly connected") + # check the directly connected networks + if dnet in sap.adapters: + if _debug: NetworkServiceElement._debug(" - directly connected") - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) - break else: - # check for networks I know about - if npdu.wirtnNetwork in sap.networks: - rref = sap.networks[npdu.wirtnNetwork] - if rref.adapter is adapter: - if _debug: NetworkServiceElement._debug(" - same net as request") - - else: - if _debug: NetworkServiceElement._debug(" - found on adapter: %r", rref.adapter) + # see if there is routing information for this source network + router_info = sap.router_info_cache.get_router_info(dnet) + if router_info: + if _debug: NetworkServiceElement._debug(" - router found") + + router_net, router_address, router_status = router_info + if _debug: NetworkServiceElement._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + if router_net not in sap.adapters: + if _debug: NetworkServiceElement._debug(" - path error (6)") + return - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) else: if _debug: NetworkServiceElement._debug(" - forwarding request to other adapters") # build a request - whoisrtn = WhoIsRouterToNetwork(npdu.wirtnNetwork, user_data=npdu.pduUserData) + whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) whoisrtn.pduDestination = LocalBroadcast() # if the request had a source, forward it along @@ -692,7 +781,7 @@ def WhoIsRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) # send it to all of the (other) adapters - for xadapter in sap.adapters: + for xadapter in sap.adapters.values(): if xadapter is not adapter: if _debug: NetworkServiceElement._debug(" - sending on adapter: %r", xadapter) self.request(xadapter, whoisrtn) @@ -700,8 +789,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("IAmRouterToNetwork %r %r", adapter, npdu) + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + # pass along to the service access point - self.elementService.add_router_references(adapter, npdu.pduSource, npdu.iartnNetworkList) + sap.add_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) + + # skip if this is not a router + if len(sap.adapters) > 1: + # build a broadcast annoucement + iamrtn = IAmRouterToNetwork(npdu.iartnNetworkList, user_data=npdu.pduUserData) + iamrtn.pduDestination = LocalBroadcast() + + # send it to all of the connected adapters + for xadapter in sap.adapters.values(): + # skip the horse it rode in on + if (xadapter is adapter): + continue + + # request this + self.request(xadapter, iamrtn) + + # look for pending NPDUs for the networks + for dnet in npdu.iartnNetworkList: + pending_npdus = sap.pending_nets.get(dnet, None) + if pending_npdus is not None: + if _debug: NetworkServiceElement._debug(" - %d pending to %r", len(pending_npdus), dnet) + + # delete the references + del sap.pending_nets[dnet] + + # now reprocess them + for pending_npdu in pending_npdus: + if _debug: NetworkServiceElement._debug(" - sending %s", repr(pending_npdu)) + + # the destination is the address of the router + pending_npdu.pduDestination = npdu.pduSource + + # send the packet downstream + adapter.process_npdu(pending_npdu) def ICouldBeRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("ICouldBeRouterToNetwork %r %r", adapter, npdu) diff --git a/py34/bacpypes/npdu.py b/py34/bacpypes/npdu.py index c6ad9d97..436f6bb8 100755 --- a/py34/bacpypes/npdu.py +++ b/py34/bacpypes/npdu.py @@ -476,7 +476,7 @@ def __init__(self, netList=[], *args, **kwargs): def encode(self, npdu): NPCI.update(npdu, self) - for net in self.ratnNetworkList: + for net in self.rbtnNetworkList: npdu.put_short(net) def decode(self, npdu): @@ -543,6 +543,12 @@ def __init__(self, dnet=None, portID=None, portInfo=None): self.rtPortID = portID self.rtPortInfo = portInfo + def __eq__(self, other): + """Return true iff entries are identical.""" + return (self.rtDNET == other.rtDNET) and \ + (self.rtPortID == other.rtPortID) and \ + (self.rtPortInfo == other.rtPortInfo) + def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" # make/extend the dictionary of content @@ -735,6 +741,11 @@ class WhatIsNetworkNumber(NPDU): messageType = 0x12 + def __init__(self, *args, **kwargs): + super(WhatIsNetworkNumber, self).__init__(*args, **kwargs) + + self.npduNetMessage = WhatIsNetworkNumber.messageType + def encode(self, npdu): NPCI.update(npdu, self) @@ -755,25 +766,32 @@ def npdu_contents(self, use_dict=None, as_class=dict): class NetworkNumberIs(NPDU): - _debug_contents = ('nniNET', 'nniFlag',) + _debug_contents = ('nniNet', 'nniFlag',) messageType = 0x13 + def __init__(self, net=None, flag=None, *args, **kwargs): + super(NetworkNumberIs, self).__init__(*args, **kwargs) + + self.npduNetMessage = NetworkNumberIs.messageType + self.nniNet = net + self.nniFlag = flag + def encode(self, npdu): NPCI.update(npdu, self) - npdu.put_short( self.nniNET ) + npdu.put_short( self.nniNet ) npdu.put( self.nniFlag ) def decode(self, npdu): NPCI.update(self, npdu) - self.nniNET = npdu.get_short() + self.nniNet = npdu.get_short() self.nniFlag = npdu.get() def npdu_contents(self, use_dict=None, as_class=dict): return key_value_contents(use_dict=use_dict, as_class=as_class, key_values=( ('function', 'NetorkNumberIs'), - ('net', self.nniNET), + ('net', self.nniNet), ('flag', self.nniFlag), )) diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index dad8a383..9f2a4e99 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -81,6 +81,7 @@ def _register(xcls): # build a property dictionary by going through the class and all its parents _properties = {} for c in cls.__mro__: + if _debug: register_object_type._debug(" - c: %r", c) for prop in getattr(c, 'properties', []): if prop.identifier not in _properties: _properties[prop.identifier] = prop @@ -215,6 +216,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + elif issubclass(self.datatype, AnyAtomic): + if _debug: Property._debug(" - property is any atomic, checking value") + if not isinstance(value, Atomic): + raise InvalidParameterDatatype("%s must be an atomic instance" % ( + self.identifier, + )) + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): @@ -1414,7 +1422,7 @@ class EventEnrollmentObject(Object): , ReadableProperty('eventTimeStamps', ArrayOf(TimeStamp)) , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString)) , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString)) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference) , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) diff --git a/py34/bacpypes/primitivedata.py b/py34/bacpypes/primitivedata.py index 0638de4e..3e89abe1 100755 --- a/py34/bacpypes/primitivedata.py +++ b/py34/bacpypes/primitivedata.py @@ -14,6 +14,9 @@ from .errors import DecodingError, InvalidTag, InvalidParameterDatatype from .pdu import PDUData +# import the task manager to get the "current" date and time +from .task import TaskManager as _TaskManager + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -1352,6 +1355,9 @@ def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): elif isinstance(arg, Date): self.value = arg.value + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") @@ -1380,11 +1386,31 @@ def CalcDayOfWeek(self): # put it back together self.value = (year, month, day, day_of_week) - def now(self): - tup = time.localtime() + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) + self.value = (tup[0]-1900, tup[1], tup[2], tup[6] + 1) + return self + def __float__(self): + """Convert to seconds since the epoch.""" + # rip apart the value + year, month, day, day_of_week = self.value + + # check for special values + if (year == 255) or (month in _special_mon_inv) or (day in _special_day_inv): + raise ValueError("no wildcard values") + + # convert to time.time() value + return time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.dateAppTag, bytearray(self.value)) @@ -1464,19 +1490,40 @@ def __init__(self, arg=None, hour=255, minute=255, second=255, hundredth=255): tup_list[3] = tup_list[3] * 10 self.value = tuple(tup_list) + elif isinstance(arg, Time): self.value = arg.value + + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") - def now(self): - now = time.time() - tup = time.localtime(now) + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) - self.value = (tup[3], tup[4], tup[5], int((now - int(now)) * 100)) + self.value = (tup[3], tup[4], tup[5], int((when - int(when)) * 100)) return self + def __float__(self): + """Return the current value as an offset from midnight.""" + if 255 in self.value: + raise ValueError("no wildcard values") + + # rip it apart + hour, minute, second, hundredth = self.value + + # put it together + return (hour * 3600.0) + (minute * 60.0) + second + (hundredth / 100.0) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.timeAppTag, bytearray(self.value)) diff --git a/py34/bacpypes/service/device.py b/py34/bacpypes/service/device.py index 7a97051f..11be52b0 100644 --- a/py34/bacpypes/service/device.py +++ b/py34/bacpypes/service/device.py @@ -4,136 +4,16 @@ from ..capability import Capability from ..pdu import GlobalBroadcast -from ..primitivedata import Date, Time, ObjectIdentifier -from ..constructeddata import ArrayOf -from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU, Error +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU from ..errors import ExecutionError, InconsistentParameters, \ MissingRequiredParameter, ParameterOutOfRange -from ..object import register_object_type, registered_object_types, \ - Property, DeviceObject from ..task import FunctionTask -from .object import CurrentPropertyListMixIn - # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -@bacpypes_debugging -class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for local time - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, int): - object_identifier = ('device', object_identifier) - - # the object list is provided - if 'objectList' in kwargs: - raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) - # # Who-Is I-Am Services # diff --git a/py34/bacpypes/service/object.py b/py34/bacpypes/service/object.py index ca8d3fe9..3289075d 100755 --- a/py34/bacpypes/service/object.py +++ b/py34/bacpypes/service/object.py @@ -20,57 +20,6 @@ # handy reference ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) -# -# CurrentPropertyList -# - -@bacpypes_debugging -class CurrentPropertyList(Property): - - def __init__(self): - if _debug: CurrentPropertyList._debug("__init__") - Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) - - # make a list of the properties that have values - property_list = [k for k, v in obj._values.items() - if v is not None - and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') - ] - if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) - - # sort the list so it's stable - property_list.sort() - - # asking for the whole thing - if arrayIndex is None: - return ArrayOfPropertyIdentifier(property_list) - - # asking for the length - if arrayIndex == 0: - return len(property_list) - - # asking for an index - if arrayIndex > len(property_list): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return property_list[arrayIndex - 1] - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentPropertyListMixIn -# - -@bacpypes_debugging -class CurrentPropertyListMixIn(Object): - - properties = [ - CurrentPropertyList(), - ] - # # ReadProperty and WriteProperty Services # diff --git a/py34/bacpypes/task.py b/py34/bacpypes/task.py index 1c89a4f7..0c25c33a 100755 --- a/py34/bacpypes/task.py +++ b/py34/bacpypes/task.py @@ -8,6 +8,7 @@ from time import time as _time from heapq import heapify, heappush, heappop +import itertools from .singleton import SingletonLogging from .debugging import DebugContents, Logging, ModuleLogger, bacpypes_debugging @@ -276,6 +277,9 @@ def __init__(self): # task manager is this instance _task_manager = self + # unique sequence counter for tasks scheduled at the same time + self.counter = itertools.count() + # there may be tasks created that couldn't be scheduled # because a task manager wasn't created yet. if _unscheduled_tasks: @@ -300,7 +304,7 @@ def install_task(self, task): self.suspend_task(task) # save this in the task list - heappush( self.tasks, (task.taskTime, task) ) + heappush( self.tasks, (task.taskTime, next(self.counter), task) ) if _debug: TaskManager._debug(" - tasks: %r", self.tasks) task.isScheduled = True @@ -313,7 +317,7 @@ def suspend_task(self, task): if _debug: TaskManager._debug("suspend_task %r", task) # remove this guy - for i, (when, curtask) in enumerate(self.tasks): + for i, (when, n, curtask) in enumerate(self.tasks): if task is curtask: if _debug: TaskManager._debug(" - task found") del self.tasks[i] @@ -348,7 +352,7 @@ def get_next_task(self): if self.tasks: # look at the first task - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] if when <= now: # pull it off the list and mark that it's no longer scheduled heappop(self.tasks) @@ -356,7 +360,7 @@ def get_next_task(self): task.isScheduled = False if self.tasks: - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] # peek at the next task, return how long to wait delta = max(when - now, 0.0) else: diff --git a/py34/bacpypes/vlan.py b/py34/bacpypes/vlan.py index 953e266e..5908e898 100755 --- a/py34/bacpypes/vlan.py +++ b/py34/bacpypes/vlan.py @@ -32,9 +32,13 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.name = name self.nodes = [] + self.broadcast_address = broadcast_address self.drop_percent = drop_percent + # point to a TrafficLog instance + self.traffic_log = None + def add_node(self, node): """ Add a node to this network, let the node know which network it's on. """ if _debug: Network._debug("add_node %r", node) @@ -59,6 +63,10 @@ def process_pdu(self, pdu): """ if _debug: Network._debug("process_pdu(%s) %r", self.name, pdu) + # if there is a traffic log, call it with the network name and pdu + if self.traffic_log: + self.traffic_log(self.name, pdu) + # randomly drop a packet if self.drop_percent != 0.0: if (random.random() * 100.0) < self.drop_percent: @@ -154,9 +162,9 @@ class IPNetwork(Network): ('1.2.3.255', 5) and the other nodes must have the same tuple. """ - def __init__(self): + def __init__(self, name=''): if _debug: IPNetwork._debug("__init__") - Network.__init__(self) + Network.__init__(self, name=name) def add_node(self, node): if _debug: IPNetwork._debug("add_node %r", node) @@ -206,11 +214,12 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): @bacpypes_debugging class IPRouterNode(Client): - def __init__(self, router, addr, lan=None): + def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) - # save the reference to the router + # save the references to the router for packets and the lan for debugging self.router = router + self.lan = lan # make ourselves an IPNode and bind to it self.node = IPNode(addr, lan=lan, promiscuous=True, spoofing=True) @@ -231,6 +240,10 @@ def process_pdu(self, pdu): # pass it downstream self.request(pdu) + def __repr__(self): + return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + + # # IPRouter # diff --git a/samples/AccumulatorObject.py b/samples/AccumulatorObject.py index a03026ea..d317d418 100755 --- a/samples/AccumulatorObject.py +++ b/samples/AccumulatorObject.py @@ -15,7 +15,7 @@ from bacpypes.object import AccumulatorObject from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject from bacpypes.service.object import ReadWritePropertyMultipleServices # some debugging @@ -70,13 +70,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=('device', int(args.ini.objectidentifier)), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) @@ -84,13 +79,6 @@ def main(): # add the additional service this_application.add_capability(ReadWritePropertyMultipleServices) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a random input object accumulator = AccumulatorObject( objectIdentifier=('accumulator', 1), diff --git a/samples/BBMD2VLANRouter.py b/samples/BBMD2VLANRouter.py index 31095626..492a950e 100755 --- a/samples/BBMD2VLANRouter.py +++ b/samples/BBMD2VLANRouter.py @@ -24,7 +24,8 @@ from bacpypes.app import Application from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint -from bacpypes.service.device import LocalDeviceObject, WhoIsIAmServices +from bacpypes.local.device import LocalDeviceObject +from bacpypes.service.device import WhoIsIAmServices from bacpypes.service.object import ReadWritePropertyServices from bacpypes.primitivedata import Real diff --git a/samples/COVClient.py b/samples/COVClient.py index 29281fa4..dd6f8aa0 100755 --- a/samples/COVClient.py +++ b/samples/COVClient.py @@ -20,7 +20,7 @@ SimpleAckPDU, RejectPDU, AbortPDU from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -217,24 +217,12 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = SubscribeCOVApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = SubscribeCOVConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/COVClientApp.py b/samples/COVClientApp.py new file mode 100755 index 00000000..41c601f0 --- /dev/null +++ b/samples/COVClientApp.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python + +""" +Configured with a subscription context object which is passed to the +application, it sends a SubscribeCOVRequest and listens for confirmed or +unconfirmed COV notifications, lines them up with the context, and passes the +APDU to the context to print out. + +Making multiple subscription contexts and keeping them active based on their +lifetime is left as an exercise for the reader. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run, deferred +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.apdu import SubscribeCOVRequest, SimpleAckPDU +from bacpypes.errors import ExecutionError + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None + +subscription_contexts = {} +next_proc_id = 1 + +# +# SubscriptionContext +# + +@bacpypes_debugging +class SubscriptionContext: + + def __init__(self, address, objid, confirmed=None, lifetime=None): + if _debug: SubscriptionContext._debug("__init__ %r %r confirmed=%r lifetime=%r", address, objid, confirmed, lifetime) + global subscription_contexts, next_proc_id + + # destination for subscription requests + self.address = address + + # assign a unique process identifer and keep track of it + self.subscriberProcessIdentifier = next_proc_id + next_proc_id += 1 + subscription_contexts[self.subscriberProcessIdentifier] = self + + self.monitoredObjectIdentifier = objid + self.issueConfirmedNotifications = confirmed + self.lifetime = lifetime + + def cov_notification(self, apdu): + if _debug: SubscriptionContext._debug("cov_notification %r", apdu) + + # make a rash assumption that the property value is going to be + # a single application encoded tag + print("{} {} changed\n {}".format( + apdu.pduSource, + apdu.monitoredObjectIdentifier, + ",\n ".join("{} = {}".format( + element.propertyIdentifier, + str(element.value.tagList[0].app_to_object().value), + ) for element in apdu.listOfValues), + )) + +# +# SubscribeCOVApplication +# + +@bacpypes_debugging +class SubscribeCOVApplication(BIPSimpleApplication): + + def __init__(self, *args): + if _debug: SubscribeCOVApplication._debug("__init__ %r", args) + BIPSimpleApplication.__init__(self, *args) + + def send_subscription(self, context): + if _debug: SubscribeCOVApplication._debug("send_subscription %r", context) + + # build a request + request = SubscribeCOVRequest( + subscriberProcessIdentifier=context.subscriberProcessIdentifier, + monitoredObjectIdentifier=context.monitoredObjectIdentifier, + ) + request.pduDestination = context.address + + # optional parameters + if context.issueConfirmedNotifications is not None: + request.issueConfirmedNotifications = context.issueConfirmedNotifications + if context.lifetime is not None: + request.lifetime = context.lifetime + + # make an IOCB + iocb = IOCB(request) + if _debug: SubscribeCOVApplication._debug(" - iocb: %r", iocb) + + # callback when it is acknowledged + iocb.add_callback(self.subscription_acknowledged) + + # give it to the application + this_application.request_io(iocb) + + def subscription_acknowledged(self, iocb): + if _debug: SubscribeCOVApplication._debug("subscription_acknowledged %r", iocb) + + # do something for success + if iocb.ioResponse: + if _debug: SubscribeCOVApplication._debug(" - response: %r", iocb.ioResponse) + + # do something for error/reject/abort + if iocb.ioError: + if _debug: SubscribeCOVApplication._debug(" - error: %r", iocb.ioError) + + def do_ConfirmedCOVNotificationRequest(self, apdu): + if _debug: SubscribeCOVApplication._debug("do_ConfirmedCOVNotificationRequest %r", apdu) + + # look up the process identifier + context = subscription_contexts.get(apdu.subscriberProcessIdentifier, None) + if not context or apdu.pduSource != context.address: + if _debug: SubscribeCOVApplication._debug(" - no context") + + # this is turned into an ErrorPDU and sent back to the client + raise ExecutionError('services', 'unknownSubscription') + + # now tell the context object + context.cov_notification(apdu) + + # success + response = SimpleAckPDU(context=apdu) + if _debug: SubscribeCOVApplication._debug(" - simple_ack: %r", response) + + # return the result + self.response(response) + + def do_UnconfirmedCOVNotificationRequest(self, apdu): + if _debug: SubscribeCOVApplication._debug("do_UnconfirmedCOVNotificationRequest %r", apdu) + + # look up the process identifier + context = subscription_contexts.get(apdu.subscriberProcessIdentifier, None) + if not context or apdu.pduSource != context.address: + if _debug: SubscribeCOVApplication._debug(" - no context") + return + + # now tell the context object + context.cov_notification(apdu) + +# +# __main__ +# + +def main(): + global this_application + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) + + # make a simple application + this_application = SubscribeCOVApplication(this_device, args.ini.address) + + # make a subscription context + context = SubscriptionContext(Address("10.0.1.31"), ('analogValue', 1), False, 60) + + # send the subscription when the stack is ready + deferred(this_application.send_subscription, context) + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/samples/COVServer.py b/samples/COVServer.py index c1c2de8e..4c32dcda 100755 --- a/samples/COVServer.py +++ b/samples/COVServer.py @@ -18,7 +18,7 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.object import AnalogValueObject, BinaryValueObject -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject from bacpypes.service.cov import ChangeOfValueServices # some debugging @@ -365,16 +365,11 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - test_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application - test_application = SubscribeCOVApplication(test_device, args.ini.address) + test_application = SubscribeCOVApplication(this_device, args.ini.address) # make an analog value object test_av = AnalogValueObject( @@ -388,7 +383,7 @@ def main(): # add it to the device test_application.add_object(test_av) - _log.debug(" - object list: %r", test_device.objectList) + _log.debug(" - object list: %r", this_device.objectList) # make a binary value object test_bv = BinaryValueObject( @@ -402,13 +397,6 @@ def main(): # add it to the device test_application.add_object(test_bv) - # get the services supported - services_supported = test_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - test_device.protocolServicesSupported = services_supported.value - # make a console if args.console: test_console = COVConsoleCmd() diff --git a/samples/CommandableMixin.py b/samples/CommandableMixin.py index 16b13401..2a268e09 100644 --- a/samples/CommandableMixin.py +++ b/samples/CommandableMixin.py @@ -26,8 +26,8 @@ TimeValueObject, TimePatternValueObject, ChannelObject from bacpypes.app import BIPSimpleApplication -from bacpypes.service.object import CurrentPropertyListMixIn -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.object import CurrentPropertyListMixIn +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -379,13 +379,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/DeviceCommunicationControl.py b/samples/DeviceCommunicationControl.py index a5247559..d679a097 100755 --- a/samples/DeviceCommunicationControl.py +++ b/samples/DeviceCommunicationControl.py @@ -18,7 +18,7 @@ from bacpypes.apdu import DeviceCommunicationControlRequest from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -123,24 +123,12 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = DCCConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/DeviceDiscovery.py b/samples/DeviceDiscovery.py index 7c6b1c82..11b0c1af 100755 --- a/samples/DeviceDiscovery.py +++ b/samples/DeviceDiscovery.py @@ -16,13 +16,12 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.apdu import WhoIsRequest, IAmRequest, ReadPropertyRequest, ReadPropertyACK +from bacpypes.apdu import WhoIsRequest, ReadPropertyRequest, ReadPropertyACK from bacpypes.primitivedata import CharacterString -from bacpypes.basetypes import ServicesSupported -from bacpypes.errors import DecodingError +from bacpypes.errors import MissingRequiredParameter from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 1 @@ -149,7 +148,6 @@ def do_whois(self, args): try: # gather the parameters - request = WhoIsRequest() if (len(args) == 1) or (len(args) == 3): addr = Address(args[0]) del args[0] @@ -200,24 +198,12 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = DiscoveryApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = DiscoveryConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/DeviceDiscoveryForeign.py b/samples/DeviceDiscoveryForeign.py index fa315b9b..8634552b 100755 --- a/samples/DeviceDiscoveryForeign.py +++ b/samples/DeviceDiscoveryForeign.py @@ -16,13 +16,12 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.apdu import WhoIsRequest, IAmRequest, ReadPropertyRequest, ReadPropertyACK +from bacpypes.apdu import WhoIsRequest, ReadPropertyRequest, ReadPropertyACK from bacpypes.primitivedata import CharacterString -from bacpypes.basetypes import ServicesSupported -from bacpypes.errors import DecodingError +from bacpypes.errors import MissingRequiredParameter from bacpypes.app import BIPForeignApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 1 @@ -149,7 +148,6 @@ def do_whois(self, args): try: # gather the parameters - request = WhoIsRequest() if (len(args) == 1) or (len(args) == 3): addr = Address(args[0]) del args[0] @@ -200,13 +198,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = DiscoveryApplication( @@ -215,13 +208,6 @@ def main(): int(args.ini.foreignttl), ) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = DiscoveryConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/HTTPServer.py b/samples/HTTPServer.py index 9d99cb4b..64681e63 100755 --- a/samples/HTTPServer.py +++ b/samples/HTTPServer.py @@ -22,7 +22,7 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -179,24 +179,12 @@ class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # local host, special port HOST, PORT = "", int(args.port) server = ThreadedTCPServer((HOST, PORT), ThreadedHTTPRequestHandler) diff --git a/samples/HandsOnLab/Sample1_SimpleApplication.py b/samples/HandsOnLab/Sample1_SimpleApplication.py index b96478b7..dd02bd3c 100644 --- a/samples/HandsOnLab/Sample1_SimpleApplication.py +++ b/samples/HandsOnLab/Sample1_SimpleApplication.py @@ -13,7 +13,7 @@ from bacpypes.core import run from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 1 @@ -72,13 +72,6 @@ def main(): this_application = SampleApplication(this_device, args.ini.address) if _debug: _log.debug(" - this_application: %r", this_application) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - _log.debug("running") run() diff --git a/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py b/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py index 27d00499..2a185f96 100644 --- a/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py +++ b/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py @@ -15,7 +15,7 @@ from bacpypes.core import run from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -90,13 +90,6 @@ def main(): # make a sample application this_application = WhoIsIAmApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - _log.debug("running") run() diff --git a/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py b/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py index 9c06c0b2..c44e8dd0 100644 --- a/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py +++ b/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py @@ -16,7 +16,7 @@ from bacpypes.core import run from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -92,13 +92,6 @@ def main(): # make a sample application this_application = WhoHasIHaveApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - _log.debug("running") # run until stopped, ^C works diff --git a/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py b/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py index 8aeb5a44..c5de7083 100644 --- a/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py +++ b/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py @@ -21,7 +21,7 @@ from bacpypes.errors import ExecutionError from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -99,13 +99,6 @@ def main(): # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make some random input objects for i in range(1, RANDOM_OBJECT_COUNT+1): ravo = RandomAnalogValueObject( diff --git a/samples/IP2VLANRouter.py b/samples/IP2VLANRouter.py new file mode 100755 index 00000000..87f36870 --- /dev/null +++ b/samples/IP2VLANRouter.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python + +""" +This sample application presents itself as a router sitting on an IP network +to a VLAN. The VLAN has one or more devices on it with an analog +value object that returns a random value for the present value. + +Note that the device instance number of the virtual device will be 100 times +the network number plus its address (net2 * 100 + n). +""" + +import random +import argparse + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run +from bacpypes.comm import bind + +from bacpypes.pdu import Address, LocalBroadcast +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.bvllservice import BIPSimple, AnnexJCodec, UDPMultiplexer + +from bacpypes.app import Application +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.service.device import LocalDeviceObject, WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices + +from bacpypes.primitivedata import Real +from bacpypes.object import AnalogValueObject, Property + +from bacpypes.vlan import Network, Node +from bacpypes.errors import ExecutionError + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# RandomValueProperty +# + +@bacpypes_debugging +class RandomValueProperty(Property): + + def __init__(self, identifier): + if _debug: RandomValueProperty._debug("__init__ %r", identifier) + Property.__init__(self, identifier, Real, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: RandomValueProperty._debug("ReadProperty %r arrayIndex=%r", obj, arrayIndex) + + # access an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return a random value + value = random.random() * 100.0 + if _debug: RandomValueProperty._debug(" - value: %r", value) + + return value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + if _debug: RandomValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# Random Value Object Type +# + +@bacpypes_debugging +class RandomAnalogValueObject(AnalogValueObject): + + properties = [ + RandomValueProperty('presentValue'), + ] + + def __init__(self, **kwargs): + if _debug: RandomAnalogValueObject._debug("__init__ %r", kwargs) + AnalogValueObject.__init__(self, **kwargs) + +# +# VLANApplication +# + +@bacpypes_debugging +class VLANApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): + + def __init__(self, vlan_device, vlan_address, aseID=None): + if _debug: VLANApplication._debug("__init__ %r %r aseID=%r", vlan_device, vlan_address, aseID) + Application.__init__(self, vlan_device, vlan_address, aseID) + + # include a application decoder + self.asap = ApplicationServiceAccessPoint() + + # pass the device object to the state machine access point so it + # can know if it should support segmentation + self.smap = StateMachineAccessPoint(vlan_device) + + # the segmentation state machines need access to the same device + # information cache as the application + self.smap.deviceInfoCache = self.deviceInfoCache + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # create a vlan node at the assigned address + self.vlan_node = Node(vlan_address) + + # bind the stack to the node, no network number + self.nsap.bind(self.vlan_node) + + def request(self, apdu): + if _debug: VLANApplication._debug("[%s]request %r", self.vlan_node.address, apdu) + Application.request(self, apdu) + + def indication(self, apdu): + if _debug: VLANApplication._debug("[%s]indication %r", self.vlan_node.address, apdu) + Application.indication(self, apdu) + + def response(self, apdu): + if _debug: VLANApplication._debug("[%s]response %r", self.vlan_node.address, apdu) + Application.response(self, apdu) + + def confirmation(self, apdu): + if _debug: VLANApplication._debug("[%s]confirmation %r", self.vlan_node.address, apdu) + Application.confirmation(self, apdu) + +# +# VLANRouter +# + +@bacpypes_debugging +class VLANRouter: + + def __init__(self, local_address, local_network): + if _debug: VLANRouter._debug("__init__ %r %r", local_address, local_network) + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # create a BIPSimple, bound to the Annex J server + # on the UDP multiplexer + self.bip = BIPSimple(local_address) + self.annexj = AnnexJCodec() + self.mux = UDPMultiplexer(local_address) + + # bind the bottom layers + bind(self.bip, self.annexj, self.mux.annexJ) + + # bind the BIP stack to the local network + self.nsap.bind(self.bip, local_network, local_address) + +# +# __main__ +# + +def main(): + # parse the command line arguments + parser = ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + # add an argument for interval + parser.add_argument('addr1', type=str, + help='address of first network', + ) + + # add an argument for interval + parser.add_argument('net1', type=int, + help='network number of first network', + ) + + # add an argument for interval + parser.add_argument('net2', type=int, + help='network number of second network', + ) + + # add an argument for how many virtual devices + parser.add_argument('--count', type=int, + help='number of virtual devices', + default=1, + ) + + # now parse the arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + local_address = Address(args.addr1) + local_network = args.net1 + vlan_network = args.net2 + + # create the VLAN router, bind it to the local network + router = VLANRouter(local_address, local_network) + + # create a VLAN + vlan = Network(broadcast_address=LocalBroadcast()) + + # create a node for the router, address 1 on the VLAN + router_node = Node(Address(1)) + vlan.add_node(router_node) + + # bind the router stack to the vlan network through this node + router.nsap.bind(router_node, vlan_network) + + # make some devices + for device_number in range(2, 2 + args.count): + # device identifier is assigned from the address + device_instance = vlan_network * 100 + device_number + _log.debug(" - device_instance: %r", device_instance) + + # make a vlan device object + vlan_device = \ + LocalDeviceObject( + objectName="VLAN Node %d" % (device_instance,), + objectIdentifier=('device', device_instance), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=15, + ) + _log.debug(" - vlan_device: %r", vlan_device) + + # make the application, add it to the network + vlan_app = VLANApplication(vlan_device, Address(device_number)) + vlan.add_node(vlan_app.vlan_node) + _log.debug(" - vlan_app: %r", vlan_app) + + # make a random value object + ravo = RandomAnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='Random1' % (device_instance,), + ) + _log.debug(" - ravo1: %r", ravo) + + # add it to the device + vlan_app.add_object(ravo) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/LocalScheduleObject.py b/samples/LocalScheduleObject.py new file mode 100644 index 00000000..83776c85 --- /dev/null +++ b/samples/LocalScheduleObject.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python + +""" +This application creates a series of Local Schedule Objects and then prompts +to test dates and times. +""" + +from time import localtime as _localtime + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run + +from bacpypes.primitivedata import Null, Integer, Real, Date, Time, CharacterString +from bacpypes.constructeddata import ArrayOf, SequenceOf +from bacpypes.basetypes import CalendarEntry, DailySchedule, DateRange, \ + DeviceObjectPropertyReference, SpecialEvent, SpecialEventPeriod, TimeValue + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject +from bacpypes.local.schedule import LocalScheduleObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +schedule_objects = [] + +# +# TestConsoleCmd +# + +@bacpypes_debugging +class TestConsoleCmd(ConsoleCmd): + + def do_test(self, args): + """test