class DelayBox(Moveable): """Toni TOFTOF-type programmable delay box.""" attached_devices = { 'bus': Attach('Toni communication bus', ToniBus), } parameters = { 'addr': Param('Bus address of the supply controller', type=intrange(0xF0, 0xFF), mandatory=True), } parameter_overrides = { 'fmtstr': Override(default='%d'), } valuetype = int def doRead(self, maxage=0): return self._attached_bus.communicate('D?', self.addr, expect_hex=4) def doStart(self, target): self._attached_bus.communicate('D=%04X' % target, self.addr, expect_ok=True) def doIsAllowed(self, target): if 0 <= target <= 65535: return True, '' else: return False, '%r is not in the allowed range [0, 65535], please '\ 'check your delay calculation' % (target,) def doStatus(self, maxage=0): return status.OK, ''
class WindowMoveable(HasLimits, EpicsMoveable): """ Some devices do not have a way to determine their status. The only way to test for completion is to read the value back and test if it is within a certain window of the target. This is done here. """ parameters = { 'window': Param('Tolerance used for testing for completion', type=float, mandatory=True) } parameter_overrides = { 'target': Override(settable=True), } valuetype = float _driveTarget = None def doStart(self, value): # I have to use my private _driveTarget as the target # attribute is marked volatile in EpicsMoveable and is # not holding the real target. self._driveTarget = value EpicsMoveable.doStart(self, value) def doStatus(self, maxage=0): pos = self.doRead(0) if self._driveTarget: if abs(pos - self._driveTarget) < self.window: self._driveTarget = None return status.OK, 'Done' else: return status.BUSY, 'Moving' return status.OK, 'Done'
class UsesFastshutter(DeviceMixinBase): """ Adds the ability to open a fast shutter before the acquisition. The given shutter devices MUST BE of type <nicos.devices.generic.Switcher> and MUST HAVE the values 'open' and 'closed'. This state is enforced to avoid setups that configure almighty monster-detectors. """ attached_devices = { 'fastshutter': Attach('Fast shutter switcher device', Switcher), } parameters = { 'openfastshutter': Param( 'Open fast shutter before the acquisition. ' 'Caution: It has to be closed manually', type=bool, settable=True, default=True), } def openFastshutter(self): # open fastshutter automatically if desired if self.openfastshutter: # reset fast shutter if in error state (the shutter sometimes goes # into error state because it couldn't be opened, but it works # again after reset on the next try fastshutter = self._attached_fastshutter if fastshutter.status(0)[0] == status.ERROR: self.log.warning('resetting fast shutter before opening: it is' ' in error state') fastshutter.reset() waitForCompletion(fastshutter) fastshutter.maw('open')
class Authenticator(BaseAuthenticator): """Authenticates against PAM. This unfortunately only works against the local shadow database if the daemon runs as the root user. The access level info can be put into the "gecos" field. Example: John Doe,access=20 where 20 is the 'ADMIN' level. (see `nicos.core.utils.py` file) """ parameters = { 'defaultlevel': Param('Default user level if not in PAM settings', settable=False, userparam=False, type=oneof(GUEST, USER, ADMIN), default=GUEST), } def authenticate(self, username, password): try: pam.authenticate(username, password, resetcred=0) entry = pwd.getpwnam(username) idx = access_re.search(entry.pw_gecos) if idx: access = int(idx.group('level')) if access in (GUEST, USER, ADMIN): return User(username, access) return User(username, self.defaultlevel) except pam.PAMError as err: raise AuthenticationError( 'PAM authentication failed: %s' % err) from None except Exception as err: raise AuthenticationError( 'exception during PAM authentication: %s' % err) from None
class TASIndex(AutoDevice, Moveable): """ "Partial" devices for the H, K, L, E indices of the TAS instrument. """ parameters = { 'index': Param('The index into the TAS value', type=int), } attached_devices = { 'tas': Attach('The spectrometer to control', TAS), } valuetype = float hardware_access = False def doRead(self, maxage=0): return self._attached_tas.read(maxage)[self.index] def doStart(self, pos): current = list(self._attached_tas.read(0.5)) current[self.index] = pos self._attached_tas.start(current)
class AsymmetricMagnet(HasTimeout, Actuator): """Class for the asymmetric ccmsans. Provides the ability to set the current field, and the asymmetry ratio. """ parameters = { 'asymmetry': Param('Asymmetry ratio', type=oneof(0, 11, 25, 39, 53, 70), settable=True, volatile=True), } parameter_overrides = { # max range * max ramp + 5' 'timeout': Override(mandatory=False, default=5400 + 300) } def doReadAsymmetry(self): return self._dev.asymmetry def doWriteAsymmetry(self, value): self._dev.asymmetry = value
class Ratemeter(Readable): """Toni ratemeter inside a "crate".""" attached_devices = { 'bus': Attach('Toni communication bus', ModBus), } parameters = { 'addr': Param('Bus address of crate', type=intrange(0xF0, 0xFF), mandatory=True), } def doRead(self, maxage=0): bus = self._attached_bus self._cachelock_acquire() try: # ratemeter is on channel 2 bus.communicate('C2', self.addr, expect_ok=True) # send command (T = transmit, X = anything for input buffer update bus.communicate('TX', self.addr, expect_ok=True) # wait until response is ready rlen = -1 t = 0 while 1: sleep(0.05) ret = bus.communicate('R?', self.addr) if rlen == -1 or len(ret) == rlen: return ret t += 1 if t == 10: raise CommunicationError('timeout while waiting for ' 'response') finally: self._cachelock_release()
class DigitalInput(Moveable): """A test DigitalInput.""" parameters = { '_value': Param('Simulated value', type=intrange(0, 0xFFFFFFFF), default=0, settable=False, internal=True), } parameter_overrides = { 'unit': Override(mandatory=False, settable=False, default=''), 'fmtstr': Override(default='%d'), } valuetype = intrange(0, 0xFFFFFFFF) def doRead(self, maxage=0): return self._value def doStatus(self, maxage=0): return status.OK, 'idle'
class VirtualScanningDetector(SubscanMeasurable): """A virtual detector whose data acquisition consists of scanning a moveable device, and taking data points with another detector. """ attached_devices = { 'scandev': Attach('Current device to scan', Moveable), 'detector': Attach('Detector to scan', Measurable), } parameters = { 'positions': Param('Positions to scan over', type=listof(float)) } def doInit(self, mode): self._last = [0, ''] self._preset = None def doSetPreset(self, **preset): self._preset = preset def doStart(self): positions = [[p] for p in self.positions] dataset = Scan([self._adevs['scandev']], positions, positions, detlist=[self._adevs['detector']], preset=self._preset, subscan=True).run() # process the values... yvalues = [subset.detvaluelist[0] for subset in dataset.subsets] self._last = [sum(yvalues) / float(len(yvalues)), dataset.filenames[0]] def valueInfo(self): return (Value(self.name + '.mean', unit='cts', fmtstr='%.1f'), Value(self.name + '.file', unit='', type='info')) def doRead(self, maxage=0): return self._last
class AmorSlitLogicalMotor(AmorLogicalMotor): """ Class to represent the logical slit motors in AMOR. """ parameters = { 'motortype': Param('Type of motor %s' % ','.join(motortypes), type=oneof(*motortypes), mandatory=True), } parameter_overrides = { 'unit': Override(mandatory=False, default='degree'), 'target': Override(volatile=True), 'abslimits': Override(mandatory=False, default=(-3.0, 3.0)), 'userlimits': Override(mandatory=False, default=(-3.0, 3.0)) } attached_devices = { 'controller': Attach('Controller for the logical motors', AmorSlitHandler) } def doRead(self, maxage=0): return self._attached_controller.doRead(maxage)
class VSTiltMotor(EpicsMotor): """ The tilt motor for a velocity selector can only be moved when the selector is standing. This class ensures just that. """ attached_devices = { 'vs_rotation': Attach('Velcocity Selector Rotation', Moveable), } parameters = { 'limit': Param('Limit below which the rotation is considered standing', type=float, mandatory=True) } def doIsAllowed(self, target): if self._attached_vs_rotation.read(0) > self.limit: return False, \ 'Velocity Selector must be stopped before moving tilt' return True, '' def doStart(self, pos): EpicsMotor.doStart(self, pos)
class HistogramConfBank(HistogramConfElement): """ The Bank configuration element of the XML file. The bank has it's associated id which is to be used when fetching data from this bank. The bank can also have several axes attached. The rank of the bank (number of dimensions of the bank) is deduced from the number of axes attached to this bank. """ parameters = { 'bankid': Param('Integer id of the bank', type=int, mandatory=True) } attached_devices = { 'axes': Attach('Axis for this bank', HistogramConfAxis, multiple=True) } @property def arrays(self): """ Provides a set of of all the arrays that appear in this bank. """ # Collect all the arrays that occur in axis return {ax._attached_array for ax in self.axes if ax._attached_array} @property def axes(self): return self._attached_axes @property def shape(self): return [ax.length for ax in self.axes] def getXml(self): elem = etree.Element('bank', rank=str(len(self.axes))) for ax in self.axes: elem.append(ax.getXml()) return elem
class LiveViewSink(ImageSink): """A DataSink that sends images to attached clients for "live" preview. This sinks sends any data it receives in putResults, and also notifies clients about data filenames at the end of a measurement. For this sink to actually send "live" data, i.e. while counting is in progress, the detector(s) must return LIVE in their `duringMeasureHook`. Whenever it does, the NICOS acquire loop will read out data immediately and push it to the sink. Returning INTERMEDIATE is also possible, but designed for use by data sinks that want to save data as "checkpoints" while counting, not just for live display. The frequency of the hook returning something other than None determines how often live data is updated. For the `nicos.devices.generic.Detector`, this is controlled by its "liveinterval" and "saveintervals" parameters. "datasets" is a list of the amount of data sets of each sent image. If more images are sent then data sets are listed the last value is set for all images exceeding the list length. """ parameters = { 'datasets': Param('Amount of datasets in each liveimage.', type=listof(int), default=[1]) } parameter_overrides = { # this is fixed string for labeling cached live data 'filenametemplate': Override(mandatory=False, userparam=False, default=['<Live>@%d']), } handlerclass = LiveViewSinkHandler
class EpicsMappedMoveable(MappedMoveable, EpicsDigitalMoveableEss): """ EPICS based implementation of MappedMoveable. Useful for PVs that contain enums or bools. """ parameters = { 'ignore_stop': Param('Whether to do anything when stop is called', type=bool, default=False, userparam=False), } def doInit(self, mode): EpicsDigitalMoveableEss.doInit(self, mode) MappedMoveable.doInit(self, mode) def doReadTarget(self): target_value = EpicsDigitalMoveableEss.doReadTarget(self) # If this is from EPICS, it needs to be mapped, otherwise not if self.targetpv: return self._mapReadValue(target_value) return target_value def _readRaw(self, maxage=0): return EpicsDigitalMoveableEss.doRead(self, maxage) def _startRaw(self, target): EpicsDigitalMoveableEss.doStart(self, target) def doStop(self): if not self.ignore_stop: EpicsDigitalMoveableEss.doStop(self)
class VSForbiddenMoveable(WindowMoveable): """ Velocity selectors have forbidden regions in which they are not supposed to run for reason of excessive vibration. This class checks for this """ parameters = { 'forbidden_regions': Param('List of forbidden regions', type=listof(tupleof(float, float)), mandatory=True) } valuetype = float def doIsAllowed(self, value): for region in self.forbidden_regions: if region[0] < value < region[1]: return False, \ 'Desired value value is within ' \ 'forbidden region %f to %f' \ % (region[0], region[1]) return True, ''
class AmorSingleDetectorImageChannel(HistogramImageChannel): """ The three single detectors in AMOR write the data on second bank in the histogram memory with each row representing the TOF data from a particular detector """ parameters = { 'detectorid': Param('ID of the single detector', type=int), } def _dimDesc(self): desc = HistogramImageChannel._dimDesc(self) return [desc[1]] @property def startid(self): return self.detectorid * self.bank.shape[1] @property def endid(self): return (self.detectorid + 1) * self.bank.shape[1] @property def shape(self): return [self.bank.shape[1]]
class SecopDevice(Readable): # based on Readable instead of Device, as we want to have a status attached_devices = { 'secnode': Attach('sec node', SecNodeDevice), } parameters = { 'secop_module': Param('SECoP module', type=str, settable=False, userparam=False), } parameter_overrides = { # do not force to give unit in setup file (take from SECoP description) 'unit': Override(default='', mandatory=False), } _status = (SecopStatus.ERROR, 'disconnected') STATUS_MAP = { 0: status.DISABLED, 1: status.OK, 2: status.WARN, 3: status.BUSY, 4: status.ERROR, } _maintype = staticmethod(anytype) _defunct = False @classmethod def makeDevClass(cls, name, **config): """create a class with the needed doRead/doWrite methods for accessing the assigned SECoP module """ secnodedev = session.getDevice(config['secnode']) # make a copy, as we will modify later params_override = config.pop('params_cfg', None) setup_info = secnodedev.get_setup_info() if name in setup_info: devcfg = dict(setup_info[name][1]) params_cfg = devcfg.pop('params_cfg') else: devcfg, params_cfg = {}, {} if params_override is not None: params_cfg = dict(params_cfg) for pname, pold in list(params_cfg.items()): pnew = params_override.get(pname) if pnew is not None: params_cfg[pname] = dict(pold, **pnew) elif pname not in cls.parameters: params_cfg.pop(pname) # remove parameters not mentioned devcfg.update(config) parameters = {} # create parameters and methods attrs = dict(parameters=parameters, __module__=cls.__module__) if 'valuetype' in config: # this is in fact the target value type attrs['valuetype'] = config.pop('valuetype') if 'maintype' in config: attrs['_maintype'] = staticmethod(config.pop('maintype')) for pname, kwargs in params_cfg.items(): typ = kwargs['type'] if 'fmtstr' not in kwargs and (typ is float or isinstance(typ, floatrange)): # the fmtstr default differs in SECoP and NICOS kwargs = dict(kwargs, fmtstr='%g') # copy kwargs as it may be read only parameters[pname] = Param(volatile=True, **kwargs) def do_read(self, maxage=None, pname=pname, validator=typ): return self._read(pname, maxage, validator) attrs['doRead%s' % pname.title()] = do_read if kwargs.get('settable', False): def do_write(self, value, pname=pname, validator=typ): return self._write(pname, value, validator) attrs['doWrite%s' % pname.title()] = do_write classname = cls.__name__ + '_' + name # create a new class extending SecopDevice, apply DeviceMeta in order to # include the added parameters newclass = DeviceMeta.__new__(DeviceMeta, classname, (cls,), attrs) # pylint: disable=too-many-function-args newclass._modified_config = devcfg # store temporarily for __init__ return newclass def __new__(cls, name, **config): """called when an instance of the class is created but before __init__ instead of returning a SecopDevice, we create an object of an extended class here """ newclass = cls.makeDevClass(name, **config) return Readable.__new__(newclass) def __init__(self, name, **config): """apply modified config""" Readable.__init__(self, name, **self._modified_config) del self.__class__._modified_config def replaceClass(self, config): """replace the class on the fly happens when the structure fo the device has changed """ cls = self.__class__.__bases__[0] newclass = cls.makeDevClass(self.name, **config) bad_attached = False for dev, cls in get_attaching_devices(self): if issubclass(newclass, cls): self.log.warning('reattach %s to %s' % (dev.name, self.name)) else: self.log.error('can not attach %s to %s' % (dev.name, self.name)) bad_attached = True if bad_attached: raise ConfigurationError('device class mismatch') for dev, cls in get_aliases(self): if issubclass(newclass, cls): self.log.warning('redirect alias %s to %s' % (dev.name, self.name)) else: self.log.error('release alias %s from %s' % (dev.name, self.name)) dev.alias = '' self.__class__ = newclass # as we do not go through self.__init__ again, we have to update self._config self._config = dict((name.lower(), value) for (name, value) in config.items()) for aname in self.attached_devices: self._config.pop(aname, None) def doPreinit(self, mode): if mode != SIMULATION: self._attached_secnode.registerDevice(self) def _update(self, module, parameter, value, timestamp, readerror): if parameter not in self.parameters: return if readerror: return # do not know how to indicate an error on a parameter try: # ignore timestamp for now self._setROParam(parameter, value) except Exception as err: self.log.error(repr(err)) self.log.error('can not set %s:%s to %r' % (module, parameter, value)) def _raise_defunct(self): if session.devices.get(self.name) == self: raise DefunctDevice('SECoP device %s no longer available' % self.name) raise DefunctDevice('refers to a replaced defunct SECoP device %s' % self.name) def _read(self, param, maxage, validator): try: secnode = self._attached_secnode._secnode except AttributeError: self._raise_defunct() value, timestamp, _ = secnode.cache[self.secop_module, param] if maxage is not None and time.time() > (timestamp or 0) + maxage: value = secnode.getParameter(self.secop_module, param)[0] if value is not None: value = validator(value) return value def _write(self, param, value, validator): try: value = validator(value) self._attached_secnode._secnode.setParameter(self.secop_module, param, value) return value except AttributeError: self._raise_defunct() def setDefunct(self): if self._defunct: self.log.error('device is already defunct') return self.updateSecopStatus((SecopStatus.ERROR, 'defunct')) self._defunct = True if self._attached_secnode is not None: self._attached_secnode.unregisterDevice(self) # make defunct self._attached_secnode = None self._cache = None def setAlive(self, secnode): self._defunct = False self._cache = secnode._cache self._attached_secnode = secnode secnode.registerDevice(self) # clear defunct status self.updateSecopStatus((SecopStatus.IDLE, '')) def doShutdown(self): if not self._defunct: self.setDefunct() def doRead(self, maxage=0): """dummy, as there is no value""" return '' def updateSecopStatus(self, value): """update status from SECoP status value""" self._status = value if self._cache: self._cache.put(self, 'status', self.doStatus()) def _update_status(self, module, parameter, value, timestamp, readerror): if value is not None: self.updateSecopStatus(tuple(value)) def doStatus(self, maxage=0): code, text = self._status if 390 <= code < 400: # SECoP status finalizing return status.OK, text # treat SECoP code 401 (unknown) as error - should be distinct from NICOS status unknown return self.STATUS_MAP.get(code // 100, status.UNKNOWN), text
class SecNodeDevice(Readable): """SEC node device want to have a status -> based on Readable """ parameters = { 'prefix': Param("Prefix for the generated devices\n\n'$' will be replaced by the equipment id", type=str, default='$_', settable=True), 'uri': Param('tcp://<host>:<port>', type=str, settable=True), 'auto_create': Param('flag for automatic creation of devices', type=bool, settable=False, prefercache=False, default=False, userparam=False), 'setup_info': Param('setup info', type=anytype, default={}, settable=True), } parameter_overrides = { 'unit': Override(default='', mandatory=False), } valuetype = str _secnode = None _value = '' _status = status.OK, 'unconnected' _devices = {} def doPreinit(self, mode): self._devices = {} def doInit(self, mode): if mode == MASTER: if self.uri: try: self._connect() except Exception: pass elif mode == SIMULATION: setup_info = self.get_setup_info() if self.auto_create: self.makeDynamicDevices(setup_info) else: self._setROParam('setup_info', setup_info) def get_setup_info(self): if self._mode == SIMULATION: db = session.getSyncDb() return db.get('%s/setup_info' % self.name.lower()) return self.setup_info def doRead(self, maxage=0): if self._secnode: if self._secnode.online: self._value = self._secnode.nodename else: self._value = '' return self._value def doStatus(self, maxage=0): return self._status def _set_status(self, code, text): self._status = code, text if self._cache: self._cache.put(self, 'status', self._status) self._cache.put(self, 'value', self.doRead()) def doWriteUri(self, value): """change uri and reconnect""" self._setROParam('uri', value) # make sure uri is set before reconnect if self.uri: self._connect() else: self._disconnect() return value def _connect(self): """try to connect called on creation and on uri change, but NOT on automatic reconnection """ if not self.uri: self._disconnect() if self._secnode: self._secnode.disconnect() self._secnode = SecopClient(self.uri, log=self.log) self._secnode.register_callback(None, self.nodeStateChange, self.descriptiveDataChange) try: self._secnode.connect() self._set_status(status.OK, 'connected') self.createDevices() return except Exception as e: if not isinstance(e, CommunicationFailedError): raise self.log.warning('can not connect to %s (%s), retry in background' % (self.uri, e)) self._set_status(status.ERROR, 'try connecting') start_event = Event() self._secnode.spawn_connect(start_event.set) def _disconnect(self): if not self._secnode: return self.removeDevices() self._secnode.disconnect() self._set_status(status.OK, 'unconnected') self._secnode = None def descriptiveDataChange(self, module, description): """called when descriptive data changed after an automatic reconnection""" self.log.warning('node description changed') self.createDevices() def get_status(self, online, state): if not online: return status.ERROR, state return status.OK if state == 'connected' else status.WARN, state def nodeStateChange(self, online, state): """called when the state of the connection changes 'online' is True when connected or reconnecting, False when disconnected or connecting 'state' is the connection state as a string """ if online and state == 'connected': self._set_status(status.OK, 'connected') elif not online: self._set_status(status.ERROR, 'reconnecting') for device in self._devices.values(): device.updateSecopStatus((400, 'disconnected')) else: self._set_status(status.WARN, state) def doShutdown(self): self._disconnect() if self._devices: self.log.error('can not remove devices %r' % list(self._devices)) def _get_prefix(self): if not self._secnode: return None equipment_name = clean_identifier(self._secnode.nodename).lower() return self.prefix.replace('$', equipment_name) @usermethod def showModules(self): """show modules of the connected SECoP server and intended devices names using the given prefix """ prefix = self._get_prefix() if prefix is None: self.log.error('secnode is not connected') return items = [(prefix + m, m, mod_desc.get('properties', {}).get('description', '').split('\n')[0]) for m, mod_desc in self._secnode.modules.items()] printTable(['foreseen device name', 'SECoP module', 'description'], items, self.log.info) def registerDevice(self, device): if not self._secnode: raise IOError('unconnected') self.log.debug('register %s on %s' % (device, self)) self._devices[device.name] = device module = device.secop_module if module not in self._secnode.modules: raise ConfigurationError('no module %r found on this SEC node' % module) for parameter in self._secnode.modules[module]['parameters']: updatefunc = getattr(device, '_update_' + parameter, device._update) self._secnode.register_callback((module, parameter), updateEvent=updatefunc) try: data = self._secnode.cache[module, parameter] if data: updatefunc(module, parameter, *data) else: self.log.warning('No data for %s:%s' % (module, parameter)) except KeyError: self.log.warning('No cache for %s:%s' % (module, parameter)) def unregisterDevice(self, device): self.log.debug('unregister %s from %s' % (device, self)) session.configured_devices.pop(device.name, None) if self._devices.pop(device.name, None) is None: self.log.info('device %s already removed' % device.name) return module = device.secop_module try: moddesc = self._secnode.modules[module] except KeyError: # do not complain again about missing module return for parameter in moddesc['parameters']: updatefunc = getattr(device, '_update_' + parameter, device._update) self._secnode.unregister_callback((module, parameter), updateEvent=updatefunc) def createDevices(self): """create drivers and devices for the devices to be created from the connected SECoP server """ if not self._secnode: self.log.error('secnode is not connected') return modules = self._secnode.modules prefix = self._get_prefix() setup_info = {} for module, mod_desc in modules.items(): params_cfg = {} module_properties = mod_desc.get('properties', None) for ifclass in (module_properties.get('interface_classes', []) or module_properties.get('interface_class', [])): try: clsname = IFCLASSES[ifclass.title()] break except KeyError: continue else: clsname = 'SecopDevice' kwds = {} for pname, props in mod_desc['parameters'].items(): datatype = props['datatype'] typ = get_validator(**datatype.export_datatype()) pargs = dict(type=typ, description=props['description']) if not props.get('readonly', True) and pname != 'target': pargs['settable'] = True unit = '' fmtstr = None if isinstance(datatype, FloatRange): fmtstr = getattr(datatype, 'fmtstr', '%g') unit = getattr(datatype, 'unit', '') elif isinstance(datatype, ScaledInteger): fmtstr = getattr(datatype, 'fmtstr', '%%%df' % max(0, -floor(log10(props['scale'])))) unit = getattr(datatype, 'unit', '') if unit: pargs['unit'] = unit if pname == 'status': continue if pname == 'value': try: kwds['unit'] = datatype.unit except AttributeError: pass if fmtstr is not None: kwds['fmtstr'] = fmtstr else: kwds['fmtstr'] = '%r' kwds['maintype'] = typ continue if pname == 'target': kwds['valuetype'] = typ if fmtstr is not None and fmtstr != '%g': pargs['fmtstr'] = fmtstr params_cfg[pname] = pargs if clsname != 'SecopDevice': kwds.setdefault('unit', '') # unit is mandatory on Readables desc = dict(secnode=self.name, description=mod_desc.get('properties', {}).get('description', ''), secop_module=module, params_cfg=params_cfg, **kwds) setup_info[prefix + module] = ('nicos.devices.secop.%s' % clsname, desc) if not setup_info: self.log.info('creating devices for %s skipped' % self.name) return if self.auto_create: self.makeDynamicDevices(setup_info) else: self._setROParam('setup_info', setup_info) def removeDevices(self): self.makeDynamicDevices({}) def makeDynamicDevices(self, setup_info): """create and destroy dynamic devices create devices from setup_info, and store the name of the setup creating the creator device in session.creator_devices for session.getSetupInfo() Based on the previous setup_info from self.setup_info, devices are created, recreated, destroyed or remain unchanged. If setup_info is empty, destroy all devices. """ prevdevices = set(self.setup_info.keys()) self._setROParam('setup_info', setup_info) # find setup of this secnode result = session.getSetupInfo() for setupname in session.loaded_setups: info = result.get(setupname, None) if info and self.name in info['devices']: break else: raise ConfigurationError('can not find setup') # add new or changed devices for devname, devcfg in setup_info.items(): prevdevices.discard(devname) dev = session.devices.get(devname, None) if dev: if not isinstance(dev, SecopDevice) or (dev._attached_secnode and dev._attached_secnode != self): self.log.error('device %s already exists' % devname) continue base = dev.__class__.__bases__[0] prevcfg = base.__module__ + '.' + base.__name__, dict(secnode=self.name, **dev._config) else: prevcfg = None if prevcfg != devcfg: session.configured_devices[devname] = devcfg session.dynamic_devices[devname] = setupname # pylint: disable=undefined-loop-variable if dev is None: # add new device session.createDevice(devname, recreate=True, explicit=True) dev = session.devices[devname] else: # modify existing device if dev._attached_secnode: dev._attached_secnode.unregisterDevice(dev) session.configured_devices[devname] = devcfg session.dynamic_devices[devname] = setupname # pylint: disable=undefined-loop-variable try: dev.replaceClass(devcfg[1]) dev.setAlive(self) except ConfigurationError: # above failed because an alias or attaching device requires a specific class # make old device defunct and replace by a new device session.destroyDevice(dev) session.dynamic_devices.pop(devname, None) session.createDevice(devname, recreate=True, explicit=True) prevdevices.discard(devname) dev = session.devices[devname] if not isinstance(dev, SecopReadable): # we will not get status updates for these dev.updateSecopStatus((SecopStatus.IDLE, '')) defunct = set() # defunct devices no longer available for devname in prevdevices: dev = session.devices.get(devname) if dev is None or dev._attached_secnode != self: continue if dev._sdevs: self.log.warning('defunct device is attached to %s' % ', '.join(dev._sdevs)) dev.setDefunct() defunct.add(devname) # inform client that setups have changed session.setupCallback(list(session.loaded_setups), list(session.explicit_setups))
class Andor2LimaCCD(GenericLimaCCD): """ This device class is an extension to the GenericLimaCCD that adds the hardware specific functionality for all Andor SDK2 based cameras. """ HSSPEEDS = [5, 3, 1, 0.05] # Values from sdk manual VSSPEEDS = [16, 38.55, 76.95] # Values from sdk manual PGAINS = [1, 2, 4] # Values from sdk manual HSSPEED_RE = re.compile(r'ADC0_(\d+\.\d+|\d+)MHZ') VSSPEED_RE = re.compile(r'(\d+(?:\.\d+)?)USEC') PGAIN_RE = re.compile(r'X(\d)') parameters = { 'hsspeed': Param('Horizontal shift speed', type=oneof(*HSSPEEDS), settable=True, default=5, unit='MHz', volatile=True, category='general'), 'vsspeed': Param('Vertical shift speed', type=oneof(*VSSPEEDS), settable=True, default=76.95, unit='ms/shift', volatile=True, category='general'), 'pgain': Param('Preamplifier gain', type=oneof(*PGAINS), settable=True, default=4, volatile=True, category='general'), } parameter_overrides = { 'hwdevice': Override(mandatory=True), } def doInfo(self): for p in ('hsspeed', 'vsspeed', 'pgain'): self._pollParam(p) return [] def doReadHsspeed(self): val = float(self.HSSPEED_RE.match(self._hwDev._dev.adc_speed).group(1)) return val def doWriteHsspeed(self, value): self._hwDev._dev.adc_speed = 'ADC0_%sMHZ' % value def doReadVsspeed(self): val = float(self.VSSPEED_RE.match(self._hwDev._dev.vs_speed).group(1)) return val def doWriteVsspeed(self, value): self._hwDev._dev.vs_speed = '%gUSEC' % value def doReadPgain(self): val = float(self.PGAIN_RE.match(self._hwDev._dev.p_gain).group(1)) return val def doWritePgain(self, value): self._hwDev._dev.p_gain = 'X%s' % value
class McStasImage(ImageChannelMixin, PassiveChannel): """Image channel based on McStas simulation.""" _mythread = None _process = None parameters = { 'size': Param( 'Detector size in pixels (x, y)', settable=False, type=tupleof(intrange(1, 8192), intrange(1, 8192)), default=(1, 1), ), 'mcstasprog': Param('Name of the McStas simulation executable', type=str, settable=False), 'mcstasdir': Param('Directory where McStas stores results', type=str, default='singlecount', settable=False), 'mcstasfile': Param('Name of the McStas data file', type=str, settable=False), 'mcsiminfo': Param('Name for the McStas Siminfo file', settable=False, type=str, default='mccode.sim'), 'ci': Param('Constant ci applied to simulated intensity I', settable=False, type=floatrange(0.), default=1e3) } def doInit(self, mode): self.arraydesc = ArrayDesc(self.name, self.size, '<u4') self._workdir = os.getcwd() def doReadArray(self, quality): self.log.debug('quality: %s', quality) if quality == LIVE: self._send_signal(SIGUSR2) elif quality == FINAL: if self._mythread and self._mythread.is_alive(): self._mythread.join(1.) if self._mythread.is_alive(): self.log.exception("Couldn't join readout thread.") else: self._mythread = None self._readpsd(quality == LIVE) return self._buf def _prepare_params(self): """Return a list of key=value strings. Each entry defines a parameter setting for the mcstas simulation call. examples: param=10 """ raise NotImplementedError('Please implement _prepare_params method') def doPrepare(self): self._mcstas_params = ' '.join(self._prepare_params()) self.log.debug('McStas parameters: %s', self._mcstas_params) self._buf = np.zeros(self.size[::-1]) self.readresult = [0] def valueInfo(self): return (Value(self.name + '.sum', unit='cts', type='counter', errors='sqrt', fmtstr='%d'), ) def doStart(self): self._mythread = createThread('detector %s' % self, self._run) def doStatus(self, maxage=0): if self._mythread and self._mythread.is_alive(): return status.BUSY, 'busy' return status.OK, 'idle' def doFinish(self): self.log.debug('finish') self._send_signal(SIGTERM) def _send_signal(self, sig): if self._process and self._process.is_running(): self._process.send_signal(sig) # wait for mcstas releasing fds datafile = path.join(self._workdir, self.mcstasdir, self.mcstasfile) siminfo = path.join(self._workdir, self.mcstasdir, self.mcsiminfo) try: while self._process and self._process.is_running(): fnames = [f.path for f in self._process.open_files()] if siminfo not in fnames and datafile not in fnames: break session.delay(.01) except (AccessDenied, NoSuchProcess): self.log.debug( 'McStas process already terminated in _send_signal(%r)', sig) self.log.debug('McStas process has written file on signal (%r)', sig) def _run(self): """Run McStas simulation executable. The current settings of the instrument parameters will be transferred to it. """ try: shutil.rmtree(self.mcstasdir) except (IOError, OSError): self.log.info('could not remove old data') command = '%s -n 1e8 -d %s %s' % (self.mcstasprog, self.mcstasdir, self._mcstas_params) self.log.debug('run %s', command) try: self._process = Popen(command.split(), stdout=PIPE, stderr=PIPE, cwd=self._workdir) out, err = self._process.communicate() if out: self.log.debug('McStas output:') for line in out.splitlines(): self.log.debug('[McStas] %s', line) if err: self.log.warning('McStas found some problems:') for line in err.splitlines(): self.log.warning('[McStas] %s', line) except OSError as e: self.log.error('Execution failed: %s', e) self._process.wait() self._process = None def _readpsd(self, ignore_error=False): try: with open( path.join(self._workdir, self.mcstasdir, self.mcstasfile), 'r') as f: lines = f.readlines()[-3 * (self.size[0] + 1):] if lines[0].startswith('# Data') and self.mcstasfile in lines[0]: self._buf = ( np.loadtxt(lines[1:self.size[0] + 1], dtype=np.float32) * self.ci).astype(np.uint32) self.readresult = [self._buf.sum()] elif not ignore_error: raise IOError('Did not find start line: %s' % lines[0]) except IOError: if not ignore_error: self.log.exception('Could not read result file')
class EpicsMotor(CanDisable, CanReference, HasOffset, EpicsAnalogMoveableEss, Motor): """ This device exposes some of the functionality provided by the EPICS motor record. The PV names for the fields of the record (readback, speed, etc.) are derived by combining the motorpv-parameter with the predefined field names. The errorbitpv and reseterrorpv can be provided optionally in case the controller supports reporting errors and a reset-mechanism that tries to recover from certain errors. If present, these are used when calling the reset()-method. Another optional PV is the errormsgpv, which contains an error message that may originate from the motor controller or the IOC. If it is present, doStatus uses it for some of the status messages. """ parameters = { 'motorpv': Param('Name of the motor record PV.', type=pvname, mandatory=True, settable=False, userparam=False), 'errormsgpv': Param('Optional PV with error message.', type=pvname, mandatory=False, settable=False, userparam=False), 'errorbitpv': Param('Optional PV with error bit.', type=pvname, mandatory=False, settable=False, userparam=False), 'reseterrorpv': Param('Optional PV with error reset switch.', type=pvname, mandatory=False, settable=False, userparam=False), } parameter_overrides = { # readpv and writepv are determined automatically from the base PV 'readpv': Override(mandatory=False, userparam=False, settable=False), 'writepv': Override(mandatory=False, userparam=False, settable=False), # speed, limits and offset may change from outside, can't rely on cache 'speed': Override(volatile=True), 'offset': Override(volatile=True, chatty=False), 'abslimits': Override(volatile=True), } # Fields of the motor record for which an interaction via Channel Access # is required. def _get_record_fields(self): return { 'readpv': 'RBV', 'writepv': 'VAL', 'stop': 'STOP', 'donemoving': 'DMOV', 'moving': 'MOVN', 'miss': 'MISS', 'homeforward': 'HOMF', 'homereverse': 'HOMR', 'speed': 'VELO', 'offset': 'OFF', 'highlimit': 'HLM', 'lowlimit': 'LLM', 'softlimit': 'LVIO', 'lowlimitswitch': 'LLS', 'highlimitswitch': 'HLS', 'enable': 'CNEN', 'set': 'SET', 'foff': 'FOFF', } def _get_pv_parameters(self): """ Implementation of inherited method to automatically account for fields present in motor record. :return: List of PV aliases. """ pvs = set(self._record_fields) if self.errormsgpv: pvs.add('errormsgpv') if self.errorbitpv: pvs.add('errorbitpv') if self.reseterrorpv: pvs.add('reseterrorpv') return pvs def _get_pv_name(self, pvparam): """ Implementation of inherited method that translates between PV aliases and actual PV names. Automatically adds a prefix to the PV name according to the motorpv parameter. :param pvparam: PV alias. :return: Actual PV name. """ motor_record_prefix = getattr(self, 'motorpv') motor_field = self._record_fields.get(pvparam) if motor_field is not None: return '.'.join((motor_record_prefix, motor_field)) return getattr(self, pvparam) def doReadSpeed(self): return self._get_pv('speed') def doWriteSpeed(self, newValue): speed = self._get_valid_speed(newValue) if speed != newValue: self.log.warning( 'Selected speed %s is outside the parameter ' 'limits, using %s instead.', newValue, speed) self._put_pv('speed', speed) def doReadOffset(self): return self._get_pv('offset') def doWriteOffset(self, value): # In EPICS, the offset is defined in following way: # USERval = HARDval + offset if self.offset != value: diff = value - self.offset # Set the offset in motor record self._put_pv_blocking('offset', value) # Read the absolute limits from the device as they have changed. self.abslimits # pylint: disable=pointless-statement # Adjust user limits self.userlimits = (self.userlimits[0] + diff, self.userlimits[1] + diff) self.log.info('The new user limits are: ' + str(self.userlimits)) def doAdjust(self, oldvalue, newvalue): diff = oldvalue - newvalue # For EPICS the offset sign convention differs to that of the base # implementation. self.offset -= diff def _get_valid_speed(self, newValue): min_speed = self._get_pvctrl('speed', 'lower_ctrl_limit', 0.0) max_speed = self._get_pvctrl('speed', 'upper_ctrl_limit', 0.0) valid_speed = newValue if min_speed != 0.0: valid_speed = max(min_speed, valid_speed) if max_speed != 0.0: valid_speed = min(max_speed, valid_speed) return valid_speed def doRead(self, maxage=0): return self._get_pv('readpv') def doStart(self, pos): self._put_pv('writepv', pos) def doReadTarget(self): return self._get_pv('writepv') def doStatus(self, maxage=0): general_epics_status, _ = self._get_mapped_epics_status() message = self._get_status_message() if general_epics_status == status.ERROR: return status.ERROR, message or 'Unknown problem in record' done_moving = self._get_pv('donemoving') moving = self._get_pv('moving') if done_moving == 0 or moving != 0: return status.BUSY, message or 'Motor is moving to target...' miss = self._get_pv('miss') if miss != 0: return (status.NOTREACHED, message or 'Did not reach target position.') high_limitswitch = self._get_pv('highlimitswitch') if high_limitswitch != 0: return status.WARN, message or 'At high limit switch.' low_limitswitch = self._get_pv('lowlimitswitch') if low_limitswitch != 0: return status.WARN, message or 'At low limit switch.' limit_violation = self._get_pv('softlimit') if limit_violation != 0: return status.WARN, message or 'Soft limit violation.' return status.OK, message def _get_status_message(self): """ Get the status message from the motor if the PV exists. :return: The status message if it exists, otherwise an empty string. """ if not self.errormsgpv: return '' return self._get_pv('errormsgpv', as_string=True) def doStop(self): self._put_pv('stop', 1, False) def _checkLimits(self, limits): # Called by doReadUserlimits and doWriteUserlimits low, high = self.abslimits if low == 0 and high == 0: # No limits defined in IOC. # Could be a rotation stage for example. return if limits[0] < low or limits[1] > high: raise ConfigurationError('cannot set user limits outside of ' 'absolute limits (%s, %s)' % (low, high)) def doReadAbslimits(self): absmin = self._get_pv('lowlimit') absmax = self._get_pv('highlimit') return absmin, absmax def doReference(self): self._put_pv_blocking('homeforward', 1) def doReset(self): if self.errorbitpv and self.reseterrorpv: error_bit = self._get_pv('errorbitpv') if error_bit == 0: self.log.warning( 'Error bit is not set, can not reset error state.') else: self._put_pv('reseterrorpv', 1) def _enable(self, on): what = 1 if on else 0 self._put_pv('enable', what, False) def doSetPosition(self, pos): self._put_pv('set', 1) self._put_pv('foff', 1) self._put_pv('writepv', pos) self._put_pv('set', 0) self._put_pv('foff', 0)
class BaseCacheClient(Device): """ An extensible read/write client for the NICOS cache. """ parameters = { 'cache': Param('"host[:port]" of the cache instance to connect to', type=host(defaultport=DEFAULT_CACHE_PORT), mandatory=True), 'prefix': Param('Cache key prefix', type=str, mandatory=True), } remote_callbacks = True _worker = None _startup_done = None def doInit(self, mode): # Should the worker connect or disconnect? self._should_connect = True # this event is set as soon as: # * the connection is established and the connect_action is done, or # * the initial connection failed # this prevents devices from polling parameter values before all values # from the cache have been received self._startup_done = threading.Event() self._connected = False self._socket = None self._secsocket = None self._sec_lock = threading.RLock() self._prefix = self.prefix.strip('/') if self._prefix: self._prefix += '/' self._selecttimeout = CYCLETIME # seconds self._do_callbacks = self.remote_callbacks self._disconnect_warnings = 0 # maps newprefix -> oldprefix without self._prefix prepended self._inv_rewrites = {} # maps oldprefix -> set of new prefixes without self._prefix prepended self._rewrites = {} self._prefixcallbacks = {} self._stoprequest = False self._queue = queue.Queue() self._synced = True # create worker thread, but do not start yet, leave that to subclasses self._worker = createThread('CacheClient worker', self._worker_thread, start=False) def _getCache(self): return None def doShutdown(self): self._stoprequest = True if self._worker and self._worker.is_alive(): self._worker.join() def _connect(self): self._do_callbacks = False self._startup_done.clear() self.log.debug('connecting to %s', self.cache) try: self._socket = tcpSocket(self.cache, DEFAULT_CACHE_PORT, timeout=5, keepalive=10) except Exception as err: self._disconnect('unable to connect to %s: %s' % (self.cache, err)) else: self.log.info('now connected to %s', self.cache) self._connected = True self._disconnect_warnings = 0 try: self._connect_action() except Exception as err: self._disconnect('unable to init connection to %s: %s' % (self.cache, err)) self._startup_done.set() self._do_callbacks = self.remote_callbacks def _disconnect(self, why=''): self._connected = False self._startup_done.clear() if why: if self._disconnect_warnings % 10 == 0: self.log.warning(why) self._disconnect_warnings += 1 if self._socket: closeSocket(self._socket) self._socket = None # close secondary socket with self._sec_lock: if self._secsocket: closeSocket(self._secsocket) self._secsocket = None self._disconnect_action() def _wait_retry(self): sleep(self._long_loop_delay) def _wait_data(self): pass def _connect_action(self): # send request for all keys and updates.... # (send a single request for a nonexisting key afterwards to # determine the end of data) msg = '@%s%s\n%s%s\n' % (self._prefix, OP_WILDCARD, END_MARKER, OP_ASK) self._socket.sendall(to_utf8(msg)) # read response data, n = b'', 0 sentinel = to_utf8(END_MARKER + OP_TELLOLD + '\n') while not data.endswith(sentinel) and n < 1000: data += self._socket.recv(BUFSIZE) n += 1 # send request for all updates msg = '@%s%s\n' % (self._prefix, OP_SUBSCRIBE) self._socket.sendall(to_utf8(msg)) for prefix in self._prefixcallbacks: msg = '@%s%s\n' % (prefix, OP_SUBSCRIBE) self._socket.sendall(to_utf8(msg)) self._process_data(data) def _disconnect_action(self): pass def _handle_msg(self, time, ttlop, ttl, tsop, key, op, value): raise NotImplementedError('implement _handle_msg in subclasses') def _process_data(self, data, sync_str=to_utf8(SYNC_MARKER + OP_TELLOLD), lmatch=line_pattern.match, mmatch=msg_pattern.match): # n = 0 i = 0 # avoid making a string copy for every line match = lmatch(data, i) while match: line = match.group(1) i = match.end() if sync_str in line: self.log.debug('process data: received sync: %r', line) self._synced = True else: msgmatch = mmatch(from_utf8(line)) # ignore invalid lines if msgmatch: # n += 1 try: self._handle_msg(**msgmatch.groupdict()) except Exception: self.log.exception('error handling message %r', msgmatch.group()) # continue loop match = lmatch(data, i) # self.log.debug('processed %d items', n) return data[i:] def _worker_thread(self): while True: try: self._worker_inner() except Exception: self.log.exception('exception in cache worker thread; ' 'restarting (please report a bug)') if self._stoprequest: break # ensure we do not restart during shutdown else: # normal termination break def _worker_inner(self): data = b'' process = self._process_data while not self._stoprequest: if self._should_connect: if not self._socket: self._connect() if not self._socket: self._wait_retry() continue else: if self._socket: self._disconnect() self._wait_retry() continue # process data so far data = process(data) # wait for a whole line of data to arrive while b'\n' not in data and self._socket and self._should_connect \ and not self._stoprequest: # optionally do some action while waiting self._wait_data() if self._queue.empty(): # NOTE: the queue.empty() check is not 100% reliable, but # that is not important here: all we care is about not # having the select always return immediately for writing writelist = [] else: writelist = [self._socket] # read or write some data while 1: try: res = select.select([self._socket], writelist, [], self._selecttimeout) except EnvironmentError as e: if e.errno == errno.EINTR: continue raise except TypeError: # socket was None, let the outer loop handle that res = ([], [], []) break if res[1]: # determine if something needs to be sent tosend = '' itemcount = 0 try: # bunch a few messages together, but not unlimited for _ in xrange(10): tosend += self._queue.get(False) itemcount += 1 except queue.Empty: pass # write data try: self._socket.sendall(to_utf8(tosend)) except Exception: self._disconnect('disconnect: send failed') # report data as processed, but then re-queue it to send # after reconnect for _ in range(itemcount): self._queue.task_done() data = b'' self._queue.put(tosend) break for _ in range(itemcount): self._queue.task_done() if res[0]: # got some data try: newdata = self._socket.recv(BUFSIZE) except Exception: newdata = b'' if not newdata: # no new data from blocking read -> abort self._disconnect('disconnect: recv failed') data = b'' break data += newdata if self._socket: # send rest of data tosend = '' itemcount = 0 try: while 1: tosend += self._queue.get(False) itemcount += 1 except queue.Empty: pass try: self._socket.sendall(to_utf8(tosend)) except Exception: self.log.debug('exception while sending last batch of updates', exc=1) # no reraise, we'll disconnect below anyways for _ in range(itemcount): self._queue.task_done() # end of while loop self._disconnect() def _single_request(self, tosend, sentinel=b'\n', retry=2, sync=False): """Communicate over the secondary socket.""" if not self._socket: self._disconnect('single request: no socket') if not self._socket: raise CacheError('cache not connected') if sync: # sync has to be false for lock requests, as these occur during startup self._queue.join() with self._sec_lock: if not self._secsocket: try: self._secsocket = tcpSocket(self.cache, DEFAULT_CACHE_PORT) except Exception as err: self.log.warning( 'unable to connect secondary socket ' 'to %s: %s', self.cache, err) self._secsocket = None self._disconnect('secondary socket: could not connect') raise CacheError('secondary socket could not be created') try: # write request # self.log.debug("get_explicit: sending %r", tosend) self._secsocket.sendall(to_utf8(tosend)) # give 10 seconds time to get the whole reply timeout = currenttime() + 10 # read response data = b'' while not data.endswith(sentinel): newdata = self._secsocket.recv(BUFSIZE) # blocking read if not newdata: raise socket.error('cache closed connection') if currenttime() > timeout: # do not just break, we need to reopen the socket raise socket.error('getting response took too long') data += newdata except socket.error: self.log.warning('error during cache query', exc=1) closeSocket(self._secsocket) self._secsocket = None if retry: for m in self._single_request(tosend, sentinel, retry - 1): yield m return raise lmatch = line_pattern.match mmatch = msg_pattern.match i = 0 # self.log.debug("get_explicit: data =%r", data) match = lmatch(data, i) while match: line = match.group(1) i = match.end() msgmatch = mmatch(from_utf8(line)) if not msgmatch: # ignore invalid lines continue # self.log.debug('line processed: %r', line) yield msgmatch match = lmatch(data, i) def waitForStartup(self, timeout): self._startup_done.wait(timeout) def flush(self): """wait for empty output queue""" self._synced = False self._queue.put('%s%s\n' % (SYNC_MARKER, OP_ASK)) self._queue.join() for _ in range(100): # self.log.debug('flush; waiting for sync...') if self._synced: break sleep(CYCLETIME) def addPrefixCallback(self, prefix, function): """Add a "prefix" callback, which is called for every key and value that does not match the prefix parameter of the client, but matches the prefix given to this function. """ if prefix not in self._prefixcallbacks: self._queue.put('@%s%s\n' % (prefix, OP_SUBSCRIBE)) self._prefixcallbacks[prefix] = function def removePrefixCallback(self, prefix): """Remove a "prefix" callback. This removes the callback previously installed by addPrefixCallback. If prefix is unknown, then do nothing. """ if prefix in self._prefixcallbacks: self._queue.put('@%s%s\n' % (prefix, OP_UNSUBSCRIBE)) del self._prefixcallbacks[prefix] # methods to make this client usable as the main device in a simple session def start(self, *args): self._connect() self._worker.start() def wait(self): while not self._stoprequest: sleep(self._long_loop_delay) if self._worker and self._worker.is_alive(): self._worker.join() def quit(self, signum=None): self.log.info('quitting on signal %s...', signum) self._stoprequest = True def lock(self, key, ttl=None, unlock=False, sessionid=None): """Locking/unlocking: opens a separate connection.""" tosend = '%s%s%s%s%s\n' % (self._prefix, key.lower(), OP_LOCK, unlock and OP_LOCK_UNLOCK or OP_LOCK_LOCK, sessionid or session.sessionid) if ttl is not None: tosend = ('+%s@' % ttl) + tosend for msgmatch in self._single_request(tosend, sync=False): if msgmatch.group('value'): raise CacheLockError(msgmatch.group('value')) return # no response received; let's assume standalone mode self.log.warning('allowing lock/unlock operation without cache ' 'connection') def unlock(self, key, sessionid=None): return self.lock(key, ttl=None, unlock=True, sessionid=sessionid) def storeSysInfo(self, service): """Store info about the service in the cache.""" if not self._socket: return try: key, res = getSysInfo(service) msg = '%s@%s%s%s\n' % (currenttime(), key, OP_TELL, cache_dump(res)) self._socket.sendall(to_utf8(msg)) except Exception: self.log.exception('storing sysinfo failed')
class CenteredDistanceDevice(HasLimits, CanReference, Moveable): """ Device that controls the distance between attached devices A and B that can be moved symmetrically with respect to their center. It is assumed that the distance between A and B is 0 when both devices are at 0. When the coordinates parameter is 'opposite', when both devices' coordinates increase with distance from the center. For 'equal', device A is assumed to move in negative direction from the center, device B in the positive direction. This borrows heavily from Slit, essentially it is a one-axis slit in centered mode. """ attached_devices = { 'a': Attach('Device A', HasPrecision), 'b': Attach('Device B', HasPrecision), } parameters = { 'coordinates': Param('Coordinate convention for device A and B', default='equal', type=oneof('equal', 'opposite')), } parameter_overrides = { 'unit': Override(mandatory=False), 'abslimits': Override(mandatory=False), } hardware_access = False def doInit(self, mode): self._axes = [self._attached_a, self._attached_b] self._axnames = ['A', 'B'] def doReadAbslimits(self): limits_a = self._attached_a.abslimits limits_b = self._attached_b.abslimits if self.coordinates == 'equal': return limits_b[0] - limits_a[1], limits_b[1] - limits_a[0] return limits_b[0] + limits_a[0], limits_b[1] + limits_a[1] def doStart(self, target): self._doStartPositions(self._getPositions(target)) def _doStartPositions(self, positions): for ax, pos in zip(self._axes, positions): ax.move(pos) def _getPositions(self, target): half_distance = target / 2. if self.coordinates == 'equal': return (-half_distance, half_distance) return (half_distance, half_distance) def doReset(self): multiReset(self._axes) multiWait(self._axes) def doReference(self): for ax in self._axes: if isinstance(ax, CanReference): self.log.info('referencing %s...', ax) ax.reference() else: self.log.warning('%s cannot be referenced', ax) def doRead(self, maxage=0): pos_a = self._attached_a.read(maxage) pos_b = self._attached_b.read(maxage) if self.coordinates == 'equal': return pos_b - pos_a return pos_b + pos_a def doStatus(self, maxage=0): return multiStatus(list(zip(self._axnames, self._axes))) def doReadUnit(self): return self._attached_a.unit
class PixelmanUDPChannel(ActiveChannel): """ Trigger detector controlled by Pixelman software via UDP service One of the detectors at V20 can be triggered through a simple UDP service which listens on the computer that controls the detector. It expects a keyword to start acquisition and then sends a keyword back once it's done. The service expects a new connection for each data acquisition, so the connection is established in the doStart method and removed in doFinish. In order to recover from inconsistent states, the socket is also torn down in doStop, although that won't stop the detector, just reset the connection. At the moment it's not possible to obtain count information from the service, but that may change in the future. """ parameters = { 'host': Param('IP and port for Pixelman Detector UDP interface.', type=host()), 'acquire': Param('Keyword to send for starting the acquisition', type=str), 'finished': Param( 'Keyword to wait for to determine ' 'whether the acquisition is done', type=str), 'acquiring': Param('Internal parameter to synchronise between processes.', type=bool, internal=True, default=False, mandatory=False, settable=False) } parameter_overrides = {'ismaster': Override(default=True, settable=True)} def valueInfo(self): return Value(self.name, unit=self.unit, type='other', fmtstr=self.fmtstr), def doInit(self, mode): self._socket = None def doStart(self): if self._socket is None: self.log.debug('Socket is None, creating socket.') pm_host, pm_port = self.host.split(':') self.log.debug('Connection: ' + self.host) self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._socket.connect((pm_host, int(pm_port))) self.log.debug('Sending Keyword: ' + self.acquire) self._socket.sendall(self.acquire) self._socket.setblocking(0) self._setROParam('acquiring', True) self.log.debug('Acquisition started') else: self.log.info( 'Socket already exists, starting again has no effect.') def doFinish(self): self.log.debug('Finishing...') if self._socket is not None: self.log.debug('Actually shutting down...') self._socket.close() self._socket = None self._setROParam('acquiring', False) def doRead(self, maxage=0): return 0 def doStop(self): self.doFinish() def doStatus(self, maxage=0): if not self._check_complete(): return status.BUSY, 'Acquiring...' return status.OK, 'Idle' def duringMeasureHook(self, elapsed): return None def _check_complete(self): self.log.debug('Checking completion...') if session.sessiontype != POLLER: if self._socket is not None: self.log.debug('Actually performing check...') try: data = self._socket.recv(1024) self.log.debug('Got data: ' + data) return data == self.finished except socket.error: return False self.log.debug('Falling back to Cache...') return not self.acquiring
class SingleSlit(PseudoNOK, HasOffset, Moveable): """Slit using one axis.""" hardware_access = False attached_devices = { 'motor': Attach('moving motor', Moveable), } parameters = { 'mode': Param('Beam mode', type=oneof(*MODES), settable=True, userparam=True, default='slit', category='general'), '_offsets': Param('List of offsets per mode position', settable=False, internal=True, type=dictof(str, float), default={}), 'opmode': Param('Mode of operation for the slit', type=oneof(CENTERED), userparam=True, settable=True, default=CENTERED, category='experiment'), } parameter_overrides = { 'masks': Override(type=dictwith(**{name: float for name in MODES}), unit='', mandatory=True), } valuetype = float def doWriteOffset(self, value): HasOffset.doWriteOffset(self, value) # deep copy is need to be able to change the values d = self._offsets.copy() d[self.mode] = value self._setROParam('_offsets', d) def doRead(self, maxage=0): return self._attached_motor.read(maxage) - self.masks[self.mode] - \ self.offset def doIsAllowed(self, target): return self._attached_motor.isAllowed(target + self.masks[self.mode]) def doStop(self): self._attached_motor.stop() def doStart(self, target): self._attached_motor.start(target + self.masks[self.mode] + self.offset) def doWriteMode(self, mode): self._attached_motor.start( self._attached_motor.read(0) + self.masks[mode] - self.masks[self.mode]) # update the offset parameter from offset mapping self._setROParam('offset', self._offsets.get(mode, 0.)) self.log.debug('New offset is now: %f', self.offset)
class DoubleSlit(PseudoNOK, Moveable): """Double slit using two SingleSlits.""" hardware_access = False attached_devices = { 'slit_r': Attach('Reactor side single slit', SingleSlit), 'slit_s': Attach('Sample side single slit', SingleSlit), } parameters = { 'mode': Param('Modus of Beam', type=oneof(*MODES), settable=True, userparam=True, default='slit', category='experiment'), 'maxheight': Param('Max opening of the slit', type=floatrange(0), settable=False, default=12.), 'opmode': Param( 'Mode of operation for the slit', type=oneof(CENTERED), # '2blades' is possible userparam=True, settable=True, default=CENTERED, category='experiment'), } parameter_overrides = { 'nok_start': Override(volatile=True), 'nok_end': Override(volatile=True), } def doInit(self, mode): # Even if the slit could not be become closer then 0 and not more # opened the maxheight the instrument scientist want to scan over # the limits to find out the 'open' and 'closed' point for the neutrons self.valuetype = tupleof(floatrange(-1, self.maxheight + 1), float) # generate auto devices for name, idx, opmode in [('height', 0, CENTERED), ('center', 1, CENTERED)]: self.__dict__[name] = SingleSlitAxis('%s.%s' % (self.name, name), slit=self, unit=self.unit, lowlevel=True, index=idx, opmode=opmode) self._motors = [self._attached_slit_r, self._attached_slit_s] def doStatus(self, maxage=0): st = Moveable.doStatus(self, maxage) if st[0] == status.OK: return st[0], self.name # display device name return st def doWriteMode(self, mode): for d in self._adevs.values(): d.mode = mode def _calculate_slits(self, arg, direction): self.log.debug('calculate slits: dir:%s mode:%s arg %s', direction, self.mode, str(arg)) if direction: reactor, sample = arg opening = self.maxheight - (sample - reactor) height = (sample + reactor) / 2.0 res = [opening, height] else: opening, height = arg reactor = height - (self.maxheight - opening) / 2.0 sample = height + (self.maxheight - opening) / 2.0 res = [reactor, sample] self.log.debug('res %s', res) return res def doRead(self, maxage=0): return self._calculate_slits([ self._attached_slit_r.read(maxage), self._attached_slit_s.read(maxage) ], True) def doIsAllowed(self, targets): self.log.debug('DoubleSlit doIsAllowed %s', targets) why = [] try: self.valuetype((targets[0], 0)) except ValueError as e: why.append('%s' % e) for dev, pos in zip([self._attached_slit_r, self._attached_slit_s], self._calculate_slits(targets, False)): ok, _why = dev.isAllowed(pos) if not ok: why.append('%s: requested position %.3f %s out of limits; %s' % (dev, pos, dev.unit, _why)) else: self.log.debug('%s: requested position %.3f %s allowed', dev, pos, dev.unit) if why: return False, '; '.join(why) return True, '' # def doIsAtTarget(self, targets): # # check precision, only move if needed! # self.log.debug('DoubleSlit doIsAtTarget %s', targets) # targets = self.rechnen_motor(targets, False, 'doIsAtTarget') # self.log.debug('%s', targets) # traveldists = [target - dev.doRead(0) # for target, dev in zip(targets, self._devices)] # return max(abs(v) for v in traveldists) <= self.precision def doStop(self): for dev in self._adevs.values(): dev.stop() def doStart(self, targets): """Generate and start a sequence if none is running.""" for dev, target in zip([self._attached_slit_r, self._attached_slit_s], self._calculate_slits(targets, False)): dev.start(target) def doReadNok_Start(self): return self._attached_slit_r.nok_start def doReadNok_End(self): return self._attached_slit_s.nok_end def doPoll(self, n, maxage): # also poll sub-AutoDevices we created for dev in devIter(self.__dict__, baseclass=AutoDevice): dev.poll(n, maxage) def valueInfo(self): return Value('%s.height' % self, unit=self.unit, fmtstr='%.2f'), \ Value('%s.center' % self, unit=self.unit, fmtstr='%.2f')
class HasSwitchPv(DeviceMixinBase): """ A mixin that can be used with EPICS based devices. Devices that inherit this mixin get a new property that indicates whether the device is switched on (that may mean different things in different devices): dev.isSwitchedOn To switch the device on or off, use the provided methods: dev.switchOn() dev.switchOff() The link to EPICS is configured via the switchpvs and switchstates parameters. The former defines which PV to read for the status information as well as which one to write to when using the methods. The latter defines what values the PV accepts for on and off respectively. """ parameters = { 'switchstates': Param('Map of boolean switch states to underlying type', type=dictwith(on=anytype, off=anytype), userparam=False), 'switchpvs': Param('Read and write pv for switching device on and off.', type=dictwith(read=pvname, write=pvname), userparam=False) } def _get_pv_parameters(self): # Use colon prefix to prevent name clashes with # PVs specified in EpicsDevice.param switch_pvs = {'switchpv:' + pv for pv in self.switchpvs} return super(HasSwitchPv, self)._get_pv_parameters() | switch_pvs def _get_pv_name(self, pvparam): components = pvparam.split(':', 1) if len(components) == 2 and components[0] == 'switchpv': return self.switchpvs[components[1]] return super(HasSwitchPv, self)._get_pv_name(pvparam) @property def isSwitchedOn(self): """ True if the device is switched on. """ raw_value = self._get_pv('switchpv:read') if raw_value not in self.switchstates.values(): self.log.warning('State by attached switch device not recognized. ' 'Returning raw value.') return raw_value return raw_value == self.switchstates['on'] @usermethod def switchOn(self): """ Switch the device on (writes the 'on' of switchstates map to the write-pv specified in switchpvs). """ if not self.isSwitchedOn: self._put_pv('switchpv:write', self.switchstates['on']) else: self.log.info('Device is already switched on') @usermethod def switchOff(self): """ Switch the device off (writes the 'off' of switchstates map to the write-pv specified in switchpvs). """ if self.isSwitchedOn: self._put_pv('switchpv:write', self.switchstates['off']) else: self.log.info('Device is already switched off')
class SamplePos(Moveable): """Control selector speed via SPS I/O devices.""" attached_devices = { 'active_ap': Attach('Alias for active aperture', DeviceAlias), 'active_x': Attach('Alias for active x translation', DeviceAlias), 'active_y': Attach('Alias for active y translation', DeviceAlias), } parameters = { 'alloweddevs': Param('List of allowed devices for presets', type=listof(str)), 'presets': Param('Presets for sample position switching', type=dictof(str, dictof(str, anytype))), } parameter_overrides = { 'unit': Override(default='', mandatory=False), } def doInit(self, mode): self.valuetype = oneof(*sorted(self.presets, key=num_sort)) self._waitdevs = [] self._aliases = {} self._devpos = {} for setting, values in self.presets.items(): values = dict(values) try: self._aliases[setting] = (values.pop('active_ap'), values.pop('active_x'), values.pop('active_y')) except KeyError: raise ConfigurationError( self, 'setting %r needs active_ap, active_x and active_y ' 'settings' % setting) from None try: for name in self._aliases[setting]: session.getDevice(name) except NicosError as exc: raise ConfigurationError( self, 'could not create/find alias targets for setting %r' % setting) from exc for key in values: if key not in self.alloweddevs: raise ConfigurationError( self, 'device %s is not allowed ' 'to be moved by sample_pos' % key) self._devpos[setting] = values def doRead(self, maxage=0): current_targets = ( self._attached_active_ap.alias, self._attached_active_x.alias, self._attached_active_y.alias, ) setting = None for setting, targets in self._aliases.items(): if targets == current_targets: break else: return 'unknown' ok = True for devname, devpos in self._devpos[setting].items(): dev = session.getDevice(devname) devval = dev.read(maxage) if isinstance(dev, HasPrecision): ok &= abs(devval - devpos) <= dev.precision elif isinstance(dev, Slit): ok &= all(abs(v - w) <= 0.1 for (v, w) in zip(devval, devpos)) else: ok &= devval == devpos if ok: return setting return 'unknown' def doStart(self, target): aliases = self._aliases[target] self._attached_active_ap.alias = aliases[0] self._attached_active_x.alias = aliases[1] self._attached_active_y.alias = aliases[2] self._waitdevs = [] for dev, devpos in self._devpos[target].items(): dev = session.getDevice(dev) dev.move(devpos) self._waitdevs.append(dev) def _getWaiters(self): if self._waitdevs: return self._waitdevs return self._adevs
class FPGAChannelBase(PyTangoDevice, ActiveChannel): """Basic Tango Device for ZEA-2 Counter Card.""" parameters = { 'extmode': Param('Arm for external start instead of starting', type=bool, default=False, settable=True), 'extmask': Param('Bitmask of the inputs to use for external start', type=int, default=0), 'extwait': Param( 'If nonzero, we are waiting for external start ' 'since that timestamp', type=float, default=0, settable=True, internal=True), } def _setPreselection(self): """This method must be present and should set the the preselection value for the card before start. """ raise NotImplementedError def doStart(self): if self.ismaster: self._dev.DevFPGACountReset() # preselection has to be set here and not in doWritePreset # because `DevFPGACountReset()` resets all values. self._setPreselection() if self.extmode: self.extwait = currenttime() self._dev.DevFPGACountArmForExternalStart(self.extmask) else: self._dev.DevFPGACountStart() def doFinish(self): self.extwait = 0 self._dev.DevFPGACountStop() def doStop(self): self.doFinish() def doPause(self): if self.extmode: return False self.finish() return True def doResume(self): self._dev.DevFPGACountStart() def doRead(self, maxage=0): raise NotImplementedError def doStatus(self, maxage=0): # Workaround self._dev.State() does not return DevState.MOVING if self._dev.DevFPGACountGateStatus(): return (status.BUSY, 'counting') else: return (status.OK, '') def doReset(self): if self.status(0)[0] == status.BUSY: self.finish() self._dev.DevFPGACountReset()
class HttpConnector(HasCommunication, Readable): """ Device to connect to the HTTP Server using HTTP Basic Authentication. *baseurl* provided in the parameters is prepended while connecting to the server using GET or POST. Parameter *base64auth* provides a way to authenticate the connection. """ parameters = { 'baseurl': Param('Base request URL to be used', type=str, mandatory=True), 'base64auth': Param('HTTP authentication encoded in base64', type=str, mandatory=True, userparam=False), 'byteorder': Param('Endianness of the raw data on server(big/little)', type=oneof('big', 'little'), default='little'), 'curstatus': Param('Current status of the connection (readonly)', type=tupleof(int, str), settable=True, internal=True) } parameter_overrides = { 'unit': Override(mandatory=False, userparam=False, settable=False) } status_code_msg = { 400: 'Bad request', 403: 'Authentication did not work..', 404: 'Somehow, address was not found!', 500: 'Internal server error', } def doInit(self, mode): # Check if the base url is available self._com_retry(None, requests.get, self.baseurl, headers=self._get_auth()) def _get_auth(self): return {"Authorization": "Basic %s" % self.base64auth} def _com_return(self, result, info): # Check if the communication was successful response = result.status_code if response in self.status_code_msg: raise CommunicationError(self.status_code_msg.get(response)) elif response != 200: raise CommunicationError('Error while connecting to server!') self._setROParam('curstatus', (status.OK, '')) return result def _com_raise(self, err, info): self._setROParam('curstatus', (status.ERROR, 'Communication Error!')) HasCommunication._com_raise(self, err, info) def _com_warn(self, retries, name, err, info): self._com_raise(err, info) def doRead(self, maxage=0): return '' def doStatus(self, maxage=0): return self.curstatus def get(self, name='', params=()): """Connect to *baseurl/name* using the GET protocol :param name: String to be appended to the *baseurl* :param params: GET parameters to be passed :return: (requests.Response) response """ return self._com_retry(None, requests.get, self.baseurl + '/' + name, headers=self._get_auth(), params=params) def post(self, name='', data=()): """Connect to *baseurl/name* using the POST protocol :param name: String to be appended to the *baseurl* :param data: POST parameters to be passed :return: (requests.Response) response """ return self._com_retry(None, requests.post, self.baseurl + '/' + name, headers=self._get_auth(), data=data)