From ee040ce98adbe4cac8661750e91cd381000470ad Mon Sep 17 00:00:00 2001 From: Paul Neves Date: Mon, 17 Jun 2024 20:11:36 +0200 Subject: [PATCH] added RP100, ACM1219, and dummy classes, and razorbillUC220T config file --- cfg/razorbillUC220T_cfg.py | 66 ++++++++++++++++++ frappy_psi/ACM1219.py | 136 +++++++++++++++++++++++++++++++++++++ frappy_psi/RP100.py | 109 +++++++++++++++++++++++++++++ frappy_psi/dummy.py | 12 ++++ 4 files changed, 323 insertions(+) create mode 100644 cfg/razorbillUC220T_cfg.py create mode 100644 frappy_psi/ACM1219.py create mode 100644 frappy_psi/RP100.py create mode 100644 frappy_psi/dummy.py diff --git a/cfg/razorbillUC220T_cfg.py b/cfg/razorbillUC220T_cfg.py new file mode 100644 index 0000000..f8b792c --- /dev/null +++ b/cfg/razorbillUC220T_cfg.py @@ -0,0 +1,66 @@ +# call $ bin/frappy-server razorbillUC220T +# in frappy directory, with python with frappy libraries installed. + +Node('UC220T.psi.ch', + 'A Razorbill UC220T controlled by a RP100 high voltage powersupply and a ACM1219 (AD7746) capacitance meter', + interface='tcp://5123') + +Mod('io1', + 'frappy_psi.RP100.RP100IO', + 'communication', + uri='serial:///dev/tty.usbmodem1401?baudrate=9600+bytesize=8+parity=none+stopbits=1') +Mod('V1', + 'frappy_psi.RP100.VoltageChannel', + 'Voltage Channel 1', + temp='T', + io='io1', + target=Param(min=-200, max=200), + max_target=120, + min_target=-20, + slew_rate=100, + channel=1) +Mod('V2', + 'frappy_psi.RP100.VoltageChannel', + 'Voltage Channel 2', + temp='T', + io='io1', + target=Param(min=-200, max=200), + max_target=120, + min_target=-20, + slew_rate=100, + channel=2) + +Mod('io2', + 'frappy_psi.ACM1219.ACM1219IO', + 'communication', + uri='serial:///dev/tty.usbserial-A700fmAI?baudrate=9600+bytesize=8+parity=none+stopbits=1') +Mod('C1C2', + 'frappy_psi.ACM1219.BothChannels', + 'Capacitance channels 1 and 2', + io='io2') +Mod('d', + 'frappy_psi.ACM1219.Displacement', + 'razorbill displacement from capacitance', + cap='C1C2', + channel=1, + alpha290K=56.710, + d0=95.443, + Cp=0.01883, + d0_curve={'a':4.21,'b':-0.00157,'c':-3.38e-5,'d':5.28e-8,'e':-6.93e-11}, + temp='T') +Mod('F', + 'frappy_psi.ACM1219.Force', + 'razorbill force from capacitance', + cap='C1C2', + channel=2, + alpha290K=374.23, + f0=315.63, + Cp=0.0755, + f0_curve={'a':38.9,'b':-0.0147,'c':-0.000346,'d':8.96e-7,'e':-1.58e-9}, + temp='T') + +Mod('T', + 'frappy_psi.dummy.Temp', + 'dummy T written from client', + target=Param(min=1, max=325), + ) \ No newline at end of file diff --git a/frappy_psi/ACM1219.py b/frappy_psi/ACM1219.py new file mode 100644 index 0000000..8479292 --- /dev/null +++ b/frappy_psi/ACM1219.py @@ -0,0 +1,136 @@ +from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, IntRange,\ + IDLE, BUSY, WARN, ERROR, Drivable, BoolType, Attached, StructOf + + +class ACM1219IO(StringIO): + """communication with ACM1219""" + end_of_line = '\n' + wait_before = 0.05 + identification = [('*IDN?', r'.*')] + + +class BothChannels(HasIO, Readable): + """read both capacitance channels in multiplex mode""" + + # define the communication class for automatic creation of the IO module + ioClass = ACM1219IO + + # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype) + value = Parameter('output voltage', + StructOf(C1=FloatRange(0, 21.096, unit='pF'), C2=FloatRange(0, 21.096, unit='pF'), VT=FloatRange(-1, 1000, unit='')), + readonly=True) + channels_enabled = Parameter('channel 1 on or off', BoolType(), readonly=False) + _ch_enabled = False + + def read_value(self): + # using the inherited HasIO.communicate method to send a command and get the reply + natempt = 0 + maxAttempts = 5 + + while natempt < maxAttempts: + try: + reply = self.communicate(f'readMUC') + print(reply) + reply = reply.split(',') + C1 = float(reply[0]) + C2 = float(reply[1]) + VT = float(reply[2]) + return {'C1': C1, 'C2': C2, 'VT': VT} + except: + '' + natempt+=1 + if natempt >= maxAttempts: + print('Max attempt reached for reading arduino.') + return self.value + + def read_status(self): + # code = self.communicate(f'readStatus') # returns tons of data + return IDLE, '' + + def read_channels_enabled(self): + return self._ch_enabled + + def write_channels_enabled(self, channels_enabled): + if channels_enabled: + self.communicate(f'setCIN 1,0,00.0,00.0,0,00.0,00.0') + self.communicate(f'setCIN 2,0,00.0,00.0,0,00.0,00.0') + self._ch_enabled = True + else: + self.communicate(f'setCIN 0,0,00.0,00.0,0,00.0,00.0') + self._ch_enabled = False + + return self.read_channels_enabled() + + +class Displacement(Readable): + + # attached classes for capacitance and temperature + cap = Attached() + temp = Attached() + + # internal property to configure the channel + channel = Property('the voltage channel for displacement capacitor', datatype=IntRange(1,2)) + + # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype) + value = Parameter('displacement', FloatRange(None, None, unit='um'), readonly=True) + alpha290K = Parameter('capacitor constant at 290 K', FloatRange(None, None, unit='um pF'), readonly=True) + d0 = Parameter('offset displacement', FloatRange(None, None, unit='um'), readonly=True) + Cp = Parameter('parallel capacitance', FloatRange(None, None, unit='pF'), readonly=True) + d0_curve = Parameter('calibration curve for offset displacement', + StructOf(a=FloatRange(None, None, unit='um'), + b=FloatRange(None, None, unit='um/K'), + c=FloatRange(None, None, unit='um/K^2'), + d=FloatRange(None, None, unit='um/K^3'), + e=FloatRange(None, None, unit='um/K^4'),), + readonly=True) + + def read_value(self): + # get temperature and capacitance + temp = self.temp.target + cap = self.cap.value[f'C{self.channel}'] + + # calculate displacement from temperature and capacitance + d0_T = self.d0_curve['a'] + self.d0_curve['b']*temp + self.d0_curve['c']*temp**2 + self.d0_curve['d']*temp**3 + self.d0_curve['e']*temp**4 + + disp = self.alpha290K / (cap - self.Cp) - self.d0 - d0_T + + return disp + + +class Force(Readable): + + # attached classes for capacitance and temperature + cap = Attached() + temp = Attached() + + # internal property to configure the channel + channel = Property('the voltage channel for force capacitor', datatype=IntRange(1,2)) + + # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype) + value = Parameter('force', FloatRange(None, None, unit='N'), readonly=True) + alpha290K = Parameter('capacitor constant at 290 K', FloatRange(None, None, unit='N pF'), readonly=True) + f0 = Parameter('offset force', FloatRange(None, None, unit='N'), readonly=True) + Cp = Parameter('parallel capacitance', FloatRange(None, None, unit='pF'), readonly=True) + f0_curve = Parameter('calibration curve for offset force', + StructOf(a=FloatRange(None, None, unit='N'), + b=FloatRange(None, None, unit='N/K'), + c=FloatRange(None, None, unit='N/K^2'), + d=FloatRange(None, None, unit='N/K^3'), + e=FloatRange(None, None, unit='N/K^4'),), + readonly=True) + + def read_value(self): + # get temperature and capacitance + temp = self.temp.target + cap = self.cap.value[f'C{self.channel}'] + + # calculate force from temperature and capacitance + alpha = self.alpha290K * (0.91 + 5e-5*temp + 9e-7*temp**2) + f0_T = self.f0_curve['a'] + self.f0_curve['b']*temp + self.f0_curve['c']*temp**2 + self.f0_curve['d']*temp**3 + self.f0_curve['e']*temp**4 + + force = alpha / (cap - self.Cp) - self.f0 - f0_T + + return force + + + \ No newline at end of file diff --git a/frappy_psi/RP100.py b/frappy_psi/RP100.py new file mode 100644 index 0000000..db63e6f --- /dev/null +++ b/frappy_psi/RP100.py @@ -0,0 +1,109 @@ +from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, IntRange,\ + IDLE, BUSY, WARN, ERROR, Drivable, BoolType, Attached +from ast import literal_eval + + +class RP100IO(StringIO): + """communication with RP100""" + end_of_line = '\n' + #wait_before = 0.05 + identification = [('*IDN?', r'Razorbill,.*')] + + +class VoltageChannel(HasIO, Drivable): + """a voltage output with loop""" + + temp = Attached() + + # define the communication class for automatic creation of the IO module + ioClass = RP100IO + + # internal property to configure the channel + channel = Property('the voltage channel', datatype=IntRange(1,2)) + + # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype) + value = Parameter('output voltage', FloatRange(-210, 210, unit='V'), + readonly=True) + target = Parameter('target voltage', FloatRange(-210, 210, unit='V'), + readonly=False) + meas_voltage = Parameter('measured output voltage', FloatRange(-250, 250, unit='V'), + readonly=True) + meas_current = Parameter('measured output current', FloatRange(-0.007, 0.007, unit='A'), + readonly=True) + max_target = Parameter('max. target', FloatRange(0, 210, unit='V'), readonly=False) + min_target = Parameter('max. target', FloatRange(-210, 0, unit='V'), readonly=False) + slew_rate = Parameter('voltage slew rate', FloatRange(0.1e-3, 100e3, unit='V/s'), readonly=False) + output_state = Parameter('output on or off', BoolType(), readonly=False) + + def doPoll(self): + super().doPoll() + + # calculate temperature dependent voltage limits + temp = self.temp.target + if temp > 250: + self.max_target = 120 + self.min_target = -20 + elif temp >= 100: + self.max_target = 120 + self.min_target = -50 + (temp-100)/5 + elif temp >= 10: + self.max_target = 200 - 8*(temp-10)/9 + self.min_target = -200 + 5*(temp-10)/3 + elif temp < 10: + self.max_target = 200 + self.min_target = -200 + + # if the current voltage exceeds these limits, reduce voltage to max/min + if self.target > self.max_target: + self.write_target(self.max_target) + if self.target < self.min_target: + self.write_target(self.min_target) + + def read_value(self): + # using the inherited HasIO.communicate method to send a command and get the reply + reply = self.communicate(f'SOUR{self.channel}:VOLT:NOW?') + return float(reply) + + def read_status(self): + while 1: + code, text = literal_eval(self.communicate(f'SYST:ERR?')) + if code == 0: + break + self.log.warning('got error %d %s', code, text) + return IDLE, '' + + def read_target(self): + # read back the target value + target = float(self.communicate(f'SOUR{self.channel}:VOLT?')) + return target + + def write_target(self, target): + # write here the target to the hardware + if target > self.max_target: + target = self.max_target + self.log.warning('Attempted to set voltage above maximum allowed voltage. Setting to max allowed instead.') + if target < self.min_target: + target = self.min_target + self.log.warning('Attempted to set voltage below minimum allowed voltage. Setting to min allowed instead.') + self.communicate(f'SOUR{self.channel}:VOLT {target};*OPC?') + return self.read_target() # return the read back value + + def read_slew_rate(self): + return float(self.communicate(f'SOUR{self.channel}:VOLT:SLEW?')) + + def write_slew_rate(self, slew_rate): + self.communicate(f'SOUR{self.channel}:VOLT:SLEW {slew_rate};*OPC?') + return self.read_slew_rate() + + def read_output_state(self): + return bool(self.communicate(f'OUTP{self.channel}?')) + + def write_output_state(self, output_state): + self.communicate(f'OUTP{self.channel} {int(output_state)};*OPC?') + return self.read_slew_rate() + + def read_meas_voltage(self): + return float(self.communicate(f'MEAS{self.channel}:VOLT?')) + + def read_meas_current(self): + return float(self.communicate(f'MEAS{self.channel}:CURR?')) \ No newline at end of file diff --git a/frappy_psi/dummy.py b/frappy_psi/dummy.py new file mode 100644 index 0000000..4b1c9e1 --- /dev/null +++ b/frappy_psi/dummy.py @@ -0,0 +1,12 @@ +from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, IntRange,\ + IDLE, BUSY, WARN, ERROR, Drivable, BoolType, Attached, Writable + + +class Temp(Writable): + """a voltage output with loop""" + + target = Parameter('target voltage', FloatRange(0, 325, unit='K'), + readonly=False) + + def write_target(self, target): + self.value = target \ No newline at end of file