diff --git a/pipboyMESH/mesh_server.py b/pipboyMESH/mesh_server.py index 889d0a5..c7f68e2 100644 --- a/pipboyMESH/mesh_server.py +++ b/pipboyMESH/mesh_server.py @@ -1,5 +1,6 @@ #fs imports -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, WebSocket +from fastapi.responses import HTMLResponse from uvicorn import run as UR from contextlib import asynccontextmanager @@ -28,13 +29,23 @@ class Servlet: message: List[Message] = [] msh_state = NOT_CONNECTED + ws_clients: List[WebSocket] = [] + + lstChange = { + "channels": time(), + "messages": time(), + "nodes": time(), + "state": time() + } + def __init__(self, args): self.app = FastAPI(lifespan=self.lifespan) self.args = args if args.transport == "serial": self.transport = SerialTransport(port=args.serial_port, baudrate=args.serial_baudrate) self.device = MeshtDevice(self.transport) - self.buildRoutes() + self.buildRoutes() + self.buildWS() def run(self): UR(self.app, host=self.args.host, port=self.args.port)#, reload = True)#todo @@ -46,57 +57,125 @@ class Servlet: yield print("server is shutdown now") + async def ws_update(self, data): + data.update({"type": WS_TYPE_NEW}) + for client in self.ws_clients: + try: + await client.send_json(data) + except: + traceback.print_exc() + async def meshWorker(self): run = True while run: try: await self.device.start() self.msh_state = WAIT_CONFIG + self.lstChange["state"] = time() + await self.ws_update({"event": WS_EVENT_STATE, "data": self.msh_state}) init_data = await _wait_for_config_complete(self.device) for from_radio in init_data:#todo check dubs - self.meshCollector(from_radio) + await self.meshCollector(from_radio) + + self.msh_state = AVAILABLE + self.lstChange["state"] = time() + await self.ws_update({"event": WS_EVENT_STATE, "data": self.msh_state}) + await self.ws_update({"event": WS_EVENT_MYID, "data": self.device.my_node_id}) while True: - self.msh_state = AVAILABLE self.pulse = time() from_radio, _ = await self.device.recv() - self.meshCollector(from_radio) + await self.meshCollector(from_radio) + except asyncio.exceptions.CancelledError: print("Web close") run = False pass except: self.msh_state = ERR + await self.ws_update({"event": WS_EVENT_STATE, "data": self.msh_state}) + self.lstChange["state"] = time() print("Mesh worker is has error, reconnect to mesh device after seconds...") traceback.print_exc() self.msh_state = RECONNECT + await self.ws_update({"event": WS_EVENT_STATE, "data": self.msh_state}) + self.lstChange["state"] = time() await asyncio.sleep(1) finally: await self.device.close() - def meshCollector(self, from_radio): + async def meshCollector(self, from_radio): try: print(from_radio) if Channel.isThis(from_radio): print("Found channel packet") channel = Channel(from_radio) self.channels[channel.index] = channel + await self.ws_update({"type":WS_TYPE_NEW, "event": WS_EVENT_CHANNEL, "data": channel.__dict__}) + self.lstChange["channels"] = time() return if Node.isThis(from_radio): print("Found node packet") node = Node(from_radio) self.nodes[node.num] = node + await self.ws_update({"type":WS_TYPE_NEW, "event": WS_EVENT_NODE, "data": {"id": node.num, "name": str(node)}}) + self.lstChange["nodes"] = time() return if Message.isThis(from_radio): print("Found message packet") - self.message.append(Message(from_radio)) + msg = Message(from_radio) + self.message.append(msg) + await self.ws_update({"type":WS_TYPE_NEW, "event": WS_EVENT_MESSAGE, "data": msg.__dict__}) + self.lstChange["messages"] = time() + return except: print("cannot parse packet") traceback.print_exc() + def buildWS(self): + #from json import dumps, loads + from mesh_ws_debug import page + + @self.app.get("/mesh/wsdebug", response_class=HTMLResponse) + async def wsdebug(): + return page + + async def ws_hello(websocket: WebSocket): + print("send hello") + try: + await websocket.send_json({"type":WS_TYPE_INIT, "event": WS_EVENT_MYID, "data": self.device.nid}) + #await websocket.send_json({"type":WS_TYPE_INIT, "event": WS_EVENT_MYNODE, "data": self.nodes.get(self.device.nid).__dict__ if self.device.nid in self.nodes else None}) + await websocket.send_json({"type":WS_TYPE_INIT, "event": WS_EVENT_CHANNEL, "data": [channel.__dict__ for channel in self.channels.values()]}) + await websocket.send_json({"type":WS_TYPE_INIT, "event": WS_EVENT_STATE, "data": self.msh_state}) + await websocket.send_json({"type":WS_TYPE_INIT, "event": WS_EVENT_NODE, "data": [{"id": node.num, "name": str(node)} for node in self.nodes.values()]}) + await websocket.send_json({"type":WS_TYPE_INIT, "event": WS_EVENT_MESSAGE, "data": [message.__dict__ for message in self.message]}) + except: + traceback.print_exc() + pass + + @self.app.websocket("/mesh/ws") + async def ws_endpoint(websocket: WebSocket): + await websocket.accept() + self.ws_clients.append(websocket) + #грузим фуру кала + try: + await ws_hello(websocket) + while True: + msg = await websocket.receive_json() + print(msg) + except: + traceback.print_exc() + pass + finally: + self.ws_clients.remove(websocket) + try: + await websocket.close() + except: + pass + def buildRoutes(self): route = "/mesh/api" diff --git a/pipboyMESH/mesh_ws_debug.py b/pipboyMESH/mesh_ws_debug.py new file mode 100644 index 0000000..c398266 --- /dev/null +++ b/pipboyMESH/mesh_ws_debug.py @@ -0,0 +1,65 @@ +page = """ + + + + Home page + + + + +
+ +

Debug websocket page

+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + + + + """ \ No newline at end of file diff --git a/pipboyMESH/mesht_device.py b/pipboyMESH/mesht_device.py index 92afab6..537b2d7 100644 --- a/pipboyMESH/mesht_device.py +++ b/pipboyMESH/mesht_device.py @@ -201,6 +201,7 @@ class MeshtDevice: self.lora_config = None # Track local node number from MyNodeInfo self.my_node_id = "00000000" + self.nid = None async def start(self): await self.transport.start() @@ -259,8 +260,8 @@ class MeshtDevice: if not mi: return - my_node_num = mi.get("my_node_num", 0) - self.my_node_id = f"{my_node_num & 0xFFFFFFFF:08x}" + self.nid = mi.get("my_node_num", 0) + self.my_node_id = f"{self.nid & 0xFFFFFFFF:08x}" def _maybe_store_channel(self, from_radio): if not isinstance(from_radio, dict): diff --git a/pipboyMESH/mesht_models.py b/pipboyMESH/mesht_models.py index 3dbb1e9..dc37970 100644 --- a/pipboyMESH/mesht_models.py +++ b/pipboyMESH/mesht_models.py @@ -1,4 +1,5 @@ import pydantic +from time import time PUB_CH = 0xFFFFFFFF PACKET = "packet" @@ -9,6 +10,17 @@ AVAILABLE = 2 ERR = 3 RECONNECT = 4 +WS_EVENT_CHANNEL = 0 +WS_EVENT_NODE = 1 +WS_EVENT_STATE = 2 +WS_EVENT_MESSAGE = 3 +WS_EVENT_MYID = 4 +#WS_EVENT_MYNODE = 5 + +WS_TYPE_INIT = 0 +WS_TYPE_NEW = 1 +WS_TYPE_FRONTEND = 2 + async def _wait_for_config_complete(device, extraInfo = False): print("wait config") packets = [] @@ -34,7 +46,7 @@ class Channel: class Node: #aka node info def __init__(self, fr): self.num = fr["node_info"]["num"] - self.user = User(fr["node_info"]) + self.user = fr["node_info"].get("user", {}) self.snr = fr["node_info"].get("snr", None) self.last_heard = fr["node_info"].get("last_heard", None) self.hops_away = fr["node_info"].get("hops_away", None) @@ -44,18 +56,7 @@ class Node: #aka node info return isinstance(fr, dict) and "node_info" in fr def __str__(self): - return f"({self.user.short_name}) {self.user.long_name}" - -class User: - def __init__(self, ni): - if "user" in ni: - self.id = ni["user"]["id"] - self.long_name = ni["user"]["long_name"] - self.short_name = ni["user"]["short_name"] - else: - self.id = None - self.long_name = None - self.short_name = None + return f"({self.user.get('short_name', None)}) {self.user.get('long_name', None)}" class NewMessage(pydantic.BaseModel): channel_id: int = 0 @@ -73,6 +74,7 @@ class Message: self.hop_limit = fr[PACKET].get("hop_limit", None) self.rx_rssi = fr[PACKET].get("rx_rssi", None) self.channel = fr[PACKET].get("channel", 0) + self.append_time = time() @property def isDm(self):