diff --git a/.gitattriutes b/.gitattriutes new file mode 100644 index 0000000..07764a7 --- /dev/null +++ b/.gitattriutes @@ -0,0 +1 @@ +* text eol=lf \ No newline at end of file diff --git a/.vscode/.ropeproject/config.py b/.vscode/.ropeproject/config.py deleted file mode 100644 index d1e8f3d..0000000 --- a/.vscode/.ropeproject/config.py +++ /dev/null @@ -1,100 +0,0 @@ -# The default ``config.py`` -# flake8: noqa - - -def set_prefs(prefs): - """This function is called before opening the project""" - - # Specify which files and folders to ignore in the project. - # Changes to ignored resources are not added to the history and - # VCSs. Also they are not returned in `Project.get_files()`. - # Note that ``?`` and ``*`` match all characters but slashes. - # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' - # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' - # '.svn': matches 'pkg/.svn' and all of its children - # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' - # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' - prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', - '.hg', '.svn', '_svn', '.git', '.tox'] - - # Specifies which files should be considered python files. It is - # useful when you have scripts inside your project. Only files - # ending with ``.py`` are considered to be python files by - # default. - #prefs['python_files'] = ['*.py'] - - # Custom source folders: By default rope searches the project - # for finding source folders (folders that should be searched - # for finding modules). You can add paths to that list. Note - # that rope guesses project source folders correctly most of the - # time; use this if you have any problems. - # The folders should be relative to project root and use '/' for - # separating folders regardless of the platform rope is running on. - # 'src/my_source_folder' for instance. - #prefs.add('source_folders', 'src') - - # You can extend python path for looking up modules - #prefs.add('python_path', '~/python/') - - # Should rope save object information or not. - prefs['save_objectdb'] = True - prefs['compress_objectdb'] = False - - # If `True`, rope analyzes each module when it is being saved. - prefs['automatic_soa'] = True - # The depth of calls to follow in static object analysis - prefs['soa_followed_calls'] = 0 - - # If `False` when running modules or unit tests "dynamic object - # analysis" is turned off. This makes them much faster. - prefs['perform_doa'] = True - - # Rope can check the validity of its object DB when running. - prefs['validate_objectdb'] = True - - # How many undos to hold? - prefs['max_history_items'] = 32 - - # Shows whether to save history across sessions. - prefs['save_history'] = True - prefs['compress_history'] = False - - # Set the number spaces used for indenting. According to - # :PEP:`8`, it is best to use 4 spaces. Since most of rope's - # unit-tests use 4 spaces it is more reliable, too. - prefs['indent_size'] = 4 - - # Builtin and c-extension modules that are allowed to be imported - # and inspected by rope. - prefs['extension_modules'] = [] - - # Add all standard c-extensions to extension_modules list. - prefs['import_dynload_stdmods'] = True - - # If `True` modules with syntax errors are considered to be empty. - # The default value is `False`; When `False` syntax errors raise - # `rope.base.exceptions.ModuleSyntaxError` exception. - prefs['ignore_syntax_errors'] = False - - # If `True`, rope ignores unresolvable imports. Otherwise, they - # appear in the importing namespace. - prefs['ignore_bad_imports'] = False - - # If `True`, rope will insert new module imports as - # `from import ` by default. - prefs['prefer_module_from_imports'] = False - - # If `True`, rope will transform a comma list of imports into - # multiple separate import statements when organizing - # imports. - prefs['split_imports'] = False - - # If `True`, rope will sort imports alphabetically by module name - # instead of alphabetically by import statement, with from imports - # after normal imports. - prefs['sort_imports_alphabetically'] = False - - -def project_opened(project): - """This function is called after opening the project""" - # Do whatever you like here! diff --git a/.vscode/.ropeproject/objectdb b/.vscode/.ropeproject/objectdb deleted file mode 100644 index 51ac1c9..0000000 Binary files a/.vscode/.ropeproject/objectdb and /dev/null differ diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 39881c1..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Python", - "type": "python", - "request": "launch", - "stopOnEntry": false, - "pythonPath": "${config:python.pythonPath}", - "program": "${file}", - "cwd": "${workspaceRoot}", - "env": {}, - "envFile": "${workspaceRoot}/.env", - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] - }, - { - "name": "Integrated Terminal/Console", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", - "program": "${file}", - "console": "integratedTerminal", - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit" - ] - }, - { - "name": "External Terminal/Console", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", - "program": "${file}", - "console": "externalTerminal", - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit" - ] - }, - { - "name": "Django", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", - "program": "${workspaceRoot}/manage.py", - "args": [ - "runserver", - "--noreload" - ], - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput", - "DjangoDebugging" - ] - }, - { - "name": "Flask", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", - "program": "${workspaceRoot}/run.py", - "args": [], - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] - }, - { - "name": "Watson", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "pythonPath": "${config.python.pythonPath}", - "program": "${workspaceRoot}/console.py", - "args": [ - "dev", - "runserver", - "--noreload=True" - ], - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] - }, - { - "name": "Attach (Remote Debug)", - "type": "python", - "request": "attach", - "localRoot": "${workspaceRoot}", - "remoteRoot": "${workspaceRoot}", - "port": 3000, - "secret": "my_secret", - "host": "localhost" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 25edfbf..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -// Place your settings in this file to overwrite default and user settings. -{ - "python.pythonPath": "C:/Python27/python.exe", - "python.autoComplete.extraPaths": [ - "${env.SPARK_HOME}\\python", - "${env.SPARK_HOME}\\python\\pyspark" - ], - "python.linting.pylintPath": "c:/Python27/Scripts/pylint.exe" -} \ No newline at end of file diff --git a/chamberconnectlibrary/__init__.py b/chamberconnectlibrary/__init__.py index e69de29..6ad7737 100644 --- a/chamberconnectlibrary/__init__.py +++ b/chamberconnectlibrary/__init__.py @@ -0,0 +1,15 @@ +''' +A standardized interface used to interact with process controllers that +Espec North America uses. + +:copyright: (C) Espec North America, INC. +:license: MIT, see LICENSE for more details. +''' +from espec import Espec +from especp300 import EspecP300 +from especp300extended import EspecP300Extended +from especp300vib import EspecP300Vib +from especscp220 import EspecSCP220 +from watlowf4 import WatlowF4 +from watlowf4t import WatlowF4T +from controllerinterface import ControllerInterfaceError diff --git a/chamberconnectlibrary/controllerinterface.py b/chamberconnectlibrary/controllerinterface.py index 016f20f..c484872 100644 --- a/chamberconnectlibrary/controllerinterface.py +++ b/chamberconnectlibrary/controllerinterface.py @@ -1,4 +1,4 @@ -''' +''' Common interface for all All ChamberConnectLibrary upper level interfaces :copyright: (C) Espec North America, INC. @@ -824,6 +824,37 @@ def set_event(self, N, value): ''' pass + @abstractmethod + def get_air_speed(self): + ''' + Get the state of the programmable air speed + + returns: + {"current": int, "constant": int} + ''' + pass + + @abstractmethod + def get_air_speeds(self): + ''' + Get the available air speeds for the programmable air speed. + + returns: + [int] + ''' + pass + + @abstractmethod + def set_air_speed(self, value): + ''' + Set value for the chamber air speed + + Args: + speed (int): The number of constant output + constant (int): The number of constant speed mode + ''' + pass + @abstractmethod def get_status(self): ''' @@ -1044,7 +1075,7 @@ def sample(self, lookup=None, **kwargs): if tmap['type'] == 'cascade': items += ['enable_cascade', 'deviation'] lkps = [lkp for lkp in lookup[tmap['type']] if lkp['number'] == tmap['num']] - lpdata = lkps[0].copy() if lookup else {} + lpdata = lkps[0].copy() if lkps else {} lpdata.update(self.get_loop(tmap['num'], tmap['type'], items, exclusive=False)) ret['loops'].append(lpdata) if kwargs.get('get_status', True) or kwargs.get('get_program_status', False): @@ -1079,6 +1110,13 @@ def sample(self, lookup=None, **kwargs): ret['refrig'] = self.get_refrig(exclusive=False) except NotImplementedError: ret['refrig'] = None + if kwargs.get('get_air_speed', False): + try: + ret['air'] = self.get_air_speed(exclusive=False) + except NotImplementedError: + ret['air'] = None + except ControllerInterfaceError: + ret['air'] = None return ret @abstractmethod diff --git a/chamberconnectlibrary/espec.py b/chamberconnectlibrary/espec.py index f293ce6..a5e0e79 100644 --- a/chamberconnectlibrary/espec.py +++ b/chamberconnectlibrary/espec.py @@ -1,4 +1,4 @@ -''' +''' Upper level interface for Espec Corp. Controllers (just the P300 for now) :copyright: (C) Espec North America, INC. @@ -33,6 +33,7 @@ class Espec(ControllerInterface): ''' def __init__(self, **kwargs): + print 'Warning: Espec Class is no longer being maintained as of version 2.3.0; use EspecP300 or EspecSCP220 classes instead.' self.client, self.loops, self.cascades = None, None, None self.init_common(**kwargs) self.freshness = kwargs.get('freshness', 0) @@ -482,6 +483,18 @@ def set_event(self, N, value): raise ValueError('There are only 12 events') self.client.write_relay([value if i == N else None for i in range(1, 13)]) + @exclusive + def get_air_speed(self): + raise NotImplementedError + + @exclusive + def get_air_speeds(self): + raise NotImplementedError + + @exclusive + def set_air_speed(self, value): + raise NotImplementedError + @exclusive def get_status(self): if self.cached(self.client.read_mon)['alarms'] > 0: diff --git a/chamberconnectlibrary/especinteract.py b/chamberconnectlibrary/especinteract.py index d087f02..29c2fbb 100644 --- a/chamberconnectlibrary/especinteract.py +++ b/chamberconnectlibrary/especinteract.py @@ -1,4 +1,4 @@ -''' +''' Handle the actual communication with Espec Corp. Controllers :copyright: (C) Espec North America, INC. @@ -8,6 +8,7 @@ import socket import serial import time +from controllerinterface import ControllerInterfaceError ERROR_DESCIPTIONS = { 'CMD ERR':'Unrocognized command', @@ -40,7 +41,7 @@ 'CHB NOT READY':'Could not act on given command.' } -class EspecError(Exception): +class EspecError(ControllerInterfaceError): ''' Generic Espec Corp controller error ''' @@ -144,20 +145,20 @@ def interact(self, message): raises: EspecError ''' - message = message.encode('ascii', 'ignore') - # TCP forwarder doesnt handle address properly so we are ignoring it. - # if self.address: - # self.socket.send('%d,%s%s'%(self.address, message, self.delimeter)) - # else: - # self.socket.send('%s%s'%(message, self.delimeter)) - self.socket.send('%s%s'%(message, self.delimeter)) - recv = '' - while recv[0-len(self.delimeter):] != self.delimeter: - recv += self.socket.recv(1) - if recv.startswith('NA:'): - errmsg = recv[3:0-len(self.delimeter)] - msg = 'EspecError: command:"%s" genarated Error:"%s"(%s)' % ( - message, errmsg, ERROR_DESCIPTIONS.get(errmsg, 'missing description') - ) - raise EspecError(msg) - return recv[:-2] + if not isinstance(message, (list, tuple)): + message = [message] + recvs = [] + for msg in message: + msg = msg.encode('ascii', 'ignore') + self.socket.send('%s%s'%(msg, self.delimeter)) + recv = '' + while recv[0-len(self.delimeter):] != self.delimeter: + recv += self.socket.recv(1) + if recv.startswith('NA:'): + errmsg = recv[3:0-len(self.delimeter)] + msg = 'EspecError: command:"%s" genarated Error:"%s"(%s)' % ( + message, errmsg, ERROR_DESCIPTIONS.get(errmsg, 'missing description') + ) + raise EspecError(msg) + recvs.append(recv[:-1*len(self.delimeter)]) + return recvs if len(recvs) > 1 else recvs[0] diff --git a/chamberconnectlibrary/especp300.py b/chamberconnectlibrary/especp300.py new file mode 100644 index 0000000..c697118 --- /dev/null +++ b/chamberconnectlibrary/especp300.py @@ -0,0 +1,708 @@ +''' +Upper level interface for Espec Corp. P300 Controller (original command set) + +:copyright: (C) Espec North America, INC. +:license: MIT, see LICENSE for more details. +''' +#pylint: disable=R0902,R0904 +import datetime +import time +from chamberconnectlibrary.controllerinterface import ControllerInterface, exclusive +from chamberconnectlibrary.controllerinterface import ControllerInterfaceError +from chamberconnectlibrary.p300 import P300 +from chamberconnectlibrary.especinteract import EspecError + +class EspecP300(ControllerInterface): + ''' + A class for interfacing with Espec controllers (P300) + + Kwargs: + interface (str): The connection method:: + "TCP" -- Use a Ethernet to serial adapter with raw TCP + "Serial" -- Use a hardware serial port + adr (int): The address of the controller (default=1) + host (str): The hostname (IP address) of the controller when interface="TCP" + serialport (str): The serial port to use when interface="Serial" (default=3(COM4)) + baudrate (int): The serial port's baud rate to use when interface="Serial" (default=9600) + loops (int): The number of control loops the controller has (default=1, max=2) + cascades (int): The number of cascade control loops the controller has (default=0, max=1) + lock (RLock): The locking method to use when accessing the controller (default=RLock()) + freshness (int): The length of time (in seconds) a command is cached (default = 0) + ''' + + def __init__(self, **kwargs): + self.client, self.loops, self.cascades = None, None, None + self.init_common(**kwargs) + self.port = kwargs.get('port', 10001) + self.freshness = kwargs.get('freshness', 0) + self.cache = {} + self.temp, self.humi = 1, 2 + self.lpd = { + 'temp':self.temp, + 'humi':self.humi, + 'temperature':self.temp, + 'humidity':self.humi, + 'Temperature':self.temp, + 'Humidity':self.humi, + self.temp:self.temp, + self.humi:self.humi + } + ttp = (self.temp, self.humi) + self.lp_exmsg = 'The P300 controller only supports 2 loops (%d:temperature,%d:humidity)'%ttp + self.cs_exmsg = 'The P300 controller can only have loop %d as cascade' % self.temp + self.alarms = 27 + self.profiles = True + self.events = 12 + self.total_programs = 40 + self.__update_loop_map() + self.connect_args = { + 'serialport':self.serialport, + 'baudrate':self.baudrate, + 'host':self.host, + 'address':self.adr, + 'port':self.port + } + + + def __update_loop_map(self): + ''' + update the loop map. + ''' + if self.cascades > 0: + self.loop_map = [{'type':'cascade', 'num':1}] + else: + self.loop_map = [{'type':'loop', 'num':1}] + if self.cascades + self.loops > 1: + self.loop_map += [{'type':'loop', 'num':2}] + self.named_loop_map = {'Temperature':0, 'temperature':0, 'Temp':0, 'temp':0} + if len(self.loop_map) > 1: + self.named_loop_map.update({'Humidity':1, 'humidity':1, 'Hum':1, 'hum':1}) + + def connect(self): + ''' + connect to the controller using the parameters provided on class initialization + ''' + self.client = P300(self.interface, **self.connect_args) + + def close(self): + ''' + close the connection to the controller + ''' + try: + self.client.close() + except AttributeError: + pass + self.client = None + + def cached(self, func, *args, **kwargs): + ''' + The P300 returns multiple parameters with each command. The commands responses will be + cached and cached responses returned if they are fresh enough (settable property) + ''' + now = time.time() + incache = func.__name__ not in self.cache + if incache or (now - self.cache[func.__name__]['timestamp'] > self.freshness): + self.cache[func.__name__] = {'timestamp':now, 'values':func(*args, **kwargs)} + return self.cache[func.__name__]['values'] + + @exclusive + def raw(self, command): + ''' + connect directly to the controller + ''' + try: + return self.client.interact(command) + except EspecError as exc: + emsg = str(exc) + if 'The chamber did not respond in time' in emsg: + return 'NA: SERIAL TIMEOUT' + qps = [i for i, c in enumerate(emsg) if c == '"'] + return 'NA:' + emsg[qps[len(qps)-2]+1:qps[len(qps)-1]] + + @exclusive + def get_refrig(self): + return self.client.read_constant_ref() + + @exclusive + def set_refrig(self, value): + self.client.write_set(**value) + + @exclusive + def set_loop(self, identifier, loop_type='loop', param_list=None, **kwargs): + #cannot use the default controllerInterface version. + lpfuncs = { + 'cascade':{ + 'setpoint':self.set_cascade_sp, + 'setPoint':self.set_cascade_sp, + 'setValue':self.set_cascade_sp, + 'range':self.set_cascade_range, + 'enable':self.set_cascade_en, + 'deviation':self.set_cascade_deviation, + 'enable_cascade':self.set_cascade_ctl, + 'mode': self.set_cascade_mode}, + 'loop':{ + 'setpoint':self.set_loop_sp, + 'setPoint':self.set_loop_sp, + 'range':self.set_loop_range, + 'enable':self.set_loop_en, + 'mode':self.set_loop_mode + } + } + if param_list is None: + param_list = kwargs + if isinstance(identifier, basestring): + my_loop_map = self.loop_map[self.named_loop_map[identifier]] + loop_number = my_loop_map['num'] + loop_type = my_loop_map['type'] + elif isinstance(identifier, (int, long)): + loop_number = identifier + else: + raise ValueError( + 'invalid argument format, call w/: ' + 'set_loop(int(identifier), str(loop_type), **kwargs) or ' + 'get_loop(str(identifier), **kwargs)' + ) + spt1 = 'setpoint' in param_list + spt2 = 'setPoint' in param_list + spt3 = 'setValue' in param_list + if spt1 or spt2 or spt3 or 'enable' in param_list or 'mode' in param_list: + if 'enable' in param_list: + enable = param_list.pop('enable') + if isinstance(enable, dict): + enable = enable['constant'] + elif 'mode' in param_list: + my_mode = param_list.pop('mode') + if isinstance(my_mode, dict): + my_mode = my_mode['constant'] + enable = my_mode in ['On', 'ON', 'on'] + else: + enable = None + if spt1: + spv = param_list.pop('setpoint') + elif spt2: + spv = param_list.pop('setPoint') + else: + spv = param_list.pop('setValue') + if isinstance(spv, dict): + spv = spv['constant'] + params = {'setpoint':spv, 'enable':enable} + if range in param_list: + params.update(param_list.pop('range')) + if self.lpd[loop_number] == self.temp: + self.client.write_temp(**params) + elif self.lpd[loop_number] == self.humi: + self.client.write_humi(**params) + else: + raise ValueError(self.lp_exmsg) + if 'deviation' in param_list and 'enable_cascade' in param_list: + if isinstance(param_list['enable_cascade'], dict): + cparams = {'enable':param_list.pop('enable_cascade')['constant']} + else: + cparams = {param_list.pop('enable_cascade')} + cparams.update(param_list.pop('deviation')) + self.client.write_temp_ptc(**cparams) + for key, val in param_list.items(): + params = {'value':val} + params.update({'exclusive':False, 'N':loop_number}) + try: + lpfuncs[loop_type][key](**params) + except KeyError: + pass + + @exclusive + def get_datetime(self): + return datetime.datetime(**self.client.read_date_time()) + + @exclusive + def set_datetime(self, value): + weekday = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'][value.weekday()] + self.client.write_time(value.hour, value.minute, value.second) + self.client.write_date(value.year, value.month, value.day, weekday) + + @exclusive + def get_loop_sp(self, N): + if self.lpd[N] == self.temp: + cur = self.cached(self.client.read_temp)['setpoint'] + con = self.cached(self.client.read_constant_temp)['setpoint'] + elif self.lpd[N] == self.humi: + cur = self.cached(self.client.read_humi)['setpoint'] + con = self.cached(self.client.read_constant_humi)['setpoint'] + else: + raise ValueError(self.lp_exmsg) + return {'constant':con, 'current':cur} + + @exclusive + def set_loop_sp(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if self.lpd[N] == self.temp: + self.client.write_temp(setpoint=value) + elif self.lpd[N] == self.humi: + self.client.write_humi(setpoint=value) + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_pv(self, N): + if self.lpd[N] == self.temp: + return {'air':self.cached(self.client.read_temp)['processvalue']} + elif self.lpd[N] == self.humi: + return {'air':self.cached(self.client.read_humi)['processvalue']} + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def set_loop_range(self, N, value): + if 'max' not in value or 'min' not in value: + raise AttributeError('missing "max" or "min" property') + if self.lpd[N] == self.temp: + self.client.write_temp(min=value['min'], max=value['max']) + elif self.lpd[N] == self.humi: + self.client.write_humi(min=value['min'], max=value['max']) + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_range(self, N): + if self.lpd[N] == self.temp: + return self.cached(self.client.read_temp)['range'] + elif self.lpd[N] == self.humi: + return self.cached(self.client.read_humi)['range'] + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_en(self, N): + if self.lpd[N] == self.temp: + return {'constant':True, 'current':True} + elif self.lpd[N] == self.humi: + return { + 'current':self.cached(self.client.read_humi)['enable'], + 'constant':self.cached(self.client.read_constant_humi)['enable'] + } + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def set_loop_en(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if self.lpd[N] == self.temp: + pass + elif self.lpd[N] == self.humi: + if value: + self.client.write_humi( + enable=True, + setpoint=self.cached(self.client.read_constant_humi)['setpoint'] + ) + else: + self.client.write_humi(enable=False) + else: raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_units(self, N): + if self.lpd[N] == self.temp: + return u'\xb0C' + elif self.lpd[N] == self.humi: + return u'%RH' + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def set_loop_mode(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if value in ['Off', 'OFF', 'off']: + self.set_loop_en(N, False, exclusive=False) + elif value in ['On', 'ON', 'on']: + self.set_loop_en(N, True, exclusive=False) + else: + raise ValueError('Mode must be on or off, recived:' + value) + + @exclusive + def get_loop_mode(self, N): + if self.lpd[N] == self.temp: + cur, con = 'On', 'On' + elif self.lpd[N] == self.humi: + cur = 'On' if self.cached(self.client.read_humi)['enable'] else 'Off' + con = 'On' if self.cached(self.client.read_constant_humi)['enable'] else 'Off' + else: + raise ValueError(self.lp_exmsg) + if self.client.read_mode() in ['OFF', 'STANDBY']: + cur = 'Off' + return {"current": cur, "constant": con} + + def get_loop_modes(self, N): + if self.lpd[N] == self.temp: + return ['On'] + elif self.lpd[N] == self.humi: + return ['Off', 'On'] + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_power(self, N): + if self.lpd[N] == self.temp: + val = self.cached(self.client.read_htr)['dry'] + elif self.lpd[N] == self.humi: + val = self.cached(self.client.read_htr)['wet'] + else: + raise ValueError(self.lp_exmsg) + return {'current':val, 'constant':val} + + def set_loop_power(self, N, value): + raise NotImplementedError + + @exclusive + def get_cascade_sp(self, N): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + cur = self.cached(self.client.read_temp_ptc) + enc = cur['enable_cascade'] + return { + 'constant':self.cached(self.client.read_constant_temp)['setpoint'], + 'current':cur['setpoint']['product'] if enc else cur['setpoint']['air'], + 'air':cur['setpoint']['air'], + 'product':cur['setpoint']['product'] + } + + @exclusive + def set_cascade_sp(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + self.client.write_temp(setpoint=value) + + @exclusive + def get_cascade_pv(self, N): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return self.cached(self.client.read_temp_ptc)['processvalue'] + + @exclusive + def get_cascade_range(self, N): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return self.get_loop_range(self.temp, exclusive=False) + + @exclusive + def set_cascade_range(self, N, value): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + self.set_loop_range(self.temp, value, exclusive=False) + + @exclusive + def get_cascade_en(self, N): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return self.get_loop_en(self.temp, exclusive=False) + + @exclusive + def set_cascade_en(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return self.set_loop_en(self.temp, value, exclusive=False) + + @exclusive + def get_cascade_units(self, N): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return self.get_loop_units(self.temp, exclusive=False) + + @exclusive + def set_cascade_mode(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return self.set_loop_mode(N, value, exclusive=False) + + @exclusive + def get_cascade_mode(self, N): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return self.get_loop_mode(self.temp, exclusive=False) + + def get_cascade_modes(self, N): + return self.get_loop_modes(N) + + @exclusive + def get_cascade_ctl(self, N): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return { + 'current': self.cached(self.client.read_temp_ptc)['enable_cascade'], + 'constant': self.cached(self.client.read_constant_ptc)['enable'] + } + + @exclusive + def set_cascade_ctl(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + params = self.cached(self.client.read_temp_ptc) + params['deviation'].update({'enable':value}) + self.client.write_temp_ptc(**params['deviation']) + + @exclusive + def get_cascade_deviation(self, N): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return self.cached(self.client.read_constant_ptc)['deviation'] + + @exclusive + def set_cascade_deviation(self, N, value): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + if 'positive' not in value or 'negative' not in value: + raise ValueError('value must contain "positive" and "negative" keys') + self.client.write_temp_ptc(self.get_cascade_ctl(self.temp, exclusive=False), **value) + + @exclusive + def get_cascade_power(self, N): + if self.lpd[N] != self.temp: + raise ValueError(self.cs_exmsg) + return self.get_loop_power(self.temp, exclusive=False) + + @exclusive + def set_cascade_power(self, N, value): + raise NotImplementedError + + @exclusive + def get_event(self, N): + if N >= 13: + raise ValueError('There are only 12 events') + return { + 'current':self.cached(self.client.read_relay)[N-1], + 'constant':self.cached(self.client.read_constant_relay)[N-1] + } + + @exclusive + def set_event(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if N >= 13: + raise ValueError('There are only 12 events') + self.client.write_relay([value if i == N else None for i in range(1, 13)]) + + @exclusive + def get_air_speed(self): + raise NotImplementedError + + @exclusive + def get_air_speeds(self): + raise NotImplementedError + + @exclusive + def set_air_speed(self, value): + raise NotImplementedError + + @exclusive + def get_status(self): + if self.cached(self.client.read_mon)['alarms'] > 0: + return 'Alarm' + return { + 'OFF':'Off', + 'STANDBY':'Standby', + 'CONSTANT':'Constant', + 'RUN':'Program Running', + 'RUN PAUSE':'Program Paused', + 'RUN END HOLD':'Program End Hold', + 'RMT RUN':'Program Remote Running', + 'RMT RUN PAUSE':'Program Remote Paused', + 'RMT RUN END HOLD':'Program Remote End Hold' + }[self.client.read_mode(True)] + + @exclusive + def get_alarm_status(self): + active = self.client.read_alarm() + alarmlist = [0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 18, 19, 21, 22, 23, 26, + 30, 31, 40, 41, 43, 46, 48, 50, 51, 99] + inactive = [x for x in alarmlist if x not in active] + return {'active':active, 'inactive':inactive} + + @exclusive + def const_start(self): + self.client.write_mode_constant() + + @exclusive + def stop(self): + self.client.write_mode_standby() + + @exclusive + def prgm_start(self, N, step): + self.client.write_prgm_run(N, step) + + @exclusive + def prgm_pause(self): + self.client.write_prgm_pause() + + @exclusive + def prgm_resume(self): + self.client.write_prgm_continue() + + @exclusive + def prgm_next_step(self): + self.client.write_prgm_advance() + + @exclusive + def get_prgm_counter(self): + prgm_set = self.client.read_prgm_set() + prgm_data = self.client.read_prgm_data(prgm_set['number']) + prgm_mon = self.client.read_prgm_mon() + ret = [ + { + 'name':'A', + 'remaining':prgm_mon['counter_a'], + 'count':prgm_data['counter_a']['cycles'] - prgm_mon['counter_a'] + }, + { + 'name':'B', + 'remaining':prgm_mon['counter_b'], + 'count':prgm_data['counter_b']['cycles'] - prgm_mon['counter_b'] + } + ] + ret[0].update(prgm_data['counter_a']) + ret[1].update(prgm_data['counter_b']) + return ret + + @exclusive + def get_prgm_cur(self): + return self.cached(self.client.read_prgm_set)['number'] + + @exclusive + def get_prgm_cstep(self): + return self.cached(self.client.read_prgm_mon)['pgmstep'] + + @exclusive + def get_prgm_cstime(self): + try: + rtime = self.cached(self.client.read_prgm_mon)['time'] + except EspecError: + rtime = self.cached(self.client.read_run_prgm_mon)['time'] + return '%d:%02d:00' % (rtime['hour'], rtime['minute']) + + @exclusive + def get_prgm_time(self, pgm=None): + if pgm is None: + pgm = self.client.read_prgm(self.cached(self.client.read_prgm_set)['number']) + try: + pgms = self.cached(self.client.read_prgm_mon) + except EspecError: + rtime = self.cached(self.client.read_run_prgm_mon)['time'] + return '%d:%02d:00' % (rtime['hour'], rtime['minute']) + + #counter_a must be the inner counter or the only counter + use_a = pgm['counter_a']['cycles'] != 0 + use_b = pgm['counter_b']['cycles'] != 0 + ae_gte_be = pgm['counter_a']['end'] >= pgm['counter_b']['end'] + as_lte_bs = pgm['counter_a']['start'] <= pgm['counter_b']['start'] + same_end = pgm['counter_a']['end'] == pgm['counter_b']['end'] + + if (ae_gte_be and as_lte_bs) or not use_a and use_b: + pgm['counter_a'], pgm['counter_b'] = pgm['counter_b'], pgm['counter_a'] + pgms['counter_a'], pgms['counter_b'] = pgms['counter_b'], pgms['counter_a'] + use_a, use_b = use_b, use_a + + nested = use_a and use_b and pgm['counter_a']['start'] >= pgm['counter_b']['start'] + nested = nested and pgm['counter_a']['end'] <= pgm['counter_b']['end'] + tminutes, tta, ttb = -1, 0, 0 + if use_a: + for i in range(pgm['counter_a']['start']-1, pgm['counter_a']['end']): + tta += pgm['steps'][i]['time']['hour']*60 + pgm['steps'][i]['time']['minute'] + if use_b: + for i in range(pgm['counter_b']['start']-1, pgm['counter_b']['end']): + if nested and i >= pgm['counter_a']['start']-1 and i < pgm['counter_a']['end']-1: + pass + elif nested and i >= pgm['counter_a']['start']-1 and i == pgm['counter_a']['end']-1: + ttb += tta*(pgm['counter_a']['cycles'] + 1) + else: + ttb += pgm['steps'][i]['time']['hour']*60 + pgm['steps'][i]['time']['minute'] + + # correct for p300 not resetting the nested counter until it hits counter.start + if nested and pgms['pgmstep'] < pgm['counter_a']['start']: + count_a = pgm['counter_a']['cycles'] + else: + count_a = pgms['counter_a'] + for i in range(pgms['pgmstep']-1, len(pgm['steps'])): #ensure that this is not off by 1 + if tminutes == -1: + tminutes = pgms['time']['hour']*60 + pgms['time']['minute'] + else: + mystep = pgm['steps'][i] + tminutes += mystep['time']['hour']*60 + mystep['time']['minute'] + if use_a and i == pgm['counter_a']['end']-1: + tminutes += tta*count_a + if use_b and i == pgm['counter_b']['end']-1: + tminutes += ttb*pgms['counter_b'] + return "%d:%02d:00" % (int(tminutes/60), tminutes%60) + + @exclusive + def get_prgm_name(self, N): + return self.cached(self.client.read_prgm_data, N)['name'] + + def set_prgm_name(self, N, value): + raise NotImplementedError + + @exclusive + def get_prgm_steps(self, N): + return self.client.read_prgm_data(N)['steps'] + + @exclusive + def get_prgms(self): + names = [] + for i in range(1, self.total_programs+1): + try: + names.append({'number':i, 'name':self.client.read_prgm_use_num(i)['name']}) + except EspecError: + names.append({'number':i, 'name':''}) + return names + + @exclusive + def get_prgm(self, N): + try: + return self.client.read_prgm(N, self.cascades > 0) + except EspecError: + raise ControllerInterfaceError('Could not read program from chamber controller.') + + @exclusive + def set_prgm(self, N, prgm): + self.client.write_prgm(N, prgm) + + @exclusive + def prgm_delete(self, N): + self.client.write_prgm_erase(N) + + + @exclusive + def process_controller(self, update=True): + if update: + self.loops = 0 + self.cascades = 0 + msg = 'P300 ' if self.client.read_rom().startswith('P3') else 'SCP-220 ' + try: + if update: + self.client.read_temp_ptc() + self.cascades = 1 + msg += 'W/PTCON ' + except EspecError: + if update: + self.loops += 1 + try: + if update: + self.client.read_humi() + self.loops += 1 + msg += 'W/Humidity' + except EspecError: + pass + self.__update_loop_map() + return msg + + @exclusive + def get_network_settings(self): + ret = self.client.read_ip_set() + ret.update({'message':'', 'host':''}) + return ret + + @exclusive + def set_network_settings(self, value): + if value: + self.client.write_ip_set(value.get('address', '0.0.0.0'), + value.get('mask', '0.0.0.0'), value.get('gateway', '0.0.0.0')) + else: + self.client.write_ip_set('0.0.0.0', '0.0.0.0', '0.0.0.0') diff --git a/chamberconnectlibrary/especp300extended.py b/chamberconnectlibrary/especp300extended.py new file mode 100644 index 0000000..2e5dd70 --- /dev/null +++ b/chamberconnectlibrary/especp300extended.py @@ -0,0 +1,49 @@ +''' +Upper level interface for Espec Corp. SCP220 Controllers + +:copyright: (C) Espec North America, INC. +:license: MIT, see LICENSE for more details. +''' +from chamberconnectlibrary.p300extended import P300Extended +from chamberconnectlibrary.especp300 import EspecP300, exclusive + +class EspecP300Extended(EspecP300): + ''' + A class for interfacing with Espec controllers (P300) + + Kwargs: + interface (str): The connection method:: + "TCP" -- Use a Ethernet to serial adapter with raw TCP + "Serial" -- Use a hardware serial port + adr (int): The address of the controller (default=1) + host (str): The hostname (IP address) of the controller when interface="TCP" + serialport (str): The serial port to use when interface="Serial" (default=3(COM4)) + baudrate (int): The serial port's baud rate to use when interface="Serial" (default=9600) + loops (int): The number of control loops the controller has (default=1, max=2) + cascades (int): The number of cascade control loops the controller has (default=0, max=1) + lock (RLock): The locking method to use when accessing the controller (default=RLock()) + freshness (int): The length of time (in seconds) a command is cached (default = 0) + enable_air_speed (bool): Set to True if this P300 has air speed control + ''' + + def __init__(self, **kwargs): + super(EspecP300Extended, self).__init__(**kwargs) + self.connect_args['enable_air_speed'] = kwargs.get('enable_air_speed', False) + + def connect(self): + self.client = P300Extended(self.interface, **self.connect_args) + + @exclusive + def get_air_speed(self): + return { + 'current':self.client.read_air()['selected'], + 'constant':self.client.read_constant_air()['selected'] + } + + @exclusive + def get_air_speeds(self): + return self.client.read_air()['options'] + + @exclusive + def set_air_speed(self, value): + self.client.write_air(value) diff --git a/chamberconnectlibrary/especp300vib.py b/chamberconnectlibrary/especp300vib.py new file mode 100644 index 0000000..6edb4ce --- /dev/null +++ b/chamberconnectlibrary/especp300vib.py @@ -0,0 +1,266 @@ +''' +Upper level interface for Espec Corp. P300 Controller's w/ vibration firmware + +:copyright: (C) Espec North America, INC. +:license: MIT, see LICENSE for more details. +''' +from chamberconnectlibrary.p300vib import P300Vib +from chamberconnectlibrary.especp300extended import EspecP300Extended, exclusive + +class EspecP300Vib(EspecP300Extended): + ''' + A class for interfacing with Espec controllers (P300) + + Kwargs: + interface (str): The connection method:: + "TCP" -- Use a Ethernet to serial adapter with raw TCP + "Serial" -- Use a hardware serial port + adr (int): The address of the controller (default=1) + host (str): The hostname (IP address) of the controller when interface="TCP" + serialport (str): The serial port to use when interface="Serial" (default=3(COM4)) + baudrate (int): The serial port's baud rate to use when interface="Serial" (default=9600) + loops (int): The number of control loops the controller has (default=1, max=2) + cascades (int): The number of cascade control loops the controller has (default=0, max=1) + lock (RLock): The locking method to use when accessing the controller (default=RLock()) + freshness (int): The length of time (in seconds) a command is cached (default = 0) + enable_air_speed (bool): Set to True if this P300 has air speed control + ''' + + def __init__(self, **kwargs): + super(EspecP300Vib, self).__init__(**kwargs) + self.temp, self.humi, self.vib = 1, None, 2 + self.lpd = { + 'temp':self.temp, + 'temperature':self.temp, + 'Temperature':self.temp, + 'vib':self.vib, + 'vibration':self.vib, + 'Vibration':self.vib, + self.temp:self.temp, + self.vib:self.vib + } + self.lp_exmsg = ( + 'The EspecP300Vib controller only supports 2 loops (1:temperature,2:vibration)' + ) + self.__update_loop_map() + + def __update_loop_map(self): + ''' + update the loop map. + ''' + if self.cascades > 0: + self.loop_map = [{'type':'cascade', 'num':1}] + else: + self.loop_map = [{'type':'loop', 'num':1}] + if self.cascades + self.loops > 1: + self.loop_map += [{'type':'loop', 'num':2}] + self.named_loop_map = {'Temperature':0, 'temperature':0, 'Temp':0, 'temp':0} + if len(self.loop_map) > 1: + self.named_loop_map.update({'Vibration':1, 'vibration':1, 'Vib':1, 'vib':1}) + + def connect(self): + self.client = P300Vib(self.interface, **self.connect_args) + + @exclusive + def set_loop(self, identifier, loop_type='loop', param_list=None, **kwargs): + ''' + cannot use the default controllerInterface version. + ''' + lpfuncs = { + 'cascade':{ + 'setpoint':self.set_cascade_sp, + 'setPoint':self.set_cascade_sp, + 'setValue':self.set_cascade_sp, + 'range':self.set_cascade_range, + 'enable':self.set_cascade_en, + 'deviation':self.set_cascade_deviation, + 'enable_cascade':self.set_cascade_ctl, + 'mode': self.set_cascade_mode + }, + 'loop':{ + 'setpoint':self.set_loop_sp, + 'setPoint':self.set_loop_sp, + 'range':self.set_loop_range, + 'enable':self.set_loop_en, + 'mode':self.set_loop_mode + } + } + if param_list is None: + param_list = kwargs + if isinstance(identifier, basestring): + my_loop_map = self.loop_map[self.named_loop_map[identifier]] + loop_number = my_loop_map['num'] + loop_type = my_loop_map['type'] + elif isinstance(identifier, (int, long)): + loop_number = identifier + else: + raise ValueError( + 'invalid argument format, call w/: ' + 'set_loop(int(identifier), str(loop_type), **kwargs) or ' + 'get_loop(str(identifier), **kwargs)' + ) + spt1 = 'setpoint' in param_list + spt2 = 'setPoint' in param_list + spt3 = 'setValue' in param_list + if spt1 or spt2 or spt3 or 'enable' in param_list or 'mode' in param_list: + if 'enable' in param_list: + enable = param_list.pop('enable') + if isinstance(enable, dict): + enable = enable['constant'] + elif 'mode' in param_list: + my_mode = param_list.pop('mode') + if isinstance(my_mode, dict): + my_mode = my_mode['constant'] + enable = my_mode in ['On', 'ON', 'on'] + else: + enable = None + if spt1: + spv = param_list.pop('setpoint') + elif spt2: + spv = param_list.pop('setPoint') + else: + spv = param_list.pop('setValue') + if isinstance(spv, dict): + spv = spv['constant'] + params = {'setpoint':spv, 'enable':enable} + if range in param_list: + params.update(param_list.pop('range')) + if self.lpd[loop_number] == self.temp: + self.client.write_temp(**params) + elif self.lpd[loop_number] == self.vib: + self.client.write_vib(**params) + else: + raise ValueError(self.lp_exmsg) + if 'deviation' in param_list and 'enable_cascade' in param_list: + if isinstance(param_list['enable_cascade'], dict): + params = {'enable':param_list.pop('enable_cascade')['constant']} + else: + params = {param_list.pop('enable_cascade')} + params.update(param_list.pop('deviation')) + self.client.write_temp_ptc(**params) + for key, val in param_list.items(): + params = {'value':val} + params.update({'exclusive':False, 'N':loop_number}) + try: + lpfuncs[loop_type][key](**params) + except KeyError: + pass + + @exclusive + def get_loop_sp(self, N): + if self.lpd[N] == self.temp: + cur = self.cached(self.client.read_temp)['setpoint'] + con = self.cached(self.client.read_constant_temp)['setpoint'] + elif self.lpd[N] == self.vib: + cur = self.cached(self.client.read_vib)['setpoint'] + con = self.cached(self.client.read_constant_vib)['setpoint'] + else: + raise ValueError(self.lp_exmsg) + return {'constant':con, 'current':cur} + + @exclusive + def set_loop_sp(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if self.lpd[N] == self.temp: + self.client.write_temp(setpoint=value) + elif self.lpd[N] == self.vib: + self.client.write_vib(setpoint=value) + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_pv(self, N): + if self.lpd[N] == self.temp: + return {'air':self.cached(self.client.read_temp)['processvalue']} + elif self.lpd[N] == self.vib: + return {'air':self.cached(self.client.read_vib)['processvalue']} + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def set_loop_range(self, N, value): + if 'max' not in value or 'min' not in value: + raise AttributeError('missing "max" or "min" property') + if self.lpd[N] == self.temp: + self.client.write_temp(min=value['min'], max=value['max']) + elif self.lpd[N] == self.vib: + self.client.write_vib(min=value['min'], max=value['max']) + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_range(self, N): + if self.lpd[N] == self.temp: + return self.cached(self.client.read_temp)['range'] + elif self.lpd[N] == self.vib: + return self.cached(self.client.read_vib)['range'] + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_en(self, N): + if self.lpd[N] == self.temp: + return {'constant':True, 'current':True} + elif self.lpd[N] == self.vib: + return { + 'current':self.cached(self.client.read_vib)['enable'], + 'constant':self.cached(self.client.read_constant_vib)['enable'] + } + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def set_loop_en(self, N, value): + value = value['constant'] if isinstance(value, dict) else value + if self.lpd[N] == self.temp: + pass + elif self.lpd[N] == self.vib: + if value: + self.client.write_vib( + enable=True, + setpoint=self.cached(self.client.read_constant_vib)['setpoint'] + ) + else: + self.client.write_vib(enable=False) + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_units(self, N): + if self.lpd[N] == self.temp: + return u'\xb0C' + elif self.lpd[N] == self.vib: + return u'Grms' + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_mode(self, N): + if self.lpd[N] == self.temp: + cur, con = 'On', 'On' + elif self.lpd[N] == self.vib: + cur = 'On' if self.cached(self.client.read_vib)['enable'] else 'Off' + con = 'On' if self.cached(self.client.read_constant_vib)['enable'] else 'Off' + else: + raise ValueError(self.lp_exmsg) + if self.client.read_mode() in ['OFF', 'STANDBY']: + cur = 'Off' + return {"current": cur, "constant": con} + + def get_loop_modes(self, N): + if self.lpd[N] == self.temp: + return ['On'] + elif self.lpd[N] == self.vib: + return ['Off', 'On'] + else: + raise ValueError(self.lp_exmsg) + + @exclusive + def get_loop_power(self, N): + if self.lpd[N] == self.temp: + val = self.cached(self.client.read_htr)['dry'] + elif self.lpd[N] == self.vib: + val = self.cached(self.client.read_htr)['vib'] + else: + raise ValueError(self.lp_exmsg) + return {'current':val, 'constant':val} diff --git a/chamberconnectlibrary/especscp220.py b/chamberconnectlibrary/especscp220.py new file mode 100644 index 0000000..b428d70 --- /dev/null +++ b/chamberconnectlibrary/especscp220.py @@ -0,0 +1,39 @@ +''' +Upper level interface for Espec Corp. SCP220 Controllers + +:copyright: (C) Espec North America, INC. +:license: MIT, see LICENSE for more details. +''' +from chamberconnectlibrary.scp220 import SCP220 +from chamberconnectlibrary.especp300 import EspecP300 + +class EspecSCP220(EspecP300): + ''' + A class for interfacing with Espec controllers (P300) + + Kwargs: + interface (str): The connection method:: + "TCP" -- Use a Ethernet to serial adapter with raw TCP + "Serial" -- Use a hardware serial port + adr (int): The address of the controller (default=1) + host (str): The hostname (IP address) of the controller when interface="TCP" + serialport (str): The serial port to use when interface="Serial" (default=3(COM4)) + baudrate (int): The serial port's baud rate to use when interface="Serial" (default=9600) + loops (int): The number of control loops the controller has (default=1, max=2) + cascades (int): The number of cascade control loops the controller has (default=0, max=1) + lock (RLock): The locking method to use when accessing the controller (default=RLock()) + freshness (int): The length of time (in seconds) a command is cached (default = 0) + ''' + + def __init__(self, **kwargs): + super(EspecSCP220, self).__init__(**kwargs) + ttp = (self.temp, self.humi) + self.lp_exmsg = 'The SCP220 controller only supports 2 loops (%d:temperature,%d:humidity)' % ttp + self.cs_exmsg = 'The SCP220 controller can only have loop %d as cascade' % self.temp + self.total_programs = 30 + + def connect(self): + ''' + connect to the controller using the parameters provided on class initialization + ''' + self.client = SCP220(self.interface, **self.connect_args) \ No newline at end of file diff --git a/chamberconnectlibrary/modbus.py b/chamberconnectlibrary/modbus.py index 2b4e551..acecd6b 100644 --- a/chamberconnectlibrary/modbus.py +++ b/chamberconnectlibrary/modbus.py @@ -1,4 +1,4 @@ -''' +''' Copyright (C) Espec North America, INC. - All Rights Reserved Written by Myles Metzler mmetzler@espec.com, Feb. 2016 @@ -10,8 +10,9 @@ import time import collections import serial +from controllerinterface import ControllerInterfaceError -class ModbusError(Exception): +class ModbusError(ControllerInterfaceError): '''Generic Modbus exception.''' pass diff --git a/chamberconnectlibrary/p300.py b/chamberconnectlibrary/p300.py index 8117ac0..60f43d0 100644 --- a/chamberconnectlibrary/p300.py +++ b/chamberconnectlibrary/p300.py @@ -1,4 +1,4 @@ -''' +''' A direct implimentation of the P300's communication interface. :copyright: (C) Espec North America, INC. @@ -48,7 +48,8 @@ def __init__(self, interface, **kwargs): else: self.ctlr = EspecTCP( host=kwargs.get('host'), - address=kwargs.get('address') + address=kwargs.get('address'), + port=kwargs.get('port', 10001) ) def __del__(self): @@ -333,7 +334,7 @@ def read_mode(self, detail=False): returns: detail=Faslse: string "OFF" or "STANDBY" or "CONSTANT" or "RUN" detail=True: string (one of the following): - "OFF""STANDBY" or "CONSTANT" or "RUN" or "RUN PAUSE" or "RUN END HOLD" or + "OFF" or "STANDBY" or "CONSTANT" or "RUN" or "RUN PAUSE" or "RUN END HOLD" or "RMT RUN" or "RMT RUN PAUSE" or "RMT RUN END HOLD" ''' return self.ctlr.interact('MODE?%s' % (',DETAIL' if detail else '')) @@ -1392,18 +1393,35 @@ def parse_prgm_data_detail(self, arg): r'(?:,([0-9.-]+))?(?:,HUMI(\w+)(?:,(\d+))?)?', arg ) + if parsed: + ret = { + 'tempDetail':{ + 'range':{'max':float(parsed.group(1)), 'min':float(parsed.group(2))}, + 'mode':parsed.group(5), + 'setpoint':parsed.group(6) + } + } + if parsed.group(3): + ret['humiDetail'] = { + 'range':{'max':float(parsed.group(3)), 'min':float(parsed.group(4))}, + 'mode':parsed.group(7), + 'setpoint':parsed.group(8) + } + return ret + #P310 hack + parsed = re.search(r'([0-9.-]+),([0-9.-]+),(?:(\d+),(\d+),)?OFF', arg) ret = { 'tempDetail':{ 'range':{'max':float(parsed.group(1)), 'min':float(parsed.group(2))}, - 'mode':parsed.group(5), - 'setpoint':parsed.group(6) + 'mode':'OFF', + 'setpoint':0.0 } } if parsed.group(3): ret['humiDetail'] = { 'range':{'max':float(parsed.group(3)), 'min':float(parsed.group(4))}, - 'mode':parsed.group(7), - 'setpoint':parsed.group(8) + 'mode':'OFF', + 'setpoint':0 } return ret diff --git a/chamberconnectlibrary/p300extended.py b/chamberconnectlibrary/p300extended.py new file mode 100644 index 0000000..708392b --- /dev/null +++ b/chamberconnectlibrary/p300extended.py @@ -0,0 +1,632 @@ +''' +A direct implementation of the SCP220's communication interface. + +:copyright: (C) Espec North America, INC. +:license: MIT, see LICENSE for more details. +''' +import re +from p300 import P300, tryfloat + +class P300Extended(P300): + ''' + P300 communications basic implementation for p300 firmware with extended command set + (air speed & constant modes 2 & 3) + + Args: + interface (str): The interface type to connect to: "Serial" or "TCP" + Kwargs: + serialport (str/int): The serial port to connect to when interface="Serial" + baudrate (int): The baud rate to connect at when interface="Serial" + address (int): The RS485 address of the chamber to connect to. + host (str): The IP address or hostname of the chamber when interface="TCP" + ''' + + def __init__(self, interface, **kwargs): + super(P300Extended, self).__init__(interface, **kwargs) + self.enable_air_speed = kwargs.get('enable_air_speed', False) + + def read_mode(self, detail=False, constant=False): + ''' + Return the chamber operation state. + + Added new feature: CONSTANT + + Args: + detail: boolean, if True get additional information (not SCP220 compatible) + constant: boolean, (overrides detail) get the constant number. + returns: + detail=False & constant=False: string "OFF" or "STANDBY" or "CONSTANT" or "RUN" + detail=True: string (one of the following): + "OFF" or "STANDBY" or "CONSTANT" or "RUN" or "RUN PAUSE" or "RUN END HOLD" or + "RMT RUN" or "RMT RUN PAUSE" or "RMT RUN END HOLD" + constant=True: string (one of the following) + "OFF" or "STANDBY" or "CONSTANT1" or "CONSTANT2" or "CONSTANT3" or "RUN" or + "RUN PAUSE" or "RUN END HOLD" or "RMT RUN" or "RMT RUN PAUSE" or "RMT RUN END HOLD" + ''' + if constant: + return self.ctlr.interact('MODE?,DETAIL,CONSTANT') + else: + return self.ctlr.interact('MODE?' + (',DETAIL' if detail else '')) + + def read_mon(self, detail=False, constant=False): + ''' + Returns the conditions inside the chamber + + Args: + detail: boolean, when True "mode" parameter has additional details + constant: boolean, if True get the number for the running consant mode. + returns: + {"temperature":float,"humidity":float,"mode":string,"alarms":int} + "humidity": only present if chamber has humidity + "mode": see read_mode for valid parameters (with and without detail flag). + ''' + if constant: + rsp = self.ctlr.interact('MON?,DETAIL,CONSTANT').split(',') + else: + rsp = self.ctlr.interact('MON?%s%s' % (',DETAIL' if detail else '', ',CONSTANT' if constant else '')).split(',') + data = {'temperature':float(rsp[0]), 'mode':rsp[2], 'alarms':int(rsp[3])} + if rsp[1]: + data['humidity'] = float(rsp[1]) + return data + + def read_air(self): #is this correct?????? + ''' + Read the currently selected air speed value and the options values. + + returns: + {'selected':int, 'options':[int]} + ''' + selected, options = self.ctlr.interact('AIR?').split('/') + return {'selected':int(selected), 'options':range(1, int(options)+1)} + + def read_constant_air(self, constant=1): + ''' + Read the selected air speed value and the options values for the specified constant mode. + + Args: + constant: int, the constant mode to read from with a range of 1 to 3; 1 is default + returns: + {'selected':int, 'options':[int]} + ''' + if constant in [1, 2, 3]: + selected, options = self.ctlr.interact('CONSTANT SET?,AIR,C%d' % constant).split('/') + else: + raise ValueError("Constant must be None or 1, 2, 3") + return {'selected':int(selected), 'options':range(1, int(options)+1)} + + def read_constant_temp(self, constant=1): + ''' + Get the constant settings for the temperature loop + + returns: + {"setpoint":float,"enable":True} + ''' + if constant in [1, 2, 3]: + rsp = self.ctlr.interact('CONSTANT SET?,TEMP,C%d' % constant).split(',') + else: + raise ValueError("Constant must be None or 1, 2, 3") + return {'setpoint':float(rsp[0]), 'enable':rsp[1] == 'ON'} + + def read_constant_humi(self, constant=1): + ''' + Get the constant settings for the humidity loop + + returns: + {"setpoint":float,"enable":boolean} + ''' + if constant in [1, 2, 3]: + rsp = self.ctlr.interact('CONSTANT SET?,HUMI,C%d' % constant).split(',') + else: + raise ValueError("Constant must be None or 1, 2, 3") + return {'setpoint':float(rsp[0]), 'enable':rsp[1] == 'ON'} + + def read_constant_ref(self, constant=1): + ''' + Get the constant settings for the refrigeration system + + returns: + {"mode":string,"setpoint":float} + ''' + if constant in [1, 2, 3]: + rsp = self.ctlr.interact('CONSTANT SET?,REF,C%d' % constant) + else: + raise ValueError("Constant must be None or 1, 2, 3") + try: + return {'mode':'manual', 'setpoint':float(rsp)} + except Exception: + return {'mode':rsp.lower(), 'setpoint':0} + + def read_constant_relay(self, constant=1): + ''' + Get the constant settings for the relays(time signals) + + returns: + [int] + ''' + if constant in [1, 2, 3]: + rsp = self.ctlr.interact('CONSTANT SET?,RELAY,C%d' % constant).split(',') + else: + raise ValueError("Constant must be None or 1, 2, 3") + return [str(i) in rsp[1:] for i in range(1, 13)] + + def read_constant_ptc(self, constant=1): + ''' + Get the constant settings for product temperature control + + returns: + {"enable":boolean,"deviation":{"positive":float,"negative":float}} + ''' + if constant in [1, 2, 3]: + rsp = self.ctlr.interact('CONSTANT SET?,PTC,C%d' % constant).split(',') + else: + raise ValueError("Constant must be None or 1, 2, 3") + return { + 'enable': rsp[0] == 'ON', + 'deviation': {'positive':float(rsp[1]), 'negative':float(rsp[2])} + } + + def read_prgm(self, pgmnum, with_ptc=False): + pgm = super(P300Extended, self).read_prgm(pgmnum, with_ptc) + if pgmnum == 0 and self.enable_air_speed: + pgm['steps'][0]['air'] = self.read_air() + return pgm + + def read_prgm_data(self, pgmnum): + ''' + get the parameters for a given program + + Args: + pgmnum: int, the program to get + returns: + { + "steps":int, + "name":string, + "end":string, + "counter_a":{"start":int, "end":int, "cycles":int}, + "counter_b":{"start":int, "end":int, "cycles":int} + } + "END"="OFF" or "CONSTANT" or "STANDBY" or "RUN" + ''' + pdata = self.ctlr.interact('PRGM DATA?,%s:%d,CONSTANT' % (self.rom_pgm(pgmnum), pgmnum)) + return self.parse_prgm_data(pdata) + + def read_prgm_data_step(self, pgmnum, pgmstep): + ''' + get a programs step parameters + + Args: + pgmnum: int, the program to read from + pgmstep: int, the step to read from + returns: + { + "number":int, + "time":{"hour":int, "minute":int}, + "paused":boolean, + "granty":boolean, + "refrig":{"mode":string, "setpoint":int}, + "temperature":{"setpoint":float, "ramp":boolean}, + "humidity":{"setpoint":float, "enable":boolean, "ramp":boolean}, + "relay":[int], + "air":{"selected":int, "options":[int]} + } + ''' + cmd = 'PRGM DATA?,%s:%d,STEP%d' + if self.enable_air_speed: + cmd += ',AIR' + tmp = self.ctlr.interact(cmd % (self.rom_pgm(pgmnum), pgmnum, pgmstep)) + return self.parse_prgm_data_step(tmp) + + def read_prgm_data_ptc(self, pgmnum): + ''' + get the parameters for a given program that includes ptc + + Args: + pgmnum: int, the program to get + returns: + { + "steps":int, + "name":string, + "end":string, + "counter_a":{"start":int, "end":int, "cycles":int}, + "counter_b":{"start":int, "end":int, "cycles":int} + } + "END"="OFF" or "CONSTANT1" or "CONSTANT2" or "CONSTANT3" or "STANDBY" or "RUN" + ''' + pdata = self.ctlr.interact('PRGM DATA PTC?,%s:%d,CONSTANT' % (self.rom_pgm(pgmnum), pgmnum)) + return self.parse_prgm_data(pdata) + + def read_prgm_data_ptc_step(self, pgmnum, pgmstep): + ''' + get a programs step parameters including ptc + + Args: + pgmnum: int, the program to read from + pgmstep: int, the step to read from + returns: + { + "number":int, + "time":{"hour":int, "minute":int}, + "paused":boolean, + "granty":boolean, + "refrig":{"mode":string, "setpoint":int}, + "temperature":{ + "setpoint":float, + "ramp":boolean, + "enable_cascade":boolean, + "deviation":{"positive":float, "negative":float} + }, + "humidity":{ + "setpoint":float, + "enable":boolean, + "ramp":boolean + }, + "relay":[int], + "air":{"selected":int, "options":[int]} + } + ''' + cmd = 'PRGM DATA PTC?,%s:%d,STEP%d' + if self.enable_air_speed: + cmd += ',AIR' + tmp = self.ctlr.interact(cmd % (self.rom_pgm(pgmnum), pgmnum, pgmstep)) + return self.parse_prgm_data_step(tmp) + + def read_run_prgm(self): + ''' + get the settings for the remote program being run + + returns: + { + "temperature":{"start":float,"end":float},"humidity":{"start":float,"end":float}, + "time":{"hours":int,"minutes":int},"refrig":{"mode":string,"setpoint":} + } + ''' + rsp = self.ctlr.interact('RUN PRGM?' + ',AIR' if self.enable_air_speed else '') + parsed = re.search( + r'TEMP([0-9.-]+) GOTEMP([0-9.-]+)(?: HUMI(\d+) GOHUMI(\d+))? TIME(\d+):(\d+) (\w+)' + r'(?: RELAYON,([0-9,]+))?(?: AIR(\d+)\/(\d+))?', + rsp + ) + ret = { + 'temperature':{'start':float(parsed.group(1)), 'end':float(parsed.group(2))}, + 'time':{'hours':int(parsed.group(5)), 'minutes':int(parsed.group(6))}, + 'refrig':self.reflookup.get(parsed.group(7), {'mode':'manual', 'setpoint':0}) + } + if parsed.group(3): + ret['humidity'] = {'start':float(parsed.group(3)), 'end':float(parsed.group(4))} + if parsed.group(8): + relays = parsed.group(8).split(',') + ret['relay'] = [str(i) in relays for i in range(1, 13)] + else: + ret['relay'] = [False for i in range(1, 13)] + if parsed.group(9): + ret['air'] = { + 'selected':int(parsed.group(9)), + 'options':range(1, int(parsed.group(10))+1) + } + + def read_timer_list_quick(self): + ''' + Read the timer settings for the quick timer(timer 0) + + returns: + {"mode":string, "time":{"hour":int, "minute":int}, "pgmnum":int, "pgmstep":int} + "mode"="STANDBY" or "OFF" or "CONSTANT1" or "CONSTANT2" or "CONSTANT3" or "RUN" + "pgmnum" and "pgmstep" only present when mode=="RUN" + ''' + parsed = re.search( + r"(\w+)(?:,R[AO]M:(\d+),STEP(\d+))?,(\d+):(\d+)", + self.ctlr.interact('TIMER LIST?,0,CONSTANT') + ) + ret = { + 'mode':parsed.group(1), + 'time':{'hour':int(parsed.group(4)), 'minute':int(parsed.group(5))} + } + if parsed.group(1) == 'RUN': + ret.update({'pgmnum':int(parsed.group(2)), 'pgmstep':int(parsed.group(3))}) + return ret + + def read_timer_list_start(self): + ''' + Read the timer settings for the start timer (timer 1) + + returns: + { + "repeat":string, + "time":{"hour":int, "minute":int}, + "mode":string", + "date":{"month":int, "day":int, "year":int}, + "day":string, + "pgmnum":int, + "pgmstep":int + } + "repeat"="once" or "weekly" or "daily" + "mode"="CONSTANT1" or "CONSTANT2" or "CONSTANT3" or "RUN" + "date" only present when "repeat"=="once" + "pgmnum" and "step" only present when "mode"=="RUN" + "days" only present when "repeat"=="weekly" + ''' + parsed = re.search( + r"1,MODE(\d)(?:,(\d+).(\d+)/(\d+))?(?:,([A-Z/]+))?,(\d+):(\d+),(\w+[123])", + self.ctlr.interact('TIMER LIST?,1,CONSTANT') + ) + ret = { + 'repeat':['once', 'weekly', 'daily'][int(parsed.group(1))-1], + 'time':{'hour':int(parsed.group(6)), 'minute':int(parsed.group(7))}, + 'mode':parsed.group(8) + } + if parsed.group(2): + ret['date'] = { + 'year':2000+int(parsed.group(2)), + 'month':int(parsed.group(3)), + 'day':int(parsed.group(4)) + } + if parsed.group(5): + ret['days'] = parsed.group(5).split('/') + if parsed.group(9): + ret.update({'pgmnum':int(parsed.group(9)), 'pgmstep':int(parsed.group(10))}) + return ret + + def write_mode_constant(self, constant=1): + ''' + Run a constant setpoint + + Args: + constant: int, the constant mode to write to; valid range of 1 to 3; 1 is default + ''' + if constant in [1, 2, 3]: + self.ctlr.interact('MODE,CONSTANT,C%d' % constant) + else: + raise ValueError("Constant must be None or 1, 2 or 3") + + def write_temp(self, **kwargs): + ''' + update the temperature parameters + + Args: + setpoint: float + max: float + min: float + range: {"max":float, "min":float} + constant: int, the constant mode to write to; valid range of 1 to 3; 1 is default + ''' + setpoint, maximum, minimum = kwargs.get('setpoint'), kwargs.get('max'), kwargs.get('min') + constant = kwargs.get('constant', 1) + if setpoint is not None and minimum is not None and maximum is not None: + cmd = 'TEMP, S%0.1f H%0.1f L%0.1f, C%d' + self.ctlr.interact(cmd % (setpoint, maximum, minimum, constant)) + else: + if setpoint is not None: + self.ctlr.interact('TEMP, S%0.1f, C%d' % (setpoint, constant)) + if minimum is not None: + self.ctlr.interact('TEMP, L%0.1f, C%d' % (minimum, constant)) + if maximum is not None: + self.ctlr.interact('TEMP, H%0.1f, C%d' % (maximum, constant)) + + def write_humi(self, **kwargs): + ''' + update the humidity parameters + + Args: + enable: boolean + setpoint: float + max: float + min: float + range: {"max":float,"min":float} + constant: int, the constant mode to write to; valid range of 1 to 3; 1 is default + ''' + setpoint, maximum, minimum = kwargs.get('setpoint'), kwargs.get('max'), kwargs.get('min') + enable, constant = kwargs.get('enable'), kwargs.get('constant', 1) + if enable is False: + spstr = 'SOFF' + elif setpoint is not None: + spstr = ' S%0.1f' % setpoint + else: + spstr = None + if spstr is not None and minimum is not None and maximum is not None: + self.ctlr.interact('HUMI,%s H%0.1f L%0.1f, C%d' % (spstr, maximum, minimum, constant)) + else: + if spstr is not None: + self.ctlr.interact('HUMI,%s, C%d' % (spstr, constant)) + if minimum is not None: + self.ctlr.interact('HUMI, L%0.1f, C%d' % (minimum, constant)) + if maximum is not None: + self.ctlr.interact('HUMI, H%0.1f, C%d' % (maximum, constant)) + + def write_relay(self, relays, constant=1): + ''' + set each relay(time signal) + + Args: + relays: [boolean] True=turn relay on, False=turn relay off, None=do nothing + constant: int, the constant mode to write to; valid range of 1 to 3; 1 is default + ''' + vals = self.parse_relays(relays) + cmd = 'RELAY,%s,%s,C%d' + if len(vals['on']) > 0: + self.ctlr.interact(cmd % ('ON', ','.join(str(v) for v in vals['on']), constant)) + if len(vals['off']) > 0: + self.ctlr.interact(cmd % ('OFF', ','.join(str(v) for v in vals['off']), constant)) + + def write_set(self, mode, setpoint=0, constant=1): + ''' + Set the constant setpoints refrig mode + + Args: + mode: string,"off" or "manual" or "auto" + setpoint: int,20 or 50 or 100 + constant: int, the constant mode to write to; valid range of 1 to 3; 1 is default + ''' + self.ctlr.interact('SET,%s,C%d' % (self.encode_refrig(mode, setpoint), constant)) + + def write_temp_ptc(self, enable, positive, negative, constant=1): + ''' + set product temperature control settings + + Args: + enable: boolean, True(on)/False(off) + positive: float, maximum positive deviation + negative: float, maximum negative deviation + constant: int, the constant mode to write to; valid range of 1 to 3; 1 is default + ''' + ttp = ('ON' if enable else 'OFF', positive, negative, constant) + self.ctlr.interact('TEMP PTC, PTC%s, DEVP%0.1f, DEVN%0.1f, C%d' % ttp) + + def write_air(self, value, constant=1): + ''' + Set the selected air speed value and the options values for the specified constant mode. + + Args: + constant: int, the constant mode to write to; valid range of 1 to 3; 1 is default + value: int, the selected air speed read_air()["options"] will give the allowable values + ''' + if constant in [1, 2, 3]: + self.ctlr.interact('AIR,%d,C%d' % (value, constant)) + else: + raise ValueError("Constant must be None or 1, 2, 3") + + def write_prgm_data_step(self, pgmnum, **pgmstep): + ''' + write a program step to the controller + + Args: + pgmnum: int, the program being written/edited + pgmstep: the program parameters, see read_prgm_data_step for parameters + ''' + cmd = 'PRGM DATA WRITE,PGM%d,STEP%d' % (pgmnum, pgmstep['number']) + if 'temperature' in pgmstep: + if 'setpoint' in pgmstep['temperature']: + cmd = '%s,TEMP%0.1f' % (cmd, pgmstep['temperature']['setpoint']) + if 'ramp' in pgmstep['temperature']: + cmd = '%s,TRAMP%s' % (cmd, 'ON' if pgmstep['temperature']['ramp'] else 'OFF') + if 'enable_cascade' in pgmstep['temperature']: + cmd = '%s,PTC%s'%(cmd, 'ON' if pgmstep['temperature']['enable_cascade'] else 'OFF') + if 'deviation' in pgmstep['temperature']: + ttp = (cmd, pgmstep['temperature']['deviation']['positive'], + pgmstep['temperature']['deviation']['negative']) + cmd = '%s,DEVP%0.1f,DEVN%0.1f' % ttp + if 'humidity' in pgmstep: + if 'setpoint' in pgmstep['humidity']: + if pgmstep['humidity']['enable']: + htmp = '%0.0f' % pgmstep['humidity']['setpoint'] + else: + htmp = 'OFF' + cmd = '%s,HUMI%s' % (cmd, htmp) + if 'ramp' in pgmstep['humidity'] and pgmstep['humidity']['enable']: + cmd = '%s,HRAMP%s' % (cmd, 'ON' if pgmstep['humidity']['ramp'] else 'OFF') + if 'time' in pgmstep: + cmd = '%s,TIME%d:%d' % (cmd, pgmstep['time']['hour'], pgmstep['time']['minute']) + if 'granty' in pgmstep: + cmd = '%s,GRANTY %s' % (cmd, 'ON' if pgmstep['granty'] else 'OFF') + if 'paused' in pgmstep: + cmd = '%s,PAUSE %s' % (cmd, 'ON' if pgmstep['paused'] else 'OFF') + if 'refrig' in pgmstep: + cmd = '%s,%s' % (cmd, self.encode_refrig(**pgmstep['refrig'])) + if 'relay' in pgmstep: + rlys = self.parse_relays(pgmstep['relay']) + if rlys['on']: + cmd = '%s,RELAY ON%s' % (cmd, '.'.join(str(v) for v in rlys['on'])) + if rlys['off']: + cmd = '%s,RELAY OFF%s' % (cmd, '.'.join(str(v) for v in rlys['off'])) + if 'air' in pgmstep: + if isinstance(pgmstep['air'], dict): + cmd = '%s,AIR%d' % (cmd, pgmstep['air']['selected']) + else: + cmd = '%s,AIR%d' % (cmd, pgmstep['air']) + self.ctlr.interact(cmd) + + def write_run_prgm(self, temp, hour, minute, gotemp=None, humi=None, gohumi=None, relays=None, air=None): + ''' + Run a remote program (single step program) + + Args: + temp: float, temperature to use at the start of the step + hour: int, # of hours to run the step + minute: int, # of minutes to run the step + gotemp: float, temperature to end the step at(optional for ramping) + humi: float, the humidity to use at the start of the step (optional) + gohumi: float, the humidity to end the steap at (optional for ramping) + relays: [boolean], True= turn relay on, False=turn relay off, None=Do nothing + air: int, selector for the air speed. + ''' + cmd = 'RUN PRGM, TEMP%0.1f TIME%d:%d' % (temp, hour, minute) + if gotemp is not None: + cmd = '%s GOTEMP%0.1f' % (cmd, gotemp) + if humi is not None: + cmd = '%s HUMI%0.0f' % (cmd, humi) + if gohumi is not None: + cmd = '%s GOHUMI%0.0f' % (cmd, gohumi) + rlys = self.parse_relays(relays) if relays is not None else {'on':None, 'off':None} + if rlys['on']: + cmd = '%s RELAYON,%s' % (cmd, ','.join(str(v) for v in rlys['on'])) + if rlys['off']: + cmd = '%s RELAYOFF,%s' % (cmd, ','.join(str(v) for v in rlys['off'])) + if air is not None: + cmd = '%s AIR%d' % (cmd, air) + self.ctlr.interact(cmd) + + def write_prgm_end(self, mode="STANDBY"): + ''' + stop the running program + + Args: + mode: string, vaid options: "HOLD"/"CONST"/"CONST1"/"CONST2"/"CONST3"/"OFF"/"STANDBY" + ''' + if mode in ["HOLD", "CONST", "CONST1", "CONST2", "CONST3", "OFF", "STANDBY"]: + self.ctlr.interact('PRGM,END,%s' % mode) + else: + raise ValueError('"mode" must be "HOLD"/"CONST"/"CONST1"/"CONST2"/"CONST3"/"OFF"/"STANDBY"') + + # --- helpers etc --- helpers etc --- helpers etc --- helpers etc -- helpers etc -- helpers etc + def parse_prgm_data_step(self, arg): + ''' + Parse a program step + ''' + parsed = re.search( + r'(\d+),TEMP([0-9.-]+),TEMP RAMP (\w+)(?:,PTC (\w+))?(?:,HUMI([^,]+)' + r'(?:,HUMI RAMP (\w+))?)?,TIME(\d+):(\d+),GRANTY (\w+),REF(\w+)' + r'(?:,RELAY ON([0-9.]+))?(?:,PAUSE (\w+))?(?:,DEVP([0-9.-]+),DEVN([0-9.-]+))?' + r'(?:,AIR(\d+)\/(\d+))?', + arg + ) + base = { + 'number':int(parsed.group(1)), + 'time':{ + 'hour':int(parsed.group(7)), + 'minute':int(parsed.group(8)) + }, + 'paused':parsed.group(12) == 'ON', + 'granty':parsed.group(9) == 'ON', + 'refrig':self.reflookup.get( + 'REF' + parsed.group(10), + {'mode':'manual', 'setpoint':0} + ), + 'temperature':{ + 'setpoint':float(parsed.group(2)), + 'ramp':parsed.group(3) == 'ON' + } + } + if parsed.group(5): # humidity settings + base['humidity'] = { + 'setpoint':tryfloat(parsed.group(5), 0.0), + 'enable':parsed.group(5) != ' OFF', + 'ramp':parsed.group(6) == 'ON' + } + if parsed.group(4): # ptcon settings + base['temperature'].update({ + 'enable_cascade':parsed.group(4) == 'ON', + 'deviation': { + 'positive':float(parsed.group(13)), + 'negative':float(parsed.group(14)) + } + }) + if parsed.group(11): # relay settings + relays = parsed.group(11).split('.') + base['relay'] = [str(i) in relays for i in range(1, 13)] + else: + base['relay'] = [False for i in range(1, 13)] + if parsed.group(15): # air speed settings + base['air'] = { + 'selected':int(parsed.group(15)), + 'options':range(1, int(parsed.group(16))+1) + } + return base diff --git a/chamberconnectlibrary/p300vib.py b/chamberconnectlibrary/p300vib.py new file mode 100644 index 0000000..d0012a6 --- /dev/null +++ b/chamberconnectlibrary/p300vib.py @@ -0,0 +1,481 @@ +''' +A direct implementation of the SCP220's communication interface. + +:copyright: (C) Espec North America, INC. +:license: MIT, see LICENSE for more details. +''' +import re +import time +from especinteract import EspecError +from p300extended import P300Extended, tryfloat + +class P300Vib(P300Extended): + ''' + This is the basic implementation for communications with the P300 + with vibration feature. + + Most of its standard features are inherited from the superclass, standard P300. + + Args: + interface (str): The interface type to connect to: "Serial" or "TCP" + Kwargs: + serialport (str/int): The serial port to connect to when interface="Serial" + baudrate (int): The baud rate to connect at when interface="Serial" + address (int): The RS485 address of the chamber to connect to. + host (str): The IP address or hostname of the chamber when interface="TCP" + ''' + + def read_vib(self): + ''' + Read and return vibration values + + returns: + { 'processvalue': float, + 'setpoint': float, + 'enable': boolean, + 'range': {'max': float, 'min': float} + } + ''' + rsp = self.ctlr.interact('VIB?').split(',') + try: + hsp = float(rsp[1]) + enable = True + except Exception: + hsp = 0 + enable = False + return { + 'processvalue': float(rsp[0]), + 'setpoint': hsp, + 'enable': enable, + 'range': { 'max': float(rsp[2]), 'min': float(rsp[3]) } + } + + def read_mon(self, detail=False, constant=False): + ''' + Returns the conditions inside the chamber + + Args: + detail: boolean, when True "mode" parameter has additional details + returns: + {"temperature":float,"vibration":float,"mode":string,"alarms":int} + "vibration": only present if chamber has vibration + "mode": see read_mode for valid parameters (with and without detail flag). + ''' + if constant: + rsp = self.ctlr.interact('MON?,DETAIL,CONSTANT').split(',') + else: + rsp = self.ctlr.interact('MON?%s%s' % (',DETAIL' if detail else '', ',CONSTANT' if constant else '')).split(',') + data = {'temperature':float(rsp[0]), 'mode':rsp[2], 'alarms':int(rsp[3])} + + rsp = self.ctlr.interact('MON?,EXT1').split(',') + if rsp[1]: + data['vibration'] = float(rsp[1]) + return data + + def read_htr(self): + ''' + Read the heater outputs and number of controllable heaters. + + returns: + { + 'dry': float, + 'vib': float, + 'vib' is only present with vibration chambers + } + ''' + rsp = self.ctlr.interact('%?,EXT1').split(',') + if len(rsp) == 3: + return { + 'dry': float(rsp[1]), + 'vib': float(rsp[2]) + } + else: + return { 'dry': float(rsp[1]) } + + def read_constant_vib(self, constant=1): + ''' + Read the constant settings for vibration loop. + + returns: + { + 'setpoint': float, + 'enable': string + } + ''' + if constant in [1, 2, 3]: + rsp = self.ctlr.interact('CONSTANT SET?, VIB, C{0:d}'.format(constant)).split(',') + else: + raise ValueError("Constant must be None or 1, 2, 3.") + return {'setpoint': float(rsp[0]), 'enable': rsp[1] == 'ON'} + + def read_prgm_mon(self): + ''' + Read status of running program + + Parameters: + 'pgmstep': int, + 'temperature': float, + 'vibration': float, + 'time':{'hour':int, 'minute':int}, + 'counter_a': int, + 'counter_b': int + + Note: 'humidity' is only available on chambers having that feature. + This means that for chambers with temperature and humidity, return + parameters will be six. Chambers without humidity will return + five parameters. Thus, two types of chambers are those with + temperature and vibration, temperature and humidity. + ''' + rsp = self.ctlr.interact('PRGM MON?,EXT1').split(',') + if len(rsp) == 6: + time = rsp[3].split(':') + return { + 'pgmstep': int(rsp[0]), + 'temperature': float(rsp[1]), + 'vibration': tryfloat(rsp[2], 0), + 'time': { 'hour': int(time[0]), 'minute': int(time[1]) }, + 'counter_a': int(rsp[4]), + 'counter_b': int(rsp[5]) + } + else: + time = rsp[2].split(':') + return { + 'pgmstep': int(rsp[0]), + 'temperature': float(rsp[1]), + 'time': { 'hour': int(time[0]), 'minute': int(time[1]) }, + 'counter_a': int(rsp[3]), + 'counter_b': int(rsp[4]) + } + + def read_prgm_data_step(self, pgmnum, pgmstep): + ''' + get a programs step parameters and air feature + + Args: + pgmnum: int, the program to read from + pgmstep: int, the step to read from + returns: + { + "number":int, + "time":{"hour":int, "minute":int}, + "paused":boolean, + "granty":boolean, + "refrig":{"mode":string, "setpoint":int}, + "temperature":{"setpoint":float, "ramp":boolean}, + "vibration":{"setpoint":float, "enable":boolean, "ramp":boolean}, + "relay":[int] + } + ''' + cmd = 'PRGM DATA?,{0}:{1:d},STEP{2:d}'.format(self.rom_pgm(pgmnum), pgmnum, pgmstep) + rtrn = self.parse_prgm_data_step(self.ctlr.interact(cmd + ',EXT1')) + if self.enable_air_speed: + rtrn['air'] = self.parse_prgm_data_step(self.ctlr.interact(cmd + ',AIR'))['air'] + return rtrn + + def read_prgm_data_ptc_step(self, pgmnum, pgmstep): + data = self.read_prgm_data_step(pgmnum, pgmstep) + data.update(super(P300Vib, self).read_prgm_data_ptc_step(pgmnum, pgmstep)) + return data + + def parse_prgm_data_step(self, arg): + ''' + Parse the program parameters with vibration feature + ''' + parsed = re.search( + r'(\d+),TEMP([0-9.-]+),TEMP RAMP (\w+)(?:,PTC (\w+))?(?:,VIB([^,]+)' + r'(?:,VIB RAMP (\w+))?)?,TIME(\d+):(\d+),GRANTY (\w+),REF(\w+)' + r'(?:,RELAY ON([0-9.]+))?(?:,PAUSE (\w+))?(?:,DEVP([0-9.-]+),DEVN([0-9.-]+))?' + r'(?:,AIR(\d+)\/(\d+))?', + arg + ) + base = {'number':int(parsed.group(1)), + 'time':{'hour':int(parsed.group(7)), + 'minute':int(parsed.group(8))}, + 'paused':parsed.group(12) == 'ON', + 'granty':parsed.group(9) == 'ON', + 'refrig':self.reflookup.get( + 'REF' + parsed.group(10), + {'mode':'manual', 'setpoint':0} + ), + 'temperature':{'setpoint':float(parsed.group(2)), + 'ramp':parsed.group(3) == 'ON'}} + if parsed.group(5): + base['vibration'] = { + 'setpoint':tryfloat(parsed.group(5), 0.0), + 'enable':parsed.group(5) != ' OFF', + 'ramp':parsed.group(6) == 'ON' + } + if parsed.group(4): + base['temperature'].update({ + 'enable_cascade':parsed.group(4) == 'ON', + 'deviation': { + 'positive':float(parsed.group(13)), + 'negative':float(parsed.group(14)) + } + }) + if parsed.group(11): + relays = parsed.group(11).split('.') + base['relay'] = [str(i) in relays for i in range(1, 13)] + else: + base['relay'] = [False for i in range(1, 13)] + if parsed.group(15): # air speed settings + base['air'] = { + 'selected':int(parsed.group(15)), + 'options':range(1, int(parsed.group(16))+1) + } + return base + + def read_prgm_data_detail(self, pgmnum): + ''' + get the conditions a program will start with and its operational range + + Args: + pgmnum: int, the program to get + returns: + { + "temperature":{"range":{"max":float, "min":float},"mode":string,"setpoint":float}, + "vibration":{"range":{"max":float,"min":float},"mode":string,"setpoint":float} + } + ''' + pdata = self.ctlr.interact('PRGM DATA?,{0}:{1:d},DETAIL,EXT1'.format( self.rom_pgm(pgmnum), + pgmnum)) + return self.parse_prgm_data_detail(pdata) # need to write parse def + + def read_prgm_data_ptc_detail(self, pgmnum): + return self.read_prgm_data_detail(pgmnum) + + def parse_prgm_data_detail(self, arg): + ''' + Parse the program data command with details flag + ''' + parsed = re.search( + r'([0-9.-]+),([0-9.-]+),([0-9.-]+),([0-9.-]+),TEMP(\w+)' + r'(?:,([0-9.-]+))?,VIB(\w+)(?:,([0-9.-]+))?', + arg + ) + + ret = { + 'tempDetail':{ + 'range':{'max':float(parsed.group(1)), 'min':float(parsed.group(2))}, + 'mode':parsed.group(5), + 'setpoint':parsed.group(6) + } + } + if parsed.group(3): + ret['vibDetail'] = { + 'range':{'max':float(parsed.group(3)), 'min':float(parsed.group(4))}, + 'mode':parsed.group(7), + 'setpoint':parsed.group(8) + } + return ret + + def write_prgm_data_details(self, pgmnum, **pgmdetail): + ''' + write the various program wide parameters to the controller + + Args: + pgmnum: int, the program being written or edited + pgmdetail: the program details see write_prgmDataDetail for parameters + ''' + if 'counter_a' in pgmdetail and pgmdetail['counter_a']['cycles'] > 0: + ttp = (pgmnum, pgmdetail['counter_a']['start'], pgmdetail['counter_a']['end'], + pgmdetail['counter_a']['cycles']) + tmp = ('PRGM DATA WRITE,PGM{0:d},COUNT,A({1:d}.{2:d}.{3:d})'.format(*ttp)) + if 'counter_b' in pgmdetail and pgmdetail['counter_b']['cycles'] > 0: + ttp = (tmp, pgmdetail['counter_b']['start'], pgmdetail['counter_b']['end'], + pgmdetail['counter_b']['cycles']) + #tmp = '%s,B(%d.%d.%d)' % ttp + tmp = ('{0:s},B({1:d}.{2:d}.{3:d})'.format(*ttp)) + self.ctlr.interact(tmp) + elif 'counter_b' in pgmdetail and pgmdetail['counter_b']['cycles'] > 0: + ttp = (pgmnum, pgmdetail['counter_b']['start'], pgmdetail['counter_b']['end'], + pgmdetail['counter_b']['cycles']) + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},COUNT,B({1:d}.{2:d}.{3:d})'.format(*ttp)) + if 'name' in pgmdetail: + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},NAME,{1:s}'.format(pgmnum, pgmdetail['name'])) + + if 'end' in pgmdetail: + if pgmdetail['end'] != 'RUN': + ttp = (pgmnum, pgmdetail['end']) + else: + ttp = (pgmnum, 'RUN,PTN{0:d}'.format(pgmdetail['next_prgm'])) + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},END,{1:s}'.format(*ttp)) + + if 'tempDetail' in pgmdetail: + if 'range' in pgmdetail['tempDetail']: + ttp = (pgmnum, pgmdetail['tempDetail']['range']['max']) + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},HTEMP,{1:0.1f}'.format(*ttp)) + ttp = (pgmnum, pgmdetail['tempDetail']['range']['min']) + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},LTEMP,{1:0.1f}'.format(*ttp)) + if 'mode' in pgmdetail['tempDetail']: + ttp = (pgmnum, pgmdetail['tempDetail']['mode']) + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},PRE MODE,TEMP,{1:s}'.format(*ttp)) + if 'setpoint' in pgmdetail['tempDetail'] and pgmdetail['tempDetail']['mode'] == 'SV': + ttp = (pgmnum, pgmdetail['tempDetail']['setpoint']) + (td, tsetp) = ttp # splitting a tuple of mixed data type into individual arguments + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},PRE TSV,{1:0.1f}'.format(td, float(tsetp)) ) + # self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},PRE TSV,{1:0.1f}'.format(*ttp)) + + if 'vibDetail' in pgmdetail: + if 'range' in pgmdetail['vibDetail']: + ttp = (pgmnum, pgmdetail['vibDetail']['range']['max']) + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},HVIB,{1:0.1f}'.format(*ttp)) + ttp = (pgmnum, pgmdetail['vibDetail']['range']['min']) + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},LVIB,{1:0.1f}'.format(*ttp)) + if 'mode' in pgmdetail['vibDetail']: + ttp = (pgmnum, pgmdetail['vibDetail']['mode']) + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},PRE MODE,VIB,{1:s}'.format(*ttp)) + if 'setpoint' in pgmdetail['vibDetail'] and pgmdetail['vibDetail']['mode'] == 'SV': + ttp = (pgmnum, pgmdetail['vibDetail']['setpoint']) + (vd, vsetp) = ttp # splitting a tuple of mixed data type into individual arguments + self.ctlr.interact('PRGM DATA WRITE,PGM{0:d},PRE VSV,{1:0.1f}'.format(vd, float(vsetp)) ) + + def write_prgm_data_step(self, pgmnum, **pgmstep): + ''' + write a program step includiong vibration feature to the controller + + Args: + pgmnum: int, the program being written/edited + pgmstep: the program parameters, see read_prgm_data_step for parameters + ''' + cmd = ('PRGM DATA WRITE,PGM{0:d},STEP{1:d}'.format(pgmnum, pgmstep['number'])) + if 'time' in pgmstep: + cmd = '{0:s},TIME{1:d}:{2:d}'.format(cmd, pgmstep['time']['hour'], pgmstep['time']['minute']) + if 'paused' in pgmstep: + cmd = '{0:s},PAUSE {1:s}'.format(cmd, 'ON' if pgmstep['paused'] else 'OFF') + if 'air' in pgmstep: + if isinstance (pgmstep['air'], dict): # added to test... + cmd = '{0:s},AIR{1:d}'.format(cmd, pgmstep['air']['selected']) + else: + cmd = '{0:s},AIR{1:d}'.format(cmd, pgmstep['air']) + if 'refrig' in pgmstep: + cmd = '{0:s},{1:s}'.format(cmd, self.encode_refrig(**pgmstep['refrig'])) + if 'granty' in pgmstep: + cmd = '{0:s},GRANTY {1:s}'.format(cmd, 'ON' if pgmstep['granty'] else 'OFF') + if 'temperature' in pgmstep: + if 'setpoint' in pgmstep['temperature']: + cmd = '{0:s},TEMP{1:0.1f}'.format(cmd, pgmstep['temperature']['setpoint']) + if 'ramp' in pgmstep['temperature']: + cmd = ('{0:s},TRAMP{1:s}' + .format(cmd, 'ON' if pgmstep['temperature']['ramp'] else 'OFF')) + if 'enable_cascade' in pgmstep['temperature']: + cmd = ('{0:s},PTC{1:s}' + .format(cmd, 'ON' if pgmstep['temperature']['enable_cascade'] else 'OFF')) + if 'deviation' in pgmstep['temperature']: + ttp = (cmd, pgmstep['temperature']['deviation']['positive'], + pgmstep['temperature']['deviation']['negative']) + cmd = '{0:s},DEVP{1:0.1f},DEVN{2:0.1f}'.format(*ttp) + + if 'vibration' in pgmstep: + if 'setpoint' in pgmstep['vibration']: + if pgmstep['vibration']['enable']: + vtmp = '{0:0.1f}'.format(pgmstep['vibration']['setpoint']) + else: + vtmp = 'OFF' + cmd = '{0:s},VIB{1:s}'.format(cmd, vtmp) + if 'ramp' in pgmstep['vibration'] and pgmstep['vibration']['enable']: + cmd = '{0:s},VRAMP{1:s}'.format(cmd, 'ON' if pgmstep['vibration']['ramp'] else 'OFF') + + if 'relay' in pgmstep: + rlys = self.parse_relays(pgmstep['relay']) + if rlys['on']: + cmd = '{0:s},RELAY ON{1:s}'.format(cmd, '.'.join(str(v) for v in rlys['on'])) + if rlys['off']: + cmd = '{0:s},RELAY OFF{1:s}'.format(cmd, '.'.join(str(v) for v in rlys['off'])) + self.ctlr.interact(cmd) + + def write_prgm_erase(self, pgmnum): + ''' + erase a program + + Args: + pgmnum: int, the program to erase + ''' + self.ctlr.interact('PRGM ERASE,{0:s}:{1:d}'.format(self.rom_pgm(pgmnum), pgmnum)) + + def write_run_prgm(self, temp, hour, minute, gotemp=None, vib=None, govib=None, relays=None, air=None): + ''' + Run a remote program (single step program) + + Args: + temp: float, temperature to use at the start of the step + hour: int, # of hours to run the step + minute: int, # of minutes to run the step + gotemp: float, temperature to end the step at(optional for ramping) + vib: float, the humidity to use at the start of the step (optional) + govib: float, the humidity to end the step at (optional for ramping) + relays: [boolean], True= turn relay on, False=turn relay off, None=Do nothing + air: int, # of air speed to provide to system to operate + ''' + cmd = 'RUN PRGM, TEMP{0:0.1f} TIME{1:d}:{2:d}'.format(temp, hour, minute) + if gotemp is not None: + cmd = '{0:s} GOTEMP{1:0.1f}'.format(cmd, gotemp) + if vib is not None: + cmd = '{0:s} VIB{1:0.1f}'.format(cmd, vib) + if govib is not None: + cmd = '{0:s} GOVIB{1:0.1f}'.format(cmd, govib) + rlys = self.parse_relays(relays) if relays is not None else {'on':None, 'off':None} + if rlys['on']: + cmd = '{0:s} RELAYON,{1:s}'.format(cmd, ','.join(str(v) for v in rlys['on'])) + if rlys['off']: + cmd = '{0:s} RELAYOFF,{1:s}'.format(cmd, ','.join(str(v) for v in rlys['off'])) + if air is not None: + cmd = '{0:s} AIR{1:d}'.format(cmd, air) + self.ctlr.interact(cmd) + + def write_vib(self, **kwargs): + ''' + update the vibration parameters + + Args: + enable: boolean + setpoint: float + max: float + min: float + range: {"max":float,"min":float} + ''' + setpoint, maximum, minimum = kwargs.get('setpoint'), kwargs.get('max'), kwargs.get('min') + enable, constant = kwargs.get('enable'), kwargs.get('constant', 1) + if enable is False: + spstr = 'SOFF' + elif setpoint is not None: + # spstr = ' S%0.1f' % setpoint + spstr = ('S{0:0.1f}'.format(setpoint)) + else: + spstr = None + + try: + time.sleep(0.1) # ensure P300 has had time to update after a wrte_temp_ptc + ptc = self.read_temp_ptc() + except EspecError: + ptc = {'enable_cascade': False} + + if spstr is not None and minimum is not None and maximum is not None: + self.ctlr.interact('VIB, {0:s} H{1:0.1f} L{2:0.1f},C{3:d}'.format(spstr, maximum, minimum, constant)) + else: + if spstr is not None: + self.ctlr.interact('VIB,%s, C%d' % (spstr, constant)) + if minimum is not None: + self.ctlr.interact('VIB, L%0.1f, C%d' % (minimum, constant)) + if maximum is not None: + self.ctlr.interact('VIB, H%0.1f, C%d' % (maximum, constant)) + if ptc['enable_cascade']: + self.write_temp_ptc(ptc['enable_cascade'], ptc['deviation']['positive'], 0-ptc['deviation']['positive'], constant) + + + def read_prgm(self, pgmnum, with_ptc=False): + ''' + read an entire program helper method + ''' + pgm = super(P300Vib, self).read_prgm(pgmnum, with_ptc) + if pgmnum == 0: + try: + pgm['vibDetail'] = { + 'mode':'OFF', + 'setpoint':None, + 'range':self.read_vib()['range'] + } + pgm['steps'][0]['vibration'] = {'enable':False, 'ramp':False, 'setpoint':0.0} + except Exception: + pass + return pgm diff --git a/chamberconnectlibrary/scp220debug.py b/chamberconnectlibrary/scp220debug.py index 79e96b7..c3f4fcb 100644 --- a/chamberconnectlibrary/scp220debug.py +++ b/chamberconnectlibrary/scp220debug.py @@ -7,8 +7,9 @@ import struct import serial from serial import SerialException +from controllerinterface import ControllerInterfaceError -class SCP220DebugError(Exception): +class SCP220DebugError(ControllerInterfaceError): '''generic error''' pass diff --git a/chamberconnectlibrary/watlowf4.py b/chamberconnectlibrary/watlowf4.py index c3854d8..13a0982 100644 --- a/chamberconnectlibrary/watlowf4.py +++ b/chamberconnectlibrary/watlowf4.py @@ -40,6 +40,7 @@ class WatlowF4(ControllerInterface): def __init__(self, **kwargs): self.iwatlow_val_dict, self.client, self.loops, self.cascades = None, None, None, None self.init_common(**kwargs) + self.port = kwargs.get('port', 502) self.cond_event = kwargs.get('cond_event') self.limits = kwargs.get('limits', []) @@ -270,7 +271,7 @@ def connect(self): ''' connect to the controller using the paramters provided on class initialization ''' - if self.interface == "RTU": + if self.interface in ["RTU", "Serial"]: self.client = ModbusRTU( address=self.adr, port=self.serialport, @@ -278,7 +279,7 @@ def connect(self): timeout=10.0 ) else: - self.client = ModbusTCP(self.adr, self.host, timeout=10.0) + self.client = ModbusTCP(self.adr, self.host, self.port, timeout=10.0) def close(self): ''' @@ -542,6 +543,18 @@ def set_event(self, N, value): value = value['constant'] if isinstance(value, dict) else value self.client.write_holding(2000 + 10*(N-1), 1 if value else 0) + @exclusive + def get_air_speed(self): + raise NotImplementedError + + @exclusive + def get_air_speeds(self): + raise NotImplementedError + + @exclusive + def set_air_speed(self, value): + raise NotImplementedError + @exclusive def get_status(self): if len(self.get_alarm_status(exclusive=False)['active']) > 0: diff --git a/chamberconnectlibrary/watlowf4t.py b/chamberconnectlibrary/watlowf4t.py index 00ef132..e0abe35 100644 --- a/chamberconnectlibrary/watlowf4t.py +++ b/chamberconnectlibrary/watlowf4t.py @@ -1,4 +1,4 @@ -''' +''' Upper level interface for the Watlow F4T controller :copyright: (C) Espec North America, INC. @@ -47,6 +47,7 @@ class WatlowF4T(ControllerInterface): def __init__(self, **kwargs): self.iwatlow_val_dict, self.client, self.loops, self.cascades = None, None, None, None self.init_common(**kwargs) + self.port = kwargs.get('port', 502) self.cond_event = kwargs.get('cond_event', 9) self.cond_event_toggle = kwargs.get('cond_event_toggle', False) @@ -172,10 +173,10 @@ def connect(self): ''' connect to the controller using the paramters provided on class initialization ''' - if self.interface == "RTU": + if self.interface in ["RTU", "Serial"]: self.client = ModbusRTU(address=self.adr, port=self.serialport, baud=self.baudrate) else: - self.client = ModbusTCP(self.adr, self.host) + self.client = ModbusTCP(self.adr, self.host, self.port) def close(self): ''' @@ -523,7 +524,17 @@ def set_event(self, N, value): else: self.client.write_holding(kpress, self.inv_watlow_val_dict('up')) + @exclusive + def get_air_speed(self): + raise NotImplementedError + + @exclusive + def get_air_speeds(self): + raise NotImplementedError + @exclusive + def set_air_speed(self, value): + raise NotImplementedError @exclusive def get_status(self): @@ -537,13 +548,12 @@ def get_status(self): return "Constant (Program Calendar Start)" else: return "Standby (Program Calendar Start)" - elif not self.get_alarm_status(exclusive=False)['active']: - if self.__read_io(self.run_module, self.run_io, exclusive=False): - return "Constant" - else: - return "Standby" - else: + elif self.__read_io(self.run_module, self.run_io, exclusive=False): + return "Constant" + elif self.get_alarm_status(exclusive=False)['active']: return "Alarm" + else: + return "Standby" @exclusive def get_alarm_status(self): @@ -999,7 +1009,7 @@ def __profile_units(self, num): try: tlist = ['absoluteTemperature', 'relativeTemperature', 'notsourced'] if self.watlow_val_dict[profpv] in tlist: - tval = self.client.read_holding(14080 if self.interface == "RTU" else 6730, 1)[0] + tval = self.client.read_holding(14080 if self.interface in ["RTU", "Serial"] else 6730, 1)[0] return u'\xb0%s' % self.watlow_val_dict[tval] else: return u'%s' % self.watlow_val_dict[profpv] @@ -1013,7 +1023,7 @@ def __get_prgm_empty(self): haswaits = self.waits[0] != '' or self.waits[1] != '' haswaits = haswaits or self.waits[2] != '' or self.waits[3] != '' gsd, ldata, evd, wdata = [], [], [], [] - for _ in range(self.loops): + for _ in range(self.loops + self.cascades): gsd.append({'value':3.0}) for j in range(self.loops + self.cascades): lpdata = {'target':0, 'rate':0, 'mode':'', 'gsoak': False, 'cascade': False, diff --git a/setup.py b/setup.py index 9042a0b..1d970d5 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ def readme(): setup( name='chamberconnectlibrary', - version='2.2.5', + version='2.3.3', description='A library for interfacing with Espec North America chambers', long_description=readme(), url='https://github.com/EspecNorthAmerica/ChamberConnectLibrary',