diff --git a/as-ps/as_ps/as_ps.py b/as-ps/as_ps/as_ps.py index 6669841a..b7291b9f 100644 --- a/as-ps/as_ps/as_ps.py +++ b/as-ps/as_ps/as_ps.py @@ -1,6 +1,5 @@ """IOC for power supplies.""" -import logging as _log import os as _os import signal as _signal import sys as _sys @@ -11,29 +10,23 @@ from PRUserial485 import EthBridgeClient as _EthBridgeClient from siriuspy import util as _util from siriuspy.envars import VACA_PREFIX as _VACA_PREFIX +from siriuspy.logging import configure_logging, get_logger from siriuspy.pwrsupply.factory import BBBFactory -from .main import __version__, App +from .main import App STOP_EVENT = False # _multiprocessing.Event() -PCAS_DRIVER = None - -_PREFIX = _VACA_PREFIX + ('-' if _VACA_PREFIX else '') -_COMMIT_HASH = __version__ def _stop_now(signum, frame): - global STOP_EVENT _ = frame - sname = _signal.Signals(signum).name - tstamp = _util.get_timestamp() - strf = f'{sname} received at {tstamp}' - _log.warning(strf) + get_logger(_stop_now).warning( + _signal.Signals(signum).name + ' received at ' + _util.get_timestamp() + ) _sys.stdout.flush() _sys.stderr.flush() + global STOP_EVENT STOP_EVENT = True - if PCAS_DRIVER is not None: - PCAS_DRIVER.app.scan = False def _attribute_access_security_group(server, dbase): @@ -46,9 +39,10 @@ def _attribute_access_security_group(server, dbase): class _PCASDriver(_pcaspy.Driver): - def __init__(self, bbblist, dbset): + def __init__(self, bbb, dbset): super().__init__() - self.app = App(self, bbblist, dbset, _PREFIX) + self.app = App(self, bbb, dbset) + self.app.add_callback(self.update_pv) def read(self, reason): value = self.app.read(reason) @@ -60,8 +54,14 @@ def read(self, reason): def write(self, reason, value): return self.app.write(reason, value) + def update_pv(self, pvname, value, **kwargs): + """Update PV.""" + _ = kwargs + self.setParam(pvname, value) + self.updatePV(pvname) -def run(bbbnames): + +def run(bbbname): """Run function. This is the main function of the IOC: @@ -71,43 +71,49 @@ def run(bbbnames): 4. Creates a Driver to handle requests 5. Starts a thread (thread_server) that listens to client connections """ - global PCAS_DRIVER - - # NOTE: change IOC to accept only one BBB !!! + logger = get_logger(run) # Define abort function _signal.signal(_signal.SIGINT, _stop_now) _signal.signal(_signal.SIGTERM, _stop_now) - _util.configure_log_file() + configure_logging() + logger.info('--- PS IOC structures initialization ---\n') - print('') - print('--- PS IOC structures initialization ---\n') - - # Create BBBs - bbblist = list() + # Create BBB dbset = dict() - for bbbname in bbbnames: - bbbname = bbbname.replace('--', ':') - bbb, dbase = BBBFactory.create(_EthBridgeClient, bbbname=bbbname) - bbblist.append(bbb) - dbset.update(dbase) - dbset = {_PREFIX: dbset} + bbb, dbase = BBBFactory.create(_EthBridgeClient, bbbname=bbbname) + dbset.update(dbase) + + version = _util.get_last_commit_hash() + ioc_prefix = _VACA_PREFIX + ('-' if _VACA_PREFIX else '') # check if another instance of this IOC is already running - pvname = _PREFIX + next(iter(dbset[_PREFIX])) + pvname = ioc_prefix + next(iter(dbset)) if _util.check_pv_online(pvname, use_prefix=False): raise ValueError('Another instance of this IOC is already running !') + # print info about the IOC + _util.print_ioc_banner( + ioc_name='PS IOC', + db=dbset, + description='Power Supply IOC (FAC)', + version=version, + prefix=ioc_prefix, + logger=logger + ) + # Create a new simple pcaspy server and driver to respond client's requests + logger.info("Creating Server...") server = _pcaspy.SimpleServer() - for prefix, dbase in dbset.items(): - # Set security access - _attribute_access_security_group(server, dbase) - server.createPV(prefix, dbase) + # Set security access + _attribute_access_security_group(server, dbset) + logger.info("Setting Server Database.") + server.createPV(ioc_prefix, dbset) # Create driver to handle requests - PCAS_DRIVER = _PCASDriver(bbblist, dbset) + logger.info("Creating Driver.") + driver = _PCASDriver(bbb, dbset) # Create a new thread responsible for listening for client connections thread_server = _pcaspy_tools.ServerThread(server) @@ -115,16 +121,20 @@ def run(bbbnames): # Start threads and processing thread_server.start() - # Main loop - run app.proccess + # Main loop - run app.process while not STOP_EVENT: try: - PCAS_DRIVER.app.process() + driver.app.process() except Exception: - _log.warning('[!!] - exception while processing main loop') + logger.exception('[!!] - exception while processing main loop') _traceback.print_exc() break + driver.app.scan = False + logger.info("Stoping Server Thread...") + # Signal received, exit - print('exiting...') thread_server.stop() thread_server.join() + logger.info("Server Thread stopped.") + logger.info("Good Bye.") diff --git a/as-ps/as_ps/main.py b/as-ps/as_ps/main.py index 2544a1aa..a2e8a24a 100644 --- a/as-ps/as_ps/main.py +++ b/as-ps/as_ps/main.py @@ -1,30 +1,33 @@ """Main application.""" -import logging as _log import re as _re import time as _time import numpy as _np from pcaspy import Alarm as _Alarm, Severity as _Severity +from siriuspy.callbacks import Callback as _Callback +from siriuspy.logging import get_logger as _get_logger, \ + LogMonHandler as _LogMonHandler from siriuspy.namesys import SiriusPVName as _SiriusPVName from siriuspy.thread import LoopQueueThread as _LoopQueueThread -from siriuspy.util import get_last_commit_hash as _get_last_commit_hash, \ - print_ioc_banner as _print_ioc_banner +from siriuspy.util import get_last_commit_hash as _get_last_commit_hash __version__ = _get_last_commit_hash() -class App: +class App(_Callback): """Power Supply IOC Application.""" _sleep_scan = 0.050 # [s] _regexp_setpoint = _re.compile('^.*-(SP|Sel)$') - def __init__(self, driver, bbblist, dbset, prefix): + def __init__(self, driver, bbb, dbset): """Init application.""" # --- init begin self._driver = driver + self._logger = _get_logger(self) + _get_logger().addHandler(_LogMonHandler(self.update_log)) # flag to indicate idff processing is taking place self._idff_processing = False @@ -37,33 +40,21 @@ def __init__(self, driver, bbblist, dbset, prefix): self._counter_wfmoffsetkick_sp = 0 # mapping device to bbb - self._bbblist = bbblist - # NOTE: change IOC to accept only one BBB !!! + self._bbb = bbb - idffmode_pvname = self.bbblist[0].psnames[0] + ':IDFFMode-Sts' - if idffmode_pvname in dbset[prefix]: + idffmode_pvname = self.bbb.psnames[0] + ':IDFFMode-Sts' + if idffmode_pvname in dbset: self._has_idffmode = True else: self._has_idffmode = False # build dictionaries - self._dev2bbb, self._dev2conn, self._interval = \ + self._dev2bbb, self._dev2conn, self._interval = ( self._create_bbb_dev_dict() + ) # initializes beaglebones - for bbb in bbblist: - bbb.init() - - # -- init end - print('---\n') - - # print info about the IOC - _print_ioc_banner( - ioc_name='PS IOC', - db=dbset[prefix], - description='Power Supply IOC (FAC)', - version=__version__, - prefix=prefix) + self._bbb.init() @property def driver(self): @@ -71,9 +62,9 @@ def driver(self): return self._driver @property - def bbblist(self): - """Return list of beaglebone objects.""" - return self._bbblist + def bbb(self): + """Return beaglebone object.""" + return self._bbb def process(self): """Process all write requests in queue and does a BBB scan.""" @@ -82,22 +73,21 @@ def process(self): qsize = self._queue.qsize() if qsize > 2: logmsg = f'[Q] - write queue size is large: {qsize}' - _log.warning(logmsg) + self._logger.warning(logmsg, extra={'to_logmon': True}) # then scan bbb state for updates. - for bbb in self.bbblist: - self.scan_bbb(bbb) + self.scan_bbb(self.bbb) # sleep, if necessary dt_ = self._interval - (_time.time() - t0_) _time.sleep(max(dt_, 0)) # NOTE: measure this interval for various BBBs... - # _log.info("process.... {:.3f} ms".format(1000*(t1_-t0_))) + # self._logger.info("process.... {:.3f} ms".format(1000*(t1_-t0_))) def read(self, reason): """Read from database.""" - # _log.info("[{:.2s}] - {:.32s} = {:.50s}".format( + # self._logger.info("[{:.2s}] - {:.32s} = {:.50s}".format( # 'R ', reason, str(self.driver.getParam(reason)))) def write(self, reason, value): @@ -112,14 +102,17 @@ def write(self, reason, value): else: idff_state = False - strf = "[{:.2s}] - {:.32s} = {:.50s}{}" + strf = '[{:.2s}] - {:.32s} = {:.50s}{}' # In IDFFMode only accept specific writes if idff_state and pvname.propty not in ( - 'IDFFMode-Sel', - 'OpMode-Sel', 'PwrState-Sel'): + 'IDFFMode-Sel', + 'OpMode-Sel', + 'PwrState-Sel', + ): ignorestr, wstr = (' (IDFFMode On)', 'W!') - _log.info(strf.format(wstr, reason, str(value), ignorestr)) + logmsg = strf.format(wstr, reason, str(value), ignorestr) + self._logger.info(logmsg, extra={'to_logmon': True}) return if idff_state and 'IDFF' not in reason: @@ -131,23 +124,29 @@ def write(self, reason, value): self._counter_wfmoffsetkick_sp += 1 if self._counter_wfmoffsetkick_sp == 100: ignorestr = ' (100 events)' - _log.info(strf.format(wstr, reason, str(value), ignorestr)) + logmsg = strf.format(wstr, reason, str(value), ignorestr) + self._logger.info(logmsg, extra={'to_logmon': True}) self._counter_wfmoffsetkick_sp = 0 else: # print all other write events - _log.info(strf.format(wstr, reason, str(value), ignorestr)) + logmsg = strf.format(wstr, reason, str(value), ignorestr) + self._logger.info(logmsg, extra={'to_logmon': True}) # NOTE: This modified behaviour is to allow loading # global_config to complete without artificial warning # messages or unnecessary delays. Whether we should extend # it to all power supplies remains to be checked. if self._check_write_immediately(reason, value): - self.driver.setParam(reason, value) - self.driver.updatePV(reason) + self.run_callbacks(reason, value) bbb = self._dev2bbb[pvname.device_name] self._queue.put( - (self._write_operation, (bbb, pvname, value)), block=False) + (self._write_operation, (bbb, pvname, value)), block=False + ) + + def update_log(self, msg): + """Method used by the IOC to update logs.""" + self.run_callbacks('Log-Mon', msg) def scan_bbb(self, bbb): """Scan BBB devices and update ioc epics DB.""" @@ -157,11 +156,10 @@ def scan_bbb(self, bbb): def scan_device(self, bbb, devname, force_update=False): """Scan BBB device and update ioc epics DB.""" - dev_connected = \ - bbb.check_connected(devname) and \ - bbb.check_connected_strength(devname) - self._update_ioc_database(bbb, devname, dev_connected, - force_update) + dev_connected = bbb.check_connected( + devname + ) and bbb.check_connected_strength(devname) + self._update_ioc_database(bbb, devname, dev_connected, force_update) # --- private methods --- @@ -169,14 +167,11 @@ def _create_bbb_dev_dict(self): # build _bbb_devices dict dev2bbb = dict() dev2conn = dict() - interval = float('Inf') - for bbb in self.bbblist: - # get minimum time interval for BBB - interval = min(interval, bbb.update_interval()) - # create bbb_device dict - for dev_name in bbb.psnames: - dev2conn[dev_name] = None - dev2bbb[dev_name] = bbb + interval = self.bbb.update_interval() + # create bbb_device dict + for dev_name in self.bbb.psnames: + dev2conn[dev_name] = None + dev2bbb[dev_name] = self.bbb return dev2bbb, dev2conn, interval def _write_operation(self, bbb, pvname, value): @@ -184,10 +179,11 @@ def _write_operation(self, bbb, pvname, value): priority_pvs = bbb.write(pvname.device_name, pvname.propty, value) for reason, val in priority_pvs.items(): if val is not None: - self.driver.setParam(reason, val) + self.run_callbacks(reason, val) else: self.driver.setParamStatus( - reason, _Alarm.TIMEOUT_ALARM, _Severity.INVALID_ALARM) + reason, _Alarm.TIMEOUT_ALARM, _Severity.INVALID_ALARM + ) self.driver.updatePV(reason) # print('{:<30s} : {:>9.3f} ms'.format( @@ -202,8 +198,8 @@ def _check_write_immediately(self, reason, value): return True def _update_ioc_database( - self, bbb, devname, dev_connected=True, force_update=False): - + self, bbb, devname, dev_connected=True, force_update=False + ): # connection state changed? if dev_connected == self._dev2conn[devname]: conn_changed = False @@ -223,16 +219,22 @@ def _update_ioc_database( strength_names = tuple() for reason, new_value in data.items(): - # set strength limits for strename in strength_names: if strename is not None and strename in reason: lims = bbb.strength_limits(devname) if None not in lims: kwargs = self.driver.getParamInfo(reason) - kwargs.update({ - 'hihi': lims[1], 'high': lims[1], 'hilim': lims[1], - 'lolim': lims[0], 'low': lims[0], 'lolo': lims[0]}) + kwargs.update( + { + 'hihi': lims[1], + 'high': lims[1], + 'hilim': lims[1], + 'lolim': lims[0], + 'low': lims[0], + 'lolo': lims[0], + } + ) self.driver.setParamInfo(reason, kwargs) self.driver.updatePV(reason) @@ -256,10 +258,12 @@ def _update_ioc_database( if conn_changed: if dev_connected: self.driver.setParamStatus( - reason, _Alarm.NO_ALARM, _Severity.NO_ALARM) + reason, _Alarm.NO_ALARM, _Severity.NO_ALARM + ) else: self.driver.setParamStatus( - reason, _Alarm.TIMEOUT_ALARM, _Severity.INVALID_ALARM) + reason, _Alarm.TIMEOUT_ALARM, _Severity.INVALID_ALARM + ) # if reason state was set, update its PV db entry if value_changed or conn_changed: @@ -273,16 +277,18 @@ def _check_value_changed(self, reason, new_value): return False old_value = self.driver.getParam(reason) try: - if isinstance(old_value, (tuple, list, _np.ndarray)) or \ - isinstance(new_value, (tuple, list, _np.ndarray)): + if isinstance(old_value, (tuple, list, _np.ndarray)) or isinstance( + new_value, (tuple, list, _np.ndarray) + ): # transform to numpy arrays if not isinstance(old_value, _np.ndarray): old_value = _np.array(old_value) if not isinstance(new_value, _np.ndarray): new_value = _np.array(new_value) # compare - if len(old_value) != len(new_value) or \ - not _np.all(old_value == new_value): + if len(old_value) != len(new_value) or not _np.all( + old_value == new_value + ): # NOTE: for a 4000-element numpy array comparison in # a standard intel CPU takes: # 1) np.all(a == b) -> ~4 us @@ -298,11 +304,24 @@ def _check_value_changed(self, reason, new_value): # simple type comparison return new_value != old_value except Exception as exception: - print() - print('--- debug ---') - print('exception : {}'.format(type(exception))) - print('reason : {}'.format(reason)) - print('old_value : {}'.format(str(old_value)[:1000])) - print('new_value : {}'.format(str(new_value)[:1000])) - print(' !!!') + self._logger.exception('--- debug ---', extra={'to_logmon': True}) + self._logger.exception( + 'exception : %s', + str(type(exception)), + extra={'to_logmon': True}, + ) + self._logger.exception( + 'reason : %s', reason, extra={'to_logmon': True} + ) + self._logger.exception( + 'old_value : %s', + str(old_value)[:1000], + extra={'to_logmon': True}, + ) + self._logger.exception( + 'new_value : %s', + str(new_value)[:1000], + extra={'to_logmon': True}, + ) + self._logger.exception(' !!!', extra={'to_logmon': True}) return True diff --git a/as-ps/scripts/sirius-ioc-as-ps.py b/as-ps/scripts/sirius-ioc-as-ps.py index 0d2935fd..04928998 100755 --- a/as-ps/scripts/sirius-ioc-as-ps.py +++ b/as-ps/scripts/sirius-ioc-as-ps.py @@ -3,6 +3,7 @@ import os import sys + from as_ps import as_ps as ioc_module # NOTE: maximum epics array size @@ -39,7 +40,8 @@ def main(): args.remove('--help') print_help() if args: - ioc_module.run(args) + bbbname = args[0] + ioc_module.run(bbbname) if __name__ == "__main__":