From 3c223f711b28cba95b1ef7fe7c28e06bfdb544f0 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Fri, 19 May 2017 22:04:58 -0400 Subject: [PATCH 01/72] bump the version, this will be an API/behavior change --- 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 37011ff6..90604514 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.16.1' +__version__ = '0.17.0' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 37011ff6..90604514 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.16.1' +__version__ = '0.17.0' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index bd7919de..5ea5be2f 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.16.1' +__version__ = '0.17.0' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' From 777ee0484319c34157840fe56dd8623701a3565e Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 23 May 2017 02:38:25 -0400 Subject: [PATCH 02/72] slightly better error message --- py27/bacpypes/vlan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py27/bacpypes/vlan.py b/py27/bacpypes/vlan.py index 855e6ff7..fee20069 100755 --- a/py27/bacpypes/vlan.py +++ b/py27/bacpypes/vlan.py @@ -57,7 +57,7 @@ def process_pdu(self, pdu): return if not pdu.pduDestination or not isinstance(pdu.pduDestination, Address): - raise RuntimeError("invalid destination address") + raise RuntimeError("invalid destination address: %r" % (pdu.pduDestination,)) elif pdu.pduDestination.addrType == Address.localBroadcastAddr: for n in self.nodes: @@ -70,7 +70,7 @@ def process_pdu(self, pdu): n.response(deepcopy(pdu)) else: - raise RuntimeError("invalid destination address type") + raise RuntimeError("invalid destination address type: %r" % (pdu.pduDestination,)) def __len__(self): """ Simple way to determine the number of nodes in the network. """ From e9c0111027c34ce5dc983d8c554a4756513abc71 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 23 May 2017 02:39:09 -0400 Subject: [PATCH 03/72] ongoing changes --- py27/bacpypes/netservice.py | 603 ++++++++++++++++++++++-------------- 1 file changed, 364 insertions(+), 239 deletions(-) diff --git a/py27/bacpypes/netservice.py b/py27/bacpypes/netservice.py index 6d02f687..f2dfed71 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) - _debug_contents = ('adapter-', 'address', 'networks', '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 - 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 + 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) + + # 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 - self.localAdapter = None # which one is local - self.localAddress = None # what is the local address + # 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 = {} + + # 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) + if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) - # make a key for the router reference - rkey = (adapter, address) + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - for snet in netlist: - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] + # pass this along to the cache + self.router_info_cache.update_router_info(snet, address, dnets) - 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] - - ### check to see if it is OK to add the new entry - - # 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) - - # 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 - ### make sure the direct connect is OK, may need to connect + # 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 - ### make sure the peer router is OK, may need to connect + # check cache for an available path + path_info = self.router_info_cache.get_router_info(dnet) + + # 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) + + # 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: # decode as a generic APDU apdu = _APDU(user_data=npdu.pduUserData) @@ -409,7 +473,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 ) @@ -418,7 +482,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: @@ -443,10 +507,9 @@ def process_npdu(self, adapter, npdu): # pass upstream to the application layer self.response(apdu) - - if not forwardMessage: - return 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) @@ -459,15 +522,19 @@ def process_npdu(self, adapter, npdu): # pass to the service element self.sap_request(adapter, xpdu) - if not forwardMessage: - return + # 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 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 @@ -488,48 +555,66 @@ 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): + if _debug: NetworkServiceAccessPoint._debug(" - global broadcasting to: %r", xadapter) xadapter.process_npdu(newpdu) return 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(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(newpdu) + return - ### check to make sure the router is OK + # see if there is routing information for this source 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(newpdu) + xadapter.process_npdu(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 @@ -537,16 +622,16 @@ 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) + return - ### log this, what to do? - 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) @@ -618,17 +703,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) @@ -643,42 +726,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 @@ -689,7 +776,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) @@ -697,8 +784,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) From 73cfa2d0ecd13f6fb9081d6457636b11be1772e9 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 23 May 2017 02:40:18 -0400 Subject: [PATCH 04/72] test application --- sandbox/vlan_to_vlan.py | 342 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100755 sandbox/vlan_to_vlan.py diff --git a/sandbox/vlan_to_vlan.py b/sandbox/vlan_to_vlan.py new file mode 100755 index 00000000..012b5036 --- /dev/null +++ b/sandbox/vlan_to_vlan.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python + +""" +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run, deferred +from bacpypes.comm import bind + +from bacpypes.pdu import Address +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.bvllservice import BIPBBMD, 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.apdu import ReadPropertyRequest + +from bacpypes.vlan import Network, Node +from bacpypes.errors import ExecutionError + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# more than one test +which_test = 4 + +# +# 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) + if _debug: VLANApplication._debug(" - vlan_node: %r", self.vlan_node) + + # bind the stack to the node, no network number + self.nsap.bind(self.vlan_node) + if _debug: VLANApplication._debug(" - node bound") + + def request(self, apdu): + if _debug: VLANApplication._debug("[%s]request %r", self.localDevice.objectName, apdu) + Application.request(self, apdu) + + def indication(self, apdu): + if _debug: VLANApplication._debug("[%s]indication %r", self.localDevice.objectName, apdu) + Application.indication(self, apdu) + + def response(self, apdu): + if _debug: VLANApplication._debug("[%s]response %r", self.localDevice.objectName, apdu) + Application.response(self, apdu) + + def confirmation(self, apdu): + if _debug: VLANApplication._debug("[%s]confirmation %r", self.localDevice.objectName, apdu) + +# +# VLANRouter +# + +@bacpypes_debugging +class VLANRouter: + + def __init__(self): + if _debug: VLANRouter._debug("__init__") + + # 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) + +# +# __main__ +# + +def main(): + # parse the command line arguments + parser = ArgumentParser(description=__doc__) + + # now parse the arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # + # Router1 + # + + # create the router + router1 = VLANRouter() + if _debug: _log.debug(" - router1: %r", router1) + + # + # VLAN-1 + # + + # create VLAN-1 + vlan1 = Network() + if _debug: _log.debug(" - vlan1: %r", vlan1) + + # create a node for the router, address 1 on the VLAN + vlan1_router1_node = Node(Address(1)) + vlan1.add_node(vlan1_router1_node) + + # bind the router stack to the vlan network through this node + router1.nsap.bind(vlan1_router1_node, 1) + if _debug: _log.debug(" - router1 bound to VLAN-1") + + # make a vlan device object + vlan1_device = \ + LocalDeviceObject( + objectName="VLAN Node 102", + objectIdentifier=('device', 102), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=15, + ) + _log.debug(" - vlan1_device: %r", vlan1_device) + + # make the application, add it to the network + vlan1_app = VLANApplication(vlan1_device, Address(2)) + vlan1.add_node(vlan1_app.vlan_node) + _log.debug(" - vlan1_app: %r", vlan1_app) + + # + # VLAN-2 + # + + # create VLAN-2 + vlan2 = Network() + if _debug: _log.debug(" - vlan2: %r", vlan2) + + # create a node for the router, address 1 on the VLAN + vlan2_router1_node = Node(Address(1)) + vlan2.add_node(vlan2_router1_node) + + # bind the router stack to the vlan network through this node + router1.nsap.bind(vlan2_router1_node, 2) + if _debug: _log.debug(" - router1 bound to VLAN-2") + + # make a vlan device object + vlan2_device = \ + LocalDeviceObject( + objectName="VLAN Node 202", + objectIdentifier=('device', 202), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=15, + ) + _log.debug(" - vlan2_device: %r", vlan2_device) + + # make the application, add it to the network + vlan2_app = VLANApplication(vlan2_device, Address(2)) + vlan2.add_node(vlan2_app.vlan_node) + _log.debug(" - vlan2_app: %r", vlan2_app) + + # + # VLAN-3 + # + + # create VLAN-3 + vlan3 = Network() + if _debug: _log.debug(" - vlan3: %r", vlan3) + + # create a node for the router, address 1 on the VLAN + vlan3_router1_node = Node(Address(1)) + vlan3.add_node(vlan3_router1_node) + + # bind the router stack to the vlan network through this node + router1.nsap.bind(vlan3_router1_node, 3) + if _debug: _log.debug(" - router1 bound to VLAN-3") + + # make a vlan device object + vlan3_device = \ + LocalDeviceObject( + objectName="VLAN Node 302", + objectIdentifier=('device', 302), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=15, + ) + _log.debug(" - vlan3_device: %r", vlan3_device) + + # make the application, add it to the network + vlan3_app = VLANApplication(vlan3_device, Address(2)) + vlan3.add_node(vlan3_app.vlan_node) + _log.debug(" - vlan3_app: %r", vlan3_app) + + + # + # Router2 + # + + # create the router + router2 = VLANRouter() + if _debug: _log.debug(" - router2: %r", router2) + + # create a node for the router, address 255 on the VLAN-3 + vlan3_router2_node = Node(Address(255)) + vlan3.add_node(vlan3_router2_node) + + # bind the router stack to the vlan network through this node + router2.nsap.bind(vlan3_router2_node, 3) + if _debug: _log.debug(" - router2 bound to VLAN-3") + + # + # VLAN-4 + # + + # create VLAN-4 + vlan4 = Network() + if _debug: _log.debug(" - vlan4: %r", vlan4) + + # create a node for the router, address 1 on the VLAN + vlan4_router2_node = Node(Address(1)) + vlan4.add_node(vlan4_router2_node) + + # bind the router stack to the vlan network through this node + router2.nsap.bind(vlan4_router2_node, 4) + if _debug: _log.debug(" - router2 bound to VLAN-4") + + # make a vlan device object + vlan4_device = \ + LocalDeviceObject( + objectName="VLAN Node 402", + objectIdentifier=('device', 402), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=15, + ) + _log.debug(" - vlan4_device: %r", vlan4_device) + + # make the application, add it to the network + vlan4_app = VLANApplication(vlan4_device, Address(2)) + vlan4.add_node(vlan4_app.vlan_node) + _log.debug(" - vlan4_app: %r", vlan4_app) + + + # + # Test 1 + # + + if which_test == 1: + # ask the first device to Who-Is everybody + deferred(vlan1_app.who_is) + + + # + # Test 2 + # + + if which_test == 2: + # make a read request + read_property_request = ReadPropertyRequest( + destination=Address("2:2"), + objectIdentifier=('device', 202), + propertyIdentifier='objectName', + ) + + # ask the first device to send it + deferred(vlan1_app.request, read_property_request) + + + # + # Test 3 + # + + if which_test == 3: + # make a read request + read_property_request = ReadPropertyRequest( + destination=Address("3:2"), + objectIdentifier=('device', 302), + propertyIdentifier='objectName', + ) + + # ask the first device to send it + deferred(vlan1_app.request, read_property_request) + + + # + # Test 4 + # + + if which_test == 4: + # make a read request + read_property_request = ReadPropertyRequest( + destination=Address("4:2"), + objectIdentifier=('device', 402), + propertyIdentifier='objectName', + ) + + # ask the first device to send it + deferred(vlan1_app.request, read_property_request) + + + # + # Let the test run + # + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() From 8b85900ff83a8c64454cbaf25210c563d699a91d Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 23 May 2017 02:48:42 -0400 Subject: [PATCH 05/72] application to check the scope of vendor extensible things --- sandbox/explore_types.py | 74 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 sandbox/explore_types.py diff --git a/sandbox/explore_types.py b/sandbox/explore_types.py new file mode 100644 index 00000000..effbf3a1 --- /dev/null +++ b/sandbox/explore_types.py @@ -0,0 +1,74 @@ +#!/bin/bash python3 + +""" +""" + +from bacpypes.primitivedata import Enumerated +from bacpypes.constructeddata import Any, Choice, Element, Sequence, SequenceOf + +import bacpypes.basetypes + +vendor_types = set() + +# look for all of the enumerated types that have a vendor range +print('') +print("Enumerated Types with a vendor range") +print('') +for x in dir(bacpypes.basetypes): + c = getattr(bacpypes.basetypes, x) + if not isinstance(c, type): + pass + elif issubclass(c, Enumerated) and hasattr(c, 'vendor_range'): + print(c) + vendor_types.add(c) + +# now look for sequences and choices that have an extensible enumeration +print('') +print("Sequences and Choice with an extensible enumeration") +print('') +for x in dir(bacpypes.basetypes): + c = getattr(bacpypes.basetypes, x) + if not isinstance(c, type): + pass + elif issubclass(c, Sequence): + for e in c.sequenceElements: + if e.klass in vendor_types: + print(c, e.name, e.klass) + vendor_types.add(c) + elif issubclass(c, Choice): + for e in c.choiceElements: + if e.klass in vendor_types: + print(c, e.name, e.klass) + vendor_types.add(c) + +import bacpypes.apdu + +# look for all of the enumerated types that have a vendor range +print('') +print("Vendor Enumerations in APDU Module") +print('') +for x in dir(bacpypes.apdu): + c = getattr(bacpypes.apdu, x) + if not isinstance(c, type): + pass + elif issubclass(c, Enumerated) and hasattr(c, 'vendor_range'): + print(c) + vendor_types.add(c) + +# now look for sequences and choices in APDUs that are one of these +print('') +print("Sequences and Choices with Vendor Enumerations") +print('') +for x in dir(bacpypes.apdu): + c = getattr(bacpypes.apdu, x) + if not isinstance(c, type): + pass + elif issubclass(c, Sequence): + for e in c.sequenceElements: + if e.klass in vendor_types: + print(c, e.name, e.klass) + elif issubclass(c, Choice): + for e in c.choiceElements: + if e.klass in vendor_types: + print(c, e.name, e.klass) + From 252a669dd5a32959269ce6edfb791f298aa61bff Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Wed, 24 May 2017 23:42:34 -0400 Subject: [PATCH 06/72] combine boilerplate code --- sandbox/vlan_to_vlan.py | 140 +++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 72 deletions(-) diff --git a/sandbox/vlan_to_vlan.py b/sandbox/vlan_to_vlan.py index 012b5036..9674d702 100755 --- a/sandbox/vlan_to_vlan.py +++ b/sandbox/vlan_to_vlan.py @@ -3,6 +3,8 @@ """ """ +import sys + from bacpypes.debugging import bacpypes_debugging, ModuleLogger from bacpypes.consolelogging import ArgumentParser @@ -27,9 +29,6 @@ _debug = 0 _log = ModuleLogger(globals()) -# more than one test -which_test = 4 - # # VLANApplication # @@ -37,8 +36,24 @@ @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) + def __init__(self, objectName, deviceInstance, address, aseID=None): + if _debug: VLANApplication._debug("__init__ %r %r %r aseID=%r", objectName, deviceInstance, address, aseID) + + # make an address + vlan_address = Address(address) + _log.debug(" - vlan_address: %r", vlan_address) + + # make a device object + vlan_device = LocalDeviceObject( + objectName=objectName, + objectIdentifier=('device', deviceInstance), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=15, + ) + _log.debug(" - vlan_device: %r", vlan_device) + + # continue with the initialization Application.__init__(self, vlan_device, vlan_address, aseID) # include a application decoder @@ -102,6 +117,19 @@ def __init__(self): self.nse = NetworkServiceElement() bind(self.nse, self.nsap) + def bind(self, vlan, address, net): + if _debug: VLANRouter._debug("bind %r %r %r", vlan, address, net) + + # create a VLAN node for the router with the given address + vlan_node = Node(Address(address)) + + # add it to the VLAN + vlan.add_node(vlan_node) + + # bind the router stack to the vlan network through this node + self.nsap.bind(vlan_node, net) + if _debug: _log.debug(" - bound to vlan") + # # __main__ # @@ -110,6 +138,11 @@ def main(): # parse the command line arguments parser = ArgumentParser(description=__doc__) + # add an argument for which test to run + parser.add_argument('test_id', type=int, + help='test number', + ) + # now parse the arguments args = parser.parse_args() @@ -132,27 +165,16 @@ def main(): vlan1 = Network() if _debug: _log.debug(" - vlan1: %r", vlan1) - # create a node for the router, address 1 on the VLAN - vlan1_router1_node = Node(Address(1)) - vlan1.add_node(vlan1_router1_node) - - # bind the router stack to the vlan network through this node - router1.nsap.bind(vlan1_router1_node, 1) + # bind the router to the vlan + router1.bind(vlan1, 1, 1) if _debug: _log.debug(" - router1 bound to VLAN-1") - # make a vlan device object - vlan1_device = \ - LocalDeviceObject( - objectName="VLAN Node 102", - objectIdentifier=('device', 102), - maxApduLengthAccepted=1024, - segmentationSupported='noSegmentation', - vendorIdentifier=15, - ) - _log.debug(" - vlan1_device: %r", vlan1_device) - # make the application, add it to the network - vlan1_app = VLANApplication(vlan1_device, Address(2)) + vlan1_app = VLANApplication( + objectName="VLAN Node 102", + deviceInstance=102, + address=2, + ) vlan1.add_node(vlan1_app.vlan_node) _log.debug(" - vlan1_app: %r", vlan1_app) @@ -164,27 +186,16 @@ def main(): vlan2 = Network() if _debug: _log.debug(" - vlan2: %r", vlan2) - # create a node for the router, address 1 on the VLAN - vlan2_router1_node = Node(Address(1)) - vlan2.add_node(vlan2_router1_node) - # bind the router stack to the vlan network through this node - router1.nsap.bind(vlan2_router1_node, 2) + router1.bind(vlan2, 1, 2) if _debug: _log.debug(" - router1 bound to VLAN-2") - # make a vlan device object - vlan2_device = \ - LocalDeviceObject( - objectName="VLAN Node 202", - objectIdentifier=('device', 202), - maxApduLengthAccepted=1024, - segmentationSupported='noSegmentation', - vendorIdentifier=15, - ) - _log.debug(" - vlan2_device: %r", vlan2_device) - # make the application, add it to the network - vlan2_app = VLANApplication(vlan2_device, Address(2)) + vlan2_app = VLANApplication( + objectName="VLAN Node 202", + deviceInstance=202, + address=2, + ) vlan2.add_node(vlan2_app.vlan_node) _log.debug(" - vlan2_app: %r", vlan2_app) @@ -196,12 +207,8 @@ def main(): vlan3 = Network() if _debug: _log.debug(" - vlan3: %r", vlan3) - # create a node for the router, address 1 on the VLAN - vlan3_router1_node = Node(Address(1)) - vlan3.add_node(vlan3_router1_node) - # bind the router stack to the vlan network through this node - router1.nsap.bind(vlan3_router1_node, 3) + router1.bind(vlan3, 1, 3) if _debug: _log.debug(" - router1 bound to VLAN-3") # make a vlan device object @@ -216,7 +223,11 @@ def main(): _log.debug(" - vlan3_device: %r", vlan3_device) # make the application, add it to the network - vlan3_app = VLANApplication(vlan3_device, Address(2)) + vlan3_app = VLANApplication( + objectName="VLAN Node 302", + deviceInstance=302, + address=2, + ) vlan3.add_node(vlan3_app.vlan_node) _log.debug(" - vlan3_app: %r", vlan3_app) @@ -229,12 +240,8 @@ def main(): router2 = VLANRouter() if _debug: _log.debug(" - router2: %r", router2) - # create a node for the router, address 255 on the VLAN-3 - vlan3_router2_node = Node(Address(255)) - vlan3.add_node(vlan3_router2_node) - # bind the router stack to the vlan network through this node - router2.nsap.bind(vlan3_router2_node, 3) + router2.bind(vlan3, 255, 3) if _debug: _log.debug(" - router2 bound to VLAN-3") # @@ -245,27 +252,16 @@ def main(): vlan4 = Network() if _debug: _log.debug(" - vlan4: %r", vlan4) - # create a node for the router, address 1 on the VLAN - vlan4_router2_node = Node(Address(1)) - vlan4.add_node(vlan4_router2_node) - # bind the router stack to the vlan network through this node - router2.nsap.bind(vlan4_router2_node, 4) + router2.bind(vlan4, 1, 4) if _debug: _log.debug(" - router2 bound to VLAN-4") - # make a vlan device object - vlan4_device = \ - LocalDeviceObject( - objectName="VLAN Node 402", - objectIdentifier=('device', 402), - maxApduLengthAccepted=1024, - segmentationSupported='noSegmentation', - vendorIdentifier=15, - ) - _log.debug(" - vlan4_device: %r", vlan4_device) - # make the application, add it to the network - vlan4_app = VLANApplication(vlan4_device, Address(2)) + vlan4_app = VLANApplication( + objectName="VLAN Node 402", + deviceInstance=402, + address=2, + ) vlan4.add_node(vlan4_app.vlan_node) _log.debug(" - vlan4_app: %r", vlan4_app) @@ -274,7 +270,7 @@ def main(): # Test 1 # - if which_test == 1: + if args.test_id == 1: # ask the first device to Who-Is everybody deferred(vlan1_app.who_is) @@ -283,7 +279,7 @@ def main(): # Test 2 # - if which_test == 2: + if args.test_id == 2: # make a read request read_property_request = ReadPropertyRequest( destination=Address("2:2"), @@ -299,7 +295,7 @@ def main(): # Test 3 # - if which_test == 3: + if args.test_id == 3: # make a read request read_property_request = ReadPropertyRequest( destination=Address("3:2"), @@ -315,7 +311,7 @@ def main(): # Test 4 # - if which_test == 4: + if args.test_id == 4: # make a read request read_property_request = ReadPropertyRequest( destination=Address("4:2"), From 26792bb941a2cffb185c34db90f9bbfb6e05d616 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Thu, 25 May 2017 00:25:23 -0400 Subject: [PATCH 07/72] give names to vlan nodes, makes it a little each to see which one is getting an indication --- py27/bacpypes/vlan.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/py27/bacpypes/vlan.py b/py27/bacpypes/vlan.py index fee20069..eb6d680a 100755 --- a/py27/bacpypes/vlan.py +++ b/py27/bacpypes/vlan.py @@ -25,9 +25,10 @@ @bacpypes_debugging class Network: - def __init__(self, dropPercent=0.0): - if _debug: Network._debug("__init__ dropPercent=%r", dropPercent) + def __init__(self, name='', dropPercent=0.0): + if _debug: Network._debug("__init__ name=%r dropPercent=%r", name, dropPercent) + self.name = name self.nodes = [] self.dropPercent = dropPercent @@ -38,6 +39,10 @@ def add_node(self, node): self.nodes.append(node) node.lan = self + # update the node name + if not node.name: + node.name = '[%s:%s]' % (self.name, node.address) + def remove_node(self, node): """ Remove a node from this network. """ if _debug: Network._debug("remove_node %r", node) @@ -60,14 +65,14 @@ def process_pdu(self, pdu): raise RuntimeError("invalid destination address: %r" % (pdu.pduDestination,)) elif pdu.pduDestination.addrType == Address.localBroadcastAddr: - for n in self.nodes: - if (pdu.pduSource != n.address): - n.response(deepcopy(pdu)) + for node in self.nodes: + if (pdu.pduSource != node.address): + node.response(deepcopy(pdu)) elif pdu.pduDestination.addrType == Address.localStationAddr: - for n in self.nodes: - if n.promiscuous or (pdu.pduDestination == n.address): - n.response(deepcopy(pdu)) + for node in self.nodes: + if node.promiscuous or (pdu.pduDestination == node.address): + node.response(deepcopy(pdu)) else: raise RuntimeError("invalid destination address type: %r" % (pdu.pduDestination,)) @@ -94,6 +99,10 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): if not isinstance(addr, Address): raise TypeError("addr must be an address") + # start out with no name + self.name = '' + + # unbound self.lan = None self.address = addr @@ -113,7 +122,7 @@ def bind(self, lan): def indication(self, pdu): """Send a message.""" - if _debug: Node._debug("indication %r", pdu) + if _debug: Node._debug("%sindication %r", self.name, pdu) # make sure we're connected if not self.lan: From 5dad827c8284c9de5eb598e4377fc03194041314 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Mon, 26 Jun 2017 17:45:49 -0400 Subject: [PATCH 08/72] give some names to the networks --- sandbox/vlan_to_vlan.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sandbox/vlan_to_vlan.py b/sandbox/vlan_to_vlan.py index 9674d702..4fbe19d6 100755 --- a/sandbox/vlan_to_vlan.py +++ b/sandbox/vlan_to_vlan.py @@ -162,7 +162,7 @@ def main(): # # create VLAN-1 - vlan1 = Network() + vlan1 = Network(name='1') if _debug: _log.debug(" - vlan1: %r", vlan1) # bind the router to the vlan @@ -183,7 +183,7 @@ def main(): # # create VLAN-2 - vlan2 = Network() + vlan2 = Network(name='2') if _debug: _log.debug(" - vlan2: %r", vlan2) # bind the router stack to the vlan network through this node @@ -204,7 +204,7 @@ def main(): # # create VLAN-3 - vlan3 = Network() + vlan3 = Network(name='3') if _debug: _log.debug(" - vlan3: %r", vlan3) # bind the router stack to the vlan network through this node @@ -249,7 +249,7 @@ def main(): # # create VLAN-4 - vlan4 = Network() + vlan4 = Network(name='4') if _debug: _log.debug(" - vlan4: %r", vlan4) # bind the router stack to the vlan network through this node From 21a1f2b1e3ad400dcac56519909df06ad92f8a20 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 19 Sep 2017 23:39:03 -0400 Subject: [PATCH 09/72] fix encoding function, add a comparison function for testing --- py25/bacpypes/npdu.py | 8 +++++++- py27/bacpypes/npdu.py | 8 +++++++- py34/bacpypes/npdu.py | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/py25/bacpypes/npdu.py b/py25/bacpypes/npdu.py index 8e2ebaa8..944e197c 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 diff --git a/py27/bacpypes/npdu.py b/py27/bacpypes/npdu.py index c6ad9d97..b5d0af89 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 diff --git a/py34/bacpypes/npdu.py b/py34/bacpypes/npdu.py index c6ad9d97..b5d0af89 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 From c93b72664d3d95394c48145146af11d8352acbdb Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 19 Sep 2017 23:39:56 -0400 Subject: [PATCH 10/72] being network layer tests --- tests/__init__.py | 1 + tests/test_npdu/__init__.py | 8 + tests/test_npdu/test_codec.py | 298 ++++++++++++++++++++++++++++++++++ 3 files changed, 307 insertions(+) create mode 100644 tests/test_npdu/__init__.py create mode 100644 tests/test_npdu/test_codec.py diff --git a/tests/__init__.py b/tests/__init__.py index 3bac612a..12432173 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -21,6 +21,7 @@ from . import test_vlan from . import test_bvll +from . import test_npdu from . import test_service diff --git a/tests/test_npdu/__init__.py b/tests/test_npdu/__init__.py new file mode 100644 index 00000000..dc737153 --- /dev/null +++ b/tests/test_npdu/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/python + +""" +Test Network Layer Functionality +""" + +from . import test_codec + diff --git a/tests/test_npdu/test_codec.py b/tests/test_npdu/test_codec.py new file mode 100644 index 00000000..747bd361 --- /dev/null +++ b/tests/test_npdu/test_codec.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test NPDU Encoding and Decoding +------------------------------- +""" + +import string +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, btox, xtob + +from bacpypes.comm import Client, Server, bind +from bacpypes.pdu import PDU, Address, LocalBroadcast + +from bacpypes.npdu import ( + npdu_types, NPDU, + WhoIsRouterToNetwork, IAmRouterToNetwork, ICouldBeRouterToNetwork, + RejectMessageToNetwork, RouterBusyToNetwork, RouterAvailableToNetwork, + RoutingTableEntry, InitializeRoutingTable, InitializeRoutingTableAck, + EstablishConnectionToNetwork, DisconnectConnectionToNetwork, + WhatIsNetworkNumber, NetworkNumberIs, + ) + +from ..trapped_classes import TrappedClient, TrappedServer +from ..state_machine import match_pdu + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class NPDUCodec(Client, Server): + + def __init__(self): + if _debug: NPDUCodec._debug("__init__") + + Client.__init__(self) + Server.__init__(self) + + def indication(self, npdu): + if _debug: NPDUCodec._debug("indication %r", npdu) + + # first as a generic NPDU + xpdu = NPDU() + npdu.encode(xpdu) + + # now as a vanilla PDU + ypdu = PDU() + xpdu.encode(ypdu) + if _debug: NPDUCodec._debug(" - encoded: %r", ypdu) + + # send it downstream + self.request(ypdu) + + def confirmation(self, pdu): + if _debug: NPDUCodec._debug("confirmation %r", pdu) + + # decode as a generic NPDU + xpdu = NPDU() + xpdu.decode(pdu) + + # do a deeper decode of the NPDU + ypdu = npdu_types[xpdu.npduNetMessage]() + ypdu.decode(xpdu) + + # send it upstream + self.response(ypdu) + + +@bacpypes_debugging +class TestNPDUCodec(unittest.TestCase): + + def setup_method(self, method): + """This function is called before each test method is called as is + given a reference to the test method.""" + if _debug: TestNPDUCodec._debug("setup_method %r", method) + + # minature trapped stack + self.client = TrappedClient() + self.codec = NPDUCodec() + self.server = TrappedServer() + bind(self.client, self.codec, self.server) + + def request(self, pdu): + """Pass the PDU to the client to send down the stack.""" + self.client.request(pdu) + + def indication(self, pdu_type=None, **pdu_attrs): + """Check what the server received.""" + assert match_pdu(self.server.indication_received, pdu_type, **pdu_attrs) + + def response(self, pdu): + """Pass the PDU to the server to send up the stack.""" + self.server.response(pdu) + + def confirmation(self, pdu_type=None, **pdu_attrs): + """Check what the client received.""" + assert match_pdu(self.client.confirmation_received, pdu_type, **pdu_attrs) + + def test_who_is_router_to_network(self): + """Test the Result encoding and decoding.""" + if _debug: TestNPDUCodec._debug("test_who_is_router_to_network") + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '00 0001' # message type and network + ) + + self.request(WhoIsRouterToNetwork(1)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(WhoIsRouterToNetwork, wirtnNetwork=1) + + def test_i_am_router_to_network_empty(self): + """Test the IAmRouterToNetwork with no networks encoding and decoding.""" + if _debug: TestNPDUCodec._debug("test_i_am_router_to_network_empty") + + # Request successful + network_list = [] + pdu_bytes = xtob('01.80' # version, network layer message + '01' # message type, no networks + ) + + self.request(IAmRouterToNetwork(network_list)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(IAmRouterToNetwork, iartnNetworkList=network_list) + + def test_i_am_router_to_networks(self): + """Test the IAmRouterToNetwork with multiple networks encoding and decoding.""" + if _debug: TestNPDUCodec._debug("test_i_am_router_to_networks") + + # Request successful + network_list = [1, 2, 3] + pdu_bytes = xtob('01.80' # version, network layer message + '01 0001 0002 0003' # message type and network list + ) + + self.request(IAmRouterToNetwork(network_list)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(IAmRouterToNetwork, iartnNetworkList=network_list) + + def test_i_could_be_router_to_network(self): + """Test the ICouldBeRouterToNetwork encoding and decoding.""" + if _debug: TestNPDUCodec._debug("test_i_could_be_router_to_network") + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '02 0001 02' # message type, network, performance + ) + + self.request(ICouldBeRouterToNetwork(1, 2)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(ICouldBeRouterToNetwork, icbrtnNetwork=1, icbrtnPerformanceIndex=2) + + def test_reject_message_to_network(self): + """Test the RejectMessageToNetwork encoding and decoding.""" + if _debug: TestNPDUCodec._debug("test_reject_message_to_network") + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '03 01 0002' # message type, network, performance + ) + + self.request(RejectMessageToNetwork(1, 2)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(RejectMessageToNetwork, rmtnRejectionReason=1, rmtnDNET=2) + + def test_router_busy_to_network_empty(self): + """Test the RouterBusyToNetwork with no networks encoding and decoding.""" + if _debug: TestNPDUCodec._debug("test_router_busy_to_network_empty") + + # Request successful + network_list = [] + pdu_bytes = xtob('01.80' # version, network layer message + '04' # message type, no networks + ) + + self.request(RouterBusyToNetwork(network_list)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(RouterBusyToNetwork, rbtnNetworkList=network_list) + + def test_router_busy_to_networks(self): + """Test the RouterBusyToNetwork with multiple networks encoding and decoding.""" + if _debug: TestNPDUCodec._debug("test_router_busy_to_networks") + + # Request successful + network_list = [1, 2, 3] + pdu_bytes = xtob('01.80' # version, network layer message + '04 0001 0002 0003' # message type and network list + ) + + self.request(RouterBusyToNetwork(network_list)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(RouterBusyToNetwork, rbtnNetworkList=network_list) + + def test_router_available_to_network_empty(self): + """Test the RouterAvailableToNetwork with no networks encoding and decoding.""" + if _debug: TestNPDUCodec._debug("test_router_available_to_network_empty") + + # Request successful + network_list = [] + pdu_bytes = xtob('01.80' # version, network layer message + '05' # message type, no networks + ) + + self.request(RouterAvailableToNetwork(network_list)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(RouterAvailableToNetwork, ratnNetworkList=network_list) + + def test_router_available_to_networks(self): + """Test the RouterAvailableToNetwork with multiple networks encoding and decoding.""" + if _debug: TestNPDUCodec._debug("test_router_available_to_networks") + + # Request successful + network_list = [1, 2, 3] + pdu_bytes = xtob('01.80' # version, network layer message + '05 0001 0002 0003' # message type and network list + ) + + self.request(RouterAvailableToNetwork(network_list)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(RouterAvailableToNetwork, ratnNetworkList=network_list) + + def test_initialize_routing_table_empty(self): + """Test the InitializeRoutingTable with no routing table entries.""" + if _debug: TestNPDUCodec._debug("test_initialize_routing_table_empty") + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '06 00' # message type and list length + ) + + self.request(InitializeRoutingTable()) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(InitializeRoutingTable, irtTable=[]) + + def test_initialize_routing_table_01(self): + """Test the RouterAvailableToNetwork with a routing table entry.""" + if _debug: TestNPDUCodec._debug("test_initialize_routing_table_01") + + # build a routing table entry + rte = RoutingTableEntry(1, 2, xtob('')) + rt_entries = [rte] + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '06 01' # message type and list length + '0001 02 00' # network, port number, port info + ) + + self.request(InitializeRoutingTable(rt_entries)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(InitializeRoutingTable, irtTable=rt_entries) + + def test_initialize_routing_table_02(self): + """Test the RouterAvailableToNetwork with a routing table entry.""" + if _debug: TestNPDUCodec._debug("test_initialize_routing_table_02") + + # build a routing table entry + rte = RoutingTableEntry(3, 4, xtob('deadbeef')) + rt_entries = [rte] + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '06 01' # message type and list length + '0003 04 04 DEADBEEF' # network, port number, port info + ) + + self.request(InitializeRoutingTable(rt_entries)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(InitializeRoutingTable, irtTable=rt_entries) + From 61c18fa3d024ddc9143ac21f14ac160fb68722a1 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Wed, 20 Sep 2017 21:01:22 -0400 Subject: [PATCH 11/72] fix WhatIsNetworkNumber and NetworkNumberIs encoding/decoding --- py25/bacpypes/npdu.py | 14 +++++++++++++- py27/bacpypes/npdu.py | 20 ++++++++++++++++---- py34/bacpypes/npdu.py | 20 ++++++++++++++++---- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/py25/bacpypes/npdu.py b/py25/bacpypes/npdu.py index 944e197c..69033ed8 100755 --- a/py25/bacpypes/npdu.py +++ b/py25/bacpypes/npdu.py @@ -744,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) @@ -764,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/py27/bacpypes/npdu.py b/py27/bacpypes/npdu.py index b5d0af89..436f6bb8 100755 --- a/py27/bacpypes/npdu.py +++ b/py27/bacpypes/npdu.py @@ -741,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) @@ -761,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/npdu.py b/py34/bacpypes/npdu.py index b5d0af89..436f6bb8 100755 --- a/py34/bacpypes/npdu.py +++ b/py34/bacpypes/npdu.py @@ -741,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) @@ -761,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), )) From 3459dad17fd3e69a7cd7270d80895576fff6f0f1 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Wed, 20 Sep 2017 21:01:46 -0400 Subject: [PATCH 12/72] finish the codec tests --- tests/test_npdu/test_codec.py | 100 ++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tests/test_npdu/test_codec.py b/tests/test_npdu/test_codec.py index 747bd361..39e0060b 100644 --- a/tests/test_npdu/test_codec.py +++ b/tests/test_npdu/test_codec.py @@ -296,3 +296,103 @@ def test_initialize_routing_table_02(self): self.response(PDU(pdu_bytes)) self.confirmation(InitializeRoutingTable, irtTable=rt_entries) + def test_initialize_routing_table_ack_01(self): + """Test the InitializeRoutingTableAck with a routing table entry.""" + if _debug: TestNPDUCodec._debug("test_initialize_routing_table_ack_01") + + # build a routing table entry + rte = RoutingTableEntry(1, 2, xtob('')) + rt_entries = [rte] + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '07 01' # message type and list length + '0001 02 00' # network, port number, port info + ) + + self.request(InitializeRoutingTableAck(rt_entries)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(InitializeRoutingTableAck, irtaTable=rt_entries) + + def test_initialize_routing_table_ack_02(self): + """Test the RouterAvailableToNetwork with a routing table entry.""" + if _debug: TestNPDUCodec._debug("test_initialize_routing_table_ack_02") + + # build a routing table entry + rte = RoutingTableEntry(3, 4, xtob('deadbeef')) + rt_entries = [rte] + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '07 01' # message type and list length + '0003 04 04 DEADBEEF' # network, port number, port info + ) + + self.request(InitializeRoutingTableAck(rt_entries)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(InitializeRoutingTableAck, irtaTable=rt_entries) + + def test_establish_connection_to_network(self): + """Test the EstablishConnectionToNetwork with a routing table entry.""" + if _debug: TestNPDUCodec._debug("test_establish_connection_to_network") + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '08 0005 06' # message type, network, termination time + ) + + self.request(EstablishConnectionToNetwork(5, 6)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(EstablishConnectionToNetwork, ectnDNET=5, ectnTerminationTime=6) + + def test_disconnect_connection_to_network(self): + """Test the DisconnectConnectionToNetwork with a routing table entry.""" + if _debug: TestNPDUCodec._debug("test_disconnect_connection_to_network") + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '09 0007' # message type, network + ) + + self.request(DisconnectConnectionToNetwork(7)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(DisconnectConnectionToNetwork, dctnDNET=7) + + def test_what_is_network_number(self): + """Test the WhatIsNetworkNumber with a routing table entry.""" + if _debug: TestNPDUCodec._debug("test_what_is_network_number") + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '12' # message type + ) + + self.request(WhatIsNetworkNumber()) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(WhatIsNetworkNumber) + + def test_network_number_is(self): + """Test the NetworkNumberIs with a routing table entry.""" + if _debug: TestNPDUCodec._debug("test_network_number_is") + + # Request successful + pdu_bytes = xtob('01.80' # version, network layer message + '13 0008 01' # message type, network, flag + ) + + self.request(NetworkNumberIs(8, 1)) + self.indication(pduData=pdu_bytes) + + self.response(PDU(pdu_bytes)) + self.confirmation(NetworkNumberIs, nniNet=8, nniFlag=1) + From b10250f8fa415acd515c81a2075969f77a2fa1ee Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Fri, 22 Sep 2017 00:27:04 -0400 Subject: [PATCH 13/72] additional tests --- tests/test_npdu/__init__.py | 1 + tests/test_npdu/helpers.py | 212 +++++++++++++++++++++++++++++++++ tests/test_npdu/test_codec.py | 41 +------ tests/test_npdu/test_simple.py | 208 ++++++++++++++++++++++++++++++++ 4 files changed, 423 insertions(+), 39 deletions(-) create mode 100644 tests/test_npdu/helpers.py create mode 100644 tests/test_npdu/test_simple.py diff --git a/tests/test_npdu/__init__.py b/tests/test_npdu/__init__.py index dc737153..661199cf 100644 --- a/tests/test_npdu/__init__.py +++ b/tests/test_npdu/__init__.py @@ -5,4 +5,5 @@ """ from . import test_codec +from . import test_simple diff --git a/tests/test_npdu/helpers.py b/tests/test_npdu/helpers.py new file mode 100644 index 00000000..269007c5 --- /dev/null +++ b/tests/test_npdu/helpers.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python + +""" +Network VLAN Helper Classes +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger + +from bacpypes.comm import Client, Server, bind +from bacpypes.pdu import Address, LocalBroadcast, PDU +from bacpypes.npdu import npdu_types, NPDU +from bacpypes.vlan import Node + +from bacpypes.app import Application +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement + +from ..state_machine import ClientStateMachine + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class NPDUCodec(Client, Server): + + def __init__(self): + if _debug: NPDUCodec._debug("__init__") + + Client.__init__(self) + Server.__init__(self) + + def indication(self, npdu): + if _debug: NPDUCodec._debug("indication %r", npdu) + + # first as a generic NPDU + xpdu = NPDU() + npdu.encode(xpdu) + + # now as a vanilla PDU + ypdu = PDU() + xpdu.encode(ypdu) + if _debug: NPDUCodec._debug(" - encoded: %r", ypdu) + + # send it downstream + self.request(ypdu) + + def confirmation(self, pdu): + if _debug: NPDUCodec._debug("confirmation %r", pdu) + + # decode as a generic NPDU + xpdu = NPDU() + xpdu.decode(pdu) + + # drop application layer messages + if xpdu.npduNetMessage is None: + return + + # do a deeper decode of the NPDU + ypdu = npdu_types[xpdu.npduNetMessage]() + ypdu.decode(xpdu) + + # send it upstream + self.response(ypdu) + + +# +# _repr +# + +class _repr: + + def __repr__(self): + if not self.running: + state_text = "idle " + else: + state_text = "in " + state_text += repr(self.current_state) + + return "<%s(%s) %s at %s>" % ( + self.__class__.__name__, + getattr(self, 'address', '?'), + state_text, + hex(id(self)), + ) + + +# +# SnifferNode +# + +@bacpypes_debugging +class SnifferNode(_repr, ClientStateMachine): + + def __init__(self, address, vlan): + if _debug: SnifferNode._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 = Node(self.address, vlan, promiscuous=True) + if _debug: SnifferNode._debug(" - node: %r", self.node) + + # bind this to the node + bind(self, self.node) + +# +# NetworkLayerNode +# + +@bacpypes_debugging +class NetworkLayerNode(_repr, ClientStateMachine): + + def __init__(self, address, vlan): + if _debug: NetworkLayerNode._debug("__init__ %r %r", address, vlan) + ClientStateMachine.__init__(self) + + # save the name and address + self.name = address + self.address = Address(address) + + # create a network layer encoder/decoder + self.codec = NPDUCodec() + if _debug: SnifferNode._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) + + # bind this to the node + bind(self, self.codec, self.node) + +# +# RouterNode +# + +@bacpypes_debugging +class RouterNode: + + def __init__(self): + if _debug: RouterNode._debug("__init__") + + # 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) + + def add_network(self, address, vlan, net): + if _debug: RouterNode._debug("add_network %r %r %r", address, vlan, net) + + # convert the address to an Address + address = Address(address) + + # create a node, added to the network + node = Node(address, vlan) + if _debug: RouterNode._debug(" - node: %r", self.node) + + # bind the BIP stack to the local network + self.nsap.bind(node, net) + +# +# ApplicationNode +# + +@bacpypes_debugging +class ApplicationNode(_repr, Application, ClientStateMachine): + + def __init__(self, localDevice, vlan): + if _debug: ApplicationNode._debug("__init__ %r %r", address, vlan) + + # build an address and save it + self.address = Address(localDevice.objectIdentifier[1]) + if _debug: ApplicationNode._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 + node = Node(self.address, vlan) + if _debug: RouterNode._debug(" - node: %r", self.node) + + # bind the BIP stack to the local network + self.nsap.bind(node, net) + diff --git a/tests/test_npdu/test_codec.py b/tests/test_npdu/test_codec.py index 39e0060b..2e6c8889 100644 --- a/tests/test_npdu/test_codec.py +++ b/tests/test_npdu/test_codec.py @@ -26,50 +26,13 @@ from ..trapped_classes import TrappedClient, TrappedServer from ..state_machine import match_pdu +from .helpers import NPDUCodec + # some debugging _debug = 0 _log = ModuleLogger(globals()) -@bacpypes_debugging -class NPDUCodec(Client, Server): - - def __init__(self): - if _debug: NPDUCodec._debug("__init__") - - Client.__init__(self) - Server.__init__(self) - - def indication(self, npdu): - if _debug: NPDUCodec._debug("indication %r", npdu) - - # first as a generic NPDU - xpdu = NPDU() - npdu.encode(xpdu) - - # now as a vanilla PDU - ypdu = PDU() - xpdu.encode(ypdu) - if _debug: NPDUCodec._debug(" - encoded: %r", ypdu) - - # send it downstream - self.request(ypdu) - - def confirmation(self, pdu): - if _debug: NPDUCodec._debug("confirmation %r", pdu) - - # decode as a generic NPDU - xpdu = NPDU() - xpdu.decode(pdu) - - # do a deeper decode of the NPDU - ypdu = npdu_types[xpdu.npduNetMessage]() - ypdu.decode(xpdu) - - # send it upstream - self.response(ypdu) - - @bacpypes_debugging class TestNPDUCodec(unittest.TestCase): diff --git a/tests/test_npdu/test_simple.py b/tests/test_npdu/test_simple.py new file mode 100644 index 00000000..2ab31a16 --- /dev/null +++ b/tests/test_npdu/test_simple.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test NPDU Encoding and Decoding +------------------------------- +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, btox, xtob + +from bacpypes.comm import Client, Server, bind +from bacpypes.pdu import PDU, Address, LocalBroadcast +from bacpypes.vlan import Network + +from bacpypes.npdu import ( + npdu_types, NPDU, + WhoIsRouterToNetwork, IAmRouterToNetwork, ICouldBeRouterToNetwork, + RejectMessageToNetwork, RouterBusyToNetwork, RouterAvailableToNetwork, + RoutingTableEntry, InitializeRoutingTable, InitializeRoutingTableAck, + EstablishConnectionToNetwork, DisconnectConnectionToNetwork, + WhatIsNetworkNumber, NetworkNumberIs, + ) + +from ..state_machine import match_pdu, StateMachineGroup +from ..time_machine import reset_time_machine, run_time_machine + +from .helpers import SnifferNode, NetworkLayerNode, RouterNode + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# TNetwork +# + +@bacpypes_debugging +class TNetwork(StateMachineGroup): + + def __init__(self): + if _debug: TNetwork._debug("__init__") + StateMachineGroup.__init__(self) + + # reset the time machine + reset_time_machine() + if _debug: TNetwork._debug(" - time machine reset") + + # make a little LAN + self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) + + # test device + self.td = NetworkLayerNode("1", self.vlan1) + self.append(self.td) + + # sniffer node + self.sniffer1 = SnifferNode("2", self.vlan1) + self.append(self.sniffer1) + + # make another little LAN + self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) + + # sniffer node + self.sniffer2 = SnifferNode("3", self.vlan2) + self.append(self.sniffer2) + + # implementation under test + self.iut = RouterNode() + + # add the networks + self.iut.add_network("4", self.vlan1, 1) + self.iut.add_network("5", self.vlan2, 2) + + 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") + for state_machine in self.state_machines: + TNetwork._debug(" - machine: %r", state_machine) + for direction, pdu in state_machine.transaction_log: + TNetwork._debug(" %s %s", direction, str(pdu)) + + # check for success + all_success, some_failed = super(TNetwork, self).check_for_success() + assert all_success + + +@bacpypes_debugging +class TestSimple(unittest.TestCase): + + def test_idle(self): + """Test an idle network, nothing happens is success.""" + if _debug: TestSimple._debug("test_idle") + + # create a network + tnet = TNetwork() + + # all start states are successful + tnet.td.start_state.success() + tnet.sniffer1.start_state.success() + tnet.sniffer2.start_state.success() + + # run the group + tnet.run() + + +@bacpypes_debugging +class TestWhoIsRouterToNetwork(unittest.TestCase): + + def test_01(self): + """Test broadcast for any router.""" + if _debug: TestWhoIsRouterToNetwork._debug("test_01") + + # create a network + tnet = TNetwork() + + # all start states are successful + tnet.td.start_state.doc("1-1-0") \ + .send(WhoIsRouterToNetwork( + destination=LocalBroadcast(), + )).doc("1-1-1") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[2], + ).doc("1-1-2") \ + .success() + + tnet.sniffer1.start_state.success() + + # nothing received on network 2 + tnet.sniffer2.start_state.doc("1-2-0") \ + .timeout(3).doc("1-2-1") \ + .success() + + # run the group + tnet.run() + + def test_02(self): + """Test broadcast for existing router.""" + if _debug: TestWhoIsRouterToNetwork._debug("test_02") + + # create a network + tnet = TNetwork() + + # all start states are successful + tnet.td.start_state.doc("2-1-0") \ + .send(WhoIsRouterToNetwork(2, + destination=LocalBroadcast(), + )).doc("2-1-1") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[2], + ).doc("2-1-2") \ + .success() + + tnet.sniffer1.start_state.success() + + # nothing received on network 2 + tnet.sniffer2.start_state.doc("2-2-0") \ + .timeout(3).doc("2-2-1") \ + .success() + + # run the group + tnet.run() + + def test_03(self): + """Test broadcast for a non-existent router.""" + if _debug: TestWhoIsRouterToNetwork._debug("test_03") + + # create a network + tnet = TNetwork() + + # send request, receive nothing back + tnet.td.start_state.doc("3-1-0") \ + .send(WhoIsRouterToNetwork(3, + destination=LocalBroadcast(), + )).doc("3-1-1") \ + .timeout(3).doc("3-1-2") \ + .success() + + # sniffer on network 1 sees the request + tnet.sniffer1.start_state.doc("3-2-0") \ + .receive(PDU, + pduData=xtob('01.80' # version, network layer + '00 0003' # message type and network + ) + ).doc("3-2-1") \ + .success() + + # sniffer on network 2 sees request forwarded by router + tnet.sniffer2.start_state.doc("3-3-0") \ + .receive(PDU, + pduData=xtob('01.88' # version, network layer, routed + '0001 01 01' # snet/slen/sadr + '00 0003' # message type and network + ), + ).doc("3-3-1") \ + .success() + + # run the group + tnet.run() + From c892cd259d426c5afe098f1fe53054e2a965a854 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Sat, 23 Sep 2017 01:40:43 -0400 Subject: [PATCH 14/72] add a way to capture vlan messages when they are processed --- py27/bacpypes/vlan.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/py27/bacpypes/vlan.py b/py27/bacpypes/vlan.py index 953e266e..dbf0293d 100755 --- a/py27/bacpypes/vlan.py +++ b/py27/bacpypes/vlan.py @@ -35,6 +35,9 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.broadcast_address = broadcast_address self.drop_percent = drop_percent + # hidden sniffer function + self._sniffer = 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 +62,10 @@ def process_pdu(self, pdu): """ if _debug: Network._debug("process_pdu(%s) %r", self.name, pdu) + # if there is a sniffer, call it with the network name and pdu + if self._sniffer: + self._sniffer(self.name, pdu) + # randomly drop a packet if self.drop_percent != 0.0: if (random.random() * 100.0) < self.drop_percent: From f9f5959935aa42863b715033fa935b14e3ea4385 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Sat, 23 Sep 2017 01:41:36 -0400 Subject: [PATCH 15/72] add a function that returns the current time from inside the time machine --- tests/time_machine.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/time_machine.py b/tests/time_machine.py index 140328b5..a9a6f8b0 100755 --- a/tests/time_machine.py +++ b/tests/time_machine.py @@ -193,3 +193,10 @@ def run_time_machine(time_limit): if _debug: run_time_machine._debug(" - no more to do") break + +def current_time(): + """Return the current time from the time machine.""" + global time_machine + + return time_machine.current_time + From 3ff3a036d6b30a45e39ec095d1cdad0ec86386ae Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Sat, 23 Sep 2017 01:41:53 -0400 Subject: [PATCH 16/72] more tests --- tests/test_npdu/__init__.py | 3 +- .../{test_simple.py => test_net_1.py} | 69 ++++- tests/test_npdu/test_net_2.py | 291 ++++++++++++++++++ 3 files changed, 346 insertions(+), 17 deletions(-) rename tests/test_npdu/{test_simple.py => test_net_1.py} (74%) create mode 100644 tests/test_npdu/test_net_2.py diff --git a/tests/test_npdu/__init__.py b/tests/test_npdu/__init__.py index 661199cf..6dbc21d9 100644 --- a/tests/test_npdu/__init__.py +++ b/tests/test_npdu/__init__.py @@ -5,5 +5,6 @@ """ from . import test_codec -from . import test_simple +from . import test_net_1 +from . import test_net_2 diff --git a/tests/test_npdu/test_simple.py b/tests/test_npdu/test_net_1.py similarity index 74% rename from tests/test_npdu/test_simple.py rename to tests/test_npdu/test_net_1.py index 2ab31a16..3ed474d3 100644 --- a/tests/test_npdu/test_simple.py +++ b/tests/test_npdu/test_net_1.py @@ -2,8 +2,11 @@ # -*- coding: utf-8 -*- """ -Test NPDU Encoding and Decoding -------------------------------- +Test Network Discovery +---------------------- + +The TD is on network 1 with sniffer1, network 2 has sniffer2, network 3 has +sniffer3. All three networks are connected to one IUT router. """ import unittest @@ -48,6 +51,9 @@ def __init__(self): reset_time_machine() if _debug: TNetwork._debug(" - time machine reset") + # implementation under test + self.iut = RouterNode() + # make a little LAN self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) @@ -59,20 +65,29 @@ def __init__(self): self.sniffer1 = SnifferNode("2", self.vlan1) self.append(self.sniffer1) + # add the network + self.iut.add_network("3", self.vlan1, 1) + # make another little LAN self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) # sniffer node - self.sniffer2 = SnifferNode("3", self.vlan2) + self.sniffer2 = SnifferNode("4", self.vlan2) self.append(self.sniffer2) - # implementation under test - self.iut = RouterNode() - - # add the networks - self.iut.add_network("4", self.vlan1, 1) + # add the network self.iut.add_network("5", self.vlan2, 2) + # make another little LAN + self.vlan3 = Network(name="vlan3", broadcast_address=LocalBroadcast()) + + # sniffer node + self.sniffer3 = SnifferNode("6", self.vlan3) + self.append(self.sniffer3) + + # add the network + self.iut.add_network("7", self.vlan3, 3) + def run(self, time_limit=60.0): if _debug: TNetwork._debug("run %r", time_limit) @@ -107,6 +122,7 @@ def test_idle(self): tnet.td.start_state.success() tnet.sniffer1.start_state.success() tnet.sniffer2.start_state.success() + tnet.sniffer3.start_state.success() # run the group tnet.run() @@ -122,21 +138,38 @@ def test_01(self): # create a network tnet = TNetwork() - # all start states are successful + # test device sends request, sees response tnet.td.start_state.doc("1-1-0") \ .send(WhoIsRouterToNetwork( destination=LocalBroadcast(), )).doc("1-1-1") \ .receive(IAmRouterToNetwork, - iartnNetworkList=[2], + iartnNetworkList=[2, 3], ).doc("1-1-2") \ .success() - tnet.sniffer1.start_state.success() + # sniffer on network 1 sees the request and the response + tnet.sniffer1.start_state.doc("1-2-0") \ + .receive(PDU, + pduData=xtob('01.80' # version, network layer + '00' # message type, no network + ) + ).doc("1-2-1") \ + .receive(PDU, + pduData=xtob('01.80' # version, network layer + '01 0002 0003' # message type and network list + ) + ).doc("1-2-2") \ + .success() # nothing received on network 2 - tnet.sniffer2.start_state.doc("1-2-0") \ - .timeout(3).doc("1-2-1") \ + tnet.sniffer2.start_state.doc("1-3-0") \ + .timeout(3).doc("1-3-1") \ + .success() + + # nothing received on network 3 + tnet.sniffer3.start_state.doc("1-4-0") \ + .timeout(3).doc("1-4-1") \ .success() # run the group @@ -166,6 +199,8 @@ def test_02(self): .timeout(3).doc("2-2-1") \ .success() + tnet.sniffer3.start_state.success() + # run the group tnet.run() @@ -178,7 +213,7 @@ def test_03(self): # send request, receive nothing back tnet.td.start_state.doc("3-1-0") \ - .send(WhoIsRouterToNetwork(3, + .send(WhoIsRouterToNetwork(4, destination=LocalBroadcast(), )).doc("3-1-1") \ .timeout(3).doc("3-1-2") \ @@ -188,7 +223,7 @@ def test_03(self): tnet.sniffer1.start_state.doc("3-2-0") \ .receive(PDU, pduData=xtob('01.80' # version, network layer - '00 0003' # message type and network + '00 0004' # message type and network ) ).doc("3-2-1") \ .success() @@ -198,11 +233,13 @@ def test_03(self): .receive(PDU, pduData=xtob('01.88' # version, network layer, routed '0001 01 01' # snet/slen/sadr - '00 0003' # message type and network + '00 0004' # message type and network ), ).doc("3-3-1") \ .success() + tnet.sniffer3.start_state.success() + # run the group tnet.run() diff --git a/tests/test_npdu/test_net_2.py b/tests/test_npdu/test_net_2.py new file mode 100644 index 00000000..87ba40c3 --- /dev/null +++ b/tests/test_npdu/test_net_2.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Network Discovery +---------------------- + +The TD is on network 1 with sniffer1, network 2 has sniffer2, network 3 has +sniffer3. Network 1 and 2 are connected with a router, network 2 and 3 +are connected by a different router. +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, btox, xtob + +from bacpypes.comm import Client, Server, bind +from bacpypes.pdu import PDU, Address, LocalBroadcast +from bacpypes.vlan import Network + +from bacpypes.npdu import ( + npdu_types, NPDU, + WhoIsRouterToNetwork, IAmRouterToNetwork, ICouldBeRouterToNetwork, + RejectMessageToNetwork, RouterBusyToNetwork, RouterAvailableToNetwork, + RoutingTableEntry, InitializeRoutingTable, InitializeRoutingTableAck, + EstablishConnectionToNetwork, DisconnectConnectionToNetwork, + WhatIsNetworkNumber, NetworkNumberIs, + ) + +from ..state_machine import match_pdu, StateMachineGroup +from ..time_machine import reset_time_machine, run_time_machine, current_time + +from .helpers import SnifferNode, NetworkLayerNode, RouterNode + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# TrafficLog +# + +@bacpypes_debugging +class TrafficLog: + + def __init__(self): + """Initialize with no traffic.""" + self.traffic = [] + + def __call__(self, *args): + """Capture the current time and the arguments.""" + self.traffic.append((current_time(),) + args) + + def dump(self): + """Dump the traffic.""" + for args in self.traffic: + arg_format = " %6.3f:" + for arg in args[1:]: + if hasattr(arg, 'debug_contents'): + arg_format += " %r" + else: + arg_format += " %s" + TrafficLog._debug(arg_format, *args) + +# +# TNetwork +# + +@bacpypes_debugging +class TNetwork(StateMachineGroup): + + def __init__(self): + if _debug: TNetwork._debug("__init__") + 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() + + # implementation under test + self.iut1 = RouterNode() # router from vlan1 to vlan2 + self.iut2 = RouterNode() # router from vlan2 to vlan3 + + # make a little LAN + self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) + self.vlan1._sniffer = self.traffic_log + + # test device + self.td = NetworkLayerNode("1", self.vlan1) + self.append(self.td) + + # sniffer node + self.sniffer1 = SnifferNode("2", self.vlan1) + self.append(self.sniffer1) + + # connect vlan1 to iut1 + self.iut1.add_network("3", self.vlan1, 1) + + # make another little LAN + self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) + self.vlan2._sniffer = self.traffic_log + + # sniffer node + self.sniffer2 = SnifferNode("4", self.vlan2) + self.append(self.sniffer2) + + # connect vlan2 to both routers + self.iut1.add_network("5", self.vlan2, 2) + self.iut2.add_network("6", self.vlan2, 2) + + # make another little LAN + self.vlan3 = Network(name="vlan3", broadcast_address=LocalBroadcast()) + self.vlan3._sniffer = self.traffic_log + + # sniffer node + self.sniffer3 = SnifferNode("7", self.vlan3) + self.append(self.sniffer3) + + # connect vlan3 to the second router + self.iut2.add_network("8", self.vlan3, 3) + + 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") + for state_machine in self.state_machines: + TNetwork._debug(" - machine: %r", state_machine) + for direction, pdu in state_machine.transaction_log: + TNetwork._debug(" %s %s", direction, str(pdu)) + self.traffic_log.dump() + + # check for success + all_success, some_failed = super(TNetwork, self).check_for_success() + assert all_success + + +@bacpypes_debugging +class TestSimple(unittest.TestCase): + + def test_idle(self): + """Test an idle network, nothing happens is success.""" + if _debug: TestSimple._debug("test_idle") + + # create a network + tnet = TNetwork() + + # all start states are successful + tnet.td.start_state.success() + tnet.sniffer1.start_state.success() + tnet.sniffer2.start_state.success() + tnet.sniffer3.start_state.success() + + # run the group + tnet.run() + + +@bacpypes_debugging +class TestWhoIsRouterToNetwork(unittest.TestCase): + + def test_01(self): + """Test broadcast for any router.""" + if _debug: TestWhoIsRouterToNetwork._debug("test_01") + + # create a network + tnet = TNetwork() + + # test device sends request, sees response + tnet.td.start_state.doc("1-1-0") \ + .send(WhoIsRouterToNetwork( + destination=LocalBroadcast(), + )).doc("1-1-1") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[2], + ).doc("1-1-2") \ + .success() + + # sniffer on network 1 sees the request and the response + tnet.sniffer1.start_state.doc("1-2-0") \ + .receive(PDU, + pduData=xtob('01.80' # version, network layer + '00' # message type, no network + ) + ).doc("1-2-1") \ + .receive(PDU, + pduData=xtob('01.80' # version, network layer + '01 0002' # message type and network list + ) + ).doc("1-2-2") \ + .success() + + # nothing received on network 2 + tnet.sniffer2.start_state.doc("1-3-0") \ + .timeout(3).doc("1-3-1") \ + .success() + + # nothing received on network 3 + tnet.sniffer3.start_state.doc("1-4-0") \ + .timeout(3).doc("1-4-1") \ + .success() + + # run the group + tnet.run() + + def test_02(self): + """Test broadcast for existing router.""" + if _debug: TestWhoIsRouterToNetwork._debug("test_02") + + # create a network + tnet = TNetwork() + + # test device sends request, receives response + tnet.td.start_state.doc("2-1-0") \ + .send(WhoIsRouterToNetwork(2, + destination=LocalBroadcast(), + )).doc("2-1-1") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[2], + ).doc("2-1-2") \ + .success() + + tnet.sniffer1.start_state.success() + + # nothing received on network 2 + tnet.sniffer2.start_state.doc("2-2-0") \ + .timeout(3).doc("2-2-1") \ + .success() + + # nothing received on network 3 + tnet.sniffer3.start_state.doc("2-3-0") \ + .timeout(3).doc("2-3-1") \ + .success() + + # run the group + tnet.run() + + def test_03(self): + """Test broadcast for a non-existent router.""" + if _debug: TestWhoIsRouterToNetwork._debug("test_03") + + # create a network + tnet = TNetwork() + + # send request, receive nothing back + tnet.td.start_state.doc("3-1-0") \ + .send(WhoIsRouterToNetwork(4, + destination=LocalBroadcast(), + )).doc("3-1-1") \ + .timeout(3).doc("3-1-2") \ + .success() + + # sniffer on network 1 sees the request + tnet.sniffer1.start_state.doc("3-2-0") \ + .receive(PDU, + pduData=xtob('01.80' # version, network layer + '00 0004' # message type and network + ) + ).doc("3-2-1") \ + .success() + + # sniffer on network 2 sees request forwarded by router + tnet.sniffer2.start_state.doc("3-3-0") \ + .receive(PDU, + pduData=xtob('01.88' # version, network layer, routed + '0001 01 01' # snet/slen/sadr + '00 0004' # message type and network + ), + ).doc("3-3-1") \ + .success() + + tnet.sniffer3.start_state.doc("3-4-0") \ + .receive(PDU, + pduData=xtob('01.88' # version, network layer, routed + '0001 01 01' # snet/slen/sadr + '00 0004' # message type and network + ), + ).doc("3-4-1") \ + .success() + + # run the group + tnet.run() + From d491f649c064b98e81cfa3d7c8ace1d3a25d47f1 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Mon, 25 Sep 2017 21:16:34 -0400 Subject: [PATCH 17/72] rename the traffic log from something hidden --- py27/bacpypes/vlan.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/py27/bacpypes/vlan.py b/py27/bacpypes/vlan.py index dbf0293d..ead2d03a 100755 --- a/py27/bacpypes/vlan.py +++ b/py27/bacpypes/vlan.py @@ -35,8 +35,8 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.broadcast_address = broadcast_address self.drop_percent = drop_percent - # hidden sniffer function - self._sniffer = None + # 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. """ @@ -62,9 +62,9 @@ def process_pdu(self, pdu): """ if _debug: Network._debug("process_pdu(%s) %r", self.name, pdu) - # if there is a sniffer, call it with the network name and pdu - if self._sniffer: - self._sniffer(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: From 5667822aa21864163df9c9012ab492a11b840ab3 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Mon, 25 Sep 2017 21:18:05 -0400 Subject: [PATCH 18/72] add the TrafficLog class to the state machine stuff --- tests/state_machine.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/state_machine.py b/tests/state_machine.py index e4415389..e874a70f 100755 --- a/tests/state_machine.py +++ b/tests/state_machine.py @@ -16,6 +16,8 @@ from bacpypes.comm import Client, Server from bacpypes.task import OneShotTask +from .time_machine import current_time + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -1260,3 +1262,29 @@ def indication(self, pdu): if _debug: ServerStateMachine._debug("indication %r", pdu) self.receive(pdu) + +# +# TrafficLog +# + +class TrafficLog: + + def __init__(self): + """Initialize with no traffic.""" + self.traffic = [] + + def __call__(self, *args): + """Capture the current time and the arguments.""" + self.traffic.append((current_time(),) + args) + + def dump(self, handler_fn): + """Dump the traffic, pass the correct handler like SomeClass._debug""" + for args in self.traffic: + arg_format = " %6.3f:" + for arg in args[1:]: + if hasattr(arg, 'debug_contents'): + arg_format += " %r" + else: + arg_format += " %s" + handler_fn(arg_format, *args) + From f666068ce3bbd4fdad35be65abe57793bd774ba3 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Mon, 25 Sep 2017 21:18:55 -0400 Subject: [PATCH 19/72] simplify debugging because state machine repr's have improved --- tests/test_bvll/helpers.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/tests/test_bvll/helpers.py b/tests/test_bvll/helpers.py index 58da6f74..5146319f 100644 --- a/tests/test_bvll/helpers.py +++ b/tests/test_bvll/helpers.py @@ -80,33 +80,12 @@ def confirmation(self, pdu): # continue upstream self.response(PDU(pdu, source=src, destination=dest)) -# -# _repr -# - -class _repr: - - def __repr__(self): - if not self.running: - state_text = "idle " - else: - state_text = "in " - state_text += repr(self.current_state) - - return "<%s(%s) %s at %s>" % ( - self.__class__.__name__, - getattr(self, 'address', '?'), - state_text, - hex(id(self)), - ) - - # # SnifferNode # @bacpypes_debugging -class SnifferNode(_repr, ClientStateMachine): +class SnifferNode(ClientStateMachine): def __init__(self, address, vlan): if _debug: SnifferNode._debug("__init__ %r %r", address, vlan) @@ -129,7 +108,7 @@ def __init__(self, address, vlan): # @bacpypes_debugging -class CodecNode(_repr, ClientStateMachine): +class CodecNode(ClientStateMachine): def __init__(self, address, vlan): if _debug: CodecNode._debug("__init__ %r %r", address, vlan) @@ -154,7 +133,7 @@ def __init__(self, address, vlan): # @bacpypes_debugging -class SimpleNode(_repr, ClientStateMachine): +class SimpleNode(ClientStateMachine): def __init__(self, address, vlan): if _debug: SimpleNode._debug("__init__ %r %r", address, vlan) @@ -180,7 +159,7 @@ def __init__(self, address, vlan): # @bacpypes_debugging -class ForeignNode(_repr, ClientStateMachine): +class ForeignNode(ClientStateMachine): def __init__(self, address, vlan): if _debug: ForeignNode._debug("__init__ %r %r", address, vlan) @@ -205,7 +184,7 @@ def __init__(self, address, vlan): # @bacpypes_debugging -class BBMDNode(_repr, ClientStateMachine): +class BBMDNode(ClientStateMachine): def __init__(self, address, vlan): if _debug: BBMDNode._debug("__init__ %r %r", address, vlan) From 6abfc11a6341ef3074ab2618ec4a834694e0efbe Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Mon, 25 Sep 2017 21:19:18 -0400 Subject: [PATCH 20/72] simplify debugging because state machine repr's have improved --- tests/test_npdu/helpers.py | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/tests/test_npdu/helpers.py b/tests/test_npdu/helpers.py index 269007c5..8954dfb7 100644 --- a/tests/test_npdu/helpers.py +++ b/tests/test_npdu/helpers.py @@ -65,33 +65,12 @@ def confirmation(self, pdu): self.response(ypdu) -# -# _repr -# - -class _repr: - - def __repr__(self): - if not self.running: - state_text = "idle " - else: - state_text = "in " - state_text += repr(self.current_state) - - return "<%s(%s) %s at %s>" % ( - self.__class__.__name__, - getattr(self, 'address', '?'), - state_text, - hex(id(self)), - ) - - # # SnifferNode # @bacpypes_debugging -class SnifferNode(_repr, ClientStateMachine): +class SnifferNode(ClientStateMachine): def __init__(self, address, vlan): if _debug: SnifferNode._debug("__init__ %r %r", address, vlan) @@ -113,7 +92,7 @@ def __init__(self, address, vlan): # @bacpypes_debugging -class NetworkLayerNode(_repr, ClientStateMachine): +class NetworkLayerNode(ClientStateMachine): def __init__(self, address, vlan): if _debug: NetworkLayerNode._debug("__init__ %r %r", address, vlan) @@ -169,7 +148,7 @@ def add_network(self, address, vlan, net): # @bacpypes_debugging -class ApplicationNode(_repr, Application, ClientStateMachine): +class ApplicationNode(Application, ClientStateMachine): def __init__(self, localDevice, vlan): if _debug: ApplicationNode._debug("__init__ %r %r", address, vlan) From 0d97bb398392f1fcb483abcb1b59e69640eba515 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Mon, 25 Sep 2017 21:20:04 -0400 Subject: [PATCH 21/72] shift around debugging --- tests/test_npdu/test_net_2.py | 44 ++++++++++------------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/tests/test_npdu/test_net_2.py b/tests/test_npdu/test_net_2.py index 87ba40c3..10f554fc 100644 --- a/tests/test_npdu/test_net_2.py +++ b/tests/test_npdu/test_net_2.py @@ -27,8 +27,8 @@ WhatIsNetworkNumber, NetworkNumberIs, ) -from ..state_machine import match_pdu, StateMachineGroup -from ..time_machine import reset_time_machine, run_time_machine, current_time +from ..state_machine import match_pdu, StateMachineGroup, TrafficLog +from ..time_machine import reset_time_machine, run_time_machine from .helpers import SnifferNode, NetworkLayerNode, RouterNode @@ -37,32 +37,6 @@ _log = ModuleLogger(globals()) -# -# TrafficLog -# - -@bacpypes_debugging -class TrafficLog: - - def __init__(self): - """Initialize with no traffic.""" - self.traffic = [] - - def __call__(self, *args): - """Capture the current time and the arguments.""" - self.traffic.append((current_time(),) + args) - - def dump(self): - """Dump the traffic.""" - for args in self.traffic: - arg_format = " %6.3f:" - for arg in args[1:]: - if hasattr(arg, 'debug_contents'): - arg_format += " %r" - else: - arg_format += " %s" - TrafficLog._debug(arg_format, *args) - # # TNetwork # @@ -87,7 +61,7 @@ def __init__(self): # make a little LAN self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) - self.vlan1._sniffer = self.traffic_log + self.vlan1.traffic_log = self.traffic_log # test device self.td = NetworkLayerNode("1", self.vlan1) @@ -102,7 +76,7 @@ def __init__(self): # make another little LAN self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) - self.vlan2._sniffer = self.traffic_log + self.vlan2.traffic_log = self.traffic_log # sniffer node self.sniffer2 = SnifferNode("4", self.vlan2) @@ -114,7 +88,7 @@ def __init__(self): # make another little LAN self.vlan3 = Network(name="vlan3", broadcast_address=LocalBroadcast()) - self.vlan3._sniffer = self.traffic_log + self.vlan3.traffic_log = self.traffic_log # sniffer node self.sniffer3 = SnifferNode("7", self.vlan3) @@ -133,11 +107,17 @@ def run(self, time_limit=60.0): run_time_machine(time_limit) if _debug: TNetwork._debug(" - time machine finished") + + # list the state machines which shows their current state for state_machine in self.state_machines: TNetwork._debug(" - machine: %r", state_machine) + + # each one has a list of sent/received pdus for direction, pdu in state_machine.transaction_log: TNetwork._debug(" %s %s", direction, str(pdu)) - self.traffic_log.dump() + + # traffic log has what was processed on each vlan + self.traffic_log.dump(TNetwork._debug) # check for success all_success, some_failed = super(TNetwork, self).check_for_success() From d6851cfed3c482e8a36b28b7ec8c4818663d95aa Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Wed, 1 Nov 2017 22:21:04 -0400 Subject: [PATCH 22/72] sample application for mutable schedule obejct --- sandbox/mutable_schedule_object.py | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 sandbox/mutable_schedule_object.py diff --git a/sandbox/mutable_schedule_object.py b/sandbox/mutable_schedule_object.py new file mode 100644 index 00000000..f2cf2880 --- /dev/null +++ b/sandbox/mutable_schedule_object.py @@ -0,0 +1,70 @@ +#!/usr/bin/python + +""" +Muteable Schedule Object +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.primitivedata import Unsigned +from bacpypes.constructeddata import ArrayOf +from bacpypes.basetypes import DailySchedule +from bacpypes.object import WritableProperty, ScheduleObject, register_object_type + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# MyScheduleObject +# + +@register_object_type(vendor_id=999) +class MyScheduleObject(ScheduleObject): + + properties = [ + WritableProperty('weeklySchedule', ArrayOf(DailySchedule)), + WritableProperty('priorityForWriting', Unsigned), + ] + + def __init__(self, **kwargs): + if _debug: MyScheduleObject._debug("__init__ %r", kwargs) + ScheduleObject.__init__(self, **kwargs) + +# +# +# + +# parse the command line arguments +parser = ArgumentParser(usage=__doc__) +args = parser.parse_args() + +if _debug: _log.debug("initialization") +if _debug: _log.debug(" - args: %r", args) + +# create a schedule object +mso = MyScheduleObject( + objectIdentifier=('schedule', 1), + objectName="myScheduleObject", + weeklySchedule=[], + priorityForWriting=1, + ) + +print("getting value") +print(mso.priorityForWriting) +print("") + +print("setting value") +mso.priorityForWriting = 2 +print("") + +print("reading value") +value = mso.ReadProperty('priorityForWriting') +print("{}".format(value)) +print("") + +print("writing value") +mso.WriteProperty('priorityForWriting', 3) +print("") + From 0279b4ca756c8407f8b8fb06de915c1a00691cff Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Wed, 6 Dec 2017 21:56:03 -0500 Subject: [PATCH 23/72] bring branch up-to-date with stage, keep 0.17.0 version --- py25/bacpypes/app.py | 11 + py25/bacpypes/basetypes.py | 22 +- py25/bacpypes/bvllservice.py | 1 + py25/bacpypes/constructeddata.py | 13 +- py25/bacpypes/iocb.py | 3 +- py25/bacpypes/object.py | 81 ++++-- py25/bacpypes/pdu.py | 4 +- py25/bacpypes/primitivedata.py | 14 +- py25/bacpypes/service/device.py | 35 +-- py25/bacpypes/service/object.py | 60 +++- py25/bacpypes/udp.py | 2 + py27/bacpypes/app.py | 11 + py27/bacpypes/basetypes.py | 23 +- py27/bacpypes/bvllservice.py | 1 + py27/bacpypes/constructeddata.py | 13 +- py27/bacpypes/iocb.py | 5 +- py27/bacpypes/object.py | 82 ++++-- py27/bacpypes/pdu.py | 4 +- py27/bacpypes/primitivedata.py | 12 +- py27/bacpypes/service/device.py | 37 +-- py27/bacpypes/service/object.py | 60 +++- py27/bacpypes/udp.py | 2 + py34/bacpypes/app.py | 11 + py34/bacpypes/basetypes.py | 22 +- py34/bacpypes/bvllservice.py | 1 + py34/bacpypes/constructeddata.py | 15 +- py34/bacpypes/object.py | 81 ++++-- py34/bacpypes/service/device.py | 35 +-- py34/bacpypes/service/object.py | 60 +++- py34/bacpypes/udp.py | 2 + samples/MultipleReadPropertyHammer.py | 229 +++++++++++++++ samples/RandomAnalogValueSleep.py | 148 ++++++++++ samples/ReadAllProperties.py | 188 +++++++++++++ samples/ReadPropertyMultipleServer.py | 7 +- samples/ReadWriteEventMessageTexts.py | 265 ++++++++++++++++++ sandbox/add_remove_property.py | 59 ++++ tests/__init__.py | 1 + tests/test_constructed_data/__init__.py | 14 + tests/test_constructed_data/helpers.py | 88 ++++++ tests/test_constructed_data/test_any.py | 4 + .../test_constructed_data/test_any_atomic.py | 4 + tests/test_constructed_data/test_array_of.py | 198 +++++++++++++ tests/test_constructed_data/test_choice.py | 4 + tests/test_constructed_data/test_sequence.py | 204 ++++++++++++++ .../test_constructed_data/test_sequence_of.py | 4 + tests/test_pdu/test_address.py | 175 ++++++++++-- .../test_character_string.py | 9 +- tests/test_primitive_data/test_enumerated.py | 42 ++- .../test_object_identifier.py | 10 +- tests/test_primitive_data/test_object_type.py | 22 +- tests/test_service/test_object.py | 257 ++++++++++++++++- 51 files changed, 2447 insertions(+), 208 deletions(-) create mode 100755 samples/MultipleReadPropertyHammer.py create mode 100644 samples/RandomAnalogValueSleep.py create mode 100755 samples/ReadAllProperties.py create mode 100644 samples/ReadWriteEventMessageTexts.py create mode 100644 sandbox/add_remove_property.py create mode 100644 tests/test_constructed_data/__init__.py create mode 100644 tests/test_constructed_data/helpers.py create mode 100644 tests/test_constructed_data/test_any.py create mode 100644 tests/test_constructed_data/test_any_atomic.py create mode 100644 tests/test_constructed_data/test_array_of.py create mode 100644 tests/test_constructed_data/test_choice.py create mode 100644 tests/test_constructed_data/test_sequence.py create mode 100644 tests/test_constructed_data/test_sequence_of.py diff --git a/py25/bacpypes/app.py b/py25/bacpypes/app.py index 0dc4b464..059da41e 100755 --- a/py25/bacpypes/app.py +++ b/py25/bacpypes/app.py @@ -135,8 +135,12 @@ def get_device_info(self, key): 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) @@ -177,11 +181,18 @@ def release_device_info(self, info): has finished with the device information.""" if _debug: DeviceInfoCache._debug("release_device_info %r", 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 + 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") bacpypes_debugging(DeviceInfoCache) diff --git a/py25/bacpypes/basetypes.py b/py25/bacpypes/basetypes.py index 5caa3039..b5485f9d 100755 --- a/py25/bacpypes/basetypes.py +++ b/py25/bacpypes/basetypes.py @@ -898,6 +898,7 @@ class EventType(Enumerated): , 'unsignedOutOfRange':16 , 'changeOfCharacterstring':17 , 'changeOfStatusFlags':18 + , 'changeOfReliability':19 } class FaultType(Enumerated): @@ -1471,7 +1472,18 @@ class Reliability(Enumerated): , 'multiStateFault':9 , 'configurationError':10 , 'communicationFailure':12 - , 'numberFault':13 + , 'memberFault': 13 + , 'monitoredObjectFault': 14 + , 'tripped': 15 + , 'lampFailure': 16 + , 'activationFailure': 17 + , 'renewDHCPFailure': 18 + , 'renewFDRegistration-failure': 19 + , 'restartAutoNegotiationFailure': 20 + , 'restartFailure': 21 + , 'proprietaryCommandFailure': 22 + , 'faultsListed': 23 + , 'referencedObjectFault': 24 } class RestartReason(Enumerated): @@ -2261,6 +2273,13 @@ class NotificationParametersChangeOfStatusFlagsType(Sequence): , Element('referencedFlags', StatusFlags, 1) ] +class NotificationParametersChangeOfReliabilityType(Sequence): + sequenceElements = \ + [ Element('reliability', Reliability, 0) + , Element('statusFlags', StatusFlags, 1) + , Element('propertyValues', SequenceOf(PropertyValue), 2) + ] + class NotificationParameters(Choice): choiceElements = \ [ Element('changeOfBitstring', NotificationParametersChangeOfBitstring, 0) @@ -2280,6 +2299,7 @@ class NotificationParameters(Choice): , Element('unsignedOutOfRange', NotificationParametersUnsignedOutOfRangeType, 16) , Element('changeOfCharacterString', NotificationParametersChangeOfCharacterStringType, 17) , Element('changeOfStatusFlags', NotificationParametersChangeOfStatusFlagsType, 18) + , Element('changeOfReliability', NotificationParametersChangeOfReliabilityType, 19) ] class ObjectPropertyValue(Sequence): diff --git a/py25/bacpypes/bvllservice.py b/py25/bacpypes/bvllservice.py index fe1fe29d..af4128c0 100755 --- a/py25/bacpypes/bvllservice.py +++ b/py25/bacpypes/bvllservice.py @@ -102,6 +102,7 @@ def __init__(self, addr=None, noBroadcast=False): bind(self.direct, self.broadcastPort) else: self.broadcast = None + self.broadcastPort = None # create and bind the Annex H and J servers self.annexH = _MultiplexServer(self) diff --git a/py25/bacpypes/constructeddata.py b/py25/bacpypes/constructeddata.py index 2e589b8e..f06d2eee 100755 --- a/py25/bacpypes/constructeddata.py +++ b/py25/bacpypes/constructeddata.py @@ -565,7 +565,7 @@ def __getitem__(self, item): def __setitem__(self, item, value): # no wrapping index - if (item < 1) or (item > self.value[0]): + if (item < 0) or (item > self.value[0]): raise IndexError("index out of range") # special length handling for index 0 @@ -575,7 +575,11 @@ def __setitem__(self, item, value): self.value = self.value[0:value + 1] elif value > self.value[0]: # extend - self.value.extend( [None] * (value - self.value[0]) ) + if issubclass(self.subtype, Atomic): + self.value.extend( [self.subtype().value] * (value - self.value[0]) ) + else: + for i in range(value - self.value[0]): + self.value.append(self.subtype()) else: return self.value[0] = value @@ -600,6 +604,11 @@ def index(self, value): # not found raise ValueError("%r not in array" % (value,)) + def remove(self, item): + # find the index of the item and delete it + indx = self.index(item) + self.__delitem__(indx) + def encode(self, taglist): if _debug: ArrayOf._debug("(%r)encode %r", self.__class__.__name__, taglist) diff --git a/py25/bacpypes/iocb.py b/py25/bacpypes/iocb.py index 880e2ce2..6906d3c4 100644 --- a/py25/bacpypes/iocb.py +++ b/py25/bacpypes/iocb.py @@ -723,6 +723,7 @@ def request_io(self, iocb): # if there was an error, abort the request if err: + if _debug: IOQController._debug(" - aborting") self.abort_io(iocb, err) def process_io(self, iocb): @@ -767,7 +768,7 @@ def complete_io(self, iocb, msg): # schedule a call in the future task = FunctionTask(IOQController._wait_trigger, self) - task.install_task(delay=self.wait_time) + task.install_task(delta=self.wait_time) else: # change our state diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index 7cc7b320..3bbac83c 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -167,6 +167,7 @@ def ReadProperty(self, obj, arrayIndex=None): # get the value value = obj._values[self.identifier] + if _debug: Property._debug(" - value: %r", value) # access an array if arrayIndex is not None: @@ -200,14 +201,61 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False if not self.mutable: raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + # if changing the length of the array, the value is unsigned + if arrayIndex == 0: + if not Unsigned.is_valid(value): + raise InvalidParameterDatatype("length of %s must be unsigned" % ( + self.identifier, + )) + # if it's atomic, make sure it's valid - if issubclass(self.datatype, Atomic): + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): raise InvalidParameterDatatype("%s must be of type %s" % ( self.identifier, self.datatype.__name__, )) + # if it's an array, make sure it's valid regarding arrayIndex provided + elif issubclass(self.datatype, Array): + if _debug: Property._debug(" - property is array, checking subtype and index") + + # changing a single element + if arrayIndex is not None: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(value): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.__name__, + )) + # constructed type + elif not isinstance(value, self.datatype.subtype): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # replacing the array + elif isinstance(value, list): + # check validity regarding subtype + for item in value: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(item): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__, + )) + # constructed type + elif not isinstance(item, self.datatype.subtype): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # value is mutated into a new array + value = self.datatype(value) + + # some kind of constructed data elif not isinstance(value, self.datatype): if _debug: Property._debug(" - property is not atomic and wrong type") raise InvalidParameterDatatype("%s must be of type %s" % ( @@ -389,13 +437,6 @@ def __init__(self, **kwargs): # empty list of property monitors self._property_monitors = defaultdict(list) - # start with a clean array of property identifiers - if 'propertyList' in initargs: - propertyList = None - else: - propertyList = ArrayOf(PropertyIdentifier)() - initargs['propertyList'] = propertyList - # initialize the object for propid, prop in self._properties.items(): if propid in initargs: @@ -404,20 +445,12 @@ def __init__(self, **kwargs): # defer to the property object for error checking prop.WriteProperty(self, initargs[propid], direct=True) - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - elif prop.default is not None: if _debug: Object._debug(" - setting %s from default", propid) # default values bypass property interface self._values[propid] = prop.default - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - else: if not prop.optional: if _debug: Object._debug(" - %s value required", propid) @@ -478,19 +511,12 @@ def add_property(self, prop): self._properties[prop.identifier] = prop self._values[prop.identifier] = prop.default - # tell the object it has a new property - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier not in property_list: - if _debug: Object._debug(" - adding to property list") - property_list.append(prop.identifier) - def delete_property(self, prop): """Delete a property from an object. The property is an instance of a Property or one of its derived classes, but only the property is relavent. Deleting a property disconnects it from the collection of properties common to all of the objects of its class.""" - if _debug: Object._debug("delete_property %r", value) + if _debug: Object._debug("delete_property %r", prop) # make a copy of the properties dictionary self._properties = _copy(self._properties) @@ -500,13 +526,6 @@ def delete_property(self, prop): if prop.identifier in self._values: del self._values[prop.identifier] - # remove the property identifier from its list of know properties - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier in property_list: - if _debug: Object._debug(" - removing from property list") - property_list.remove(prop.identifier) - def ReadProperty(self, propid, arrayIndex=None): if _debug: Object._debug("ReadProperty %r arrayIndex=%r", propid, arrayIndex) diff --git a/py25/bacpypes/pdu.py b/py25/bacpypes/pdu.py index 4ed3df52..825309ab 100755 --- a/py25/bacpypes/pdu.py +++ b/py25/bacpypes/pdu.py @@ -91,7 +91,7 @@ def decode_address(self, addr): self.addrAddr = struct.pack('B', addr) self.addrLen = 1 - elif isinstance(addr, str): + elif isinstance(addr, basestring): if _debug: Address._debug(" - str") m = ip_address_mask_port_re.match(addr) @@ -263,7 +263,7 @@ def decode_address(self, addr): addr, port = addr self.addrPort = int(port) - if isinstance(addr, str): + if isinstance(addr, basestring): if not addr: # when ('', n) is passed it is the local host address, but that # could be more than one on a multihomed machine, the empty string diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index 1335041c..64070aa9 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -1073,7 +1073,7 @@ def __init__(self, arg=None): # convert it to a string if you can self.value = self._xlate_table.get(arg, arg) - elif isinstance(arg, str): + elif isinstance(arg, basestring): if arg not in self._xlate_table: raise ValueError("undefined enumeration '%s'" % (arg,)) self.value = arg @@ -1088,7 +1088,7 @@ def __getitem__(self, item): def get_long(self): if isinstance(self.value, (int, long)): return self.value - elif isinstance(self.value, str): + elif isinstance(self.value, basestring): return long(self._xlate_table[self.value]) else: raise TypeError("%s is an invalid enumeration value datatype" % (type(self.value),)) @@ -1132,7 +1132,7 @@ def encode(self, tag): value = long(self.value) elif isinstance(self.value, long): value = self.value - elif isinstance(self.value, str): + elif isinstance(self.value, basestring): value = self._xlate_table[self.value] else: raise TypeError("%s is an invalid enumeration value datatype" % (type(self.value),)) @@ -1170,7 +1170,7 @@ def is_valid(cls, arg): value is wrong for the enumeration, the encoding will fail. """ return (isinstance(arg, (int, long)) and (arg >= 0)) or \ - isinstance(arg, str) + isinstance(arg, basestring) def __str__(self): return "%s(%s)" % (self.__class__.__name__, self.value) @@ -1607,7 +1607,7 @@ def set_tuple(self, objType, objInstance): objType = self.objectTypeClass._xlate_table.get(objType, objType) elif isinstance(objType, long): objType = self.objectTypeClass._xlate_table.get(objType, int(objType)) - elif isinstance(objType, str): + elif isinstance(objType, basestring): # make sure the type is known if objType not in self.objectTypeClass._xlate_table: raise ValueError("unrecognized object type '%s'" % (objType,)) @@ -1629,7 +1629,7 @@ def get_tuple(self): pass elif isinstance(objType, long): objType = int(objType) - elif isinstance(objType, str): + elif isinstance(objType, basestring): # turn it back into an integer objType = self.objectTypeClass()[objType] else: @@ -1680,7 +1680,7 @@ def __str__(self): # rip it apart objType, objInstance = self.value - if isinstance(objType, str): + if isinstance(objType, basestring): typestr = objType elif objType < 0: typestr = "Bad %d" % (objType,) diff --git a/py25/bacpypes/service/device.py b/py25/bacpypes/service/device.py index ac904052..87e618f3 100644 --- a/py25/bacpypes/service/device.py +++ b/py25/bacpypes/service/device.py @@ -14,6 +14,8 @@ Property, DeviceObject from ..task import FunctionTask +from .object import CurrentPropertyListMixIn + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -25,7 +27,7 @@ class CurrentDateProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -47,7 +49,7 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False class CurrentTimeProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -66,7 +68,7 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False # LocalDeviceObject # -class LocalDeviceObject(DeviceObject): +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): properties = \ [ CurrentTimeProperty('localTime') @@ -107,6 +109,18 @@ def __init__(self, **kwargs): 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") + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + else: + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([ + kwargs['objectIdentifier'], + ]) + # check for a minimum value if kwargs['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") @@ -115,20 +129,7 @@ def __init__(self, **kwargs): if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) # proceed as usual - DeviceObject.__init__(self, **kwargs) - - # create a default implementation of an object list for local devices. - # If it is specified in the kwargs, that overrides this default. - if ('objectList' not in kwargs): - self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) - - # if the object has a property list and one wasn't provided - # in the kwargs, then it was created by default and the objectList - # property should be included - if ('propertyList' not in kwargs) and self.propertyList: - # make sure it's not already there - if 'objectList' not in self.propertyList: - self.propertyList.append('objectList') + super(LocalDeviceObject, self).__init__(**kwargs) bacpypes_debugging(LocalDeviceObject) diff --git a/py25/bacpypes/service/object.py b/py25/bacpypes/service/object.py index 60f87e83..ae8e7dad 100755 --- a/py25/bacpypes/service/object.py +++ b/py25/bacpypes/service/object.py @@ -3,20 +3,74 @@ from ..debugging import bacpypes_debugging, ModuleLogger from ..capability import Capability -from ..basetypes import ErrorType +from ..basetypes import ErrorType, PropertyIdentifier from ..primitivedata import Atomic, Null, Unsigned -from ..constructeddata import Any, Array +from ..constructeddata import Any, Array, ArrayOf from ..apdu import Error, \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import PropertyError +from ..object import Property, Object, PropertyError # 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(), + ] + # # ReadProperty and WriteProperty Services # diff --git a/py25/bacpypes/udp.py b/py25/bacpypes/udp.py index b917ad85..2ea59ddc 100755 --- a/py25/bacpypes/udp.py +++ b/py25/bacpypes/udp.py @@ -253,6 +253,8 @@ def close_socket(self): if _debug: UDPDirector._debug("close_socket") self.socket.close() + self.close() + self.socket = None def handle_close(self): """Remove this from the monitor when it's closed.""" diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index bd35a64d..3aa33ec6 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -135,8 +135,12 @@ def get_device_info(self, key): 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) @@ -177,11 +181,18 @@ def release_device_info(self, info): has finished with the device information.""" if _debug: DeviceInfoCache._debug("release_device_info %r", 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 + 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") # # Application diff --git a/py27/bacpypes/basetypes.py b/py27/bacpypes/basetypes.py index 18b1371c..b5485f9d 100755 --- a/py27/bacpypes/basetypes.py +++ b/py27/bacpypes/basetypes.py @@ -513,6 +513,7 @@ class EngineeringUnits(Enumerated): , 'megavoltAmpereHoursReactive':244 #Mvarh , 'voltsPerDegreeKelvin':176 , 'voltsPerMeter':177 + , 'voltsSquareHours':245 , 'degreesPhase':14 , 'powerFactor':15 , 'webers':178 @@ -897,6 +898,7 @@ class EventType(Enumerated): , 'unsignedOutOfRange':16 , 'changeOfCharacterstring':17 , 'changeOfStatusFlags':18 + , 'changeOfReliability':19 } class FaultType(Enumerated): @@ -1470,7 +1472,18 @@ class Reliability(Enumerated): , 'multiStateFault':9 , 'configurationError':10 , 'communicationFailure':12 - , 'numberFault':13 + , 'memberFault': 13 + , 'monitoredObjectFault': 14 + , 'tripped': 15 + , 'lampFailure': 16 + , 'activationFailure': 17 + , 'renewDHCPFailure': 18 + , 'renewFDRegistration-failure': 19 + , 'restartAutoNegotiationFailure': 20 + , 'restartFailure': 21 + , 'proprietaryCommandFailure': 22 + , 'faultsListed': 23 + , 'referencedObjectFault': 24 } class RestartReason(Enumerated): @@ -2260,6 +2273,13 @@ class NotificationParametersChangeOfStatusFlagsType(Sequence): , Element('referencedFlags', StatusFlags, 1) ] +class NotificationParametersChangeOfReliabilityType(Sequence): + sequenceElements = \ + [ Element('reliability', Reliability, 0) + , Element('statusFlags', StatusFlags, 1) + , Element('propertyValues', SequenceOf(PropertyValue), 2) + ] + class NotificationParameters(Choice): choiceElements = \ [ Element('changeOfBitstring', NotificationParametersChangeOfBitstring, 0) @@ -2279,6 +2299,7 @@ class NotificationParameters(Choice): , Element('unsignedOutOfRange', NotificationParametersUnsignedOutOfRangeType, 16) , Element('changeOfCharacterString', NotificationParametersChangeOfCharacterStringType, 17) , Element('changeOfStatusFlags', NotificationParametersChangeOfStatusFlagsType, 18) + , Element('changeOfReliability', NotificationParametersChangeOfReliabilityType, 19) ] class ObjectPropertyValue(Sequence): diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index 56f41d51..5abb24e0 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -103,6 +103,7 @@ def __init__(self, addr=None, noBroadcast=False): bind(self.direct, self.broadcastPort) else: self.broadcast = None + self.broadcastPort = None # create and bind the Annex H and J servers self.annexH = _MultiplexServer(self) diff --git a/py27/bacpypes/constructeddata.py b/py27/bacpypes/constructeddata.py index 75818ad3..88fe28de 100755 --- a/py27/bacpypes/constructeddata.py +++ b/py27/bacpypes/constructeddata.py @@ -563,7 +563,7 @@ def __getitem__(self, item): def __setitem__(self, item, value): # no wrapping index - if (item < 1) or (item > self.value[0]): + if (item < 0) or (item > self.value[0]): raise IndexError("index out of range") # special length handling for index 0 @@ -573,7 +573,11 @@ def __setitem__(self, item, value): self.value = self.value[0:value + 1] elif value > self.value[0]: # extend - self.value.extend( [None] * (value - self.value[0]) ) + if issubclass(self.subtype, Atomic): + self.value.extend( [self.subtype().value] * (value - self.value[0]) ) + else: + for i in range(value - self.value[0]): + self.value.append(self.subtype()) else: return self.value[0] = value @@ -598,6 +602,11 @@ def index(self, value): # not found raise ValueError("%r not in array" % (value,)) + def remove(self, item): + # find the index of the item and delete it + indx = self.index(item) + self.__delitem__(indx) + def encode(self, taglist): if _debug: ArrayOf._debug("(%r)encode %r", self.__class__.__name__, taglist) diff --git a/py27/bacpypes/iocb.py b/py27/bacpypes/iocb.py index 02d08800..db96cba9 100644 --- a/py27/bacpypes/iocb.py +++ b/py27/bacpypes/iocb.py @@ -209,7 +209,7 @@ def set_timeout(self, delay, err=TimeoutError): self.ioTimeout = FunctionTask(self.abort, err) # (re)schedule it - self.ioTimeout.install_task(delay=delay) + self.ioTimeout.install_task(delta=delay) def __repr__(self): xid = id(self) @@ -718,6 +718,7 @@ def request_io(self, iocb): # if there was an error, abort the request if err: + if _debug: IOQController._debug(" - aborting") self.abort_io(iocb, err) def process_io(self, iocb): @@ -762,7 +763,7 @@ def complete_io(self, iocb, msg): # schedule a call in the future task = FunctionTask(IOQController._wait_trigger, self) - task.install_task(delay=self.wait_time) + task.install_task(delta=self.wait_time) else: # change our state diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 9de474f8..2cb56485 100755 --- a/py27/bacpypes/object.py +++ b/py27/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 @@ -168,6 +169,7 @@ def ReadProperty(self, obj, arrayIndex=None): # get the value value = obj._values[self.identifier] + if _debug: Property._debug(" - value: %r", value) # access an array if arrayIndex is not None: @@ -201,14 +203,61 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False if not self.mutable: raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + # if changing the length of the array, the value is unsigned + if arrayIndex == 0: + if not Unsigned.is_valid(value): + raise InvalidParameterDatatype("length of %s must be unsigned" % ( + self.identifier, + )) + # if it's atomic, make sure it's valid - if issubclass(self.datatype, Atomic): + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): raise InvalidParameterDatatype("%s must be of type %s" % ( self.identifier, self.datatype.__name__, )) + # if it's an array, make sure it's valid regarding arrayIndex provided + elif issubclass(self.datatype, Array): + if _debug: Property._debug(" - property is array, checking subtype and index") + + # changing a single element + if arrayIndex is not None: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(value): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.__name__, + )) + # constructed type + elif not isinstance(value, self.datatype.subtype): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # replacing the array + elif isinstance(value, list): + # check validity regarding subtype + for item in value: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(item): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__, + )) + # constructed type + elif not isinstance(item, self.datatype.subtype): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # value is mutated into a new array + value = self.datatype(value) + + # some kind of constructed data elif not isinstance(value, self.datatype): if _debug: Property._debug(" - property is not atomic and wrong type") raise InvalidParameterDatatype("%s must be of type %s" % ( @@ -396,13 +445,6 @@ def __init__(self, **kwargs): # empty list of property monitors self._property_monitors = defaultdict(list) - # start with a clean array of property identifiers - if 'propertyList' in initargs: - propertyList = None - else: - propertyList = ArrayOf(PropertyIdentifier)() - initargs['propertyList'] = propertyList - # initialize the object for propid, prop in self._properties.items(): if propid in initargs: @@ -411,20 +453,12 @@ def __init__(self, **kwargs): # defer to the property object for error checking prop.WriteProperty(self, initargs[propid], direct=True) - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - elif prop.default is not None: if _debug: Object._debug(" - setting %s from default", propid) # default values bypass property interface self._values[propid] = prop.default - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - else: if not prop.optional: if _debug: Object._debug(" - %s value required", propid) @@ -485,19 +519,12 @@ def add_property(self, prop): self._properties[prop.identifier] = prop self._values[prop.identifier] = prop.default - # tell the object it has a new property - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier not in property_list: - if _debug: Object._debug(" - adding to property list") - property_list.append(prop.identifier) - def delete_property(self, prop): """Delete a property from an object. The property is an instance of a Property or one of its derived classes, but only the property is relavent. Deleting a property disconnects it from the collection of properties common to all of the objects of its class.""" - if _debug: Object._debug("delete_property %r", value) + if _debug: Object._debug("delete_property %r", prop) # make a copy of the properties dictionary self._properties = _copy(self._properties) @@ -507,13 +534,6 @@ def delete_property(self, prop): if prop.identifier in self._values: del self._values[prop.identifier] - # remove the property identifier from its list of know properties - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier in property_list: - if _debug: Object._debug(" - removing from property list") - property_list.remove(prop.identifier) - def ReadProperty(self, propid, arrayIndex=None): if _debug: Object._debug("ReadProperty %r arrayIndex=%r", propid, arrayIndex) diff --git a/py27/bacpypes/pdu.py b/py27/bacpypes/pdu.py index 55053617..eb848bd6 100755 --- a/py27/bacpypes/pdu.py +++ b/py27/bacpypes/pdu.py @@ -93,7 +93,7 @@ def decode_address(self, addr): self.addrAddr = struct.pack('B', addr) self.addrLen = 1 - elif isinstance(addr, str): + elif isinstance(addr, basestring): if _debug: Address._debug(" - str") m = ip_address_mask_port_re.match(addr) @@ -259,7 +259,7 @@ def decode_address(self, addr): addr, port = addr self.addrPort = int(port) - if isinstance(addr, str): + if isinstance(addr, basestring): if not addr: # when ('', n) is passed it is the local host address, but that # could be more than one on a multihomed machine, the empty string diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index 1047f43e..91c10d7d 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -1078,7 +1078,7 @@ def __init__(self, arg=None): # convert it to a string if you can self.value = self._xlate_table.get(arg, arg) - elif isinstance(arg, str): + elif isinstance(arg, basestring): if arg not in self._xlate_table: raise ValueError("undefined enumeration '%s'" % (arg,)) self.value = arg @@ -1093,7 +1093,7 @@ def __getitem__(self, item): def get_long(self): if isinstance(self.value, (int, long)): return self.value - elif isinstance(self.value, str): + elif isinstance(self.value, basestring): return long(self._xlate_table[self.value]) else: raise TypeError("%s is an invalid enumeration value datatype" % (type(self.value),)) @@ -1137,7 +1137,7 @@ def encode(self, tag): value = long(self.value) elif isinstance(self.value, long): value = self.value - elif isinstance(self.value, str): + elif isinstance(self.value, basestring): value = self._xlate_table[self.value] else: raise TypeError("%s is an invalid enumeration value datatype" % (type(self.value),)) @@ -1613,7 +1613,7 @@ def set_tuple(self, objType, objInstance): objType = self.objectTypeClass._xlate_table.get(objType, objType) elif isinstance(objType, long): objType = self.objectTypeClass._xlate_table.get(objType, int(objType)) - elif isinstance(objType, str): + elif isinstance(objType, basestring): # make sure the type is known if objType not in self.objectTypeClass._xlate_table: raise ValueError("unrecognized object type '%s'" % (objType,)) @@ -1635,7 +1635,7 @@ def get_tuple(self): pass elif isinstance(objType, long): objType = int(objType) - elif isinstance(objType, str): + elif isinstance(objType, basestring): # turn it back into an integer objType = self.objectTypeClass()[objType] else: @@ -1686,7 +1686,7 @@ def __str__(self): # rip it apart objType, objInstance = self.value - if isinstance(objType, str): + if isinstance(objType, basestring): typestr = objType elif objType < 0: typestr = "Bad %d" % (objType,) diff --git a/py27/bacpypes/service/device.py b/py27/bacpypes/service/device.py index 0382f195..647714bd 100644 --- a/py27/bacpypes/service/device.py +++ b/py27/bacpypes/service/device.py @@ -14,6 +14,8 @@ Property, DeviceObject from ..task import FunctionTask +from .object import CurrentPropertyListMixIn + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -25,7 +27,7 @@ class CurrentDateProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -47,7 +49,7 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False class CurrentTimeProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -67,7 +69,7 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False # @bacpypes_debugging -class LocalDeviceObject(DeviceObject): +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): properties = \ [ CurrentTimeProperty('localTime') @@ -102,12 +104,24 @@ def __init__(self, **kwargs): raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - # check for local time + # 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") + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + else: + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([ + kwargs['objectIdentifier'], + ]) + # check for a minimum value if kwargs['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") @@ -116,20 +130,7 @@ def __init__(self, **kwargs): if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) # proceed as usual - DeviceObject.__init__(self, **kwargs) - - # create a default implementation of an object list for local devices. - # If it is specified in the kwargs, that overrides this default. - if ('objectList' not in kwargs): - self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) - - # if the object has a property list and one wasn't provided - # in the kwargs, then it was created by default and the objectList - # property should be included - if ('propertyList' not in kwargs) and self.propertyList: - # make sure it's not already there - if 'objectList' not in self.propertyList: - self.propertyList.append('objectList') + 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 6cbcf133..ca8d3fe9 100644 --- a/py27/bacpypes/service/object.py +++ b/py27/bacpypes/service/object.py @@ -3,20 +3,74 @@ from ..debugging import bacpypes_debugging, ModuleLogger from ..capability import Capability -from ..basetypes import ErrorType +from ..basetypes import ErrorType, PropertyIdentifier from ..primitivedata import Atomic, Null, Unsigned -from ..constructeddata import Any, Array +from ..constructeddata import Any, Array, ArrayOf from ..apdu import Error, \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import PropertyError +from ..object import Property, Object, PropertyError # 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(), + ] + # # ReadProperty and WriteProperty Services # diff --git a/py27/bacpypes/udp.py b/py27/bacpypes/udp.py index 643c45b7..589c5fdd 100755 --- a/py27/bacpypes/udp.py +++ b/py27/bacpypes/udp.py @@ -252,6 +252,8 @@ def close_socket(self): if _debug: UDPDirector._debug("close_socket") self.socket.close() + self.close() + self.socket = None def handle_close(self): """Remove this from the monitor when it's closed.""" diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index bd35a64d..3aa33ec6 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -135,8 +135,12 @@ def get_device_info(self, key): 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) @@ -177,11 +181,18 @@ def release_device_info(self, info): has finished with the device information.""" if _debug: DeviceInfoCache._debug("release_device_info %r", 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 + 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") # # Application diff --git a/py34/bacpypes/basetypes.py b/py34/bacpypes/basetypes.py index 5caa3039..b5485f9d 100755 --- a/py34/bacpypes/basetypes.py +++ b/py34/bacpypes/basetypes.py @@ -898,6 +898,7 @@ class EventType(Enumerated): , 'unsignedOutOfRange':16 , 'changeOfCharacterstring':17 , 'changeOfStatusFlags':18 + , 'changeOfReliability':19 } class FaultType(Enumerated): @@ -1471,7 +1472,18 @@ class Reliability(Enumerated): , 'multiStateFault':9 , 'configurationError':10 , 'communicationFailure':12 - , 'numberFault':13 + , 'memberFault': 13 + , 'monitoredObjectFault': 14 + , 'tripped': 15 + , 'lampFailure': 16 + , 'activationFailure': 17 + , 'renewDHCPFailure': 18 + , 'renewFDRegistration-failure': 19 + , 'restartAutoNegotiationFailure': 20 + , 'restartFailure': 21 + , 'proprietaryCommandFailure': 22 + , 'faultsListed': 23 + , 'referencedObjectFault': 24 } class RestartReason(Enumerated): @@ -2261,6 +2273,13 @@ class NotificationParametersChangeOfStatusFlagsType(Sequence): , Element('referencedFlags', StatusFlags, 1) ] +class NotificationParametersChangeOfReliabilityType(Sequence): + sequenceElements = \ + [ Element('reliability', Reliability, 0) + , Element('statusFlags', StatusFlags, 1) + , Element('propertyValues', SequenceOf(PropertyValue), 2) + ] + class NotificationParameters(Choice): choiceElements = \ [ Element('changeOfBitstring', NotificationParametersChangeOfBitstring, 0) @@ -2280,6 +2299,7 @@ class NotificationParameters(Choice): , Element('unsignedOutOfRange', NotificationParametersUnsignedOutOfRangeType, 16) , Element('changeOfCharacterString', NotificationParametersChangeOfCharacterStringType, 17) , Element('changeOfStatusFlags', NotificationParametersChangeOfStatusFlagsType, 18) + , Element('changeOfReliability', NotificationParametersChangeOfReliabilityType, 19) ] class ObjectPropertyValue(Sequence): diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index a4d99e8c..6abb5d7d 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -103,6 +103,7 @@ def __init__(self, addr=None, noBroadcast=False): bind(self.direct, self.broadcastPort) else: self.broadcast = None + self.broadcastPort = None # create and bind the Annex H and J servers self.annexH = _MultiplexServer(self) diff --git a/py34/bacpypes/constructeddata.py b/py34/bacpypes/constructeddata.py index 45dc2285..88fe28de 100755 --- a/py34/bacpypes/constructeddata.py +++ b/py34/bacpypes/constructeddata.py @@ -563,7 +563,7 @@ def __getitem__(self, item): def __setitem__(self, item, value): # no wrapping index - if (item < 1) or (item > self.value[0]): + if (item < 0) or (item > self.value[0]): raise IndexError("index out of range") # special length handling for index 0 @@ -573,7 +573,11 @@ def __setitem__(self, item, value): self.value = self.value[0:value + 1] elif value > self.value[0]: # extend - self.value.extend( [None] * (value - self.value[0]) ) + if issubclass(self.subtype, Atomic): + self.value.extend( [self.subtype().value] * (value - self.value[0]) ) + else: + for i in range(value - self.value[0]): + self.value.append(self.subtype()) else: return self.value[0] = value @@ -598,6 +602,11 @@ def index(self, value): # not found raise ValueError("%r not in array" % (value,)) + def remove(self, item): + # find the index of the item and delete it + indx = self.index(item) + self.__delitem__(indx) + def encode(self, taglist): if _debug: ArrayOf._debug("(%r)encode %r", self.__class__.__name__, taglist) @@ -919,7 +928,7 @@ def decode(self, taglist): # check for the correct closing tag tag = taglist.Pop() if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: - raise DecodingError("'%s' expected closing tag %d" % (element.name, element.context)) + raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) # done if _debug: Choice._debug(" - found choice (structure)") diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index cc28ad7a..9f31dd90 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -168,6 +168,7 @@ def ReadProperty(self, obj, arrayIndex=None): # get the value value = obj._values[self.identifier] + if _debug: Property._debug(" - value: %r", value) # access an array if arrayIndex is not None: @@ -201,14 +202,61 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False if not self.mutable: raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + # if changing the length of the array, the value is unsigned + if arrayIndex == 0: + if not Unsigned.is_valid(value): + raise InvalidParameterDatatype("length of %s must be unsigned" % ( + self.identifier, + )) + # if it's atomic, make sure it's valid - if issubclass(self.datatype, Atomic): + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): raise InvalidParameterDatatype("%s must be of type %s" % ( self.identifier, self.datatype.__name__, )) + # if it's an array, make sure it's valid regarding arrayIndex provided + elif issubclass(self.datatype, Array): + if _debug: Property._debug(" - property is array, checking subtype and index") + + # changing a single element + if arrayIndex is not None: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(value): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.__name__, + )) + # constructed type + elif not isinstance(value, self.datatype.subtype): + raise InvalidParameterDatatype("%s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # replacing the array + elif isinstance(value, list): + # check validity regarding subtype + for item in value: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(item): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__, + )) + # constructed type + elif not isinstance(item, self.datatype.subtype): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # value is mutated into a new array + value = self.datatype(value) + + # some kind of constructed data elif not isinstance(value, self.datatype): if _debug: Property._debug(" - property is not atomic and wrong type") raise InvalidParameterDatatype("%s must be of type %s" % ( @@ -396,13 +444,6 @@ def __init__(self, **kwargs): # empty list of property monitors self._property_monitors = defaultdict(list) - # start with a clean array of property identifiers - if 'propertyList' in initargs: - propertyList = None - else: - propertyList = ArrayOf(PropertyIdentifier)() - initargs['propertyList'] = propertyList - # initialize the object for propid, prop in self._properties.items(): if propid in initargs: @@ -411,20 +452,12 @@ def __init__(self, **kwargs): # defer to the property object for error checking prop.WriteProperty(self, initargs[propid], direct=True) - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - elif prop.default is not None: if _debug: Object._debug(" - setting %s from default", propid) # default values bypass property interface self._values[propid] = prop.default - # add it to the property list if we are building one - if propertyList is not None: - propertyList.append(propid) - else: if not prop.optional: if _debug: Object._debug(" - %s value required", propid) @@ -485,19 +518,12 @@ def add_property(self, prop): self._properties[prop.identifier] = prop self._values[prop.identifier] = prop.default - # tell the object it has a new property - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier not in property_list: - if _debug: Object._debug(" - adding to property list") - property_list.append(prop.identifier) - def delete_property(self, prop): """Delete a property from an object. The property is an instance of a Property or one of its derived classes, but only the property is relavent. Deleting a property disconnects it from the collection of properties common to all of the objects of its class.""" - if _debug: Object._debug("delete_property %r", value) + if _debug: Object._debug("delete_property %r", prop) # make a copy of the properties dictionary self._properties = _copy(self._properties) @@ -507,13 +533,6 @@ def delete_property(self, prop): if prop.identifier in self._values: del self._values[prop.identifier] - # remove the property identifier from its list of know properties - if 'propertyList' in self._values: - property_list = self.propertyList - if prop.identifier in property_list: - if _debug: Object._debug(" - removing from property list") - property_list.remove(prop.identifier) - def ReadProperty(self, propid, arrayIndex=None): if _debug: Object._debug("ReadProperty %r arrayIndex=%r", propid, arrayIndex) diff --git a/py34/bacpypes/service/device.py b/py34/bacpypes/service/device.py index 0382f195..f8871c7b 100644 --- a/py34/bacpypes/service/device.py +++ b/py34/bacpypes/service/device.py @@ -14,6 +14,8 @@ Property, DeviceObject from ..task import FunctionTask +from .object import CurrentPropertyListMixIn + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -25,7 +27,7 @@ class CurrentDateProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -47,7 +49,7 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False class CurrentTimeProperty(Property): def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) + Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) def ReadProperty(self, obj, arrayIndex=None): # access an array @@ -67,7 +69,7 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False # @bacpypes_debugging -class LocalDeviceObject(DeviceObject): +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): properties = \ [ CurrentTimeProperty('localTime') @@ -108,6 +110,18 @@ def __init__(self, **kwargs): 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") + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + else: + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([ + kwargs['objectIdentifier'], + ]) + # check for a minimum value if kwargs['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") @@ -116,20 +130,7 @@ def __init__(self, **kwargs): if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) # proceed as usual - DeviceObject.__init__(self, **kwargs) - - # create a default implementation of an object list for local devices. - # If it is specified in the kwargs, that overrides this default. - if ('objectList' not in kwargs): - self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) - - # if the object has a property list and one wasn't provided - # in the kwargs, then it was created by default and the objectList - # property should be included - if ('propertyList' not in kwargs) and self.propertyList: - # make sure it's not already there - if 'objectList' not in self.propertyList: - self.propertyList.append('objectList') + 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 6cbcf133..ca8d3fe9 100755 --- a/py34/bacpypes/service/object.py +++ b/py34/bacpypes/service/object.py @@ -3,20 +3,74 @@ from ..debugging import bacpypes_debugging, ModuleLogger from ..capability import Capability -from ..basetypes import ErrorType +from ..basetypes import ErrorType, PropertyIdentifier from ..primitivedata import Atomic, Null, Unsigned -from ..constructeddata import Any, Array +from ..constructeddata import Any, Array, ArrayOf from ..apdu import Error, \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import PropertyError +from ..object import Property, Object, PropertyError # 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(), + ] + # # ReadProperty and WriteProperty Services # diff --git a/py34/bacpypes/udp.py b/py34/bacpypes/udp.py index d2c10633..47ede293 100755 --- a/py34/bacpypes/udp.py +++ b/py34/bacpypes/udp.py @@ -252,6 +252,8 @@ def close_socket(self): if _debug: UDPDirector._debug("close_socket") self.socket.close() + self.close() + self.socket = None def handle_close(self): """Remove this from the monitor when it's closed.""" diff --git a/samples/MultipleReadPropertyHammer.py b/samples/MultipleReadPropertyHammer.py new file mode 100755 index 00000000..7bfa4e8b --- /dev/null +++ b/samples/MultipleReadPropertyHammer.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python + +""" +Mutliple Read Property Hammer + +This application blasts a list of ReadPropertyRequest messages with no +regard to the number of simultaneous requests to the same device. The +ReadPointListApplication is constructed like the BIPSimpleApplication but +without the ApplicationIOController interface and sieve. +""" + +import os +from time import time as _time +from copy import copy as _copy + +from random import shuffle + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run +from bacpypes.comm import bind +from bacpypes.task import RecurringTask + +from bacpypes.pdu import Address + +from bacpypes.app import Application +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.bvllservice import BIPSimple, AnnexJCodec, UDPMultiplexer + +from bacpypes.apdu import ReadPropertyRequest + +from bacpypes.service.device import LocalDeviceObject, WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices + + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +args = None +this_application = None + +# settings +INTERVAL = float(os.getenv('INTERVAL', 10.0)) + +# point list, set according to your device +point_list = [ + ('10.0.1.21:47809', 'analogValue', 1, 'presentValue'), + ('10.0.1.21:47809', 'analogValue', 2, 'presentValue'), + ('10.0.1.21:47809', 'analogValue', 3, 'presentValue'), + ('10.0.1.21:47809', 'analogValue', 4, 'presentValue'), + ('10.0.1.21:47809', 'analogValue', 5, 'presentValue'), + ] + +# +# ReadPointListApplication +# + +@bacpypes_debugging +class ReadPointListApplication(Application, WhoIsIAmServices, ReadWritePropertyServices, RecurringTask): + + def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): + if _debug: ReadPointListApplication._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) + global args + + Application.__init__(self, localDevice, deviceInfoCache, aseID=aseID) + RecurringTask.__init__(self, args.interval * 1000) + + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) + + # 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 generic BIP stack, bound to the Annex J server + # on the UDP multiplexer + self.bip = BIPSimple() + self.annexj = AnnexJCodec() + self.mux = UDPMultiplexer(self.localAddress) + + # bind the bottom layers + bind(self.bip, self.annexj, self.mux.annexJ) + + # bind the BIP stack to the network, no network number + self.nsap.bind(self.bip) + + # install the task + self.install_task() + + # timer + self.start_time = None + + # pending requests + self.pending_requests = {} + + def process_task(self): + if _debug: ReadPointListApplication._debug("process_task") + global point_list + + # we might not have finished from the last round + if self.pending_requests: + if _debug: ReadPointListApplication._debug(" - %d pending", len(self.pending_requests)) + return + + # start the clock + self.start_time = _time() + + # make a copy of the point list and shuffle it + point_list_copy = _copy(point_list) + shuffle(point_list_copy) + + # loop through the points + for addr, obj_type, obj_inst, prop_id in point_list_copy: + # build a request + request = ReadPropertyRequest( + objectIdentifier=(obj_type, obj_inst), + propertyIdentifier=prop_id, + ) + request.pduDestination = Address(addr) + if _debug: ReadPointListApplication._debug(" - request: %r", request) + + # send the request + self.request(request) + + # get the destination address from the pdu + request_key = request.pduDestination, request.apduInvokeID + if _debug: ReadPointListApplication._debug(" - request_key: %r", request_key) + + # make sure it's unused + if request_key in self.pending_requests: + raise RuntimeError("request key already used: %r" % (request_key,)) + + # add this to pending requests + self.pending_requests[request_key] = request + + def confirmation(self, apdu): + if _debug: ReadPointListApplication._debug("confirmation %r", apdu) + + # get the source address from the pdu + request_key = apdu.pduSource, apdu.apduInvokeID + if _debug: ReadPointListApplication._debug(" - request_key: %r", request_key) + + # make sure it's unused + if request_key not in self.pending_requests: + raise RuntimeError("request missing: %r" % (request_key,)) + + # this is no longer pending + del self.pending_requests[request_key] + + # we could be done with this interval + if not self.pending_requests: + elapsed_time = _time() - self.start_time + if _debug: ReadPointListApplication._debug(" - completed interval, %r seconds", elapsed_time) + + +# +# __main__ +# + +def main(): + global args, this_application + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + + # add an option to override the interval time + parser.add_argument('--interval', type=float, + help="amount of time between intervals", + default=INTERVAL, + ) + + # parse the command line arguments + 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 = ReadPointListApplication(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() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/RandomAnalogValueSleep.py b/samples/RandomAnalogValueSleep.py new file mode 100644 index 00000000..ea71cdbc --- /dev/null +++ b/samples/RandomAnalogValueSleep.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python + +""" +Random Value Property with Sleep + +This application is a server of analog value objects that return a random +number when the present value is read. This version has an additional +'sleep' time that slows down its performance. +""" + +import os +import random +import time + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run + +from bacpypes.primitivedata import Real +from bacpypes.object import AnalogValueObject, Property, register_object_type +from bacpypes.errors import ExecutionError + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# settings +SLEEP_TIME = float(os.getenv('SLEEP_TIME', 0.1)) +RANDOM_OBJECT_COUNT = int(os.getenv('RANDOM_OBJECT_COUNT', 10)) + +# globals +args = None + +# +# RandomValueProperty +# + +class RandomValueProperty(Property): + + def __init__(self, identifier): + if _debug: RandomValueProperty._debug("__init__ %r", identifier) + Property.__init__(self, identifier, Real, default=0.0, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: RandomValueProperty._debug("ReadProperty %r arrayIndex=%r", obj, arrayIndex) + global args + + # access an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # sleep a little + time.sleep(args.sleep) + + # 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') + +bacpypes_debugging(RandomValueProperty) + +# +# Random Value Object Type +# + +class RandomAnalogValueObject(AnalogValueObject): + + properties = [ + RandomValueProperty('presentValue'), + ] + + def __init__(self, **kwargs): + if _debug: RandomAnalogValueObject._debug("__init__ %r", kwargs) + AnalogValueObject.__init__(self, **kwargs) + +bacpypes_debugging(RandomAnalogValueObject) +register_object_type(RandomAnalogValueObject) + +# +# __main__ +# + +def main(): + global args + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + + # add an option to override the sleep time + parser.add_argument('--sleep', type=float, + help="sleep before returning the value", + default=SLEEP_TIME, + ) + + # parse the command line arguments + 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=('device', int(args.ini.objectidentifier)), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # 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( + objectIdentifier=('analogValue', i), + objectName='Random-%d' % (i,), + ) + _log.debug(" - ravo: %r", ravo) + this_application.add_object(ravo) + + # make sure they are all there + _log.debug(" - object list: %r", this_device.objectList) + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/samples/ReadAllProperties.py b/samples/ReadAllProperties.py new file mode 100755 index 00000000..b52d8721 --- /dev/null +++ b/samples/ReadAllProperties.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +""" +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. +""" + +import sys +from collections import deque + +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.app import BIPSimpleApplication +from bacpypes.object import get_object_class, get_datatype +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None +device_address = None +object_identifier = None +property_list = None + +# +# ReadPropertyApplication +# + +@bacpypes_debugging +class ReadPropertyApplication(BIPSimpleApplication): + + def __init__(self, *args): + if _debug: ReadPropertyApplication._debug("__init__ %r", args) + BIPSimpleApplication.__init__(self, *args) + + # current property being read + self.property_identifier = None + + def next_request(self): + if _debug: ReadPropertyApplication._debug("next_request") + global device_address, object_identifier, property_list + + # check to see if we're done + if not property_list: + if _debug: ReadPropertyApplication._debug(" - done") + stop() + return + + # get the next request + self.property_identifier = property_list.popleft() + if _debug: ReadPropertyApplication._debug(" - property_identifier: %r", self.property_identifier) + + # build a request + request = ReadPropertyRequest( + destination=device_address, + objectIdentifier=object_identifier, + propertyIdentifier=self.property_identifier, + ) + if _debug: ReadPropertyApplication._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + + # set a callback for the response + iocb.add_callback(self.complete_request) + if _debug: ReadPropertyApplication._debug(" - iocb: %r", iocb) + + # send the request + this_application.request_io(iocb) + + def complete_request(self, iocb): + if _debug: ReadPropertyApplication._debug("complete_request %r", iocb) + + if iocb.ioResponse: + apdu = iocb.ioResponse + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], self.property_identifier) + if _debug: ReadPropertyApplication._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # special case for array parts, others are managed by cast_out + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadPropertyApplication._debug(" - value: %r", value) + + sys.stdout.write(self.property_identifier + " = " + str(value) + '\n') + if hasattr(value, 'debug_contents'): + value.debug_contents(file=sys.stdout) + sys.stdout.flush() + + if iocb.ioError: + if _debug: ReadPropertyApplication._debug(" - error: %r", iocb.ioError) + + # if it is an unknown property, just skip to the next one + if getattr(iocb.ioError, 'errorCode', '') != 'unknownProperty': + sys.stdout.write(self.property_identifier + "! " + str(iocb.ioError) + '\n') + sys.stdout.flush() + + # fire off another request + deferred(self.next_request) + + +# +# __main__ +# + +def main(): + global this_application, device_address, object_identifier, property_list + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + parser.add_argument( + "address", + help="device address", + ) + parser.add_argument( + "objtype", + help="object types, e.g., analogInput", + ) + parser.add_argument( + "objinstance", type=int, + help="object instance", + ) + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # interpret the address + device_address = Address(args.address) + if _debug: _log.debug(" - device_address: %r", device_address) + + # build an identifier + object_identifier = (args.objtype, args.objinstance) + if _debug: _log.debug(" - object_identifier: %r", object_identifier) + + # get the object class + object_class = get_object_class(args.objtype) + if _debug: _log.debug(" - object_class: %r", object_class) + + # make a queue of the properties + property_list = deque(prop.identifier for prop in object_class.properties) + 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), + ) + + # make a simple application + this_application = ReadPropertyApplication(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 + + # fire off a request when the core has a chance + deferred(this_application.next_request) + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/samples/ReadPropertyMultipleServer.py b/samples/ReadPropertyMultipleServer.py index 8cc0e80e..961bd633 100755 --- a/samples/ReadPropertyMultipleServer.py +++ b/samples/ReadPropertyMultipleServer.py @@ -12,7 +12,8 @@ from bacpypes.core import run -from bacpypes.primitivedata import Real +from bacpypes.primitivedata import Real, CharacterString +from bacpypes.constructeddata import ArrayOf from bacpypes.object import AnalogValueObject, Property, register_object_type from bacpypes.errors import ExecutionError @@ -73,6 +74,7 @@ class RandomAnalogValueObject(AnalogValueObject): properties = [ RandomValueProperty('presentValue'), + Property('eventMessageTexts', ArrayOf(CharacterString), mutable=True), ] def __init__(self, **kwargs): @@ -107,7 +109,8 @@ def main(): # make a random input object ravo1 = RandomAnalogValueObject( - objectIdentifier=('analogValue', 1), objectName='Random1' + objectIdentifier=('analogValue', 1), objectName='Random1', + eventMessageTexts=ArrayOf(CharacterString)(["hello"]), ) _log.debug(" - ravo1: %r", ravo1) diff --git a/samples/ReadWriteEventMessageTexts.py b/samples/ReadWriteEventMessageTexts.py new file mode 100644 index 00000000..124b0db0 --- /dev/null +++ b/samples/ReadWriteEventMessageTexts.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python + +""" +This application is similar to ReadWriteProperty but it is just for beating on +the event message texts, an array of character strings. +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.object import get_datatype + +from bacpypes.apdu import SimpleAckPDU, \ + ReadPropertyRequest, ReadPropertyACK, WritePropertyRequest +from bacpypes.primitivedata import Unsigned, CharacterString +from bacpypes.constructeddata import Array, ArrayOf, Any + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None +context = None + +# +# ReadWritePropertyConsoleCmd +# + +@bacpypes_debugging +class ReadWritePropertyConsoleCmd(ConsoleCmd): + + def do_read(self, args): + """read [ ]""" + args = args.split() + if _debug: ReadWritePropertyConsoleCmd._debug("do_read %r", args) + global context + + try: + addr, obj_type, obj_inst = context + prop_id = 'eventMessageTexts' + + datatype = get_datatype(obj_type, prop_id) + if not datatype: + raise ValueError("invalid property for object type") + + # build a request + request = ReadPropertyRequest( + objectIdentifier=(obj_type, obj_inst), + propertyIdentifier=prop_id, + ) + request.pduDestination = Address(addr) + + if len(args) == 1: + request.propertyArrayIndex = int(args[0]) + if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: ReadWritePropertyConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: ReadWritePropertyConsoleCmd._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: ReadWritePropertyConsoleCmd._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 (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadWritePropertyConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(str(value) + '\n') + if hasattr(value, 'debug_contents'): + value.debug_contents(file=sys.stdout) + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') + + except Exception as error: + ReadWritePropertyConsoleCmd._exception("exception: %r", error) + + def do_write(self, args): + """ + write + write 0 + write [ ]... + """ + args = args.split() + ReadWritePropertyConsoleCmd._debug("do_write %r", args) + + try: + addr, obj_type, obj_inst = context + prop_id = 'eventMessageTexts' + + indx = None + if args and args[0].isdigit(): + indx = int(args[0]) + if indx == 0: + value = Unsigned(int(args[1])) + else: + value = CharacterString(args[1]) + else: + value = ArrayOf(CharacterString)(args[0:]) + + # build a request + request = WritePropertyRequest( + objectIdentifier=(obj_type, obj_inst), + propertyIdentifier=prop_id + ) + request.pduDestination = Address(addr) + + # save the value + request.propertyValue = Any() + try: + request.propertyValue.cast_in(value) + except Exception as error: + ReadWritePropertyConsoleCmd._exception("WriteProperty cast error: %r", error) + + # optional array index + if indx is not None: + request.propertyArrayIndex = indx + + if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: ReadWritePropertyConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + # should be an ack + if not isinstance(iocb.ioResponse, SimpleAckPDU): + if _debug: ReadWritePropertyConsoleCmd._debug(" - not an ack") + return + + sys.stdout.write("ack\n") + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') + + except Exception as error: + ReadWritePropertyConsoleCmd._exception("exception: %r", error) + + def do_rtn(self, args): + """rtn ... """ + args = args.split() + if _debug: ReadWritePropertyConsoleCmd._debug("do_rtn %r", args) + + # safe to assume only one adapter + adapter = this_application.nsap.adapters[0] + if _debug: ReadWritePropertyConsoleCmd._debug(" - adapter: %r", adapter) + + # provide the address and a list of network numbers + router_address = Address(args[0]) + network_list = [int(arg) for arg in args[1:]] + + # pass along to the service access point + this_application.nsap.add_router_references(adapter, router_address, network_list) + + +# +# __main__ +# + +def main(): + global this_application, context + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + parser.add_argument( + "address", + help="address of server", + ) + parser.add_argument( + "objtype", + help="object type", + ) + parser.add_argument( + "objinst", type=int, + help="object instance", + ) + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # set the context, the collection of the above parameters + 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), + ) + + # 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 = ReadWritePropertyConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/sandbox/add_remove_property.py b/sandbox/add_remove_property.py new file mode 100644 index 00000000..e5a90be7 --- /dev/null +++ b/sandbox/add_remove_property.py @@ -0,0 +1,59 @@ + +from bacpypes.basetypes import PropertyIdentifier +from bacpypes.constructeddata import ArrayOf +from bacpypes.object import AnalogValueObject + +# create an array of property identifiers datatype +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +aopi = ArrayOfPropertyIdentifier() +aopi.append('objectName') +aopi.append('objectType') +aopi.append('description') +aopi.debug_contents() + +aopi.remove('objectType') +aopi.debug_contents() + +print("Create an Analog Value Object") +av = AnalogValueObject( + objectName='av-sample', + objectIdentifier=('analogValue', 1), + description="sample", + ) +av.debug_contents() +print("") + +print("Change the description") +av.description = "something else" +av.debug_contents() +print("") + + +# get the description property by the attribute name +description_property = av._attr_to_property('description') +print("description_property = %r" % (description_property,)) +print("") + +print("Delete the property") +av.delete_property(description_property) +print("...property deleted") + +try: + av.description = "this raises an exception" +except Exception as err: + print(repr(err)) +av.debug_contents() +print("") + +print("===== Add the property") +av.add_property(description_property) +print("...property added") + +try: + av.description = "this works" +except Exception as err: + print(repr(err)) +av.debug_contents() +print("") + diff --git a/tests/__init__.py b/tests/__init__.py index 12432173..5de5b601 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -17,6 +17,7 @@ from . import test_comm from . import test_pdu from . import test_primitive_data +from . import test_constructed_data from . import test_utilities from . import test_vlan diff --git a/tests/test_constructed_data/__init__.py b/tests/test_constructed_data/__init__.py new file mode 100644 index 00000000..38786476 --- /dev/null +++ b/tests/test_constructed_data/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/python + +""" +Test Constructed Data Module +""" + +from . import test_sequence +from . import test_sequence_of +from . import test_array_of +from . import test_choice +from . import test_any +from . import test_any_atomic + + diff --git a/tests/test_constructed_data/helpers.py b/tests/test_constructed_data/helpers.py new file mode 100644 index 00000000..06395b89 --- /dev/null +++ b/tests/test_constructed_data/helpers.py @@ -0,0 +1,88 @@ +#!/usr/bin/python + +""" +Helper classes for constructed data tests. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.errors import MissingRequiredParameter +from bacpypes.primitivedata import Boolean, Integer, Tag, TagList +from bacpypes.constructeddata import Element, Sequence + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class SequenceEquality: + + """ + This mixin class adds an equality function for matching values for all of + the elements, even if they are optional. It will raise an exception for + missing elements, even if they are missing in both objects. + """ + + def __eq__(self, other): + if _debug: SequenceEquality._debug("__eq__ %r", other) + + # loop through this sequences elements + for element in self.sequenceElements: + self_value = getattr(self, element.name, None) + other_value = getattr(other, element.name, None) + + if (not element.optional) and ((self_value is None) or (other_value is None)): + raise MissingRequiredParameter("%s is a missing required element of %s" % (element.name, self.__class__.__name__)) + if not (self_value == other_value): + return False + + # success + return True + + +@bacpypes_debugging +class EmptySequence(Sequence, SequenceEquality): + + def __init__(self, *args, **kwargs): + if _debug: EmptySequence._debug("__init__ %r %r", args, kwargs) + Sequence.__init__(self, *args, **kwargs) + + +@bacpypes_debugging +class SimpleSequence(Sequence, SequenceEquality): + + sequenceElements = [ + Element('hydrogen', Boolean), + ] + + def __init__(self, *args, **kwargs): + if _debug: SimpleSequence._debug("__init__ %r %r", args, kwargs) + Sequence.__init__(self, *args, **kwargs) + + +@bacpypes_debugging +class CompoundSequence1(Sequence, SequenceEquality): + + sequenceElements = [ + Element('hydrogen', Boolean), + Element('helium', Integer), + ] + + def __init__(self, *args, **kwargs): + if _debug: CompoundSequence1._debug("__init__ %r %r", args, kwargs) + Sequence.__init__(self, *args, **kwargs) + + +@bacpypes_debugging +class CompoundSequence2(Sequence, SequenceEquality): + + sequenceElements = [ + Element('lithium', Boolean, optional=True), + Element('beryllium', Integer), + ] + + def __init__(self, *args, **kwargs): + if _debug: CompoundSequence2._debug("__init__ %r %r", args, kwargs) + Sequence.__init__(self, *args, **kwargs) + diff --git a/tests/test_constructed_data/test_any.py b/tests/test_constructed_data/test_any.py new file mode 100644 index 00000000..f323afee --- /dev/null +++ b/tests/test_constructed_data/test_any.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# placeholder diff --git a/tests/test_constructed_data/test_any_atomic.py b/tests/test_constructed_data/test_any_atomic.py new file mode 100644 index 00000000..f323afee --- /dev/null +++ b/tests/test_constructed_data/test_any_atomic.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# placeholder diff --git a/tests/test_constructed_data/test_array_of.py b/tests/test_constructed_data/test_array_of.py new file mode 100644 index 00000000..8e848332 --- /dev/null +++ b/tests/test_constructed_data/test_array_of.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Array +---------- +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.errors import MissingRequiredParameter +from bacpypes.primitivedata import Integer, Tag, TagList +from bacpypes.constructeddata import Element, Sequence, ArrayOf + +from .helpers import SimpleSequence + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# array of integers +IntegerArray = ArrayOf(Integer) + +@bacpypes_debugging +class TestIntegerArray(unittest.TestCase): + + def test_empty_array(self): + if _debug: TestIntegerArray._debug("test_empty_array") + + # create an empty array + ary = IntegerArray() + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # array sematics + assert len(ary) == 0 + assert ary[0] == 0 + + # encode it in a tag list + tag_list = TagList() + ary.encode(tag_list) + if _debug: TestIntegerArray._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + ary = IntegerArray() + ary.decode(tag_list) + if _debug: TestIntegerArray._debug(" - seq: %r", seq) + + def test_append(self): + if _debug: TestIntegerArray._debug("test_append") + + # create an empty array + ary = IntegerArray() + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # append an integer + ary.append(2) + assert len(ary) == 1 + assert ary[0] == 1 + assert ary[1] == 2 + + def test_delete_item(self): + if _debug: TestIntegerArray._debug("test_delete_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # delete something + del ary[2] + assert len(ary) == 2 + assert ary[0] == 2 + assert ary.value[1:] == [1, 3] + + def test_index_item(self): + if _debug: TestIntegerArray._debug("test_index_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # find something + assert ary.index(3) == 3 + + # not find something + with self.assertRaises(ValueError): + indx = ary.index(4) + + def test_remove_item(self): + if _debug: TestIntegerArray._debug("test_remove_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # remove something + ary.remove(2) + assert ary.value[1:] == [1, 3] + + # not remove something + with self.assertRaises(ValueError): + ary.remove(4) + + def test_resize(self): + if _debug: TestIntegerArray._debug("test_resize") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # make it shorter + ary[0] = 2 + assert ary.value[1:] == [1, 2] + + # make it longer + ary[0] = 4 + assert ary.value[1:] == [1, 2, 0, 0] + + def test_get_item(self): + if _debug: TestIntegerArray._debug("test_get_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # BACnet semantics + assert ary[1] == 1 + + def test_set_item(self): + if _debug: TestIntegerArray._debug("test_set_item") + + # create an array + ary = IntegerArray([1, 2, 3]) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # BACnet semantics, no type checking + ary[1] = 10 + assert ary[1] == 10 + + def test_codec(self): + if _debug: TestIntegerArray._debug("test_codec") + + # test array contents + ary_value = [1, 2, 3] + + # create an array + ary = IntegerArray(ary_value) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) + + # encode it in a tag list + tag_list = TagList() + ary.encode(tag_list) + if _debug: TestIntegerArray._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + ary = IntegerArray() + ary.decode(tag_list) + if _debug: TestIntegerArray._debug(" - ary %r", ary) + + # value matches + assert ary.value[1:] == ary_value + + +# array of a sequence +SimpleSequenceArray = ArrayOf(SimpleSequence) + +@bacpypes_debugging +class TestSimpleSequenceArray(unittest.TestCase): + + def test_codec(self): + if _debug: TestSimpleSequenceArray._debug("test_codec") + + # test array contents + ary_value = [ + SimpleSequence(hydrogen=True), + SimpleSequence(hydrogen=False), + SimpleSequence(hydrogen=True), + ] + + # create an array + ary = SimpleSequenceArray(ary_value) + if _debug: TestSimpleSequenceArray._debug(" - ary: %r", ary) + + # encode it in a tag list + tag_list = TagList() + ary.encode(tag_list) + if _debug: TestSimpleSequenceArray._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + ary = SimpleSequenceArray() + ary.decode(tag_list) + if _debug: TestSimpleSequenceArray._debug(" - ary %r", ary) + + # value matches + assert ary.value[1:] == ary_value + diff --git a/tests/test_constructed_data/test_choice.py b/tests/test_constructed_data/test_choice.py new file mode 100644 index 00000000..f323afee --- /dev/null +++ b/tests/test_constructed_data/test_choice.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# placeholder diff --git a/tests/test_constructed_data/test_sequence.py b/tests/test_constructed_data/test_sequence.py new file mode 100644 index 00000000..512ed3e5 --- /dev/null +++ b/tests/test_constructed_data/test_sequence.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Constructed Data Sequence +------------------------------ +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.errors import MissingRequiredParameter +from bacpypes.primitivedata import Boolean, Integer, Tag, TagList +from bacpypes.constructeddata import Element, Sequence + +from .helpers import EmptySequence, SimpleSequence, CompoundSequence1, \ + CompoundSequence2 + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class TestEmptySequence(unittest.TestCase): + + def test_empty_sequence(self): + if _debug: TestEmptySequence._debug("test_empty_sequence") + + # create a sequence + seq = EmptySequence() + if _debug: TestEmptySequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestEmptySequence._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = EmptySequence() + seq.decode(tag_list) + if _debug: TestEmptySequence._debug(" - seq: %r", seq) + + def test_no_elements(self): + if _debug: TestEmptySequence._debug("test_no_elements") + + # create a sequence with an undefined element + with self.assertRaises(TypeError): + seq = EmptySequence(some_element=None) + + +@bacpypes_debugging +class TestSimpleSequence(unittest.TestCase): + + def test_missing_element(self): + if _debug: TestSimpleSequence._debug("test_missing_element") + + # create a sequence with a missing required element + seq = SimpleSequence() + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + def test_wrong_type(self): + if _debug: TestSimpleSequence._debug("test_wrong_type") + + # create a sequence with wrong element value type + seq = SimpleSequence(hydrogen=12) + with self.assertRaises(TypeError): + tag_list = TagList() + seq.encode(tag_list) + + def test_codec(self): + if _debug: TestSimpleSequence._debug("test_codec") + + # create a sequence + seq = SimpleSequence(hydrogen=False) + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestSimpleSequence._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = SimpleSequence() + seq.decode(tag_list) + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + +@bacpypes_debugging +class TestCompoundSequence1(unittest.TestCase): + + def test_missing_element(self): + if _debug: TestCompoundSequence1._debug("test_missing_element") + + # create a sequence with a missing required element + seq = CompoundSequence1() + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + # create a sequence with a missing required element + seq = CompoundSequence1(hydrogen=True) + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + # create a sequence with a missing required element + seq = CompoundSequence1(helium=2) + if _debug: TestSimpleSequence._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + def test_codec(self): + if _debug: TestCompoundSequence1._debug("test_codec") + + # create a sequence + seq = CompoundSequence1(hydrogen=True, helium=2) + if _debug: TestCompoundSequence1._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestCompoundSequence1._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = CompoundSequence1() + seq.decode(tag_list) + if _debug: TestCompoundSequence1._debug(" - seq: %r", seq) + + +@bacpypes_debugging +class TestCompoundSequence2(unittest.TestCase): + + def test_missing_element(self): + if _debug: TestCompoundSequence2._debug("test_missing_element") + + # create a sequence with a missing required element + seq = CompoundSequence2() + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + # create a sequence with a missing required element + seq = CompoundSequence2(lithium=True) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + with self.assertRaises(MissingRequiredParameter): + seq.encode(tag_list) + + def test_codec_1(self): + if _debug: TestCompoundSequence2._debug("test_codec_1") + + # create a sequence + seq = CompoundSequence2(beryllium=2) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestCompoundSequence2._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = CompoundSequence2() + seq.decode(tag_list) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + def test_codec_2(self): + if _debug: TestCompoundSequence2._debug("test_codec_2") + + # create a sequence + seq = CompoundSequence2(lithium=True, beryllium=3) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + # encode it in a tag list + tag_list = TagList() + seq.encode(tag_list) + if _debug: TestCompoundSequence2._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + seq = CompoundSequence2() + seq.decode(tag_list) + if _debug: TestCompoundSequence2._debug(" - seq: %r", seq) + + diff --git a/tests/test_constructed_data/test_sequence_of.py b/tests/test_constructed_data/test_sequence_of.py new file mode 100644 index 00000000..f323afee --- /dev/null +++ b/tests/test_constructed_data/test_sequence_of.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# placeholder diff --git a/tests/test_pdu/test_address.py b/tests/test_pdu/test_address.py index af382970..682a24e5 100644 --- a/tests/test_pdu/test_address.py +++ b/tests/test_pdu/test_address.py @@ -74,8 +74,8 @@ def test_address_int(self): with self.assertRaises(ValueError): Address(256) - def test_address_ipv4(self): - if _debug: TestAddress._debug("test_address_ipv4") + def test_address_ipv4_str(self): + if _debug: TestAddress._debug("test_address_ipv4_str") # test IPv4 local station address test_addr = Address("1.2.3.4") @@ -92,16 +92,42 @@ def test_address_ipv4(self): self.match_address(test_addr, 2, None, 6, '01020304bb7f') assert str(test_addr) == "0x01020304bb7f" - def test_address_eth(self): - if _debug: TestAddress._debug("test_address_eth") + def test_address_ipv4_unicode(self): + if _debug: TestAddress._debug("test_address_ipv4_unicode") + + # test IPv4 local station address + test_addr = Address(u"1.2.3.4") + self.match_address(test_addr, 2, None, 6, '01020304BAC0') + assert str(test_addr) == u"1.2.3.4" + + # test IPv4 local station address with non-standard port + test_addr = Address(u"1.2.3.4:47809") + self.match_address(test_addr, 2, None, 6, '01020304BAC1') + assert str(test_addr) == u"1.2.3.4:47809" + + # test IPv4 local station address with unrecognized port + test_addr = Address(u"1.2.3.4:47999") + self.match_address(test_addr, 2, None, 6, '01020304bb7f') + assert str(test_addr) == u"0x01020304bb7f" + + def test_address_eth_str(self): + if _debug: TestAddress._debug("test_address_eth_str") # test Ethernet local station address test_addr = Address("01:02:03:04:05:06") self.match_address(test_addr, 2, None, 6, '010203040506') assert str(test_addr) == "0x010203040506" - def test_address_local_station(self): - if _debug: TestAddress._debug("test_address_local_station") + def test_address_eth_unicode(self): + if _debug: TestAddress._debug("test_address_eth_unicode") + + # test Ethernet local station address + test_addr = Address(u"01:02:03:04:05:06") + self.match_address(test_addr, 2, None, 6, '010203040506') + assert str(test_addr) == u"0x010203040506" + + def test_address_local_station_str(self): + if _debug: TestAddress._debug("test_address_local_station_str") # test integer local station test_addr = Address("1") @@ -134,16 +160,58 @@ def test_address_local_station(self): self.match_address(test_addr, 2, None, 2, '0102') assert str(test_addr) == "0x0102" - def test_address_local_broadcast(self): - if _debug: TestAddress._debug("test_address_local_broadcast") + def test_address_local_station_unicode(self): + if _debug: TestAddress._debug("test_address_local_station_unicode") + + # test integer local station + test_addr = Address(u"1") + self.match_address(test_addr, 2, None, 1, '01') + assert str(test_addr) == u"1" + + test_addr = Address(u"254") + self.match_address(test_addr, 2, None, 1, 'fe') + assert str(test_addr) == u"254" + + # test bad integer string + with self.assertRaises(ValueError): + Address("256") + + # test modern hex string + test_addr = Address(u"0x01") + self.match_address(test_addr, 2, None, 1, '01') + assert str(test_addr) == u"1" + + test_addr = Address(u"0x0102") + self.match_address(test_addr, 2, None, 2, '0102') + assert str(test_addr) == u"0x0102" + + # test old school hex string + test_addr = Address(u"X'01'") + self.match_address(test_addr, 2, None, 1, '01') + assert str(test_addr) == u"1" + + test_addr = Address(u"X'0102'") + self.match_address(test_addr, 2, None, 2, '0102') + assert str(test_addr) == u"0x0102" + + def test_address_local_broadcast_str(self): + if _debug: TestAddress._debug("test_address_local_broadcast_str") # test local broadcast test_addr = Address("*") self.match_address(test_addr, 1, None, None, None) assert str(test_addr) == "*" - def test_address_remote_broadcast(self): - if _debug: TestAddress._debug("test_address_remote_broadcast") + def test_address_local_broadcast_unicode(self): + if _debug: TestAddress._debug("test_address_local_broadcast_unicode") + + # test local broadcast + test_addr = Address(u"*") + self.match_address(test_addr, 1, None, None, None) + assert str(test_addr) == u"*" + + def test_address_remote_broadcast_str(self): + if _debug: TestAddress._debug("test_address_remote_broadcast_str") # test remote broadcast test_addr = Address("1:*") @@ -154,8 +222,20 @@ def test_address_remote_broadcast(self): with self.assertRaises(ValueError): Address("65536:*") - def test_address_remote_station(self): - if _debug: TestAddress._debug("test_address_remote_station") + def test_address_remote_broadcast_unicode(self): + if _debug: TestAddress._debug("test_address_remote_broadcast_unicode") + + # test remote broadcast + test_addr = Address(u"1:*") + self.match_address(test_addr, 3, 1, None, None) + assert str(test_addr) == u"1:*" + + # test remote broadcast bad network + with self.assertRaises(ValueError): + Address("65536:*") + + def test_address_remote_station_str(self): + if _debug: TestAddress._debug("test_address_remote_station_str") # test integer remote station test_addr = Address("1:2") @@ -198,14 +278,66 @@ def test_address_remote_station(self): with self.assertRaises(ValueError): Address("65536:X'02'") - def test_address_global_broadcast(self): - if _debug: TestAddress._debug("test_address_global_broadcast") + def test_address_remote_station_unicode(self): + if _debug: TestAddress._debug("test_address_remote_station_unicode") + + # test integer remote station + test_addr = Address(u"1:2") + self.match_address(test_addr, 4, 1, 1, '02') + assert str(test_addr) == u"1:2" + + test_addr = Address(u"1:254") + self.match_address(test_addr, 4, 1, 1, 'fe') + assert str(test_addr) == u"1:254" + + # test bad network and node + with self.assertRaises(ValueError): + Address(u"65536:2") + with self.assertRaises(ValueError): + Address(u"1:256") + + # test modern hex string + test_addr = Address(u"1:0x02") + self.match_address(test_addr, 4, 1, 1, '02') + assert str(test_addr) == u"1:2" + + # test bad network + with self.assertRaises(ValueError): + Address(u"65536:0x02") + + test_addr = Address(u"1:0x0203") + self.match_address(test_addr, 4, 1, 2, '0203') + assert str(test_addr) == u"1:0x0203" + + # test old school hex string + test_addr = Address(u"1:X'02'") + self.match_address(test_addr, 4, 1, 1, '02') + assert str(test_addr) == u"1:2" + + test_addr = Address(u"1:X'0203'") + self.match_address(test_addr, 4, 1, 2, '0203') + assert str(test_addr) == u"1:0x0203" + + # test bad network + with self.assertRaises(ValueError): + Address(u"65536:X'02'") + + def test_address_global_broadcast_str(self): + if _debug: TestAddress._debug("test_address_global_broadcast_str") # test local broadcast test_addr = Address("*:*") self.match_address(test_addr, 5, None, None, None) assert str(test_addr) == "*:*" + def test_address_global_broadcast_unicode(self): + if _debug: TestAddress._debug("test_address_global_broadcast_unicode") + + # test local broadcast + test_addr = Address(u"*:*") + self.match_address(test_addr, 5, None, None, None) + assert str(test_addr) == u"*:*" + @bacpypes_debugging class TestLocalStation(unittest.TestCase, MatchAddressMixin): @@ -368,8 +500,8 @@ def test_global_broadcast(self): @bacpypes_debugging class TestAddressEquality(unittest.TestCase, MatchAddressMixin): - def test_address_equality(self): - if _debug: TestAddressEquality._debug("test_address_equality") + def test_address_equality_str(self): + if _debug: TestAddressEquality._debug("test_address_equality_str") assert Address(1) == LocalStation(1) assert Address("2") == LocalStation(2) @@ -377,3 +509,14 @@ def test_address_equality(self): assert Address("3:4") == RemoteStation(3, 4) assert Address("5:*") == RemoteBroadcast(5) assert Address("*:*") == GlobalBroadcast() + + def test_address_equality_unicode(self): + if _debug: TestAddressEquality._debug("test_address_equality_unicode") + + assert Address(1) == LocalStation(1) + assert Address(u"2") == LocalStation(2) + assert Address(u"*") == LocalBroadcast() + assert Address(u"3:4") == RemoteStation(3, 4) + assert Address(u"5:*") == RemoteBroadcast(5) + assert Address(u"*:*") == GlobalBroadcast() + diff --git a/tests/test_primitive_data/test_character_string.py b/tests/test_primitive_data/test_character_string.py index 15f66ec8..10430561 100644 --- a/tests/test_primitive_data/test_character_string.py +++ b/tests/test_primitive_data/test_character_string.py @@ -93,6 +93,13 @@ def test_character_string_str(self): assert obj.value == "hello" assert str(obj) == "CharacterString(0,X'68656c6c6f')" + def test_character_string_unicode(self): + if _debug: TestCharacterString._debug("test_character_string_unicode") + + obj = CharacterString(u"hello") + assert obj.value == u"hello" + assert str(obj) == "CharacterString(0,X'68656c6c6f')" + def test_character_string_tag(self): if _debug: TestCharacterString._debug("test_character_string_tag") @@ -126,4 +133,4 @@ def test_character_string_endec(self): obj = CharacterString(character_string_tag('')) character_string_endec("", '00') - character_string_endec("abc", '00616263') \ No newline at end of file + character_string_endec("abc", '00616263') diff --git a/tests/test_primitive_data/test_enumerated.py b/tests/test_primitive_data/test_enumerated.py index 67d823c6..31fe4c1c 100644 --- a/tests/test_primitive_data/test_enumerated.py +++ b/tests/test_primitive_data/test_enumerated.py @@ -18,6 +18,14 @@ _log = ModuleLogger(globals()) +class QuickBrownFox(Enumerated): + enumerations = { + 'quick': 0, + 'brown': 1, + 'fox': 2, + } + + @bacpypes_debugging def enumerated_tag(x): """Convert a hex string to an enumerated application tag.""" @@ -93,6 +101,38 @@ def test_enumerated_int(self): with self.assertRaises(ValueError): Enumerated(-1) + def test_enumerated_str(self): + if _debug: TestEnumerated._debug("test_enumerated_str") + + obj = QuickBrownFox('quick') + assert obj.value == 'quick' + assert str(obj) == "QuickBrownFox(quick)" + + with self.assertRaises(ValueError): + QuickBrownFox(-1) + with self.assertRaises(ValueError): + QuickBrownFox('lazyDog') + + tag = Tag(Tag.applicationTagClass, Tag.enumeratedAppTag, 1, xtob('01')) + obj = QuickBrownFox(tag) + assert obj.value == 'brown' + + def test_enumerated_unicode(self): + if _debug: TestEnumerated._debug("test_enumerated_unicode") + + obj = QuickBrownFox(u'quick') + assert obj.value == u'quick' + assert str(obj) == "QuickBrownFox(quick)" + + with self.assertRaises(ValueError): + QuickBrownFox(-1) + with self.assertRaises(ValueError): + QuickBrownFox(u'lazyDog') + + tag = Tag(Tag.applicationTagClass, Tag.enumeratedAppTag, 1, xtob('01')) + obj = QuickBrownFox(tag) + assert obj.value == u'brown' + def test_enumerated_tag(self): if _debug: TestEnumerated._debug("test_enumerated_tag") @@ -138,4 +178,4 @@ def test_enumerated_endec(self): enumerated_endec(8388608, '800000') enumerated_endec(2147483647, '7fffffff') - enumerated_endec(2147483648, '80000000') \ No newline at end of file + enumerated_endec(2147483648, '80000000') diff --git a/tests/test_primitive_data/test_object_identifier.py b/tests/test_primitive_data/test_object_identifier.py index d14f161c..c97a1444 100644 --- a/tests/test_primitive_data/test_object_identifier.py +++ b/tests/test_primitive_data/test_object_identifier.py @@ -97,6 +97,12 @@ def test_object_identifier_int(self): def test_object_identifier_tuple(self): if _debug: TestObjectIdentifier._debug("test_object_identifier_tuple") + obj = ObjectIdentifier(('analogInput', 0)) + assert obj.value == ('analogInput', 0) + + obj = ObjectIdentifier((u'analogInput', 0)) + assert obj.value == (u'analogInput', 0) + with self.assertRaises(ValueError): ObjectIdentifier((0, -1)) with self.assertRaises(ValueError): @@ -136,5 +142,7 @@ def test_object_identifier_endec(self): # test standard types object_identifier_endec(('analogInput', 0), '00000000') + object_identifier_endec((u'analogInput', 0), '00000000') + + # test vendor types - # test vendor types \ No newline at end of file diff --git a/tests/test_primitive_data/test_object_type.py b/tests/test_primitive_data/test_object_type.py index 62b8eb0d..dc6e4707 100644 --- a/tests/test_primitive_data/test_object_type.py +++ b/tests/test_primitive_data/test_object_type.py @@ -23,7 +23,7 @@ class MyObjectType(ObjectType): 'myAnalogInput': 128, 'myAnalogOutput': 129, 'myAnalogValue': 130, - } + } expand_enumerations(MyObjectType) @@ -113,6 +113,13 @@ def test_object_type_str(self): obj = ObjectType('analogInput') assert obj.value == 'analogInput' + def test_object_type_unicode(self): + if _debug: TestObjectType._debug("test_object_type_unicode") + + # known strings are accepted + obj = ObjectType(u'analogInput') + assert obj.value == u'analogInput' + def test_extended_object_type_int(self): if _debug: TestObjectType._debug("test_extended_object_type_int") @@ -137,6 +144,17 @@ def test_extended_object_type_str(self): with self.assertRaises(ValueError): MyObjectType('snork') + def test_extended_object_type_unicode(self): + if _debug: TestObjectType._debug("test_extended_object_type_unicode") + + # known strings are accepted + obj = MyObjectType(u'myAnalogInput') + assert obj.value == u'myAnalogInput' + + # unknown strings are rejected + with self.assertRaises(ValueError): + MyObjectType(u'snork') + def test_object_type_tag(self): if _debug: TestObjectType._debug("test_object_type_tag") @@ -174,4 +192,4 @@ def test_object_type_endec(self): object_type_endec('analogOutput', '01') object_type_endec(127, '7f') - object_type_endec(128, '80') \ No newline at end of file + object_type_endec(128, '80') diff --git a/tests/test_service/test_object.py b/tests/test_service/test_object.py index fdffa2a0..52040421 100644 --- a/tests/test_service/test_object.py +++ b/tests/test_service/test_object.py @@ -1 +1,256 @@ -# placeholder +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Object Services +-------------------- +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.errors import ExecutionError, InvalidParameterDatatype +from bacpypes.primitivedata import CharacterString +from bacpypes.constructeddata import ArrayOf +from bacpypes.object import register_object_type, ReadableProperty, \ + WritableProperty, Object + +from bacpypes.service.object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class TestBasic(unittest.TestCase): + + def test_basic(self): + """Test basic configuration of a network.""" + if _debug: TestBasic._debug("test_basic") + + # create an object, no properties + obj = Object() + + +@bacpypes_debugging +@register_object_type(vendor_id=999) +class SampleReadableLocation(Object): + + objectType = 'sampleReadableLocation' + properties = [ + ReadableProperty('location', CharacterString), + ] + + def __init__(self, **kwargs): + if _debug: SampleReadableLocation._debug("__init__ %r", kwargs) + Object.__init__(self, **kwargs) + + +@bacpypes_debugging +class TestReadableLocation(unittest.TestCase): + + def test_sample(self): + """Test basic configuration of a network.""" + if _debug: TestReadableLocation._debug("test_sample") + + # create an object, default property value is None + obj = SampleReadableLocation() + assert obj.location == None + + # create an object with a location + obj = SampleReadableLocation(location="home") + assert obj.ReadProperty('location') == "home" + + # not an array, write access denied + with self.assertRaises(ExecutionError): + obj.ReadProperty('location', 0) + with self.assertRaises(ExecutionError): + obj.WriteProperty('location', "work") + + +@bacpypes_debugging +@register_object_type(vendor_id=999) +class SampleWritableLocation(Object): + + objectType = 'sampleWritableLocation' + properties = [ + WritableProperty('location', CharacterString), + ] + + def __init__(self, **kwargs): + if _debug: SampleWritableLocation._debug("__init__ %r", kwargs) + Object.__init__(self, **kwargs) + + +@bacpypes_debugging +class TestWritableLocation(unittest.TestCase): + + def test_sample(self): + """Test basic configuration of a network.""" + if _debug: TestWritableLocation._debug("test_sample") + + # create an object with a location + obj = SampleWritableLocation(location="home") + assert obj.ReadProperty('location') == "home" + + # not an array, write access denied + with self.assertRaises(ExecutionError): + obj.ReadProperty('location', 0) + + # write access successful + obj.WriteProperty('location', "work") + assert obj.location == "work" + + # wrong data type + with self.assertRaises(InvalidParameterDatatype): + obj.WriteProperty('location', 12) + + +# array of character strings +ArrayOfCharacterString = ArrayOf(CharacterString) + +@bacpypes_debugging +@register_object_type(vendor_id=999) +class SampleWritableArray(Object): + + objectType = 'sampleWritableLocation' + properties = [ + WritableProperty('location', ArrayOfCharacterString), + ] + + def __init__(self, **kwargs): + if _debug: SampleWritableArray._debug("__init__ %r", kwargs) + Object.__init__(self, **kwargs) + + +@bacpypes_debugging +class TestWritableArray(unittest.TestCase): + + def test_empty_array(self): + """Test basic configuration of a network.""" + if _debug: TestWritableArray._debug("test_empty_array") + + # create an object with a location + obj = SampleWritableArray(location=ArrayOfCharacterString()) + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + assert len(obj.location) == 0 + assert obj.location[0] == 0 + + def test_short_array(self): + if _debug: TestWritableArray._debug("test_short_array") + + # create an object with a location + obj = SampleWritableArray(location=ArrayOfCharacterString(["home"])) + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + assert obj.ReadProperty('location', 0) == 1 + assert obj.ReadProperty('location', 1) == "home" + + def test_changing_length(self): + if _debug: TestWritableArray._debug("test_changing_length") + + # create an object with a location + obj = SampleWritableArray(location=ArrayOfCharacterString(["home"])) + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + # change the length of the array + obj.WriteProperty('location', 2, arrayIndex=0) + assert obj.ReadProperty('location', 0) == 2 + + # array extended with none, should get property default value + assert obj.ReadProperty('location', 2) == "" + + # wrong datatype + with self.assertRaises(InvalidParameterDatatype): + obj.WriteProperty('location', "nope", arrayIndex=0) + + def test_changing_item(self): + if _debug: TestWritableArray._debug("test_changing_item") + + # create an object with a location + obj = SampleWritableArray(location=ArrayOfCharacterString(["home"])) + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + # change the element + obj.WriteProperty('location', "work", arrayIndex=1) + assert obj.ReadProperty('location', 1) == "work" + + # wrong datatype + with self.assertRaises(InvalidParameterDatatype): + obj.WriteProperty('location', 12, arrayIndex=1) + + def test_replacing_array(self): + if _debug: TestWritableArray._debug("test_replacing_array") + + # create an object with a location + obj = SampleWritableArray() + if _debug: TestWritableArray._debug(" - obj.location: %r", obj.location) + + # replace the array + obj.WriteProperty('location', ArrayOfCharacterString(["home", "work"])) + assert obj.ReadProperty('location', 0) == 2 + assert obj.ReadProperty('location', 1) == "home" + assert obj.ReadProperty('location', 2) == "work" + + +@bacpypes_debugging +@register_object_type(vendor_id=999) +class SampleLocationObject(CurrentPropertyListMixIn, Object): + + objectType = 'sampleLocationObject' + properties = [ + WritableProperty('location', CharacterString), + ] + + def __init__(self, **kwargs): + if _debug: SampleWritableArray._debug("__init__ %r", kwargs) + Object.__init__(self, **kwargs) + + +@bacpypes_debugging +class TestCurrentPropertyListMixIn(unittest.TestCase): + + def test_with_location(self): + if _debug: TestCurrentPropertyListMixIn._debug("test_with_location") + + # create an object without a location + obj = SampleLocationObject(location="home") + if _debug: TestCurrentPropertyListMixIn._debug(" - obj.location: %r", obj.location) + + assert obj.propertyList.value == [1, "location"] + + def test_without_location(self): + if _debug: TestCurrentPropertyListMixIn._debug("test_property_list_1") + + # create an object without a location + obj = SampleLocationObject() + if _debug: TestCurrentPropertyListMixIn._debug(" - obj.location: %r", obj.location) + + assert obj.propertyList.value == [0] + + def test_location_appears(self): + if _debug: TestCurrentPropertyListMixIn._debug("test_location_appears") + + # create an object without a location + obj = SampleLocationObject() + if _debug: TestCurrentPropertyListMixIn._debug(" - obj.location: %r", obj.location) + + # give it a location + obj.location = "away" + assert obj.propertyList.value == [1, "location"] + + def test_location_disappears(self): + if _debug: TestCurrentPropertyListMixIn._debug("test_location_disappears") + + # create an object without a location + obj = SampleLocationObject(location="home") + if _debug: TestCurrentPropertyListMixIn._debug(" - obj.location: %r", obj.location) + + # location 'removed' + obj.location = None + + assert obj.propertyList.value == [0] + From de1d895c8715cd059fbce04b1b18a00a6cfedb03 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Wed, 6 Dec 2017 22:49:46 -0500 Subject: [PATCH 24/72] bring in code from older branch #96 --- py27/bacpypes/constructeddata.py | 15 +- py27/bacpypes/object.py | 7 + samples/LocalScheduleObject.py | 831 +++++++++++++++++++++++++++++++ samples/ReadWriteProperty.py | 26 +- tests/time_machine.py | 107 +++- 5 files changed, 977 insertions(+), 9 deletions(-) create mode 100644 samples/LocalScheduleObject.py diff --git a/py27/bacpypes/constructeddata.py b/py27/bacpypes/constructeddata.py index 88fe28de..029b4d2a 100755 --- a/py27/bacpypes/constructeddata.py +++ b/py27/bacpypes/constructeddata.py @@ -406,6 +406,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: @@ -593,6 +596,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): @@ -1153,7 +1159,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) @@ -1184,8 +1190,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/object.py b/py27/bacpypes/object.py index 2cb56485..1c09e16e 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -211,6 +211,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + if 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): diff --git a/samples/LocalScheduleObject.py b/samples/LocalScheduleObject.py new file mode 100644 index 00000000..953157d0 --- /dev/null +++ b/samples/LocalScheduleObject.py @@ -0,0 +1,831 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime, 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, deferred +from bacpypes.task import OneShotTask + +from bacpypes.primitivedata import Atomic, Null, Integer, Unsigned, Real, Date, Time, CharacterString +from bacpypes.constructeddata import Array, ArrayOf, SequenceOf, AnyAtomic +from bacpypes.basetypes import CalendarEntry, DailySchedule, DateRange, \ + DeviceObjectPropertyReference, SpecialEvent, SpecialEventPeriod, TimeValue +from bacpypes.object import register_object_type, get_datatype, WritableProperty, ScheduleObject + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +schedule_objects = [] + +# +# 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 + if sys.version_info[0] == 2: + weeknday_unpacked = [ord(c) for c in weeknday] + elif sys.version_info[0] == 3: + weeknday_unpacked = [c for c in weeknday] + else: + raise NotImplementedError("match_weeknday requires Python 2.x or 3.x") + 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 +@register_object_type(vendor_id=999) +class LocalScheduleObject(ScheduleObject): + + properties = [ + WritableProperty('presentValue', AnyAtomic), + ] + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors + 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") + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + # 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: + obj_type = obj_prop_ref.objectIdentifier[0] + + # get the datatype of the property to be written + 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 %r %r", old_value, new_value) + + 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': + return + + # get the date and time from the device object + 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) + + # 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 + +# +# TestConsoleCmd +# + +@bacpypes_debugging +class TestConsoleCmd(ConsoleCmd): + + def do_test(self, args): + """test