backport fixes from MLZ repo
- unification of secop.stringio/bytesio to secop.io - persistent parameters Change-Id: I76307cccc5191ac8cbb5cfec6fb7450fcf6945f1
This commit is contained in:
parent
2ff3a17427
commit
a037accbb8
@ -1,90 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- 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:
|
|
||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
||||||
# *****************************************************************************
|
|
||||||
"""byte oriented stream communication"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
|
||||||
from secop.modules import Property, Command
|
|
||||||
from secop.stringio import BaseIO
|
|
||||||
from secop.datatypes import BLOBType, IntRange, ArrayOf, TupleOf, StringType
|
|
||||||
from secop.errors import CommunicationFailedError, CommunicationSilentError
|
|
||||||
|
|
||||||
|
|
||||||
HEX_CODE = re.compile(r'[0-9a-fA-F][0-9a-fA-F]$')
|
|
||||||
|
|
||||||
|
|
||||||
class BytesIO(BaseIO):
|
|
||||||
identification = Property(
|
|
||||||
"""identification
|
|
||||||
|
|
||||||
a list of tuples with commands and expected responses, to be sent on connect.
|
|
||||||
commands and responses are whitespace separated items
|
|
||||||
an item is either:
|
|
||||||
- a two digit hexadecimal number (byte value)
|
|
||||||
- a character
|
|
||||||
- ?? indicating ignored bytes in responses
|
|
||||||
""", datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
|
|
||||||
|
|
||||||
def connectStart(self):
|
|
||||||
if not self.is_connected:
|
|
||||||
uri = self.uri
|
|
||||||
self._conn = AsynConn(uri, b'')
|
|
||||||
self.is_connected = True
|
|
||||||
for command, match in self.identification:
|
|
||||||
cmdbytes = bytes([int(c, 16) if HEX_CODE.match(c) else ord(c) for c in command.split()])
|
|
||||||
replypat = [int(c, 16) if HEX_CODE.match(c.replace('??', '-1')) else ord(c) for c in command.split()]
|
|
||||||
reply = self.communicate(cmdbytes, len(replypat))
|
|
||||||
if any(b != c and c != -1 for b, c in zip(reply, replypat)):
|
|
||||||
self.closeConnection()
|
|
||||||
raise CommunicationFailedError('bad response: %r does not match %r' % (command, match))
|
|
||||||
|
|
||||||
@Command((BLOBType(), IntRange(0)), result=BLOBType())
|
|
||||||
def communicate(self, command, nbytes):
|
|
||||||
"""send a command and receive nbytes as reply"""
|
|
||||||
if not self.is_connected:
|
|
||||||
self.read_is_connected() # try to reconnect
|
|
||||||
if not self._conn:
|
|
||||||
raise CommunicationSilentError('can not connect to %r' % self.uri)
|
|
||||||
try:
|
|
||||||
with self._lock:
|
|
||||||
# read garbage and wait before send
|
|
||||||
try:
|
|
||||||
if self.wait_before:
|
|
||||||
time.sleep(self.wait_before)
|
|
||||||
garbage = self._conn.flush_recv()
|
|
||||||
if garbage:
|
|
||||||
self.log.debug('garbage: %r', garbage)
|
|
||||||
self._conn.send(command)
|
|
||||||
self.log.debug('send: %r', command)
|
|
||||||
reply = self._conn.readbytes(nbytes, self.timeout)
|
|
||||||
except ConnectionClosed:
|
|
||||||
self.closeConnection()
|
|
||||||
raise CommunicationFailedError('disconnected')
|
|
||||||
self.log.debug('recv: %r', reply)
|
|
||||||
return reply
|
|
||||||
except Exception as e:
|
|
||||||
if str(e) == self._last_error:
|
|
||||||
raise CommunicationSilentError(str(e))
|
|
||||||
self._last_error = str(e)
|
|
||||||
self.log.error(self._last_error)
|
|
||||||
raise
|
|
@ -36,5 +36,5 @@ from secop.params import Command, Parameter
|
|||||||
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
|
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
from secop.proxy import Proxy, SecNode, proxy_class
|
from secop.proxy import Proxy, SecNode, proxy_class
|
||||||
from secop.stringio import HasIodev, StringIO
|
from secop.io import HasIodev, StringIO, BytesIO
|
||||||
from secop.persistent import PersistentMixin, PersistentParam
|
from secop.persistent import PersistentMixin, PersistentParam
|
||||||
|
@ -233,7 +233,7 @@ class Parameter(Accessible):
|
|||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
# deep copy, as datatype might be altered from config
|
# deep copy, as datatype might be altered from config
|
||||||
res = Parameter()
|
res = type(self)()
|
||||||
res.name = self.name
|
res.name = self.name
|
||||||
res.init(self.propertyValues)
|
res.init(self.propertyValues)
|
||||||
res.datatype = res.datatype.copy()
|
res.datatype = res.datatype.copy()
|
||||||
@ -363,7 +363,7 @@ class Command(Accessible):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
res = Command()
|
res = type(self)()
|
||||||
res.name = self.name
|
res.name = self.name
|
||||||
res.func = self.func
|
res.func = self.func
|
||||||
res.init(self.propertyValues)
|
res.init(self.propertyValues)
|
||||||
|
@ -24,15 +24,14 @@
|
|||||||
For hardware not keeping parameters persistent, we might want to store them in Frappy.
|
For hardware not keeping parameters persistent, we might want to store them in Frappy.
|
||||||
|
|
||||||
The following example will make 'param1' and 'param2' persistent, i.e. whenever
|
The following example will make 'param1' and 'param2' persistent, i.e. whenever
|
||||||
one of the parameters is changed, either by a change command or by access from
|
one of the parameters is changed, either by a change command or when reading back
|
||||||
an other interface to the hardware, it is saved to a file, and reloaded after
|
from the hardware, it is saved to a file, and reloaded after
|
||||||
a power down / power up cycle. In order to make this work properly, there is a
|
a power down / power up cycle. In order to make this work properly, there is a
|
||||||
mechanism needed to detect power down (i.e. a reading a hardware parameter
|
mechanism needed to detect power down (i.e. a reading a hardware parameter
|
||||||
taking a special value on power up).
|
taking a special value on power up).
|
||||||
|
|
||||||
An additional use might be the example of a motor with cyclic reading of an
|
An additional use might be the example of a motor with an encoder which looses
|
||||||
encoder value, which looses the counts of how many turns already happened on
|
the counts of how many turns already happened on power down.
|
||||||
power down.
|
|
||||||
This can be solved by comparing the loaded encoder value self.encoder with a
|
This can be solved by comparing the loaded encoder value self.encoder with a
|
||||||
fresh value from the hardware and then adjusting the zero point accordingly.
|
fresh value from the hardware and then adjusting the zero point accordingly.
|
||||||
|
|
||||||
@ -56,8 +55,6 @@ class MyClass(PersistentMixin, ...):
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from secop.errors import BadValueError, ConfigError, InternalError, \
|
|
||||||
ProgrammingError, SECoPError, SilentError, secop_error
|
|
||||||
from secop.lib import getGeneralConfig
|
from secop.lib import getGeneralConfig
|
||||||
from secop.params import Parameter, Property, BoolType, Command
|
from secop.params import Parameter, Property, BoolType, Command
|
||||||
from secop.modules import HasAccessibles
|
from secop.modules import HasAccessibles
|
||||||
@ -70,14 +67,15 @@ class PersistentParam(Parameter):
|
|||||||
class PersistentMixin(HasAccessibles):
|
class PersistentMixin(HasAccessibles):
|
||||||
def __init__(self, *args, **kwds):
|
def __init__(self, *args, **kwds):
|
||||||
super().__init__(*args, **kwds)
|
super().__init__(*args, **kwds)
|
||||||
# write=False: write will happen later
|
persistentdir = os.path.join(getGeneralConfig()['logdir'], 'persistent')
|
||||||
|
os.makedirs(persistentdir, exist_ok=True)
|
||||||
|
self.persistentFile = os.path.join(persistentdir, '%s.%s.json' % (self.DISPATCHER.equipment_id, self.name))
|
||||||
self.initData = {}
|
self.initData = {}
|
||||||
for pname in self.parameters:
|
for pname in self.parameters:
|
||||||
pobj = self.parameters[pname]
|
pobj = self.parameters[pname]
|
||||||
if not pobj.readonly and getattr(pobj, 'persistent', False):
|
if not pobj.readonly and getattr(pobj, 'persistent', False):
|
||||||
self.initData[pname] = pobj.value
|
self.initData[pname] = pobj.value
|
||||||
self.writeDict.update(self.loadParameters(write=False))
|
self.writeDict.update(self.loadParameters(write=False))
|
||||||
print('initData', self.initData)
|
|
||||||
|
|
||||||
def loadParameters(self, write=True):
|
def loadParameters(self, write=True):
|
||||||
"""load persistent parameters
|
"""load persistent parameters
|
||||||
@ -87,8 +85,6 @@ class PersistentMixin(HasAccessibles):
|
|||||||
is called upon startup and may be called from a module
|
is called upon startup and may be called from a module
|
||||||
when a hardware powerdown is detected
|
when a hardware powerdown is detected
|
||||||
"""
|
"""
|
||||||
persistentdir = os.path.join(getGeneralConfig()['logdir'], 'persistent')
|
|
||||||
self.persistentFile = os.path.join(persistentdir, '%s.%s.json' % (self.DISPATCHER.equipment_id, self.name))
|
|
||||||
try:
|
try:
|
||||||
with open(self.persistentFile, 'r') as f:
|
with open(self.persistentFile, 'r') as f:
|
||||||
self.persistentData = json.load(f)
|
self.persistentData = json.load(f)
|
||||||
@ -140,8 +136,7 @@ class PersistentMixin(HasAccessibles):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@Command()
|
@Command()
|
||||||
def factory_reset(self):
|
def factory_reset(self):
|
||||||
self.writeDict.update(self.initData)
|
self.writeDict.update(self.initData)
|
||||||
self.writeInitParams()
|
self.writeInitParams()
|
||||||
|
@ -27,7 +27,7 @@ import struct
|
|||||||
from math import log10
|
from math import log10
|
||||||
|
|
||||||
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \
|
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \
|
||||||
HasIodev, Parameter, Property, Drivable, TupleOf, Done, PersistentMixin, PersistentParam
|
HasIodev, Parameter, Property, Drivable, PersistentMixin, PersistentParam
|
||||||
from secop.bytesio import BytesIO
|
from secop.bytesio import BytesIO
|
||||||
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError
|
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError
|
||||||
|
|
||||||
@ -77,10 +77,6 @@ class HwParam(PersistentParam):
|
|||||||
class Motor(PersistentMixin, HasIodev, Drivable):
|
class Motor(PersistentMixin, HasIodev, Drivable):
|
||||||
address = Property('module address', IntRange(0, 255), default=1)
|
address = Property('module address', IntRange(0, 255), default=1)
|
||||||
|
|
||||||
# limit_pin_mask = Property('input pin mask for lower/upper limit switch',
|
|
||||||
# TupleOf(IntRange(0, 15), IntRange(0, 15)),
|
|
||||||
# default=(8, 0))
|
|
||||||
|
|
||||||
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'))
|
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'))
|
||||||
zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
|
zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
|
||||||
encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
|
encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
|
||||||
@ -155,7 +151,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
raise CommunicationFailedError('checksum error')
|
raise CommunicationFailedError('checksum error')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if itry == 1:
|
if itry == 1:
|
||||||
raise
|
raise
|
||||||
exc = e
|
exc = e
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
@ -208,7 +204,8 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360
|
adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360
|
||||||
if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance:
|
if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance:
|
||||||
# encoder module0 360 has changed
|
# encoder module0 360 has changed
|
||||||
self.log.error('saved encoder value (%.2f) does not match reading (%.2f %.2f)', self.encoder, encoder_from_hw, adjusted_encoder)
|
self.log.error('saved encoder value (%.2f) does not match reading (%.2f %.2f)',
|
||||||
|
self.encoder, encoder_from_hw, adjusted_encoder)
|
||||||
if adjusted_encoder != encoder_from_hw:
|
if adjusted_encoder != encoder_from_hw:
|
||||||
self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder)
|
self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder)
|
||||||
self._need_reset = True
|
self._need_reset = True
|
||||||
@ -252,7 +249,6 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
self.log.error('encoder (%.2f) does not match internal pos (%.2f)', self.encoder, self.steppos)
|
self.log.error('encoder (%.2f) does not match internal pos (%.2f)', self.encoder, self.steppos)
|
||||||
return self.Status.ERROR, 'encoder does not match internal pos'
|
return self.Status.ERROR, 'encoder does not match internal pos'
|
||||||
return self.status
|
return self.status
|
||||||
now = time.time()
|
|
||||||
if oldpos != self.steppos or not (self.read_target_reached() or self.read_move_status()
|
if oldpos != self.steppos or not (self.read_target_reached() or self.read_move_status()
|
||||||
or self.read_error_bits()):
|
or self.read_error_bits()):
|
||||||
return self.Status.BUSY, 'moving'
|
return self.Status.BUSY, 'moving'
|
||||||
@ -260,15 +256,6 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
if abs(diff) <= self.tolerance:
|
if abs(diff) <= self.tolerance:
|
||||||
self._started = 0
|
self._started = 0
|
||||||
return self.Status.IDLE, ''
|
return self.Status.IDLE, ''
|
||||||
#if (abs(self.target - self.steppos) < self.tolerance and
|
|
||||||
# abs(self.encoder - self.steppos) < self.encoder_tolerance):
|
|
||||||
# self._try_count += 1
|
|
||||||
# if self._try_count < 3:
|
|
||||||
# # occasionaly, two attempts are needed, as steppos and encoder might have been
|
|
||||||
# # off by 1-2 full steps before moving
|
|
||||||
# self.fix_steppos(self.tolerance, self.target)
|
|
||||||
# self.log.warning('try move again')
|
|
||||||
# return self.Status.BUSY, 'try again'
|
|
||||||
self.log.error('out of tolerance by %.3g', diff)
|
self.log.error('out of tolerance by %.3g', diff)
|
||||||
self._started = 0
|
self._started = 0
|
||||||
return self.Status.ERROR, 'out of tolerance'
|
return self.Status.ERROR, 'out of tolerance'
|
||||||
@ -286,9 +273,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
|
|||||||
self.status = self.Status.ERROR, 'encoder does not match internal pos'
|
self.status = self.Status.ERROR, 'encoder does not match internal pos'
|
||||||
raise HardwareError('need reset (encoder does not match internal pos)')
|
raise HardwareError('need reset (encoder does not match internal pos)')
|
||||||
self.set('steppos', self.encoder - self.zero)
|
self.set('steppos', self.encoder - self.zero)
|
||||||
# self.fix_steppos(self.encoder_tolerance)
|
|
||||||
self._started = time.time()
|
self._started = time.time()
|
||||||
# self._try_count = 0
|
|
||||||
self.log.info('move to %.1f', target)
|
self.log.info('move to %.1f', target)
|
||||||
self.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE)
|
self.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE)
|
||||||
self.status = self.Status.BUSY, 'changed target'
|
self.status = self.Status.BUSY, 'changed target'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user