""" Daemon TCP server. The server will run indefinitely
listening on the specified TCP (see the
:class:`~lab_utils.socket_comm.Server` documentation).
When a client connects and sends a message string, the
message parser will call the appropriate method. The
following commands are supported by the parser (options
must be used with a double dash \\- \\-):
+-----------+-----------------------+---------------------------------------------------------------------------+
| quit | | Stops the daemon and cleans up database and serial port |
+-----------+-----------------------+---------------------------------------------------------------------------+
| status | | TODO: Not implemented yet |
+-----------+-----------------------+---------------------------------------------------------------------------+
| tpg_256a | on/off/restart | Connects / disconnects / restarts the TPG 256A device |
+ +-----------------------+---------------------------------------------------------------------------+
| | test | Performs a serial port test and returns the device firmware |
+ +-----------------------+---------------------------------------------------------------------------+
| | config {file} | Reloads the default (or given) config file (logging is stopped) |
+ +-----------------------+---------------------------------------------------------------------------+
| | gauge-info | Returns gauge type, status and latest value |
+ +-----------------------+---------------------------------------------------------------------------+
| | single-readout | Performs a single read-out to the device (logging is stopped) |
+-----------+-----------------------+---------------------------------------------------------------------------+
| logging | start / stop | Launches or stops the separate device monitoring thread |
+ +-----------------------+---------------------------------------------------------------------------+
| | terminal | Prints output to the terminal with *info* level |
+ +-----------------------+---------------------------------------------------------------------------+
| | database | Enables data saving to a PostgreSQL database |
+-----------+-----------------------+---------------------------------------------------------------------------+
"""
# Imports
from serial import SerialException
import argparse
import configparser
from psycopg2 import DatabaseError
# Third party
from lab_utils.socket_comm import Server
# Local
from .TPG_256A import TPG_256A, StateError
from .Monitor import Monitor
from .__project__ import (
__documentation__ as docs_url,
__description__ as prog_desc,
__module_name__ as mod_name,
)
[docs]class Daemon(Server):
""" Base class of the daemon, derived from
:class:`~lab_utils.socket_comm.Server`. The daemon
holds pointers to the :attr:`device` driver and the
:attr:`monitor` thread, and communicates with them
upon message reception. """
# Attributes
device: TPG_256A = None #: Device handler.
monitor: Monitor = None #: Monitor thread.
[docs] def __init__(self,
config_file: str = None,
pid_file_name: str = None):
""" Initializes the :class:`Daemon` object.
The :attr:`device` constructor is called:
serial connection is established and hardware
information is retrieved from the controller.
Parameters
----------
config_file : str, optional
See parent class :class:`~lab_utils.socket_comm.Server`.
pid_file_name : str, optional
See parent class :class:`~lab_utils.socket_comm.Server`.
Raises
------
:class:`configparser.Error`
Configuration file error
:class:`LockError`
The PID file could not be locked (see parent
class :class:`~lab_utils.socket_comm.Server`).
:class:`OSError`
Socket errors (see parent class
:class:`~lab_utils.socket_comm.Server`).
:class:`~serial.SerialException`
The connection to the :attr:`device` has failed
:class:`IOError`
Communication error, probably message misspelt.
:class:`StateError`
:attr:`device` was in the wrong state, e.g. already ON.
"""
# Call the parent class initializer
super().__init__(config_file, pid_file_name)
# Add custom arguments to the message parser
self.update_parser()
# Initialize device device
self.device = TPG_256A()
[docs] def update_parser(self):
""" Sets up the message
:attr:`~lab_utils.socket_comm.Server.parser`. """
self.logger.debug('Setting up custom message parser')
# Set some properties of the base class argument parser
self.parser.prog = mod_name
self.parser.description = prog_desc
self.parser.epilog = 'Check out the package documentation for more information:\n{}'.format(docs_url)
# Subparsers for each acceptable command
# 1. STATUS
sp_status = self.sp.add_parser(
name='status',
description='checks the status of the daemon',
)
sp_status.set_defaults(
func=self.status,
which='status')
# 2. TPG-256A
sp_tpg_256a = self.sp.add_parser(
name='tpg_256a',
description='interface to the Pfeiffer TPG 256A device',
)
sp_tpg_256a.set_defaults(
func=self.tpg_256a,
which='tpg_256a'
)
sp_g1 = sp_tpg_256a.add_mutually_exclusive_group()
sp_g1.add_argument(
'--on',
action='store_true',
help='connects to the device',
default=False,
dest='turn_on',
)
sp_g1.add_argument(
'--off',
action='store_true',
help='closes the connection',
default=False,
dest='turn_off',
)
sp_g1.add_argument(
'--restart, -r',
action='store_true',
help='restarts the connection',
default=False,
dest='restart',
)
sp_tpg_256a.add_argument(
'--test, -t',
action='store_true',
help='performs a connection check',
default=False,
dest='test',
)
sp_tpg_256a.add_argument(
'--config,-c',
default=argparse.SUPPRESS, # If --config is not given, it will not show up in the namespace
nargs='?', # If --config is given, it may be used with or without an extra argument
const=None, # If --config is given without an extra argument, 'dest' = None
help='reloads the configuration file (and resets the file if given, absolute path only)',
dest='config_file',
)
sp_tpg_256a.add_argument(
'--gauge-info,-g',
action='store_true',
help='prints gauge status and information',
default=False,
dest='gauge_info',
)
sp_tpg_256a.add_argument(
'--single-readout,-r',
action='store_true',
help='performs a single readout to the device',
default=False,
dest='single_readout',
)
# 3. Monitor
sp_monitor = self.sp.add_parser(
name='logging',
description='manages the logging thread',
)
sp_monitor.set_defaults(
func=self.logging,
which='logging'
)
sp_g2 = sp_monitor.add_mutually_exclusive_group()
sp_g2.add_argument(
'--start',
action='store_true',
help='starts the monitor thread',
default=False,
dest='start',
)
sp_g2.add_argument(
'--stop',
action='store_true',
help='stops the monitor thread',
default=False,
dest='stop',
)
sp_monitor.add_argument(
'--terminal,-t',
action='store_true',
help='prints the monitor output to the application logging sink',
default=False,
dest='terminal',
)
sp_monitor.add_argument(
'--database,-d',
action='store_true',
help='logs data to a PostgreSQL database',
default=False,
dest='database',
)
[docs] def quit(self):
""" Stops the daemon, called with message 'quit'.
The method overrides the original
:meth:`~lab_utils.socket_comm.Server.quit` to do
proper clean-up of the monitoring
:attr:`thread<monitor>` and the :attr:`device`
handler.
"""
self.logger.info('Launching quitting sequence')
# Monitor
if self.monitor is not None and self.monitor.is_alive():
if self.monitor.stop():
self.reply += 'Monitor thread stopped\n'
else:
self.reply += 'Thread error! Monitor thread did not respond to the quit signal and is still running\n'
# Serial connection
if self.device.connected:
try:
self.device.disconnect()
except (SerialException, StateError, IOError) as e:
self.reply += 'Clean-up error! {}: {}'.format(type(e).__name__, e)
self.logger.debug('Serial connection could not be closed')
else:
self.reply += 'Clean-up: device is now off\n'
self.logger.info('Serial connection closed')
self.logger.info('Stopping daemon TCP server now')
self.reply += 'Stopping daemon TCP server now'
[docs] def status(self):
""" TODO
"""
self.reply += 'Status: doing great!'
[docs] def tpg_256a(self):
""" Modifies or checks the status of the TPG 256A
:attr:`device`. Provides functionality to:
- Connect and disconnect the controller.
- Retrieve hardware information.
- Reload device configuration.
- Perform a single read-out of the gauges
"""
self.logger.debug('Method \'tpg_256a\' called by the message parser')
# Turn ON
if self.namespace.turn_on:
# Check current status
if self.device.connected:
self.logger.info('Device already connected')
self.reply += 'Device was already connected\n'
else:
try:
self.device.connect()
except (SerialException, StateError, IOError) as e:
self.reply += 'Error! {}: {}'.format(type(e).__name__, e)
return
else:
self.reply += 'Connection successful\n'
# Turn OFF
if self.namespace.turn_off:
# TODO: stop logging as well
# Check current status
if not self.device.connected:
self.logger.info('Device already off')
self.reply += 'Device was already off!\n'
else:
try:
self.device.disconnect()
except (SerialException, StateError, IOError) as e:
self.reply += 'Error! {}: {}'.format(type(e).__name__, e)
return
else:
self.reply += 'Device is now off\n'
# Restart device (if ON, turn OFF then ON; if OFF, turn ON)
if self.namespace.restart:
# Turn OFF if connected
# TODO: stop logging as well
if self.device.connected:
try:
self.device.disconnect()
except (SerialException, StateError, IOError) as e:
self.reply += 'Error! {}: {}'.format(type(e).__name__, e)
return
else:
self.reply += 'Device was shut down\n'
# Turn ON
try:
self.device.connect()
except SerialException as e:
self.reply += 'Error! {}: {}'.format(type(e).__name__, e)
return
else:
self.reply += 'Device was restarted\n'
# Test serial connection and device access
if self.namespace.test:
# Check current status
# TODO: check device is not running
if not self.device.connected:
self.reply += 'Device is not connected, cannot perform test'
else:
try:
# Read some device information
firmware = self.device.program_number()
except (SerialException, StateError, IOError) as e:
self.reply += 'Error! {}: {}'.format(type(e).__name__, e)
return
else:
self.reply += 'Connection test successful, device firmware: {}'.format(firmware)
self.logger.debug('Device firmware: {}'.format(firmware))
# Reset and load configuration file
if "config_file" in self.namespace:
self.logger.info('Reloading device configuration')
device_was_on = self.device.connected
try:
# Stop the device if it is running
# TODO: stop logging as well
if self.device.connected:
self.reply += 'Turning off the device\n'
self.device.disconnect()
# Apply configuration
self.device.config(self.namespace.config_file)
self.reply += 'Configuration file {} loaded\n'.format(self.device.config_file)
# Turn on again?
if device_was_on:
self.device.connect()
self.reply += 'Device was reconnected\n'
except (SerialException, StateError, configparser.Error, IOError) as e:
self.reply += 'Error! {}: {}'.format(type(e).__name__, e)
return
# Single readout
if self.namespace.single_readout:
# Check current status
# TODO: check device is not running
if not self.device.connected:
self.reply += 'Device is not connected, cannot perform pressure readout'
else:
try:
self.device.pressure_gauges()
except (SerialException, StateError, IOError) as e:
self.reply += 'Error! {}: {}'.format(type(e).__name__, e)
return
else:
pass
# Gauge status and information
if self.namespace.gauge_info:
# Check device is ON
if not self.device.connected:
self.reply += 'Gauge information: Device is not connected, information might be outdated'
else:
# Build nice table
header = '{:15}{:25}{}\n'.format('Gauge', 'Status', 'Latest Value')
self.reply += ''.join('-' for _ in range(len(header)))
self.reply += '\n'
self.reply += header
self.reply += ''.join('-' for _ in range(len(header)))
self.reply += '\n'
for ch in self.device.channel_info:
if ch.label is None:
continue
status = 'Active, logging'
if not ch.active:
status = 'Inactive'
if ch.active and not ch.logging:
status = 'Active, not logging'
data = ch.data
if data is None:
data = 'None'
self.reply += '{:15}{:25}{:15}\n'.format(ch.label, status, str(data))
self.reply += ''.join('-' for _ in range(len(header)))
self.reply += '\n'
[docs] def logging(self):
""" Manages the :attr:`logging thread<monitor>`.
Provides functionality to:
- Start and stop the thread.
- Enable or disable database usage.
- Enable or disable terminal output.
"""
self.logger.debug('Method \'logging\' called by the message parser')
# Start
if self.namespace.start:
# Check current status
if not self.device.connected:
self.logger.warning('Device is not connected')
self.reply += 'Device is not connected\n'
elif self.monitor is not None and self.monitor.is_alive():
self.logger.warning('Monitor thread is already running')
self.reply += 'Monitor thread is already running\n'
else:
self.logger.info('Launching logging thread')
try:
self.monitor = Monitor(
device=self.device,
name='Daemon Thread',
terminal_flag=self.namespace.terminal,
database_flag=self.namespace.database,
)
except (StateError, RuntimeError, DatabaseError) as e:
self.reply += 'Error launching Daemon Thread! {}: {}'.format(type(e).__name__, e)
return
else:
self.reply += 'Daemon Thread launched\nYou can check its status with the \'status\' option\n'
# Stop
if self.namespace.stop:
# Check current status
if self.monitor is None or not self.monitor.is_alive():
self.logger.info('Monitor thread is not running')
self.reply += 'Monitor thread is not running\n'
else:
if self.monitor.stop():
self.reply += 'Daemon thread stopped\n'
else:
self.reply += 'Daemon thread error, still running...\n'