import base64 import re from json import loads as json_parse, dumps as _json_stringify from typing import Dict, Optional, Any from biopro.controller import ComplexResponse from biopro.devlib.menu import Menu from biopro.file.segment import FileSegment, FileSegmentContent from biopro.file.util import RootInfo, DirectoryInfo, FileInfo from biopro.recording.file import RecordingMetaFile from biopro.message.request import DataMessageHandler from biopro.util.console import MAGENTA from biopro.util.json import JSON_OBJECT from biopro.util.stack import print_exception from biopro.util.text import part_prefix from .cache import FileHandle from .controller import ControlServerAPI, HardwareInfo from .dispatcher import APIDispatcher from .socket import ServerThread import threading # try: import paho.mqtt.client as mqtt # except ImportError: # Client = None # MQTTMessage = None _RUNTIME_COMPILE = False def json_stringify(o: Any) -> str: return _json_stringify(o, separators=(',', ':')) # noinspection PyUnusedLocal class MqttThread(threading.Thread): """Mqtt server thread **Subscribe Topic** **Publish Topic** **Publish Content Format** :: response = json_data | error_message | file_segment json_data = { 'data'?: JSON, 'binary'?: str # base64 encode 'cookie'?: JSON_OBJECT } error_message = { 'error' = { 'type': str 'message': str } } """ def __init__(self, server: ControlServerAPI, url: str, port: int, client_id: str, log_verbose, name, alive=60, is_sub=True,): # super().__init__('MQTT-' + client_id, MAGENTA) super(MqttThread, self).__init__(name = name) from ._identify import controller_device_id self.__controller_ID = controller_device_id() self._mqtt_url = url self._mqtt_port = port self._mqtt_alive = alive self._mqtt_client = None self._mqtt_client_local = None self._client_id = client_id self.log_verbose = log_verbose if self._client_id is 'control_server': self._handler = server self._dispatcher = APIDispatcher(ControlServerAPI, server) else: self._handler = None self._dispatcher = None self.controller_available = False self.is_sub = is_sub @property def mqtt_url(self) -> str: return self._mqtt_url @property def mqtt_port(self) -> int: return self._mqtt_port def run(self) -> None: if mqtt.Client is None: if _RUNTIME_COMPILE: raise RuntimeError('remote server is disabled') else: raise RuntimeError('python module paho.mqtt not found : ' 'please type "sudo pip install paho-mqtt==1.4.0"') self.log_verbose('use controller ID', self.__controller_ID) # super().setup() # self._mqtt_client = Client( self.__controller_ID + '_' + self._client_id ) # self._mqtt_client = mqtt.Client( self.__controller_ID + '_' + self._client_id ) # self.log_verbose('cloud-server: ' + '%s:%d' % (self._mqtt_url, self._mqtt_port)) # self._mqtt_client.connect_async(self._mqtt_url, self._mqtt_port, self._mqtt_alive) # self._mqtt_client.on_connect = self.on_connect # self._mqtt_client.on_disconnect = self.on_disconnect # self._mqtt_client.on_message = self.on_message # self._mqtt_client_local = Client( self.__controller_ID + '_' + self._client_id + '_local' ) self._mqtt_client_local = mqtt.Client( self.__controller_ID + '_' + self._client_id + '_local' ) self.log_verbose('localhost: 127.0.0.1:1883') self._mqtt_client_local.connect_async('127.0.0.1', self._mqtt_port, self._mqtt_alive) self._mqtt_client_local.on_connect = self.on_connect_local self._mqtt_client_local.on_disconnect = self.on_disconnect_local self._mqtt_client_local.on_message = self.on_message self.routine() def shutdown(self) -> None: if self._mqtt_client is not None: self._mqtt_client.loop_stop() self._mqtt_client.disconnect() if self._mqtt_client_local is not None: self._mqtt_client_local.loop_stop() self._mqtt_client_local.disconnect() def routine(self, event = None) -> None: if self._mqtt_client_local is not None: self._mqtt_client_local.loop_start() if self._mqtt_client is not None: self._mqtt_client.loop_start() # self._mqtt_client_local.loop_start() # self._mqtt_client.loop_start() def on_connect(self, client: mqtt.Client, user_data, flags: Dict[str, Any], rc: int): self.log_verbose('cloud-server connected') topic = self.__controller_ID + '_user' if self.is_sub: self.log_verbose('cloud-server subscribe', topic) self._mqtt_client.subscribe(topic) def on_connect_local(self, client: mqtt.Client, user_data, flags: Dict[str, Any], rc: int): self.log_verbose('localhost connected') topic = self.__controller_ID + '_user' if self.is_sub: self.log_verbose('localhost subscribe', topic) self._mqtt_client_local.subscribe(topic) def on_disconnect(self, client: mqtt.Client, user_data, rc): self.log_verbose('cloud-server disconnected') self.sleep(3) self._mqtt_client.reconnect() def on_disconnect_local(self, client: mqtt.Client, user_data, rc): self.log_verbose('localhost disconnected') self.sleep(3) self._mqtt_client_local.reconnect() def publish(self, topic: str, payload: str, inter = False, qos = 2): if inter: _topic = self.__controller_ID + '_user' else: _topic = self.__controller_ID + '/' + topic if self._mqtt_client_local is not None: info = self._mqtt_client_local.publish(_topic, payload, qos = qos) if self._mqtt_client is not None and inter == False: info = self._mqtt_client.publish(_topic, payload, qos = qos) def broadcast_command(self, command: str, device: Optional[int] = None): if self._mqtt_client_local is not None: if command == 'state:device_available': self.controller_available = True self._mqtt_client_local.publish(self.__controller_ID + '/broadcast', command) if self._mqtt_client is not None: if command == 'state:device_available': self.controller_available = True self._mqtt_client.publish(self.__controller_ID + '/broadcast', command) def on_message(self, client: mqtt.Client, user_data, message): json = json_parse(message.payload.decode()) method = json.get('header', None) if self._client_id is 'data_server': if 'hardware_scan' in str(method): self.broadcast_command('scan:start') return None if self.controller_available is False: return None if 'file_info' in str(method): if json.get('path', None) in '': return None device = json.get('device', None) self.log_verbose('Request:', method, 'Data: ', str(json)) if method is None or len(method) == 0: return else: argument = dict(json) del argument['header'] response_topic = self.__controller_ID + '/' + method try: result = self._on_command(method, argument) except BaseException as e: print_exception() response = { 'error': { 'type': e.__class__.__name__, 'message': str(e) } } if self._mqtt_client_local is not None: self._mqtt_client_local.publish(response_topic, json_stringify(response)) if self._mqtt_client is not None: self._mqtt_client.publish(response_topic, json_stringify(response)) else: cookie = None if isinstance(result, ComplexResponse): cookie = result.response_cookie result = result.response response = { 'data': result } elif isinstance(result, bytes): response = { 'binary': base64.b64encode(result).decode() } elif isinstance(result, list): result = [d.as_json() for d in result] response = { 'data': result } elif isinstance(result, (HardwareInfo, Menu, RootInfo, DirectoryInfo, FileInfo, FileHandle, RecordingMetaFile)): result = result.as_json() response = { 'data': result } elif isinstance(result, (FileSegment, FileSegmentContent)): result = result.as_json() try: _index = re.search(r'/export', str(result['path']) ).start() + 8 result['path'] = result['path'].as_posix()[_index:] except BaseException as e: result['path'] = result['path'].as_posix()[35:] # print(e) response = { 'data': result } else: response = { 'data': result } if cookie is not None: response['cookie'] = cookie if device is not None: response['device'] = device if self._mqtt_client_local is not None: self._mqtt_client_local.publish(response_topic, json_stringify(response)) if self._mqtt_client is not None: self._mqtt_client.publish(response_topic, json_stringify(response)) # tmp = re.split('/', method) # if tmp[0] == "device_parameter" and tmp[1] == "0": # device_id = str(json.get('device', None)) # client.publish(self.__controller_ID + '/broadcast', # "message:" + device_id + ":" + json_stringify(response)) self.log_verbose('*- mes: ' + str(json) + ', res: ' + json_stringify(response) + ', topic:' + response_topic) BLACK_LIST = { # 'hardware_datetime', 'broadcast_command', 'device_internal_command', 'websocket_data_request', # 'file_handle_data', # 'file_handle_data_start', # 'file_handle_data_stop' } def _on_command(self, method: str, options: Dict[str, Any]) -> Any: if self._client_id is not 'control_server': return None method, _ = part_prefix(method, '/', missing=method) # print( method, options ) if method in self.BLACK_LIST: return None if method == 'file_segment_info': return self._handler.file_segment('info', **options) elif method == 'file_segment_new': return self._handler.file_segment('new', **options) elif method == 'file_segment_reload': return self._handler.file_segment('reload', **options) elif method == 'file_segment_invalid': return self._handler.file_segment('invalid', **options) elif method == 'file_handle_data': return self._file_handle_data(**options) elif method == 'file_handle_data_start': return self._file_handle_stream(True, **options) elif method == 'file_handle_data_stop': return self._file_handle_stream(False, **options) else: return self._dispatcher.method_dispatch(method, (), options) def _file_handle_stream(self, start: bool, user_session: Optional[str] = None): if self._client_id is not 'control_server': return None if start: session = self._handler.get_user_session(user_session, new_session=True, allow_empty=True) else: session = self._handler.get_user_session(user_session) if session is None: return None data_handle = session.get_data('data_handle', type_check=MqttDataMessageHandler) if data_handle is None or not isinstance(data_handle, MqttDataMessageHandler): if not start: return data_handle = session.set_data('data_handle', MqttDataMessageHandler(self, 'file_handle_data')) # self._handler.data_request_stream(start, data_handle) # if start : # data_handle = MqttDataMessageHandler(self, 'file_handle_data') # self._handler.data_request_stream(start, data_handle) # else: # return def _file_handle_data(self, expression: str, user_session: Optional[str] = None) -> ComplexResponse: """request data **Request Url** **Request Body** data request expression :class:`biopro.message.request.DataMessageRequest` **response format** websocket bytes data **relative method** :func:`biopro.server.controller.ControlServer.websocket_data_request` :func:`biopro.server.cache.CacheManager.get_data_message` :param expression: :param user_session: user session ID :return: """ if self._client_id is not 'control_server': return None session = self._handler.get_user_session(user_session, new_session=True, allow_empty=True) if session is None: return None data_handle = session.get_data('data_handle', type_check=MqttDataMessageHandler, init=lambda: MqttDataMessageHandler(self, 'file_handle_data')) ret = self._handler.websocket_data_request(data_handle, expression) # ret = self._handler.websocket_data_request(MqttDataMessageHandler(self, 'file_handle_data'), expression) resp = ComplexResponse(ret) resp.set_cookie(user_session=session.user_session) return resp def hardware_test(self) -> JSON_OBJECT: return { 'mqtt_url': self._mqtt_url, 'mqtt_port': self._mqtt_port, 'mqtt_alive': self._mqtt_alive, # TODO move content for testing 'controller_id': self.__controller_ID } class MqttDataMessageHandler(DataMessageHandler): __slots__ = '__client', '__topic' def __init__(self, client: MqttThread, topic: str): super().__init__() self.__client = client self.__topic = topic def on_message(self, message, topic=None): # response = { # # 'binary': base64.b64encode(message).decode() # 'binary': str(message) # } response = None # if len(self.__topic) > 20: # response = { # 'binary': message # } # # print( 'mqtt_datastream: ' + str(json_stringify(response)) + ', topic:' + self.__topic) # else: # response = { # 'binary': base64.b64encode(message).decode() # } if topic is None: self.__client.publish(self.__topic, message, qos = 0) else: self.__client.publish(self.__topic + '/' + topic, message, qos = 0) del response del message return def __str__(self): return 'DataMessageHandler[%s]' % ('mqtt:' + self.__topic)