1381 lines
46 KiB
Python
1381 lines
46 KiB
Python
from time import sleep
|
|
from typing import Iterable
|
|
from datetime import datetime
|
|
|
|
import biopro.impl.vcgencmd as vcg
|
|
from biopro.data import DataServerOptions, DataAPI
|
|
from biopro.device.manager import DeviceManagerOptions, DeviceHardwareInterface
|
|
from biopro.devlib.data import DataDecodeFormat, RawDataDecoder
|
|
from biopro.devlib.device import DeviceCommandAction, DeviceCommonInstruction
|
|
from biopro.devlib.instruction import InternalInstruction, DeviceInstructionError
|
|
# from biopro.exp_pro.manager import ExpManagerOption, ExpManager
|
|
from biopro.file.manager import FileOptions
|
|
from biopro.message import *
|
|
from biopro.util.address import address_str
|
|
from biopro.util.datetime import *
|
|
from biopro.util.logger import logging_info, logging_verbose
|
|
from biopro.util.sleep import long_sleep
|
|
from biopro.util.sys import system_call, current_command_header
|
|
from .cache import CacheAPI, CacheNotifyHandler, CacheManager
|
|
from .controller import *
|
|
from .data import DataServerProcess
|
|
from .data_stream import DataStreamThread
|
|
from .led import LEDControlThread
|
|
from .mqtt import MqttThread
|
|
from .connect import RoutineConnectDeviceThread
|
|
from .socket import SocketServer, ServerThread, ServerShutdownException
|
|
from .trash import RoutineTrashThread
|
|
from .ups import UPSDetectThread
|
|
from .api import ApiRequests
|
|
from biopro.api.auth import AuthAPI
|
|
from biopro.api.controller import ControllerAPI
|
|
from biopro.api.device import DeviceAPI
|
|
from biopro.project.project_manager import ProjectManager
|
|
### do not import Websocket mode
|
|
###from .websocket import WebsocketThread
|
|
|
|
_RUNTIME_COMPILE = False
|
|
|
|
T = TypeVar('T')
|
|
|
|
|
|
def _fc(c):
|
|
return c % len(FORE_COLOR) + FORE_COLOR[0]
|
|
|
|
|
|
class ControlServer(SocketServer, ControlServerAPI):
|
|
"""Data Server"""
|
|
|
|
def __init__(self, *options):
|
|
"""create data server.
|
|
|
|
**Server Options**
|
|
|
|
accept
|
|
|
|
1. :class:`biopro.server.controller.ControlServerOptions`
|
|
#. :class:`biopro.data.DataServerOptions`
|
|
#. :class:`biopro.file._api.FileOptions`
|
|
#. :class:`biopro.device.manager.DeviceManagerOptions`
|
|
|
|
:param options: server options
|
|
"""
|
|
super().__init__((ControlAPI), 'Controller', self.ADDR,
|
|
socket_listen=10,
|
|
thread_pool_count=16)
|
|
|
|
# controller ID
|
|
from ._identify import controller_device_id
|
|
self.__controller_ID = controller_device_id()
|
|
|
|
# auth set jwt
|
|
AuthAPI.set_key()
|
|
|
|
# controller server
|
|
self._controller_options = c = ControlServerOptions.get_options(*options)
|
|
|
|
# file managers
|
|
self._file_manager = self._init_file_manager(c)
|
|
self._file_save_cache = {}
|
|
|
|
# device manager
|
|
device_option = DeviceManagerOptions.get_options(*options)
|
|
self._device_manager = DeviceManager(device_option, self)
|
|
|
|
# data server
|
|
data_options = DataServerOptions.get_options(*options)
|
|
data_options.repository = device_option.repository
|
|
self.data_server = DataServerProcess(data_options)
|
|
|
|
# remote server / MQTT
|
|
self.mqtt_thread = None
|
|
if not c.flag_disable_remote_server:
|
|
self.mqtt_thread = MqttThread(self,
|
|
self.socket_mqtt_ip,
|
|
self.socket_mqtt_port,
|
|
'control_server',
|
|
self.log_verbose,
|
|
'MQTT-control-server')
|
|
|
|
# LED control
|
|
self.led_thread = LEDControlThread(disable=c.flag_disable_led_control,
|
|
led_count=c.led_count,
|
|
refresh_mode=not c.flag_disable_led_refresh)
|
|
|
|
# device detect
|
|
self._detect_device_thread = RoutineConnectDeviceThread(self._device_manager, self.mqtt_thread, self.led_thread, 3)
|
|
|
|
self._project_manager = ProjectManager(self.device_manager, self.mqtt_thread)
|
|
|
|
if not c.flag_disable_led_control:
|
|
self.led_thread.set_state(LED.COLOR_RED)
|
|
|
|
# UPS detect
|
|
self.ups_thread = None
|
|
if not c.flag_disable_ups_detect:
|
|
self.log_verbose('UPS_ENABLE', True)
|
|
self.ups_thread = UPSDetectThread(self, c.flag_ups_wait_time)
|
|
|
|
# user session
|
|
self._session = {}
|
|
self._user_session_expire = c.user_session_expire_days * 24 * 60 * 60 # sec
|
|
|
|
# indicate those device together
|
|
self._device_instruction_group = None
|
|
|
|
# scan max duration
|
|
self._scan_max_timeout = 5
|
|
|
|
# _api request build
|
|
self._api = ApiRequests()
|
|
|
|
# init method
|
|
|
|
def _init_file_manager(self, option: ControlServerOptions) -> FileManager:
|
|
"""initialize file manager
|
|
|
|
:param option: control server cli options
|
|
:return: file manager instance
|
|
"""
|
|
r = Path(option.storage_root).absolute()
|
|
|
|
if not r.exists():
|
|
self.log_info('create storage root', r)
|
|
r.mkdir(parents=True)
|
|
|
|
file_manager_options = FileOptions.with_default_structure(r)
|
|
|
|
e = option.export_root
|
|
if e is not None:
|
|
file_manager_options.export_root = str(Path(e).absolute())
|
|
|
|
# create manager
|
|
return FileManager(file_manager_options)
|
|
|
|
# abstract property override
|
|
|
|
@property
|
|
def options(self) -> ControlServerOptions:
|
|
return self._controller_options
|
|
|
|
@property
|
|
def file_manager(self) -> FileManager:
|
|
return self._file_manager
|
|
|
|
@property
|
|
def device_manager(self) -> DeviceManager:
|
|
return self._device_manager
|
|
|
|
@property
|
|
def project_manager(self) -> ProjectManager:
|
|
return self._project_manager
|
|
|
|
|
|
# server setup
|
|
|
|
def setup(self) -> None:
|
|
"""setup the control server
|
|
"""
|
|
# print allow host
|
|
from biopro.util.inet import allow_host
|
|
|
|
for host in allow_host():
|
|
self.log_info('allow_host', host)
|
|
|
|
# UPS
|
|
if self.ups_thread is not None:
|
|
self.run_thread(self.ups_thread)
|
|
|
|
# LED
|
|
self.run_thread(self.led_thread)
|
|
self.led_thread.set_state(LED.POWER_ON)
|
|
|
|
# sha checking
|
|
# if _RUNTIME_COMPILE or not self._controller_options.flag_disable_sha_check:
|
|
# self.run_thread(_RandomCrashThread())
|
|
|
|
# data server
|
|
interface = self.device_manager.interface
|
|
options = self.data_server.options
|
|
if interface == DeviceHardwareInterface.INTERFACE_DUMMY:
|
|
options.flag_spi_mode = DataAPI.MODE_DISABLE
|
|
elif interface == DeviceHardwareInterface.INTERFACE_CC2650_S:
|
|
options.flag_spi_mode = DataAPI.MODE_SINGLE
|
|
|
|
options.storage_root = str(self.file_manager.storage_root)
|
|
|
|
self.data_server.start_process()
|
|
|
|
# file manager
|
|
self.log_verbose('setup file manager')
|
|
self.file_manager.setup_storage()
|
|
self.file_manager.update_storage()
|
|
self.submit_thread(self._setup_file_manager)
|
|
|
|
# device manager
|
|
self.log_verbose('setup device manager')
|
|
self.device_manager.setup()
|
|
self.submit_thread(self._setup_library)
|
|
|
|
# device manager reset
|
|
self.submit_thread(self._setup_device)
|
|
|
|
self._setup_controller()
|
|
|
|
# self.run_thread(self._detect_device_thread)
|
|
|
|
# broadcast greeting
|
|
self.broadcast_command(DataMessage.greeting())
|
|
|
|
# remote server
|
|
if self.mqtt_thread is not None:
|
|
self.mqtt_thread.start()
|
|
|
|
def _setup_file_manager(self):
|
|
self.log_verbose('setup file manager')
|
|
|
|
# file manager clean external directory
|
|
self.file_manager.umount_external()
|
|
|
|
if not self._controller_options.flag_disable_external_storage:
|
|
# file manager mount external storage
|
|
self.file_manager.mount_external_auto()
|
|
|
|
if len(self._controller_options.external_path_map):
|
|
for f, t in self._controller_options.external_path_map:
|
|
self.file_manager.mount_external(f, Path(t))
|
|
|
|
self.log_verbose('cache root info')
|
|
### init file info get
|
|
# self.file_manager.info('').as_json()
|
|
# self.file_manager.list('').as_json()
|
|
return
|
|
|
|
def _setup_library(self):
|
|
self.log_verbose('reload library')
|
|
self.device_manager.reload_library()
|
|
|
|
def _setup_controller(self):
|
|
hardware_info = self.hardware_info().as_json_1()
|
|
|
|
from ._identify import controller_device_id
|
|
mqtt_id = controller_device_id()
|
|
|
|
hardware_info['mqtt_id'] = mqtt_id
|
|
hardware_info['status'] = 'running'
|
|
|
|
controller = ControllerAPI.getByMac(hardware_info['mac_address'])
|
|
|
|
if len(controller) == 0:
|
|
ControllerAPI.create(hardware_info)
|
|
elif len(controller) > 0:
|
|
if hardware_info['mac_address'] != controller[0]['mac_address'] or hardware_info['software_version'] != controller[0]['software_version'] or hardware_info['mqtt_id'] != controller[0]['mqtt_id'] or hardware_info['wifi_name'] != controller[0]['wifi_name']:
|
|
ControllerAPI.updateByAttrs (
|
|
'mac_address', hardware_info['mac_address'], {
|
|
'software_version': hardware_info['software_version'],
|
|
'mqtt_id': mqtt_id,
|
|
'name': hardware_info['name'],
|
|
'wifi_name': hardware_info['wifi_name']
|
|
}
|
|
)
|
|
|
|
def _setup_device(self):
|
|
# wait data server
|
|
|
|
self.log_verbose('device hardware reset')
|
|
self.device_manager.reset(software_reset=False)
|
|
|
|
self.log_verbose('wait data server')
|
|
if self.device_manager.interface == DeviceHardwareInterface.INTERFACE_DUMMY:
|
|
sleep(1)
|
|
else:
|
|
sleep(3)
|
|
|
|
try:
|
|
available_channel = self._setup_get_available_channel()
|
|
|
|
if available_channel is None:
|
|
self.device_manager.setup(DeviceHardwareInterface.INTERFACE_DUMMY)
|
|
else:
|
|
self.broadcast_command('state:device_available')
|
|
|
|
except ServerShutdownException as e:
|
|
self.interrupt(e)
|
|
return
|
|
|
|
except RuntimeError as e:
|
|
self.log_warn('reset fail', ':', str(e))
|
|
print_exception(self)
|
|
|
|
if self._controller_options.shutdown_if_reset_fail:
|
|
self.interrupt(ServerShutdownException())
|
|
return
|
|
|
|
self._setup_restart_device_manager(DeviceHardwareInterface.INTERFACE_DUMMY)
|
|
|
|
self._setup_reset_data_server()
|
|
|
|
except:
|
|
print_exception(self)
|
|
|
|
finally:
|
|
self.led_thread.set_state(LED.AVAILABLE)
|
|
|
|
def _setup_get_available_channel(self) -> Optional[List[int]]:
|
|
client = self.data_server.client()
|
|
|
|
if client is None:
|
|
raise RuntimeError('data server not available')
|
|
|
|
retry = 0
|
|
while retry < 10:
|
|
try:
|
|
with client:
|
|
client.reset()
|
|
break
|
|
except RuntimeError:
|
|
self.log_verbose('wait data server', 'x%d' % retry)
|
|
sleep(1)
|
|
retry += 1
|
|
else:
|
|
raise RuntimeError('data server not available')
|
|
|
|
# reset data server
|
|
with client:
|
|
data_spi_mode = client.spi_mode()
|
|
self.log_verbose('data server spi mode', data_spi_mode)
|
|
|
|
available_channel = client.available_channel()
|
|
|
|
# check channel
|
|
if len(available_channel) == 0:
|
|
if data_spi_mode == DataAPI.MODE_DISABLE:
|
|
return None
|
|
else:
|
|
raise RuntimeError('empty channel')
|
|
|
|
expect_channel = self._controller_options.expect_available_channel
|
|
if expect_channel is not None:
|
|
self.log_verbose('expect channel', expect_channel)
|
|
|
|
retry = 0
|
|
while available_channel != expect_channel and retry < 3:
|
|
sleep(1)
|
|
with client:
|
|
available_channel = client.available_channel(reset=True)
|
|
|
|
retry += 1
|
|
|
|
self.log_verbose('available channel', available_channel)
|
|
return available_channel
|
|
|
|
def _setup_restart_device_manager(self, interface: str):
|
|
self.log_info('shutdown device manager')
|
|
self.device_manager.shutdown()
|
|
# self.broadcast_command('shutdown')
|
|
|
|
self.log_info('restart device manager with interface', interface)
|
|
self.device_manager.setup(interface)
|
|
|
|
def _setup_reset_data_server(self):
|
|
"""change data server spi mode according device manager hardware interface"""
|
|
|
|
spi_mode = None
|
|
interface = self.device_manager.interface
|
|
|
|
if interface == DeviceHardwareInterface.INTERFACE_DUMMY:
|
|
spi_mode = DataAPI.MODE_DISABLE
|
|
|
|
elif interface == DeviceHardwareInterface.INTERFACE_CC2650_S:
|
|
spi_mode = DataAPI.MODE_SINGLE
|
|
|
|
if spi_mode is not None:
|
|
self.log_verbose('reset data server to mode', spi_mode)
|
|
|
|
client = self.data_server.client()
|
|
|
|
if client is not None:
|
|
with client:
|
|
client.reset(spi_mode)
|
|
|
|
# server close
|
|
|
|
def close(self, external_interrupt: bool):
|
|
self.broadcast_command('shutdown')
|
|
self.data_server.stop_process(external_interrupt)
|
|
|
|
self.device_manager.shutdown()
|
|
|
|
self.file_manager.shutdown(umount_external=True)
|
|
|
|
self.led_thread.close()
|
|
|
|
if self.mqtt_thread is not None:
|
|
self.mqtt_thread.shutdown()
|
|
|
|
def _post_device_list(self) -> List[CompletedDevice]:
|
|
"""general post action for getting list of connected devices.
|
|
|
|
:return: connected device list
|
|
"""
|
|
ret = self.device_manager.list_device()
|
|
|
|
if len(ret) == 0:
|
|
# no connected device
|
|
self.led_thread.set_state(LED.AVAILABLE)
|
|
|
|
else:
|
|
self.led_thread.set_state(LED.CONNECTED)
|
|
|
|
return ret
|
|
|
|
def _post_device_connected(self, device: Optional[int]):
|
|
"""general post action for any device has connected.
|
|
|
|
:param device: device connected
|
|
"""
|
|
self.led_thread.set_state(LED.CONNECTED)
|
|
self.broadcast_command(DataMessage.connected(device))
|
|
|
|
def _post_device_disconnected(self, device: Optional[int]):
|
|
"""general post action for any device has disconnected.
|
|
|
|
:param device: device disconnected.
|
|
"""
|
|
client = self.data_server.client()
|
|
if client is not None:
|
|
with client:
|
|
if client.is_sync(device):
|
|
client.stop_sync(device)
|
|
|
|
if len(self.device_manager.list_device()) == 0:
|
|
self.led_thread.set_state(LED.AVAILABLE)
|
|
|
|
data = {
|
|
"connect_status": 'idle'
|
|
}
|
|
self._api.updateDeviceByAttr('connect_status', 'update', data)
|
|
|
|
self.broadcast_command(DataMessage.disconnected(device))
|
|
# self.broadcast_command('device/disconnected/' + str(device))
|
|
|
|
# server provide method implement : hardware information
|
|
|
|
@logging_info
|
|
def hardware_info(self) -> HardwareInfo:
|
|
return HardwareInfo(
|
|
name=self.server_name,
|
|
device_ID=self.__controller_ID,
|
|
device_mac=self._controller_mac_address()
|
|
)
|
|
|
|
def _controller_mac_address(self) -> Optional[str]:
|
|
from biopro.util.inet import ip_addr
|
|
|
|
if _RUNTIME_COMPILE:
|
|
interface_list = ip_addr('eth0')
|
|
else:
|
|
interface_list = ip_addr()
|
|
|
|
for interface in interface_list:
|
|
if _RUNTIME_COMPILE:
|
|
if interface.interface == 'eth0':
|
|
break
|
|
else:
|
|
if interface.interface.startswith('e'):
|
|
break
|
|
else:
|
|
return None
|
|
|
|
return interface.link_addr
|
|
|
|
def hardware_datetime(self, content: Optional[str]) -> str:
|
|
if content is not None:
|
|
frontend_time = datetime.fromtimestamp(int(content) / 1000)
|
|
local_time = datetime.now()
|
|
|
|
if frontend_time > local_time:
|
|
self.log_verbose('timestamp update', local_time, frontend_time, frontend_time-local_time)
|
|
set_datetime(frontend_time)
|
|
# d = str_datetime(content)
|
|
# print('d',d)
|
|
# d = datetime
|
|
# # set System datetime
|
|
# self.file_manager.update_storage()
|
|
|
|
return datetime_str()
|
|
|
|
@logging_info
|
|
def hardware_menu(self) -> Optional[Menu]:
|
|
return super().hardware_menu()
|
|
|
|
# server provide method implement : hardware functions
|
|
|
|
@logging_info
|
|
def hardware_scan(self) -> bool:
|
|
self.hardware_scan_device()
|
|
return True
|
|
|
|
def hardware_scan_device(self):
|
|
self.broadcast_command(DataMessage.scan(''))
|
|
|
|
device_not_found = True
|
|
|
|
# we want once find any device, update the information ASAP by broadcasting
|
|
def callback(_: DeviceResponseInfo):
|
|
nonlocal device_not_found
|
|
|
|
device_not_found = False
|
|
self.broadcast_command(DataMessage.scan('update'))
|
|
|
|
if self.device_manager.scan_callback(callback, timeout=self._scan_max_timeout, all_device=True):
|
|
self.broadcast_command(DataMessage.scan('done'))
|
|
|
|
# if device_not_found:
|
|
# # no device found, longer the max scan time
|
|
# self._scan_max_timeout = min(10, self._scan_max_timeout + 2)
|
|
|
|
@logging_info
|
|
def hardware_device(self) -> List[DeviceResponseInfo]:
|
|
return self.device_manager.found()
|
|
|
|
@logging_info
|
|
def hardware_device_found(self, content: Union[str, JSON_OBJECT, DeviceInfo], *,
|
|
scan = False) -> Optional[DeviceResponseInfo]:
|
|
return super().hardware_device_found(content, scan=scan)
|
|
|
|
@logging_info
|
|
def hardware_device_connect(self, content: Union[str, JSON_OBJECT, DeviceInfo]) -> CompletedDevice:
|
|
if isinstance(content, DeviceResponseInfo):
|
|
response = content
|
|
else:
|
|
response = to_device_info(content)
|
|
|
|
try:
|
|
ret = self.device_manager.connect(response)
|
|
except DeviceInstructionError as e:
|
|
'''when device reset fail error'''
|
|
return None
|
|
else:
|
|
if ret is not None:
|
|
# update device status by id
|
|
DeviceAPI.updateByMac(
|
|
address_str(ret.mac_address),
|
|
{
|
|
'battery': ret.battery,
|
|
'connect_id': ret.device_id,
|
|
'connect_status': 'connect',
|
|
'device_version': ret.device_version,
|
|
'library': ret.library_name,
|
|
'library_version': ret.library_version,
|
|
}
|
|
)
|
|
self._post_device_connected(ret.device_id)
|
|
|
|
return ret
|
|
|
|
def hardware_device_direct_connect(self, content: Union[str, JSON_OBJECT, DeviceInfo]) -> CompletedDevice:
|
|
if isinstance(content, DeviceResponseInfo):
|
|
response = content
|
|
else:
|
|
response = to_device_info(content)
|
|
|
|
try:
|
|
ret = self.device_manager.connect(response)
|
|
except DeviceInstructionError as e:
|
|
'''when device reset fail error'''
|
|
return None
|
|
else:
|
|
if ret is not None:
|
|
self._post_device_connected(ret.device_id)
|
|
|
|
return ret
|
|
|
|
@logging_info
|
|
def hardware_device_disconnect(self, device: Optional[int], force=False) -> bool:
|
|
connect_device = self.device_manager.get_device(device)
|
|
|
|
DeviceAPI.updateByMac(
|
|
address_str(connect_device.mac_address),
|
|
{
|
|
'connect_id': -1,
|
|
'connect_status': 'idle',
|
|
'auto_connect': False,
|
|
}
|
|
)
|
|
|
|
if device is None:
|
|
self.device_manager.disconnect_all(force)
|
|
ret = True
|
|
else:
|
|
ret = self.device_manager.disconnect(device, force)
|
|
|
|
data = {
|
|
"connect_status": 'idle',
|
|
"connect_id": -1,
|
|
"connect_time": 0,
|
|
"auto_connect": False
|
|
}
|
|
self._api.updateDeviceByAttr('connect_id', str(device), data)
|
|
|
|
self._post_device_disconnected(device)
|
|
|
|
return ret
|
|
|
|
@logging_info
|
|
def hardware_send(self, command: str, **options: str):
|
|
# hardware command for development mode
|
|
if command == 'led':
|
|
if not _RUNTIME_COMPILE:
|
|
return self._hardware_send_led(options)
|
|
|
|
elif command == 'power_off':
|
|
return self._hardware_send_power_off(options)
|
|
|
|
elif command == 'test':
|
|
if not _RUNTIME_COMPILE:
|
|
try:
|
|
return self._hardware_send_test(options)
|
|
except:
|
|
print_exception(self)
|
|
|
|
elif command == 'test_set':
|
|
if not _RUNTIME_COMPILE:
|
|
|
|
try:
|
|
return self._hardware_send_test_set(options)
|
|
except:
|
|
print_exception(self)
|
|
|
|
elif command == 'device':
|
|
try:
|
|
return self._hardware_send_device(options)
|
|
except:
|
|
print_exception(self)
|
|
|
|
elif command == 'measure':
|
|
try:
|
|
return self._hardware_send_measure(options['item'])
|
|
except:
|
|
pass
|
|
|
|
else:
|
|
ret = self.device_manager.handle_command(None, command, options)
|
|
self._post_hardware_send(command, None, ret)
|
|
|
|
return None
|
|
|
|
def _hardware_send_led(self, options: Dict[str, str]) -> bool:
|
|
state = options.get('state', None)
|
|
|
|
if state is not None:
|
|
self.led_thread.set_state(state)
|
|
|
|
return True
|
|
|
|
def _hardware_send_power_off(self, options: Dict[str, str]) -> bool:
|
|
s = options.get('s', 0)
|
|
|
|
try:
|
|
s = int(s)
|
|
except ValueError:
|
|
s = 0
|
|
|
|
def _power_off(_sec: int):
|
|
if not long_sleep(_sec, lambda: self.is_closed):
|
|
UPSDetectThread.power_off()
|
|
|
|
self.submit_thread(_power_off, s)
|
|
|
|
return True
|
|
|
|
def _hardware_send_measure(self, item: str) -> Union[int, float]:
|
|
if item == 'temp':
|
|
return vcg.measure_temp()
|
|
|
|
else:
|
|
raise RuntimeError('unknown measure item')
|
|
|
|
def _hardware_send_device(self, options: Dict[str, str]) -> JSON:
|
|
command = options.get('manager', None)
|
|
|
|
if command is not None:
|
|
# manager command
|
|
|
|
if command == 'all':
|
|
# noinspection PyTypeChecker
|
|
return ['all', 'reload']
|
|
|
|
elif command == 'reload':
|
|
self.device_manager.reload_library()
|
|
return True
|
|
|
|
else:
|
|
# TODO other manager command
|
|
pass
|
|
|
|
return False
|
|
|
|
else:
|
|
# TODO command for device
|
|
pass
|
|
|
|
def _post_hardware_send(self, command: str, device: Optional[int], action: DeviceCommandAction):
|
|
"""
|
|
|
|
:param command: send command
|
|
:param device: target device
|
|
:param action: what's happen
|
|
"""
|
|
self.log_verbose('hardware send', command, 'device', device, ':', action)
|
|
|
|
if action == DeviceCommandAction.CONNECTED:
|
|
self._post_device_connected(device)
|
|
|
|
elif action == DeviceCommandAction.DISCONNECTED:
|
|
self._post_device_disconnected(device)
|
|
|
|
elif action == DeviceCommandAction.SCAN:
|
|
self.broadcast_command(DataMessage.scan('done'))
|
|
|
|
# server provide method implement : device functions
|
|
|
|
@logging_info
|
|
def get_device_info_all(self) -> List[CompletedDevice]:
|
|
return self._post_device_list()
|
|
|
|
@logging_info
|
|
def get_device_info(self, device: int) -> Optional[CompletedDevice]:
|
|
return super().get_device_info(device)
|
|
|
|
@logging_info
|
|
def device_instruction_all(self, device: int) -> List[str]:
|
|
return self.device_manager.list_device_instruction(device)
|
|
|
|
@logging_info
|
|
def device_all_instruction(self, instruction: str, content: str) -> bool:
|
|
if len(content) == 0:
|
|
# for all device
|
|
device_id = list(map(lambda d: d.device_id, self.device_manager.list_device()))
|
|
|
|
else:
|
|
device_id = list(map(int, filter(len, content.split(' '))))
|
|
|
|
self.log_verbose('device', device_id, instruction)
|
|
|
|
self._device_instruction_group = device_id
|
|
|
|
try:
|
|
return self._device_all_instruction(instruction, device_id)
|
|
|
|
except:
|
|
print_exception(self)
|
|
|
|
finally:
|
|
self._device_instruction_group = None
|
|
|
|
def _device_all_instruction(self, instruction: str, device_id: List[int]) -> bool:
|
|
if instruction == DeviceCommonInstruction.START:
|
|
for device in device_id:
|
|
self.device_manager.device_instruction(device, instruction)
|
|
|
|
client = self.data_server.client()
|
|
if client is not None:
|
|
with client:
|
|
client.start_sync(*device_id)
|
|
|
|
for device in device_id:
|
|
self.broadcast_command(DataMessage.start(device))
|
|
|
|
elif instruction == DeviceCommonInstruction.INTERRUPT or instruction == DeviceCommonInstruction.RESET:
|
|
client = self.data_server.client()
|
|
if client is not None:
|
|
with client:
|
|
client.stop_sync(*device_id)
|
|
|
|
for device in device_id:
|
|
self.device_manager.device_instruction(device, instruction)
|
|
|
|
# for device in device_id:
|
|
# self.broadcast_command(DataMessage.stop(device))
|
|
|
|
if instruction == DeviceCommonInstruction.RECORD_ALL:
|
|
for device in device_id:
|
|
self.device_manager.device_instruction(device, instruction)
|
|
|
|
client = self.data_server.client()
|
|
if client is not None:
|
|
with client:
|
|
client.start_sync(*device_id)
|
|
|
|
# for device in device_id:
|
|
# self.broadcast_command(DataMessage.start(device))
|
|
|
|
return True
|
|
|
|
@logging_info
|
|
def device_instruction(self, device: int, instruction: str) -> bool:
|
|
self.device_manager.device_instruction(device, instruction)
|
|
return True
|
|
|
|
@logging_info
|
|
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
|
|
|
|
@logging_info
|
|
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
|
|
|
|
@logging_info
|
|
def device_sd_card_status(self, device:int) -> None:
|
|
return None
|
|
|
|
@logging_info
|
|
def device_update_calibration(self, device: int) -> Union[bool, str]:
|
|
connect_device = self.device_manager.get_device(device)
|
|
mac_address = connect_device.mac_address
|
|
|
|
try:
|
|
if connect_device.library.name.startswith('Neulive3.'):
|
|
connect_device.calibration_info('NeuliveThreeOne')
|
|
elif connect_device.library.name.startswith('Neulive'):
|
|
connect_device.calibration_info('TDC4VC')
|
|
elif connect_device.library.name.startswith('EliteEIS'):
|
|
connect_device.calibration_info('EISZeroOne')
|
|
|
|
except BaseException as e:
|
|
# reset device info in db
|
|
DeviceAPI.updateByMac(
|
|
address_str(mac_address),
|
|
{
|
|
'connect_id': -1,
|
|
'connect_status': 'idle',
|
|
'auto_connect': False,
|
|
}
|
|
)
|
|
# disconnect device
|
|
self.hardware_device_disconnect(device, True)
|
|
print(e)
|
|
return False
|
|
else:
|
|
# update_calibration_info
|
|
DeviceAPI.updateByMac(
|
|
address_str(mac_address),
|
|
{
|
|
'calibration': str(connect_device.calibration)
|
|
}
|
|
)
|
|
# DeviceAPI.updateByAttrs('connect_status', 'update', {"connect_status": 'connect'})
|
|
return True
|
|
|
|
@logging_info
|
|
def device_update_radio_frequency(self, device: int, content:int) -> bool:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.update_radio_frequency(content)
|
|
return res
|
|
|
|
@logging_info
|
|
def device_update_led_brightness(self, device: int, content:int) -> bool:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.update_led_brightness(content)
|
|
return res
|
|
|
|
@logging_info
|
|
def device_update_accelerator_sensitivity(self, device: int, content:int) -> bool:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.update_accelerator_sensitivity(content)
|
|
return res
|
|
|
|
@logging_info
|
|
def device_set_central_rf(self, device: int, content: int) -> bool:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.central_rf_set(content)
|
|
|
|
@logging_info
|
|
def device_get_central_version(self, device: int) -> Optional[list]:
|
|
d = self.device_manager.get_device(device)
|
|
res = d.central_version_get()
|
|
return res
|
|
|
|
@logging_info
|
|
def device_parameter_last_update_timestamp(self, device: int) -> int:
|
|
d = self.device_manager.get_device(device)
|
|
|
|
if d is None:
|
|
raise RuntimeError(DEVICE_NOT_FOUND, str(device))
|
|
|
|
return d.parameter.last_modified_timestamp
|
|
|
|
@logging_info
|
|
def device_parameter(self, device: int, parameter: str, content: Optional[str] = None) -> Any:
|
|
return self.device_manager.handle_device_parameter_command(device, parameter, content)
|
|
|
|
@logging_info
|
|
def device_parameter_value(self, device: int, parameter: str) -> Any:
|
|
return self.device_manager.handle_device_parameter_value(device, parameter)
|
|
|
|
@logging_info
|
|
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:
|
|
try:
|
|
del self._file_save_cache[device]
|
|
except KeyError:
|
|
pass
|
|
|
|
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 = self._file_save_cache.get(device, self.TEMP_FILENAME_EXPR)
|
|
|
|
content = format_filename(content, d)
|
|
|
|
if '/' not in content:
|
|
content = directory + '/' + content
|
|
|
|
self.log_verbose('device_save_path', device, content)
|
|
|
|
return content
|
|
|
|
@logging_info
|
|
def device_save_path_set(self, device: int, content: str, dry_run: bool) -> str:
|
|
d = self.device_manager.get_device(device)
|
|
|
|
if d is None:
|
|
raise RuntimeError(DEVICE_NOT_FOUND, str(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
|
|
|
|
self._file_save_cache[d.device_id] = content
|
|
|
|
if '/' not in ret:
|
|
ret = directory + '/' + ret
|
|
|
|
self.log_verbose('device_save_path_set', device, ret)
|
|
|
|
return ret
|
|
|
|
# server provide method implement : device history
|
|
|
|
@logging_info
|
|
def device_history_get(self, user: Optional[str]) -> List[DeviceResponseInfo]:
|
|
raise NotImplementedError()
|
|
|
|
@logging_info
|
|
def device_history_found_all(self, user: str) -> List[DeviceResponseInfo]:
|
|
raise NotImplementedError()
|
|
|
|
@logging_info
|
|
def device_history_found(self, user: str, device: int) -> Optional[DeviceResponseInfo]:
|
|
raise NotImplementedError()
|
|
|
|
@logging_info
|
|
def device_history_delete(self, user: str, device: Optional[int]) -> bool:
|
|
raise NotImplementedError()
|
|
|
|
@logging_info
|
|
def run_project(self, project) -> bool:
|
|
if project is not None:
|
|
project = self._project_manager.create(project)
|
|
self._project_manager.run_project(project)
|
|
return project.as_json()
|
|
|
|
@logging_info
|
|
def stop_project(self, project) -> bool:
|
|
if project is not None:
|
|
self._project_manager.stop_project(project)
|
|
return True
|
|
|
|
@logging_info
|
|
def get_running_project(self) -> bool:
|
|
return self._project_manager.get()
|
|
|
|
@logging_info
|
|
def device_internal_command(self, device: int, oper: str, value: Any) -> bool:
|
|
device = self.device_manager.get_device(device)
|
|
|
|
if oper in (InternalInstruction.PREDEFINED_DATA_FORMAT, InternalInstruction.PREDEFINED_DATA_FORMAT_CALI):
|
|
if device is not None:
|
|
self._device_set_data_format(device, value)
|
|
return True
|
|
|
|
elif oper == InternalInstruction.PREDEFINED_SYNC:
|
|
if device is not None:
|
|
self._device_set_sync(device, bool(value))
|
|
return True
|
|
|
|
elif oper == InternalInstruction.PREDEFINED_DISABLE_CACHE:
|
|
return True
|
|
|
|
else:
|
|
return False
|
|
|
|
def _device_set_data_format(self, device: CompletedDevice, value: Any):
|
|
if isinstance(value, (str, bytes)):
|
|
value = DataDecodeFormat.parse(value)
|
|
|
|
if not isinstance(value, DataDecodeFormat):
|
|
raise TypeError('not DataDecodeFormat : ' + value.__class__.__name__)
|
|
|
|
self.log_verbose('update data format', value)
|
|
|
|
client = self.data_server.client()
|
|
if client is not None:
|
|
info = self.file_manager.use(device)
|
|
|
|
if info.is_meta_file:
|
|
# overwrite
|
|
info = self.file_manager.save(device, info, overwrite=True)
|
|
else:
|
|
filename = self._file_save_cache.get(device.device_id, self.TEMP_FILENAME_EXPR)
|
|
filename = format_filename(filename, device)
|
|
info = self.file_manager.save(device, filename)
|
|
|
|
with client:
|
|
client.update_device_configuration(device, info.meta_file, value)
|
|
|
|
def _device_set_disable_cache(self, device: CompletedDevice, disable):
|
|
if disable:
|
|
self.log_verbose('disable cache', device.device_id)
|
|
|
|
info = self.file_manager.use(device)
|
|
|
|
if not info.is_meta_file:
|
|
raise RuntimeError('%s should set after %s' % (
|
|
InternalInstruction.PREDEFINED_DISABLE_CACHE,
|
|
InternalInstruction.PREDEFINED_DATA_FORMAT
|
|
))
|
|
|
|
meta_file = info.meta_file
|
|
assert meta_file is not None
|
|
|
|
self.cache_manager.disable_cache(device, meta_file)
|
|
|
|
else:
|
|
self.cache_manager.enable_cache(device)
|
|
|
|
def _device_set_sync(self, device: CompletedDevice, sync: bool):
|
|
if sync:
|
|
client = self.data_server.client()
|
|
if client is not None:
|
|
info = self.file_manager.use(device)
|
|
|
|
with client:
|
|
if not info.is_meta_file:
|
|
# save data in raw format.
|
|
filename = format_filename(self.TEMP_FILENAME_EXPR, device)
|
|
info = self.file_manager.save(device, filename)
|
|
client.update_device_configuration(device, info.meta_file, RawDataDecoder())
|
|
|
|
if self._device_instruction_group is None:
|
|
client.start_sync(device)
|
|
|
|
device.status = 1
|
|
else:
|
|
if self._device_instruction_group is None:
|
|
start_sync = False
|
|
client = self.data_server.client()
|
|
if client is not None:
|
|
with client:
|
|
# start_sync = client.is_sync(device)
|
|
start_sync = True
|
|
|
|
if start_sync:
|
|
client.stop_sync(device)
|
|
|
|
if start_sync:
|
|
# unset file info, but keep file path cache
|
|
self.file_manager.unset(device.device_id)
|
|
device.status = 0
|
|
|
|
|
|
# server provide method implement : websocket broadcast functions
|
|
@logging_info
|
|
def broadcast_command(self, command: str, device: Optional[int] = None):
|
|
self.log_verbose(pc('broadcast_command', GREEN),
|
|
'(All)' if device is None else '(%d)' % device, command)
|
|
### self.websocket_thread.broadcast_command(command, device=device)
|
|
|
|
if self.mqtt_thread is not None:
|
|
self.mqtt_thread.broadcast_command(command, device=device)
|
|
|
|
# noinspection PyShadowingNames
|
|
def _handle_command_response(self, handler: DataMessageHandler, command: Optional[str]):
|
|
"""
|
|
|
|
:param command: websocket data request command
|
|
:return: error message
|
|
"""
|
|
response = handler.update_command(command)
|
|
|
|
if response is None:
|
|
return
|
|
|
|
if len(handler.listen_device) == 0:
|
|
handler.on_error("empty channel")
|
|
|
|
elif handler.sample_rate is None and handler.data_points is None:
|
|
handler.on_error("sample rate not set")
|
|
|
|
else:
|
|
self.cache_manager.get_data_message(handler, response)
|
|
|
|
# led function
|
|
def get_led_state(self) -> str:
|
|
return self.led_thread.state
|
|
|
|
def set_led_state(self, state: str):
|
|
self.led_thread.set_state(state)
|
|
|
|
# hardware send test
|
|
if not _RUNTIME_COMPILE:
|
|
|
|
def _hardware_send_test(self, options: Dict[str, str]):
|
|
|
|
ret = {
|
|
'name': NAME,
|
|
}
|
|
|
|
if len(options) == 0 or 'software' in options:
|
|
ret['software'] = self._hardware_send_test_software(options.get('software', None))
|
|
|
|
if len(options) == 0 or 'system' in options:
|
|
ret['system'] = self._hardware_send_test_system(options.get('system', None))
|
|
|
|
if len(options) == 0 or 'device' in options:
|
|
ret['device'] = self.device_manager.hardware_test()
|
|
|
|
if len(options) == 0 or 'hardware' in options:
|
|
ret['hardware'] = self._hardware_send_test_hardware(options.get('hardware', None))
|
|
|
|
return ret
|
|
|
|
def _hardware_send_test_software(self, section: Optional[str] = None) -> Dict[str, Any]:
|
|
if section is not None:
|
|
if section == '':
|
|
section = None
|
|
else:
|
|
section = section.split(',')
|
|
|
|
ret = {
|
|
'version': VERSION,
|
|
'git-rev': REV_VERSION,
|
|
'vendor': VENDOR,
|
|
'copyright': COPYRIGHT,
|
|
}
|
|
|
|
if section is None or 'file' in section:
|
|
ret['file'] = {
|
|
'storage_root': str(self.file_manager.storage_root),
|
|
'export_root': str(self.file_manager.export_root)
|
|
}
|
|
|
|
if section is None or 'mqtt' in section:
|
|
ret['mqtt'] = None if self.mqtt_thread is None else self.mqtt_thread.hardware_test()
|
|
|
|
if section is None or 'led' in section:
|
|
ret['led'] = {
|
|
'state_all': list(LED.COLOR.keys()),
|
|
'state': self.led_thread.state,
|
|
'disable': self.led_thread.is_disabled,
|
|
'count': self.led_thread.led_count,
|
|
'refresh': self.led_thread.refresh_mode
|
|
}
|
|
|
|
if section is None or 'ups' in section:
|
|
ret['ups'] = self.ups_thread is not None
|
|
|
|
if section is None or 'control' in section:
|
|
ret['control'] = {
|
|
'socket_file': self.ADDR,
|
|
'cmds': current_command_header()
|
|
}
|
|
|
|
if section is None or 'data_process' in section:
|
|
ret['data_process'] = {
|
|
'socket_file': str(self.data_server.socket_file),
|
|
'spi_mode': self.data_server.options.flag_spi_mode,
|
|
'disable_data_statistics': self.data_server.options.flag_disable_data_statistics,
|
|
'cmds': self.data_server.get_process_arguments()
|
|
}
|
|
|
|
if section is None or 'data_server' in section:
|
|
client = self.data_server.client()
|
|
|
|
if client is not None:
|
|
with client:
|
|
ret['data_server'] = client.hardware_test()
|
|
|
|
if section is None or 'django' in section:
|
|
from biopro.util.inet import allow_host
|
|
|
|
ret['django'] = {
|
|
'allowed_hosts': allow_host()
|
|
}
|
|
|
|
if section is None or 'session' in section:
|
|
ret['session'] = [{
|
|
'user_session': s.user_session,
|
|
'time_stamp': s.time(update=False),
|
|
'property': list(filter(lambda attr: attr not in ('time', 'user_session'),
|
|
filter(lambda attr: not attr.startswith('_'),
|
|
dir(s))))
|
|
} for s in self._session.values()]
|
|
|
|
return ret
|
|
|
|
def _hardware_send_test_system(self, section: Optional[str] = None) -> Dict[str, Any]:
|
|
if section is not None:
|
|
if section == '':
|
|
section = None
|
|
else:
|
|
section = section.split(',')
|
|
|
|
ret = {}
|
|
|
|
if section is None or 'uname' in section:
|
|
ret['uname'] = system_call('uname', '-a')
|
|
|
|
if section is None or 'volume' in section:
|
|
root = self.file_manager.index()
|
|
|
|
ret['volume'] = {
|
|
'total': root.volume_total,
|
|
'usage': root.volume_usage,
|
|
'free': root.volume_free,
|
|
}
|
|
|
|
if section is None or 'package_sys' in section:
|
|
from biopro.util.package import Package
|
|
|
|
ret['package_manager'] = Package.manager()
|
|
ret['package_sys'] = [{
|
|
'name': pkg.name,
|
|
'version': pkg.version,
|
|
} for pkg in Package.list()]
|
|
|
|
if section is None or 'package_pip' in section:
|
|
from biopro.util.package import PipPackage
|
|
|
|
ret['package_pip'] = [{
|
|
'name': pkg.name,
|
|
'version': pkg.version,
|
|
} for pkg in PipPackage.list()]
|
|
|
|
return ret
|
|
|
|
def _hardware_send_test_hardware(self, section: Optional[str] = None) -> Dict[str, Any]:
|
|
if section is not None:
|
|
if section == '':
|
|
section = None
|
|
else:
|
|
section = section.split(',')
|
|
|
|
ret = {}
|
|
|
|
if section is None or 'measure' in section:
|
|
ret['measure'] = {
|
|
'clock_arm': vcg.measure_clock('arm'),
|
|
'clock_core': vcg.measure_clock('core'),
|
|
'voltage': vcg.measure_volts('core'),
|
|
'temperature': vcg.measure_temp()
|
|
}
|
|
|
|
if section is None or 'ext_memory' in section:
|
|
from biopro.impl.ext_mem import hardware_test
|
|
ret['ext_memory'] = hardware_test()
|
|
|
|
if section is None or 'gpio' in section:
|
|
try:
|
|
from biopro.impl.gpio import hardware_test
|
|
except ImportError:
|
|
result = None
|
|
else:
|
|
result = hardware_test()
|
|
|
|
ret['gpio'] = result
|
|
|
|
return ret
|
|
|
|
def _hardware_send_test_set(self, options: Dict[str, str]) -> bool:
|
|
# TODO write options files
|
|
return False
|
|
|
|
@logging_info
|
|
def show_device_data(self, device: int):
|
|
client = self.data_server.client()
|
|
if client is not None:
|
|
with client:
|
|
client.show_data(device)
|
|
|
|
class _RandomCrashThread(ServerThread):
|
|
def __init__(self):
|
|
super().__init__('Crash')
|
|
self.log_enable = False
|
|
|
|
def setup(self) -> None:
|
|
super().setup()
|
|
|
|
from biopro.util.sha import sha256sum_validate
|
|
|
|
try:
|
|
from ._sha import SUM
|
|
except ImportError:
|
|
if _RUNTIME_COMPILE:
|
|
return
|
|
else:
|
|
self.close()
|
|
return
|
|
|
|
for s, f in SUM: # type: str, str
|
|
if not sha256sum_validate(f, s, ignore_missing=True):
|
|
return
|
|
|
|
self.close()
|
|
|
|
def routine(self, event = None):
|
|
from random import randint
|
|
|
|
event.wait(randint(60, 600), disable_interrupt=True)
|
|
|
|
UPSDetectThread.power_off()
|