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)