backport fixes from MLZ repo

- unification of secop.stringio/bytesio to secop.io
- persistent parameters

Change-Id: I76307cccc5191ac8cbb5cfec6fb7450fcf6945f1
This commit is contained in:
zolliker 2021-07-27 08:11:51 +02:00
parent 2ff3a17427
commit a037accbb8
6 changed files with 17 additions and 127 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,7 +136,6 @@ 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)

View File

@ -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'),
@ -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'