uniax: added force control as seperate module
Change-Id: I8b14fc90f5885834e342f6bc5e5dd0ea711b27ea
This commit is contained in:
41
cfg/sim_uniax.cfg
Normal file
41
cfg/sim_uniax.cfg
Normal 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"
|
@ -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,30 +47,22 @@ 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
|
||||||
|
|
||||||
MAGNITUDE = {'1': 1, '2': 10, '3': 100, '4': 1e3, '5': 1e4, '6': 1e5,
|
MAGNITUDE = {'1': 1, '2': 10, '3': 100, '4': 1e3, '5': 1e4, '6': 1e5,
|
||||||
'9':-1, 'A':-10, 'B':-100, 'C':-1e3, 'D':-1e4, 'E':-1e5}
|
'9': -1, 'A': -10, 'B': -100, 'C': -1e3, 'D': -1e4, 'E': -1e5}
|
||||||
|
|
||||||
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
|
||||||
@ -103,9 +97,9 @@ class DPM3(HasIodev, Drivable):
|
|||||||
|
|
||||||
def write_digits(self, value):
|
def write_digits(self, value):
|
||||||
# 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
42
secop_psi/simdpm.py
Normal 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
129
secop_psi/uniax.py
Normal 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'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Reference in New Issue
Block a user