"""Data server API. it define the data/message protocol between the controller server and the data server. **Data Server** A server handle the data from device and saving. """ import abc from pathlib import Path from typing import Union, Tuple, List, Optional, AnyStr from biopro.devlib.data import DataDecodeFormat from biopro.devlib.device import Device from biopro.devlib.library import DeviceLibraryRepository from biopro.recording import RecordingMetaFile from biopro.server.process import ControllerProcess from biopro.util.cli_base import CliOptions, cli_options, cli_flags from biopro.util.json import JSON_OBJECT from biopro.util.socket import SocketClient, SocketClientMacro from json import loads as json_parse, dumps as _json_stringify def json_stringify(o) -> str: return _json_stringify(o, separators=(',', ':')) class DataAPI(metaclass=abc.ABCMeta): """Application Programming Interface for Data server. """ ADDR = '/tmp/BPS-data.socket' '''data server socket file path''' MODE_DISABLE = 'disable' '''data server hardware interface mode. disable''' MODE_SINGLE = 'single' '''data server hardware interface mode. single device support''' MODE_SELECTOR = 'selector' '''data server hardware interface mode. multiple device support''' MODE = (MODE_DISABLE, MODE_SINGLE, MODE_SELECTOR) '''data server hardware interface mode.''' __slots__ = () @abc.abstractmethod def reset(self, spi_mode: Optional[str] = None): """reset data server :param spi_mode: reset spi mode """ pass @abc.abstractmethod def shutdown(self): """shutdown data server""" pass @abc.abstractmethod def spi_mode(self) -> str: """data server hardware interface mode""" pass @abc.abstractmethod def available_channel(self, reset=False) -> List[int]: """get available channel. It will exam the hardware if *reset* is True. :param reset: re-check channel :return: available channel. """ pass @abc.abstractmethod def unset_device(self, device: int): """clean device configuration. :param device: device ID """ pass @abc.abstractmethod def update_device_configuration(self, device: Union[int, Device], meta_file: Union[str, Path, RecordingMetaFile], data_format: Union[AnyStr, DataDecodeFormat]): """set device configuration. :param device: device ID :param meta_file: meta file path :param data_format: data decode format """ pass @abc.abstractmethod def is_sync(self, device: Union[None, int, Device] = None) -> bool: """is start sync :param device: device :return: None for any """ pass @abc.abstractmethod def start_sync(self, *device: Union[int, Device]): """start sync :param device: device, empty for all """ pass @abc.abstractmethod def stop_sync(self, *device: Union[int, Device]): """stop sync :param device: device IDs, empty for all """ pass @abc.abstractmethod def sync_file(self): """sync file, write-back dirty file. """ pass @abc.abstractmethod def hardware_test(self) -> JSON_OBJECT: """""" pass # noinspection PyAbstractClass class DataClient(SocketClient, DataAPI, metaclass=SocketClientMacro(DataAPI)): """Data server socket client. """ __slots__ = () def __init__(self): super().__init__(self.ADDR) def update_device_configuration(self, device: Union[int, Device], meta_file: Union[str, Path, RecordingMetaFile], data_format: Union[AnyStr, DataDecodeFormat]): # meta file if isinstance(meta_file, str): meta_file = Path(meta_file) if isinstance(meta_file, Path): # load meta file to make sure file is existed and loadable. meta_file = RecordingMetaFile.load(meta_file) if not isinstance(meta_file, RecordingMetaFile): raise TypeError('not a recording meta file : ' + meta_file.__class__.__name__) # meta_file.device = device meta_file = str(meta_file.filepath) # device if isinstance(device, Device): # device = device.device_id device = json_stringify(device.as_json()) # data format if isinstance(data_format, (str, bytes)): pass elif isinstance(data_format, DataDecodeFormat): data_format = data_format.name else: raise TypeError('not a data decode format : ' + data_format.__class__.__name__) # send self.send_command('update_device_configuration', device, meta_file, data_format) def is_sync(self, device: Union[None, int, Device] = None) -> bool: if isinstance(device, Device): device = device.device_id return self.send_command('is_sync', device) def start_sync(self, *device: Union[int, Device]): self.send_command('start_sync', *self._to_device_id(*device)) def stop_sync(self, *device: Union[int, Device]): self.send_command('stop_sync', *self._to_device_id(*device)) @staticmethod def _to_device_id(*device: Union[int, Device]) -> Tuple[int, ...]: return tuple(map(lambda d: d.device_id if isinstance(d, Device) else d, device)) # noinspection PyUnusedLocal class DataServerOptions(CliOptions): """cli options for data server. """ def __init__(self): # spi mode self.flag_spi_mode: str = 'selector' '''data server hardware interface mode''' self.repository = DeviceLibraryRepository(no_system_default=True) '''device library repository''' self.flag_disable_data_statistics = False '''flag of statistics the device data missing''' @cli_options('--spi-mode') def _spi_mode(self, opt: str, value: str): """spi mode. could be: disable : single : use ExtMemSpiInterface selector : use MultiExtMemSpiInterface (default) """ if value not in DataAPI.MODE: raise ValueError(value) self.flag_spi_mode = value @cli_options('-L', '--library', value='PATH') def _library_path(self, opt: str, value: str): self.repository.add_library_path(value) @cli_flags('--disable-data-statistics') def _disable_data_statistics(self, opt: str): """disable data lost statistics""" self.flag_disable_data_statistics = True class DataServerProcess(ControllerProcess[DataServerOptions, DataClient]): """data server process """ def __init__(self, options: DataServerOptions, flag_disable=False): super().__init__('DataServer', options, flag_disable=flag_disable) @property def socket_file(self) -> Path: return Path(DataClient.ADDR) def get_process_arguments(self) -> List[str]: opt = self.options cmd = ['data', '--spi-mode', opt.flag_spi_mode] if opt.flag_disable_data_statistics: cmd.append('--disable-data-statistics') for path in opt.repository.library_path: cmd.append('-L') cmd.append(str(path)) return cmd def new_client(self) -> DataClient: return DataClient() def shutdown(self): with DataClient() as client: client.shutdown()