diff --git a/devices/HW_device_template.py b/devices/HW_device_template.py index 950faf39daf790962ef1f4cc3b93fb00382533fb..0249747de12fa08893d0c1c4c3534827b61f59f1 100644 --- a/devices/HW_device_template.py +++ b/devices/HW_device_template.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # -# This file is part of the PCC project -# -# +# This file wraps around a tango device class and provides a number of abstractions useful for hardware devices. It works together # # Distributed under the terms of the APACHE license. # See LICENSE.txt for more info. -""" Hardware Device Server for LOFAR2.0 +""" """ @@ -15,6 +13,7 @@ from tango.server import run # Additional import +from src.attribute_wrapper import * from src.hardware_device import * __all__ = ["HW_dev"] @@ -22,19 +21,19 @@ __all__ = ["HW_dev"] class HW_dev(hardware_device): """ - This class is the minimal (read empty) implementation of a class using 'hardware_device' - """ + This class is the minimal (read empty) implementation of a class using 'hardware_device' + """ # ---------- # Attributes # ---------- """ - attribute wrapper objects can be declared here. All attribute wrapper objects will get automatically put in a ist (attr_list) for easy access + attribute wrapper objects can be declared here. All attribute wrapper objects will get automatically put in a list (attr_list) for easy access - example = attribute_wrapper(comms_annotation="this is an example", datatype=numpy.double, dims=(8, 2), access=AttrWriteType.READ_WRITE) - ... + example = attribute_wrapper(comms_annotation="this is an example", datatype=numpy.double, dims=(8, 2), access=AttrWriteType.READ_WRITE) + ... - """ + """ def always_executed_hook(self): """Method always executed before any TANGO command is executed.""" @@ -43,10 +42,10 @@ class HW_dev(hardware_device): def delete_device(self): """Hook to delete resources allocated in init_device. - This method allows for any memory or other resources allocated in the - init_device method to be released. This method is called by the device - destructor and by the device Init command (a Tango built-in). - """ + This method allows for any memory or other resources allocated in the + init_device method to be released. This method is called by the device + destructor and by the device Init command (a Tango built-in). + """ self.debug_stream("Shutting down...") self.Off() diff --git a/devices/PCC.py b/devices/PCC.py index 9e605cac2f33983c12d70f2a3d41fb6b2b9cd00f..d6b255335f50e75b49ff8d2898ed59f085d86246 100644 --- a/devices/PCC.py +++ b/devices/PCC.py @@ -112,7 +112,6 @@ class PCC(hardware_device): # -------- def off(self): """ user code here. is called when the state is set to OFF """ - # Stop keep-alive self.OPCua_client.stop() diff --git a/devices/SNMP.py b/devices/SNMP.py new file mode 100644 index 0000000000000000000000000000000000000000..5af5d75a49eb96396c9ab35cc0b1ddaff5f271e1 --- /dev/null +++ b/devices/SNMP.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the PCC project +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +""" SNMP Device for LOFAR2.0 + +""" + +# PyTango imports +from tango.server import run +from tango.server import device_property +# Additional import + +from clients.SNMP_client import SNMP_client +from src.attribute_wrapper import * +from src.hardware_device import * + +__all__ = ["SNMP", "main"] + + +class SNMP(hardware_device): + """ + + **Properties:** + + - Device Property + SNMP_community + - Type:'DevString' + SNMP_host + - Type:'DevULong' + SNMP_timeout + - Type:'DevDouble' + """ + + # ----------------- + # Device Properties + # ----------------- + + SNMP_community = device_property( + dtype='DevString', + mandatory=True + ) + + SNMP_host = device_property( + dtype='DevString', + mandatory=True + ) + + SNMP_timeout = device_property( + dtype='DevDouble', + mandatory=True + ) + + # ---------- + # Attributes + # ---------- + + sys_description_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.1.0"}, datatype=numpy.str_) + sys_objectID_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.2.0", "type": "OID"}, datatype=numpy.int64) + sys_uptime_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.3.0", "type": "TimeTicks"}, datatype=numpy.str_) + sys_name_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.5.0"}, datatype=numpy.str_) + ip_route_mask_127_0_0_1_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.4.21.1.11.127.0.0.1", "type": "IpAddress"}, datatype=numpy.str_) + TCP_active_open_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.6.5.0", "type": "Counter32"}, datatype=numpy.str_) + + sys_contact_RW = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.4.0"}, datatype=numpy.str_, access= AttrWriteType.READ_WRITE) + sys_contact_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.4.0"}, datatype=numpy.str_) + + TCP_Curr_estab_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.6.9.0", "type": "Gauge"}, datatype=numpy.int64) + + # inferred spectrum + if_index_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.2.2.1.1"}, dims=(10,), datatype=numpy.int64) + + def always_executed_hook(self): + """Method always executed before any TANGO command is executed.""" + pass + + def delete_device(self): + """Hook to delete resources allocated in init_device. + + This method allows for any memory or other resources allocated in the + init_device method to be released. This method is called by the device + destructor and by the device Init command (a Tango built-in). + """ + self.debug_stream("Shutting down...") + + self.Off() + self.debug_stream("Shut down. Good bye.") + + # -------- + # overloaded functions + # -------- + def initialise(self): + """ user code here. is called when the state is set to STANDBY """ + + # set up the SNMP ua client + self.snmp_manager = SNMP_client(self.SNMP_community, self.SNMP_host, self.SNMP_timeout, self.Fault, self) + + # map the attributes to the OPC ua comm client + for i in self.attr_list(): + i.set_comm_client(self.snmp_manager) + + self.snmp_manager.start() + + +# -------- +# Commands +# -------- + + +# ---------- +# Run server +# ---------- +def main(args=None, **kwargs): + """Main function of the PCC module.""" + return run((SNMP,), args=args, **kwargs) + + +if __name__ == '__main__': + main() diff --git a/devices/clients/SNMP_client.py b/devices/clients/SNMP_client.py new file mode 100644 index 0000000000000000000000000000000000000000..fe6d20dc6874267268d5c4f7e0cceae0fa109c02 --- /dev/null +++ b/devices/clients/SNMP_client.py @@ -0,0 +1,156 @@ +from src.comms_client import * +import snmp +import numpy +import traceback + +__all__ = ["SNMP_client"] + + +snmp_to_numpy_dict = { + snmp.types.INTEGER: numpy.int64, + snmp.types.TimeTicks: numpy.int64, + snmp.types.OCTET_STRING: str, + snmp.types.OID: str +} + +snmp_types = { + "Integer": numpy.int64, + "Gauge": numpy.int64, + "TimeTick": numpy.int64, + "Counter32": numpy.int64, + "OctetString": str, + "IpAddress": str, + "OID": str, +} + + +# numpy_to_snmp_dict = { +# numpy.int64, +# numpy.int64, +# str, +# } + + +class SNMP_client(CommClient): + """ + messages to keep a check on the connection. On connection failure, reconnects once. + """ + + def start(self): + super().start() + + def __init__(self, community, host, timeout, fault_func, streams, try_interval=2): + """ + Create the SNMP and connect() to it + """ + super().__init__(fault_func, streams, try_interval) + + self.community = community + self.host = host + self.manager = snmp.Manager(community=bytes(community, "utf8")) + + # Explicitly connect + if not self.connect(): + # hardware or infra is down -- needs fixing first + fault_func() + return + + def connect(self): + """ + Try to connect to the client + """ + self.streams.debug_stream("Connecting to community: %s, host: %s", self.community, self.host) + + self.connected = True + return True + + def ping(self): + """ + ping the client to make sure the connection with the client is still functional. + """ + pass + + def _setup_annotation(self, annotation): + """ + This class's Implementation of the get_mapping function. returns the read and write functions + """ + + if isinstance(annotation, dict): + # check if required path inarg is present + if annotation.get('oids') is None: + AssertionError("SNMP get attributes require an oid") + oids = annotation.get("oids") # required + else: + TypeError("SNMP attributes require a dict with oid(s)") + return + + return oids + + def setup_value_conversion(self, attribute): + """ + gives the client access to the attribute_wrapper object in order to access all data it could potentially need. + """ + + dim_x = attribute.dim_x + dim_y = attribute.dim_y + + return dim_x, dim_y + + def get_oids(self, x, y, in_oid): + + if x == 0: + x = 1 + if y == 0: + y = 1 + + nof_oids = x * y + + if nof_oids == 1: + # is scalar + if type(in_oid) is str: + # for ease of handling put single oid in a 1 element list + in_oid = [in_oid] + return in_oid + + elif type(in_oid) is list and len(in_oid) == nof_oids: + # already is an array and of the right length + return in_oid + elif type(in_oid) is list and len(in_oid) != nof_oids: + # already is an array but the wrong length. Unable to handle this + raise ValueError("SNMP oids need to either be a single value or an array the size of the attribute dimensions. got: {} expected: {}x{}={}".format(len(in_oid),x,y,x*y)) + else: + out_oids = [] + + for i in range(nof_oids): + out_oids.append(in_oid + ".{}".format(i+1)) + + return out_oids + + def setup_attribute(self, annotation, attribute): + """ + MANDATORY function: is used by the attribute wrapper to get read/write functions. must return the read and write functions + """ + + # process the annotation + oids = self._setup_annotation(annotation) + + # get all the necessary data to set up the read/write functions from the attribute_wrapper + dim_x, dim_y = self.setup_value_conversion(attribute) + oids = self.get_oids(dim_x, dim_y, oids) + + def _read_function(): + vars = self.manager.get(self.host, *oids) + + value = [] + for i in vars: + val = snmp_to_numpy_dict[type(i.value)](str(i.value)) + value.append(val) + return value + + def _write_function(value): + for i in range(len(oids)): + self.manager.set(self.host, oids[i], value[i]) + + + # return the read/write functions + return _read_function, _write_function diff --git a/devices/src/attribute_wrapper.py b/devices/src/attribute_wrapper.py index 35670b0705e75668e628283198daba26107e78a2..03bd9d5e5148b8539e339d57328d2f53c6262d70 100644 --- a/devices/src/attribute_wrapper.py +++ b/devices/src/attribute_wrapper.py @@ -11,17 +11,22 @@ logger = logging.getLogger() class attribute_wrapper(attribute): """ - Wraps all the attributes in a wrapper class to manage most of the redundant code behind the scenes - """ + Wraps all the attributes in a wrapper class to manage most of the redundant code behind the scenes + """ def __init__(self, comms_annotation=None, datatype=None, dims=(1,), access=AttrWriteType.READ, init_value=None, **kwargs): """ - wraps around the tango Attribute class. Provides an easier interface for 1d or 2d arrays. Also provides a way to abstract - managing the communications interface. - """ + wraps around the tango Attribute class. Provides an easier interface for 1d or 2d arrays. Also provides a way to abstract + managing the communications interface. + + comms_annotation: data passed along to the attribute. can be given any form of data. handling is up to client implementation + datatype: any numpy datatype + dims: dimensions of the + init_value: value + """ # ensure the type is a numpy array - if "numpy" not in str(datatype) and type(datatype) != str: + if "numpy" not in str(datatype) and datatype != str: raise TypeError("Attribute needs to be a Tango-supported numpy or str type, but has type \"%s\"" % (datatype,)) self.comms_annotation = comms_annotation # store data that can be used by the comms interface. not used by the wrapper itself