Files
controller-wisetopdataserver/python/biopro/server/mqtt.py
T
2023-06-05 11:55:36 +08:00

462 lines
16 KiB
Python

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, analysis=False, qos = 2):
if inter:
_topic = self.__controller_ID + '_user'
elif analysis:
_topic = self.__controller_ID + '_data_analysis/get_analysis_data'
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)