Skip to content

Commit

Permalink
some more zebra2 changes, with supporting gui
Browse files Browse the repository at this point in the history
  • Loading branch information
coretl committed Nov 30, 2015
1 parent da752de commit 0d1438d
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 24 deletions.
76 changes: 76 additions & 0 deletions docs/devices.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,82 @@ Maybe we should combine the VTypes and Attributes? Then we get::
)
self.exposure.set_pv(0.1)

We should also be able to name VMaps, VTables and VEnums. This would allow us to
created named objects that could be mapped to Java types::

class Xspress3ConfigParams(VMap):
"Parameters to pass to Xspress3"
hdfFile=VString("Hdf filename to write", REQUIRED)
exposure=VDouble("Exposure time of detector", 0.1)
period=VDouble("Time between frames, defaults to exposure", OPTIONAL)
runTimeout=VDouble("Typical time taken for run")
@publish(Xspress3ConfigParams, only_in=DState.canConfig())
def configure(self, args):
args = self.validate(args)
self.do_configure(args)
Can use ordering number in VTypes to work out definition order. However, this stops us
dynamically allocating parameter attributes at runtime and using PV subclasses which
may be an oversight. Maybe::

class Xspress3ConfigParams(VMap):
"Parameters to pass to Xspress3"
hdfFile=Attribute(REQUIRED)
exposure=Attribute(0.1)
period=Attribute(OPTIONAL)
runTimeout=VDouble("Typical time taken for run", OPTIONAL)

def add_all_attributes(self):
self.exposure = PVDouble(
"Exposure time of detector",
self.prefix + "Exposure",
rbv_suff="_RBV")
self.START = VDouble(
"Zebra start value",
setter=self.set_start)
# configure is special, do this in baseclass
for attr in Xspress3ConfigParams.attributes():
self.attributes()[attr].setter = self.partial_configure

@accepts(Xspress3ConfigParams)
@returns(Xspress3ConfigParams)
def validate(self, args):
return super(Xspress3, self).validate(args)

@accepts(from_method="validate")
@valid_states(DState.canConfig())
def configure(self, args):
"""Configure the device"""
args = self.validate(args)
self.do_configure(args)
# this will be called self.partial_configure(exposure=0.1)
def partial_configure(self, **args):
# get values and update args
self.configure(args)
@valid_states(DState.canRun())
def run(self):
"""Run the device"""
typical = self._get_default_times()["runTime"]
extra = self._get_default_times("run") - typical
while True:
todo = 1 - float(self.currentStep) / self.totalSteps
# Sets expiry time that report_wait can check against
Thread.set_timeout(typical * todo + extra)
try:
return self.do_run()
except StateChangedException as e:
if e.state != DState.Rewinding:
raise
Thread.set_timeout(None)
event = self.report_wait()
while event.typ != Event.State and event.value != DState.Running:
event = self.report_wait()

Need to make process globally accessible for this to work

Need a better way to make Methods programmatically too. Attribute setters should
Expand Down
1 change: 0 additions & 1 deletion examples/zebra2_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from pkg_resources import require
require("pyzmq==13.1.0")
require("cothread==2.14b1")
import os
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
from malcolm.imalcolm import IMalcolmServer
from malcolm.devices.zebra2.zebra2 import Zebra2
Expand Down
35 changes: 27 additions & 8 deletions malcolm/devices/zebra2/zebra2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from malcolm.core.loop import TimerLoop
from malcolm.core.alarm import Alarm, AlarmSeverity, AlarmStatus
from malcolm.core import Attribute, VString
from malcolm.core.vtype import VBool


class Zebra2(FlowGraph):
Expand All @@ -23,6 +24,8 @@ def __init__(self, name, hostname, port):
# Read the bit enums
self._bits = self.comms.get_bits()
self._positions = self.comms.get_positions()
# (block, field) -> [list of .VAL attributes that need updating]
self._muxes = {}
# Now create N block objects based on this info
for block, num in self.num_blocks.items():
field_data = self.comms.get_field_data(block)
Expand All @@ -37,7 +40,9 @@ def __init__(self, name, hostname, port):
def make_block(self, block, i, field_data):
blockname = "{}:{}{}".format(self.name, block, i)
self._blocks["{}{}".format(block, i)] = self.process.create_device(
Zebra2Block, blockname, comms=self.comms, field_data=field_data)
Zebra2Block, blockname, comms=self.comms, field_data=field_data,
bits=[x for x in self._bits if x],
positions=[x for x in self._positions if x])

def do_poll(self):
changes = self.comms.get_changes()
Expand All @@ -46,13 +51,7 @@ def do_poll(self):
assert block in self._blocks, \
"Block {} not known".format(block)
block = self._blocks[block]
if "." in field:
field, suffix = field.split(".", 1)
field = "{}_{}".format(field, suffix)
if suffix not in ["UNITS", "CAPTURE", "SCALE", "OFFSET"]:
print "Not supported yet {}.{}".format(block.name, field)
continue
self.update_attribute(block, field, val)
self.update_attribute(block, field.replace(".", ":"), val)

def update_attribute(self, block, field, val):
assert field in block.attributes, \
Expand All @@ -64,4 +63,24 @@ def update_attribute(self, block, field, val):
"Not in range")
attr.update(alarm=alarm)
else:
if isinstance(attr.typ, VBool):
val = bool(int(val))
# TODO: make pos_out and bit_out things toggle while changing
attr.update(val)
for val_attr in self._muxes.get((block, field), []):
val_attr.update(val)
# if we changed the value of a pos_mux or bit_mux, update its value
if field in block.field_data and \
block.field_data[field][1] in ("bit_mux", "pos_mux"):
# this is the attribute that needs to update
val_attr = block.attributes[field + ":VAL"]
for mux_list in self._muxes.values():
try:
mux_list.remove(val_attr)
except ValueError:
pass
# add it to the list of things that need to update
mon_block, mon_field = val.split(".", 1)
self._muxes.setdefault((mon_block, mon_field), []).append(val_attr)
# update it to the right value
val_attr.update(self._blocks[mon_block].attributes[mon_field].value)
31 changes: 19 additions & 12 deletions malcolm/devices/zebra2/zebra2block.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

class Zebra2Block(Device):

def __init__(self, name, comms, field_data):
def __init__(self, name, comms, field_data, bits, positions):
self.comms = comms
self.field_data = field_data
self.bits = bits
self.positions = positions
self._configurable = OrderedDict()
super(Zebra2Block, self).__init__(name)
# Now add a setter method for each param
Expand All @@ -18,15 +20,14 @@ def __init__(self, name, comms, field_data):

def make_setter(self, block, attr):
param = attr.name
comms_param = param.replace("_", ".")
set_name = "set_{}".format(param)
isbool = type(attr.typ) == VBool

def set_func(device, **args):
value = args[param]
if isbool:
value = int(args[param])
device.comms.set_field(block, comms_param, value)
device.comms.set_field(block, param.replace(":", "."), value)
setattr(device, param, value)

method = ClientMethod(set_name, set_name, set_func)
Expand All @@ -53,7 +54,9 @@ def make_read_attribute(self, field, typ):
elif typ == "bit":
ret[field] = Attribute(VBool, field)
elif typ == "bit_mux":
ret[field] = Attribute(VString, field)
ret[field] = Attribute(VEnum(self.bits), field)
self.add_attribute(field + ":VAL", Attribute(
VBool, field + " current value"))
elif typ == "enum":
block = self.name.split(":", 1)[1]
labels = self.comms.get_enum_labels(block, field)
Expand All @@ -66,14 +69,16 @@ def make_read_attribute(self, field, typ):
ret[field] = Attribute(VString, field)
elif typ == "position":
ret[field] = Attribute(VDouble, field)
ret[field + "_UNITS"] = Attribute(
ret[field + ":UNITS"] = Attribute(
VString, field + " position units")
ret[field + "_SCALE"] = Attribute(
ret[field + ":SCALE"] = Attribute(
VString, field + " scale")
ret[field + "_OFFSET"] = Attribute(
ret[field + ":OFFSET"] = Attribute(
VString, field + " offset")
elif typ == "pos_mux":
ret[field] = Attribute(VString, field)
ret[field] = Attribute(VEnum(self.positions), field)
self.add_attribute(field + ":VAL", Attribute(
VDouble, field + " current value"))
elif typ == "time":
ret[field] = Attribute(VDouble, field)
else:
Expand All @@ -92,8 +97,11 @@ def make_param_attribute(self, field, typ):
self._configurable.update(ret)

def make_out_attribute(self, field, typ):
self.make_read_attribute(field, typ)
field = field + "_CAPTURE"
ret = self.make_read_attribute(field, typ)
for name, attr in ret.items():
if ":" in name:
self._configurable[name] = attr
field = field + ":CAPTURE"
attr = Attribute(VBool, "Capture {} in PCAP?".format(field))
self.add_attribute(field, attr)
self._configurable[field] = attr
Expand Down Expand Up @@ -126,7 +134,6 @@ def make_time_attribute(self, field, typ):
self.add_attribute(field, attr)
self._configurable[field] = attr
attr = Attribute(VEnum("s,ms,us"), field + " time units")
field = field + "_UNITS"
field = field + ":UNITS"
self.add_attribute(field, attr)
self._configurable[field] = attr

2 changes: 1 addition & 1 deletion malcolm/devices/zebra2/zebra2comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def set_field(self, block, field, value):
def get_bits(self):
bits = []
for i in range(4):
bits.append(self.send_recv("*BITS{}?\n".format(i)))
bits += self.send_recv("*BITS{}?\n".format(i))
return bits

def get_positions(self):
Expand Down
52 changes: 51 additions & 1 deletion malcolm/gui/delegate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum
from PyQt4.Qt import QStyledItemDelegate, QStyle, QStyleOptionButton, \
QApplication, QEvent, QPushButton
QApplication, QEvent, QPushButton, QComboBox, QLineEdit, QVariant, Qt
from malcolm.core import VBool, VEnum, Attribute
from malcolm.core.method import Method


Expand All @@ -10,6 +11,42 @@ class MState(Enum):

class Delegate(QStyledItemDelegate):

def createEditor(self, parent, option, index):
if index.isValid() and index.column() == 1:
item = index.internalPointer()
if isinstance(item.data, Attribute):
attr = item.data
if isinstance(attr.typ, VEnum):
editor = SpecialComboBox(parent)
editor.delegate = self
editor.setEditable(True)
editor.addItems(attr.typ.labels)
elif isinstance(attr.typ, VBool):
editor = SpecialComboBox(parent)
editor.delegate = self
editor.setEditable(True)
editor.addItems(["False", "True"])
else:
editor = QLineEdit(parent)
return editor

def setEditorData(self, editor, index):
if isinstance(editor, QComboBox):
i = editor.findText(index.data(Qt.EditRole).toString())
if i > -1:
editor.setCurrentIndex(i)
else:
editor.setEditText(index.data(Qt.EditRole).toString())
editor.lineEdit().selectAll()
else:
return QStyledItemDelegate.setEditorData(self, editor, index)

def setModelData(self, editor, model, index):
if isinstance(editor, QComboBox):
model.setData(index, QVariant(editor.currentText()), Qt.EditRole)
else:
return QStyledItemDelegate.setModelData(self, editor, model, index)

def paint(self, painter, option, index):
# If we are looking at a method then draw a button
# http://www.qtcentre.org/threads/26916-inserting-custom-Widget-to-listview?p=128623#post128623
Expand Down Expand Up @@ -56,3 +93,16 @@ def editorEvent(self, event, model, option, index):
model.setData(index, 0)
return True
QStyledItemDelegate.editorEvent(self, event, model, option, index)


class SpecialComboBox(QComboBox):
# Qt outputs an activated signal if you start typing then mouse click on the
# down arrow. By delaying the activated event until after the mouse click
# we avoid this problem
def closeEvent(self, i):
self.delegate.commitData.emit(self)
self.delegate.closeEditor.emit(self, QStyledItemDelegate.SubmitModelCache)

def mousePressEvent(self, event):
QComboBox.mousePressEvent(self, event)
self.activated.connect(self.closeEvent)
3 changes: 3 additions & 0 deletions malcolm/gui/guimodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from malcolm.core.method import Method
from malcolm.core.alarm import AlarmSeverity, Alarm
from collections import OrderedDict
from malcolm.core.vtype import VBool

# https://www.daniweb.com/programming/software-development/threads/312211/pyqt4-treemodel-example

Expand Down Expand Up @@ -238,6 +239,8 @@ def setData(self, index, value, role=Qt.EditRole):
return True
elif self.isWriteable(item):
newvalue = str(value.toString())
if isinstance(item.data.typ, VBool):
newvalue = newvalue.lower() != "false"
import cothread
cothread.Spawn(self.do_put, index, newvalue)
return True
Expand Down
2 changes: 1 addition & 1 deletion tests/test_devices/test_zebra2.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_bits(self):
names = []
for j in range(32):
names.append("field {}".format(i * 32 + j))
bits.append(names)
bits += names
messages += ["!{}\n".format(f) for f in names]
messages.append(".\n")
self.c.sock.recv.side_effect = messages
Expand Down

0 comments on commit 0d1438d

Please sign in to comment.