1 Commits

Author SHA1 Message Date
a7fd90cd6d flowsas project as of 2025-04-14 2025-04-14 11:40:12 +02:00
23 changed files with 695 additions and 900 deletions

View File

@ -1,231 +0,0 @@
Node('cfg/dilsc1.cfg',
'triton test',
interface='5000',
name='dilsc1',
)
Mod('triton',
'frappy_psi.mercury.IO',
'connection to triton software',
uri='tcp://192.168.2.33:33576',
)
Mod('T_mix',
'frappy_psi.triton.TemperatureSensor',
'mix. chamber temperature',
slot='T8',
io='triton',
)
Mod('T_pt2head',
'frappy_psi.triton.TemperatureSensor',
'PTR2 head temperature',
slot='T1',
io='triton',
)
Mod('T_pt2plate',
'frappy_psi.triton.TemperatureSensor',
'PTR2 plate temperature',
slot='T2',
io='triton',
)
Mod('T_still',
'frappy_psi.triton.TemperatureSensor',
'still temperature',
slot='T3',
io='triton',
)
Mod('htr_still',
'frappy_psi.triton.HeaterOutput',
'still heater',
slot='H2',
io='triton',
)
Mod('T_coldpl',
'frappy_psi.triton.TemperatureSensor',
'cold plate temperature',
slot='T4',
io='triton',
)
Mod('T_mixcx',
'frappy_psi.triton.TemperatureSensor',
'mix. chamber cernox',
slot='T5',
io='triton',
)
Mod('T_pt1head',
'frappy_psi.triton.TemperatureSensor',
'PTR1 head temperature',
slot='T6',
io='triton',
)
Mod('T_pt1plate',
'frappy_psi.triton.TemperatureSensor',
'PTR1 plate temperature',
slot='T7',
io='triton',
)
Mod('T_pucksensor',
'frappy_psi.triton.TemperatureLoop',
'puck sensor temperature',
output_module='htr_pucksensor',
slot='TA',
io='triton',
)
Mod('htr_pucksensor',
'frappy_psi.triton.HeaterOutputWithRange',
'mix. chamber heater',
slot='H1,TA',
io='triton',
)
Mod('T_magnet',
'frappy_psi.triton.TemperatureSensor',
'magnet temperature',
slot='T13',
io='triton',
)
Mod('action',
'frappy_psi.triton.Action',
'higher level scripts',
io='triton',
slot='DR',
)
Mod('p_dump',
'frappy_psi.mercury.PressureSensor',
'dump pressure',
slot='P1',
io='triton',
)
Mod('p_cond',
'frappy_psi.mercury.PressureSensor',
'condenser pressure',
slot='P2',
io='triton',
)
Mod('p_still',
'frappy_psi.mercury.PressureSensor',
'still pressure',
slot='P3',
io='triton',
)
Mod('p_fore',
'frappy_psi.mercury.PressureSensor',
'pressure on the pump side',
slot='P5',
io='triton',
)
Mod('p_back',
'frappy_psi.mercury.PressureSensor',
'pressure on the back side of the pump',
slot='P4',
io='triton',
)
Mod('p_ovc',
'frappy_psi.mercury.PressureSensor',
'outer vacuum pressure',
slot='P6',
io='triton',
)
Mod('V1',
'frappy_psi.triton.Valve',
'valve V1',
slot='V1',
io='triton',
)
Mod('V2',
'frappy_psi.triton.Valve',
'valve V2',
slot='V2',
io='triton',
)
Mod('V4',
'frappy_psi.triton.Valve',
'valve V4',
slot='V4',
io='triton',
)
Mod('V5',
'frappy_psi.triton.Valve',
'valve V5',
slot='V5',
io='triton',
)
Mod('V9',
'frappy_psi.triton.Valve',
'valve V9',
slot='V9',
io='triton',
)
Mod('ips',
'frappy_psi.mercury.IO',
'IPS for magnet',
uri='192.168.127.254:3001',
)
Mod('mf',
'frappy_psi.dilsc.VectorField',
'vector field',
x='mfx',
y='mfy',
z='mfz',
sphere_radius=0.6,
cylinders=((0.23, 5.2), (0.45, 0.8)),
)
Mod('mfx',
'frappy_psi.ips_mercury.SimpleField',
'magnetic field, x-axis',
slot='GRPX',
io='ips',
tolerance=0.0001,
wait_stable_field=0.0,
nunits=2,
target=Param(max=0.6),
ramp=0.225,
)
Mod('mfy',
'frappy_psi.ips_mercury.SimpleField',
'magnetic field, y axis',
slot='GRPY',
io='ips',
tolerance=0.0001,
wait_stable_field=0.0,
nunits=2,
target=Param(max=0.6),
ramp=0.225,
)
Mod('mfz',
'frappy_psi.ips_mercury.Field',
'magnetic field, z-axis',
slot='GRPZ',
io='ips',
tolerance=0.0001,
target=Param(max=5.2),
mode='DRIVEN',
ramp=0.52,
)

View File

@ -1,6 +1,6 @@
Node('flowsas.psi.ch', Node('flowsas.psi.ch',
'flowsas test motors', 'flowsas test motors',
'tcp://5000', 'tcp://3000',
) )
#Mod('mot_io', #Mod('mot_io',
@ -14,7 +14,7 @@ Node('flowsas.psi.ch',
# 'horizontal axis', # 'horizontal axis',
# axis = 'X', # axis = 'X',
# io = 'mot_io', # io = 'mot_io',
# encoder_mode= 'NO', # encoder_mode = 'NO',
# ) # )
#Mod('vmot', #Mod('vmot',
@ -28,7 +28,7 @@ Node('flowsas.psi.ch',
Mod('syr_io', Mod('syr_io',
'frappy_psi.cetoni_pump.LabCannBus', 'frappy_psi.cetoni_pump.LabCannBus',
'Module for bus', 'Module for bus',
deviceconfig = "/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/config/dual_pumps", deviceconfig = "/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/config/conti_flow",
) )
Mod('syr1', Mod('syr1',
@ -37,7 +37,7 @@ Mod('syr1',
io='syr_io', io='syr_io',
pump_name = "Nemesys_S_1_Pump", pump_name = "Nemesys_S_1_Pump",
valve_name = "Nemesys_S_1_Valve", valve_name = "Nemesys_S_1_Valve",
inner_diameter_set = 10, inner_diameter_set = 14.5673,
piston_stroke_set = 60, piston_stroke_set = 60,
) )
@ -47,6 +47,14 @@ Mod('syr2',
io='syr_io', io='syr_io',
pump_name = "Nemesys_S_2_Pump", pump_name = "Nemesys_S_2_Pump",
valve_name = "Nemesys_S_2_Valve", valve_name = "Nemesys_S_2_Valve",
inner_diameter_set = 1, inner_diameter_set = 14.5673,
piston_stroke_set = 60,
)
Mod('contiflow',
'frappy_psi.cetoni_pump.ContiFlowPump',
'Continuous flow pump',
io='syr_io',
inner_diameter_set = 14.5673,
piston_stroke_set = 60, piston_stroke_set = 60,
) )

View File

@ -0,0 +1,12 @@
Node('flowsas.psi.ch',
'peristaltic pump',
'tcp://3000',
)
Mod('peripump',
'frappy_psi.gilsonpump.PeristalticPump',
'Peristaltic pump',
addr_AO = 'ao1',
addr_dir_relay = 'o1',
addr_run_relay = 'o2',
)

13
cfg/pressureTest_cfg.py Normal file
View File

@ -0,0 +1,13 @@
Node('vf.psi.ch',
'small vacuum furnace',
'tcp://5000',
)
Mod('p',
'frappy_psi.ionopimax.VoltageInput',
'Vacuum pressure',
addr = 'av2',
rawrange = (0, 10),
valuerange = (0, 10),
value = Param(unit='V'),
)

11
cfg/rheotrigger_cfg.py Normal file
View File

@ -0,0 +1,11 @@
Node('flowsas.psi.ch',
'rheometer triggering',
'tcp://3000',
)
Mod('rheo',
'frappy_psi.rheo_trigger.RheoTrigger',
'Trigger for the rheometer',
addr='dt1',
doBeep = False,
)

View File

@ -282,7 +282,6 @@ class SecopClient(ProxyClient):
self.nodename = uri self.nodename = uri
self._lock = RLock() self._lock = RLock()
self._shutdown = Event() self._shutdown = Event()
self.cleanup = []
def __del__(self): def __del__(self):
try: try:
@ -298,10 +297,6 @@ class SecopClient(ProxyClient):
with self._lock: with self._lock:
if self.io: if self.io:
return return
self.txq = queue.Queue(30)
self.pending = queue.Queue(30)
self.active_requests.clear()
self.cleanup.clear()
if self.online: if self.online:
self._set_state(True, 'reconnecting') self._set_state(True, 'reconnecting')
else: else:
@ -373,12 +368,6 @@ class SecopClient(ProxyClient):
noactivity = 0 noactivity = 0
try: try:
while self._running: while self._running:
while self.cleanup:
entry = self.cleanup.pop()
for key, prev in self.active_requests.items():
if prev is entry:
self.active_requests.pop(key)
break
# may raise ConnectionClosed # may raise ConnectionClosed
reply = self.io.readline() reply = self.io.readline()
if reply is None: if reply is None:
@ -416,14 +405,6 @@ class SecopClient(ProxyClient):
self.updateValue(module, param, value, timestamp, readerror) self.updateValue(module, param, value, timestamp, readerror)
except KeyError: except KeyError:
pass # ignore updates of unknown parameters pass # ignore updates of unknown parameters
except Exception as e:
self.log.debug(f'error when updating %s:%s %r', module, param, value)
try:
# catch errors in callback functions
self.updateValue(module, param, None, timestamp,
type(e)(f'{e} - raised on client side'))
except Exception as ee:
self.log.warn(f'can not handle error update %r for %s:%s: %r', e, module, param, ee)
if action in (EVENTREPLY, ERRORPREFIX + EVENTREPLY): if action in (EVENTREPLY, ERRORPREFIX + EVENTREPLY):
continue continue
try: try:
@ -610,10 +591,8 @@ class SecopClient(ProxyClient):
def get_reply(self, entry): def get_reply(self, entry):
"""wait for reply and return it""" """wait for reply and return it"""
if not entry[1].wait(10): # event if not entry[1].wait(10): # event
self.cleanup.append(entry)
raise TimeoutError('no response within 10s') raise TimeoutError('no response within 10s')
if not entry[2]: # reply if not entry[2]: # reply
# no cleanup needed as self.active_requests will be cleared on connect
raise ConnectionError('connection closed before reply') raise ConnectionError('connection closed before reply')
action, _, data = entry[2] # pylint: disable=unpacking-non-sequence action, _, data = entry[2] # pylint: disable=unpacking-non-sequence
if action.startswith(ERRORPREFIX): if action.startswith(ERRORPREFIX):

View File

@ -1,10 +1,6 @@
import sys from frappy.gui.qt import QCheckBox, QComboBox, QLineEdit, pyqtSignal
from frappy.gui.qt import QCheckBox, QComboBox, QDoubleSpinBox, QLineEdit, \ from frappy.datatypes import BoolType, EnumType
QSpinBox, pyqtSignal
from frappy.datatypes import BoolType, EnumType, FloatRange, IntRange, \
StringType, TextType
# ArrayOf, BLOBType, FloatRange, IntRange, StringType, StructOf, TextType, TupleOf # ArrayOf, BLOBType, FloatRange, IntRange, StringType, StructOf, TextType, TupleOf
@ -13,24 +9,11 @@ def get_input_widget(datatype, parent=None):
return { return {
EnumType: EnumInput, EnumType: EnumInput,
BoolType: BoolInput, BoolType: BoolInput,
IntRange: IntInput,
StringType: StringInput,
TextType: StringInput,
}.get(datatype.__class__, GenericInput)(datatype, parent) }.get(datatype.__class__, GenericInput)(datatype, parent)
class InputBase: class GenericInput(QLineEdit):
submitted = pyqtSignal() submitted = pyqtSignal()
input_feedback = pyqtSignal(str)
def get_input(self):
raise NotImplementedError
def submit(self):
self.submitted.emit()
class GenericInput(InputBase, QLineEdit):
def __init__(self, datatype, parent=None): def __init__(self, datatype, parent=None):
super().__init__(parent) super().__init__(parent)
self.datatype = datatype self.datatype = datatype
@ -40,28 +23,12 @@ class GenericInput(InputBase, QLineEdit):
def get_input(self): def get_input(self):
return self.datatype.from_string(self.text()) return self.datatype.from_string(self.text())
def submit(self):
class StringInput(GenericInput): self.submitted.emit()
def __init__(self, datatype, parent=None):
super().__init__(datatype, parent)
class IntInput(InputBase, QSpinBox): class EnumInput(QComboBox):
def __init__(self, datatype, parent=None): submitted = pyqtSignal()
super().__init__(parent)
self.datatype = datatype
# we dont use setMaximum and setMinimum because it is quite restrictive
# when typing, so set it as high as possible
self.setMaximum(2147483647)
self.setMinimum(-2147483648)
self.lineEdit().returnPressed.connect(self.submit)
def get_input(self):
return self.datatype(self.value())
class EnumInput(InputBase, QComboBox):
def __init__(self, datatype, parent=None): def __init__(self, datatype, parent=None):
super().__init__(parent) super().__init__(parent)
self.setPlaceholderText('choose value') self.setPlaceholderText('choose value')
@ -78,11 +45,18 @@ class EnumInput(InputBase, QComboBox):
def get_input(self): def get_input(self):
return self._map[self.currentIndex()].value return self._map[self.currentIndex()].value
def submit(self):
self.submitted.emit()
class BoolInput(InputBase, QCheckBox):
class BoolInput(QCheckBox):
submitted = pyqtSignal()
def __init__(self, datatype, parent=None): def __init__(self, datatype, parent=None):
super().__init__(parent) super().__init__(parent)
self.datatype = datatype self.datatype = datatype
def get_input(self): def get_input(self):
return self.isChecked() return self.isChecked()
def submit(self):
self.submitted.emit()

View File

@ -24,9 +24,9 @@ from frappy.gui.qt import QColor, QDialog, QHBoxLayout, QIcon, QLabel, \
QLineEdit, QMessageBox, QPropertyAnimation, QPushButton, Qt, QToolButton, \ QLineEdit, QMessageBox, QPropertyAnimation, QPushButton, Qt, QToolButton, \
QWidget, pyqtProperty, pyqtSignal QWidget, pyqtProperty, pyqtSignal
from frappy.gui.inputwidgets import get_input_widget
from frappy.gui.util import Colors, loadUi from frappy.gui.util import Colors, loadUi
from frappy.gui.valuewidgets import get_widget from frappy.gui.valuewidgets import get_widget
from frappy.gui.inputwidgets import get_input_widget
class CommandDialog(QDialog): class CommandDialog(QDialog):
@ -54,11 +54,7 @@ class CommandDialog(QDialog):
self.resize(self.sizeHint()) self.resize(self.sizeHint())
def get_value(self): def get_value(self):
try: return True, self.widgets[0].get_value()
return self.widgets[0].get_value()
except Exception as e:
QMessageBox.warning(self.parent(), 'Operation failed', str(e))
return None
def exec(self): def exec(self):
if super().exec(): if super().exec():
@ -99,9 +95,8 @@ class CommandButton(QPushButton):
if self._argintype: if self._argintype:
dlg = CommandDialog(self._cmdname, self._argintype) dlg = CommandDialog(self._cmdname, self._argintype)
args = dlg.exec() args = dlg.exec()
if args is not None: if args: # not 'Cancel' clicked
# no errors when converting value and 'Cancel' wasn't clicked self._cb(self._cmdname, args[1])
self._cb(self._cmdname, args)
else: else:
# no need for arguments # no need for arguments
self._cb(self._cmdname, None) self._cb(self._cmdname, None)
@ -447,8 +442,8 @@ class ModuleWidget(QWidget):
self.paramDetails.emit(self._name, param) self.paramDetails.emit(self._name, param)
def _button_pressed(self, param): def _button_pressed(self, param):
target = self._paramInputs[param].get_input()
try: try:
target = self._paramInputs[param].get_input()
self._node.setParameter(self._name, param, target) self._node.setParameter(self._name, param, target)
except Exception as e: except Exception as e:
QMessageBox.warning(self.parent(), 'Operation failed', str(e)) QMessageBox.warning(self.parent(), 'Operation failed', str(e))

View File

@ -42,10 +42,10 @@ try:
QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, QGridLayout, \ QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, QGridLayout, \
QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit, QMainWindow, \ QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit, QMainWindow, \
QMenu, QMessageBox, QPlainTextEdit, QPushButton, QRadioButton, \ QMenu, QMessageBox, QPlainTextEdit, QPushButton, QRadioButton, \
QScrollArea, QSizePolicy, QSlider, QSpacerItem, QSpinBox, QStyle, \ QScrollArea, QSizePolicy, QSpacerItem, QSpinBox, QStyle, \
QStyleOptionTab, QStylePainter, QTabBar, QTabWidget, QTextEdit, \ QStyleOptionTab, QStylePainter, QTabBar, QTabWidget, QTextEdit, \
QToolButton, QTreeView, QTreeWidget, QTreeWidgetItem, QVBoxLayout, \ QToolButton, QTreeView, QTreeWidget, QTreeWidgetItem, QVBoxLayout, \
QWidget QWidget,QSlider
import frappy.gui.resources_qt6 import frappy.gui.resources_qt6
@ -62,9 +62,9 @@ except ImportError as e:
QDialog, QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, \ QDialog, QDialogButtonBox, QDoubleSpinBox, QFileDialog, QFrame, \
QGridLayout, QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit, \ QGridLayout, QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit, \
QMainWindow, QMenu, QMessageBox, QPlainTextEdit, QPushButton, \ QMainWindow, QMenu, QMessageBox, QPlainTextEdit, QPushButton, \
QRadioButton, QScrollArea, QShortcut, QSizePolicy, QSlider, \ QRadioButton, QScrollArea, QShortcut, QSizePolicy, QSpacerItem, \
QSpacerItem, QSpinBox, QStyle, QStyleOptionTab, QStylePainter, \ QSpinBox, QStyle, QStyleOptionTab, QStylePainter, QTabBar, \
QTabBar, QTabWidget, QTextEdit, QToolButton, QTreeView, QTreeWidget, \ QTabWidget, QTextEdit, QToolButton, QTreeView, QTreeWidget, \
QTreeWidgetItem, QVBoxLayout, QWidget QTreeWidgetItem, QVBoxLayout, QWidget, QSlider
import frappy.gui.resources_qt5 import frappy.gui.resources_qt5

View File

@ -592,7 +592,7 @@ class Module(HasAccessibles):
if not self.io.triggerPoll: if not self.io.triggerPoll:
# when self.io.enablePoll is False, triggerPoll is not # when self.io.enablePoll is False, triggerPoll is not
# created for self.io in the else clause below # created for self.io in the else clause below
self.io.triggerPoll = threading.Event() self.triggerPoll = threading.Event()
else: else:
self.triggerPoll = threading.Event() self.triggerPoll = threading.Event()
self.polledModules.append(self) self.polledModules.append(self)

View File

@ -98,16 +98,6 @@ class Accessible(HasProperties):
props.append(f'{k}={v!r}') props.append(f'{k}={v!r}')
return f"{self.__class__.__name__}({', '.join(props)})" return f"{self.__class__.__name__}({', '.join(props)})"
def fixExport(self):
if self.export is True:
predefined_cls = PREDEFINED_ACCESSIBLES.get(self.name)
if predefined_cls is None:
self.export = '_' + self.name
elif isinstance(self, predefined_cls):
self.export = self.name
else:
raise ProgrammingError(f'can not use {self.name!r} as name of a {type(self).__name__}')
class Parameter(Accessible): class Parameter(Accessible):
"""defines a parameter """defines a parameter
@ -235,7 +225,18 @@ class Parameter(Accessible):
self.name = name self.name = name
if isinstance(self.datatype, EnumType): if isinstance(self.datatype, EnumType):
self.datatype.set_name(name) self.datatype.set_name(name)
self.fixExport()
if self.export is True:
predefined_cls = PREDEFINED_ACCESSIBLES.get(self.name, None)
if predefined_cls is Parameter:
self.export = self.name
elif predefined_cls is None:
self.export = '_' + self.name
else:
raise ProgrammingError(f'can not use {self.name!r} as name of a Parameter')
if 'export' in self.ownProperties:
# avoid export=True overrides export=<name>
self.ownProperties['export'] = self.export
def clone(self, properties, **kwds): def clone(self, properties, **kwds):
"""return a clone of ourselfs with inherited properties""" """return a clone of ourselfs with inherited properties"""
@ -279,7 +280,7 @@ class Parameter(Accessible):
:param modobj: final call, called from Module.__init__ :param modobj: final call, called from Module.__init__
""" """
self.fixExport()
if self.constant is not None: if self.constant is not None:
constant = self.datatype(self.constant) constant = self.datatype(self.constant)
# The value of the `constant` property should be the # The value of the `constant` property should be the
@ -406,8 +407,18 @@ class Command(Accessible):
if self.func is None: if self.func is None:
raise ProgrammingError(f'Command {owner.__name__}.{name} must be used as a method decorator') raise ProgrammingError(f'Command {owner.__name__}.{name} must be used as a method decorator')
self.fixExport()
self.datatype = CommandType(self.argument, self.result) self.datatype = CommandType(self.argument, self.result)
if self.export is True:
predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
if predefined_cls is Command:
self.export = name
elif predefined_cls is None:
self.export = '_' + name
else:
raise ProgrammingError(f'can not use {name!r} as name of a Command') from None
if 'export' in self.ownProperties:
# avoid export=True overrides export=<name>
self.ownProperties['export'] = self.export
if not self._inherit: if not self._inherit:
for key, pobj in self.properties.items(): for key, pobj in self.properties.items():
if key not in self.propertyValues: if key not in self.propertyValues:
@ -444,7 +455,6 @@ class Command(Accessible):
"""return a clone of ourselfs with inherited properties""" """return a clone of ourselfs with inherited properties"""
res = type(self)(**kwds) res = type(self)(**kwds)
res.name = self.name res.name = self.name
self.fixExport()
res.func = self.func res.func = self.func
res.init(properties) res.init(properties)
res.init(res.ownProperties) res.init(res.ownProperties)

View File

@ -47,11 +47,9 @@ def make_update(modulename, pobj):
if pobj.readerror: if pobj.readerror:
return (ERRORPREFIX + EVENTREPLY, f'{modulename}:{pobj.export}', return (ERRORPREFIX + EVENTREPLY, f'{modulename}:{pobj.export}',
# error-report ! # error-report !
[pobj.readerror.name, str(pobj.readerror), [pobj.readerror.name, str(pobj.readerror), {'t': pobj.timestamp}])
{'t': pobj.timestamp} if pobj.timestamp else {}])
return (EVENTREPLY, f'{modulename}:{pobj.export}', return (EVENTREPLY, f'{modulename}:{pobj.export}',
[pobj.export_value(), [pobj.export_value(), {'t': pobj.timestamp}])
{'t': pobj.timestamp} if pobj.timestamp else {}])
class Dispatcher: class Dispatcher:

View File

@ -1,237 +0,0 @@
# *****************************************************************************
# 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:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""The common parts of the SECNodes outside interfaces"""
import sys
import threading
from frappy.errors import SECoPError
from frappy.lib import formatException, formatExtendedStack, \
formatExtendedTraceback
from frappy.protocol.messages import ERRORPREFIX, HELPREPLY, HELPREQUEST, \
HelpMessage
class DecodeError(Exception):
def __init__(self, message, raw_msg):
super().__init__(message)
self._raw_msg = raw_msg
@property
def raw_msg(self):
return self._raw_msg
class ConnectionClose(Exception):
"""Indicates that receive quit due to an error."""
class RequestHandler:
"""Base class for the request handlers.
This is an extended copy of the BaseRequestHandler from socketserver.
To make a new interface, implement these methods:
ingest, next_message, decode_message, receive, send_reply and format
and extend (override) setup() and finish() if needed.
For an example, have a look at TCPRequestHandler.
"""
# Methods from BaseRequestHandler
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.log = None
try:
self.setup()
self.handle()
except Exception:
if self.log:
self.log.error(formatException())
else:
server.log.error(formatException())
finally:
self.finish()
def setup(self):
self.log = self.server.log
self.log.info("new connection %s", self.format())
# notify dispatcher of us
self.server.dispatcher.add_connection(self)
self.send_lock = threading.Lock()
self.running = True
# overwrite this with an appropriate buffer if needed
self.data = None
def handle(self):
"""handle a new connection"""
# copy state info
serverobj = self.server
# copy relevant settings from Interface
detailed_errors = serverobj.detailed_errors
# start serving
while self.running:
try:
newdata = self.receive()
if newdata is None:
# no new data during read, continue
continue
self.ingest(newdata)
except ConnectionClose:
# either normal close or error in receive
return
# put data into (de-) framer,
# de-frame data with next_message() and decode it
# call dispatcher.handle_request(self, message)
# dispatcher will queue the reply before returning
while self.running:
try:
msg = self.next_message()
if msg is None:
break # no more messages to process
except DecodeError as err:
# we have to decode 'origin' here
# use latin-1, as utf-8 or ascii may lead to encoding errors
msg = err.raw_msg.decode('latin-1').split(' ', 3) + [
None
] # make sure len(msg) > 1
result = (
ERRORPREFIX + msg[0],
msg[1],
[
'InternalError', str(err),
{
'exception': formatException(),
'traceback': formatExtendedStack()
}
]
)
print('--------------------')
print(formatException())
print('--------------------')
print(formatExtendedTraceback(sys.exc_info()))
print('====================')
else:
try:
if msg[0] == HELPREQUEST:
self.handle_help()
result = (HELPREPLY, None, None)
else:
result = serverobj.dispatcher.handle_request(self,
msg)
except SECoPError as err:
result = (
ERRORPREFIX + msg[0],
msg[1],
[
err.name,
str(err),
{
'exception': formatException(),
'traceback': formatExtendedStack()
}
]
)
except Exception as err:
# create Error Obj instead
result = (
ERRORPREFIX + msg[0],
msg[1],
[
'InternalError',
repr(err),
{
'exception': formatException(),
'traceback': formatExtendedStack()
}
]
)
print('--------------------')
print(formatException())
print('--------------------')
print(formatExtendedTraceback(sys.exc_info()))
print('====================')
if not result:
self.log.error('empty result upon msg %s', repr(msg))
if result[0].startswith(ERRORPREFIX) and not detailed_errors:
# strip extra information
result[2][2].clear()
self.send_reply(result)
def handle_help(self):
for idx, line in enumerate(HelpMessage.splitlines()):
# not sending HELPREPLY here, as there should be only one reply for
# every request
self.send_reply(('_', f'{idx + 1}', line))
def finish(self):
"""called when handle() terminates, i.e. the socket closed"""
self.log.info('closing connection %s', self.format())
# notify dispatcher
self.server.dispatcher.remove_connection(self)
# Methods for implementing in derived classes:
def ingest(self, newdata):
"""Put the new data into the buffer."""
raise NotImplementedError
def next_message(self):
"""Get the next decoded message from the buffer.
Has to return a triple of (MESSAGE, specifier, data) or None, in case
there are no further messages in the receive queue.
If there is an Error during decoding, this method has to raise a
DecodeError.
"""
raise NotImplementedError
def receive(self):
"""Receive data from the link.
Should return the received data or None if there was nothing new. Has
to raise a ConnectionClose on shutdown of the connection or on errors
that are not recoverable.
"""
raise NotImplementedError
def send_reply(self, data):
"""send reply
stops recv loop on error
"""
raise NotImplementedError
def format(self):
"""
Format available connection data into something recognizable for
logging.
For example, the remote IP address or a connection identifier.
"""
raise NotImplementedError
# TODO: server baseclass?

View File

@ -18,77 +18,122 @@
# Markus Zolliker <markus.zolliker@psi.ch> # Markus Zolliker <markus.zolliker@psi.ch>
# #
# ***************************************************************************** # *****************************************************************************
"""TCP interface to the SECoP Server""" """provides tcp interface to the SECoP Server"""
import errno import errno
import os import os
import socket import socket
import socketserver import socketserver
import sys
import threading
import time import time
from frappy.datatypes import BoolType, StringType from frappy.datatypes import BoolType, StringType
from frappy.lib import SECoP_DEFAULT_PORT from frappy.errors import SECoPError
from frappy.lib import formatException, formatExtendedStack, \
formatExtendedTraceback, SECoP_DEFAULT_PORT
from frappy.properties import Property from frappy.properties import Property
from frappy.protocol.interface import decode_msg, encode_msg_frame, get_msg from frappy.protocol.interface import decode_msg, encode_msg_frame, get_msg
from frappy.protocol.interface.handler import ConnectionClose, \ from frappy.protocol.messages import ERRORPREFIX, HELPREPLY, HELPREQUEST, \
RequestHandler, DecodeError HelpMessage
from frappy.protocol.messages import HELPREQUEST
MESSAGE_READ_SIZE = 1024 MESSAGE_READ_SIZE = 1024
HELP = HELPREQUEST.encode()
def format_address(addr): class TCPRequestHandler(socketserver.BaseRequestHandler):
if len(addr) == 2:
return '%s:%d' % addr
address, port = addr[0:2]
if address.startswith('::ffff'):
return '%s:%d' % (address[7:], port)
return '[%s]:%d' % (address, port)
class TCPRequestHandler(RequestHandler):
def setup(self): def setup(self):
super().setup() self.log = self.server.log
self.request.settimeout(1) self.running = True
self.data = b'' self.send_lock = threading.Lock()
def finish(self): def handle(self):
"""called when handle() terminates, i.e. the socket closed""" """handle a new tcp-connection"""
super().finish() # copy state info
# close socket mysocket = self.request
try: clientaddr = self.client_address
self.request.shutdown(socket.SHUT_RDWR) serverobj = self.server
except Exception:
pass
finally:
self.request.close()
def ingest(self, newdata): self.log.info("handling new connection from %s", format_address(clientaddr))
self.data += newdata data = b''
def next_message(self): # notify dispatcher of us
try: serverobj.dispatcher.add_connection(self)
message, self.data = get_msg(self.data)
if message is None:
return None
if message.strip() == b'':
return (HELPREQUEST, None, None)
return decode_msg(message)
except Exception as e:
raise DecodeError('exception in receive', raw_msg=message) from e
def receive(self): # copy relevant settings from Interface
try: detailed_errors = serverobj.detailed_errors
data = self.request.recv(MESSAGE_READ_SIZE)
mysocket.settimeout(1)
# start serving
while self.running:
try:
newdata = mysocket.recv(MESSAGE_READ_SIZE)
if not newdata:
# no timeout error, but no new data -> connection closed
return
data = data + newdata
except socket.timeout:
continue
except socket.error as e:
self.log.exception(e)
return
if not data: if not data:
raise ConnectionClose('socket was closed') continue
return data # put data into (de-) framer,
except socket.timeout: # put frames into (de-) coder and if a message appear,
return None # call dispatcher.handle_request(self, message)
except socket.error as e: # dispatcher will queue the reply before returning
self.log.exception(e) while self.running:
raise ConnectionClose() from e origin, data = get_msg(data)
if origin is None:
break # no more messages to process
origin = origin.strip()
if origin in (HELP, b''): # empty string -> send help message
for idx, line in enumerate(HelpMessage.splitlines()):
# not sending HELPREPLY here, as there should be only one reply for every request
self.send_reply(('_', f'{idx + 1}', line))
# ident matches request
self.send_reply((HELPREPLY, None, None))
continue
try:
msg = decode_msg(origin)
except Exception as err:
# we have to decode 'origin' here
# use latin-1, as utf-8 or ascii may lead to encoding errors
msg = origin.decode('latin-1').split(' ', 3) + [None] # make sure len(msg) > 1
result = (ERRORPREFIX + msg[0], msg[1], ['InternalError', str(err),
{'exception': formatException(),
'traceback': formatExtendedStack()}])
print('--------------------')
print(formatException())
print('--------------------')
print(formatExtendedTraceback(sys.exc_info()))
print('====================')
else:
try:
result = serverobj.dispatcher.handle_request(self, msg)
except SECoPError as err:
result = (ERRORPREFIX + msg[0], msg[1], [err.name, str(err),
{'exception': formatException(),
'traceback': formatExtendedStack()}])
except Exception as err:
# create Error Obj instead
result = (ERRORPREFIX + msg[0], msg[1], ['InternalError', repr(err),
{'exception': formatException(),
'traceback': formatExtendedStack()}])
print('--------------------')
print(formatException())
print('--------------------')
print(formatExtendedTraceback(sys.exc_info()))
print('====================')
if not result:
self.log.error('empty result upon msg %s', repr(msg))
if result[0].startswith(ERRORPREFIX) and not detailed_errors:
# strip extra information
result[2][2].clear()
self.send_reply(result)
def send_reply(self, data): def send_reply(self, data):
"""send reply """send reply
@ -111,9 +156,18 @@ class TCPRequestHandler(RequestHandler):
self.log.error('ERROR in send_reply %r', e) self.log.error('ERROR in send_reply %r', e)
self.running = False self.running = False
def format(self): def finish(self):
return f'from {format_address(self.client_address)}' """called when handle() terminates, i.e. the socket closed"""
self.log.info('closing connection from %s', format_address(self.client_address))
# notify dispatcher
self.server.dispatcher.remove_connection(self)
# close socket
try:
self.request.shutdown(socket.SHUT_RDWR)
except Exception:
pass
finally:
self.request.close()
class DualStackTCPServer(socketserver.ThreadingTCPServer): class DualStackTCPServer(socketserver.ThreadingTCPServer):
"""Subclassed to provide IPv6 capabilities as socketserver only uses IPv4""" """Subclassed to provide IPv6 capabilities as socketserver only uses IPv4"""
@ -176,3 +230,12 @@ class TCPServer(DualStackTCPServer):
if ntry: if ntry:
self.log.warning('tried again %d times after "Address already in use"', ntry) self.log.warning('tried again %d times after "Address already in use"', ntry)
self.log.info("TCPServer initiated") self.log.info("TCPServer initiated")
def format_address(addr):
if len(addr) == 2:
return '%s:%d' % addr
address, port = addr[0:2]
if address.startswith('::ffff'):
return '%s:%d' % (address[7:], port)
return '[%s]:%d' % (address, port)

View File

@ -1,160 +0,0 @@
# *****************************************************************************
# 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:
# Alexander Zaft <a.zaft@fz-juelich.de>
#
# *****************************************************************************
import json
from functools import partial
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
from websockets.sync.server import CloseCode, serve
from frappy.protocol.interface.handler import ConnectionClose, \
RequestHandler, DecodeError
from frappy.protocol.messages import HELPREQUEST
def encode_msg_frame_str(action, specifier=None, data=None):
""" encode a msg_triple into an msg_frame, ready to be sent
action (and optional specifier) are str strings,
data may be an json-yfied python object"""
msg = (action, specifier or '', '' if data is None else json.dumps(data))
return ' '.join(msg).strip()
class WSRequestHandler(RequestHandler):
"""Handles a Websocket connection."""
def __init__(self, conn, server):
self.conn = conn
client_address = conn.remote_address
request = conn.socket
super().__init__(request, client_address, server)
def setup(self):
super().setup()
self.server.connections.add(self)
def finish(self):
"""called when handle() terminates, i.e. the socket closed"""
super().finish()
self.server.connections.discard(self)
# this will be called for a second time if the server is shutting down,
# but in that case it will be a no-op
self.conn.close()
def ingest(self, newdata):
# recv on the websocket connection returns one message, we don't save
# anything in data
self.data = newdata
def next_message(self):
"""split the string into a message triple."""
if self.data is None:
return None
try:
message = self.data.strip()
if message == '':
return HELPREQUEST, None, None
res = message.split(' ', 2) + ['', '']
action, specifier, data = res[0:3]
self.data = None
return (
action,
specifier or None,
None if data == '' else json.loads(data)
)
except Exception as e:
raise DecodeError('exception when reading in message',
raw_msg=bytes(message, 'utf-8')) from e
def receive(self):
"""receives one message from the websocket."""
try:
return self.conn.recv()
except TimeoutError:
return None
except ConnectionClosedOK:
raise ConnectionClose from None
except ConnectionClosedError as e:
self.log.error('No close frame received from %s', self.format())
raise ConnectionClose from e
except OSError as e:
self.log.exception(e)
raise ConnectionClose from e
def send_reply(self, data):
"""send reply
stops recv loop on error (including timeout when output buffer full for
more than 1 sec)
"""
if not data:
self.log.error('should not reply empty data!')
return
outdata = encode_msg_frame_str(*data)
with self.send_lock:
if self.running:
try:
self.conn.send(outdata)
except (BrokenPipeError, IOError) as e:
self.log.debug('send_reply got an %r, connection closed?',
e)
self.running = False
except Exception as e:
self.log.error('ERROR in send_reply %r', e)
self.running = False
def format(self):
return f'{self.conn.id} from {self.client_address}'
class WSServer:
"""Server for providing a websocket interface.
Implementation note:
The websockets library doesn't provide an option to subclass its server, so
we take the returned value as an attribute and provide the neccessary
function calls.
"""
def __init__(self, name, logger, options, srv):
self.connections = set() # keep track for shutting down
self.dispatcher = srv.dispatcher
self.name = name
self.log = logger
self.port = int(options.pop('uri').split('://', 1)[-1])
self.detailed_errors = options.pop('detailed_errors', False)
handle = partial(WSRequestHandler, server=self)
# websockets only gives the serve method without an option to subclass
self.ws_server = serve(handle, '', self.port, logger=logger)
self.log.info("Websocket server %s binding to port %d", name, self.port)
def serve_forever(self):
self.ws_server.serve_forever()
def shutdown(self):
for c in list(self.connections):
c.conn.close(code=CloseCode.GOING_AWAY, reason='shutting down')
self.ws_server.shutdown()
def __enter__(self):
return self
def __exit__(self, *args):
return self.shutdown()

View File

@ -53,7 +53,6 @@ except ImportError:
class Server: class Server:
INTERFACES = { INTERFACES = {
'tcp': 'protocol.interface.tcp.TCPServer', 'tcp': 'protocol.interface.tcp.TCPServer',
'ws': 'protocol.interface.ws.WSServer',
} }
_restart = True _restart = True

View File

@ -4,13 +4,15 @@ if libpath not in sys.path:
sys.path.append(libpath) sys.path.append(libpath)
from frappy.core import Drivable, Readable, StringIO, HasIO, FloatRange, IntRange, StringType, BoolType, EnumType, \ from frappy.core import Drivable, Readable, StringIO, HasIO, FloatRange, IntRange, StringType, BoolType, EnumType, \
Parameter, Property, PersistentParam, Command, IDLE, BUSY, ERROR, Attached Parameter, Property, PersistentParam, Command, IDLE, BUSY, ERROR, WARN, Attached, Module
from qmixsdk import qmixbus from qmixsdk import qmixbus
from qmixsdk import qmixpump from qmixsdk import qmixpump
from qmixsdk import qmixvalve from qmixsdk import qmixvalve
from qmixsdk.qmixpump import ContiFlowProperty, ContiFlowSwitchingMode
from qmixsdk.qmixbus import UnitPrefix, TimeUnit from qmixsdk.qmixbus import UnitPrefix, TimeUnit
import time
class LabCannBus(Readable): class LabCannBus(Module):
deviceconfig = Property('config files', StringType(),default="/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/config/dual_pumps") deviceconfig = Property('config files', StringType(),default="/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/config/dual_pumps")
def earlyInit(self): def earlyInit(self):
@ -22,11 +24,15 @@ class LabCannBus(Readable):
super().initModule() super().initModule()
self.bus.start() self.bus.start()
with open('/sys/class/ionopimax/buzzer/beep', 'w') as f :
f.write('200 50 3')
def shutdownModule(self): def shutdownModule(self):
"""Not so gracefully close the connection""" """Close the connection"""
self.bus.stop() self.bus.stop()
self.bus.close() self.bus.close()
class SyringePump(Drivable): class SyringePump(Drivable):
io = Attached() io = Attached()
pump_name = Property('name of pump', StringType(),default="Nemesys_S_1_Pump") pump_name = Property('name of pump', StringType(),default="Nemesys_S_1_Pump")
@ -35,18 +41,26 @@ class SyringePump(Drivable):
inner_diameter_set = Property('inner diameter', FloatRange(), default=1) inner_diameter_set = Property('inner diameter', FloatRange(), default=1)
piston_stroke_set = Property('piston stroke', FloatRange(), default=60) piston_stroke_set = Property('piston stroke', FloatRange(), default=60)
value = PersistentParam('volume', FloatRange(unit='mL')) value = Parameter('volume', FloatRange(unit='uL'))
status = PersistentParam() status = Parameter()
max_flow_rate = Parameter('max flow rate', FloatRange(0,100000, unit='mL/min',), readonly=True) max_flow_rate = Parameter('max flow rate', FloatRange(0,100000, unit='uL/s',), readonly=True)
max_volume = Parameter('max volume', FloatRange(0,100000, unit='mL',), readonly=True) max_volume = Parameter('max volume', FloatRange(0,100000, unit='uL',), readonly=True)
target_flow_rate = Parameter('target flow rate', FloatRange(unit='mL/min'), readonly=False)
real_flow_rate = Parameter('actual flow rate', FloatRange(unit='mL/min'), readonly=True) target_flow_rate = Parameter('target flow rate', FloatRange(unit='uL/s'), readonly=False)
target = Parameter('target volume', FloatRange(unit='mL'), readonly=False) real_flow_rate = Parameter('actual flow rate', FloatRange(unit='uL/s'), readonly=True)
target = Parameter('target volume', FloatRange(unit='uL'), readonly=False)
no_of_valve_pos = Property('number of valve positions', IntRange(0,10), default=1) no_of_valve_pos = Property('number of valve positions', IntRange(0,10), default=1)
valve_pos = Parameter('valve position', EnumType('valve', CLOSED=0, APP=1, RES=2, OPEN=3), readonly=False) valve_pos = Parameter('valve position', EnumType('valve', CLOSED=0, APP=1, RES=2, OPEN=3), readonly=False)
force = Parameter('syringe force', FloatRange(unit='kN'), readonly=True)
max_force = Parameter('max device force', FloatRange(unit='kN'), readonly=True)
force_limit = Parameter('user force limit', FloatRange(unit='kN'), readonly=False)
_resolving_force_overload = False
def initModule(self): def initModule(self):
super().initModule() super().initModule()
@ -66,32 +80,41 @@ class SyringePump(Drivable):
self.pump.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set) self.pump.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set)
self.pump.set_volume_unit(qmixpump.UnitPrefix.milli, qmixpump.VolumeUnit.litres) self.pump.set_volume_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres)
self.pump.set_flow_unit(qmixpump.UnitPrefix.milli, qmixpump.VolumeUnit.litres, qmixpump.TimeUnit.per_minute) self.pump.set_flow_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres, qmixpump.TimeUnit.per_second)
self.max_flow_rate = self.pump.get_flow_rate_max() self.max_flow_rate = round(self.pump.get_flow_rate_max(),2)
self.max_volume = self.pump.get_volume_max() self.max_volume = round(self.pump.get_volume_max(),2)
self.no_of_valve_pos = self.valve.number_of_valve_positions()
self.valve_pos = self.valve.actual_valve_position() self.valve_pos = self.valve.actual_valve_position()
self.target_flow_rate = self.max_flow_rate * 0.5 self.target_flow_rate = round(self.max_flow_rate * 0.5,2)
self.target = self.pump.get_fill_level() self.target = max(0, round(self.pump.get_fill_level(),2))
self.pump.enable_force_monitoring(True)
self.max_force = self.pump.get_max_device_force()
self.force_limit = self.max_force
def read_value(self): def read_value(self):
return self.pump.get_fill_level() return round(self.pump.get_fill_level(),2)
def write_target(self, target): def write_target(self, target):
self.pump.set_fill_level(target, self.target_flow_rate) if self.read_valve_pos() == 0 :
self.status = BUSY, 'Target changed' self.status = ERROR, 'Cannot pump if valve is closed'
return target self.log.warn('Cannot pump if valve is closed')
return target
else:
self.pump.set_fill_level(target, self.target_flow_rate)
self.status = BUSY, 'Target changed'
self.log.info(f'Started pumping at {self.target_flow_rate} ul/s')
return target
def write_target_flow_rate(self, rate): def write_target_flow_rate(self, rate):
self.pump.target_flow_rate = rate self.target_flow_rate = rate
return rate return rate
def read_real_flow_rate(self): def read_real_flow_rate(self):
return self.pump.get_flow_is() return round(self.pump.get_flow_is(),2)
def read_valve_pos(self): def read_valve_pos(self):
return self.valve.actual_valve_position() return self.valve.actual_valve_position()
@ -100,11 +123,165 @@ class SyringePump(Drivable):
self.valve.switch_valve_to_position(target_pos) self.valve.switch_valve_to_position(target_pos)
return target_pos return target_pos
def read_force(self):
return round(self.pump.read_force_sensor(),3)
def read_force_limit(self):
return self.pump.get_force_limit()
def write_force_limit(self, limit):
self.pump.write_force_limit(limit)
return limit
def read_status(self): def read_status(self):
fault_state = self.pump.is_in_fault_state() fault_state = self.pump.is_in_fault_state()
pumping = self.pump.is_pumping() pumping = self.pump.is_pumping()
pump_enabled = self.pump.is_enabled()
safety_stop_active = self.pump.is_force_safety_stop_active()
if fault_state == True: if fault_state == True:
return ERROR, 'Pump in fault state' return ERROR, 'Pump in fault state'
elif self._resolving_force_overload :
return BUSY, 'Resolving force overload'
elif safety_stop_active:
return ERROR, 'Pressure safety stop'
elif not pump_enabled:
return ERROR, 'Pump not enabled'
elif pumping == True:
return BUSY, f'Pumping {self.real_flow_rate} ul/s'
elif self.read_valve_pos() == 0:
return IDLE, 'Valve closed'
else:
return IDLE, ''
@Command
def stop(self):
self.pump.stop_pumping()
self.target = self.pump.get_fill_level()
self.status = BUSY, 'Stopping'
@Command
def clear_errors(self):
"""Clear fault state and enable pump"""
if self.pump.is_in_fault_state():
self.pump.clear_fault()
self.log.info('Cleared faults')
if not self.pump.is_enabled():
self.pump.enable(True)
self.log.info('Pump was disabled, re-enabling')
self.target = max(0,round(self.value,2))
self.status = IDLE, ''
@Command
def resolve_force_overload(self):
"""Resolve a force overload situation"""
if not self.pump.is_force_safety_stop_active():
self.status = ERROR, 'No force overload detected'
self.log.warn('No force overload to be resolved')
return
self._resolving_force_overload = True
self.status = BUSY, 'Resolving force overload'
self.pump.enable_force_monitoring(False)
flow = 0 - self.pump.get_flow_rate_max() / 100
self.pump.generate_flow(flow)
safety_stop_active = False
while not safety_stop_active:
time.sleep(0.1)
safety_stop_active = self.pump.is_force_safety_stop_active()
self.pump.stop_pumping()
self.pump.enable_force_monitoring(True)
time.sleep(0.3)
self._resolving_force_overload = False
self.status = self.read_status()
class ContiFlowPump(Drivable):
io = Attached()
inner_diameter_set = Property('inner diameter', FloatRange(), default=1)
piston_stroke_set = Property('piston stroke', FloatRange(), default=60)
crossflow_seconds = Property('crossflow duration', FloatRange(unit='s'),default=2)
value = PersistentParam('flow rate', FloatRange(unit='uL/s'))
status = PersistentParam()
max_refill_flow = Parameter('max refill flow', FloatRange(unit='uL/s'), readonly=True)
refill_flow = Parameter('refill flow', FloatRange(unit='uL/s'), readonly=False)
max_flow_rate = Parameter('max flow rate', FloatRange(0,100000, unit='uL/s',), readonly=True)
target = Parameter('target flow rate', FloatRange(unit='uL/s'), readonly=False)
def initModule(self):
super().initModule()
self.pump = qmixpump.ContiFlowPump()
self.pump.lookup_by_name("ContiFlowPump_1")
def initialReads(self):
if self.pump.is_in_fault_state():
self.pump.clear_fault()
if not self.pump.is_enabled():
self.pump.enable(True)
self.syringe_pump1 = self.pump.get_syringe_pump(0)
self.syringe_pump1.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set)
self.syringe_pump2 = self.pump.get_syringe_pump(1)
self.syringe_pump2.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set)
self.pump.set_volume_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres)
self.pump.set_flow_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres, qmixpump.TimeUnit.per_second)
self.pump.set_device_property(ContiFlowProperty.SWITCHING_MODE, ContiFlowSwitchingMode.CROSS_FLOW)
self.max_refill_flow = self.pump.get_device_property(ContiFlowProperty.MAX_REFILL_FLOW)
self.pump.set_device_property(ContiFlowProperty.REFILL_FLOW, self.max_refill_flow / 2.0)
self.pump.set_device_property(ContiFlowProperty.CROSSFLOW_DURATION_S, self.crossflow_seconds)
self.pump.set_device_property(ContiFlowProperty.OVERLAP_DURATION_S, 0)
self.max_flow_rate = self.pump.get_flow_rate_max()
self.target = 0
def read_value(self):
return round(self.pump.get_flow_is(),3)
def write_target(self, target):
if target <= 0:
self.pump.stop_pumping()
self.status = self.read_status()
return 0
else:
self.pump.generate_flow(target)
self.status = BUSY, 'Target changed'
return target
def read_refill_flow(self):
return round(self.pump.get_device_property(ContiFlowProperty.REFILL_FLOW),3)
def write_refill_flow(self, refill_flow):
self.pump.set_device_property(ContiFlowProperty.REFILL_FLOW, refill_flow)
self.max_flow_rate = self.pump.get_flow_rate_max()
return refill_flow
def read_status(self):
fault_state = self.pump.is_in_fault_state()
pumping = self.pump.is_pumping()
pump_enabled = self.pump.is_enabled()
pump_initialised = self.pump.is_initialized()
pump_initialising = self.pump.is_initializing()
if fault_state == True:
return ERROR, 'Pump in fault state'
elif not pump_enabled:
return ERROR, 'Pump not enabled'
elif not pump_initialised:
return WARN, 'Pump not initialised'
elif pump_initialising:
return BUSY, 'Pump initialising'
elif pumping == True: elif pumping == True:
return BUSY, 'Pumping' return BUSY, 'Pumping'
else: else:
@ -113,4 +290,24 @@ class SyringePump(Drivable):
@Command @Command
def stop(self): def stop(self):
self.pump.stop_pumping() self.pump.stop_pumping()
self.target = self.pump.get_fill_level() self.target = 0
self.status = BUSY, 'Stopping'
@Command
def clear_errors(self):
"""Clear fault state and enable pump"""
if self.pump.is_in_fault_state():
self.pump.clear_fault()
self.log.info('Cleared faults')
if not self.pump.is_enabled():
self.pump.enable(True)
self.log.info('Pump was disabled, re-enabling')
self.target = 0
self.status = IDLE, ''
@Command
def initialise(self):
"""Initialise the ConfiFlow pump"""
self.pump.initialize()

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# ***************************************************************************** # *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under # 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 # the terms of the GNU General Public License as published by the Free Software
@ -19,78 +18,79 @@
# ***************************************************************************** # *****************************************************************************
"""vector field""" """vector field"""
import math from frappy.core import Drivable, Done, BUSY, IDLE, WARN, ERROR
from frappy.core import Drivable, Done, BUSY, IDLE, ERROR, Parameter, TupleOf, ArrayOf, FloatRange from frappy.errors import BadValueError
from frappy.errors import RangeError
from frappy_psi.vector import Vector from frappy_psi.vector import Vector
from frappy.states import HasStates, Retry, status_code
DECREASE = 1 DECREASE = 1
INCREASE = 2 INCREASE = 2
class VectorField(HasStates, Vector, Drivable): class VectorField(Vector, Drivable):
sphere_radius = Parameter('max. sphere', datatype=FloatRange(0, 0.7, unit='T'), readonly=True, default=0.6) _state = None
cylinders = Parameter('allowed cylinders (list of radius and height)',
datatype=ArrayOf(TupleOf(FloatRange(0, 0.6, unit='T'), FloatRange(0, 5.2, unit='T')), 1, 9),
readonly=True, default=((0.23, 5.2), (0.45, 0.8)))
def initModule(self): def doPoll(self):
super().initModule() """periodically called method"""
# override check_limits of the components with a check for restrictions on the vector try:
for idx, component in enumerate(self.components): if self._starting:
# first decrease components
def outer_check(target, vector=self, i=idx, inner_check=component.check_target): driving = False
inner_check(target) for target, component in zip(self.target, self.components):
value = [c.value - math.copysign(c.tolerance, c.value) if target * component.value < 0:
for c in vector.components] # change sign: drive to zero first
value[i] = target target = 0
vector._check_limits(value) if abs(target) < abs(component.target):
if target != component.target:
component.check_target = outer_check component.write_target(target)
if component.isDriving():
driving = True
if driving:
return
# now we can go to the final targets
for target, component in zip(self.target, self.components):
component.write_target(target)
self._starting = False
else:
for component in self.components:
if component.isDriving():
return
self.setFastPoll(False)
finally:
super().doPoll()
def merge_status(self): def merge_status(self):
return self.status names = [c.name for c in self.components if c.status[0] >= ERROR]
if names:
return ERROR, 'error in %s' % ', '.join(names)
names = [c.name for c in self.components if c.isDriving()]
if self._state:
# self.log.info('merge %r', [c.status for c in self.components])
if names:
direction = 'down ' if self._state == DECREASE else ''
return BUSY, 'ramping %s%s' % (direction, ', '.join(names))
if self.status[0] == BUSY:
return self.status
return BUSY, 'driving'
if names:
return WARN, 'moving %s directly' % ', '.join(names)
names = [c.name for c in self.components if c.status[0] >= WARN]
if names:
return WARN, 'warnings in %s' % ', '.join(names)
return IDLE, ''
def _check_limits(self, value): def write_target(self, value):
"""check if value is within one of the safe shapes"""
if sum((v ** 2 for v in value)) <= self.sphere_radius ** 2:
return
for r, h in self.cylinders:
if sum(v ** 2 for v in value[0:2]) <= r ** 2 and abs(value[2]) <= h:
return
raise RangeError('vector %s does not fit in any limiting shape' % repr(value))
def write_target(self, target):
"""initiate target change""" """initiate target change"""
# check limits first # first make sure target is valid
for component_target, component in zip(target, self.components): for target, component in zip(self.target, self.components):
# check against limits of individual components # check against limits if individual components
component.check_target(component_target, vector=None) # no outer check here! component.check_limits(target)
self._check_limits(target) if sum(v * v for v in value) > 1:
for component_target, component in zip(target, self.components): raise BadValueError('norm of vector too high')
if component_target * component.value < 0: self.log.info('decrease')
# change sign: drive to zero first self.setFastPoll(True)
component_target = 0 self.target = value
if abs(component_target) > abs(component.value): self._state = DECREASE
continue # do not drive yet self.doPoll()
component.write_target(component_target) self.log.info('done write_target %r', value)
self.start_machine(self.ramp_down, target=target) return Done
return target
@status_code(BUSY)
def ramp_down(self, state):
for target, component in zip(state.target, self.components):
if component.isDriving():
return Retry()
for target, component in zip(state.target, self.components):
component.write_target(target)
return self.final_ramp
@status_code(BUSY)
def final_ramp(self, state):
for component in self.components:
if component.isDriving():
return Retry()
return self.final_status()

104
frappy_psi/gilsonpump.py Normal file
View File

@ -0,0 +1,104 @@
# Author: Wouter Gruenewald<wouter.gruenewald@psi.ch>
from frappy.core import StringType, BoolType, EnumType, FloatRange, Parameter, Property, PersistentParam, Command, IDLE, ERROR, WARN, BUSY, Drivable
class PeristalticPump(Drivable):
value = Parameter('Pump speed', FloatRange(0,100,unit="%"), default=0)
target = Parameter('Target pump speed', FloatRange(0,100,unit="%"), default=0)
status = Parameter()
addr_AO = Property('Address of the analog out', StringType())
addr_dir_relay = Property('Address of the direction relay', StringType())
addr_run_relay = Property('Address of the running relay', StringType())
direction = Parameter('pump direction', EnumType('direction', CLOCKWISE=0, ANTICLOCKWISE=1), default=0, readonly=False)
active = Parameter('pump running', BoolType(), default=False, readonly=False)
def initModule(self):
super().initModule()
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_enabled', 'w') as f :
f.write('0')
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_mode', 'w') as f :
f.write('V')
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'w') as f :
f.write('0')
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_enabled', 'w') as f :
f.write('1')
def shutdownModule(self):
'''Disable analog output'''
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'w') as f :
f.write('0')
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_enabled', 'w') as f :
f.write('0')
def read_value(self):
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'r') as f :
raw_value = f.read().strip('\n')
value = (int(raw_value) / 5000) * 100
return value
def write_target(self, target):
raw_value = (target / 100)*5000
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'w') as f :
f.write(str(int(raw_value)))
return target
def read_direction(self):
with open('/sys/class/ionopimax/digital_out/'+self.addr_dir_relay, 'r') as f :
raw_direction = f.read().strip('\n')
if raw_direction == '0' or raw_direction == 'F':
return 0
if raw_direction == '1' or raw_direction == 'S':
return 1
else:
return None
def write_direction(self, direction):
if direction == 0:
raw_direction = '0'
elif direction == 1:
raw_direction = '1'
with open('/sys/class/ionopimax/digital_out/'+self.addr_dir_relay, 'w') as f :
f.write(raw_direction)
return direction
def read_active(self):
with open('/sys/class/ionopimax/digital_out/'+self.addr_run_relay, 'r') as f :
raw_active = f.read().strip('\n')
if raw_active == '0' or raw_active == 'F':
return False
elif raw_active == '1' or raw_active == 'S':
return True
else:
return None
def write_active(self, active):
if active == False:
raw_active = '0'
elif active == True:
raw_active = '1'
with open('/sys/class/ionopimax/digital_out/'+self.addr_run_relay, 'w') as f :
f.write(raw_active)
return active
def read_status(self):
with open('/sys/class/ionopimax/digital_out/'+self.addr_dir_relay, 'r') as f :
raw_direction = f.read().strip('\n')
with open('/sys/class/ionopimax/digital_out/'+self.addr_run_relay, 'r') as f :
raw_active = f.read().strip('\n')
if raw_direction == 'F' or raw_direction == 'S':
return ERROR, 'Fault on direction relay'
elif raw_active == 'F' or raw_active == 'S':
return ERROR, 'Fault on pump activation relay'
elif self.active == True:
return BUSY, 'Pump running'
else:
return IDLE, ''
@Command
def stop(self):
self.write_active(False)

View File

@ -62,7 +62,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
encoder_mode = Parameter('how to treat the encoder', EnumType('encoder', NO=0, READ=1, CHECK=2), encoder_mode = Parameter('how to treat the encoder', EnumType('encoder', NO=0, READ=1, CHECK=2),
default=1, readonly=False) default=1, readonly=False)
check_limit_switches = Parameter('whethter limit switches are checked',BoolType(), check_limit_switches = Parameter('whether limit switches are checked',BoolType(),
default=0, readonly=False) default=0, readonly=False)
value = PersistentParam('angle', FloatRange(unit='deg')) value = PersistentParam('angle', FloatRange(unit='deg'))
status = PersistentParam() status = PersistentParam()
@ -90,6 +90,8 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
status_bits = ['power stage error', 'undervoltage', 'overtemperature', 'active', status_bits = ['power stage error', 'undervoltage', 'overtemperature', 'active',
'lower switch active', 'upper switch active', 'step failure', 'encoder error'] 'lower switch active', 'upper switch active', 'step failure', 'encoder error']
_doing_reference = False
def get(self, cmd): def get(self, cmd):
return self.communicate(f'{self.address:x}{self.axis}{cmd}') return self.communicate(f'{self.address:x}{self.axis}{cmd}')
@ -178,10 +180,14 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
def doPoll(self): def doPoll(self):
super().doPoll() super().doPoll()
if self._running and not self.isBusy(): if self._running and not self.isBusy() and not self._doing_reference:
if time.time() > self._stopped_at + 5: if time.time() > self._stopped_at + 5:
self.log.warning('stop motor not started by us') self.log.warning('stop motor not started by us')
self.hw_stop() self.hw_stop()
if self._doing_reference and self.get('=H') == 'E' :
self.status = IDLE, ''
self.target = 0
self._doing_reference = False
def read_status(self): def read_status(self):
hexstatus = 0x100 hexstatus = 0x100
@ -207,6 +213,9 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
if status[0] == ERROR: if status[0] == ERROR:
self._blocking_error = status[1] self._blocking_error = status[1]
return status return status
if self._doing_reference and self.get('=H') == 'N':
status = BUSY, 'Doing reference run'
return status
return super().read_status() # status from state machine return super().read_status() # status from state machine
def check_moving(self): def check_moving(self):
@ -346,3 +355,10 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
self.status = 'IDLE', 'after error reset' self.status = 'IDLE', 'after error reset'
self._blocking_error = None self._blocking_error = None
self.target = self.value # clear error in target self.target = self.value # clear error in target
@Command
def make_ref_run(self):
'''Do reference run'''
self._doing_reference = True
self.status = BUSY, 'Doing reference run'
self.communicate(f'{self.address:x}{self.axis}0-')

View File

@ -0,0 +1,70 @@
from frappy.core import StringType, BoolType, Parameter, Property, PersistentParam, Command, IDLE, ERROR, WARN, Writable
import time
class RheoTrigger(Writable):
addr = Property('Port address', StringType())
value = Parameter('Output state', BoolType(), default=0)
target = Parameter('target', BoolType(), default=0, readonly=False)
status = Parameter()
doBeep = Property('Make noise', BoolType(), default=0)
_status = 0
def initModule(self):
super().initModule()
with open('/sys/class/ionopimax/digital_io/'+self.addr+'_mode', 'w') as f :
f.write('out')
if self.doBeep:
with open('/sys/class/ionopimax/buzzer/beep', 'w') as f :
f.write('200 50 3')
def read_value(self):
with open('/sys/class/ionopimax/digital_io/'+self.addr, 'r') as f :
file_value = f.read()
if file_value == '0\n':
value = False
self._status = 0
elif file_value == '1\n':
value = True
self._status = 1
else:
self._status = -1
value = False
return value
def write_target(self,target):
if target == self.value:
return target
else:
with open('/sys/class/ionopimax/digital_io/'+self.addr, 'w') as f :
if target == True:
f.write('1')
elif target == False:
f.write('0')
time.sleep(0.05)
if self.doBeep:
with open('/sys/class/ionopimax/buzzer/beep', 'w') as f :
f.write('200')
self.status = self.read_status()
return target
def read_status(self):
self.value = self.read_value()
if self._status == 0:
return IDLE, 'Signal low'
elif self._status == 1:
return IDLE, 'Signal high'
else:
return ERROR, 'Cannot read status'
@Command
def toggle(self):
"""Toggle output"""
value = self.read_value()
if value == True:
self.write_target(False)
else:
self.write_target(True)

View File

@ -5,8 +5,6 @@ mlzlog >=0.2.0
# daemonizing # daemonizing
psutil psutil
python-daemon >=2.0 python-daemon >=2.0
# websocket interface:
websockets>=11.0
# for zmq interface # for zmq interface
#pyzmq>=13.1.0 #pyzmq>=13.1.0
#for ppms on windows #for ppms on windows

View File

@ -19,7 +19,6 @@
# #
# ***************************************************************************** # *****************************************************************************
from frappy.io import HasIO
from frappy.modules import Module, Attached from frappy.modules import Module, Attached
from frappy.protocol.dispatcher import Dispatcher from frappy.protocol.dispatcher import Dispatcher
@ -30,9 +29,6 @@ class LoggerStub:
info = warning = exception = debug info = warning = exception = debug
handlers = [] handlers = []
def getChild(self, name):
return self
logger = LoggerStub() logger = LoggerStub()
@ -55,7 +51,6 @@ class ServerStub:
def __init__(self): def __init__(self):
self.secnode = SecNodeStub() self.secnode = SecNodeStub()
self.dispatcher = Dispatcher('dispatcher', logger, {}, self) self.dispatcher = Dispatcher('dispatcher', logger, {}, self)
self.log = logger
def test_attach(): def test_attach():
@ -69,22 +64,3 @@ def test_attach():
srv.secnode.add_module(a, 'a') srv.secnode.add_module(a, 'a')
srv.secnode.add_module(m, 'm') srv.secnode.add_module(m, 'm')
assert m.att == a assert m.att == a
def test_attach_hasio_uri():
class TestIO(Module):
def __init__(self, name, logger, cfgdict, srv):
self._uri = cfgdict.pop('uri')
super().__init__(name, logger, cfgdict, srv)
class HasIOTest(HasIO):
ioClass = TestIO
srv = ServerStub()
m = HasIOTest('m', logger, {'description': '', 'uri': 'abc'}, srv)
assert srv.secnode.modules['m_io']._uri == 'abc'
assert m.io == srv.secnode.modules['m_io']
# two modules with the same IO should use the same io module
m2 = HasIOTest('m', logger, {'description': '', 'uri': 'abc'}, srv)
assert m2.io == srv.secnode.modules['m_io']