diff --git a/frappy/client/__init__.py b/frappy/client/__init__.py index 0d4ddfc..1904798 100644 --- a/frappy/client/__init__.py +++ b/frappy/client/__init__.py @@ -391,16 +391,17 @@ class SecopClient(ProxyClient): else: module_param = self.internal.get(f'{ident}:value', None) if module_param is not None: + now = time.time() if action.startswith(ERRORPREFIX): - timestamp = data[2].get('t', None) + timestamp = data[2].get('t', now) readerror = frappy.errors.make_secop_error(*data[0:2]) value = None else: - timestamp = data[1].get('t', None) + timestamp = data[1].get('t', now) value = data[0] readerror = None module, param = module_param - timestamp = min(time.time(), timestamp) # no timestamps in the future! + timestamp = min(now, timestamp) # no timestamps in the future! try: self.updateValue(module, param, value, timestamp, readerror) except KeyError: diff --git a/frappy/io.py b/frappy/io.py index 5a59d7c..307fa97 100644 --- a/frappy/io.py +++ b/frappy/io.py @@ -239,7 +239,7 @@ class StringIO(IOBase): a flag to indicate whether the first message should be resent once to avoid data that may still be in the buffer to garble the message''', - datatype=BoolType(), default=False) + datatype=BoolType(), default=True) def _convert_eol(self, value): if isinstance(value, str): diff --git a/frappy/mixins.py b/frappy/mixins.py index d35f09b..c8e56ea 100644 --- a/frappy/mixins.py +++ b/frappy/mixins.py @@ -71,8 +71,11 @@ class HasOutputModule: def initModule(self): super().initModule() - if self.output_module: - self.output_module.register_input(self.name, self.deactivate_control) + try: + if self.output_module: + self.output_module.register_input(self.name, self.deactivate_control) + except Exception: + self.log.info(f'{self.name} has no output module') def set_control_active(self, active): """to be overridden for switching hw control""" diff --git a/frappy/protocol/dispatcher.py b/frappy/protocol/dispatcher.py index cc76225..1de1113 100644 --- a/frappy/protocol/dispatcher.py +++ b/frappy/protocol/dispatcher.py @@ -489,6 +489,9 @@ class Dispatcher: modobj.setRemoteLogging(conn, level) def handle_logging(self, conn, specifier, level): + if specifier == '#': + self.log.handlers[1].setLevel(int(level)) + return LOGGING_REPLY, specifier, level if specifier and specifier != '.': modobj = self._modules[specifier] modobj.setRemoteLogging(conn, level) diff --git a/frappy/protocol/interface/tcp.py b/frappy/protocol/interface/tcp.py index cd39ce2..8f688cb 100644 --- a/frappy/protocol/interface/tcp.py +++ b/frappy/protocol/interface/tcp.py @@ -143,6 +143,7 @@ class TCPRequestHandler(socketserver.BaseRequestHandler): if not data: self.log.error('should not reply empty data!') return + self.log.debug('send %r', data) outdata = encode_msg_frame(*data) with self.send_lock: if self.running: diff --git a/frappy_psi/bkpower.py b/frappy_psi/bkpower.py new file mode 100644 index 0000000..2171ab8 --- /dev/null +++ b/frappy_psi/bkpower.py @@ -0,0 +1,67 @@ +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# Jael Celia Lorenzana +# ***************************************************************************** +"""Powersupply B&K Precision BK168xB""" + +from frappy.core import StringIO, Readable, Parameter, FloatRange, Writable, HasIO, BoolType + +class IO(StringIO): + end_of_line = ('OK\r', '\r') + default_settings = {'baudrate': 9600} + + +class Power(HasIO, Readable): + value = Parameter(datatype=FloatRange(0,300,unit='W')) + + def read_value(self): + reply = self.communicate('GETD') + volt = float(reply[0:4])/100 + current = float(reply[4:8])/100 + return volt*current + + +class Output(HasIO, Writable): + value = Parameter(datatype=FloatRange(0,100,unit='%')) + target = Parameter(datatype=FloatRange(0,100,unit='%')) + maxvolt = Parameter('voltage at 100%',datatype=FloatRange(0,60,unit='V'),default=50,readonly=False) + maxcurrent = Parameter('current at 100%',datatype=FloatRange(0,5,unit='A'),default=2,readonly=False) + output_enable = Parameter('control on/off', BoolType(), readonly=False) + + def initModule(self): + super().initModule() + self.write_output_enable(False) + + def write_target(self, target): + self.write_output_enable(target != 0) + self.communicate(f'VOLT{round(max(8,target*self.maxvolt/10)):03d}') + self.communicate(f'CURR{round(target*self.maxcurrent):03d}') + self.value = target + + def write_output_enable(self, value): + self.communicate(f'SOUT{int(not value)}') + + def write_maxvolt(self, maxvolt): + self.communicate(f'SOVP{round(maxvolt*10):03d}') + + def write_maxcurrent(self, maxcurrent): + self.communicate(f'SOCP{round(maxcurrent*100):03d}') + + def shutdown(self): + self.write_target(0) + diff --git a/frappy_psi/drums.py b/frappy_psi/drums.py new file mode 100644 index 0000000..b06da48 --- /dev/null +++ b/frappy_psi/drums.py @@ -0,0 +1,65 @@ +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""iono pi max relais drums + +for demo purposes +""" + +from frappy.core import Module, Attached, FloatRange, StringType, Writable, Parameter + + +class Drums(Writable): + target = Parameter('drum speed', FloatRange(unit='beats/min')) + left = Attached(Writable) + right = Attached(Writable) + pollinterval = Parameter('drum interval', FloatRange(0, 10, unit='s'), readonly=False) + pattern = Parameter('''pattern + + a string containing: + L,R: left / right relais on + l,r: left / right relais off + blank: wait''', StringType(), readonly=False) + _pos = 0 + _wait = 0 + + def initModule(self): + super().initModule() + self.actions = {'L': self.left, 'R': self.right} + + def write_target(self, target): + self.pollinterval = 60 / max(1, target) + self.value = target + + def doPoll(self): + if not self.target: + return + if self._wait: + self._wait -= 1 + return + if self._pos >= len(self.pattern): + self._pos = 0 + for i, action in enumerate(self.pattern[self._pos:]): + upper = action.upper() + relais = self.actions.get(action.upper()) + if relais: + relais.write_target(upper == action) # True when capital letter + else: + self._wait = int(action) - 1 + self._pos += i + 1 + return diff --git a/frappy_psi/furnace.py b/frappy_psi/furnace.py new file mode 100644 index 0000000..6db7e3f --- /dev/null +++ b/frappy_psi/furnace.py @@ -0,0 +1,49 @@ +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** + +"""interlocks for furnance""" + +import time +from frappy.core import Module, Writable, Attached, Parameter, FloatRange, Readable, BoolType, ERROR, IDLE + +class Interlocks(Module): + input = Attached(Readable, 'the input module') + vacuum = Attached (Readable, 'the vacuum pressure') + wall_T = Attached (Readable, 'the wall temperature') + control = Attached(Module, 'the control module') + relais = Attached(Writable, 'the interlock relais') + wall_limit = Parameter('maximum wall temperature', FloatRange(0, unit='degC'), + default = 50, readonly = False) + vacuum_limit = Parameter('maximum vacuum pressure', FloatRange(0, unit='mbar'), + default = 0.1, readonly = False) + + def doPoll(self): + super().doPoll() + if self.input.status[0] >= ERROR: + self.control.status = self.input.status + elif self.vacuum.value > self.vacuum_limit: + self.control.status = ERROR, 'bad vacuum' + elif self.wall_T.value > self.wall_limit: + self.control.status = ERROR, 'wall overheat' + else: + return + self.control.write_control_active(False) + self.relais.write_target(False) + + \ No newline at end of file diff --git a/frappy_psi/ionopimax.py b/frappy_psi/ionopimax.py new file mode 100644 index 0000000..f43871a --- /dev/null +++ b/frappy_psi/ionopimax.py @@ -0,0 +1,125 @@ +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# Jael Celia Lorenzana +# ***************************************************************************** + +from frappy.core import Readable, Writable, Parameter, BoolType, StringType,\ + FloatRange, Property, TupleOf, ERROR, IDLE +from math import log + + +class Base: + addr = Property('address', StringType()) + + def read(self, addr, scale=None): + with open(f'/sys/class/ionopimax/{self.devclass}/{addr}') as f: + result = f.read() + if scale: + return float(result) / scale + return result + + def write(self, addr, value, scale=None): + value = str(round(value * scale)) if scale else str(value) + with open(f'/sys/class/ionopimax/{self.devclass}/{addr}', 'w') as f: + f.write(value) + + +class DigitalInput(Base, Readable): + value = Parameter('input state', BoolType()) + devclass = 'digital_in' + + def read_value(self): + return self.read(self.addr, 1) + + +class DigitalOutput(DigitalInput, Writable): + target = Parameter('output state', BoolType(), readonly=False) + devclass = 'digital_out' + + def write_target(self, value): + self.write(self.addr, value, 1) + + +class AnalogInput(Base, Readable): + value = Parameter('analog value', FloatRange()) + rawrange = Property('raw range(electronic)', TupleOf(FloatRange(),FloatRange())) + valuerange = Property('value range(physical)', TupleOf(FloatRange(),FloatRange())) + devclass = 'analog_in' + + def read_value(self): + x0, x1 = self.rawrange + y0, y1 = self.valuerange + self.x = self.read(self.addr, self.scale) + return y0 + (y1 - y0) * (self.x - x0) / (x1 - x0) + + +class VoltageInput(AnalogInput): + scale = 1e5 + + def initModule(self): + super().initModule() + self.write(f'{self.addr}_mode','U') + + +class LogVoltageInput(VoltageInput): + + def read_value(self): + x0, x1 = self.rawrange + y0, y1 = self.valuerange + self.x = self.read(self.addr, self.scale) + a = (x1-x0)/log(y1/y0,10) + return 10**((self.x-x1)/a)*y1 + + +class CurrentInput(AnalogInput): + scale = 1e6 + rawrange = (0.004,0.02) + + def initModule(self): + super().initModule() + self.write(f'{self.addr}_mode','U') + + def read_value(self): + result = super().read_value() + if self.x > 0.021: + self.status = ERROR, 'sensor broken' + else: + self.status = IDLE, '' + return result + + +class AnalogOutput(AnalogInput, Writable): + target = Parameter('outputvalue', FloatRange()) + devclass = 'analog_out' + + def write_target(self, value): + x0, x1 = self.rawrange + y0, y1 = self.valuerange + self.write(self.addr, x0 + (x1 - x0) * (value - y0) / (y1 - y0),self.scale) + + +class VoltageOutput(AnalogOutput): + rawrange = (0,10) + scale = 1e3 + + def initModule(self): + super().initModule() + self.write(f'{self.addr}_enabled', '0') + self.write(f'{self.addr}_mode', 'V') + self.write(f'{self.addr}', '0') + self.write(f'{self.addr}_enabled', '1') diff --git a/frappy_psi/ips_mercury.py b/frappy_psi/ips_mercury.py index 467f9eb..045adfd 100644 --- a/frappy_psi/ips_mercury.py +++ b/frappy_psi/ips_mercury.py @@ -28,9 +28,9 @@ from frappy_psi.magfield import Magfield, SimpleMagfield, Status from frappy_psi.mercury import MercuryChannel, off_on, Mapped from frappy.states import Retry -Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3) +Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3, not_found=4) hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set, - RTOZ=Action.run_to_zero, CLMP=Action.clamped) + RTOZ=Action.run_to_zero, CLMP=Action.clamped, NOT_FOUND=Action.not_found) CURRENT_CHECK_SIZE = 2 @@ -161,6 +161,7 @@ class Field(SimpleField, Magfield): if self.switch_heater == self.switch_heater.on: self.__persistent_field = current self.forced_persistent_field = False + self._field_mismatch = False return current pf = self.query('DEV::PSU:SIG:PFLD') if self.__persistent_field is None: diff --git a/frappy_psi/mercury.py b/frappy_psi/mercury.py index 5a8294b..6698bd8 100644 --- a/frappy_psi/mercury.py +++ b/frappy_psi/mercury.py @@ -294,6 +294,7 @@ class HeaterOutput(HasInput, Writable): volt = 0.0 # target voltage _last_target = None _volt_target = None + _resistivity = 10 def read_limit(self): return self.query('DEV::HTR:VLIM') ** 2 / self.resistivity @@ -329,10 +330,15 @@ class HeaterOutput(HasInput, Writable): res = volt / current tol = res * max(max(0.0003, abs(volt - self._volt_target)) / volt, 0.0001 / current, 0.0001) if abs(res - self.resistivity) > tol + 0.07 and self._last_target: - self.write_resistivity(round(res, 1)) - if self.controlled_by == 0: - self._volt_target = math.sqrt(self._last_target * self.resistivity) - self.change('DEV::HTR:SIG:VOLT', self._volt_target, tolerance=2e-4) + res = round(res, 1) + if self._resistivity != res and 10 <= res <= 100: + # we want twice the same value before changing + self._resistivity = res + else: + self.write_resistivity(res) + if self.controlled_by == 0: + self._volt_target = math.sqrt(self._last_target * self.resistivity) + self.change('DEV::HTR:SIG:VOLT', self._volt_target, tolerance=2e-4) return volt * current def read_target(self): diff --git a/frappy_psi/pdld.py b/frappy_psi/pdld.py new file mode 100644 index 0000000..14bea92 --- /dev/null +++ b/frappy_psi/pdld.py @@ -0,0 +1,68 @@ +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""PDLD Laser""" + + +from frappy.core import StringIO, Parameter, HasIO, \ + Writable, FloatRange, BoolType, IDLE, ERROR + + +class IO(StringIO): + end_of_line = ('>', '\r') + + +class Laser(HasIO, Writable): + ioClass = IO + value = Parameter('on/off', BoolType()) + target = Parameter('on/off', BoolType()) + + def get_par(self, cmd): + return float(self.communicate(cmd).split()[-1]) + + def read_value(self): + reply = float(self.communicate('MCM').split()[-1]) + if reply == 10: + self.status = IDLE, '' + return True + if reply in (0, 40): + self.status = IDLE, '' + return False + return ERROR, 'bad OP mode %s', reply + + def write_target(self, value): + if value: + self.communicate('SALO') + else: + self.communicate('SALS') + + +class LaserPower(HasIO, Writable): + value = Parameter('power readback', FloatRange(unit='mW')) + target = Parameter('power setpoint', FloatRange(0, 300, unit='mW'), readonly=False) + ioClass = IO + + def read_value(self): + return float(self.communicate('MPO').split()[-1]) + + def read_target(self): + return float(self.communicate('MPR').split()[-1]) + + def write_target(self, value): + self.communicate(f'SPR{int(value):03d}') + diff --git a/frappy_psi/pfeiffer.py b/frappy_psi/pfeiffer.py new file mode 100644 index 0000000..2511ef4 --- /dev/null +++ b/frappy_psi/pfeiffer.py @@ -0,0 +1,64 @@ +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""pfeiffer TPG vacuum pressure reading""" + +from frappy.core import StringIO, HasIO, Command, StringType, IntRange, \ + IDLE, WARN, ERROR, Readable, Parameter, Property +from frappy.errors import CommunicationFailedError + +ACK = '\x06' +ENQ = '\x05' + + +class IO(StringIO): + end_of_line = '\r\n' + default_settings = {'baudrate': 9600} + + def communicate(self, command, noreply=False): + with self._lock: + ack = super().communicate(command) + if ack != ACK: + raise CommunicationFailedError('no ack received') + if noreply: + # to be used for changing parameters when needed + return None + return super().communicate(ENQ) + + +class Pressure(HasIO, Readable): + value = Parameter(unit='mbar') + channel = Property('channel number', IntRange(1,2), default=1) + + STATUS_MAP = { + '0': (IDLE, ''), + '1': (WARN, 'underrange'), + '2': (WARN, 'overrange'), + '3': (ERROR, 'sensor error'), + '4': (ERROR, 'sensor off'), + '5': (ERROR, 'no sensor'), + '6': (ERROR, 'identification error'), + } + + ioClass = IO + + def read_value(self): + reply = self.communicate(f'PR{self.channel}') + status, strvalue = reply.split(',') + self.status = self.STATUS_MAP.get(status, (ERROR, 'bad status')) + return float(strvalue) diff --git a/frappy_psi/picontrol.py b/frappy_psi/picontrol.py new file mode 100644 index 0000000..f754a98 --- /dev/null +++ b/frappy_psi/picontrol.py @@ -0,0 +1,70 @@ +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# Jael Celia Lorenzana +# ***************************************************************************** + +"""PI control for furnance""" + +import time +from frappy.core import Writable, Attached, Parameter, FloatRange, Readable, BoolType, ERROR, IDLE + +class PI(Writable): + input = Attached(Readable, 'the input module') + output = Attached(Writable, 'the output module') + relais = Attached(Writable, 'the interlock relais') + p = Parameter('proportional term', FloatRange(0), readonly=False) + i = Parameter('integral term', FloatRange(0), readonly=False) + control_active = Parameter('control flag', BoolType(), readonly=False, default=False) + value = Parameter(unit='degC') + _lastdiff = None + _lasttime = 0 + + def doPoll(self): + super().doPoll() + if not self.control_active: + return + self.value = self.input.value + self.status = IDLE, 'controlling' + now = time.time() + deltat = min(10, now-self._lasttime) + self._lasttime = now + diff = self.target - self.value + if self.value > 300: + self.write_control_active(False) + return + if self._lastdiff is None: + self._lastdiff = diff + deltadiff = diff - self._lastdiff + self._lastdiff = diff + output = self.output.target + output += self.p * deltadiff + self.i * deltat * diff + if output > 100: + output = 100 + elif output < 0: + output = 0 + self.output.write_target(output) + + def write_control_active(self, value): + if not value: + self.output.write_target(0) + + def write_target(self, value): + self.control_active = True + self.relais.write_target(1) + + \ No newline at end of file diff --git a/frappy_psi/tdkpower.py b/frappy_psi/tdkpower.py new file mode 100644 index 0000000..64cad5c --- /dev/null +++ b/frappy_psi/tdkpower.py @@ -0,0 +1,62 @@ +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# Leon Zimmermann +# ***************************************************************************** +"""Powersupply TDK-Lambda GEN8-400-1P230""" + +from frappy.core import StringIO, Readable, Parameter, FloatRange, Writable, HasIO, BoolType + +class IO(StringIO): + end_of_line = ('OK\r', '\r') + default_settings = {'baudrate': 9600} + + +class Power(HasIO, Readable): + value = Parameter(datatype=FloatRange(0,3300,unit='W')) + + def read_value(self): + reply_volt = self.communicate('MV?') + reply_current = self.communicate('MC?') + volt = float(reply_volt) + current = float(reply_current) + return volt*current + + +class Output(HasIO, Writable): + value = Parameter(datatype=FloatRange(0,100,unit='%')) + target = Parameter(datatype=FloatRange(0,100,unit='%')) + maxvolt = Parameter('voltage at 100%',datatype=FloatRange(0,8,unit='V'),readonly=False) + maxcurrent = Parameter('current at 100%',datatype=FloatRange(0,400,unit='A'),readonly=False) + output_enable = Parameter('control on/off', BoolType(), readonly=False) + + def initModule(self): + super().initModule() + self.write_output_enable(False) + + def write_target(self, target): + self.write_output_enable(target != 0) + self.communicate(f'PV {target*self.maxvolt:.5f}') + self.communicate(f'PC {target*self.maxcurrent:.5f}') + self.value = target + + def write_output_enable(self, value): + self.communicate(f'OUT {int(value)}') + + def shutdown(self): + self.write_target(0) +