1341 lines
42 KiB
Python
1341 lines
42 KiB
Python
import abc
|
|
import inspect
|
|
import re
|
|
import uuid
|
|
from datetime import datetime
|
|
from functools import wraps
|
|
from json import loads as json_parse
|
|
from pathlib import Path
|
|
from time import time
|
|
from typing import Tuple, Dict, TypeVar, Callable, overload, Type
|
|
|
|
from biopro import *
|
|
from biopro.controller import ControlAPI, ComplexResponse
|
|
from biopro.device.manager import DeviceManager, DeviceInternalCommandHandler, MasterDeviceError
|
|
from biopro.devlib.device import DeviceResponseInfo, DeviceSerialNumber, DeviceInfo
|
|
from biopro.devlib.library import CompletedDevice
|
|
from biopro.devlib.menu import Menu
|
|
from biopro.exp_pro.protocol import ExpProtocolInfo, ExpOption
|
|
from biopro.exp_pro.schedule import AbstractExpProtocolState, ExpProtocolState
|
|
from biopro.file.export import ExternalExportFunction, ExternalParameter
|
|
from biopro.file.manager import FileManager, FileSegment
|
|
from biopro.file.segment import FileSegmentContent
|
|
from biopro.file.user.alias import UserDeviceAlias
|
|
from biopro.file.user.setting import AbstractUserSetting, RestrictUserSetting
|
|
from biopro.file.user.setup import UserSetup
|
|
from biopro.file.util import RootInfo, DirectoryInfo, FileInfo
|
|
from biopro.impl.led import LED
|
|
from biopro.message.request import DataMessageHandler
|
|
from biopro.recording import RecordingMetaFile
|
|
from biopro.text import *
|
|
from biopro.util.address import ADDRESS, str_address, address_str, is_address_type
|
|
from biopro.util.cli import *
|
|
from biopro.util.console import *
|
|
from biopro.util.datetime import datetime_str
|
|
from biopro.util.json import JsonSerialize, JSON, JSON_OBJECT
|
|
from biopro.util.promise import Promise
|
|
from biopro.util.stack import print_exception
|
|
from biopro.util.text import expr_format, part_suffix, part_prefix
|
|
from .cache import FileHandle
|
|
from .socket import T_ServerThread
|
|
|
|
_RUNTIME_COMPILE = False
|
|
|
|
T = TypeVar('T')
|
|
|
|
|
|
# noinspection PyUnusedLocal
|
|
class ControlServerOptions(CliOptions):
|
|
DEFAULT_STORAGE_ROOT = '.BPS'
|
|
|
|
DEFAULT_WEBSOCKET_ADDRESS = '0.0.0.0'
|
|
DEFAULT_WEBSOCKET_PORT = 9542
|
|
|
|
DEFAULT_MQTT_ADDRESS = '52.194.17.114'
|
|
DEFAULT_MQTT_PORT = 1883
|
|
|
|
DEFAULT_CACHE_SIZE = None
|
|
DEFAULT_CACHE_TIME = 10
|
|
|
|
DEFAULT_UPS_WAIT_TIME = 10 * 60 # 10 min
|
|
|
|
def __init__(self):
|
|
# storage root
|
|
self.storage_root = str(Path.home() / self.DEFAULT_STORAGE_ROOT)
|
|
'''storage root'''
|
|
|
|
self.export_root = None
|
|
'''export root'''
|
|
|
|
# device
|
|
self.shutdown_if_reset_fail = False
|
|
'''shutdown server if device reset fail'''
|
|
|
|
self.expect_available_channel = None
|
|
'''given expect device channel, reduce the server setup times'''
|
|
|
|
# user session
|
|
self.user_session_expire_days = 1
|
|
'''expire days for user session keeping'''
|
|
|
|
# file manager
|
|
self.flag_disable_routine_trash = False
|
|
'''disable trashing routine thread'''
|
|
|
|
self.trash_routine_time = 24 # 1 day
|
|
'''interval of trashing routine'''
|
|
|
|
self.trash_expire_time = 100 # almost 3 month
|
|
'''expire days for trashing the untouched file'''
|
|
|
|
self.trash_remove_expire_time = 30 # 1 month
|
|
'''expire days for removing the trashed file '''
|
|
|
|
# websocket
|
|
self.websocket_address = self.DEFAULT_WEBSOCKET_ADDRESS
|
|
'''connection address for websocket service'''
|
|
|
|
self.websocket_port = self.DEFAULT_WEBSOCKET_PORT
|
|
'''connection port for websocket service'''
|
|
|
|
# MQTT
|
|
self.flag_disable_remote_server = False
|
|
'''disable remote service'''
|
|
|
|
self.mqtt_address = self.DEFAULT_MQTT_ADDRESS
|
|
'''remote server connection address'''
|
|
|
|
self.mqtt_port = self.DEFAULT_MQTT_PORT
|
|
'''remote server connection port'''
|
|
|
|
# data server
|
|
self.flag_disable_data_server = False
|
|
'''disable data server'''
|
|
|
|
# experimental protocol manager
|
|
self.flag_disable_exp_manager = False
|
|
'''disable experimental protocol manager'''
|
|
|
|
# cache server
|
|
self.flag_disable_cache_server = False
|
|
'''disable cache server'''
|
|
|
|
self.cache_server_cache_size = self.DEFAULT_CACHE_SIZE
|
|
'''data cache buffer size'''
|
|
|
|
self.cache_server_cache_time = self.DEFAULT_CACHE_TIME
|
|
'''data cache time in second'''
|
|
|
|
# external storage
|
|
self.flag_disable_external_storage = False
|
|
'''disable external storage'''
|
|
|
|
self.external_path_map = []
|
|
'''external storage mapping'''
|
|
|
|
# LED control
|
|
self.flag_disable_led_control = False
|
|
'''disable LED control'''
|
|
|
|
self.flag_disable_led_refresh = False
|
|
'''refresh the LED instruction'''
|
|
|
|
self.led_count = 16
|
|
'''total LED chip number'''
|
|
|
|
# sha checking
|
|
self.flag_disable_sha_check = False
|
|
'''disable SHA sum checking'''
|
|
|
|
# UPS detect
|
|
self.flag_disable_ups_detect = False
|
|
'''disable UPS detecting'''
|
|
|
|
self.flag_ups_wait_time = self.DEFAULT_UPS_WAIT_TIME
|
|
'''the waiting time for power off after UPS mode'''
|
|
|
|
@cli_options('-C', '--root', value='PATH')
|
|
@cli_help_section('File Manager Options')
|
|
def _storage_root(self, opt: str, value: str):
|
|
"""change the storage root"""
|
|
self.storage_root = value
|
|
|
|
@cli_options('-E', '--export', value='PATH')
|
|
@cli_help_section('File Manager Options')
|
|
def _export_root(self, opt: str, value: str):
|
|
"""change the export root"""
|
|
self.export_root = value
|
|
|
|
@cli_options('--user-session-expire', value='DAYS')
|
|
def _user_session_expire(self, opt: str, value: str):
|
|
"""set user session expire days"""
|
|
self.user_session_expire_days = int(value)
|
|
|
|
# remote system
|
|
|
|
@cli_flags('--disable-remote-server')
|
|
@cli_help_section('Remote System')
|
|
def _disable_remote_server(self, opt: str):
|
|
"""disable remote server connecting"""
|
|
self.flag_disable_remote_server = True
|
|
|
|
if not _RUNTIME_COMPILE:
|
|
@cli_options('--remote-server-address', value='IP:PORT')
|
|
@cli_help_section('Remote System')
|
|
def _remote_server_address(self, opt: str, value: str):
|
|
"""change the remote server IP"""
|
|
|
|
url, port = part_suffix(value, ':', missing=self.DEFAULT_MQTT_PORT)
|
|
|
|
port = int(port)
|
|
|
|
self.mqtt_address = url
|
|
self.mqtt_port = port
|
|
|
|
@cli_flags('--disable-data-server')
|
|
@cli_help_section('Data Server Options')
|
|
def _disable_data_server(self, opt: str):
|
|
"""disable data server process"""
|
|
self.flag_disable_data_server = True
|
|
|
|
@cli_flags('--disable-exp-manager')
|
|
@cli_help_section('Experimental Protocol Options')
|
|
def _disable_exp_pro(self, opt: str):
|
|
"""disable experimental protocol"""
|
|
self.flag_disable_exp_manager = True
|
|
|
|
@cli_flags('--disable-cache-server')
|
|
@cli_help_section('Cache Manager')
|
|
def _disable_cache_server(self, opt: str):
|
|
"""disable cache server process"""
|
|
self.flag_disable_cache_server = True
|
|
|
|
@cli_options('--cache-size', value='SIZE')
|
|
@cli_help_section('Cache Manager')
|
|
def _cache_size(self, opt: str, value: str):
|
|
"""cache size limit. default : %DEFAULT_CACHE_SIZE%"""
|
|
self.cache_server_cache_size = int(value)
|
|
|
|
@cli_options('--cache-time', value='SEC')
|
|
@cli_help_section('Cache Manager')
|
|
def _cache_time(self, opt: str, value: str):
|
|
"""cache data time. default : %DEFAULT_CACHE_TIME%"""
|
|
self.cache_server_cache_time = float(value)
|
|
|
|
@cli_flags('--shutdown-if-reset-fail')
|
|
@cli_help_section('Device Manager Options')
|
|
def _shutdown_if_reset_fail(self, opt: str):
|
|
"""shutdown the controller if device reset fail"""
|
|
self.shutdown_if_reset_fail = True
|
|
|
|
@cli_options('--expected-channel', value="CH,...")
|
|
@cli_help_section('Device Manager Options')
|
|
def _expected_channel(self, opt: str, value: str):
|
|
"""expect device channel. could be:
|
|
ALL : all channel
|
|
NULL : empty
|
|
INIT : by check
|
|
@FILE : read from file, format: CHANNEL,...
|
|
CHANNEL,... : given channel number
|
|
"""
|
|
from biopro.impl.selector import Selector
|
|
|
|
if value == 'ALL':
|
|
c = list(range(Selector.SIZE))
|
|
|
|
elif value == 'NULL':
|
|
c = []
|
|
|
|
elif value == 'INIT':
|
|
c = None
|
|
|
|
elif value.startswith('@'):
|
|
c = self._expected_channel_file(value[1:])
|
|
|
|
else:
|
|
c = list(map(int, value.split(',')))
|
|
|
|
self.expect_available_channel = c
|
|
|
|
def _expected_channel_file(self, filepath: str) -> Optional[List[int]]:
|
|
p = Path(filepath)
|
|
|
|
if not p.exists():
|
|
return None
|
|
|
|
with p.open() as f:
|
|
line = f.readline().strip()
|
|
|
|
return list(map(int, line.split(',')))
|
|
|
|
@cli_flags('--disable-routine-trash')
|
|
@cli_help_section('File Manager Options')
|
|
def _disable_routine_trash(self, flag: str):
|
|
"""disable routine trashing"""
|
|
self.flag_disable_routine_trash = True
|
|
|
|
@cli_options('--expire-trash', value='DAYS')
|
|
@cli_help_section('File Manager Options')
|
|
def _expire_trash(self, opt: str, value: str):
|
|
"""the expire time for file to be trashed"""
|
|
self.trash_expire_time = int(value)
|
|
|
|
@cli_options('--expire-remove', value='DAYS')
|
|
@cli_help_section('File Manager Options')
|
|
def _expire_remove(self, opt: str, value: str):
|
|
"""the expire time for file in trash to be removed"""
|
|
self.trash_remove_expire_time = int(value)
|
|
|
|
@cli_options('--routine-trash', value='HOURS')
|
|
@cli_help_section('File Manager Options')
|
|
def _routine_trash(self, opt: str, value: str):
|
|
"""routine period for trash files"""
|
|
self.trash_routine_time = int(value)
|
|
|
|
@cli_flags('--disable-external-storage')
|
|
@cli_help_section('File Manager Options')
|
|
def _disable_external_storage(self, opt: str):
|
|
"""disable use external storage device"""
|
|
self.flag_disable_external_storage = True
|
|
|
|
@cli_options('-M', '--external-map', value='NAME:PATH')
|
|
@cli_help_section('File Manager Options')
|
|
def _external_path_map(self, opt: str, value: str):
|
|
"""map external storage path"""
|
|
f, t = part_suffix(value, ':')
|
|
self.external_path_map.append((f, t))
|
|
|
|
@cli_options('--websocket-address', value='URL[:PORT]')
|
|
@cli_help_section('Websocket Server')
|
|
def _server_websocket_address(self, opt: str, value: str):
|
|
"""set the websocket address and port"""
|
|
addr, port = part_suffix(value, ':', missing=self.DEFAULT_WEBSOCKET_PORT)
|
|
self.websocket_address = addr
|
|
self.websocket_port = int(port)
|
|
|
|
@cli_options('--websocket-port', value='PORT')
|
|
@cli_help_section('Websocket Server')
|
|
def _server_websocket_port(self, opt: str, value: str):
|
|
"""set the websocket port"""
|
|
self.websocket_port = int(value)
|
|
|
|
@cli_flags('--disable-led-control')
|
|
@cli_help_section('LED control')
|
|
def _disable_led_control(self, flag: str):
|
|
"""disable LED control"""
|
|
self.flag_disable_led_control = True
|
|
|
|
@cli_flags('--disable-led-refresh')
|
|
@cli_help_section('LED control')
|
|
def _disable_led_refresh(self, flag: str):
|
|
"""disable LED refresh simple color"""
|
|
self.flag_disable_led_refresh = True
|
|
|
|
@cli_options('--led-count', value='COUNT')
|
|
@cli_help_section('LED control')
|
|
def _led_count(self, opt: str, value: str):
|
|
"""set led count"""
|
|
self.led_count = int(value)
|
|
|
|
if not _RUNTIME_COMPILE:
|
|
@cli_flags('--disable-sha-check')
|
|
def _disable_sha_check(self, flag: str):
|
|
"""disable sha checking"""
|
|
self.flag_disable_sha_check = True
|
|
|
|
@cli_flags('--disable-ups-detect')
|
|
@cli_help_section('UPS control')
|
|
def _disable_ups_detect(self, flag: str):
|
|
"""disable UPS power use detect"""
|
|
self.flag_disable_ups_detect = True
|
|
|
|
@cli_options('--ups-wait-time', value='MIN')
|
|
@cli_help_section('UPS control')
|
|
def _ups_wait_time(self, opt: str, value: str):
|
|
"""ups wait time when low power"""
|
|
self.flag_ups_wait_time = int(value) * 60
|
|
|
|
|
|
class UserSession:
|
|
"""user session. It has unique session ID that the front-end application should keep
|
|
this session ID (for example, used as cookie), make server can keep the data during
|
|
the request.
|
|
|
|
To attach a data into session, just assign it as a session attribute.
|
|
|
|
|
|
"""
|
|
|
|
def __init__(self, user_session: Optional[str]):
|
|
if user_session is None:
|
|
user_session = str(uuid.uuid1())
|
|
else:
|
|
user_session = str(uuid.UUID(user_session))
|
|
|
|
self.__uuid = user_session
|
|
self.__time = time()
|
|
|
|
@property
|
|
def user_session(self) -> str:
|
|
return self.__uuid
|
|
|
|
def time(self, update=True) -> float:
|
|
ret = self.__time
|
|
if update:
|
|
self.__time = time()
|
|
return ret
|
|
|
|
@overload
|
|
def get_data(self, key: str, type_check: Type[T] = None) -> Optional[T]:
|
|
pass
|
|
|
|
@overload
|
|
def get_data(self, key: str, type_check: Type[T], init: Callable[[], T]) -> T:
|
|
pass
|
|
|
|
def get_data(self, key: str, type_check: Type[T] = None, init=None):
|
|
try:
|
|
ret = getattr(self, key)
|
|
|
|
if type_check is not None:
|
|
if not isinstance(ret, type_check):
|
|
raise TypeError
|
|
|
|
except (AttributeError, TypeError):
|
|
if init is None:
|
|
return None
|
|
|
|
ret = init()
|
|
|
|
setattr(self, key, ret)
|
|
|
|
return ret
|
|
|
|
def set_data(self, key: str, value: T) -> T:
|
|
setattr(self, key, value)
|
|
return value
|
|
|
|
def del_data(self, key: str):
|
|
try:
|
|
delattr(self, key)
|
|
except AttributeError:
|
|
pass
|
|
|
|
|
|
class HardwareInfo(JsonSerialize):
|
|
"""hardware information.
|
|
::
|
|
|
|
hardware_info = {
|
|
# general information
|
|
"name" : str
|
|
"version" : str
|
|
"build" :str
|
|
"vendor" :str
|
|
"copyright" :str
|
|
"web_site" :str
|
|
"device_ID" :str
|
|
"device_mac" :str?
|
|
"wifi_name" :str
|
|
}
|
|
|
|
"""
|
|
|
|
def __init__(self,
|
|
name: str,
|
|
spec = None,
|
|
version = None,
|
|
device_ID = None,
|
|
device_mac = None):
|
|
self.name = WIFI_NAME
|
|
|
|
self.spec = spec
|
|
'''server name'''
|
|
|
|
self.version = version if version else VERSION
|
|
'''software API version
|
|
|
|
**format**
|
|
|
|
::
|
|
|
|
major.minor.patch[-mark]
|
|
|
|
major
|
|
update with hardware
|
|
|
|
minor
|
|
update for software
|
|
|
|
patch
|
|
update with bug-fixing
|
|
|
|
mark
|
|
alpha, beta, dev, rc, ...
|
|
'''
|
|
|
|
self.build = REV_VERSION
|
|
'''software build version'''
|
|
|
|
self.vendor = VENDOR
|
|
'''software vendor'''
|
|
|
|
self.copyright = COPYRIGHT
|
|
'''software copyright'''
|
|
|
|
self.web_site = WEB_SITE
|
|
'''official web site'''
|
|
|
|
self.device_ID = device_ID
|
|
'''device ID'''
|
|
|
|
self.device_mac = device_mac
|
|
'''device mac address'''
|
|
|
|
self.wifi_name = WIFI_NAME
|
|
|
|
def as_json(self) -> JSON_OBJECT:
|
|
return {
|
|
'name': self.name,
|
|
'version': self.version,
|
|
'build': self.build,
|
|
'vendor': self.vendor,
|
|
'copyright': self.copyright,
|
|
'web_site': self.web_site,
|
|
'device_ID': self.device_ID,
|
|
'device_mac': self.device_mac,
|
|
'wifi_name': self.wifi_name,
|
|
}
|
|
|
|
def as_json_1(self) -> JSON_OBJECT:
|
|
return {
|
|
'name': self.name,
|
|
'spec': self.spec,
|
|
'software_version': self.build,
|
|
'mac_address': self.device_mac,
|
|
'wifi_name': self.wifi_name,
|
|
}
|
|
|
|
|
|
class ControlServerAPI(ControlAPI,
|
|
DeviceInternalCommandHandler,
|
|
metaclass=abc.ABCMeta):
|
|
TEMP_FILENAME_EXPR = '%t-%d'
|
|
|
|
__slots__ = ()
|
|
|
|
@property
|
|
def is_closed(self) -> bool:
|
|
return False
|
|
|
|
def interrupt(self, exception: BaseException) -> None:
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def options(self) -> ControlServerOptions:
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def file_manager(self) -> FileManager:
|
|
pass
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def device_manager(self) -> DeviceManager:
|
|
pass
|
|
|
|
def new_user_session(self) -> str:
|
|
"""new user session
|
|
|
|
:return: user uuid, use for *user_session* in other Relative Methods.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def del_user_session(self, user_session: Optional[str] = None):
|
|
"""delete user session
|
|
|
|
:param user_session: user session, None for all
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_user_session(self,
|
|
user_session: Optional[str],
|
|
new_session=False,
|
|
allow_empty=False) -> Optional[UserSession]:
|
|
"""get user session
|
|
|
|
:param user_session: user session ID
|
|
:param new_session: new a user session if *user_session* not found.
|
|
:param allow_empty: consider empty *user_session* None
|
|
:return: user session
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def expire_user_session(self):
|
|
"""expire user session
|
|
|
|
"""
|
|
# TODO who call this methods?
|
|
raise NotImplementedError()
|
|
|
|
def hardware_info(self) -> HardwareInfo:
|
|
raise NotImplementedError()
|
|
|
|
def hardware_datetime(self, content: Optional[str]) -> str:
|
|
return datetime_str()
|
|
|
|
def hardware_scan(self) -> bool:
|
|
self.device_manager.scan()
|
|
return True
|
|
|
|
def hardware_menu(self) -> Optional[Menu]:
|
|
return self.device_manager.get_menu()
|
|
|
|
def hardware_device(self) -> List[DeviceResponseInfo]:
|
|
return self.device_manager.found()
|
|
|
|
def hardware_device_found(self, content: Union[str, JSON_OBJECT, ADDRESS, DeviceInfo], *,
|
|
scan = False) -> Optional[DeviceResponseInfo]:
|
|
if isinstance(content, DeviceInfo):
|
|
response = content
|
|
else:
|
|
response = to_device_info(content)
|
|
|
|
if isinstance(scan, str):
|
|
scan = scan.upper() == 'TRUE'
|
|
|
|
if scan:
|
|
try:
|
|
return self.device_manager.scan_device(response)
|
|
except MasterDeviceError:
|
|
return None
|
|
else:
|
|
return self.device_manager.found_device(response)
|
|
|
|
def hardware_device_connect(self, content: Union[str, JSON_OBJECT, DeviceInfo]) -> CompletedDevice:
|
|
if isinstance(content, DeviceInfo):
|
|
response = content
|
|
else:
|
|
response = to_device_info(content)
|
|
|
|
return self.device_manager.connect(response)
|
|
|
|
def hardware_device_disconnect(self, device: Optional[int], force=False) -> bool:
|
|
if device is None:
|
|
self.device_manager.disconnect_all(force)
|
|
return True
|
|
else:
|
|
return self.device_manager.disconnect(device, force)
|
|
|
|
def hardware_send(self, command: str, **options: str) -> JSON:
|
|
return None
|
|
|
|
def get_device_info_all(self) -> List[CompletedDevice]:
|
|
return self.device_manager.list_device()
|
|
|
|
def get_device_info(self, device: int) -> Optional[CompletedDevice]:
|
|
return self.device_manager.get_device(device).as_json()
|
|
|
|
def device_instruction_all(self, device: int) -> List[str]:
|
|
return self.device_manager.list_device_instruction(device)
|
|
|
|
def device_all_instruction(self, instruction: str, content: str) -> bool:
|
|
for device in self.device_manager.list_device():
|
|
self.device_manager.device_instruction(device.device_id, instruction)
|
|
return True
|
|
|
|
def device_instruction(self, device: int, instruction: str) -> bool:
|
|
self.device_manager.device_instruction(device, instruction)
|
|
return True
|
|
|
|
def device_battery(self) -> List[int]:
|
|
device_list = self.device_manager.list_device()
|
|
if len(device_list) > 0:
|
|
battery_list = list(map(lambda x: self.device_manager.get_device(x).battery, device_list))
|
|
|
|
return battery_list
|
|
|
|
def device_parent(self, device: int, content: Optional[str] = None) -> List[int]:
|
|
if content is not None:
|
|
self.device_manager.get_device(device).update_parent_info(JsonSerialize.to_json(content))
|
|
|
|
return self.device_manager.get_device(device).parent
|
|
|
|
def device_recording_file_name(self, device: int, content: Optional[str] = None) -> List[int]:
|
|
if content is not None:
|
|
self.device_manager.get_device(device).update_recording_file_name_info(str(content))
|
|
|
|
return self.device_manager.get_device(device).recording_file_name
|
|
|
|
def device_sd_card_status(self, device:int) -> None:
|
|
return None
|
|
|
|
def device_update_calibration(self) -> None:
|
|
device_list = self.device_manager.list_device()
|
|
for x in device_list:
|
|
x.update_calibration_info('TDC4VC')
|
|
|
|
def device_update_radio_frequency(self, device: int, radio_frequency:int) -> bool:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.update_radio_frequency(radio_frequency)
|
|
return res
|
|
|
|
def device_update_led_brightness(self, device: int, led_brightness:int) -> bool:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.update_led_brightness(led_brightness)
|
|
return res
|
|
|
|
def device_update_accelerator_sensitivity(self, device: int, accelerator_sensitivity:int) -> bool:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.update_accelerator_sensitivity(accelerator_sensitivity)
|
|
return res
|
|
|
|
def device_set_central_rf(self, device: int, content: int) -> bool:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.central_rf_set(content)
|
|
console.log('set_central_ref', res)
|
|
|
|
def device_get_central_version(self, device: int) -> Optional[list]:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.central_version_get()
|
|
console.log('get_central_ref', res)
|
|
|
|
def device_parameter_last_update_timestamp(self, device: int) -> int:
|
|
d = self.device_manager.get_device(device)
|
|
return d.parameter.last_modified_timestamp
|
|
|
|
def device_parameter(self, device: int, parameter: str, content: Optional[str] = None) -> Any:
|
|
return self.device_manager.handle_device_parameter_command(device, parameter, content)
|
|
|
|
def device_parameter_value(self, device: int, parameter: str) -> Any:
|
|
return self.device_manager.handle_device_parameter_value(device, parameter)
|
|
|
|
def device_save_path(self, device: int, unset: bool) -> Optional[str]:
|
|
d = self.device_manager.get_device(device)
|
|
|
|
if d is None:
|
|
raise RuntimeError(DEVICE_NOT_FOUND, str(device))
|
|
|
|
if unset:
|
|
return self.file_manager.unset(device)
|
|
else:
|
|
info = self.file_manager.use(device)
|
|
|
|
if info.is_meta_file:
|
|
# do not used file_path which contain file extension
|
|
return info.parent_path + '/' + info.file_name
|
|
|
|
directory = info.file_path
|
|
|
|
content = format_filename(self.TEMP_FILENAME_EXPR, d)
|
|
|
|
if '/' in content:
|
|
return content
|
|
else:
|
|
return directory + '/' + content
|
|
|
|
def device_save_path_set(self, device: int, content: str, dry_run: bool) -> str:
|
|
d = self.device_manager.get_device(device)
|
|
|
|
content = re.sub("^ +", "", content)
|
|
content = re.sub(" +$", "", content)
|
|
content = re.sub(" +", "_", content)
|
|
|
|
ret = format_filename(content, d)
|
|
|
|
if dry_run:
|
|
return ret
|
|
|
|
else:
|
|
info = self.file_manager.use(device)
|
|
|
|
if info.is_meta_file:
|
|
directory = info.parent_path
|
|
else:
|
|
directory = info.file_path
|
|
|
|
if '/' in ret:
|
|
return ret
|
|
else:
|
|
return directory + '/' + ret
|
|
|
|
def device_history_get(self, user: Optional[str]) -> List[DeviceResponseInfo]:
|
|
return []
|
|
|
|
def device_history_found_all(self, user: str) -> List[DeviceResponseInfo]:
|
|
raise []
|
|
|
|
def device_history_found(self, user: str, device: int) -> Optional[DeviceResponseInfo]:
|
|
return None
|
|
|
|
def device_history_delete(self, user: str, device: Optional[int]) -> bool:
|
|
return True
|
|
|
|
def file_clean_storage(self) -> bool:
|
|
return self.file_manager.clean_storage()
|
|
|
|
def file_index(self) -> RootInfo:
|
|
return self.file_manager.index()
|
|
|
|
def file_list(self, path: str) -> DirectoryInfo:
|
|
return self.file_manager.list(path)
|
|
|
|
def file_list_range(self, path: str, start: int, end: int) -> DirectoryInfo:
|
|
return self.file_manager.list_range(path, start, end)
|
|
|
|
def file_rename(self, path: str, content: str) -> FileInfo:
|
|
return self.file_manager.rename(path, content)
|
|
|
|
def file_mkdir(self, path: str) -> DirectoryInfo:
|
|
return self.file_manager.mkdir(path)
|
|
|
|
def file_delete(self, path: str) -> bool:
|
|
return self.file_manager.delete(path)
|
|
|
|
def file_info(self, path: str) -> FileInfo:
|
|
return self.file_manager.info(path)
|
|
|
|
def file_meta_info(self, path: str) -> RecordingMetaFile:
|
|
return self.file_manager.meta(path)
|
|
|
|
def file_export_info(self, path: str) -> Dict[str, Optional[Path]]:
|
|
return JsonSerialize.to_json(self.file_manager.export_info(path))
|
|
|
|
def file_export_functions(self) -> Tuple[ExternalExportFunction, ...]:
|
|
return self.file_manager.get_export_functions()
|
|
|
|
def file_export_parameters(self, ftype: str) -> Tuple[ExternalParameter, ...]:
|
|
return self.file_manager.get_export_parameters(ftype)
|
|
|
|
def file_export(self, ftype: str, path: str, **options: str) -> str:
|
|
return '***********'
|
|
if isinstance(options, str):
|
|
options = self._file_process_options(options)
|
|
elif isinstance(options, dict):
|
|
pass
|
|
else:
|
|
raise RuntimeError('unknown parameter type : ' + options.__class__.__name__)
|
|
|
|
ret = self.file_manager.export_file(path, None, ftype, async_call=True, **options)
|
|
|
|
return str(ret)
|
|
|
|
@staticmethod
|
|
def _file_process_options(argv: str) -> Dict[str, Any]:
|
|
ret = {}
|
|
|
|
for arg in re.split('[&;\n]', argv):
|
|
k, v = part_suffix(arg, '=', missing=True)
|
|
|
|
k = k.replace('-', '_')
|
|
|
|
if isinstance(v, str):
|
|
try:
|
|
v = float(v)
|
|
except ValueError:
|
|
try:
|
|
v = int(v)
|
|
except ValueError:
|
|
pass
|
|
|
|
ret[k] = v
|
|
|
|
return ret
|
|
|
|
def file_segment(self, method: str, path: str) -> Union[None, bool, FileSegment]:
|
|
# ********
|
|
# path = '2020-05-04/2020-05-04-18-35-54-5_2_0.bprf'
|
|
if method == 'info':
|
|
return self.file_manager.segment_info(path)
|
|
|
|
elif method == 'new':
|
|
# print('*****4', path)
|
|
return self.file_manager.segment_info(path, create=True)
|
|
|
|
elif method == 'reload':
|
|
info = self.file_manager.segment_info(path)
|
|
|
|
if info is not None:
|
|
info.reload()
|
|
|
|
return True
|
|
|
|
elif method == 'invalid':
|
|
info = self.file_manager.segment_info(path)
|
|
|
|
if info is not None:
|
|
info.invalid()
|
|
|
|
return True
|
|
|
|
elif method == 'invalid_all':
|
|
FileSegment.invalid_all()
|
|
|
|
return True
|
|
|
|
else:
|
|
raise RuntimeError('unknown method : ' + method)
|
|
|
|
def file_segment_get(self, segment: int, path: str) -> FileSegmentContent:
|
|
info = self.file_manager.segment_info(path)
|
|
|
|
if info is None:
|
|
raise RuntimeError('file segment not create : ' + path)
|
|
|
|
return info.get_segment(segment)
|
|
|
|
def file_handle_open(self, content: str) -> Optional[FileHandle]:
|
|
return None
|
|
|
|
def file_handle_info(self, handle: int) -> Optional[FileHandle]:
|
|
return None
|
|
|
|
def file_handle_close(self, handle: int):
|
|
pass
|
|
|
|
def user_all(self) -> List[str]:
|
|
return self.file_manager.user_setting.user_list()
|
|
|
|
def user_info(self, user: str, *, user_session: Optional[str] = None) -> Optional[AbstractUserSetting]:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is not None and len(user_name) == 0:
|
|
user_name = None
|
|
|
|
if len(user) == '' and user_name is None:
|
|
# no user login
|
|
return None
|
|
|
|
elif len(user) == '':
|
|
# current user
|
|
user = user_name
|
|
|
|
info = self.file_manager.user_setting.user_info(user)
|
|
|
|
if user == user_name:
|
|
return info
|
|
else:
|
|
# other user, return restrict mode
|
|
return RestrictUserSetting(info)
|
|
|
|
def user_login(self, user: str, content: str, *,
|
|
user_session = None) -> ComplexResponse[AbstractUserSetting]:
|
|
if not self.file_manager.user_setting.user_validate(user, content):
|
|
raise RuntimeError(USER_INCORRECT_PASSWORD, user)
|
|
|
|
session = self.get_user_session(user_session, new_session=True, allow_empty=True)
|
|
session.user_name = user
|
|
|
|
info = self.file_manager.user_setting.user_info(user)
|
|
|
|
return response_with_user_session(info, session)
|
|
|
|
def user_logout(self, user_session: Optional[str] = None) -> None:
|
|
session = self.get_user_session(user_session)
|
|
|
|
if session is not None:
|
|
# remove user information from current session
|
|
session.del_data('user_name')
|
|
|
|
def user_add(self, user: str, content: str, *,
|
|
user_session = None) -> ComplexResponse[AbstractUserSetting]:
|
|
self.file_manager.user_setting.user_add(user, content)
|
|
|
|
session = self.get_user_session(user_session, new_session=True, allow_empty=True)
|
|
session.user_name = user
|
|
|
|
info = self.file_manager.user_setting.user_info(user)
|
|
|
|
return response_with_user_session(info, session)
|
|
|
|
def user_del(self, user: str, content: str, *,
|
|
user_session: Optional[str]) -> bool:
|
|
session = self.get_user_session(user_session, new_session=False, allow_empty=False)
|
|
|
|
if session is None:
|
|
return False
|
|
|
|
user_name = session.get_data('user_name')
|
|
|
|
if user_name is None:
|
|
return False
|
|
|
|
if user_name != user:
|
|
# only user can delete himself
|
|
# TODO or super-user can delete everyone
|
|
return False
|
|
|
|
if self.file_manager.user_setting.user_del(user, content):
|
|
# and also logout user
|
|
session.del_data("user_name")
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def user_sync(self, user_session: Optional[str]) -> bool:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
# no user login
|
|
return False
|
|
|
|
try:
|
|
self.file_manager.user_setting.user_sync(user_name)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def user_configuration_list(self, user_session: Optional[str], **options: str) -> List[str]:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return []
|
|
|
|
return self.file_manager.user_setting.configuration_filter(user_name, **options)
|
|
|
|
def user_configuration_get(self, name: str, user_session: Optional[str] = None) -> Optional[RecordingMetaFile]:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return None
|
|
|
|
return self.file_manager.user_setting.configuration_get(user_name, name)
|
|
|
|
def user_configuration_add(self, name: str, content: str, user_session: Optional[str] = None) -> bool:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return False
|
|
|
|
self.file_manager.user_setting.configuration_add(user_name, name, content)
|
|
return True
|
|
|
|
def user_configuration_del(self, name: str, user_session: Optional[str] = None) -> bool:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return False
|
|
|
|
return self.file_manager.user_setting.configuration_del(user_name, name)
|
|
|
|
def user_configuration_clone(self, name: str, content: str, user_session: Optional[str] = None) -> bool:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return False
|
|
|
|
from_user, new_name = part_prefix(content, ':')
|
|
|
|
return self.file_manager.user_setting.configuration_clone(user_name, name, new_name, from_user)
|
|
|
|
def user_setup_list(self, user_session: Optional[str] = None) -> List[str]:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return []
|
|
|
|
return self.file_manager.user_setting.setup_list(user_name)
|
|
|
|
def user_setup_info(self, name: str, user_session: Optional[str] = None) -> Optional[UserSetup]:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return None
|
|
|
|
return self.file_manager.user_setting.setup_info(user_name, name)
|
|
|
|
def user_setup_new(self, name: str, user_session: Optional[str] = None) -> Optional[UserSetup]:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return None
|
|
|
|
return self.file_manager.user_setting.setup_new(user_name, name)
|
|
|
|
def user_setup_del(self, name: str, user_session: Optional[str] = None) -> bool:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return False
|
|
|
|
return self.file_manager.user_setting.setup_del(user_name, name)
|
|
|
|
def user_alias_list(self, user_session: Optional[str] = None) -> List[UserDeviceAlias]:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return []
|
|
|
|
return self.file_manager.user_setting.alias_list(user_name)
|
|
|
|
def user_alias_info(self, name: str, user_session: Optional[str] = None) -> Optional[UserDeviceAlias]:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return None
|
|
|
|
user_setting = self.file_manager.user_setting
|
|
|
|
try:
|
|
address = str_address(name)
|
|
except ValueError:
|
|
return user_setting.alias_info(user_name, name)
|
|
else:
|
|
return user_setting.alias_info(user_name, address)
|
|
|
|
def user_alias_edit(self, name: str, content: str, user_session: Optional[str] = None) -> Optional[UserDeviceAlias]:
|
|
user_name = self._session_user_name(user_session)
|
|
|
|
if user_name is None:
|
|
return None
|
|
|
|
user_setting = self.file_manager.user_setting
|
|
address = None
|
|
|
|
try:
|
|
address = str_address(name)
|
|
except ValueError:
|
|
alias = user_setting.alias_info(user_name, name)
|
|
else:
|
|
alias = user_setting.alias_info(user_name, address)
|
|
|
|
if alias is None:
|
|
# new alias
|
|
if address is None:
|
|
return None
|
|
|
|
alias = user_setting.alias_new(user_name, address)
|
|
|
|
# try to give device name
|
|
device = self.hardware_device_found(address)
|
|
if device is not None:
|
|
alias.device_name = device.device_name
|
|
|
|
else:
|
|
address = alias.device_address
|
|
|
|
if len(content) == 0:
|
|
# delete alias
|
|
user_setting.alias_del(user_name, address)
|
|
return None
|
|
|
|
# update alias name
|
|
alias.device_alias = content
|
|
|
|
return alias
|
|
|
|
def exp_pro_list(self) -> List[ExpProtocolInfo]:
|
|
raise NotImplementedError()
|
|
|
|
def exp_pro_create(self, content: str, *, user_session: Optional[str] = None) -> int:
|
|
raise NotImplementedError()
|
|
|
|
def exp_pro_options(self, protocol: int) -> List[ExpOption]:
|
|
raise NotImplementedError()
|
|
|
|
def exp_pro_option(self, protocol: int, option: str, content: Optional[str] = None) -> bool:
|
|
raise NotImplementedError()
|
|
|
|
def exp_pro_status_all(self) -> List[AbstractExpProtocolState]:
|
|
raise NotImplementedError()
|
|
|
|
def exp_pro_status(self, protocol: int) -> Optional[ExpProtocolState]:
|
|
raise NotImplementedError()
|
|
|
|
def exp_pro_command(self, protocol: int, content: str) -> bool:
|
|
raise NotImplementedError()
|
|
|
|
def broadcast_command(self, command: str, device: Optional[int] = None):
|
|
"""broadcast command to clients
|
|
|
|
:param command:
|
|
:param device:
|
|
:return:
|
|
"""
|
|
pass
|
|
|
|
def data_request_stream(self, start: bool, handler: DataMessageHandler):
|
|
"""
|
|
|
|
:param start:
|
|
:param handler:
|
|
:return:
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def websocket_command(self, command: str) -> Union[None, str, bytes, List[str]]:
|
|
"""
|
|
|
|
:param command:
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def websocket_data_request(self, handler: DataMessageHandler, command: Optional[str]):
|
|
"""
|
|
|
|
:param handler:
|
|
:param command:
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_led_state(self) -> str:
|
|
return LED.AVAILABLE
|
|
|
|
def set_led_state(self, state: str):
|
|
pass
|
|
|
|
# thread
|
|
|
|
@abc.abstractmethod
|
|
def submit_thread(self, f, *args):
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def run_thread(self, thread: T_ServerThread) -> T_ServerThread:
|
|
pass
|
|
|
|
def submit_promise(self, name: str, f: Callable[..., T], *args) -> Promise[T]:
|
|
promise = Promise(name)
|
|
|
|
def _promise():
|
|
try:
|
|
ret = f(*args)
|
|
except BaseException as e:
|
|
print_exception()
|
|
promise.reject(e)
|
|
else:
|
|
promise.complete(ret)
|
|
|
|
self.submit_thread(_promise)
|
|
|
|
return promise
|
|
|
|
def restart_server(self) -> None:
|
|
raise NotImplementedError()
|
|
|
|
# utility function
|
|
|
|
def _session_user_name(self, user_session: Optional[str]) -> Optional[str]:
|
|
"""get the name of the current login-ed user
|
|
|
|
:param user_session: cookie
|
|
:return: the name of the current login-ed user
|
|
"""
|
|
if user_session is None:
|
|
return None
|
|
|
|
session = self.get_user_session(user_session)
|
|
return session.get_data('user_name')
|
|
|
|
|
|
def response_with_user_session(response: T, session: UserSession) -> ComplexResponse[T]:
|
|
ret = ComplexResponse(response)
|
|
ret.set_cookie(user_session=session.user_session)
|
|
|
|
return ret
|
|
|
|
|
|
def include_session(f: Callable[..., T]) -> Callable[..., ComplexResponse[T]]:
|
|
"""**decorator**"""
|
|
signature = inspect.signature(f)
|
|
|
|
@wraps(f)
|
|
def _inner(self, *args, **kwargs):
|
|
bind = signature.bind(self, *args, **kwargs)
|
|
|
|
user_session = bind.arguments['user_session']
|
|
|
|
session = self.get_user_session(user_session, new_session=True, allow_empty=True)
|
|
|
|
bind.arguments['user_session'] = session
|
|
|
|
ret = f(*bind.args, **bind.kwargs)
|
|
|
|
return response_with_user_session(ret, session)
|
|
|
|
return _inner
|
|
|
|
|
|
def format_filename(expr: str, device: CompletedDevice) -> str:
|
|
t = datetime.now()
|
|
|
|
def _mapper(flag: str) -> Optional[str]:
|
|
if flag == 't':
|
|
return '%4d-%02d-%02d-%02d-%02d-%02d' % (t.year, t.month, t.day, t.hour, t.minute, t.second)
|
|
elif flag == 'Td':
|
|
return '%4d-%02d-%02d' % (t.year, t.month, t.day)
|
|
elif flag == 'Tt':
|
|
return '%02d:%02d:%02d' % (t.hour, t.minute, t.second)
|
|
elif flag == 'TY':
|
|
return '%4d' % t.year
|
|
elif flag == 'Tn':
|
|
return '%d' % t.month
|
|
elif flag == 'TN':
|
|
return ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][t.month]
|
|
elif flag == 'Tw':
|
|
return '%d' % t.isoweekday()
|
|
elif flag == 'TW':
|
|
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][t.weekday()]
|
|
elif flag == 'TD':
|
|
return '%d' % t.day
|
|
elif flag == 'Th':
|
|
return '%d' % (t.hour % 12)
|
|
elif flag == 'TH':
|
|
return '%d' % t.hour
|
|
elif flag == 'Tm':
|
|
return '%d' % t.minute
|
|
elif flag == 'Ts':
|
|
return '%d' % t.second
|
|
elif flag == 'd':
|
|
return '%d' % device.device_id
|
|
elif flag == 'Dn':
|
|
return device.device_name
|
|
elif flag == 'Dm':
|
|
return address_str(device.mac_address)
|
|
elif flag == 'Dt':
|
|
return device.library_name
|
|
else:
|
|
return None
|
|
|
|
return expr_format(expr, _mapper)
|
|
|
|
|
|
def to_device_info(content: Union[str, ADDRESS, JSON_OBJECT]) -> DeviceResponseInfo:
|
|
if is_address_type(content):
|
|
return DeviceResponseInfo(None,
|
|
serial_number=DeviceSerialNumber.UNKNOWN, # match all
|
|
mac_address=content)
|
|
|
|
if isinstance(content, str):
|
|
json = json_parse(content)
|
|
|
|
if isinstance(json, str):
|
|
json = {'device_address': json}
|
|
else:
|
|
json = content
|
|
|
|
try:
|
|
device_address = json['device_address']
|
|
except KeyError as e:
|
|
raise RuntimeError(DEVICE_NOT_FOUND) from e
|
|
|
|
address = str_address(device_address)
|
|
serial = json.get('serial_number', None)
|
|
|
|
if not isinstance(serial, list):
|
|
# illegal format
|
|
raise RuntimeError(DEVICE_NOT_FOUND)
|
|
|
|
if serial is not None:
|
|
serial = DeviceSerialNumber(*serial)
|
|
|
|
return DeviceResponseInfo(None,
|
|
serial_number=serial,
|
|
mac_address=address)
|
|
|
|
def run_project(self, project) -> bool:
|
|
return False
|