Commit 9d9f2161 authored by Jose Tiago Coutinho Macara's avatar Jose Tiago Coutinho Macara Committed by bliss administrator

ct2: improvements

- simple web configuration
- ensure card has minimal default configuration
- displace config outside CT2Device
- input trigger configuration moved to static config
- output gate configuration moved to static config
- add counter name
- add zerorpc client and server
- make card API serializable
parent cce78f1c
This diff is collapsed.
<!--
This file is part of the bliss project
Copyright (c) 2016 Beamline Control Unit, ESRF
Distributed under the GNU LGPLv3. See LICENSE for more info.
-->
<div class="container">
<div class="row">
<div class="btn-group btn-group" role="group" aria-label="global functionality">
<a class="btn btn-default" href="javascript:void(0)">
{{ config['filename'] if config['filename'] else "unknown file" }}
</a>
<button id="apply" type="button" class="btn btn-primary"
data-toggle="tooltip" title="save changes">
Save
<span class="fa fa-save"></span>
</button>
<button id="revert" type="button" class="btn btn-primary"
data-toggle="tooltip" title="revert changes">
Revert
<span class="fa fa-undo"></span>
</button>
</div>
</div>
</div>
<div style="height:10px;"></div>
<form id="device_form" class="form-horizontal">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label" for="device-name"
data-toggle="tooltip" title="card name">
Name
</label>
<div class="col-md-4">
<input id="device-name" name="device-name"
class="form-control input-md" type="text"
value="{{ config['name']}}" />
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label" for="device-name"
data-toggle="tooltip" title="type of card">
Type
</label>
<div class="col-md-4">
<select class="selectpicker" id="device-type" name="device-type">
<option {{ "selected" if config["type"] == "P201"}}>P201</option>
<option {{ "selected" if config["type"] == "C208"}}>C208</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label" for="device-address"
data-toggle="tooltip" title="device address">
Address
</label>
<div class="col-md-4">
<input id="device-address" name="device-address"
class="form-control input-md" type="text"
value="{{ config['address']}}" />
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label" for="device-clock"
data-toggle="tooltip" title="internal clock speed">
Clock
</label>
<div class="col-md-4">
<select class="selectpicker" id="device-clock" name="device-clock">
{% for clock in card.Clock %}
<option {{ "selected" if clock.name == config["clock"] }}>{{ clock.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label" for="input-polarity"
data-toggle="tooltip" title="If an input channel is selected invert or not its polarity">
Trig./Gate Input polarity inverted
</label>
<div class="col-md-4">
<input id="input-polarity" name="input-polarity"
class="form-control input-md" type="checkbox"
{{ "checked" if config["external sync"]["input"]["polarity inverted"] }} />
</div>
</div>
<div class="panel panel-default">
<table id="ct2_channels_extra" class="table table-striped table-bordered"
data-toggle="table" data-show-toggle="true"
data-show-columns="true" style="tr, th, td {padding:2px;}">
<thead>
<tr>
<th>#</th>
<th>Counter name</th>
<th>Level</th>
<th>50 &#8486;</th>
<th>Trig./Gate Input</th>
<th>Gate Output</th>
</tr>
</thead>
<tbody>
{% for ch in config["channels"] %}
{% set ch_id = ch["address"] %}
<tr>
<td>{{ ch_id }}</td>
<td>
<input id="ch-{{ch_id}}-counter-name"
name="ch-{{ch_id}}-counter-name"
class="form-control input-sm" type="text"
value="{{ ch['counter name'] }}"
style="border-radius: 0px;" />
</td>
<td>
<select class="selectpicker" id="ch-{{ch_id}}-level"
name="ch-{{ch_id}}-level">
{% for level in card.Level %}
<option {{ "selected" if ch["level"] == level.name }}>
{{ level.name }}
</option>
{% endfor %}
</select>
</td>
<td>
<input id="ch-{{ch_id}}-50-ohm" name="ch-{{ch_id}}-50-ohm"
class="form-control input-sm" type="checkbox"
{{ "checked" if ch["50 ohm"] == True }} />
</td>
<td>
{% if ch_id in klass.INPUT_CHANNELS %}
{% set ch_input = ch_id == config["external sync"]["input"].get("channel") %}
<input id="ch-{{ch_id}}-input" name="ch-input"
class="form-control input-sm" type="radio"
value="{{ch_id}}"
{{ "checked" if ch_input }} />
{% endif %}
</td>
<td>
{% if ch_id in klass.OUTPUT_CHANNELS %}
{% set ch_output = ch_id == config["external sync"]["output"].get("channel") %}
<input id="ch-{{ch_id}}-output" name="ch-output"
class="form-control input-sm" type="radio"
value="{{ch_id}}"
{{ "checked" if ch_output }} />
{% endif %}
</td>
</tr>
{% endfor %}
<tr>
<td colspan="4"></td>
<td align="center">
<button type="button" class="btn btn-default"
id="clear-input">Clear</button>
<td align="center">
<button type="button" class="btn btn-default"
id="clear-output">Clear</button>
</tbody>
</table>
</fieldset>
</form>
<script type="text/javascript">
function submit_form(form) {
var formData = new FormData(form);
formData.append("__original_name__", "{{ config['name'] }}");
$.ajax({
url: "plugin/ct2/device_edit",
type: "POST",
cache: false,
contentType: false,
processData: false,
data: formData,
success: function(result) {
data = $.parseJSON(result);
show_item(data.name);
show_notification(data.message, data.type);
}
});
}
function update_fields() {
var any_in = false;
for(ch = 1; ch <= {{ klass.CHANNELS|length }}; ch++) {
var in_ch = $("#ch-" + ch + "-input").prop("checked");
var out_ch = $("#ch-" + ch + "-output").prop("checked");
var text_field = $("#ch-" + ch + "-counter-name");
any_in |= in_ch;
if (in_ch || out_ch) {
text_field.hide();
}
else {
text_field.show();
}
}
if (!any_in) {
$("#input-polarity").prop("checked", false);
}
$("#input-polarity").prop("disabled", !any_in);
}
$(document).ready(function() {
$(".selectpicker option").addClass("small");
$(".selectpicker")
.addClass("show-tick")
.selectpicker({
style: "btn-sm btn-default",
selectedTextFormat: "count>6",
width: "100%",
});
$("#apply").on("click", function() {
submit_form($("#device_form")[0]);
return false;
});
$("#revert").on("click", function() {
show_item("{{ config["name"] }}");
return false;
});
$("#reset").on("click", function() {
// TODO
return false;
});
$("#clear-input").on("click", function() {
$("input[name*=input]").prop("checked", false);
update_fields();
});
$("#clear-output").on("click", function() {
$("input[name*=output]").prop("checked", false);
update_fields();
});
$("input[name*=input]").on("click", function() {
update_fields();
});
$("input[name*=output]").on("click", function() {
update_fields();
});
update_fields();
});
</script>
This diff is collapsed.
......@@ -108,6 +108,16 @@ level.
http://intranet.esrf.fr/ISDD/detector-and-electronics/electronics/DigitalElectronicsLab/Publications/released/c208
"""
from .ct2 import *
from .device import *
def create_objects_from_config_node(config, node):
name = node.get("name")
klass = node.get("class")
if klass == 'CT2':
address = node['address']
if address.startswith('tcp://'):
from . import client as module
else:
from . import device as module
else:
from . import card as module
return module.create_object_from_config_node(config, node)
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2016 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
"""
CT2 (P201/C208) ESRF PCI counter card device
Minimalistic configuration example:
.. code-block:: yaml
plugin: ct2
name: p201
class: CT2
address: tcp://lid312:8909
(for the complete CT2 YAML_ specification see :ref:`bliss-ct2-yaml`)
"""
import inspect
import gevent
import zerorpc
import msgpack_numpy
from . import device
from bliss.common.event import dispatcher
msgpack_numpy.patch()
def __create_property(name, member):
def fget(self):
return self.get_property(name)
def fset(self, value):
self.set_property(name, value)
return property(fget, fset, doc=member.__doc__)
def __fill_properties(this, klass):
# Workaround to create property access on this class since
# RPC only supports method calls.
# This adds to *this* the same properties as *klass*.
# Each property getter/setter will call RPC *get/set_property*
for name, member in inspect.getmembers(klass):
if name.startswith('_') or not inspect.isdatadescriptor(member):
continue
setattr(this, name, __create_property(name, member))
return this
class CT2(zerorpc.Client):
def connect(self, *args, **kwargs):
super(CT2, self).connect(*args, **kwargs)
self.__events_task = gevent.spawn(self.__dispatch_events)
def close(self):
self.__events_task.kill()
super(CT2, self).close()
def __dispatch_events(self):
events = self.events()
for event in events:
dispatcher.send(event[0], self, event[1])
__fill_properties(CT2, device.CT2)
def __get_device_config(name):
from bliss.config.static import get_config
config = get_config()
device_config = config.get_config(name)
return device_config
def create_and_configure_device(config_or_name):
"""
Create a device from the given configuration (beacon compatible) or its
configuration name.
Args:
config_or_name: (config or name: configuration dictionary (or
dictionary like object) or configuration name
Returns:
a new instance of :class:`CT2` configured and ready to go
"""
if isinstance(config_or_name, (str, unicode)):
device_config = __get_device_config(config_or_name)
name = config_or_name
else:
device_config = config_or_name
name = device_config['name']
timeout = device_config.get('timeout', 1)
device = CT2(device_config['address'], timeout=timeout)
device.configure(device_config)
return device
def create_object_from_config_node(config, node):
"""
To be used by the ct2 bliss config plugin
"""
name = node.get("name")
device = create_and_configure_device(node)
return {name:device}, {name:device}
This diff is collapsed.
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2016 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
"""
CT2 (P201 and C208) interface over the network using zerorpc.
This requires zerorpc and msgpack_numpy.
Usage:
$ python -m bliss.controllers.ct2.server
Serving ct2 on tcp://0.0.0.0:8909 ...
Test on the client machine using:
$ zerorpc tcp://hostname:8909 -?
"""
# Imports
import sys
import logging
import weakref
import argparse
import six
import louie
import gevent.queue
import zerorpc
import msgpack_numpy
from bliss.controllers.ct2 import card
from bliss.controllers.ct2 import device
DEFAULT_BIND = '0.0.0.0'
DEFAULT_PORT = 8909
DEFAULT_HEARTBEAT = 5
DEFAULT_CARD_TYPE = 'P201'
DEFAULT_CARD_ADDRESS = '/dev/ct2_0'
DEFAULT_LOG_LEVEL = 'INFO'
log = logging.getLogger('CT2Server')
# Patching
msgpack_numpy.patch()
class CT2(device.CT2):
def __init__(self, *args, **kwargs):
super(CT2, self).__init__(*args, **kwargs)
@zerorpc.stream
def events(self, signal=None):
if signal is None:
signal = louie.All
log.info('new stream (signal=%s)', signal)
stream = gevent.queue.Queue()
def receiver(value, signal, sender):
stream.put((signal, value))
louie.connect(receiver, signal, self)
for msg in stream:
if msg is None:
break
yield msg
def get_property(self, key):
result = getattr(self, key)
return result
def set_property(self, key, value):
setattr(self, key, value)
def create_device(card_type, address):
config = {
'class': card_type + 'Card',
'address': address,
}
card_obj = card.create_and_configure_card(config)
device = CT2(card_obj)
return device
def run(bind=DEFAULT_BIND, port=DEFAULT_PORT, heartbeat=DEFAULT_HEARTBEAT,
card_type=DEFAULT_CARD_TYPE, address=DEFAULT_CARD_ADDRESS):
access = "tcp://{}:{}".format(bind, port)
device = create_device(card_type, address)
server = zerorpc.Server(device, heartbeat=heartbeat)
server.bind(access)
log.info('Serving CT2 on {access}...'.format(access=access))
try:
server.run()
except KeyboardInterrupt:
log.info('Interrupted. Bailing out!')
finally:
server.close()
def main(args=None):
if args is None:
args = sys.argv[1:]
parser = argparse.ArgumentParser(description='CT2 server')
parser.add_argument('--address', default=DEFAULT_CARD_ADDRESS, type=str,
help='card address')
parser.add_argument('--type', default=DEFAULT_CARD_TYPE, type=str,
dest='card_type',
help='card type', choices=['P201', 'C208'])
parser.add_argument('--port', default=DEFAULT_PORT, type=int,
help='server port')
parser.add_argument('--bind', default=DEFAULT_BIND, type=str,
help='server bind')
parser.add_argument('--heartbeat', default=DEFAULT_HEARTBEAT, type=int,
help='heartbeat')
parser.add_argument('--log-level', default=DEFAULT_LOG_LEVEL, type=str,
help='log level',
choices=['DEBUG', 'INFO', 'WARN', 'ERROR'])
arguments = vars(parser.parse_args(args))
log_level = arguments.pop('log_level', DEFAULT_LOG_LEVEL).upper()
fmt = '%(levelname)s %(asctime)-15s %(name)s: %(message)s'
logging.basicConfig(level=getattr(logging, log_level), format=fmt)
run(**arguments)
if __name__ == '__main__':
main()
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
# Copyright (c) 2016 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
from __future__ import absolute_import
import numpy
from bliss.common.tango import PyTango, EventType, DeviceProxy
from bliss.controllers.ct2 import BaseCT2Device, AcqMode, AcqStatus
PyTango.requires_pytango('8.1.8')
class CT2Device(BaseCT2Device):
"""
Helper for a remote TANGO device CT2 card (P201/C208).
"""
def __init__(self, device_name):
BaseCT2Device.__init__(self)
self.__tango_device = DeviceProxy(device_name)
# Uncomment when CT2 server supports events again (tango bug solved)
# self.__tango_device.subscribe_event("acq_status",
# EventType.CHANGE_EVENT,
# self.__on_status)
# self.__tango_device.subscribe_event("last_point_nb",
# EventType.CHANGE_EVENT,
# self.__on_point_nb)
# self.__tango_device.subscribe_event("last_error",
# EventType.CHANGE_EVENT,
# self.__on_error)
def __on_status(self, event):
self._send_status(AcqStatus[event.attr_value.value])
def __on_point_nb(self, event):
self._send_point_nb(event.attr_value.value)
def __on_error(self, event):
self._send_error(event.attr_value.value)
@property
def _device(self):
return self.__tango_device
def apply_config(self):
self.card_config.save()
BaseCT2Device.apply_config(self)
def read_data(self):
data = self._device.data
if data is None:
data = numpy.array([[]], dtype=numpy.uint32)
return data
def dump_memory(self):
return bytes(super(CT2Device, self).dump_memory().data)
......@@ -13,10 +13,10 @@ CT2 (P201/C208) ESRF counter card TANGO device
__all__ = ["CT2", "main"]
import time
import logging
import warnings
import numpy
import gevent
from gevent import select
from PyTango import Util, GreenMode
from PyTango import AttrQuality, AttrWriteType, DispLevel, DevState
......@@ -24,24 +24,27 @@ from PyTango.server import Device, DeviceMeta
from PyTango.server import attribute, command
from PyTango.server import class_property, device_property
from bliss.common.event import connect
from bliss.config.static import get_config
from bliss.controllers.ct2 import CT2Device, AcqMode, AcqStatus
from bliss.controllers.ct2 import ErrorSignal, PointNbSignal, StatusSignal
from bliss.controllers.ct2.card import BaseCard
from bliss.controllers.ct2.device import AcqMode, AcqStatus
def switch_state(tg_dev, state=None, status=None):
"""Helper to switch state and/or status and send event"""
if state is not None:
tg_dev.set_state(state)
# tg_dev.push_change_event("state")
if state in (DevState.ALARM, DevState.UNKNOWN, DevState.FAULT):
msg = "State changed to " + str(state)
if status is not None:
msg += ": " + status
if status is not None:
tg_dev.set_status(status)
# tg_dev.push_change_event("status")
def _to_enum(value, etype):
if isinstance(value, (etype, str, unicode)):
return etype[value]
return etype(value)
class CT2(Device):
......@@ -51,43 +54,23 @@ class CT2(Device):
__metaclass__ = DeviceMeta