uniax: added force control as seperate module

Change-Id: I8b14fc90f5885834e342f6bc5e5dd0ea711b27ea
This commit is contained in:
2021-10-06 13:33:34 +02:00
parent 70b9c88f82
commit 29226a1062
4 changed files with 230 additions and 61 deletions

41
cfg/sim_uniax.cfg Normal file
View File

@ -0,0 +1,41 @@
[NODE]
id = uniax_sim.psi.ch
description = [sim] uniaxial pressure device
[INTERFACE]
uri=tcp://5000
[drv]
class = secop.simulation.SimDrivable
extra_params = speed
description = simulated motor
value.default = 0
speed.readonly = False
speed.default = 10
interval = 0.11
[transducer]
class = secop_psi.simdpm.DPM3
description = simulated force
motor = drv
[force]
class = secop_psi.uniax.Uniax
description = uniax driver
motor = drv
transducer = transducer
[res]
class = secop.simulation.SimReadable
description = raw temperature sensor on the stick
extra_params = jitter
jitter.default = 1
value.default = 99
value.datatype = {"type":"double", "unit":"Ohm"}
[T]
class=secop_psi.softcal.Sensor
description=temperature sensor, soft calibration
rawsensor=res
calib = X132254.340
value.unit = "K"

View File

@ -21,8 +21,10 @@
# ***************************************************************************** # *****************************************************************************
"""transducer DPM3 read out""" """transducer DPM3 read out"""
from secop.core import Drivable, Parameter, FloatRange, BoolType, StringIO,\ import time
HasIodev, IntRange, Done, Attached, Command
from secop.core import Readable, Parameter, FloatRange, StringIO,\
HasIodev, IntRange, Done
class DPM3IO(StringIO): class DPM3IO(StringIO):
@ -45,7 +47,7 @@ def float2hex(value, digits):
return '%06X' % intvalue return '%06X' % intvalue
class DPM3(HasIodev, Drivable): class DPM3(HasIodev, Readable):
OFFSET = 0x8f OFFSET = 0x8f
SCALE = 0x8c SCALE = 0x8c
@ -54,21 +56,13 @@ class DPM3(HasIodev, Drivable):
iodevClass = DPM3IO iodevClass = DPM3IO
motor = Attached()
digits = Parameter('number of digits for value', IntRange(0, 5), initwrite=True, readonly=False)
value = Parameter(datatype=FloatRange(unit='N')) value = Parameter(datatype=FloatRange(unit='N'))
target = Parameter(datatype=FloatRange(unit='N')) digits = Parameter('number of digits for value', IntRange(0, 5), initwrite=True, readonly=False)
step = Parameter('maximum motor step', FloatRange(unit='deg'), default=5, readonly=False) # Note: we have to treat the units properly.
# We got an output of 150 for 10N. The maximal force we want to deal with is 100N,
# thus a maximal output of 1500. 10=150/f
offset = Parameter('', FloatRange(-1e5, 1e5), readonly=False, poll=True) offset = Parameter('', FloatRange(-1e5, 1e5), readonly=False, poll=True)
#Note: we have tro treat the units properly.
""" We got an output of 150 for 10N. The maximal force we want to deal with is 100N,
thus a maximl output of 1500. 10=150/f
"""
scale_factor = Parameter('', FloatRange(-1e5, 1e5, unit='input_units/N'), readonly=False, poll=True) scale_factor = Parameter('', FloatRange(-1e5, 1e5, unit='input_units/N'), readonly=False, poll=True)
_target = None
fast_pollfactor = 0.01
def query(self, adr, value=None): def query(self, adr, value=None):
if value is not None: if value is not None:
@ -78,7 +72,7 @@ class DPM3(HasIodev, Drivable):
if 10000 <= round(value * mag, 0) <= 99999: if 10000 <= round(value * mag, 0) <= 99999:
break break
else: else:
# not suitable range found # no suitable range found
if absval >= 99999.5: # overrange if absval >= 99999.5: # overrange
raise ValueError('%s is out of range' % value) raise ValueError('%s is out of range' % value)
# underrange: take lowest # underrange: take lowest
@ -105,7 +99,7 @@ class DPM3(HasIodev, Drivable):
# value defines the number of digits # value defines the number of digits
back_value = self._iodev.communicate('*1F135%02X\r*1G135' % (value + 1)) back_value = self._iodev.communicate('*1F135%02X\r*1G135' % (value + 1))
self.digits = int(back_value, 16) - 1 self.digits = int(back_value, 16) - 1
# realculate proper scale and offset # recalculate proper scale and offset
self.write_scale_factor(self.scale_factor) self.write_scale_factor(self.scale_factor)
self.write_offset(self.offset) self.write_offset(self.offset)
return Done return Done
@ -115,44 +109,7 @@ class DPM3(HasIodev, Drivable):
return int(back_value,16) - 1 return int(back_value,16) - 1
def read_value(self): def read_value(self):
value = float(self._iodev.communicate('*1B1')) return float(self._iodev.communicate('*1B1'))
mot = self._motor
if self._target is None:
if mot.isBusy():
self.status = self.Status.IDLE, 'stopping'
else:
self.status = self.Status.IDLE, ''
else:
if self._direction * (self._target - value) > 0:
if self._mot_target != mot.target:
self.stop()
self.status = self.Status.IDLE, 'motor was stopped'
elif not mot.isBusy():
step = self.step * self._direction
mot_target = mot.value + step
self._mot_target = mot.write_target(mot_target)
else:
self.stop()
self.status = self.Status.IDLE, 'target reached'
return value
def write_target(self, target):
self._target = target
if target - self.value > 0:
self._direction = 1
else:
self._direction = -1
self.status = self.Status.BUSY, 'moving motor'
if self._motor.status[0] == self.Status.ERROR:
self._motor.reset()
self._mot_target = self._motor.target
return target
@Command()
def stop(self):
self._target = None
self._motor.stop()
self.status = self.Status.IDLE, 'stopped'
def read_offset(self): def read_offset(self):
reply = self.query(self.OFFSET) reply = self.query(self.OFFSET)

42
secop_psi/simdpm.py Normal file
View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# 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:
# M. Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""simulated transducer DPM3 read out"""
import random
from secop.core import Readable, Parameter, FloatRange, Attached
from secop.lib import clamp
class DPM3(Readable):
motor = Attached()
jitter = Parameter('simulated jitter', FloatRange(unit='N'), default=1, readonly=False)
hysteresis = Parameter('simulated hysteresis', FloatRange(unit='deg'), default=100, readonly=False)
friction = Parameter('friction', FloatRange(unit='N/deg'), default=1, readonly=False)
slope = Parameter('slope', FloatRange(unit='N/deg'), default=10, readonly=False)
_pos = 0 # effective piston position, main hysteresis taken into account
def read_value(self):
mot = self._motor
self._pos = clamp(self._pos, mot.value - self.hysteresis * 0.5, mot.value + self.hysteresis * 0.5)
return clamp(0, 1, mot.value - self._pos) * self.friction \
+ self._pos * self.slope + self.jitter * (random.random() - random.random())

129
secop_psi/uniax.py Normal file
View File

@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# 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:
# M. Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""use transducer and motor to adjust force"""
import time
from secop.core import Drivable, Parameter, FloatRange, Done, Attached, Command
class Uniax(Drivable):
motor = Attached()
transducer = Attached()
step = Parameter('maximum motor step', FloatRange(unit='deg'), default=5, readonly=False)
fine_step = Parameter('motor step for fine adjustment', FloatRange(unit='deg'), default=1, readonly=False)
tolerance = Parameter('force tolerance', FloatRange(), readonly=False, default=1)
filter_interval = Parameter('filter time', FloatRange(0, 60, unit='s'), readonly=False, default=1)
span = Parameter('difference between max and min', FloatRange(), needscfg=False)
pollinterval = 0.1
fast_pollfactor = 1
_target = None
_mot_target = None
_direction = 0
_last_era = 0
_cnt = 0
_sum = 0
_min = None
_max = None
_last_min = None
_last_max = None
def read_value(self):
force = self._transducer
mot = self._motor
era = int(time.time() / self.filter_interval)
newera = era != self._last_era
value = force.read_value()
if newera:
self._last_era = era
if self._min is None:
self._min = self._max = self._last_max = self._last_min = value
if self._cnt > 0:
self.value = self._sum / self._cnt
self._sum = self._cnt = 0
self.span = self._max - self._min
self._last_min = self._min
self._last_max = self._max
self._min = self._max = value
self._min = min(self._min, value)
self._max = max(self._max, value)
self._sum += value
self._cnt += 1
high_value = max(self._max, self._last_max)
low_value = min(self._min, self._last_min)
if self._target is None:
if mot.isBusy():
self.status = self.Status.IDLE, 'stopping'
else:
self.status = self.Status.IDLE, ''
return Done
step = 0
if self._direction > 0:
if high_value < self._target:
step = self.step
else:
if low_value > self._target:
step = -self.step
if not step:
if self._direction * (self._target - value) < self.tolerance:
self.stop()
self.status = self.Status.IDLE, 'target reached'
return Done
if newera:
step = self.fine_step * self._direction
if step and not mot.isBusy():
mot_target = mot.value + step
self._mot_target = mot.write_target(mot_target)
return Done
def write_target(self, target):
self._target = target
if target - self.value > 0:
self._direction = 1
else:
self._direction = -1
if self._motor.status[0] == self.Status.ERROR:
self._motor.reset()
self.status = self.Status.BUSY, 'changed target'
self._mot_target = self._motor.target
self.read_value()
return target
@Command()
def stop(self):
self._target = None
self._motor.stop()
self.status = self.Status.IDLE, 'stopped'