class ReadonlySwitcher(MappedReadable): """Same as the `Switcher`, but for read-only underlying devices.""" attached_devices = { 'readable': Attach('The continuous device which is read', Readable), } parameters = { 'precision': Param('Precision for comparison', type=floatrange(0), default=0), } parameter_overrides = { 'fallback': Override(userparam=False, type=none_or(str), mandatory=False), } hardware_access = False def _readRaw(self, maxage=0): return self._attached_readable.read(maxage) def _mapReadValue(self, pos): prec = self.precision for name, value in iteritems(self.mapping): if prec: if abs(pos - value) <= prec: return name elif pos == value: return name if self.fallback is not None: return self.fallback raise PositionError(self, 'unknown position of %s' % self._attached_readable) def doStatus(self, maxage=0): # if the underlying device is moving or in error state, # reflect its status move_status = self._attached_readable.status(maxage) if move_status[0] not in (status.OK, status.WARN): return move_status # otherwise, we have to check if we are at a known position, # and otherwise return an error status try: if self.read(maxage) == self.fallback: return status.NOTREACHED, 'unconfigured position of %s or '\ 'still moving' % self._attached_readable except PositionError as e: return status.NOTREACHED, str(e) return status.OK, '' def doReset(self): self._attached_readable.reset()
class VirtualReferenceMotor(CanReference, VirtualMotor): """Virtual motor device with reference capability.""" parameters = { 'refpos': Param('Reference position if given', type=none_or(float), settable=False, default=None, unit='main'), 'addr': Param('Bus address of the motor', type=intrange(32, 255), default=71), 'refswitch': Param('Type of the reference switch', type=oneof('high', 'low', 'ref'), default='high', settable=False), } def doReference(self, *args): refswitch = args[0] if args and isinstance(args[0], string_types) \ else None self.log.debug('reference: %s', refswitch) self._setrefcounter() if self.refpos is not None: ret = self.read(0) self.log.debug('%s %r', self.name, self.isAtReference()) return ret return self.refpos def _setrefcounter(self): self.log.debug('in setrefcounter') if self.refpos is not None: self.setPosition(self.refpos) self._setROParam('target', self.refpos) self.log.debug('%r %r', self.refpos, self.target) session.delay(0.1) if not self.isAtReference(): raise UsageError('cannot set reference counter, not at reference ' 'point') def isAtReference(self, refswitch=None): if self.refpos is None: return False pos = self.read(0) is_at_refpos = abs(self.refpos - self.read(0)) <= self.precision if refswitch == 'low': return is_at_refpos and (abs(self.abslimits[0] - pos) < abs(self.abslimits[1] - pos)) elif refswitch == 'high': return is_at_refpos and (abs(self.abslimits[0] - pos) > abs(self.abslimits[1] - pos)) return is_at_refpos
class Sample(NicosSample): """A special device to represent a sample. This has the MLZ-specific sample ID from the sample tracker. """ parameters = { 'sampleid': Param('Sample ID from the sample tracker', type=none_or(int), settable=True, category='sample'), } def clear(self): """Clear experiment-specific information.""" NicosSample.clear(self) self.sampleid = None def _applyParams(self, number, parameters): """Apply sample parameters.""" NicosSample._applyParams(self, number, parameters) self.sampleid = parameters.get('id')
class EpicsDevice(DeviceMixinBase): """ Basic EPICS device. """ hardware_access = True valuetype = anytype parameters = { 'epicstimeout': Param('Timeout for getting EPICS PVs', type=none_or(floatrange(0.1, 60)), default=1.0), 'usepva': Param('If true, PVAcess is used instead of ChannelAccess', type=bool, default=False, preinit=True) } # A set of all parameters that indicate PV names. Since PVs are very # limited, an EpicsDevice is expected to use many different PVs a lot # of times. pv_parameters = set() pv_cache_relations = {} # This will store PV objects for each PV param. _pvs = {} _pvctrls = {} def doPreinit(self, mode): # Don't create PVs in simulation mode self._pvs = {} self._pvctrls = {} if mode != SIMULATION: # When there are standard names for PVs (see motor record), the PV names # may be derived from some prefix. To make this more flexible, the pv_parameters # are obtained via a method that can be overridden in subclasses. pv_parameters = self._get_pv_parameters() # For cases where for example readpv and writepv are the same, this dict makes # sure that only one Channel object is created per PV. pv_names = {} for pvparam in pv_parameters: # Retrieve the actual PV-name from (potentially overridden) method pv_name = self._get_pv_name(pvparam) try: pv = pv_names.setdefault(pv_name, self._create_pv(pv_name)) self._pvs[pvparam] = pv pv.setTimeout(self.epicstimeout) self._pvctrls[pvparam] = pv.get('display').toDict().get('display') if self._pvctrls[pvparam] is None: self._pvctrls[pvparam] = pv.get('control').toDict().get('control', {}) except pvaccess.PvaException: raise CommunicationError(self, 'could not connect to PV %r' % pv_name) else: for pvparam in self._get_pv_parameters(): self._pvs[pvparam] = HardwareStub(self) self._pvctrls[pvparam] = {} def doInit(self, mode): if mode != SIMULATION: self._register_pv_callbacks() def _create_pv(self, pv_name): return pvaccess.Channel(pv_name, pvaccess.PVA if self.usepva else pvaccess.CA) def _get_pv_parameters(self): # The default implementation of this method simply returns the pv_parameters set return self.pv_parameters def _get_pv_name(self, pvparam): # In the default case, the name of a PV-parameter is stored in a parameter. # This method can be overridden in subclasses in case the name can be derived # using some other information. return getattr(self, pvparam) def doStatus(self, maxage=0): return status.OK, '' def _setMode(self, mode): super(EpicsDevice, self)._setMode(mode) # remove the PVs on entering simulation mode, to prevent # accidental access to the hardware if mode == SIMULATION: for key in self._pvs: self._pvs[key] = HardwareStub(self) def _get_pv(self, pvparam, field='value', as_string=False): """ Uses pvaccess to obtain a field from a PV. The default field is value, so that val = self._get_pv('readpv') retrieves the value of the PV. To obtain alarm or other status information, the field parameter can be specified: alarm = self._get_pv('readpv', field='alarm') Args: pvparam: The PV parameter to be queried. Is translated to a PV name internally. field: Field of the PV to obtain, default is value. Returns: Value of the queried PV field. """ # result = self._pvs[pvparam].get(field).toDict().get(field) result = _pvget(self._pvs[pvparam], field, as_string) if result is None: # timeout or channel not connected raise CommunicationError(self, 'timed out getting PV %r from EPICS' % self._get_pv_name(pvparam)) return result def _get_pvctrl(self, pvparam, ctrl, default=None, update=False): if update: self._pvctrls[pvparam] = self._pvs[pvparam].get('display').toDict().get('display') if self._pvctrls[pvparam] is None: self._pvctrls[pvparam] = self._pvs[pvparam].get('control').toDict().get( 'control', {}) return self._pvctrls[pvparam].get(ctrl, default) def _get_pv_datatype(self, pvparam): pv_data = self._pvs[pvparam].get().getStructureDict()['value'] if not isinstance(pv_data, list): return FTYPE_TO_VALUETYPE.get(pv_data, anytype) else: return [FTYPE_TO_VALUETYPE.get(dt, anytype) for dt in pv_data] def _put_pv(self, pvparam, value, wait=True): self._pvs[pvparam].put(value) def _put_pv_blocking(self, pvparam, value, update_rate=0.1): # TODO: figure out why putGet segfaults self._put_pv(pvparam, value) def _register_pv_callbacks(self): """ If this is a poller session, monitor the PVs specified in the ``pv_cache_relations`` member for updates and put the values into the cache. This happens in addition to polling, but makes sure that values get inserted into the cache immediately when they are available. This example would map the value of readpv to the ``value`` of the device: pv_cache_relations = { 'readpv': 'value', } This method has to be called explicitly in ``doInit``, should it be re-implemented. """ if session.sessiontype == POLLER: for pvparam in self._get_pv_parameters(): corresponding_cache_key = self.pv_cache_relations.get(pvparam) if corresponding_cache_key is not None: self._register_pv_update_callback(pvparam, corresponding_cache_key) def _register_pv_update_callback(self, pvparam, cache_key, pv_field='value'): """ Subscribes to a PV monitor that updates the cache whenever the PV is updated via ChannelAccess. Args: pvparam: The pvparam to subscribe to, for example readpv or writepv cache_key: The cache key that corresponds to the PV's value pv_field: Field of the PV to obtain, default is value. """ self.log.info('Registering callback for %s (PV: %s)', pvparam, self._get_pv_name(pvparam)) def update_callback(pv_object, obj=self, key=cache_key): if isinstance(obj, Readable): if key == 'value' or key == 'status': ret = obj.poll() ct = currenttime() obj._cache.put(self, 'status', ret[0], ct, self.maxage) obj._cache.put(self, 'value', ret[1], ct, self.maxage) else: obj._pollParam(key) pv = self._pvs[pvparam] pv.setMonitorMaxQueueLength(10) pv.subscribe('_'.join((self.name, pvparam, cache_key, 'poller')), update_callback) #if not pv.isMonitorActive(): pv.startMonitor('') def _get_mapped_epics_status(self): # Checks the status and severity of all the associated PVs. # Returns the worst status (error prone first) and # a list of all associated pvs having that error status_map = {} for name in self._pvs: epics_status = self._get_pvctrl(name, 'status', update=True) epics_severity = self._get_pvctrl(name, 'severity') mapped_status = STAT_TO_STATUS.get(epics_status, None) if mapped_status is None: mapped_status = SEVERITY_TO_STATUS.get( epics_severity, status.UNKNOWN) status_map.setdefault(mapped_status, []).append( self._get_pv_name(name)) return max(status_map.items())
class EpicsDevice(DeviceMixinBase): """ Basic EPICS device. """ hardware_access = True valuetype = anytype parameters = { 'epicstimeout': Param('Timeout for getting EPICS PVs', type=none_or(floatrange(0.1, 60)), default=1.0), } # A set of all parameters that indicate PV names. Since PVs are very # limited, an EpicsDevice is expected to use many different PVs a lot # of times. pv_parameters = set() # This will store PV objects for each PV param. _pvs = {} _pvctrls = {} def doPreinit(self, mode): # Don't create PVs in simulation mode self._pvs = {} self._pvctrls = {} if mode != SIMULATION: # in case we get started in a thread, make sure to use the global # CA context in that thread if epics.ca.current_context() is None: epics.ca.use_initial_context() # When there are standard names for PVs (see motor record), the PV # names may be derived from some prefix. To make this more flexible, # the pv_parameters are obtained via a method that can be overridden # in subclasses. pv_parameters = self._get_pv_parameters() for pvparam in pv_parameters: # Retrieve the actual PV-name from (potentially overridden) method pvname = self._get_pv_name(pvparam) if not pvname: raise ConfigurationError(self, 'PV for parameter %s was ' 'not found!' % pvparam) pv = self._pvs[pvparam] = epics.pv.PV( pvname, connection_timeout=self.epicstimeout) pv.connect() if not pv.wait_for_connection(timeout=self.epicstimeout): raise CommunicationError(self, 'could not connect to PV %r' % pvname) self._pvctrls[pvparam] = pv.get_ctrlvars() or {} else: for pvparam in self._get_pv_parameters(): self._pvs[pvparam] = HardwareStub(self) self._pvctrls[pvparam] = {} def _get_pv_parameters(self): # The default implementation of this method simply returns the # pv_parameters set return self.pv_parameters def _get_pv_name(self, pvparam): # In the default case, the name of a PV-parameter is stored in ai # parameter. This method can be overridden in subclasses in case the # name can be derived using some other information. return getattr(self, pvparam) def doStatus(self, maxage=0): # Return the status and the affected pvs in case the status is not OK mapped_status, affected_pvs = self._get_mapped_epics_status() status_message = 'Affected PVs: ' + ', '.join( affected_pvs) if mapped_status != status.OK else '' return mapped_status, status_message def _get_mapped_epics_status(self): # Checks the status and severity of all the associated PVs. # Returns the worst status (error prone first) and # a list of all associated pvs having that error if epics.ca.current_context() is None: epics.ca.use_initial_context() status_map = {} for name in self._pvs: epics_status = self._get_pvctrl(name, 'status', update=True) epics_severity = self._get_pvctrl(name, 'severity') mapped_status = STAT_TO_STATUS.get(epics_status, None) if mapped_status is None: mapped_status = SEVERITY_TO_STATUS.get( epics_severity, status.UNKNOWN) status_map.setdefault(mapped_status, []).append( self._get_pv_name(name)) return max(status_map.items()) def _setMode(self, mode): super(EpicsDevice, self)._setMode(mode) # remove the PVs on entering simulation mode, to prevent # accidental access to the hardware if mode == SIMULATION: for key in self._pvs: self._pvs[key] = HardwareStub(self) def _get_pv(self, pvparam, as_string=False): # since NICOS devices can be accessed from any thread, we have to # ensure that the same context is set on every thread if epics.ca.current_context() is None: epics.ca.use_initial_context() result = self._pvs[pvparam].get(timeout=self.epicstimeout, as_string=as_string) if result is None: # timeout raise CommunicationError(self, 'timed out getting PV %r from EPICS' % self._get_pv_name(pvparam)) return result def _get_pvctrl(self, pvparam, ctrl, default=None, update=False): if update: if epics.ca.current_context() is None: epics.ca.use_initial_context() self._pvctrls[pvparam] = self._pvs[pvparam].get_ctrlvars() result = self._pvctrls[pvparam] if result is None: return default return result.get(ctrl, default) def _put_pv(self, pvparam, value, wait=False): if epics.ca.current_context() is None: epics.ca.use_initial_context() self._pvs[pvparam].put(value, wait=wait, timeout=self.epicstimeout) def _put_pv_blocking(self, pvparam, value, update_rate=0.1, timeout=60): if epics.ca.current_context() is None: epics.ca.use_initial_context() pv = self._pvs[pvparam] pv.put(value, use_complete=True) start = currenttime() while not pv.put_complete: if currenttime() - start > timeout: raise CommunicationError('Timeout in setting %s' % pv.pvname) session.delay(update_rate)
class Coder(NicosCoder): """This class supports both IPC absolute and incremental coder cards. It can be used with the `nicos.devices.generic.Axis` class. """ parameters = { 'addr': Param('Bus address of the coder', type=intrange(32, 255), mandatory=True), 'confbyte': Param('Configuration byte of the coder', settable=True, type=intrange(0, 255), prefercache=False), 'zerosteps': Param('Coder steps for physical zero', type=float, unit='steps', settable=True), 'slope': Param('Coder slope', type=float, default=1.0, unit='steps/main', settable=True), 'firmware': Param('Firmware version', type=int), 'steps': Param('Current coder position in steps', type=int, settable=False), 'circular': Param( 'Wrap-around value for circular coders, if negative' ' use it as +/-, else as 0..value, None disables ' 'this', type=none_or(float), settable=True, default=None), 'readings': Param( 'Number of readings to average over ' 'when determining current position', type=int, default=1, settable=True), } attached_devices = { 'bus': Attach('The communication bus', IPCModBus), } def doInit(self, mode): if mode != SIMULATION: self._attached_bus.ping(self.addr) try: actual_confbyte = self.doReadConfbyte() except NicosError: actual_confbyte = -1 if self.confbyte != actual_confbyte: self.doWriteConfbyte(self.confbyte) self.log.warning( 'Confbyte mismatch between setup and card, ' 'overriding card value to 0x%02x', self.confbyte) self._lasterror = None def doVersion(self): return [('IPC encoder card, %s' % self._hwtype, str(self.firmware))] def doReadFirmware(self): return self._attached_bus.get(self.addr, 151) def doReadConfbyte(self): return self._attached_bus.get(self.addr, 152) def doWriteConfbyte(self, byte): self._attached_bus.send(self.addr, 154, byte, 3) def doUpdateConfbyte(self, byte): try: self._type = self._getcodertype(byte) self._resolution = byte & 31 except Exception: self._type = None self._resolution = None @lazy_property def _hwtype(self): """Returns 'analog' or 'digital', used for features that only one of the card types supports. 'analog' type is for potis and 'digital' is for rotary encoders. """ firmware = self._attached_bus.get(self.addr, 151) confbyte = self._attached_bus.get(self.addr, 152) if confbyte < 4: return 'digital' if confbyte == 16: if firmware <= 6: return 'analog' # wild guess for resolvers if confbyte & 0xe0 == 0x20: if firmware < 20: # wild guess, but seems to work... return 'analog' return 'digital' def _getcodertype(self, byte): """Extract coder type from configuration byte.""" if byte < 4: return 'incremental encoder, 24bit, ' + \ ['no reset', 'reset once', 'reset always', 'reset once to halfrange'][byte] if self._hwtype == 'analog': if byte == 16: return 'resolver, 16bit' return 'potentiometer, %dbit' % (byte & 0x1F) proto = byte & 128 and 'endat' or 'ssi' coding = byte & 64 and 'gray' or 'binary' parity = byte & 32 and 'no parity' or 'even parity' return 'absolute encoder, %s-protocol, %s-coded, %s, %dbit' % \ (proto, coding, parity, byte & 31) def doReset(self): self._lasterror = None try: self._attached_bus.send(self.addr, 153) except NicosError: pass else: session.delay(0.5) def _fromsteps(self, value): return float((value - self.zerosteps) / self.slope) def doReadSteps(self): try: try: value = self._attached_bus.get(self.addr, 150) except NicosError: self._endatclearalarm() session.delay(1) # try again value = self._attached_bus.get(self.addr, 150) except NicosError as e: # record last error to return it from doStatus() self._lasterror = str(e) raise self._lasterror = None self.log.debug('value is %d steps', value) return value def doRead(self, maxage=0): # make sure to ask hardware, don't use cached value of steps steps = sum(self.doReadSteps() for _ in range(self.readings)) steps = int(steps / float(self.readings)) self._params['steps'] = steps if self._cache: # save last valid position in cache self._cache.put(self, 'steps', steps) pos = self._fromsteps(steps) if self.circular is not None: # make it wrap around pos = pos % abs(self.circular) # if we want +/- instead of 0 to x and value is >x/2 if self.circular < 0 and pos > -0.5 * self.circular: # subtract x to make it -x/2..0..x/2 (circular is negative # here) pos += self.circular self.log.debug('position is %s', self.format(pos)) return pos def doStatus(self, maxage=0): if self._lasterror: return status.ERROR, self._lasterror return status.OK, '' def doSetPosition(self, target): raise NicosError('setPosition not implemented for IPC coders') def _endatclearalarm(self): """Clear alarm for a binary-endat encoder.""" if self._type is not None and 'endat-protocol' not in self._type: return try: self._attached_bus.send(self.addr, 155, 185, 3) session.delay(0.5) self._attached_bus.send(self.addr, 157, 0, 3) session.delay(0.5) self.doReset() except Exception as err: raise CommunicationError(self, 'cannot clear alarm for encoder') from err
class ReferenceMotor(CanReference, Motor1): """IPC stepper card motor with reference capability.""" parameters = { 'refswitch': Param('Type of the reference switch', type=oneof('high', 'low', 'ref'), mandatory=True, settable=False), 'maxtries': Param('Number of tries to reach the target', type=int, default=3, settable=True), 'parkpos': Param('Position to move after reaching reference switch', type=none_or(float), unit='main', settable=False, default=None), 'refpos': Param('Number of steps at reference position', type=intrange(0, 999999), settable=False, default=500000), 'refspeed': Param('Speed value during the reference move', type=intrange(0, 255), settable=False), 'refstep': Param('Steps to move away from reference switch', type=intrange(0, 999999), settable=False, default=2000), 'refmove': Param('Steps to move to the reference switch', type=intrange(0, 10000), settable=False, default=100), 'refdirection': Param( 'Direction of the reference move' 'to "lower" or "upper" step values', type=oneof('lower', 'upper'), settable=False, default='lower'), } parameters_override = { 'timeout': Override(default=600.), } def doInit(self, mode): Motor1.doInit(self, mode) self._stoprequest = 0 self._refcontrol = None if mode != SIMULATION: try: current_val = self.doReadSpeed() if self.speed != current_val: self.doWriteSpeed(self.speed) except NicosError: self.log.warning("Could not write 'speed' value") try: current_val = self.doReadAccel() if self.accel != current_val: self.doWriteAccel(self.accel) except NicosError: self.log.warning("Could not write 'accel' value") try: current_val = self.doReadMicrostep() if self.microstep != current_val: self.doWriteMicrostep(self.microstep) except NicosError: self.log.warning("Could not write 'microstep' value") self._lasterror = None def doStart(self, target): try: Motor1.doStart(self, target) except Exception as e: r = self._attached_bus.get(self.addr, STATUS) if not (r & 0x20 or r & 0x40 or r & 100 or r & 200): raise self.log.info('Ignoring due to limit switch hitting: %r', e) def doStop(self): self._stoprequest = 1 Motor1.doStop(self) if self._refcontrol and self._refcontrol.is_alive(): self._refcontrol.join() self._refcontrol = None def doStatus(self, maxage=0): """Return the status of the motor controller.""" if self._mode == SIMULATION: return (status.OK, '') elif self._refcontrol and self._refcontrol.is_alive(): return (status.BUSY, 'referencing') return Motor1.doStatus(self, maxage) def doReference(self, *args): refswitch = args[0] if args and isinstance(args[0], str) else None if self.doStatus()[0] == status.BUSY: self.stop() self.wait() # The sixfold and triple cards will be reset as whole card, so values # of all channels will reset !!! # self.reset() # self.wait() if self.doStatus()[0] == status.OK: if self._refcontrol and self._refcontrol.is_alive(): self._refcontrol.join() self._refcontrol = None if self._refcontrol is None: threadname = 'referencing %s' % self self._refcontrol = createThread(threadname, self._reference, args=(refswitch, )) session.delay(0.2) else: raise NicosError(self, 'in error or busy state') def doWriteSteps(self, value): self.log.debug('setting new steps value: %s', value) self._attached_bus.send(self.addr, SET_CURR_POS, value, 6) ret = self._attached_bus.get(self.addr, CURR_POS) self.log.debug('set new steps value: %s', ret) return ret def _start(self, target): target = self._tosteps(target) pos = self._tosteps(self.read(0)) self.log.debug('target is %d steps, pos is %d steps', target, pos) diff = target - pos if diff: softlimit = self._tosteps(self.absmin if self.refdirection == 'lower' else self.absmax) self.log.debug('Diff: %d %d', softlimit, pos) if abs(softlimit - pos) < abs(diff): savediff = diff diff = softlimit - pos if not diff: return self.log.info('Distance to softlimit too short: %d -> %d', savediff, diff) self._attached_bus.send(self.addr, DIR_NEG if diff < 0 else DIR_POS) try: self._attached_bus.send(self.addr, MOVE_REL, abs(diff), 6) except Exception as e: r = self._attached_bus.get(self.addr, STATUS) self.log.debug('status: 0x%04X', r) if not (r & 0x20 or r & 0x40 or r & 100 or r & 200): raise self.log.info('Ignoring due to limit switch hitting: %r', e) session.delay(0.1) # hw_wait will not work here, since the status of the device is # always busy, but only the state of the motor is important while Motor1.doStatus(self, 0)[0] == status.BUSY and \ not self._stoprequest: session.delay(self._base_loop_delay) # motor must be stopped, the hardware will go again if any of the # limit switches is released again, this leads to unpredictable # results! Motor1.doStop(self) while Motor1.doStatus(self, 0)[0] == status.BUSY: session.delay(self._base_loop_delay) self._stoprequest = 0 def _reference(self, refswitch=None): """Drive motor to reference switch.""" self.log.debug('referencing: %s', refswitch) # init referencing self._stoprequest = 0 # if reference switch is explicitly given only drive to given limit # switch fullref = refswitch is None if refswitch is None: refswitch = self.refswitch if refswitch == self.refswitch: refdirection = self.refdirection else: # refdirection must be opposite from configured if self.refdirection == 'upper': refdirection = 'lower' else: refdirection = 'upper' self.log.debug('switch %s, direction %s, %s', refswitch, self.refdirection, refdirection) try: _min, _max, motspeed = self.min, self.max, self.speed self._resetlimits() self._drive_to_limit_switch(refswitch, refdirection) if fullref: if self.isAtReference(): self._move_away_from_reference(refswitch, refdirection) self.resetTimeout(0) self._move_until_referenced() if self.isAtReference(): if self.parkpos is not None: self._start(self.parkpos) if self._stoprequest == 1: raise NicosError(self, 'reference stopped by user') except NicosTimeoutError as e: self.log.error('%s occured during referencing', e) except NicosError as e: self.log.error('%s: occured during referencing', e) except Exception as e: self.log.error('%s: occured during referencing', e) finally: self.log.debug('stoprequest: %d', self._stoprequest) self.speed = motspeed self.min = _min self.max = _max if fullref: try: temp = self.read(0) self.log.info('new position of %s is now %.3f %s', self.name, temp, self.unit) if (self.abslimits[0] - self.precision) <= temp <= \ (self.abslimits[1] + self.precision): self._resetlimits() else: self.log.warning('in _referencing limits not restored ' 'after positioning') except NicosError as e: self.log.warning('error catched in finally positioning %s', e) def _drive_to_limit_switch(self, switch, direction): self.log.debug('driving to %r switch towards %r', switch, direction) if not self.isAtReference(switch): self.log.debug('in _drive_to_limitswitch checkrefswitch') self._drive_to_reference(self.refspeed, switch, direction) if not self.isAtReference(switch): if self._stoprequest == 1: raise NicosError(self, 'limitswitch drive stopped by user') def isAtReference(self, refswitch=None): """Check whether configured reference switch is active.""" self.log.debug('in isAtReference function') if refswitch is None: refswitch = self.refswitch return (refswitch == 'high' and self._isAtHighlimit()) or \ (refswitch == 'low' and self._isAtLowlimit()) or \ (refswitch == 'ref' and self._isAtReferenceSwitch()) def _read_status(self): return self._attached_bus.get(self.addr, STATUS) def _isAtHighlimit(self): val = self._read_status() if self._hwtype == 'sixfold': if self.firmware < 63: return bool(val & 0x20) return bool(val & 0x40) def _isAtLowlimit(self): val = self._read_status() if self._hwtype == 'sixfold': if self.firmware < 63: return bool(val & 0x40) return bool(val & 0x20) def _isAtReferenceSwitch(self): val = self._read_status() return bool(val & 0x80) def _setrefcounter(self, raise_error=True): self.log.debug('in setrefcounter') if not self.isAtReference(): if raise_error: raise UsageError('cannot set reference counter, not at ' 'reference point') self.steps = self.refpos def _resetlimits(self): alim = self.abslimits if isinstance(self, HasOffset): newlim = (alim[0] - self.offset, alim[1] - self.offset) else: newlim = alim if self.userlimits != newlim: self.userlimits = newlim def _drive_to_reference(self, refspeed, refswitch, refdirection): self.log.debug('reference: switch %s, direction %s', refswitch, refdirection) if refswitch in ['high', 'low']: if refdirection == 'lower': if self.slope > 0: stop, start = self._hw_limits else: start, stop = self._hw_limits else: if self.slope > 0: start, stop = self._hw_limits else: stop, start = self._hw_limits # if start > stop: # start, stop = stop, start while not self.isAtReference(): self.setPosition(start) self.log.debug('move to %f from %f', stop, start) self._start(stop) self.log.debug('finished at %f', self.read(0)) if self._stoprequest: raise NicosError(self, 'reference drive stopped by user') def _move_away_from_reference(self, refswitch, refdirection): self.log.debug('%s limit switch active', refswitch) self.steps = self.refpos if self.refstep != 0: d = abs(self.refstep / self.slope) if refdirection == 'lower': d = -d self.log.debug('move away from reference switch %f', d) self._start(self.read(0) - d) if self._stoprequest: raise NicosError(self, 'move away from reference stopped by user') def _move_until_referenced(self): # calculate the step size for each reference move d = abs(self.refmove / self.slope) if self.refdirection == 'lower': d = -d while not self.isAtReference(): p = self.read(0) t = p + d self.log.debug('move to %s limit switch %r -> %r', self.refswitch, p, t) self._start(t) if self._stoprequest: raise NicosError(self, 'move until reference stopped by user') if self.isTimedOut(): raise NicosTimeoutError( self, 'timeout occured during ' ' reference drive') self._setrefcounter(False) # avoid message 'target not reached' in status message self._setROParam('target', self.doRead(0)) @property def _hw_limits(self): return (self._fromsteps(self.min), self._fromsteps(self.max))
class RefAxis(Axis): parameters = { 'refpos': Param('Reference position', type=none_or(float), default=None), 'autoref': Param( 'Number of movements before autoreferencing, ' 'None/0=disable, >0:count all moves, <0:count only ' 'negative moves', type=none_or(int), default=None, settable=True), 'refspeed': Param( 'Motorspeed when referencing or None/0 to use normal' 'speed setting', type=none_or(float), default=None), } _moves = 0 _referencing = False def doStart(self, target): if target < self.read(): self._moves += 1 elif self.autoref and self.autoref > 0: self._moves += 1 if self.autoref and self._moves > abs(self.autoref) and \ not self._referencing: self.log.info('self.autoref limit reached => referencing NOW') self.reference(target) # WARNING: This takes a while ! return Axis.doStart(self, target) # handles offset as well def doReference(self, gotopos=None): # pylint: disable=W0221 """references this axis by finding the reference switch and then setting current position to refpos. 1) Finding the refswitch by going backwards until the refswitch (=negative limit switch) fires, 2) then go forward a little until the switch is not active, 3) then crawl SLOWLY backwards to hit it again. 4) current position is set to self.refpos (e.g. the reference is stored, the referencing done) If an axis can't go reliably backwards (e.g. blocking) in step 1) then this fails!!! the refpos must be within motor.abslimits! """ # Check initial conditions if self.refpos is None: self.log.error('Can\'t reference, no refpos specified!') return if self._mode not in [MASTER, MAINTENANCE]: if self._mode == SIMULATION: self.log.debug('would reference') else: self.log.error('Can\'t reference if not in master or ' 'maintenance mode!') return try: # helper for DRY: check for ANY Refswitch def refsw(motor): return motor.doStatus()[1].lower().find('limit switch') > -1 # helper: wait until the motor HW is no longer busy def wait_for_motor(m): while m.doStatus()[0] == status.BUSY: session.delay(m._base_loop_delay) m.poll() self.stop() # make sure the axis code does not interfere self._referencing = True m = self._attached_motor oldspeed = m.speed # figure out the final position (=current position or gotopos, if # gotopos is given) oldpos = self.doRead() if gotopos is None else gotopos # Step 1) Try to hit the refswitch by turning backwards in a fast # way self.log.info('Referencing: FAST Mode: find refswitch') try: # ignore Userlimits! -> use doStart m.doStart(m.abslimits[0]) except NicosError: # if refswitch is already active, doStart gives an exception pass # wait until a) refswitch fires or b) movement finished wait_for_motor(m) if not refsw(m) and self._checkTargetPosition( self.read(0), self.abslimits[0], error=False): self.log.error('Referencing: No refswitch found!!! Exiting') self.start(oldpos) return # Step 2) Try find a position without refswitch active, but close # to it. self.log.info('Referencing: FAST Mode: looking for inactive ' 'refswitch') steps = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200] for stepsize in steps: self.log.debug('trying %s', self.format(stepsize, unit=True)) m.doStart(m.doRead() + stepsize) wait_for_motor(m) if not refsw(m): break else: self.log.error( 'Referencing: RefSwitch still active after ' '%.1f %s, exiting!', sum(steps), self.unit) self.start(oldpos) return # Step 3) Now SLOWLY crawl onto the refswitch if self.refspeed: m.speed = self.refspeed tries = 7 self.log.info('Referencing: SLOW Mode: find refswitch') while not (refsw(m)) and tries > 0: self.log.debug('Another %d slots left to try', tries) try: m.doStart(m.doRead() - stepsize / 3.) # pylint: disable=W0631 except NicosError: # if refswitch is already active, doStart gives an # exception pass wait_for_motor(m) tries -= 1 m.stop() m.speed = oldspeed if tries == 0: self.log.error( 'Referencing: RefSwitch still not active after ' '%.1f %s, exiting!', self.wraparound, self.unit) self.start(oldpos) return # Step 4) We are _at_ refswitch, motor stopped # => we are at refpos, communicate this to the motor self.poll() self.log.info( 'Found Refswitch at %.2f, should have been at %.2f, ' 'lost %.3f %s', m.doRead(), self.refpos, m.doRead() - self.refpos, self.unit) for d in devIter(self._adevs): if hasattr(d, 'setPosition'): try: d.setPosition(self.refpos) except NicosError as e: self.log.error(str(e)) self.poll() self.log.info('Referenced, moving to position (%.2f)...', oldpos) self.start(oldpos) self._moves = 0 finally: m.speed = oldspeed # if gotopos was given, do not wait... if gotopos is None: self.wait() self._referencing = False
class FreeSpace(Readable): """This is a readable device that returns the free space on a filesystem. It is useful to record this in the cache, for example to enable warnings about low free space before data files cannot be saved anymore. The device status is `OK` until free space is below the value set by the `minfree` parameter. """ parameters = { 'path': Param( 'The path to the filesystem mount point (or "None") ' 'to check the experiment data directory).', type=none_or(str), default=None), 'minfree': Param('Minimum free space for "ok" status', unit='GiB', default=5, settable=True), } parameter_overrides = { 'unit': Override(default='GiB', mandatory=False), 'pollinterval': Override(default=300), # every 5 minutes is sufficient 'maxage': Override(default=330), } def doRead(self, maxage=0): if self.path is None: path = session.experiment.dataroot else: path = self.path try: if os.name == 'nt': free = ctypes.c_ulonglong(0) ret = ctypes.windll.kernel32.GetDiskFreeSpaceExW( ctypes.c_wchar_p(path), None, None, ctypes.pointer(free)) if ret == 0: raise OSError('GetDiskFreeSpaceExW call failed') return free.value / self._factor else: st = os.statvfs(path) return (st.f_frsize * st.f_bavail) / self._factor except OSError as err: raise NicosError(self, 'could not determine free space: %s' % err) def doStatus(self, maxage=0): free = self.read() munit = self.parameters['minfree'].unit mfactor = units.get(munit, (1024**3)) if free * self._factor < self.minfree * mfactor: return status.ERROR, 'free space %(free).2f %(unit)s below ' \ '%(minfree).2f %(munit)s' \ % {'free': free, 'minfree': self.minfree, 'unit': self.unit, 'munit': munit} return status.OK, '%.2f %s free' % (free, self.unit) def doUpdateMinfree(self, value): if self._cache: self._cache.invalidate(self, 'status') def doUpdateUnit(self, unit): factor = units.get(unit, None) if factor is None: raise ConfigurationError('Unsupported unit, allowed: %s' % ','.join(units)) self._factor = factor
class MultiSwitcher(MappedMoveable): """The multi-switcher generalizes the `Switcher` so that for a state change multiple underlying moveable devices can be controlled. This is useful if you have for example two motors that only ever move to certain discrete positions for selected 'configurations', e.g. a monochromator changer. Then you can control both using :: move(changer_switch, 'up') move(changer_switch, 'down') instead of moving the axis to positions hard to understand or remember:: move(changer1, 14.55, changer2, 8.15) move(changer1, 51.39, changer2, 3.14) and still have the underlying continuously moveable devices available for debugging purposes. """ attached_devices = { 'moveables': Attach('The N (continuous) devices which are' ' controlled', Moveable, multiple=True), 'readables': Attach('0 to N (continuous) devices which are' ' used for read back only', Readable, optional=True, multiple=True), } parameters = { 'precision': Param('List of allowed deviations (1 or N) from target ' 'position, or None to disable', mandatory=True, type=none_or(listof(none_or(floatrange(0))))), 'blockingmove': Param('Should we wait for the move to finish?', mandatory=False, default=True, settable=True, type=bool), } parameter_overrides = { 'mapping': Override(description='Mapping of state names to N values ' 'to move the moveables to', type=dictof(anytype, listof(anytype))), 'fallback': Override(userparam=False, type=none_or(anytype), mandatory=False), } hardware_access = False @lazy_property def devices(self): return self._attached_moveables + self._attached_readables def doInit(self, mode): MappedMoveable.doInit(self, mode) for k, t in self.mapping.items(): if len(t) != len(self.devices): raise ConfigurationError(self, 'Switcher state entry for key ' '%r has different length than ' 'moveables list' % k) if self.precision: if len(self.precision) not in [1, len(self.devices)]: raise ConfigurationError(self, 'The precision list must either' ' contain only one element or have ' 'the same amount of elements as the ' 'moveables list') def _startRaw(self, target): """target is the raw value, i.e. a list of positions""" moveables = self._attached_moveables if not isinstance(target, (tuple, list)) or \ len(target) < len(moveables): raise InvalidValueError(self, 'doStart needs a tuple of %d ' 'positions for this device!' % len(moveables)) # only check and move the moveables, which are first in self.devices for d, t in zip(moveables, target): if not d.isAllowed(t): raise InvalidValueError(self, 'target value %r not accepted ' 'by device %s' % (t, d.name)) for d, t in zip(moveables, target): self.log.debug('moving %r to %r', d, t) d.start(t) if self.blockingmove: multiWait(moveables) def _readRaw(self, maxage=0): if self._mode == SIMULATION and self.target is not None: # In simulation mode the values of the readables are assumed to be # given in the mapping table for the current target return tuple(d.read(maxage) for d in self._attached_moveables) + \ tuple(self.mapping[self.target][len(self._attached_moveables):]) return tuple(d.read(maxage) for d in self.devices) def _mapReadValue(self, pos): """maps a tuple to one of the configured values""" hasprec = bool(self.precision) if hasprec: precisions = self.precision if len(precisions) == 1: precisions = [precisions[0]] * len(self.devices) for name, values in self.mapping.items(): if hasprec: for p, v, prec in zip(pos, values, precisions): if prec: if abs(p - v) > prec: break elif p != v: break else: # if there was no break we end here... return name else: if tuple(pos) == tuple(values): return name if self.fallback is not None: return self.fallback raise PositionError(self, 'unknown position of %s: %s' % ( ', '.join(str(d) for d in self.devices), ', '.join(d.format(p) for (p, d) in zip(pos, self.devices)))) def doStatus(self, maxage=0): # if the underlying device is moving or in error state, # reflect its status move_status = multiStatus(self.devices, maxage) if move_status[0] not in (status.OK, status.WARN): return move_status return MappedReadable.doStatus(self, maxage) def doReset(self): multiReset(self._adevs) def doStop(self): multiStop(self._adevs)
class VirtualRealTemperature(HasWindowTimeout, HasLimits, Moveable): """A virtual temperature regulation device with a realistic simulation of a sample in a cryostat, with a PID-controlled heater. """ parameters = { 'jitter': Param('Jitter of the read-out value', default=0, unit='main'), 'regulation': Param('Current temperature (regulation)', settable=False, unit='main', default=2.), 'sample': Param('Current temperature (sample)', settable=False, unit='main', default=2.), 'curstatus': Param('Current status', type=tupleof(int, str), settable=True, default=(status.OK, 'idle'), no_sim_restore=True), 'ramp': Param('Ramping speed of the setpoint', settable=True, type=none_or(floatrange(0, 1000)), unit='main/min'), 'loopdelay': Param('Cycle time for internal thread', default=1, settable=True, unit='s', type=floatrange(0.2, 10)), 'setpoint': Param('Current setpoint', settable=True, unit='main', category='general', default=2.), 'heater': Param('Simulated heater output power in percent', settable=True, unit='%'), 'heaterpower': Param('Simulated heater output power in Watt', settable=False, unit='W'), 'maxpower': Param('Max heater power in W', settable=True, unit='W', default=100), 'p': Param('P-value for regulation', settable=True, default=100, unit='%/main'), 'i': Param('I-value for regulation', settable=True, default=10, unit='%/mains'), 'd': Param('D-value for regulation', settable=True, default=1, unit='%s/main'), 'mode': Param('PID control or open loop heater mode', settable=True, default='manualpid', type=oneof('manualpid', 'manual', 'openloop')), 'speedup': Param('Speed up simulation by a factor', settable=True, default=1, unit='', type=floatrange(0.01, 100)), } parameter_overrides = { 'unit': Override(mandatory=False, default='K'), 'timeout': Override(default=900), } _thread = None _window = None _starttime = 0 _stopflag = False def doInit(self, mode): if mode == SIMULATION: return if self.curstatus[0] < status.OK: # clean up old status values self._setROParam('curstatus', (status.OK, '')) if session.sessiontype != POLLER: # dont run in the poller! self._window = [] self._statusLock = threading.Lock() self._thread = createThread('cryo simulator %s' % self, self.__run) def doShutdown(self): self._stopflag = True def doStart(self, pos): # do nothing more, its handled in the thread... with self._statusLock: # insert target position into history # if target is far away -> loop goes busy # else loop sets to stable again.... currtime = time.time() self._window.append((currtime, pos)) self._starttime = currtime self.curstatus = status.BUSY, 'ramping setpoint' def doRead(self, maxage=0): return self.regulation + self.jitter * (0.5 - random.random()) def doStatus(self, maxage=0): return self.curstatus def doStop(self): self.start(self.setpoint) def doPoll(self, nr, maxage): self._pollParam('setpoint', 1) self._pollParam('curvalue', 1) self._pollParam('curstatus', 1) # # Parameters # def doWriteMaxpower(self, newpower): self.heater = clamp(self.heater * self.maxpower / float(newpower), 0, 100) def doReadTarget(self): # Bootstrapping helper, called at most once. # Start target at the initial current temperature, to avoid going into # BUSY state right away. return self.parameters['regulation'].default # # calculation helpers # def __coolerPower(self, temp): """returns cooling power in W at given temperature""" # quadratic up to 42K, is linear from 40W@42K to 100W@600K # return clamp((temp-2)**2 / 32., 0., 40.) + temp * 0.1 return clamp(15 * atan(temp * 0.01) ** 3, 0., 40.) + temp * 0.1 - 0.2 def __coolerCP(self, temp): """heat capacity of cooler at given temp""" return 75 * atan(temp / 50)**2 + 1 def __heatLink(self, coolertemp, sampletemp): """heatflow from sample to cooler. may be negative...""" flow = (sampletemp - coolertemp) * \ ((coolertemp + sampletemp) ** 2)/400. cp = clamp(self.__coolerCP(coolertemp) * self.__sampleCP(sampletemp), 1, 10) return clamp(flow, -cp, cp) def __sampleCP(self, temp): return 3 * atan(temp / 30) + \ 12 * temp / ((temp - 12.)**2 + 10) + 0.5 def __sampleLeak(self, temp): return 0.02/temp # # Model is a cooling source with __coolingPower and __coolerCP capacity # here we have THE heater and the regulation thermometer # this is connected via a __heatLink to a sample with __heatCapacity and # here we have the sample thermometer # def __run(self): try: self.__moving() except Exception as e: if not self._stopflag: self.log.exception(e) self.curstatus = status.ERROR, str(e) def __moving(self): # complex thread handling: # a) simulation of cryo (heat flow, thermal masses,....) # b) optional PID temperature controller with windup control # c) generating status+updated value+ramp # this thread is not supposed to exit! # local state keeping: regulation = self.regulation sample = self.sample timestamp = time.time() heater = 0 lastflow = 0 last_heaters = (0, 0) delta = 0 I = D = 0 lastD = 0 damper = 1 lastmode = self.mode while not self._stopflag: t = time.time() h = t - timestamp if h < self.loopdelay / damper: time.sleep(clamp(self.loopdelay / damper - h, 0.1, 60)) continue h *= self.speedup # a) sample = self.sample regulation = self.regulation heater = self.heater heatflow = self.__heatLink(regulation, sample) self.log.debug('sample = %.5f, regulation = %.5f, heatflow = %.5g', sample, regulation, heatflow) newsample = max(0, sample + (self.__sampleLeak(sample) - heatflow) / self.__sampleCP(sample) * h) # avoid instabilities due to too small CP newsample = clamp(newsample, sample, regulation) regdelta = (heater * 0.01 * self.maxpower + heatflow - self.__coolerPower(regulation)) newregulation = max(0, regulation + regdelta / self.__coolerCP(regulation) * h) # b) see # http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/ if self.mode != 'openloop': # fix artefacts due to too big timesteps # actually i would prefer reducing loopdelay, but i have no # good idea on when to increase it back again if heatflow * lastflow != -100: if (newregulation - newsample) * (regulation - sample) < 0: # newregulation = (newregulation + regulation) / 2 # newsample = (newsample + sample) / 2 damper += 1 lastflow = heatflow error = self.setpoint - newregulation # use a simple filter to smooth delta a little delta = (delta + regulation - newregulation) / 2. kp = self.p / 10. # LakeShore P = 10*k_p ki = kp * abs(self.i) / 500. # LakeShore I = 500/T_i kd = kp * abs(self.d) / 2. # LakeShore D = 2*T_d P = kp * error I += ki * error * h D = kd * delta / h # avoid reset windup I = clamp(I, 0., 100.) # I is in % # avoid jumping heaterpower if switching back to pid mode if lastmode != self.mode: # adjust some values upon switching back on I = self.heater - P - D v = P + I + D # in damping mode, use a weighted sum of old + new heaterpower if damper > 1: v = ((damper ** 2 - 1) * self.heater + v) / damper ** 2 # damp oscillations due to D switching signs if D * lastD < -0.2: v = (v + heater) / 2. # clamp new heater power to 0..100% heater = clamp(v, 0., 100.) lastD = D self.log.debug('PID: P = %.2f, I = %.2f, D = %.2f, ' 'heater = %.2f', P, I, D, heater) # check for turn-around points to detect oscillations -> # increase damper x, y = last_heaters if (x + 0.1 < y and y > heater + 0.1) or \ (x > y + 0.1 and y + 0.1 < heater): damper += 1 last_heaters = (y, heater) else: # self.heaterpower is set manually, not by pid heater = self.heater last_heaters = (0, 0) heater = round(heater, 3) sample = newsample regulation = newregulation lastmode = self.mode # c) if self.setpoint != self.target: if self.ramp == 0: maxdelta = 10000 else: maxdelta = self.ramp / 60. * h try: self.setpoint = round(self.setpoint + clamp(self.target - self.setpoint, -maxdelta, maxdelta), 3) self.log.debug('setpoint changes to %r (target %r)', self.setpoint, self.target) except (TypeError, ValueError): # self.target might be None pass # keep max self.window seconds long history self._cacheCB('value', regulation, t) # temperature is stable when all recorded values in the window # differ from setpoint by less than tolerance with self._statusLock: if self.setpoint == self.target: self._setROParam('curstatus', (status.OK, '')) damper -= (damper - 1) / 10. # max value for damper is 11 else: self._setROParam('curstatus', (status.BUSY, 'ramping setpoint')) damper -= (damper - 1) / 20. self._setROParam('regulation', round(regulation, 3)) self._setROParam('sample', round(sample, 3)) self._setROParam('heaterpower', round(heater * self.maxpower * 0.01, 3)) self._setROParam('heater', heater) timestamp = t
class Sample(Moveable): """A special device to represent a sample. An instance of this class is used as the *sample* attached device of the `Experiment` object. It can be subclassed to add special sample properties, such as lattice and orientation calculations, or more parameters describing the sample. The device stores the collection of all currently defined samples in its `samples` parameter. When changing samples, it will overwrite the device's other parameters with these values. """ parameters = { 'samplename': Param('Current sample name', type=str, settable=True, category='sample'), 'samplenumber': Param( 'Current sample number: e.g. the position in ' 'a sample changer or the index of the sample ' 'among all defined samples', type=none_or(int), settable=True), 'samples': Param('Information about all defined samples', type=dictof(int, dictof(str, anytype)), settable=True, internal=True, preinit=True), } parameter_overrides = { 'unit': Override(mandatory=False, default=''), } valuetype = str def doRead(self, maxage=0): return self.samplename def doStatus(self, maxage=0): return status.OK, '' def doStart(self, target): self.select(target) def doIsAtTarget(self, pos, target): # never warn about self.target mismatch return True @property def filename(self): return safeName(self.samplename) def doWriteSamplename(self, name): if name: session.elogEvent('sample', name) def clear(self): """Clear experiment-specific information.""" self.samplename = '' self.samplenumber = None self.samples = {} def new(self, parameters): """Create and select a new sample.""" # In this simple base class, we expect the user to use only NewSample, # so we remove stored sample information every time to avoid a buildup # of unused sample information. self.samples = {0: parameters} self.select(0) def set(self, number, parameters): """Set sample information for sample no. *number*.""" if number is None: raise InvalidValueError(self, 'cannot use None as sample number') info = self.samples.copy() if number in info: self.log.warning('overwriting parameters for sample %s (%s)', number, info[number]['name']) info[number] = parameters self.samples = info def select(self, number_or_name): """Select sample with given number or name.""" number = self._findIdent(number_or_name) try: parameters = self.samples[number] except KeyError: raise InvalidValueError( self, 'cannot find sample with number or name %r' % number_or_name) from None self._applyParams(number, parameters) session.experiment.newSample(parameters) self.poll() def _findIdent(self, number_or_name): """Find sample number. Can be overridden in subclasses.""" # look by number if number_or_name in self.samples: return number_or_name # look by name found = None for (number, parameters) in self.samples.items(): if parameters['name'] == number_or_name: if found is not None: # two samples with same name found... raise InvalidValueError( self, 'two samples with name %r ' 'were found, please use the ' 'sample number (%s or %s)' % (number_or_name, found, number)) found = number return found def _applyParams(self, number, parameters): """Apply sample parameters. Override in subclasses. All parameters beside the name should be treated as optional by subclasses, since they will not be provided for the empty sample created by NewExperiment. """ self.samplenumber = number self.samplename = parameters['name'] self._setROParam('target', parameters['name']) def doUpdateSamples(self, info): self.valuetype = oneof(*(info[n]['name'] for n in sorted(info)))
class CARESSDevice(HasCommunication): """The CARESS base device.""" _orb = None _kind = None _used_counter = 0 _caressObject = None _initialized = False _caress_name = '' _caress_maps = {} _caress_initialized = False parameters = { 'config': Param( 'Device configuration/setup string', type=str, mandatory=True, settable=False, ), 'nameserver': Param( 'Computer name running the CORBA name service', type=none_or(str), mandatory=False, default=None, ), 'objname': Param( 'Name of the CORBA object', type=none_or(str), mandatory=False, default=None, ), 'caresspath': Param( 'Directory of the CARESS installation', type=absolute_path, default='/opt/caress/parameter', settable=False, ), 'toolpath': Param( 'Path to the dump_u1 program', type=absolute_path, default='/opt/caress', settable=False, ), 'absdev': Param('CORBA object is a the legacy absdev device', type=bool, default=True, settable=False), 'loadblock': Param('Additional init block', type=str, settable=False, default=''), 'cid': Param('CARESS device ID', type=int, settable=False, internal=True, default=0), } parameter_overrides = { 'comtries': Override(default=5), 'comdelay': Override(default=0.2), } def _initORB(self, args): if not self._orb: self._orb = CORBA.ORB_init(args, CORBA.ORB_ID) def _getCID(self, device): if session.sessiontype == POLLER: while not self.cid: session.delay(0.5) return self.cid self.log.debug('get CARESS device ID: %r', device) answer = createSubprocess( 'cd %s && %s/dump_u1 -n %s' % (self.caresspath, self.toolpath, device), shell=True, stdout=subprocess.PIPE, universal_newlines=True, ).communicate()[0] self._caress_name = device if answer in ('', None): if not CARESSDevice._caress_maps: CARESSDevice._caress_maps[device] = 4096 elif device not in CARESSDevice._caress_maps: CARESSDevice._caress_maps[device] = 1 + \ max(CARESSDevice._caress_maps.values()) res = CARESSDevice._caress_maps[device] else: res = int(answer.split('=')[1]) self.log.debug('get CARESS device ID: %r', res) return res def _is_corba_device(self): return self._device_kind() == CORBA_DEVICE and (not self.absdev) def _device_kind(self): if not self._kind: tmp = self.config.split(None, 2) self._kind = int(tmp[1]) if len(tmp) > 1 else 0 return self._kind def _initObject(self): if not self._orb: raise ProgrammingError(self, 'Programmer forgot to call _initORB') obj = self._orb.resolve_initial_references('NameService') _root_context = obj._narrow(CosNaming.NamingContext) if not _root_context: raise CommunicationError( self, 'Failed to narrow the root naming' ' context') if self._is_corba_device(): try: tmp = self.objname.split('.') if self.objname else \ self.config.split()[2].split('.') if len(tmp) < 2: tmp.append('caress_object') self.log.debug('%r', tmp) obj = _root_context.resolve( [CosNaming.NameComponent(tmp[0], tmp[1])]) except CosNaming.NamingContext.NotFound as ex: raise ConfigurationError(self, 'Name not found: %s' % (ex, )) from ex self._caressObject = obj._narrow(CARESS.CORBADevice) else: try: self._caressObject = \ self._orb.string_to_object('corbaname::%s#%s.context/' 'caress.context/' 'server.context/absdev.object' % (self.nameserver, self.objname)) except CORBA.BAD_PARAM as ex: raise ConfigurationError(self, 'Name not found: %s' % (ex, )) from ex if CORBA.is_nil(self._caressObject): raise CommunicationError(self, 'Could not create a CARESS device') if hasattr(self._caressObject, 'init_module_orb'): self._caressObject.init_module = self._caressObject.init_module_orb def _normalized_config(self): tmp = self.config.split() if tmp[2].count(':') and not self.absdev: tmp[2] = tmp[2].split(':/')[1] return ' '.join(tmp) else: return self.config def _name_server(self): tmp = self.config.split() if tmp[2].count(':') and not self.absdev: return tmp[2].split(':')[0] elif self.nameserver: return self.nameserver else: raise ConfigurationError( self, 'No name server configured. Please ' 'use the "nameserver" parameter or put it' 'into the "config" parameter.') def _init(self, cid): try: if session.sessiontype != POLLER: if hasattr(self._caressObject, 'init_system_orb'): if not CARESSDevice._caress_initialized: self.log.debug('initialize the CARESS absdev ' 'container') if self._caressObject.init_system_orb(0)[0] in \ (0, CARESS.OK): CARESSDevice._caress_initialized = True else: raise CommunicationError( self, 'could not ' 'initialize CARESS absdev' ' container') _config = self._normalized_config() res = self._caressObject.init_module(INIT_CONNECT, cid, _config) self.log.debug('INIT_CONNECT: %r', res) if res[0] in (0, CARESS.OK): if res[1] == OFF_LINE: res = self._caressObject.init_module( INIT_REINIT, cid, _config) else: res = self._caressObject.init_module(INIT_NORMAL, cid, _config) self.log.debug('init module (Connect): %r', res) if res[0] not in (0, CARESS.OK) or res[1] == OFF_LINE: raise NicosError( self, 'Could not initialize module! (%r) %d' % ((res, ), self._device_kind())) # res = self._caressObject.init_module(INIT_REINIT, cid, _config) # self.log.debug('Init module (Re-Init): %r', res) # if res not in[(0, ON_LINE), (CARESS.OK, ON_LINE)]: # self.log.error('Init module (Re-Init): %r (%d, %s)', # res, cid, self.config) if self._device_kind() == CORBA_DEVICE: if self.absdev: res = self._caressObject.char_loadblock_module_orb( 0, cid, 1, len(self.loadblock), 16, self.loadblock) else: val = CARESS.Value(ab=self.loadblock.encode()) res = self._caressObject.loadblock_module( 0, cid, 1, len(self.loadblock), val) # 16, val) self._initialized = True if not self._is_corba_device(): CARESSDevice._used_counter += 1 except CORBA.TRANSIENT as err: raise CommunicationError( self, 'could not init CARESS module %r (%d: %s)' % (err, cid, self.config)) from err def doInit(self, mode): if mode == SIMULATION: return if not omniORB: raise ConfigurationError(self, 'There is no CORBA module found') self._initORB( ['-ORBInitRef', 'NameService=corbaname::%s' % self._name_server()]) self._initObject() _cid = self._getCID(self.config.split(None, 2)[0]) self._init(_cid) if session.sessiontype != POLLER: # omniORB.setClientCallTimeout(self._caressObject, 0) self._setROParam('cid', _cid) if self._cache: self._cache.invalidate(self, 'cid') def doShutdown(self): if session.mode == SIMULATION: return if session.sessiontype != POLLER: if self._caressObject and hasattr(self._caressObject, 'release_system_orb'): if CARESSDevice._used_counter: CARESSDevice._used_counter -= 1 if not CARESSDevice._used_counter: if self._caressObject.release_system_orb(0) in \ (0, CARESS.OK): CARESSDevice._caress_maps.clear() CARESSDevice._caress_initialized = False else: raise NicosError(self, 'Could not release CARESS') self._setROParam('cid', 0) if self._cache: self._cache.invalidate(self, 'cid') self._orb = None self._initialized = False def _read(self): if not self.cid: raise InvalidValueError(self, 'Connection lost to CARESS') if hasattr(self._caressObject, 'read_module'): # result = self._caressObject.read_module(0x80000000, self.cid) result, state, val = self._caressObject.read_module(0, self.cid) if result != CARESS.OK: raise CommunicationError( self, 'Could not read the CARESS module: %d' % self.cid) if hasattr(val, 'f'): return (state, val.f) return ( state, val.l, ) else: _ = () self.log.debug('read module: %d', self.cid) l, result = self._caressObject.read_module_orb(0, self.cid, _) self.log.debug('read_module: %d, %r', l, result) if l != 0: raise CommunicationError( self, 'Could not read the CARESS module: %d' % self.cid) if result[0].value() != self.cid: raise CommunicationError( self, 'Answer from wrong module!: %d %r' % (self.cid, result[0])) state = result[1].value() if state == OFF_LINE: raise NicosError(self, 'Module is off line!') if result[2].value() < 1: raise InvalidValueError(self, 'No position in data') return state, result[4].value() def doRead(self, maxage=0): try: return self._caress_guard(self._read)[1] except (InvalidValueError, CommunicationError, NicosError): if session.sessiontype == POLLER: return None raise def doStatus(self, maxage=0): try: state = self._caress_guard(self._read)[0] if state == OFF_LINE: return status.ERROR, 'device is offline' elif state in (ACTIVE, ACTIVE1, COMBO_ACTIVE): return status.BUSY, 'moving or in manual mode' elif state == DONE: return status.OK, 'idle or paused' elif state == LOADED: return status.OK, 'loaded' elif state == NOT_ACTIVE: return status.OK, 'device is not active' self.log.warning('Unhandled status : %r', state) return status.OK, 'idle' except (InvalidValueError, CommunicationError, NicosError) as e: return status.ERROR, str(e) def _caress_guard_nolog(self, function, *args): if not self._initialized or not self._caressObject: CARESSDevice.doInit(self, self._mode) # self._com_lock.aquire() try: return function(*args) except (CORBA.COMM_FAILURE, CORBA.TRANSIENT) as err: tries = self.comtries - 1 while True and tries > 0: self.log.warning('Remaining tries: %d', tries) session.delay(self.comdelay) if isinstance(err, CORBA.TRANSIENT): CARESSDevice.doShutdown(self) CARESSDevice.doInit(self, self._mode) session.delay(self.comdelay) try: return function(*args) except (CORBA.COMM_FAILURE, CORBA.TRANSIENT) as err: tries -= 1 raise CommunicationError( self, 'CARESS error: %s%r: %s' % (function.__name__, args, err)) from err finally: pass # self._com_lock.release() _caress_guard = _caress_guard_nolog
class DetectorPosSwitcher(DetectorPosSwitcherMixin, SequencerMixin, MappedMoveable): hardware_access = False attached_devices = { 'det_z': Attach('Large detector Z axis', Moveable), 'bs_x': Attach('Large detector beamstop X axis', Moveable), 'bs_y': Attach('Large detector beamstop Y axis', Moveable), } parameters = { 'presets': Param('Presets that determine the mappings', type=dictof(str, dictof(str, dictwith(x=float, y=float, z=float))), mandatory=True), 'offsets': Param( 'Offsets to correct TOF chopper-detector length ' 'for the errors in the det_z axis value', type=dictof(float, float), mandatory=True), 'mapkey': Param('Last selector position for mapping', type=str, settable=True, internal=True), 'beamstopsettlepos': Param('Settling position for beamstop y axis', type=none_or(float), settable=True, default=400), } parameter_overrides = { 'mapping': Override(mandatory=False, settable=True, internal=True), 'fallback': Override(userparam=False, type=str, mandatory=True), } def doInit(self, mode): # check that an offset is defined for each z distance for _selpos, selpresets in self.presets.items(): for _pname, preset in selpresets.items(): if preset['z'] not in self.offsets: raise ConfigurationError( self, 'no detector offset found in configuration ' 'for detector distance of %.2f m' % preset['z']) MappedMoveable.doInit(self, mode) # apply mapping of last selector pos in case it changed if mode == MASTER: self._updateMapping(self.mapkey) def _updateMapping(self, selpos): self.log.debug('updating the detector mapping for selector ' 'setting %s' % selpos) try: pos = self.presets.get(selpos, {}) new_mapping = { k: [v['x'], v['y'], v['z']] for (k, v) in pos.items() } self.mapping = new_mapping self.mapkey = selpos self.valuetype = oneof_detector(*sorted(new_mapping, key=num_sort)) if self._cache: self._cache.invalidate(self, 'value') self._cache.invalidate(self, 'status') except Exception: self.log.warning('could not update detector mapping', exc=1) def _startRaw(self, pos): if self._seq_is_running(): if self._mode == SIMULATION: self._seq_thread.join() self._seq_thread = None else: raise MoveError( self, 'Cannot start device, sequence is still ' 'running (at %s)!' % self._seq_status[1]) det_z = self._attached_det_z seq = [] seq.append(SeqDev(self._attached_bs_y, pos[1], stoppable=True)) seq.append(SeqDev(self._attached_bs_x, pos[0], stoppable=True)) seq.append(SeqDev(det_z, pos[2], stoppable=True)) # if z has to move, reposition beamstop y afterwards by going to # some other value (damping vibrations) and back if self.beamstopsettlepos is not None and \ abs(det_z.read(0) - pos[2]) > det_z.precision: seq.append( SeqDev(self._attached_bs_y, self.beamstopsettlepos, stoppable=True)) seq.append(SeqSleep(30)) seq.append(SeqDev(self._attached_bs_y, pos[1], stoppable=True)) self._startSequence(seq) def _readRaw(self, maxage=0): return { n: (d.read(maxage), getattr(d, 'precision', 0)) for (n, d) in self._adevs.items() } def _mapReadValue(self, pos): def eq(posname, val): return abs(pos[posname][0] - val) <= pos[posname][1] for name, values in self.mapping.items(): if eq('det_z', values[2]) and eq('bs_x', values[0]) and \ eq('bs_y', values[1]): return name return self.fallback def doStatus(self, maxage=0): seq_status = SequencerMixin.doStatus(self, maxage) if seq_status[0] not in (status.OK, status.WARN): return seq_status return MappedMoveable.doStatus(self, maxage) def doReset(self): multiReset(self._adevs) def doStop(self): multiStop(self._adevs)
class Monochromator(HasLimits, Moveable): """Monochromator device of antares. Used to tune the double monochromator to a wavelength between 1.4 and 6.0 Angstroms. Can be moved to None to get a white beam. Experimental version. CHECK THE FORMULAS! """ attached_devices = { 'phi1': Attach('monochromator rotation 1', Moveable), 'phi2': Attach('monochromator rotation 2', Moveable), 'translation': Attach('monochromator translation', Moveable), 'inout': Attach('monochromator inout device', Moveable), } parameters = { 'dvalue1': Param('Lattice constant of Mono1', type=float, settable=True, mandatory=True), 'dvalue2': Param('Lattice constant of Mono2', type=float, settable=True, mandatory=True), 'distance': Param('Parallactic distance of monos', type=float, settable=True, mandatory=True), 'tolphi': Param('Max deviation of phi1 or phi2 from calculated ' 'value', type=float, settable=True, default=0.01), 'toltrans': Param('Max deviation of translation from calculated ' 'value', type=float, settable=True, default=0.01), 'parkingpos': Param('Monochromator parking position', type=dictof(oneof(*attached_devices.keys()), anytype), mandatory=True), } parameter_overrides = { 'unit': Override(mandatory=False, default='Angstrom'), } valuetype = none_or(floatrange(1.4, 6.0)) hardware_access = False @lazy_property def devices(self): return list(self._adevs[i] for i in 'inout phi1 phi2 translation'.split()) def _from_lambda(self, lam): """Return 3 values used for phi1, phi2 and translation.""" phi1 = degrees(asin(lam / float(2 * self.dvalue1))) phi2 = degrees(asin(lam / float(2 * self.dvalue2))) trans = self.distance / tan(2*radians(phi1)) return phi1, phi2, trans def _to_lambda(self, phi1, phi2, trans): """Calculate lambda from phi1, phi2, trans. May raise a PositionError. Not necessarily all arguments are used. Next iteration could evaluate all 3 args and return an average value... """ try: return abs(2 * self.dvalue1 * sin(radians(phi1))) except Exception: raise PositionError('can not determine lambda!') def _moveToParkingpos(self): for dev, target in self.parkingpos.items(): self._adevs[dev].start(target) def doStart(self, target): if target is None: self.log.debug('None given; Moving to parking position') self._moveToParkingpos() return if self.devices[0].read() == 'out': self.log.debug('moving monochromator into beam') for d, v in zip(self.devices, ['in'] + list(self._from_lambda(target))): self.log.debug('sending %s to %r', d.name, v) d.start(v) def doStatus(self, maxage=0): st = multiStatus(listitems(self._adevs), maxage) if st[0] == status.OK: # check position try: self.doRead(maxage) except PositionError as e: return status.NOTREACHED, str(e) return st def doRead(self, maxage=0): pos = [d.read(maxage) for d in self.devices] # Are we in the beam? if pos[0] == 'out': return None # calculate lambda from phi1 and then check the other positions # for consistency... lam = self._to_lambda(*pos[1:]) self.log.debug('lambda seems to be %.4f Angstroms', lam) compare_pos = self._from_lambda(lam) tol = [self.tolphi, self.tolphi, self.toltrans] for d, p, t, c in zip(self.devices[1:], pos[1:], tol, compare_pos): self.log.debug('%s is at %s and should be at %s for %.4f ' 'Angstroms', d, d.format(p), d.format(c), lam) if abs(p-c) > t: raise PositionError('%s is too far away for %.4f Angstroms' % (d, lam)) return lam
class EpicsDevice(DeviceMixinBase): hardware_access = True valuetype = anytype parameters = { 'epicstimeout': Param('Timeout for getting EPICS PVs', type=none_or(floatrange(0.1, 60)), userparam=False, mandatory=False, default=1.0), } parameter_overrides = { # Hide the parameters that are irrelevant when using monitors. 'maxage': Override(userparam=False, settable=False), 'pollinterval': Override(userparam=False, settable=False), } # This will store PV objects for each PV param. _pvs = {} _epics_wrapper = None _record_fields = {} def doPreinit(self, mode): self._epics_wrapper = PvaWrapper() # Don't create PVs in simulation mode self._pvs = {} if mode != SIMULATION: for pvparam in self._get_pv_parameters(): # Retrieve the actual PV name pvname = self._get_pv_name(pvparam) if not pvname: raise ConfigurationError( self, 'PV for parameter %s was ' 'not found!' % pvparam) # Check pv exists - throws if cannot connect self._epics_wrapper.connect_pv(pvname, self.epicstimeout) self._pvs[pvparam] = pvname self._register_pv_callbacks() else: for pvparam in self._get_pv_parameters(): self._pvs[pvparam] = HardwareStub(self) def _register_pv_callbacks(self): pass def _get_pv_parameters(self): return set(self._record_fields.keys()) def _get_pv_name(self, pvparam): if hasattr(self, pvparam): return getattr(self, pvparam) stem = getattr(self, 'readpv') return '.'.join([stem, self._record_fields.get(pvparam, '')]) def doStatus(self, maxage=0): # For most devices we only care about the status of the read PV pvname = self._get_pv_name('readpv') severity, msg = self._epics_wrapper.get_alarm_status( pvname, timeout=self.epicstimeout) if severity in [status.ERROR, status.WARN]: return severity, msg return status.OK, msg def _setMode(self, mode): # remove the PVs on entering simulation mode, to prevent # accidental access to the hardware if mode == SIMULATION: for key in self._pvs: self._pvs[key] = HardwareStub(self) def _get_limits(self, pvparam): return self._epics_wrapper.get_limits(self._get_pv_name(pvparam), timeout=self.epicstimeout) def _get_pv(self, pvparam, as_string=False): cache_key = self._get_cache_relation(pvparam) or pvparam if cache_key in self._values: return self._values[cache_key] return self._epics_wrapper.get_pv_value(self._pvs[pvparam], timeout=self.epicstimeout, as_string=as_string) def _put_pv(self, pvparam, value, wait=False): self._epics_wrapper.put_pv_value(self._pvs[pvparam], value, wait=wait, timeout=self.epicstimeout) def _put_pv_blocking(self, pvparam, value, update_rate=0.1, timeout=60): self._epics_wrapper.put_pv_value_blocking(self._pvs[pvparam], value, update_rate, timeout)
class EpicsMoveable(EpicsMonitorMixin, EpicsDevice, Moveable): """ Handles EPICS devices which can set and read a value. """ parameters = { 'readpv': Param('PV for reading device value', type=pvname, mandatory=True, userparam=False), 'writepv': Param('PV for writing device target', type=pvname, mandatory=True, userparam=False), 'targetpv': Param('Optional target readback PV.', type=none_or(pvname), mandatory=False, userparam=False) } parameter_overrides = { # Units are set by EPICS, so cannot be changed 'unit': Override(mandatory=False, settable=False), 'target': Override(volatile=True), } _cache_relations = { 'readpv': 'value', 'writepv': 'target', } def _get_pv_parameters(self): if self.targetpv: return {'readpv', 'writepv', 'targetpv'} return {'readpv', 'writepv'} def doInit(self, mode): if mode == SIMULATION: return in_type = self._epics_wrapper.get_pv_type(self._pvs['readpv'], self.epicstimeout) out_type = self._epics_wrapper.get_pv_type(self._pvs['writepv'], self.epicstimeout) if in_type != self.valuetype: raise ConfigurationError( self, 'Input PV %r does not have the ' 'correct data type' % self.readpv) if out_type != self.valuetype: raise ConfigurationError( self, 'Output PV %r does not have the ' 'correct data type' % self.writepv) if self.targetpv: target_type = self._epics_wrapper.get_pv_type( self._pvs['targetpv'], self.epicstimeout) if target_type != self.valuetype: raise ConfigurationError( self, 'Target PV %r does not have the ' 'correct data type' % self.targetpv) def doReadTarget(self): if self.targetpv: return self._get_pv('targetpv') else: return self._get_pv('writepv') def doRead(self, maxage=0): return self._get_pv('readpv') def doStart(self, value): self._put_pv('writepv', value) def doStop(self): self.doStart(self.doRead())
class Switcher(MappedMoveable): """The switcher is a device that maps switch states onto discrete values of a continuously moveable device. This is useful if you have for example a motor that only every moves to certain discrete positions, e.g. a monochromator changer. Then you can control it using :: move(changer_switch, 'up') move(changer_switch, 'down') instead of moving the axis to positions hard to understand:: move(changer, 14.55) move(changer, 51.3) and still have the underlying continuously moveable device available for debugging purposes. """ attached_devices = { 'moveable': Attach('The continuous device which is controlled', Moveable), } parameters = { 'precision': Param('Precision for comparison', mandatory=True), 'blockingmove': Param('Should we wait for the move to finish?', type=bool, default=True, settable=True), } parameter_overrides = { 'fallback': Override(userparam=False, type=none_or(str), mandatory=False), } hardware_access = False def _startRaw(self, target): """Initiate movement of the moveable to the translated raw value.""" self._attached_moveable.start(target) if self.blockingmove: self._attached_moveable.wait() def _readRaw(self, maxage=0): """Return raw position value of the moveable.""" return self._attached_moveable.read(maxage) def _mapReadValue(self, pos): """Override default inverse mapping to allow a deviation <= precision. """ prec = self.precision for name, value in self.mapping.items(): if prec: if abs(pos - value) <= prec: return name elif pos == value: return name if self.fallback is not None: return self.fallback if self.relax_mapping: return self._attached_moveable.format(pos, True) raise PositionError(self, 'unknown position of %s: %s' % (self._attached_moveable, self._attached_moveable.format(pos, True)) ) def doStatus(self, maxage=0): # if the underlying device is moving or in error state, # reflect its status move_status = self._attached_moveable.status(maxage) if move_status[0] not in (status.OK, status.WARN): return move_status # otherwise, we have to check if we are at a known position, # and otherwise return an error status try: r = self.read(maxage) if r not in self.mapping: if self.fallback: return (status.UNKNOWN, 'unconfigured position of %s, ' 'using fallback' % self._attached_moveable) return (status.NOTREACHED, 'unconfigured position of %s or ' 'still moving' % self._attached_moveable) except PositionError as e: return status.NOTREACHED, str(e) return status.OK, '' def doReset(self): self._attached_moveable.reset() def doStop(self): self._attached_moveable.stop()
class LockedDevice(BaseSequencer): """A "locked" device, where each movement of the underlying device must be surrounded by moving another device (the "lock") to some value and back after the movement of the main device. The "lock" is moved to the `unlockvalue` before moving the main device. After the main device has moved successfully, the lock is moved either back to its previous value, or if `lockvalue` is not ``None``, to the `lockvalue`. If an error occurs while moving the main device, the lock is not moved back to "locked" position. The error must be resolved first to restore integrity of the device arrangement. """ attached_devices = { 'device': Attach('Moveable device which is protected by the lock', Moveable), 'lock': Attach('The lock, protecting the device', Moveable), } parameters = { 'unlockvalue': Param('The value for the lock to unlock the moveable', mandatory=True, type=anytype), 'keepfixed': Param('Whether to fix lock device if not moving', default=False, type=bool), 'lockvalue': Param('Value for the lock after movement, default None' ' goes to previous value', default=None, type=none_or(anytype)), } def _generateSequence(self, target): device = self._attached_device lock = self._attached_lock seq = [] if self.keepfixed: # release lock first seq.append(SeqMethod(lock, 'release')) # now move lock to unlockvalue seq.append(SeqDev(lock, self.unlockvalue)) if self.keepfixed: # fix lock again seq.append(SeqMethod(lock, 'fix', 'fixed unless %s moves' % self)) seq.append(SeqDev(device, target)) if self.keepfixed: # release lock again seq.append(SeqMethod(lock, 'release')) # now move lock to lockvalue seq.append(SeqDev(lock, self.lockvalue or lock.target or lock.doRead(0))) if self.keepfixed: # fix lock again seq.append(SeqMethod(lock, 'fix', 'fixed unless %s moves' % self)) return seq def doRead(self, maxage=0): return self._attached_device.read(maxage) def doIsAllowed(self, target): return self._attached_device.isAllowed(target)
class NiagShutter(HasTimeout, MappedMoveable): """ Shutter device for the NIAG beam lines. The shutter devices operate using 4 digital IOs (2 outputs and 2 inputs): - The digital outputs are used to open and close the shutter by sending a pulse - The digital inputs indicate whether the shutter is opened, closed or in interstage (in the latter case, both inputs are 0) An additional digital input indicates whether the shutter is enabled Part of the code used for this class was copied or adapted from the MultiSwitcher class written by Georg Brandl and Enrico Faulhaber. """ # the 5 digital IOs (2 pulse outputs, 3 inputs) used to control the shutter attached_devices = { 'do_open': Attach('Output to open the shutter', Moveable), 'do_close': Attach('Output to close the shutter', Moveable), 'is_open': Attach('Input to check if shutter is open', Readable), 'is_closed': Attach('Input to check if shutter is closed', Readable), 'is_enabled': Attach('Input to check if shutter is enabled', Readable), } """ copied from the MultiSwitcher class """ parameters = { 'blockingmove': Param('Should we wait for the move to finish?', mandatory=False, default=True, settable=True, type=bool), } """ copied from the MultiSwitcher class """ parameter_overrides = { 'mapping': Override(description='Mapping of state names to N values ' 'to move the moveables to', type=dictof(anytype, listof(anytype))), 'fallback': Override(userparam=False, type=none_or(anytype), mandatory=False), } """ copied from the MultiSwitcher class """ hardware_access = False """ first value in target indicates whether to open the shutter, second value whether to close it """ def _startRaw(self, target): self._attached_do_open.start(target[0]) self._attached_do_close.start(target[1]) # returns a tuple mad of the (opened?, closed?) values def _readRaw(self, maxage=0): return tuple([ self._attached_is_open.read(maxage), self._attached_is_closed.read(maxage) ]) # based on the method definition from the MultiSwitcher class, simplified # because there is no need to define the precision for digital inputs def _mapReadValue(self, pos): """maps a tuple to one of the configured values""" for name, values in self.mapping.items(): if tuple(pos) == tuple(values): return name if self.fallback is not None: return self.fallback raise PositionError( self, 'unknown position of %s: %s' % (', '.join(str(d) for d in self._adevs), ', '.join( d.format(p) for (p, d) in zip(pos, self._adevs)))) # completion is checked by verifying the feedback values, because # the "motion" of the outputs (pulses) is finished before the shutter # reaches its final position def doIsCompleted(self): reached = self.doRead() == self.target if reached: return True else: # if the position was not reached, consider that the motion # is not complete, unless an error is present """ stat = self.doStatus() return stat[0] < status.BUSY # based on the definition of the MultiSwitcher class, extended for the # needs of the shutter control def doStatus(self, maxage=0): move_status = multiStatus(self._adevs, maxage) # if any of the underlying devices has an error, return it if move_status[0] > status.BUSY: return move_status try: # if the enable bit is not active, return "disabled" status if not self._attached_is_enabled.read(maxage): return status.DISABLED, 'disabled' r = self.doRead(maxage) # the the device is in the fallback position, it is considered # still moving if r == self.fallback: return status.BUSY, 'target not yet reached' return status.OK, 'idle' except PositionError as e: return status.NOTREACHED, str(e) # only allow to start if the enable bit is active def doStart(self, pos): if self._attached_is_enabled.read(): return MappedMoveable.doStart(self, pos) raise MoveError(self, 'Device is disabled') def doReset(self): multiReset(self._adevs)
class Detector(MeasureSequencer): """SPODI specific detector. The detector moves around an axis (2theta) in a number of steps (`resosteps`) over a range of 2 degrees (given in `range`). The starting position is current position + (`resosteps` - 1) * `range` / `resosteps`. The detector data (image) will be stored in an accumulated image array: The first columns of each step will be stored in a sequence, the second columns will follow, and so on. At the end the image will be a refined picture of a single histogram. """ attached_devices = { 'motor': Attach('Axis to perform the reso(lution) steps', Moveable), 'detector': Attach('Standard detector device', GenericDetector), } parameters = { 'resosteps': Param( 'Number of steps performed by the motor to ' 'accumulate a single spectrum', type=oneof(1, 2, 4, 5, 8, 10, 20, 25, 40, 50, 80, 100), default=40, settable=True, userparam=True, category='general', ), 'range': Param( 'Fullrange for the resosteps', type=float, settable=False, default=2.0, category='instrument', ), 'numinputs': Param( 'Number of detector channels', type=int, default=80, settable=False, category='general', ), 'liveinterval': Param( 'Interval to read out live images (None to ' 'disable live readout)', type=none_or(float), unit='s', settable=True, default=0.5, ), 'rates': Param( 'The rates detected by the detector', settable=False, type=listof(float), internal=True, category='status', ), '_startpos': Param( 'Store the starting position', type=float, settable=True, internal=True, category='instrument', ), '_step': Param( 'Store the current step', type=intrange(0, 100), settable=True, internal=True, ), } parameter_overrides = { 'fmtstr': Override(volatile=True), } _last_live = 0 _step = 0 _time_preset = 0 _mon_preset = 0 _arraydesc = None _det_run = False def doInit(self, mode): self._data = [0] * len(self._attached_detector.valueInfo()) self._set_resosteps(self.resosteps) def doInfo(self): ret = self._attached_detector.doInfo() return ret def doPrepare(self): MeasureSequencer.doPrepare(self) self._attached_detector.doPrepare() def doStart(self): self._startpos = self._attached_motor.read() + \ (self.resosteps - 1) * self._step_size self.log.debug('det._startpos: %r', self._startpos) self._setROParam('rates', [0., 0., 0.]) session.experiment.data.updateMetainfo() self._last_live = 0 self._step = 0 self._array_data.fill(0) self._data = [0] * len(self._attached_detector.valueInfo()) MeasureSequencer.doStart(self) def doSetPreset(self, **preset): self._tths = None if preset: self._time_preset = preset.get('t', 0) self._mon_preset = preset.get('mon1', preset.get('mon2', 0)) if 'resosteps' in preset: self.resosteps = int(preset.pop('resosteps')) if 'tths' in preset: self._tths = float(preset.pop('tths')) self._attached_detector.doSetPreset(**preset) def _read_value(self): ret = self._attached_detector.read() self._data = [sum(x) for x in zip(self._data, ret)] # Detector is not busy anymore, but to early to consider it as # 'not busy' self._det_run = False imgret = self._attached_detector.readArrays(FINAL)[0].astype('<u4', order='F') # self.log.info('%r', imgret) if self._mode != SIMULATION: self._array_data[self._step::self.resosteps] = imgret def _incStep(self): if self._step < self.resosteps - 1: self._step += 1 def _startDet(self): """Start the detector and mark it running. Since the detector is not really in BUSY mode after start, we need an additional flag to mark the detector started. """ self._attached_detector.prepare() waitForCompletion(self._attached_detector) self._attached_detector.start() self._det_run = True def doReadArrays(self, quality): self.log.debug('doReadArrays: %d/%d: %d, %r', self._step, self.resosteps, self._array_data.sum(), self._array_data.shape) if quality == LIVE: imgret = self._attached_detector.readArrays(FINAL)[0].astype( '<u4', order='F') self._array_data[self._step::self.resosteps] = imgret return [self._array_data] def _generateSequence(self): seq = [] if self._tths: self._startpos = self._tths seq.append( SeqDev(self._attached_motor, self._startpos, stoppable=True)) for step in range(self.resosteps): pos = self._startpos - step * self._step_size seq.append(SeqDev(self._attached_motor, pos, stoppable=True)) seq.append(SeqCall(self._startDet)) seq.append(SeqWait(self._attached_detector)) seq.append(SeqCall(self._read_value)) seq.append(SeqCall(self._incStep)) return seq def doRead(self, maxage=0): if self._step < self.resosteps: if self._attached_detector.status(0)[0] == status.BUSY \ or self._det_run: ret = [self._step + 1] + \ [sum(x) for x in zip(self._data, self._attached_detector.doRead(maxage))] else: if self._step == 1 and \ MeasureSequencer.status(self, 0)[0] != status.BUSY: ret = [self._step] + self._data else: ret = [self._step + 1] + self._data else: ret = [self._step] + self._data # ret = [step, meastime, mon1, mon2, counts] meastime = ret[1] if meastime > 0.: ctrrate = ret[-1] / meastime mon1rate = ret[2] / meastime mon2rate = 0 if len(self._attached_detector._attached_monitors) > 1: mon2rate = ret[-2] / meastime self._setROParam('rates', [mon1rate, mon2rate, ctrrate]) return ret def doReset(self): self._det_run = False self._last_live = 0 self._step = 0 self._array_data.fill(0) self._attached_detector.doReset() self._data = [0] * len(self._attached_detector.valueInfo()) MeasureSequencer.doReset(self) # self._attached_motor.maw(self._startpos) def doPause(self): self._attached_detector.doPause() def doResume(self): self._attached_detector.doResume() def doFinish(self): self._attached_detector.doFinish() def doSimulate(self, preset): return [self.resosteps] + self._attached_detector.doSimulate(preset) def _set_resosteps(self, value): # TODO: shape should be (y, x) not (x, y) shape = (value * self.numinputs, 256) self._step_size = self.range / value if not self._arraydesc: self._arraydesc = ArrayDesc('data', shape=shape, dtype='<u4') self._array_data = np.zeros(shape, dtype='<u4', order='F') else: self._arraydesc.shape = shape self._array_data = np.resize(self._array_data, shape).astype('<u4', order='F') self._array_data.fill(0) if self._mode != SIMULATION: self._cache.put(self, 'fmtstr', self._fmtstr(value)) self.log.debug('%r', self._arraydesc) self.log.debug('stepsize: %f', self._step_size) def doWriteResosteps(self, value): self._set_resosteps(value) def _fmtstr(self, value): return 'step = %d' + '/%d, ' % value + \ self._attached_detector.doReadFmtstr() def doReadFmtstr(self): return self._fmtstr(self.resosteps) def doEstimateTime(self, elapsed): # TODO calculate the estimated time better in case of monitor counting # the _time_preset value is only value for time counting mode mspeed = self._attached_motor.speed or 1.0 steptime = (self.range / mspeed) / self.resosteps if MeasureSequencer.status(self, 0)[0] == status.BUSY: step = int( abs(self._attached_motor.read() - self._startpos) / self._step_size) ret = (steptime + self._time_preset) * (self.resosteps - step) else: ret = (steptime + self._time_preset) * self.resosteps detTime = self._attached_detector.doEstimateTime(elapsed) ret += detTime if detTime is not None else 0. return ret def valueInfo(self): _val_info = Value('step', unit='', type='other', fmtstr='%d' + '/%d' % self.resosteps), return _val_info + self._attached_detector.valueInfo() def arrayInfo(self): return self._arraydesc, def presetInfo(self): return {'resosteps', 'tths'} | self._attached_detector.presetInfo() def duringMeasureHook(self, elapsed): if self.liveinterval is not None: if self._last_live + self.liveinterval < elapsed: self._last_live = elapsed return LIVE return None def _stopAction(self, nr): self.log.debug('_stopAction at step: %d', nr) self._attached_detector.stop() def _cleanUp(self): if self._seq_was_stopped: self._seq_was_stopped = False self._set_seq_status(status.OK, 'idle')
# Module authors: # Georg Brandl <*****@*****.**> # # ***************************************************************************** """KWS-3 flexible temperature controller.""" from __future__ import absolute_import, division, print_function from nicos.core import Param, dictof, none_or, oneof, tangodev, tupleof from nicos.devices.generic.paramdev import ParamDevice from nicos.devices.tango import TemperatureController from nicos.pycompat import iteritems # out-dev, (in-dev, min-out, max-out, init-pid) if software-regulated entry = tupleof(tangodev, none_or(tupleof(tangodev, float, float, float, float, float))) class FlexRegulator(TemperatureController): """Temperature controller with varying setup for software and hardware regulation.""" parameters = { 'dummy': Param('Dummy input device', type=tangodev, mandatory=True), 'configs': Param('Possible setups', type=dictof(str, entry), mandatory=True), 'config': Param('Current configuration', type=str, volatile=True, settable=True), }
class PushVersionInfo(BaseCacheClient): r"""Pushes the version info to a storage service. The version infos are pushed as a JSON array. The content is not defined, but normally the following keys can be available:: instrument host nicos_root nicos_version custom_version service [opt.] base_version \ these two are only posted base_host / from this service Setting of the token can be done: a) with the nicos-keystore tool: `nicos-keystore add <tokenid>` b) with the keyring tool from th keyring package (this may require additional dependencies to be installed): `keyring -b keyrings.alt.file.EncryptedKeyring set nicos <tokenid>` """ parameters = { 'update_uri': Param( 'URI to send version information to, or None to ' 'disable. The access token and Version info is ' 'directly appended to the URI, encoded as a ' 'query parameter.', type=none_or(str), mandatory=True, userparam=False), 'tokenid': Param('Id used in the keystore for the update token', type=str, default='frm2jenkins'), 'infokey': Param('URI parameter key for the info dict', type=str, mandatory=True), } parameter_overrides = { 'prefix': Override(mandatory=False, default='sysinfo/'), } def doInit(self, mode): if self.update_uri is None: self.log.warning('No update URI configured, updates will not be ' 'sent') if not nicoskeystore.getCredential(self.tokenid): self.log.warning('No token %s found in keystore, updates will not ' 'be sent' % self.tokenid) BaseCacheClient.doInit(self, mode) def _connect_action(self): BaseCacheClient._connect_action(self) self.sendUpdate() def _handle_msg(self, time, ttlop, ttl, tsop, key, op, value): if not key.startswith(self.prefix): return if op in [OP_TELL, OP_TELLOLD]: self.processSysinfo(key, value, time, tell=False) def processSysinfo(self, key, value, time, tell): if not tell: infodict = dict(cache_load(value)) if time: dt = datetime.fromtimestamp(float(time)) infodict['time'] = dt.strftime(TIME_FMT) self.sendUpdate(infodict) def getDaemonInfo(self): instrument = config.instrument base_host = getfqdn() # collect data to send, data should be strings! infodict = dict( time=datetime.now().strftime(TIME_FMT), instrument=instrument, base_host=base_host, base_version=nicos_version, nicos_root=config.nicos_root, custom_path=config.setup_package_path, custom_version=get_custom_version(), ) return infodict def sendUpdate(self, infodict=None, time=None): # make json if not self.update_uri: return token = nicoskeystore.getCredential(self.tokenid) if not token: return if infodict is None: infodict = self.getDaemonInfo() paramdict = {self.infokey: json.dumps(infodict), 'token': token} append = '' if '?' not in self.update_uri: append = '?' elif not self.update_uri[-1] == '&': append = '&' update_string = self.update_uri + append update_string += urllib.parse.urlencode(paramdict) try: urllib.request.urlopen(update_string) self.log.debug('update sent successfully for %s', infodict.get('service', 'base')) except Exception: self.log.debug('cannot send version information! (tried:\n%r\n)', update_string, exc=True)
class EpicsMoveable(EpicsDevice, Moveable): """ Handles EPICS devices which can set and read a value. """ parameters = { 'readpv': Param('PV for reading device value', type=pvname, mandatory=True, userparam=False), 'writepv': Param('PV for writing device target', type=pvname, mandatory=True, userparam=False), 'targetpv': Param('Optional target readback PV.', type=none_or(pvname), mandatory=False, userparam=False) } parameter_overrides = { 'unit': Override(mandatory=False), 'target': Override(volatile=True), } def _get_pv_parameters(self): """ Overriden from EpicsDevice. If the targetpv parameter is specified, the PV-object should be created accordingly. Otherwise, just return the mandatory pv_parameters. """ if self.targetpv: return {'readpv', 'writepv', 'targetpv'} return {'readpv', 'writepv'} def doInit(self, mode): if mode == SIMULATION: return intype = FTYPE_TO_VALUETYPE.get(self._pvs['readpv'].ftype, anytype) outtype = FTYPE_TO_VALUETYPE.get(self._pvs['writepv'].ftype, anytype) if intype is not anytype and intype != self.valuetype: raise ConfigurationError( self, 'Input PV %r does not have the ' 'correct data type' % self.readpv) if outtype is not anytype and outtype != self.valuetype: raise ConfigurationError( self, 'Output PV %r does not have the ' 'correct data type' % self.writepv) if self.targetpv: target_type = FTYPE_TO_VALUETYPE.get(self._pvs['targetpv'].ftype, anytype) if target_type is not anytype and target_type != self.valuetype: raise ConfigurationError( self, 'Target PV %r does not have the ' 'correct data type' % self.targetpv) def doReadUnit(self): return self._get_pvctrl('readpv', 'units', '') def doReadTarget(self): """ IOCs commonly provide a read-back of the set-point according to the device (targetpv). When this read-back is not present then the writepv should be read instead. """ if self.targetpv: return self._get_pv('targetpv') return self._get_pv('writepv') def doRead(self, maxage=0): return self._get_pv('readpv') def doStart(self, value): self._put_pv('writepv', value) def doStop(self): self.doStart(self.doRead())
class GenericLimaCCD(PyTangoDevice, ImageChannelMixin, PassiveChannel): """ This device class can be used together with the LIMA TANGO server to control all common parameters and actions of the supported cameras. For hardware specific additions, have a look at the particular class. """ parameters = { 'hwdevice': Param('Hardware specific tango device', type=tangodev, default='', preinit=True), 'imagewidth': Param('Image width', type=int, volatile=True, category='general'), 'imageheight': Param('Image height', type=int, volatile=True, category='general'), 'roi': Param('Region of interest', type=tupleof(int, int, int, int), settable=True, default=(0, 0, 0, 0), volatile=True, category='general'), 'bin': Param('Binning (x,y)', type=tupleof(int, int), settable=True, default=(1, 1), volatile=True, category='general'), 'flip': Param('Flipping (x,y)', type=tupleof(bool, bool), settable=True, default=(False, False), category='general'), 'rotation': Param('Rotation', type=oneof(0, 90, 180, 270), settable=True, default=0, category='general'), 'expotime': Param('Exposure time', type=float, settable=False, volatile=True, category='general'), 'cameramodel': Param('Camera type/model', type=str, settable=False, volatile=True, # Necessary? category='general'), 'shutteropentime': Param('Shutter open time', type=none_or(float), settable=True, default=0, volatile=False, category='general'), 'shutterclosetime': Param('Shutter open time', type=none_or(float), settable=True, default=0, volatile=False, category='general'), 'shuttermode': Param('Shutter mode', type=none_or(oneof('always_open', 'always_closed', 'auto')), settable=True, default='auto', volatile=True, category='general'), '_starttime': Param('Cached counting start time', type=float, default=0, settable=False, internal=True), # some cached values are necessary as hw params are volatile on request '_curexpotime': Param('Cached exposure time for current acquisition', type=float, default=0, settable=False, internal=True), '_curshutteropentime': Param('Cached shutter open time for current' ' acquisition', type=float, default=0, settable=False, internal=True), '_curshutterclosetime': Param('Cached shutter close time for current' ' acquisition', type=float, default=0, settable=False, internal=True), } _hwDev = None def doPreinit(self, mode): PyTangoDevice.doPreinit(self, mode) # Create hw specific device if given if self.hwdevice: self._hwDev = HwDevice(self.name + '._hwDev', tangodevice=self.hwdevice, lowlevel=True) # optional components self._shutter = None if mode != SIMULATION: self._initOptionalComponents() if self._dev.camera_model.startswith('SIMCAM'): self.log.warning('Using lima simulation camera! If that\'s not' ' intended, please check the cables and ' 'restart the camera and the lima server') # set some dummy roi to avoid strange lima rotation behaviour # (not at 0, 0 to avoid possible problems with strides) self._writeRawRoi((8, 8, 8, 8)) # ensure NO rotation self._dev.image_rotation = 'NONE' # set full detector size as roi self._writeRawRoi((0, 0, 0, 0)) # cache full detector size self._width_height = (self.imagewidth, self.imageheight) else: # some dummy shape for simulation self._width_height = (2048, 1536) def doInit(self, mode): # Determine image type self.arraydesc = ArrayDesc('data', self._width_height[::-1], self._getImageType()) def doShutdown(self): if self._hwDev: self._hwDev.shutdown() def doInfo(self): for p in ('imagewidth', 'imageheight', 'roi', 'bin', 'expotime', 'cameramodel', 'shutteropentime', 'shutterclosetime', 'shuttermode'): self._pollParam(p) return [] def valueInfo(self): # no readresult by default return () def doStart(self): # ignore prep in time calc self._dev.prepareAcq() self._setROParam('_starttime', time.time()) self._setROParam('_curexpotime', self.expotime) if self._shutter is not None: self._setROParam('_curshutteropentime', self.shutteropentime) self._setROParam('_curshutterclosetime', self.shutterclosetime) self._dev.startAcq() def doFinish(self): self._dev.stopAcq() def doStop(self): self.doFinish() def doStatus(self, maxage=0): statusMap = { 'Ready': status.OK, 'Running': status.BUSY, 'Fault': status.ERROR, } limaStatus = self._dev.acq_status nicosStatus = statusMap.get(limaStatus, status.UNKNOWN) if nicosStatus == status.BUSY: deltaTime = time.time() - self._starttime if self._shutter and deltaTime <= self._curshutteropentime: limaStatus += ' (Opening shutter)' elif deltaTime <= (self._curexpotime): remaining = self._curexpotime - deltaTime limaStatus += ' (Exposing; Remaining: %.2f s)' % remaining elif self._shutter and deltaTime <= (self._curshutteropentime + self._curexpotime + self._curshutterclosetime): limaStatus += ' (Closing shutter)' else: limaStatus += ' (Readout)' return (nicosStatus, limaStatus) def doReadImagewidth(self): return self._dev.image_width def doReadImageheight(self): return self._dev.image_height def doReadRoi(self): rawRoi = self._readRawRoi() return self._convRoiFromLima(rawRoi, self.rotation, self.flip) def doWriteRoi(self, value): value = self._convRoiToLima(value, self.rotation, self.flip) self._writeRawRoi(value) def doReadBin(self): return tuple(self._dev.image_bin.tolist()) def doWriteBin(self, value): self._dev.image_bin = value def doWriteFlip(self, value): roi = self.doReadRoi() roi = self._convRoiToLima(roi, self.rotation, value) self._writeRawRoi(roi) def doWriteRotation(self, value): roi = self.doReadRoi() roi = self._convRoiToLima(roi, value, self.flip) self._writeRawRoi(roi) def doReadExpotime(self): return self._dev.acq_expo_time def doReadCameramodel(self): camType = self._dev.camera_type camModel = self._dev.camera_model return '%s (%s)' % (camType, camModel) def doReadShutteropentime(self): if self._shutter: return self._shutter.doReadShutteropentime() return None # will be overwritten def doWriteShutteropentime(self, value): if self._shutter: return self._shutter.doWriteShutteropentime(value) raise HardwareError('Not supported') def doReadShutterclosetime(self): if self._shutter: return self._shutter.doReadShutterclosetime() return None def doWriteShutterclosetime(self, value): if self._shutter: return self._shutter.doWriteShutterclosetime(value) raise HardwareError('Not supported') def doReadShuttermode(self): if self._shutter: return self._shutter.doReadShuttermode() return None def doWriteShuttermode(self, value): if self._shutter: return self._shutter.doWriteShuttermode(value) raise HardwareError('Not supported') def doReadArray(self, quality): if quality == INTERRUPTED: return None response = self._dev.readImage(0) img_data_str = response[1] # response is a tuple (type name, data) dt = numpy.dtype(self._getImageType()) dt = dt.newbyteorder('<') img_data = numpy.frombuffer(img_data_str, dt, offset=64) img_data = numpy.reshape(img_data, (self.imageheight, self.imagewidth)) img_data = numpy.rot90(img_data, self.rotation // 90) if self.flip[0]: img_data = numpy.fliplr(img_data) if self.flip[1]: img_data = numpy.flipud(img_data) return img_data def _initOptionalComponents(self): try: self._shutter = LimaShutter(self._dev, self._hwDev) except NicosError: pass def _getImageType(self): if self._mode == SIMULATION: return numpy.uint32 imageType = self._dev.image_type mapping = { 'Bpp8': numpy.uint8, 'Bpp8S': numpy.int8, 'Bpp16': numpy.uint16, 'Bpp16S': numpy.int16, 'Bpp32': numpy.uint32, 'Bpp32S': numpy.int32, } return mapping.get(imageType, numpy.uint32) def _convRoiToLima(self, roi, rotation, flip): if roi == (0, 0, 0, 0): return (0, 0) + self._width_height roi = self._flipRoi(roi, rotation, flip) roi = self._unrotateRoi(roi, rotation) return roi def _convRoiFromLima(self, roi, rotation, flip): if roi == ((0, 0) + self._width_height): return (0, 0, 0, 0) roi = self._rotateRoi(roi, rotation) roi = self._flipRoi(roi, rotation, flip) return roi def _unrotateRoi(self, roi, rotation): self.log.debug('*un*rotate roi %r by %r', roi, rotation) w, h = self._width_height[0] - 1, self._width_height[1] - 1 # transformation matrix for no rotation transmat = numpy.matrix([ [1, 0, 0], [0, 1, 0], [0, 0, 1] ]) if rotation == 90: transmat = numpy.matrix([ [0, -1, w], [1, 0, 0], [0, 0, 1] ]) elif rotation == 180: transmat = numpy.matrix([ [-1, 0, w], [ 0, -1, h], [ 0, 0, 1] ]) elif rotation == 270: transmat = numpy.matrix([ [ 0, 1, 0], [-1, 0, h], [ 0, 0, 1]]) result = self._transformRoi(roi, transmat) self.log.debug('\t=> %r', result) return result def _rotateRoi(self, roi, rotation): self.log.debug('rotate roi %r from %r', roi, rotation) w, h = self._width_height[0] - 1, self._width_height[1] - 1 # transformation matrix for no rotation transmat = numpy.matrix([ [1, 0, 0], [0, 1, 0], [0, 0, 1] ]) if rotation == 90: transmat = numpy.matrix([ [ 0, 1, 0], [-1, 0, w], [ 0, 0, 1] ]) elif rotation == 180: transmat = numpy.matrix([ [-1, 0, w], [ 0, -1, h], [ 0, 0, 1] ]) elif rotation == 270: transmat = numpy.matrix([ [0, -1, h], [1, 0, 0], [0, 0, 1]]) result = self._transformRoi(roi, transmat) self.log.debug('\t=> %r', result) return result def _flipRoi(self, roi, rotation, flip): self.log.debug('flip roi %r by %r', roi, flip) w, h = self._width_height if rotation in [90, 270]: w, h = h, w x_bot_left, y_bot_left, rw, rh = roi x_top_right = x_bot_left + rw - 1 y_top_right = y_bot_left + rh - 1 if flip[0]: x_bot_left = (w - 1) - x_bot_left x_top_right = (w - 1) - x_top_right if flip[1]: y_bot_left = (h - 1) - y_bot_left y_top_right = (h - 1) - y_top_right x_bot_left = min(x_bot_left, x_top_right) y_bot_left = min(y_bot_left, y_top_right) result = (x_bot_left, y_bot_left, rw, rh) self.log.debug('\t=> %r', result) return result def _transformRoi(self, roi, transmat): x, y, roi_width, roi_height = roi topleft = numpy.matrix([ [x], [y], [1] ]) bottomright = numpy.matrix([ [x+roi_width - 1], [y+roi_height - 1], [1] ]) topleft = transmat * topleft bottomright = transmat * bottomright x_max = max(topleft.item(0), bottomright.item(0)) x_min = min(topleft.item(0), bottomright.item(0)) y_max = max(topleft.item(1), bottomright.item(1)) y_min = min(topleft.item(1), bottomright.item(1)) return (x_min, y_min, x_max - x_min + 1 , y_max - y_min + 1) def _readRawRoi(self): return tuple(self._dev.image_roi.tolist()) def _writeRawRoi(self, value): self._dev.image_roi = value
class Regulator(Moveable): """Device to regulate a attached moveable by monitoring an attached readable. Will be used for monitoring and regulating the amplitude of the frequency generators for the resonating circuits. The regulation is done, assuming that the movable is (approximitely) directly proportional to the sensor.""" attached_devices = { 'moveable': Attach('Device to regulate', Moveable), 'sensor': Attach('Device to evaluate', Readable), } parameters = { 'stepfactor': Param('Factor of regulation steps', type=float, settable=True, mandatory=False, default=0.5), 'minstep': Param('Minimum stepsize if adjusting', type=float, settable=True, mandatory=False, default=0.001), 'deadbandwidth': Param('Width of the dead band', type=float, settable=True, mandatory=False, default=0.05), 'loopdelay': Param('Sleep time when waiting', type=float, unit='s', default=1.0, settable=True), 'maxstep': Param('Maximum step size', type=none_or(float), settable=True, mandatory=False, default=None), 'curstatus': Param('Store the current device status', internal=True, type=tupleof(int, str), settable=True), } parameter_overrides = { 'unit': Override(default='', settable=False, mandatory=False), } hardware_access = False def doInit(self, mode): self._regulation_thread = None self._stop_request = False self.valuetype = self._attached_moveable.valuetype def doShutdown(self): self.doStop() def doStart(self, target): self._stop_request = False if self._regulation_thread is None and session.sessiontype != POLLER: # no regulation thread (yet), but running in daemon -> start one self._regulation_thread = createThread( 'regulation thread %s' % self, self._regulate) self.curstatus = status.BUSY, 'regulating' self.poll() def doStop(self): self._stop_request = True if self._regulation_thread: self._regulation_thread.join() self.poll() def doRead(self, maxage=0): return self._attached_sensor.read(maxage) def doStatus(self, maxage=0): if session.sessiontype != POLLER: # XXX! if self._regulation_thread and self._regulation_thread.is_alive(): # self.curstatus = status.BUSY, 'regulating' pass else: self.curstatus = status.OK, 'idle' return self.curstatus def _regulate(self): while not self._stop_request: try: read_val = self._attached_sensor.read(0) self.log.debug('Readable value: %s', read_val) diff = abs(self.target - read_val) self.log.debug('Difference to the target: %s', diff) if diff > self.deadbandwidth: self.curstatus = status.BUSY, 'regulating' if diff > self.deadbandwidth / 2: cur_write_val = self._attached_moveable.read(0) step = self.stepfactor * (diff - self.deadbandwidth / 2) if step < self.minstep: step = self.minstep maxstep = self.maxstep or step sign = 1 if read_val < self.target else -1 step = min(step, maxstep) * sign new_target = cur_write_val + step self.log.debug( 'Regulation necessary, move attached movable:' '%s -> %s', cur_write_val, new_target) if hasattr(self._attached_moveable, 'absmax'): if new_target > self._attached_moveable.absmax: new_target = self._attached_moveable.absmax if hasattr(self._attached_moveable, 'usermax'): if new_target > self._attached_moveable.usermax: new_target = self._attached_moveable.usermax self._attached_moveable.start(new_target) # TODO: wait? else: self.curstatus = status.OK, 'stable' except NicosError as e: self.log.warning('Skip regulation: %s', e) time.sleep(self.loopdelay) self._regulation_thread = None self._stop_request = False
class Sans1ColliMotor(Sans1ColliBase, CanReference, SequencerMixin, HasTimeout, Motor): """ Device object for a digital output device via a Beckhoff modbus interface. Minimum Parameter Implementation. Relevant Parameters need to be configured in the setupfile or in the Beckhoff PLC. """ relax_mapping = True parameters = { # provided by parent class: speed, unit, fmtstr, warnlimits, abslimits, # userlimits, precision and others 'address': Param('Starting offset of Motor control Block in words', type=MOTOR_VALIDATOR, mandatory=True, settable=False, userparam=False), 'slope': Param('Slope of the Motor in FULL steps per physical ' 'unit', type=float, default=1., unit='steps/main', userparam=False, settable=True), 'microsteps': Param('Microstepping for the motor', type=oneof(1, 2, 4, 8, 16, 32, 64), default=1, userparam=False, settable=False), 'autozero': Param( 'Maximum distance from referencepoint for forced ' 'referencing before moving, or None', type=none_or(float), default=10, unit='main', settable=False), 'autopower': Param('Automatically disable Drivers if motor is idle', type=oneofdict({ 0: 'off', 1: 'on' }), default='on', settable=False), 'refpos': Param('Position of reference switch', unit='main', type=float, mandatory=True, settable=False, prefercache=False), } parameter_overrides = { 'timeout': Override(mandatory=False, default=300), } _busy_until = 0 def doInit(self, mode): Sans1ColliBase.doInit(self, mode) if mode != SIMULATION: if self.autopower == 'on': self._HW_disable() # # access-helpers for accessing the fields inside the MotorControlBlock # def _readControlBit(self, bit): self.log.debug('_readControlBit %d', bit) value = uint16(self._dev.ReadOutputWord(self.address)) return (value & (1 << int(bit))) >> int(bit) def _writeControlBit(self, bit, value): self._busy_until = time.time() + 3 self.log.debug('_writeControlBit %r, %r', bit, value) tmpval = uint16(self._dev.ReadOutputWord(self.address)) tmpval &= ~(1 << int(bit)) tmpval |= (int(value) << int(bit)) self._dev.WriteOutputWord((self.address, uint16(tmpval))) session.delay(0.5) # work around race conditions inside plc.... def _writeDestination(self, value): self.log.debug('_writeDestination %r', value) self._dev.WriteOutputDword((self.address + 2, uint32(value))) def _readStatusWord(self): value = uint16(self._dev.ReadOutputWord(self.address + 4)) self.log.debug('_readStatusWord %04x', value) return value def _readErrorWord(self): value = uint16(self._dev.ReadOutputWord(self.address + 5)) self.log.debug('_readErrorWord %04x', value) return value def _readPosition(self): value = int32(self._dev.ReadOutputDword(self.address + 6)) self.log.debug('_readPosition: -> %d steps', value) return value def _readUpperControlWord(self): self.log.error('_readUpperControlWord') return uint16(self._dev.ReadOutputWord(self.address + 1)) def _writeUpperControlWord(self, value): self.log.debug('_writeUpperControlWord 0x%04x', value) value = uint16(value) self._dev.WriteOutputWord((self.address + 1, value)) def _readDestination(self): value = int32(self._dev.ReadOutputDword(self.address + 2)) self.log.debug('_readDestination: -> %d steps', value) return value def _readReturn(self): value = int32(self._dev.ReadOutputDword(self.address + 8)) self.log.debug('_readReturn: -> %d (0x%08x)', value, value) return value # # math: transformation of position and speed: # µsteps(/s) <-> phys. unit(/s) # def _steps2phys(self, steps): value = steps / float(self.microsteps * self.slope) self.log.debug('_steps2phys: %r steps -> %s', steps, self.format(value, unit=True)) return value def _phys2steps(self, value): steps = int(value * float(self.microsteps * self.slope)) self.log.debug('_phys2steps: %s -> %r steps', self.format(value, unit=True), steps) return steps def _speed2phys(self, speed): # see manual return speed / float(self.microsteps * self.slope * 1.6384e-2) def _phys2speed(self, value): # see manual return int(value * self.slope * self.microsteps * 1.6384e-2) # # Hardware abstraction: which actions do we want to do... # def _HW_enable(self): self._writeControlBit(0, 1) # docu: bit0 = 1: enable def _HW_disable(self): self._writeControlBit(0, 0) # docu: bit0 = 1: enable def _HW_start(self, target): self._writeDestination(self._phys2steps(target)) # docu: bit2 = go to absolute position, autoresets self._writeControlBit(2, 1) def _HW_reference(self): """Do the referencing and update position to refpos""" self._writeControlBit(4, 1) # docu: bit4 = reference, autoresets # according to docu, the refpos is (also) a parameter of the KL.... def doSetPosition(self, value): for _ in range(100): if self._readStatusWord() & (1 << 7): continue break else: raise UsageError( self, 'Can not set position while motor is ' 'moving, please stop it first!') was_on = self._readControlBit(0) if was_on: self._HW_disable() # wait for inactive ACK/NACK for _ in range(1000): if self._readStatusWord() & (3 << 14) == 0: break else: raise CommunicationError( self, 'HW still busy, can not set ' 'position, please retry later.') loops = 10 for loop in range(loops): self.log.debug('setPosition: loop %d of %d', loop, loops) self._writeDestination(self._phys2steps(value)) # index=1: update current position self._writeUpperControlWord((1 << 8) | 1) # Wait for ACK/NACK bits for _ in range(100): last_sw = self._readStatusWord() if last_sw & (3 << 14) != 0: break else: self.log.warning( 'SetPosition command not recognized, retrying') if last_sw & (2 << 14) != 0: self.log.debug('setPosition: got ACK') break elif last_sw & (1 << 14): self.log.debug('setPosition: got NACK') raise CommunicationError( self, 'Setting position failed, ' 'got a NACK!') else: raise CommunicationError( self, 'setPosition command not ' 'recognized by HW, please retry later.') if was_on: self._HW_enable() def _HW_stop(self): self._writeControlBit(6, 1) # docu: bit6 = stop, autoresets def _HW_wait_while_BUSY(self): # XXX timeout? while not self._seq_stopflag: session.delay(0.1) statval = self._readStatusWord() # if motor moving==0 and target reached==1 -> break if (statval & (1 << 7) == 0) and (statval & (1 << 6)): break if statval & (7 << 10): # limit switch triggered or stop issued session.delay(0.1) break def _HW_status(self): """ used Status bits: 0-2 : Load-angle (0 good, 7 bad) 3 : limit switch - 4 : limit switch + 5 : moving in pos. direction 6 : target reached 7 : motor moving 8 : driver on and ready 9 : Overtemperature 10 : Target NOT reached, but a limit switch triggered 11 : Target NOT reached due PowerOff or Stop 12 : Can not move towards requested position, command ignored 14 : N_ACK (last set/get command was unsuccessful), auto clears after 1s 15 : ACK (last get/set command was successful, value in RETURN is valid), auto clears after 1s """ statval = self._readStatusWord() errval = self._readErrorWord() code, msg = status.ERROR, ['Unknown Status value 0x%04x!' % statval] # work around buggy SPS code: # sometimes we get 0x0105..7, which should never happen # as the lowest 3 bits are not relevant, # check only the others and return BUSY # also ignore the limit switch bits # if statval & (0xfff8) == 0x0100: if statval & (0x7fe0) == 0x0100: return status.BUSY, '0x010x!' # status Stuff if statval & (1 << 7): code, msg = status.BUSY, ['busy'] elif statval & (1 << 6): code, msg = status.OK, ['Target reached'] elif ~statval & (1 << 8): code, msg = status.OK, ['Disabled'] elif statval & (1 << 9): code, msg = status.ERROR, ['Overtemperature!'] # check any of bit 10, 11, 12 at the same time! elif statval & (7 << 10): code, msg = status.OK, ['Can not reach Target!'] if errval: code, msg = status.ERROR, ['Error'] if errval & (1 << 0): msg.append('Control voltage too low') if errval & (1 << 1): msg.append('Motor driving voltage too low') if errval & (1 << 2): msg.append('Overcurrent or short in winding A') if errval & (1 << 3): msg.append('Overcurrent or short in winding B') if errval & (1 << 4): msg.append('Open load or broken wire in winding A') if errval & (1 << 5): msg.append('Open load or broken wire in winding B') if errval & (1 << 7): msg.append('Overtemperature (T>125 degC)') if errval & 0b1111111101000000: msg.append('Unknown Error 0x%04x' % errval) # informational stuff if statval & (1 << 4): msg.append('limit switch +') if statval & (1 << 3): msg.append('limit switch -') if statval & (1 << 8): msg.append('driver on and ready') if statval & (1 << 7): msg.append('load=%d' % (statval & 0x0007)) msg = ', '.join(msg) self.log.debug('_HW_Status returns %r', (code, msg)) if self._busy_until > time.time(): code = max(code, status.BUSY) msg = 'timed busy, %s' % msg return code, msg # # Sequencing stuff # def _gen_move_sequence(self, target): # now generate a sequence of commands to execute in order seq = [] # always enable before doing anything seq.append(SeqMethod(self, '_HW_enable')) # check autoreferencing feature if self.autozero is not None: currentpos = self.read(0) mindist = min(abs(currentpos - self.refpos), abs(target - self.refpos)) if mindist < self.autozero: seq.extend(self._gen_ref_sequence()) # now just go where commanded.... seq.append(SeqMethod(self, '_HW_start', target)) seq.append(SeqMethod(self, '_HW_wait_while_BUSY')) if self.autopower == 'on': # disable if requested. seq.append(SeqMethod(self, '_HW_disable')) return seq def _gen_ref_sequence(self): seq = [] # try to mimic anatel: go to 5mm before refpos and then to the negative limit switch seq.append(SeqMethod(self, '_HW_enable')) seq.append(SeqMethod(self, '_HW_start', self.refpos + 5.)) seq.append(SeqMethod(self, '_HW_wait_while_BUSY')) seq.append( SeqMethod( self, '_HW_start', self.absmin if self.absmin < self.refpos else self.refpos - 100)) seq.append(SeqMethod(self, '_HW_wait_while_BUSY')) seq.append(SeqMethod(self, '_HW_reference')) seq.append(SeqMethod(self, '_HW_wait_while_BUSY')) seq.append(SeqMethod(self, 'doSetPosition', self.refpos)) return seq # # nicos methods # def doRead(self, maxage=0): return self._steps2phys(self._readPosition()) def doStart(self, target): if self._seq_is_running(): raise MoveError(self, 'Cannot start device, it is still moving!') self._startSequence(self._gen_move_sequence(target)) def doStop(self): if self._honor_stop: self._seq_stopflag = True self._HW_stop() def doReset(self): self._writeControlBit(7, 1) # docu: bit7 = ERROR-ACK, autoresets self._set_seq_status(status.OK, 'idle') def doStatus(self, maxage=0): """returns highest statusvalue""" if self._mode == SIMULATION: stati = [(status.OK, 'simulation'), self._seq_status] else: stati = [self._HW_status(), self._seq_status] # sort inplace by first element, i.e. status code stati.sort(key=lambda st: st[0]) # select highest (worst) status # if no status is 'worse' then _seq_status, this is _seq_status _status = stati[-1] if self._seq_is_running(): return max(status.BUSY, _status[0]), _status[1] return _status @requires(level='admin') def doReference(self): if self._seq_is_running(): raise MoveError(self, 'Cannot reference a moving device!') seq = self._gen_ref_sequence() if self.autopower == 'on': # disable if requested. seq.append(SeqMethod(self, '_HW_disable')) self._startSequence(seq)
class Detector(Measurable): """Detector using multiple (synchronized) channels. Each channel can have a "preset" set, which means that measurement stops if the channel's value (or an element thereof, for channels with multiple read values) has reached the preset value. Passive channels can only stop the measurement via soft presets (presets that are checked by NICOS) during the countloop and therefore may be overshot by some nontrivial amount. In contrast, the derived `ActiveChannel` is able to stop by itself, usually implemented in hardware, so that the preset is reached exactly, or overshot by very little. In the detector, channels with a preset are called "masters", while channels without are called "slaves". Which channels are masters and slaves can change with every count cycle. """ attached_devices = { 'timers': Attach('Timer channel', PassiveChannel, multiple=True, optional=True), 'monitors': Attach('Monitor channels', PassiveChannel, multiple=True, optional=True), 'counters': Attach('Counter channels', PassiveChannel, multiple=True, optional=True), 'images': Attach('Image channels', ImageChannelMixin, multiple=True, optional=True), 'others': Attach('Channels that return e.g. filenames', PassiveChannel, multiple=True, optional=True), } parameters = { 'liveinterval': Param( 'Interval to read out live images (None ' 'to disable live readout)', type=none_or(float), unit='s', settable=True), 'saveintervals': Param( 'Intervals to read out intermediate images ' '(empty to disable); [x, y, z] will read out ' 'after x, then after y, then every z seconds', type=listof(float), unit='s', settable=True), 'postprocess': Param( 'Post processing list containing tuples of ' '(PostprocessPassiveChannel, ' 'ImageChannelMixin or PassiveChannel, ...)', type=listof(tuple)), } parameter_overrides = { 'fmtstr': Override(volatile=True), } hardware_access = False multi_master = True _last_live = 0 _last_save = 0 _last_save_index = 0 _last_preset = None _user_comment = '' def doInit(self, _mode): self._masters = [] self._slaves = [] self._channel_presets = {} self._postprocess = [] self._postpassives = [] for tup in self.postprocess: if tup[0] not in session.configured_devices: self.log.warning( "device %r not found but configured in " "'postprocess' parameter. No " "post processing for this device. Please " "check the detector setup.", tup[0]) continue postdev = session.getDevice(tup[0]) img_or_passive_devs = [session.getDevice(name) for name in tup[1:]] if not isinstance(postdev, PostprocessPassiveChannel): raise ConfigurationError("Device '%s' is not a " "PostprocessPassiveChannel" % postdev.name) if postdev not in self._channels: raise ConfigurationError("Device '%s' has not been configured " "for this detector" % postdev.name) for dev in img_or_passive_devs: if dev not in self._channels: raise ConfigurationError("Device '%s' has not been " "configured for this detector" % dev.name) elif isinstance(dev, PassiveChannel): self._postpassives.append(dev) self._postprocess.append((postdev, img_or_passive_devs)) # allow overwriting in derived classes def _presetiter(self): """Yield (name, device, type) tuples for all 'preset-able' devices.""" # a device may react to more than one presetkey.... for i, dev in enumerate(self._attached_timers): if i == 0: yield ('t', dev, 'time') for preset in dev.presetInfo(): yield (preset, dev, 'time') for dev in self._attached_monitors: for preset in dev.presetInfo(): yield (preset, dev, 'monitor') for i, dev in enumerate(self._attached_counters): if i == 0: yield ('n', dev, 'counts') for preset in dev.presetInfo(): yield (preset, dev, 'counts') for dev in self._attached_images + self._attached_others: for preset in dev.presetInfo(): yield (preset, dev, 'counts') yield ('live', None, None) def doPreinit(self, mode): presetkeys = {} for name, dev, typ in self._presetiter(): # later mentioned presetnames dont overwrite earlier ones presetkeys.setdefault(name, (dev, typ)) self._channels = uniq(self._attached_timers + self._attached_monitors + self._attached_counters + self._attached_images + self._attached_others) self._presetkeys = presetkeys self._collectMasters() def _collectMasters(self): """Internal method to collect all masters.""" masters = [] slaves = [] for ch in self._channels: if ch.ismaster: masters.append(ch) else: slaves.append(ch) self._masters, self._slaves = masters, slaves def _getPreset(self, preset): """Returns previous preset if no preset has been set.""" if not preset and self._last_preset: return self._last_preset if 'live' not in preset: # do not store live as previous preset self._last_preset = preset return preset def doSetPreset(self, **preset): self._user_comment = preset.pop('info', '') preset = self._getPreset(preset) if not preset: # keep old settings return for master in self._masters: master.ismaster = False self._channel_presets = {} for (name, value) in preset.items(): if name in self._presetkeys and name != 'live': dev = self._presetkeys[name][0] dev.setChannelPreset(name, value) self._channel_presets.setdefault(dev, []).append((name, value)) self._collectMasters() if set(self._masters) != set(self._channel_presets): if not self._masters: self.log.warning('no master configured, detector may not stop') else: self.log.warning( 'master setting for devices %s ignored by ' 'detector', ', '.join(set(self._channel_presets) - set(self._masters))) self.log.debug(" presets: %s", preset) self.log.debug("presetkeys: %s", self._presetkeys) self.log.debug(" masters: %s", self._masters) self.log.debug(" slaves: %s", self._slaves) def doPrepare(self): for slave in self._slaves: slave.prepare() for master in self._masters: master.prepare() def doStart(self): # setting this to -interval, instead of 0, will send some live data at # the very start of each count, clearing the "live" data from last time self._last_live = -(self.liveinterval or 0) self._last_save = 0 self._last_save_index = 0 for slave in self._slaves: slave.start() for master in self._masters: master.start() def doTime(self, preset): self.doSetPreset(**preset) # okay in simmode return self.doEstimateTime(0) or 0 def doPause(self): success = True for slave in self._slaves: success &= slave.pause() for master in self._masters: success &= master.pause() return success def doResume(self): for slave in self._slaves: slave.resume() for master in self._masters: master.resume() def doFinish(self): for master in self._masters: master.finish() for slave in self._slaves: slave.finish() def doStop(self): for master in self._masters: master.stop() for slave in self._slaves: slave.stop() def doRead(self, maxage=0): ret = [] for ch in self._channels: ret.extend(ch.read()) return ret def doReadArrays(self, quality): arrays = [img.readArray(quality) for img in self._attached_images] results = [dev.read(0) for dev in self._postpassives] for postdev, img_or_passive_devs in self._postprocess: postarrays, postresults = [], [] for dev in img_or_passive_devs: if isinstance(dev, ImageChannelMixin): postarrays.append(arrays[self._attached_images.index(dev)]) else: # PassiveChannel postresults.append(results[self._postpassives.index(dev)]) postdev.setReadResult(postarrays, postresults, quality) return arrays def duringMeasureHook(self, elapsed): if self.liveinterval is not None: if self._last_live + self.liveinterval < elapsed: self._last_live = elapsed return LIVE intervals = self.saveintervals if intervals: if self._last_save + intervals[self._last_save_index] < elapsed: self._last_save_index = min(self._last_save_index + 1, len(intervals) - 1) self._last_save = elapsed return INTERMEDIATE return None def doSimulate(self, preset): self.doSetPreset(**preset) # okay in simmode return self.doRead() def doStatus(self, maxage=0): st, text = multiStatus(self._getWaiters(), maxage) if st == status.ERROR: return st, text for master in self._masters: for (name, value) in self._channel_presets.get(master, ()): if master.presetReached(name, value, maxage): return status.OK, text return st, text def doReset(self): for ch in self._channels: ch.reset() def valueInfo(self): ret = [] for ch in self._channels: ret.extend(ch.valueInfo()) return tuple(ret) def arrayInfo(self): return tuple(img.arraydesc for img in self._attached_images) def doReadFmtstr(self): return ', '.join('%s = %s' % (v.name, v.fmtstr) for v in self.valueInfo()) def presetInfo(self): return {'info'} | set(self._presetkeys) def doEstimateTime(self, elapsed): eta = {master.estimateTime(elapsed) for master in self._masters} eta.discard(None) if eta: # first master stops, so take min return min(eta) return None def doInfo(self): ret = [] if self._user_comment: ret.append(('usercomment', self._user_comment, self._user_comment, '', 'general')) presets = [] for (_dev, devpresets) in self._channel_presets.items(): for (key, value) in devpresets: presets.append((self._presetkeys[key][1], value)) if len(presets) > 1: mode = ' or '.join(p[0] for p in presets) ret.append(('mode', mode, mode, '', 'presets')) for (mode, value) in presets: ret.append( ('preset_%s' % mode, value, str(value), '', 'presets')) elif presets: mode, value = presets[0] return ret + [('mode', mode, mode, '', 'presets'), ('preset', value, str(value), '', 'presets')] return ret
class EpicsMoveable(EpicsDevice, Moveable): """ Handles EPICS devices which can set and read a value. """ parameters = { 'readpv': Param('PV for reading device value', type=pvname, mandatory=True, preinit=True), 'writepv': Param('PV for writing device target', type=pvname, mandatory=True, preinit=True), 'targetpv': Param('Optional target readback PV.', type=none_or(pvname), mandatory=False, preinit=True) } parameter_overrides = { 'unit': Override(mandatory=False), 'target': Override(volatile=True), } pv_parameters = {'readpv', 'writepv'} pv_cache_relations = { 'readpv': 'value', 'targetpv': 'target', } def _get_pv_parameters(self): """ Overriden from EpicsDevice. If the targetpv parameter is specified, the PV-object should be created accordingly. Otherwise, just return the mandatory pv_parameters. """ if self.targetpv: return self.pv_parameters | {'targetpv'} return self.pv_parameters def doInit(self, mode): if mode == SIMULATION: return intype = self._get_pv_datatype('readpv') outtype = self._get_pv_datatype('writepv') if intype != self.valuetype: raise ConfigurationError(self, 'Input PV %r does not have the ' 'correct data type' % self.readpv) if outtype != self.valuetype: raise ConfigurationError(self, 'Output PV %r does not have the ' 'correct data type' % self.writepv) if self.targetpv: target_type = self._get_pv_datatype('targetpv') if target_type != self.valuetype: raise ConfigurationError( self, 'Target PV %r does not have the ' 'correct data type' % self.targetpv) self._register_pv_callbacks() def doReadUnit(self): return self._get_pvctrl('readpv', 'units', '') def doReadTarget(self): """ In many cases IOCs provide a readback of the setpoint, here represented as targetpv. Since this is not provided everywhere, it should still be possible to get the target. """ if self.targetpv: return self._get_pv('targetpv') value = self._params.get('target') return value if value is not None else self._config.get('target') def doRead(self, maxage=0): return self._get_pv('readpv') def doStart(self, value): self._put_pv('writepv', value) def doStop(self): self.doStart(self.doRead())
class Monitor(BaseCacheClient): """ A graphical window showing values of cache keys. For cache keys that correspond to NICOS devices, not only the devicevalue, but also the device status and unit are shown. """ # server and prefix parameters come from BaseCacheClient parameters = { 'title': Param('Title of status window', type=str, default='Status'), 'layout': Param('Status monitor layout', type=list, mandatory=True), 'font': Param('Font name for the window', type=str, default='Luxi Sans'), 'valuefont': Param('Font name for the value displays', type=str), 'fontsize': Param('Basic font size', type=int, default=12, settable=True), 'timefontsize': Param('Time headline font size', type=none_or(int), default=None, settable=True), 'padding': Param('Padding for the display fields', type=int, default=2, settable=True), 'geometry': Param('Geometry for status window', type=str, settable=True, ext_desc='For the allowed settings see ' ':option:`--geometry`.'), 'resizable': Param('Whether the window is resizable', type=bool, default=True), 'colors': Param('Color scheme for value displays (dark or light ' 'background)', type=oneof('dark', 'light')), 'showwatchdog': Param('Whether to show watchdog warnings', type=bool, default=True), 'expectmaster': Param( 'Whether a message should indicate that no ' 'NICOS master is active', type=bool, default=True), } parameter_overrides = { 'prefix': Override(mandatory=False, default='nicos/'), } # methods to be implemented in concrete implementations def initGui(self): raise NotImplementedError('Implement initGui() in subclasses') def mainLoop(self): raise NotImplementedError('Implement mainLoop() in subclasses') def closeGui(self): raise NotImplementedError('Implement closeGui() in subclasses') def signalKeyChange(self, field, *args): raise NotImplementedError('Implement signalKeyChange() in subclasses') def switchWarnPanel(self, on): raise NotImplementedError('Implement switchWarnPanel() in subclasses') def reconfigureBoxes(self): raise NotImplementedError('Implement reconfigureBoxes() in subclasses') def updateTitle(self, text): raise NotImplementedError('Implement updateTitle() in subclasses') # methods implemented here _keys_expired = False # whether on disconnect all keys have been expired def start(self, options): # pylint: disable=W0221 self.log.info('monitor starting up, creating main window') self._fontsize = options.fontsize or self.fontsize self._fontsizebig = int(self._fontsize * 1.2) self._timefontsize = (options.timefontsize or self.timefontsize or (self._fontsizebig + self._fontsize)) self._padding = options.padding or self.padding self._geometry = options.geometry or self.geometry if self._geometry and self._geometry != 'fullscreen': try: m = re.match(r'(?:(\d+)x(\d+))?\+(\d+)\+(\d+)', self._geometry) w, h, x, y = m.groups() if w is None: w = h = 0 else: w, h = int(w), int(h) x, y = int(x), int(y) self._geometry = (w, h, x, y) except Exception: self.log.warning('invalid geometry %s', self._geometry) self._geometry = None # timeout for select() call self._selecttimeout = 0.2 # maps keys to field-dicts defined in self.layout (see above) self._keymap = {} # blocks to hide conditionally self._onlyblocks = [] # fields to hide conditionally (only if not in a block) self._onlyfields = [] # remembers loaded setups self._setups = set() # master active? self._masteractive = False # currently shown warnings self._currwarnings = '' # start a thread checking for modification of the setup file createThread('refresh checker', self._checker) self.initGui() # now start the worker thread self._worker.start() self.log.info('starting main loop') try: self.mainLoop() except KeyboardInterrupt: pass self._stoprequest = True def _checker(self): setupname = session.explicit_setups[0] fn = session._setup_info[setupname]['filenames'] watchFileContent(fn, self.log) self.log.info('setup file changed; restarting monitor process') reexecProcess() def wait(self): self.log.info('monitor quitting') self._worker.join() self.log.info('done') def quit(self, *ignored, **kwds): self.closeGui() self._stoprequest = True def _connect_action(self): self._keys_expired = False BaseCacheClient._connect_action(self) if self.showwatchdog: # also ask for and subscribe to all watchdog events self._socket.sendall(to_utf8('@watchdog/%s\n' % OP_WILDCARD)) self._socket.sendall(to_utf8('@watchdog/%s\n' % OP_SUBSCRIBE)) # use appname to distinguish between different instances self.storeSysInfo(session.appname) # called between connection attempts def _wait_retry(self): if not self._keys_expired: time = currenttime() for key in self._keymap: for obj in self._keymap[key]: self.signalKeyChange(obj, key, None, time, True) self._keys_expired = True self.updateTitle('Disconnected (%s)' % strftime('%d.%m.%Y %H:%M:%S')) sleep(1) # called while waiting for data def _wait_data(self): # update current time self.updateTitle('%s (%s)%s' % (self.title, strftime('%d.%m.%Y %H:%M:%S'), '' if self._masteractive or (not self.expectmaster) else ', no master active')) def register(self, widget, key): """API used by NicosListener widgets to register keys for callback.""" key = self._prefix + key.lower() self._keymap.setdefault(key, []).append(widget) return key # called to handle an incoming protocol message def _handle_msg(self, time, ttlop, ttl, tsop, key, op, value): if op not in (OP_TELL, OP_TELLOLD): return try: time = float(time) except (ValueError, TypeError): time = currenttime() try: value = cache_load(value) except ValueError: value = None if key == 'watchdog/warnings' and self.showwatchdog: self._process_warnings(value) return # self.log.debug('processing %s', [time, ttl, key, op, value]) if key == self._prefix + 'session/master': self._masteractive = value and op != OP_TELLOLD if key == self._prefix + 'session/mastersetup': self._setups = set(value) # reconfigure displayed blocks self.reconfigureBoxes() self.log.info('reconfigured display for setups %s', ', '.join(self._setups)) expired = value is None or op == OP_TELLOLD # now check if we need to update something objs = self._keymap.get(key, []) for obj in objs: self.signalKeyChange(obj, key, value, time, expired) def _process_warnings(self, warnings): # self.log.debug('new warnings: %s', warnings) self._currwarnings = warnings self.switchWarnPanel(bool(warnings)) def _resolve_block(self, block): # exchange SetupBlock objects by their definition, or raise # a ConfigurationError if it doesn't exist. if not isinstance(block, SetupBlock): return block setup, bname = block._setupname, block._blockname setupinfo = session.getSetupInfo() if setup not in setupinfo: raise ConfigurationError( self, 'Setup "%s" required by ' 'SetupBlock() does not exist' % setup) blocks = setupinfo[setup]['monitor_blocks'] if bname not in blocks: raise ConfigurationError( self, 'Setup "%s" does not define a ' 'monitor block called "%s"' % (setup, bname)) return blocks[bname]