From a037accbb870b7be1a7f1c8caa7b68c34e951a0c Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 27 Jul 2021 08:11:51 +0200 Subject: [PATCH] backport fixes from MLZ repo - unification of secop.stringio/bytesio to secop.io - persistent parameters Change-Id: I76307cccc5191ac8cbb5cfec6fb7450fcf6945f1 --- secop/bytesio.py | 90 ------------------------------------ secop/core.py | 2 +- secop/{stringio.py => io.py} | 0 secop/params.py | 4 +- secop/persistent.py | 23 ++++----- secop_psi/trinamic.py | 25 ++-------- 6 files changed, 17 insertions(+), 127 deletions(-) delete mode 100644 secop/bytesio.py rename secop/{stringio.py => io.py} (100%) diff --git a/secop/bytesio.py b/secop/bytesio.py deleted file mode 100644 index dc96d4f..0000000 --- a/secop/bytesio.py +++ /dev/null @@ -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 -# ***************************************************************************** -"""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 diff --git a/secop/core.py b/secop/core.py index 8a5d2ae..986f57c 100644 --- a/secop/core.py +++ b/secop/core.py @@ -36,5 +36,5 @@ from secop.params import Command, Parameter from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW from secop.properties import Property 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 diff --git a/secop/stringio.py b/secop/io.py similarity index 100% rename from secop/stringio.py rename to secop/io.py diff --git a/secop/params.py b/secop/params.py index 44d2933..6fe9274 100644 --- a/secop/params.py +++ b/secop/params.py @@ -233,7 +233,7 @@ class Parameter(Accessible): def copy(self): # deep copy, as datatype might be altered from config - res = Parameter() + res = type(self)() res.name = self.name res.init(self.propertyValues) res.datatype = res.datatype.copy() @@ -363,7 +363,7 @@ class Command(Accessible): return self def copy(self): - res = Command() + res = type(self)() res.name = self.name res.func = self.func res.init(self.propertyValues) diff --git a/secop/persistent.py b/secop/persistent.py index 352debd..f03d8ef 100644 --- a/secop/persistent.py +++ b/secop/persistent.py @@ -24,15 +24,14 @@ 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 -one of the parameters is changed, either by a change command or by access from -an other interface to the hardware, it is saved to a file, and reloaded after +one of the parameters is changed, either by a change command or when reading back +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 mechanism needed to detect power down (i.e. a reading a hardware parameter taking a special value on power up). -An additional use might be the example of a motor with cyclic reading of an -encoder value, which looses the counts of how many turns already happened on -power down. +An additional use might be the example of a motor with an encoder which looses +the counts of how many turns already happened on power down. 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. @@ -56,8 +55,6 @@ class MyClass(PersistentMixin, ...): import os import json -from secop.errors import BadValueError, ConfigError, InternalError, \ - ProgrammingError, SECoPError, SilentError, secop_error from secop.lib import getGeneralConfig from secop.params import Parameter, Property, BoolType, Command from secop.modules import HasAccessibles @@ -70,14 +67,15 @@ class PersistentParam(Parameter): class PersistentMixin(HasAccessibles): def __init__(self, *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 = {} for pname in self.parameters: pobj = self.parameters[pname] if not pobj.readonly and getattr(pobj, 'persistent', False): self.initData[pname] = pobj.value self.writeDict.update(self.loadParameters(write=False)) - print('initData', self.initData) def loadParameters(self, write=True): """load persistent parameters @@ -87,8 +85,6 @@ class PersistentMixin(HasAccessibles): is called upon startup and may be called from a module 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: with open(self.persistentFile, 'r') as f: self.persistentData = json.load(f) @@ -140,8 +136,7 @@ class PersistentMixin(HasAccessibles): except FileNotFoundError: pass - @Command() def factory_reset(self): - self.writeDict.update(self.initData) - self.writeInitParams() + self.writeDict.update(self.initData) + self.writeInitParams() diff --git a/secop_psi/trinamic.py b/secop_psi/trinamic.py index a0ad2ef..7254bbf 100644 --- a/secop_psi/trinamic.py +++ b/secop_psi/trinamic.py @@ -27,7 +27,7 @@ import struct from math import log10 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.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError @@ -77,10 +77,6 @@ class HwParam(PersistentParam): class Motor(PersistentMixin, HasIodev, Drivable): 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')) zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0) encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'), @@ -121,7 +117,7 @@ class Motor(PersistentMixin, HasIodev, Drivable): baudrate = Parameter('_', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}), readonly=False, default=0, poll=True, visibility=3, group='more') pollinterval = Parameter(group='more') - + iodevClass = BytesIO fast_pollfactor = 0.001 # poll as fast as possible when busy @@ -155,7 +151,7 @@ class Motor(PersistentMixin, HasIodev, Drivable): raise CommunicationFailedError('checksum error') except Exception as e: if itry == 1: - raise + raise exc = e continue break @@ -208,7 +204,8 @@ class Motor(PersistentMixin, HasIodev, Drivable): adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360 if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance: # 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: self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder) 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) return self.Status.ERROR, 'encoder does not match internal pos' return self.status - now = time.time() if oldpos != self.steppos or not (self.read_target_reached() or self.read_move_status() or self.read_error_bits()): return self.Status.BUSY, 'moving' @@ -260,15 +256,6 @@ class Motor(PersistentMixin, HasIodev, Drivable): if abs(diff) <= self.tolerance: self._started = 0 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._started = 0 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' raise HardwareError('need reset (encoder does not match internal pos)') self.set('steppos', self.encoder - self.zero) - # self.fix_steppos(self.encoder_tolerance) self._started = time.time() - # self._try_count = 0 self.log.info('move to %.1f', target) self.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE) self.status = self.Status.BUSY, 'changed target'