274 lines
7.6 KiB
Python
274 lines
7.6 KiB
Python
"""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()
|