525 lines
15 KiB
Python
525 lines
15 KiB
Python
import abc
|
|
from typing import Optional, Union, List, Dict, cast, overload
|
|
|
|
from biopro.util.channel import ChannelMask
|
|
from biopro.util.text import part_suffix
|
|
from .data import DataPresentMode, DataMessageEncoder
|
|
from .message import DataMessage
|
|
|
|
inf = float('inf')
|
|
|
|
|
|
class _DataMessageListen:
|
|
__slots__ = ('_device', '_channel_mask')
|
|
|
|
def __init__(self, device: int, channel: Union[None, int, List[int]] = None):
|
|
self._device = device
|
|
self._channel_mask = ChannelMask()
|
|
|
|
if channel is not None:
|
|
self._channel_mask.add_channel(channel)
|
|
|
|
@property
|
|
def device(self) -> int:
|
|
return self._device
|
|
|
|
def clear_listen(self):
|
|
self._channel_mask.clear()
|
|
|
|
def listen(self, channel: Union[int, List[int]]):
|
|
self._channel_mask.set_channel(channel)
|
|
|
|
def contain_channel(self, channel: int) -> bool:
|
|
return self._channel_mask.contain_channel(channel)
|
|
|
|
@property
|
|
def listen_channel_number(self) -> int:
|
|
return self._channel_mask.size
|
|
|
|
@property
|
|
def listen_channel(self) -> List[int]:
|
|
return self._channel_mask.channels()
|
|
|
|
def new_encoder(self, header: int, present: DataPresentMode, sample_rate: int) -> Optional[DataMessageEncoder]:
|
|
if self._channel_mask.size == 0:
|
|
return None
|
|
|
|
return DataMessageEncoder(header, present, sample_rate, self._device, self._channel_mask)
|
|
|
|
|
|
class DataMessageRequest:
|
|
"""
|
|
**Request Message Format**
|
|
|
|
=========== =================================================
|
|
pattern description
|
|
=========== =================================================
|
|
x reset all parameter
|
|
[M];[M] message combine
|
|
h[ID] message header ID
|
|
M multiple device mode
|
|
m single device mode
|
|
c[D]:[C,..] listen device DEVICE's channels
|
|
c clear c command
|
|
f[HANDLE] file handle
|
|
f clear f command, or use cache data
|
|
r[RATE] re-sample rate, unit 1/sec
|
|
p[COUNT] data points
|
|
d[MODE] data present mode
|
|
t[T1]:[T2] time range from T1 to T2
|
|
t[T1]: time range from T1 to latest
|
|
t[TIME] time duration TIME to latest
|
|
=========== =================================================
|
|
|
|
"""
|
|
|
|
__slots__ = ('_message_handle', '_file_handle',
|
|
'_sample_rate', '_data_points', '_data_present',
|
|
'_listen', '_response')
|
|
|
|
def __init__(self):
|
|
self._message_handle = 0
|
|
self._file_handle: Optional[int] = None
|
|
|
|
self._sample_rate: Optional[int] = None
|
|
self._data_points: Optional[int] = None
|
|
self._data_present: int = DataPresentMode.FIRST.value
|
|
|
|
# listen device,
|
|
# if None, no device listen
|
|
# if _DataMessageListen, listen one device
|
|
# if dict{device_id -> _DataMessageListen}, listen multiple devices
|
|
self._listen: Union[None, _DataMessageListen, Dict[int, _DataMessageListen]] = None
|
|
|
|
self._response: Optional[DataMessageResponse] = None
|
|
|
|
self.reset()
|
|
|
|
def reset(self):
|
|
self._message_handle = 0
|
|
self._file_handle = None
|
|
|
|
self._sample_rate = None
|
|
self._data_points = None
|
|
self._data_present = DataPresentMode.FIRST.value
|
|
|
|
self._listen = None
|
|
self._response = None
|
|
|
|
@property
|
|
def message_handle(self) -> int:
|
|
return self._message_handle
|
|
|
|
@property
|
|
def sample_rate(self) -> Optional[int]:
|
|
return self._sample_rate
|
|
|
|
@sample_rate.setter
|
|
def sample_rate(self, value: int):
|
|
self._sample_rate = value
|
|
self._data_points = None
|
|
|
|
@property
|
|
def data_points(self) -> Optional[int]:
|
|
return self._data_points
|
|
|
|
@data_points.setter
|
|
def data_points(self, value: int):
|
|
self._data_points = value
|
|
self._sample_rate = None
|
|
|
|
@property
|
|
def data_present_value(self) -> int:
|
|
return self._data_present
|
|
|
|
@property
|
|
def data_present(self) -> DataPresentMode:
|
|
return DataPresentMode(self._data_present)
|
|
|
|
@data_present.setter
|
|
def data_present(self, value: Union[int, DataPresentMode]):
|
|
if isinstance(value, int):
|
|
self._data_present = DataPresentMode(value).value
|
|
else:
|
|
self._data_present = value.value
|
|
|
|
@property
|
|
def file_handle(self) -> Optional[int]:
|
|
return self._file_handle
|
|
|
|
@file_handle.setter
|
|
def file_handle(self, value: Optional[int]):
|
|
self._file_handle = value
|
|
|
|
@property
|
|
def is_listen_multiple_device(self) -> bool:
|
|
return isinstance(self._listen, dict)
|
|
|
|
def set_listen_multiple_device(self, flag: bool):
|
|
listen = self._listen
|
|
|
|
if flag:
|
|
if listen is None:
|
|
self._listen = {}
|
|
|
|
elif isinstance(listen, _DataMessageListen):
|
|
self._listen = {listen.device: listen}
|
|
|
|
else:
|
|
self._listen = None
|
|
|
|
def clear_listen(self):
|
|
self._listen = None
|
|
|
|
def listen(self, device: int, channel: Union[int, List[int]]):
|
|
listen = self._listen
|
|
|
|
if listen is None:
|
|
self._listen = _DataMessageListen(device, channel)
|
|
|
|
elif isinstance(listen, _DataMessageListen):
|
|
if listen.device == device:
|
|
listen.listen(channel)
|
|
else:
|
|
# change another listen device
|
|
self._listen = _DataMessageListen(device, channel)
|
|
|
|
else:
|
|
all_device = cast(Dict[int, _DataMessageListen], listen)
|
|
|
|
listen = all_device.get(device)
|
|
|
|
if listen is None:
|
|
# new listen device
|
|
listen = _DataMessageListen(device, channel)
|
|
all_device[device] = listen
|
|
|
|
else:
|
|
# append listen channel
|
|
listen.listen(channel)
|
|
|
|
def contain_channel(self, device: int, channel: int) -> bool:
|
|
listen = self._listen
|
|
|
|
if listen is None:
|
|
return False
|
|
|
|
elif isinstance(listen, _DataMessageListen):
|
|
return listen.device == device and listen.contain_channel(channel)
|
|
|
|
else:
|
|
all_device: Dict[int, _DataMessageListen] = listen
|
|
|
|
listen = all_device.get(device)
|
|
|
|
if listen is None:
|
|
return False
|
|
|
|
else:
|
|
return listen.contain_channel(channel)
|
|
|
|
@property
|
|
def listen_device(self) -> List[int]:
|
|
listen = self._listen
|
|
|
|
if listen is None:
|
|
return []
|
|
|
|
elif isinstance(listen, _DataMessageListen):
|
|
return [listen.device] if listen.listen_channel_number > 0 else []
|
|
|
|
else:
|
|
return list(map(lambda ln: ln.device,
|
|
filter(lambda ln: ln.listen_channel_number > 0,
|
|
listen.values())))
|
|
|
|
def listen_channels(self, device: int) -> Optional[List[int]]:
|
|
listen = self._listen
|
|
|
|
if listen is None:
|
|
return None
|
|
|
|
elif isinstance(listen, _DataMessageListen):
|
|
if listen.device == device and listen.listen_channel_number > 0:
|
|
return listen.listen_channel
|
|
|
|
else:
|
|
try:
|
|
listen = listen[device]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
if listen.listen_channel_number > 0:
|
|
return listen.listen_channel
|
|
|
|
return None
|
|
|
|
def update_command(self, command: Optional[str] = None) -> Optional['DataMessageResponse']:
|
|
t: Optional[DataMessageResponse] = None
|
|
|
|
if command is not None:
|
|
for i, cmd in enumerate(command.split(';')):
|
|
if cmd == 'x':
|
|
self.reset()
|
|
|
|
elif cmd.startswith('h'):
|
|
self._message_handle = int(cmd[1:])
|
|
|
|
elif cmd == 'c':
|
|
self.clear_listen()
|
|
|
|
elif cmd.startswith('c'):
|
|
device, channel = part_suffix(cmd[1:], ':')
|
|
|
|
if ',' in channel:
|
|
self.listen(int(device), list(map(int, filter(lambda it: len(it), channel.split(',')))))
|
|
|
|
elif len(channel):
|
|
self.listen(int(device), int(channel))
|
|
|
|
elif cmd == 'f':
|
|
self._file_handle = None
|
|
|
|
elif cmd.startswith('f'):
|
|
self._file_handle = int(cmd[1:])
|
|
|
|
elif cmd == 'm':
|
|
self.set_listen_multiple_device(False)
|
|
|
|
elif cmd == 'M':
|
|
self.set_listen_multiple_device(True)
|
|
|
|
elif cmd.startswith('r'):
|
|
self.sample_rate = int(cmd[1:])
|
|
|
|
elif cmd.startswith('p'):
|
|
self.data_points = int(cmd[1:])
|
|
|
|
elif cmd.startswith('d'):
|
|
self.data_present = int(cmd[1:])
|
|
|
|
elif cmd.startswith('t'):
|
|
t = self._update_command(cmd)
|
|
|
|
else:
|
|
raise ValueError('unknown command : ' + cmd)
|
|
|
|
if t is not None:
|
|
return t
|
|
elif command is None:
|
|
return self._response
|
|
else:
|
|
return None
|
|
|
|
def _update_command(self, t: str) -> Optional['DataMessageResponse']:
|
|
if ':' in t:
|
|
t1, t2 = part_suffix(t[1:], ':', missing='')
|
|
|
|
if t2 == '':
|
|
self._response = ret = TimeRangeDataMessageResponse(int(t1), inf)
|
|
|
|
else:
|
|
ret = TimeRangeDataMessageResponse(int(t1), int(t2))
|
|
|
|
else:
|
|
self._response = ret = TimeDurationDataMessageResponse(int(t[1:]))
|
|
|
|
return ret
|
|
|
|
@overload
|
|
def new_encoder(self, response: 'DataMessageResponse') -> Union[None, DataMessageEncoder, List[DataMessageEncoder]]:
|
|
pass
|
|
|
|
@overload
|
|
def new_encoder(self, response: 'DataMessageResponse', device: int) -> Optional[DataMessageEncoder]:
|
|
pass
|
|
|
|
def new_encoder(self,
|
|
response: 'DataMessageResponse',
|
|
device: Optional[int] = None) -> Union[None, DataMessageEncoder, List[DataMessageEncoder]]:
|
|
"""
|
|
|
|
:param response:
|
|
:param device: listen device, None for all listened device
|
|
:return:
|
|
"""
|
|
listen = self._listen
|
|
sample_rate = response.sample_rate(self)
|
|
|
|
if isinstance(listen, _DataMessageListen):
|
|
if device is None or listen.device == device:
|
|
return listen.new_encoder(self._message_handle, self.data_present, sample_rate)
|
|
|
|
elif listen is not None and device is None:
|
|
return list(filter(lambda en: en is not None,
|
|
map(lambda ln: ln.new_encoder(self._message_handle, self.data_present, sample_rate),
|
|
listen.values())))
|
|
|
|
elif device in listen:
|
|
return listen[device].new_encoder(self._message_handle, self.data_present, sample_rate)
|
|
|
|
return None
|
|
|
|
|
|
class DataMessageResponse(metaclass=abc.ABCMeta):
|
|
__slots__ = ()
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def time_duration(self) -> float:
|
|
"""
|
|
|
|
:return: time duration in unit ms
|
|
"""
|
|
pass
|
|
|
|
def sample_rate(self, request: DataMessageRequest) -> int:
|
|
"""calculate effect sample rate according to the *request*
|
|
|
|
:param request: data message request
|
|
:return: sample rate in 1/sec
|
|
"""
|
|
sample_rate = request.sample_rate
|
|
|
|
if sample_rate is None:
|
|
points = request.data_points
|
|
|
|
if points is None:
|
|
raise ValueError('sample rate not set')
|
|
|
|
sample_rate = int(max(1.0, 1000 * points / self.time_duration))
|
|
|
|
return sample_rate
|
|
|
|
@abc.abstractmethod
|
|
def to_time_range(self, time_stop: Optional[float] = None) -> 'TimeRangeDataMessageResponse':
|
|
pass
|
|
|
|
|
|
class TimeRangeDataMessageResponse(DataMessageResponse):
|
|
__slots__ = ('_time_start', '_time_stop')
|
|
|
|
def __init__(self, time_start: float, time_stop: float):
|
|
"""
|
|
|
|
:param time_start: unit msec
|
|
:param time_stop: unit msec
|
|
"""
|
|
self._time_start = time_start
|
|
self._time_stop = time_stop
|
|
|
|
@property
|
|
def time_start(self) -> float:
|
|
return self._time_start
|
|
|
|
@property
|
|
def time_stop(self) -> float:
|
|
return self._time_stop
|
|
|
|
@property
|
|
def time_duration(self) -> float:
|
|
return self._time_stop - self._time_start
|
|
|
|
def to_time_range(self, time_stop: Optional[float] = None) -> 'TimeRangeDataMessageResponse':
|
|
if self._time_stop == inf and time_stop is not None:
|
|
return TimeRangeDataMessageResponse(self._time_start, time_stop)
|
|
else:
|
|
return self
|
|
|
|
def __str__(self):
|
|
return 'time_request[%d:%d]' % (self._time_start, self._time_stop)
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
|
|
class TimeDurationDataMessageResponse(DataMessageResponse):
|
|
__slots__ = ('_time_duration',)
|
|
|
|
def __init__(self, time_duration: float):
|
|
"""
|
|
|
|
:param time_duration: unit msec
|
|
"""
|
|
if time_duration < 0:
|
|
raise ValueError()
|
|
|
|
self._time_duration = time_duration
|
|
|
|
@property
|
|
def time_duration(self) -> float:
|
|
return self._time_duration
|
|
|
|
def to_time_range(self, time_stop: Optional[float] = None) -> TimeRangeDataMessageResponse:
|
|
if time_stop is not None:
|
|
return TimeRangeDataMessageResponse(max(0.0, time_stop - self._time_duration), time_stop)
|
|
else:
|
|
return TimeRangeDataMessageResponse(0, self._time_duration)
|
|
|
|
def __str__(self):
|
|
return 'time_request[%d]' % self._time_duration
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
|
|
class DataMessageHandler(metaclass=abc.ABCMeta):
|
|
__slots__ = '__request',
|
|
|
|
def __init__(self, request: DataMessageRequest = None):
|
|
if request is None:
|
|
request = DataMessageRequest()
|
|
|
|
self.__request = request
|
|
|
|
@property
|
|
def message_handle(self) -> int:
|
|
return self.__request.message_handle
|
|
|
|
@property
|
|
def sample_rate(self) -> Optional[int]:
|
|
return self.__request.sample_rate
|
|
|
|
@property
|
|
def data_points(self) -> Optional[int]:
|
|
return self.__request.data_points
|
|
|
|
@property
|
|
def data_present(self) -> DataPresentMode:
|
|
return self.__request.data_present
|
|
|
|
@property
|
|
def file_handle(self) -> Optional[int]:
|
|
return self.__request.file_handle
|
|
|
|
@property
|
|
def listen_device(self) -> List[int]:
|
|
return self.__request.listen_device
|
|
|
|
def listen_channels(self, device: int) -> Optional[List[int]]:
|
|
return self.__request.listen_channels(device)
|
|
|
|
def update_command(self, command: Optional[str]) -> Optional[DataMessageResponse]:
|
|
return self.__request.update_command(command)
|
|
|
|
@overload
|
|
def new_encoder(self, response: DataMessageResponse) -> Union[None, DataMessageEncoder, List[DataMessageEncoder]]:
|
|
pass
|
|
|
|
@overload
|
|
def new_encoder(self, response: DataMessageResponse, device: int) -> Optional[DataMessageEncoder]:
|
|
pass
|
|
|
|
def new_encoder(self, response: DataMessageResponse, device: Optional[int] = None):
|
|
return self.__request.new_encoder(response, device)
|
|
|
|
@abc.abstractmethod
|
|
def on_message(self, message: bytes):
|
|
pass
|
|
|
|
def on_error(self, message: str):
|
|
self.on_message(DataMessage.response(message))
|
|
|
|
def __str__(self):
|
|
return 'DataMessageHandler'
|