1158 lines
32 KiB
Python
1158 lines
32 KiB
Python
"""Device interface.
|
|
"""
|
|
|
|
import abc
|
|
import re
|
|
from enum import IntEnum
|
|
from functools import total_ordering
|
|
from time import time
|
|
from typing import Any, Optional, Tuple, Union, List, Dict, Callable, NoReturn
|
|
|
|
from biopro.text import *
|
|
from biopro.util.address import ADDRESS, EMPTY_ADDRESS, address_str
|
|
from biopro.util.console import WHITE
|
|
from biopro.util.logger import LoggerFlag
|
|
from . import *
|
|
from .menu import Menu
|
|
|
|
|
|
class DeviceCommandAction(IntEnum):
|
|
NOTHING = 0
|
|
SUCCESS = 1
|
|
CONNECTED = 2
|
|
DISCONNECTED = 3
|
|
SCAN = 4
|
|
MENU = 5
|
|
|
|
|
|
class DeviceBusyError(RuntimeError):
|
|
""""""
|
|
|
|
__slots__ = ()
|
|
|
|
|
|
class CommandNotFoundError(RuntimeError):
|
|
"""hardware command not found."""
|
|
|
|
__slots__ = ('_command',)
|
|
|
|
def __init__(self, command: str):
|
|
super().__init__(COMMAND_UNKNOWN + ' : ' + command)
|
|
|
|
self._command = command
|
|
|
|
@property
|
|
def command(self) -> Optional[str]:
|
|
return self._command
|
|
|
|
|
|
class MasterDeviceError(RuntimeError):
|
|
__slots__ = ('_master',)
|
|
|
|
def __init__(self, master: 'MasterDevice'):
|
|
super().__init__('master device error')
|
|
self._master = master
|
|
|
|
@property
|
|
def device(self) -> 'MasterDevice':
|
|
return self._master
|
|
|
|
|
|
@total_ordering
|
|
class DeviceLibraryVersion:
|
|
r"""
|
|
**version expression pattern**
|
|
|
|
::
|
|
|
|
DeviceLibraryVersion
|
|
= version_expression
|
|
| version_instance
|
|
|
|
version_instance = {
|
|
"major" :int
|
|
"minor" :int
|
|
"patch"? :int
|
|
"mark"? :str
|
|
}
|
|
|
|
**version_expression**
|
|
|
|
::
|
|
|
|
(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+))?(-(?P<mark>.+))?
|
|
|
|
"""
|
|
|
|
__slots__ = ('_major', '_minor', '_patch', '_mark')
|
|
|
|
def __init__(self, major: int, minor: int, patch: int = 0, mark: Optional[str] = None):
|
|
"""
|
|
:param major: major version number
|
|
:param minor: minor version number
|
|
:param patch: patch number
|
|
:param mark: version mark
|
|
"""
|
|
self._major = major
|
|
self._minor = minor
|
|
self._patch = patch
|
|
self._mark = mark
|
|
|
|
@property
|
|
def major(self) -> int:
|
|
return self._major
|
|
|
|
@property
|
|
def minor(self) -> int:
|
|
return self._minor
|
|
|
|
@property
|
|
def patch(self) -> int:
|
|
return self._patch
|
|
|
|
@property
|
|
def mark(self) -> Optional[str]:
|
|
return self._mark
|
|
|
|
@classmethod
|
|
def value_of(cls, json: Union[str, JSON]) -> 'DeviceLibraryVersion':
|
|
if isinstance(json, str):
|
|
return cls._value_of_str(json)
|
|
|
|
elif isinstance(json, dict):
|
|
return cls._value_of_json(json)
|
|
|
|
else:
|
|
raise RuntimeError('unknown version expression : ' + _truncate(json))
|
|
|
|
@staticmethod
|
|
def _value_of_str(expr: str) -> 'DeviceLibraryVersion':
|
|
m = re.match(r'(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+))?(-(?P<mark>.+))?', expr)
|
|
|
|
if m is None:
|
|
raise RuntimeError('illegal device library version expression : ' + expr)
|
|
|
|
try:
|
|
major = int(m.group('major'))
|
|
minor = int(m.group('minor'))
|
|
|
|
patch = m.group('patch')
|
|
if patch is not None:
|
|
patch = int(patch)
|
|
else:
|
|
patch = 0
|
|
|
|
mark = m.group('mark')
|
|
except ValueError as e:
|
|
raise RuntimeError('illegal device library version expression : ' + expr) from e
|
|
|
|
else:
|
|
return DeviceLibraryVersion(major, minor, patch, mark)
|
|
|
|
@staticmethod
|
|
def _value_of_json(json: JSON_OBJECT) -> 'DeviceLibraryVersion':
|
|
major = _get(json, 'major', int, 'version')
|
|
minor = _get(json, 'minor', int, 'version')
|
|
patch = _get(json, 'patch', int, 'version', nullable=True, default=0)
|
|
mark = _get(json, 'mark', str, 'version', nullable=True)
|
|
|
|
return DeviceLibraryVersion(major, minor, patch, mark)
|
|
|
|
def __hash__(self):
|
|
return 7 * self._major + 13 * self._minor + 17 * self._patch
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, DeviceLibraryVersion):
|
|
return False
|
|
|
|
return (
|
|
self._major == other._major and
|
|
self._minor == other._minor and
|
|
self._patch == other._patch
|
|
)
|
|
|
|
def __lt__(self, other: 'DeviceLibraryVersion'):
|
|
if self._major != other._major:
|
|
return self._major < other._major
|
|
if self._minor != other._minor:
|
|
return self._minor < other._minor
|
|
if self._patch != other._patch:
|
|
return self._patch < other._patch
|
|
return False
|
|
|
|
def __str__(self):
|
|
return '%d.%d.%d%s' % (self._major, self._minor, self._patch,
|
|
('-%s' % self._mark) if self._mark is not None else '')
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
|
|
class DeviceSerialNumber(JsonSerialize):
|
|
"""device serial number which contain six number.
|
|
|
|
**json format**
|
|
|
|
::
|
|
|
|
DeviceSerialNumber
|
|
: [int, int, int, int, int, int]
|
|
|
|
DeviceSerialNumber[0] # major product number
|
|
DeviceSerialNumber[1] # minor product number
|
|
DeviceSerialNumber[2] # major version number
|
|
DeviceSerialNumber[3] # minor version number
|
|
DeviceSerialNumber[4] # serial year number
|
|
DeviceSerialNumber[5] # serial month number
|
|
|
|
|
|
"""
|
|
UNKNOWN: 'DeviceSerialNumber' = None
|
|
|
|
__slots__ = ('_p1', '_p2', '_v1', '_v2', '_s1', '_s2')
|
|
|
|
def __init__(self,
|
|
major_product_number: int,
|
|
minor_product_number: int,
|
|
major_version_number: int,
|
|
minor_version_number: int,
|
|
serial_year_number: int,
|
|
serial_month_number: int):
|
|
self._p1 = major_product_number
|
|
self._p2 = minor_product_number
|
|
self._v1 = major_version_number
|
|
self._v2 = minor_version_number
|
|
self._s1 = serial_year_number
|
|
self._s2 = serial_month_number
|
|
|
|
@property
|
|
def major_product_number(self) -> int:
|
|
"""
|
|
:return: major product number
|
|
"""
|
|
return self._p1
|
|
|
|
@property
|
|
def minor_product_number(self) -> int:
|
|
"""
|
|
:return: minor product number
|
|
"""
|
|
return self._p2
|
|
|
|
@property
|
|
def major_version_number(self) -> int:
|
|
"""
|
|
:return: major version number
|
|
"""
|
|
return self._v1
|
|
|
|
@property
|
|
def minor_version_number(self) -> int:
|
|
"""
|
|
:return: minor version number
|
|
"""
|
|
return self._v2
|
|
|
|
@property
|
|
def serial_year_number(self) -> int:
|
|
"""
|
|
:return: serial year number. number start from 2000
|
|
"""
|
|
return self._s1
|
|
|
|
@property
|
|
def serial_month_number(self) -> int:
|
|
"""
|
|
:return: serial month number. number from 1.
|
|
"""
|
|
return self._s2
|
|
|
|
def as_json(self) -> List[int]:
|
|
return [self._p1, self._p2, self._v1, self._v2, self._s1, self._s2]
|
|
|
|
def __str__(self):
|
|
return '%d.%d/%d.%d/2%03dy%02d' % (self._p1, self._p2, self._v1, self._v2, self._s1, self._s2)
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
def __hash__(self):
|
|
ret = 7
|
|
ret = ret * 31 + self.major_product_number
|
|
ret = ret * 31 + self.minor_product_number
|
|
ret = ret * 31 + self.major_version_number
|
|
ret = ret * 31 + self.minor_version_number
|
|
ret = ret * 31 + self.serial_year_number
|
|
ret = ret * 31 + self.serial_month_number
|
|
|
|
return ret
|
|
|
|
def __eq__(self, other):
|
|
if self is other:
|
|
return True
|
|
|
|
if self is self.UNKNOWN or other is self.UNKNOWN:
|
|
return True
|
|
|
|
if not isinstance(other, DeviceSerialNumber):
|
|
raise False
|
|
|
|
return (
|
|
self.major_product_number == other.major_product_number and
|
|
self.minor_product_number == other.minor_product_number and
|
|
self.major_version_number == other.major_version_number and
|
|
self.minor_version_number == other.minor_version_number and
|
|
self.serial_year_number == other.serial_year_number and
|
|
self.serial_month_number == other.serial_month_number
|
|
)
|
|
|
|
|
|
DeviceSerialNumber.UNKNOWN = DeviceSerialNumber(0, 0, 0, 0, 0, 0)
|
|
|
|
|
|
class DeviceInfo(JsonSerialize, metaclass=abc.ABCMeta):
|
|
__slots__ = ()
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def device_id(self) -> Optional[int]:
|
|
"""device id. which given from master device"""
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def device_name(self) -> str:
|
|
"""device name."""
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def serial_number(self) -> DeviceSerialNumber:
|
|
"""device serial number"""
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def mac_address(self) -> ADDRESS:
|
|
"""device max address"""
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def library_name(self) -> str:
|
|
"""device library name.
|
|
|
|
*NOTE* this property has overwrote by the CompletedDevice. the subclass do not need to overwrite this.
|
|
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def library_version(self) -> str:
|
|
"""device library version.
|
|
|
|
*NOTE* this property has overwrote by the CompletedDevice. the subclass do not need to overwrite this.
|
|
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def battery(self) -> Optional[int]:
|
|
"""device battery voltage percentage
|
|
:return: percentage, value from 0 to 100, None if unknown.
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def parent(self) -> Optional[dict]:
|
|
"""device save file parent
|
|
:return: parent id {folder: id, ...}
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def recording_file_name(self) -> Optional[dict]:
|
|
"""device save file recording_file_name
|
|
:return: recording_file_name
|
|
"""
|
|
pass
|
|
|
|
def as_json(self) -> JSON_OBJECT:
|
|
try:
|
|
library_name = self.library_name
|
|
except:
|
|
library_name = 'unknown'
|
|
|
|
try:
|
|
library_version = self.library_version
|
|
except:
|
|
library_version = 'unknown'
|
|
|
|
return {
|
|
'device_id': self.device_id,
|
|
'device_name': self.device_name,
|
|
'device_address': list(self.mac_address),
|
|
'serial_number': self.serial_number.as_json(),
|
|
'library_name': library_name,
|
|
'library_version': library_version,
|
|
'battery': self.battery,
|
|
'parent': self.parent,
|
|
'recording_file_name': self.recording_file_name,
|
|
}
|
|
|
|
def match(self, device: 'DeviceInfo'):
|
|
return (self.device_id is None or self.device_id == device.device_id) and \
|
|
(self.device_name is None or self.device_name == device.device_name) and \
|
|
self.serial_number == device.serial_number and \
|
|
self.mac_address == device.mac_address
|
|
|
|
|
|
class DeviceResponseInfo(DeviceInfo):
|
|
"""
|
|
**json format**
|
|
|
|
::
|
|
|
|
DeviceResponseInfo = {
|
|
"device_name" :str
|
|
"device_address" :ADDRESS
|
|
"serial_number" :DeviceSerialNumber
|
|
"status" = "unknown" | "not_found" | "scanned" | "connected"
|
|
"library_name": str?
|
|
"library_version": str?,
|
|
"device_id" :int?
|
|
"battery" :int? # from 0 to 100
|
|
}
|
|
|
|
"""
|
|
|
|
STATUS_UNKNOWN = "unknown"
|
|
STATUS_NOT_FOUND = "not_found"
|
|
STATUS_SCANNED = "scanned"
|
|
STATUS_CONNECTED = "connected"
|
|
|
|
STATUS = (STATUS_UNKNOWN, STATUS_NOT_FOUND, STATUS_SCANNED, STATUS_CONNECTED)
|
|
|
|
UNKNOWN: 'DeviceResponseInfo' = None
|
|
|
|
__slots__ = (
|
|
'_device_id',
|
|
'_device_name',
|
|
'_serial_number',
|
|
'_mac_address',
|
|
'_library_name',
|
|
'_library_version',
|
|
'_status',
|
|
'_battery',
|
|
'_parent',
|
|
'_recording_file_name',
|
|
'_addr_type'
|
|
)
|
|
|
|
def __init__(self,
|
|
device_name: Optional[str],
|
|
serial_number: DeviceSerialNumber,
|
|
mac_address: ADDRESS,
|
|
library_name: Optional[str] = None,
|
|
library_version: Optional[str] = None,
|
|
status: str = STATUS_UNKNOWN,
|
|
device_id: Optional[int] = None,
|
|
battery: Optional[int] = None,
|
|
parent:Optional[dict] = None,
|
|
recording_file_name:Optional[str] = None,
|
|
addr_type: Optional[int] = None):
|
|
|
|
if status is None:
|
|
status = self.STATUS_UNKNOWN
|
|
elif status not in self.STATUS:
|
|
raise ValueError('illegal status : ' + status)
|
|
|
|
if serial_number is None:
|
|
raise RuntimeError()
|
|
if mac_address is None:
|
|
raise RuntimeError()
|
|
|
|
self._device_id = device_id
|
|
self._device_name = device_name
|
|
self._serial_number = serial_number
|
|
self._mac_address = mac_address
|
|
self._library_name = library_name
|
|
self._library_version = library_version
|
|
self._status = status
|
|
self._battery = battery
|
|
self._parent = parent
|
|
self._recording_file_name = recording_file_name
|
|
self._addr_type = addr_type
|
|
|
|
@property
|
|
def device_id(self) -> Optional[int]:
|
|
return self._device_id
|
|
|
|
@property
|
|
def device_name(self) -> Optional[str]:
|
|
return self._device_name
|
|
|
|
@property
|
|
def serial_number(self) -> DeviceSerialNumber:
|
|
return self._serial_number
|
|
|
|
@property
|
|
def mac_address(self) -> ADDRESS:
|
|
return self._mac_address
|
|
|
|
@property
|
|
def library_name(self) -> Optional[str]:
|
|
return self._library_name
|
|
|
|
@property
|
|
def library_version(self) -> Optional[str]:
|
|
return self._library_version
|
|
|
|
@property
|
|
def battery(self) -> Optional[int]:
|
|
return self._battery
|
|
|
|
@property
|
|
def parent(self) -> Optional[dict]:
|
|
return self._parent
|
|
|
|
@property
|
|
def recording_file_name(self) -> Optional[str]:
|
|
return self._recording_file_name
|
|
|
|
@property
|
|
def addr_type(self) -> Optional[int]:
|
|
return self._addr_type
|
|
|
|
@property
|
|
def device_status(self) -> str:
|
|
return self._status
|
|
|
|
@device_status.setter
|
|
def device_status(self, value: str):
|
|
self._status = value
|
|
|
|
def as_json(self):
|
|
ret = super().as_json()
|
|
ret['status'] = self._status
|
|
return ret
|
|
|
|
@classmethod
|
|
def address_of(cls, address: ADDRESS) -> 'DeviceResponseInfo':
|
|
return DeviceResponseInfo(None,
|
|
DeviceSerialNumber.UNKNOWN,
|
|
address)
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
def __str__(self):
|
|
s1 = '%s,%s,%s' % (self._device_name, address_str(self._mac_address), str(self._serial_number))
|
|
|
|
if self._library_name is not None:
|
|
s2 = self.library_version + ('-' + self._library_version) if self._library_version is not None else ''
|
|
s2 = '[' + s2 + ']'
|
|
|
|
else:
|
|
s2 = ''
|
|
|
|
if self._battery is None:
|
|
s3 = '???'
|
|
else:
|
|
s3 = str(self._battery)
|
|
|
|
return 'DeviceResponseInfo[%s]%s:%s[%s]' % (s1, s2, self._status, s3)
|
|
|
|
def __hash__(self):
|
|
ret = hash(self._device_name)
|
|
ret = ret * 31 + hash(self._serial_number)
|
|
ret = ret * 31 + hash(self._mac_address)
|
|
ret = ret * 31 + hash(self._battery)
|
|
|
|
return ret
|
|
|
|
def __eq__(self, other):
|
|
if self is other:
|
|
return True
|
|
|
|
if not isinstance(other, DeviceResponseInfo):
|
|
raise False
|
|
|
|
return (
|
|
self.device_name == other.device_name and
|
|
self.serial_number == other.serial_number and
|
|
self.mac_address == other.mac_address
|
|
)
|
|
|
|
|
|
DeviceResponseInfo.UNKNOWN = DeviceResponseInfo(None,
|
|
DeviceSerialNumber.UNKNOWN,
|
|
EMPTY_ADDRESS)
|
|
|
|
|
|
class DeviceInstruction:
|
|
"""low level device instruction type.
|
|
|
|
**Instruction Send**
|
|
|
|
raw byte data format. ::
|
|
|
|
| 0 | 1 | 2 | 3 |
|
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
|
|
-----------------------------------------------------------------
|
|
| RIS | ID | | LEN | ............................. real instruction send
|
|
| VIS | ID | OPER | LEN | ............................. virtual instruction send
|
|
| CIS | ID | OPER | LEN | ............................. command instruction send
|
|
|
|
ID
|
|
chip ID
|
|
|
|
RIS
|
|
header for real instruction type. (header=3)
|
|
|
|
VIS
|
|
header for virtual instruction type. (header=C)
|
|
|
|
CIS
|
|
header for command instruction type. (header=7)
|
|
|
|
OPER
|
|
operator/command
|
|
|
|
LEN, length
|
|
command/data length in bytes
|
|
"""
|
|
|
|
TYP_RIS = 0x30
|
|
"""header for real instruction type.
|
|
It is real instruction send to the chip which contain the chip configuration parameter.
|
|
"""
|
|
|
|
TYP_VIS = 0xC0
|
|
"""header for virtual instruction type.
|
|
It is virtual instruction send to the controller chip to control the chip.
|
|
"""
|
|
|
|
TYP_CIS = 0x70
|
|
"""header for command instruction type.
|
|
It is command send to the controller chip for the special purpose.
|
|
"""
|
|
|
|
VIS_RST = 0xF0
|
|
"""reset virtual instruction"""
|
|
|
|
VIS_DEVICE_DETECT = 0x10
|
|
"""identify device"""
|
|
|
|
VIS_DEVICE_DONE = 0x20
|
|
"""identify device done"""
|
|
|
|
VIS_STI = 0xC0
|
|
"""stimulation on virtual instruction"""
|
|
|
|
VIS_INT = 0x60
|
|
"""interrupt virtual instruction"""
|
|
|
|
VIS_CAL = 0xA0
|
|
"""device calling"""
|
|
|
|
CIS_VOLT = 0x10
|
|
"""get device battery voltage level"""
|
|
|
|
CIS_LED = 0x20
|
|
"""set device led level"""
|
|
|
|
CIS_CALI = 0x30
|
|
"""get calibration information table"""
|
|
|
|
CIS_VERSION = 0x40
|
|
"""get device verion"""
|
|
|
|
CIS_RF = 0x50
|
|
"""set device RF level"""
|
|
|
|
CIS_AS = 0x60
|
|
"""set device AS level"""
|
|
|
|
VIS_STARTR = 0x10
|
|
|
|
VIS_STARTS = 0x20
|
|
|
|
VIS_STOPR = 0x30
|
|
|
|
VIS_STOPS = 0x40
|
|
|
|
TYP_IIS = -1
|
|
"""internal instruction"""
|
|
|
|
TYP_ALL = None
|
|
|
|
__slots__ = ()
|
|
|
|
@classmethod
|
|
def valid_ins_type(cls, ins_type: int):
|
|
if ins_type not in (cls.TYP_RIS, cls.TYP_VIS, cls.TYP_CIS, cls.TYP_IIS, cls.TYP_ALL):
|
|
raise ValueError('unknown instruction type : ' + str(ins_type))
|
|
|
|
@classmethod
|
|
def get_instruction_type(cls, ins_type: str) -> int:
|
|
if ins_type == 'RIS':
|
|
return cls.TYP_RIS
|
|
elif ins_type == 'VIS':
|
|
return cls.TYP_VIS
|
|
elif ins_type == 'CIS':
|
|
return cls.TYP_CIS
|
|
elif ins_type == 'ALL':
|
|
return cls.TYP_ALL
|
|
else:
|
|
raise RuntimeError('unknown instruction type : ' + ins_type)
|
|
|
|
|
|
class DeviceCommonInstruction:
|
|
"""high level device instruction."""
|
|
|
|
RESET = 'reset'
|
|
START = 'start'
|
|
INTERRUPT = 'interrupt'
|
|
CLOSE = 'close'
|
|
FLUSH = 'flush'
|
|
CALL = 'call'
|
|
RECORD = "record"
|
|
RECORD_ALL = "record_all"
|
|
STIMULATE = "stimulate"
|
|
STOP_RECORD = "stop_record"
|
|
STOP_STIMULATE = "stop_stimulate"
|
|
VIS_DEVICE_DETECT = 'VIS_DEVICE_DETECT'
|
|
VIS_DEVICE_DONE = 'VIS_DEVICE_DONE'
|
|
CIS_VOLT = 'CIS_VOLT'
|
|
CIS_VERSION = 'CIS_VERSION'
|
|
|
|
ALL = [RESET, START, INTERRUPT, CLOSE, FLUSH, CALL]
|
|
|
|
__slots__ = ()
|
|
|
|
# don't create DeviceCommonInstruction instance, just return tuple
|
|
def __new__(cls, instruction: str) -> Optional[Tuple[str, ...]]:
|
|
if instruction == cls.RESET:
|
|
return '_sync(False)', '_notify(False)', 'VIS_RST'
|
|
elif instruction == cls.START:
|
|
return '_notify(True)', 'VIS_STI', '_sync(True)'
|
|
elif instruction == cls.INTERRUPT:
|
|
return '_notify(False)', 'VIS_INT', '_sync(False)'
|
|
elif instruction == cls.CLOSE:
|
|
return '_sync(False)', '_notify(False)', 'VIS_INT'
|
|
elif instruction == cls.CALL:
|
|
return 'VIS_CAL',
|
|
elif instruction == cls.VIS_DEVICE_DETECT:
|
|
return 'VIS_DEVICE_DETECT',
|
|
elif instruction == cls.VIS_DEVICE_DONE:
|
|
return 'VIS_DEVICE_DONE',
|
|
elif instruction == cls.RECORD:
|
|
return 'VIS_STARTR',
|
|
elif instruction == cls.RECORD_ALL:
|
|
return 'VIS_STARTR',
|
|
elif instruction == cls.STIMULATE:
|
|
return 'VIS_STARTS',
|
|
elif instruction == cls.STOP_RECORD:
|
|
return 'VIS_STOPR',
|
|
elif instruction == cls.STOP_STIMULATE:
|
|
return 'VIS_STOPS',
|
|
elif instruction == cls.CIS_VOLT:
|
|
return 'CIS_VOLT',
|
|
elif instruction == cls.CIS_VERSION:
|
|
return 'CIS_VERSION',
|
|
else:
|
|
return None
|
|
|
|
|
|
class DeviceFeature:
|
|
"""TODO add doc"""
|
|
BATTERY = 0x0001
|
|
CALIBRATION = 0x0002
|
|
|
|
|
|
class DeviceFeatureUnsupportedError(RuntimeError):
|
|
def __init__(self, device_id: int, feature: int):
|
|
super().__init__('device [%d] do not support feature code : 0x%04X' % (device_id, feature))
|
|
|
|
|
|
class Device(DeviceInfo, metaclass=abc.ABCMeta):
|
|
"""device with partial function which only provide necessary information.
|
|
|
|
**json response format**
|
|
|
|
::
|
|
|
|
Device = {
|
|
"device_id" :int
|
|
"device_name" :str
|
|
"device_address" :mac_address
|
|
"serial_number" :DeviceSerialNumber
|
|
"library_name" :str
|
|
"library_version" :str
|
|
"battery" :int # from 0 to 100
|
|
|
|
# for completed device
|
|
"configuration" :DeviceConfiguration
|
|
}
|
|
|
|
mac_address
|
|
: [int, int, int, int, int, int]
|
|
|
|
"""
|
|
|
|
__slots__ = ()
|
|
|
|
# library information
|
|
|
|
@property
|
|
def library_name(self) -> str:
|
|
"""device library name.
|
|
|
|
*NOTE* this property has overwrote by the CompletedDevice. the subclass do not need to overwrite this.
|
|
|
|
"""
|
|
raise RuntimeError('implement by CompletedDevice')
|
|
|
|
@property
|
|
def library_version(self) -> str:
|
|
"""device library version.
|
|
|
|
*NOTE* this property has overwrote by the CompletedDevice. the subclass do not need to overwrite this.
|
|
|
|
"""
|
|
raise RuntimeError('implement by CompletedDevice')
|
|
|
|
# device information
|
|
|
|
@property
|
|
def device_status(self) -> str:
|
|
return DeviceResponseInfo.STATUS_CONNECTED
|
|
|
|
@property
|
|
def response_info(self) -> DeviceResponseInfo:
|
|
"""response information"""
|
|
return DeviceResponseInfo(self.device_name,
|
|
self.serial_number,
|
|
self.mac_address,
|
|
self.library_name,
|
|
self.library_version,
|
|
DeviceResponseInfo.STATUS_CONNECTED,
|
|
self.device_id,
|
|
self.battery,
|
|
self.parent,
|
|
self.recording_file_name)
|
|
|
|
# device feature
|
|
|
|
@abc.abstractmethod
|
|
def read_feature_mask(self) -> bytes:
|
|
pass
|
|
|
|
def is_feature_support(self, feature: int) -> bool:
|
|
raise RuntimeError('implement by CompletedDevice')
|
|
|
|
def ensure_feature_support(self, feature: int):
|
|
if not self.is_feature_support(feature):
|
|
raise DeviceFeatureUnsupportedError(self.device_id, feature)
|
|
|
|
# device instruction
|
|
|
|
@abc.abstractmethod
|
|
def read_command_return_data(self) -> Optional[bytes]:
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def send_instruction(self, ins_type: int, ins_oper: int, *instruction: int):
|
|
"""send instruction to device
|
|
|
|
:param ins_type: instruction type
|
|
:param ins_oper: instruction
|
|
:param instruction: content
|
|
:raises: DeviceInstructionError
|
|
"""
|
|
pass
|
|
|
|
# device specific functions
|
|
|
|
@property
|
|
def device_version(self) -> str:
|
|
return ''
|
|
|
|
@property
|
|
def battery(self) -> int:
|
|
return 100
|
|
|
|
def update_battery_info(self):
|
|
"""update device battery information """
|
|
pass
|
|
|
|
def update_parent_info(self, _parent: dict):
|
|
"""update device parent information """
|
|
pass
|
|
|
|
def update_recording_file_name_info(self, _recording_file_name: str):
|
|
"""update device recording_file_name information """
|
|
pass
|
|
|
|
def update_recording_file_name_info(self, _recording_file_name: str):
|
|
"""update device parrecording_file_nameent information """
|
|
pass
|
|
|
|
def calibration_info(self, device_type: str) -> bytes:
|
|
raise DeviceFeatureUnsupportedError(self.device_id, DeviceFeature.CALIBRATION)
|
|
|
|
def update_calibration_info(self, device_type: str):
|
|
raise DeviceFeatureUnsupportedError(self.device_id, DeviceFeature.CALIBRATION)
|
|
|
|
def update_radio_frequency(self, radio_frequency):
|
|
"""update device radio frequency"""
|
|
pass
|
|
|
|
def update_led_brightness(self, led_brightness):
|
|
"""update device led brightness"""
|
|
pass
|
|
|
|
def update_accelerator_sensitivity(self, accelerator_sensitivity):
|
|
"""update device accelerator sensitivity"""
|
|
pass
|
|
|
|
class MasterDevice(LoggerFlag, metaclass=abc.ABCMeta):
|
|
"""master device, the actual hardware device to connect/control the slave device."""
|
|
|
|
__slots__ = ()
|
|
|
|
def __init__(self,
|
|
name: Optional[str] = None,
|
|
log_name_color: int = WHITE) -> None:
|
|
name = name if name is not None else self.__class__.__name__
|
|
LoggerFlag.__init__(self, name, log_name_color)
|
|
|
|
@abc.abstractmethod
|
|
def reset(self, device: Optional[List[int]] = None, software_reset=True):
|
|
"""reset master device
|
|
|
|
:param device: reset certain device. None for all
|
|
:param software_reset:
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def shutdown(self, release_resource=True):
|
|
"""shutdown the master device.
|
|
|
|
*release_resource* used when more than one devices share the same resource.
|
|
close the resource when other device is using, may cause hardware error.
|
|
This parameter is internal used because only work functionally in certain device class,
|
|
user should not care it.
|
|
|
|
:param release_resource: close the resource.
|
|
"""
|
|
pass
|
|
|
|
def scan(self, timeout=5, all_device=False) -> List[DeviceResponseInfo]:
|
|
"""scan the nearby device.
|
|
|
|
make sure the result should be equal to found().
|
|
|
|
:param timeout: max scanning time.
|
|
:param all_device: all device perform scanning if not working.
|
|
this options only work on some implements.
|
|
:return: found device
|
|
"""
|
|
error = None
|
|
|
|
try:
|
|
result = self.scan_callback((lambda info: None), timeout=timeout, all_device=all_device)
|
|
except RuntimeError as e:
|
|
error = e
|
|
|
|
else:
|
|
if not result:
|
|
raise RuntimeError(HARDWARE_SCAN_FAIL)
|
|
|
|
if error is not None:
|
|
# IncorrectError, due to a device is connected incorrectly (?),
|
|
# try to disconnect all devices
|
|
self.log_warn('suppressed error : ' + str(error))
|
|
|
|
raise MasterDeviceError(self)
|
|
|
|
return self.found()
|
|
|
|
@abc.abstractmethod
|
|
def scan_callback(self,
|
|
callback: Callable[[DeviceResponseInfo], None],
|
|
timeout=5,
|
|
all_device=False) -> bool:
|
|
"""scan the nearby device.
|
|
|
|
perform device scanning
|
|
|
|
:param callback:
|
|
:param timeout: max scanning time
|
|
:param all_device: all device perform scanning if not working.
|
|
this options only work on some implements.
|
|
:return: success or not
|
|
"""
|
|
pass
|
|
|
|
def scan_device(self, response: DeviceInfo, timeout=5) -> Optional[DeviceResponseInfo]:
|
|
"""scan until found the device *response*, or *timeout* reached.
|
|
|
|
:param response:
|
|
:param timeout:
|
|
:return:
|
|
"""
|
|
t = time()
|
|
|
|
while time() - t < timeout:
|
|
found = self.scan(all_device=True)
|
|
|
|
for f in found:
|
|
if response.match(f):
|
|
return f
|
|
|
|
return None
|
|
|
|
@abc.abstractmethod
|
|
def found(self) -> List[DeviceResponseInfo]:
|
|
"""found nearby device"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def connect(self, response: DeviceResponseInfo) -> Device:
|
|
"""connect to the device"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def disconnect(self, device: int, force=False) -> bool:
|
|
"""disconnect the device
|
|
|
|
:param device: device ID
|
|
:param force:
|
|
:return: success of not
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_device(self, device: int) -> Optional[Device]:
|
|
"""get the device by device ID"""
|
|
pass
|
|
|
|
def get_menu(self) -> Optional[Menu]:
|
|
"""get master device menu"""
|
|
return None
|
|
|
|
def handle_command(self, device: Optional[int], command: str, options: Dict[str, str]) -> DeviceCommandAction:
|
|
"""device command
|
|
|
|
:param device: device ID
|
|
:param command: command
|
|
:param options: command options
|
|
:return: action
|
|
"""
|
|
self._raise_command_not_found(device, command)
|
|
|
|
def handle_internal_instruction(self, device: Device, func: str, *para: Any) -> bool:
|
|
return False
|
|
|
|
@staticmethod
|
|
def _raise_command_not_found(device: Optional[int], command: str) -> NoReturn:
|
|
if device is None:
|
|
message = command
|
|
else:
|
|
message = "%d:%s" % (device, command)
|
|
|
|
raise CommandNotFoundError(message)
|
|
|
|
def hardware_test(self) -> Dict[str, Any]:
|
|
return {}
|
|
|
|
|
|
class NullMasterDevice(MasterDevice):
|
|
"""no function master device."""
|
|
|
|
__slots__ = ()
|
|
|
|
def __init__(self, name: Optional[str] = None):
|
|
super().__init__(name if name is not None else 'NullDev')
|
|
|
|
def reset(self, device: Optional[List[int]] = None, software_reset=True):
|
|
pass
|
|
|
|
def shutdown(self, release_resource=True):
|
|
pass
|
|
|
|
def scan(self, timeout=5, all_device=False) -> List[DeviceResponseInfo]:
|
|
return []
|
|
|
|
def scan_callback(self, callback: Callable[[DeviceResponseInfo], None], timeout=5, all_device=False) -> bool:
|
|
return False
|
|
|
|
def scan_device(self, response: DeviceInfo, timeout=5) -> Optional[DeviceResponseInfo]:
|
|
return None
|
|
|
|
def found(self) -> List[DeviceResponseInfo]:
|
|
return []
|
|
|
|
def connect(self, response: DeviceResponseInfo) -> Device:
|
|
raise NotImplementedError('connect')
|
|
|
|
def disconnect(self, device: int, force=False) -> bool:
|
|
raise NotImplementedError('disconnect')
|
|
|
|
def get_device(self, device: int) -> Optional[Device]:
|
|
return None
|
|
|
|
def hardware_test(self) -> Dict[str, Any]:
|
|
return {'hardware': 'null'}
|
|
|
|
def get_device_mac_in_address_format(raw_mac: list) -> ADDRESS:
|
|
if len(raw_mac) is 6:
|
|
return raw_mac[5], raw_mac[4], raw_mac[3], raw_mac[2], raw_mac[1], raw_mac[0]
|
|
else:
|
|
return EMPTY_ADDRESS
|
|
|
|
|
|
def get_device_name_in_string_format(raw_name: list) -> Optional[str]:
|
|
# initialization of string to ""
|
|
ret = ""
|
|
|
|
# traverse in the string
|
|
for x in raw_name:
|
|
if x is not 0:
|
|
try:
|
|
ret += chr(x)
|
|
|
|
except UnicodeDecodeError:
|
|
print("UnicodeDecodeError: ", raw_name)
|
|
ret = ""
|
|
|
|
# return string
|
|
# ret.rstrip("\x00")
|
|
|
|
if ret is "":
|
|
return None
|
|
else:
|
|
return ret
|
|
|
|
|
|
def get_device_company_code(raw_code: list) -> Optional[tuple]:
|
|
if len(raw_code) is 4:
|
|
return chr(raw_code[0]), chr(raw_code[1]), chr(raw_code[2]), chr(raw_code[3])
|
|
else:
|
|
return None
|
|
|
|
|
|
# used in scan response battery information
|
|
def get_device_battery_info(raw_code: list) -> Optional[tuple]:
|
|
if len(raw_code) is 5:
|
|
return chr(raw_code[0]), chr(raw_code[1]), chr(raw_code[2]), raw_code[3], raw_code[4]
|
|
else:
|
|
return None
|