You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

331 lines
13 KiB

#sys imports
import argparse
import traceback
import sys, os
import asyncio
from time import time
from typing import List, Dict
import copy
from logger import logger
#mesh
from mesht_device import MeshtDevice, USER_SCHEMA
from mesht_models import _wait_for_config_complete, PUB_CH
from mesht_models import NOT_CONNECTED, WAIT_CONFIG, AVAILABLE, ERR, RECONNECT
from protobufs_extra.telemetry_proto import *
from protobufs_extra.position_proto import *
from protobufs_extra.routing_proto import *
import pb
from protobuf_decoder.protobuf_decoder import Parser
#fs imports
from fastapi import FastAPI, HTTPException, WebSocket
from fastapi.responses import HTMLResponse
from contextlib import asynccontextmanager
#mongo
from pymongo import AsyncMongoClient
from dbService import DbService
#other
from botManager import BotManager
from utils import isInt, generate_random_string, md5hash
class MeshArgsParse:
def __init__(self, args):
self.args = args
class MeshMultiListener(MeshArgsParse):
def __init__(self, args):
self.PUB_CH = PUB_CH
super().__init__(args)
self.devices:List[MeshtDevice] = []
self.devicesUuidHashToUuid = {}
self.defaultDeviceUUID = ""
self.defaultDeviceUUIDHash = ""
self.readConfig(args.change_workdir, args.mesh_config)
'''
[
{"uuid": "692daca8-08e4-48b5-917b-a65a0f72cc12", "transport": "tcp", "address": "192.168.1.1:1234", "alive_pool_seconds": 60},
{"uuid": "5e432648-6c04-4892-af98-f32382931645", "transport": "ble", "adapter":"hci0", "address": "00:00:00:00:00:00"},
{"uuid": "0a8aa564-2f91-4e4d-a102-0aae57881bab", "transport": "serial", "port": "/dev/tty0USB", "baudrate": 115200},
{"uuid", "c64ab2a4-5305-4c70-8776-83c74a5de796", "transport": "ws", "port": "60000"}
]
'''
#not need async
def readConfig(self, change_dir = True, path = "./config/mesh.json"):
from json import load
if change_dir:
path = f"{os.path.dirname(os.path.abspath(__file__))}/{path}"
with open(path, "r") as config:
self.json_config = load(config)
for device_config in self.json_config:
if device_config.get("uuid", "") == "":
raise Exception("missed uuid section in ", device_config)
if device_config.get("transport", "") == "":
raise Exception("missed uuid section in ", device_config)
if device_config["transport"] == "serial":
logger.info("Found serial transport")
from transport_serial import SerialTransport
transport = SerialTransport(port = device_config["port"], baudrate = device_config["baudrate"])
self.devices.append(MeshtDevice(transport, device_config['uuid']))
elif device_config["transport"] == "tcp":
logger.info("Found tcp transport")
from transport_tcp import TCPTransport
ip, port = device_config["address"].split(":")
transport = TCPTransport(ip, int(port), alive_pool_connect=device_config.get("alive_pool_seconds", 60))
self.devices.append(MeshtDevice(transport, device_config['uuid']))
elif device_config["transport"] == "ble":
logger.info("Found ble transport")
from transport_ble import BLETransport
transport = BLETransport(device_config["address"], device_config["adapter"])
self.devices.append(MeshtDevice(transport, device_config['uuid']))
elif device_config["transport"] == "ws":
logger.info("Found ws transport")
from transport_ws import WSTransport
transport = WSTransport(device_config["port"], device_config["uuid"])
self.devices.append(MeshtDevice(transport, device_config['uuid'], True, False, True))
#set default mesh
self.defaultDeviceUUID = self.json_config[0]["uuid"]
self.defaultDeviceUUIDHash = md5hash(self.defaultDeviceUUID)
for device in self.devices:
print(md5hash(device.device_uuid), device)
self.devicesUuidHashToUuid[md5hash(device.device_uuid)] = device.device_uuid
async def meshListener(self, device: MeshtDevice, queue: asyncio.Queue):
run = not self.args.disable_mesh or self.args.enable_mesh
while run:
try:
await device.start()
device.state = WAIT_CONFIG
logger.info(str(device), " wait config")
init_data = await _wait_for_config_complete(device)
for from_radio in init_data:
await queue.put(from_radio)
logger.info(str(device), " available")
device.state = AVAILABLE
while True:
from_radio, _ = await device.recv()
if not device.test_client:
await queue.put(from_radio)
device.last_packet_catch = time()
except asyncio.exceptions.CancelledError:
logger.info(str(device), " kill device")
run = False
except:
logger.error(str(device), " has connect error")
device.state = ERR
traceback.print_exc()
await asyncio.sleep(1)
logger.info(str(device), " device will reconnect")
device.state = RECONNECT
finally:
await device.close()
class MeshApi(MeshArgsParse):
app: FastAPI
tasks: List
context: str = "/api"
def __init__(self, args):
super().__init__(args)
self.app = FastAPI(lifespan=self.lifespan)
self.tasks = []
self.listeners = [] #[{"func": func, "args":[]}]
from authManager import AuthManager
self.authManager = AuthManager(args)
@asynccontextmanager
async def lifespan(self, app: FastAPI):
logger.info("web started")
logger.info("create mesh listeners")
for listener in self.listeners:
asyncio.create_task(listener["func"](*listener["args"]))
logger.info("now create bg tasks")
for task in self.tasks:
asyncio.create_task(task())
yield
logger.info("kill web server")
def run(self):
import uvicorn
uvicorn.run(self.app, host=self.args.web_host, port = self.args.web_port)
class MongoDriver(MeshArgsParse):
def __init__(self, args):
super().__init__(args)
if args.mongo_url:
self.dbClient = AsyncMongoClient(args.mongo_url)
elif args.mongo_host and args.mongo_port:
self.dbClient = AsyncMongoClient(args.mongo_host, args.mongo_port)
else:
logger.error("Unknown mongo client")
sys.exit(1)
self.dbStore = self.dbClient[self.args.mongo_db]
self.dbService = DbService(self.dbStore, self)
from tileManager import TileManager
self.tileManager = TileManager(self)
async def dbSaveRadio(self, new_from_radio):
from_radio = copy.deepcopy(new_from_radio)
for k, v in from_radio.items():
if k == "device_uuid":
continue
if type(v) != dict:
v = {"data": v, "ts": time()}
else:
v["ts"] = time()
if "decoded" in v:
v.update(v["decoded"])
del v["decoded"]
if "payload" in v:
try:
if v.get("portnum", 0) == 1: #text
v["decoded_payload"] = v["payload"].decode()
elif v.get("portnum", 0) == 67: #telemetry
v["decoded_payload_object"] = pb.decode(v["payload"], TELEMETRY_SCHEME)
elif v.get("portnum", 0) == 3: #pos
v["decoded_payload_object"] = pb.decode(v["payload"], POSITION_SCHEME)
elif v.get("portnum", 0) == 5: #routing
v["decoded_payload_object"] = pb.decode(v["payload"], ROUTING_SCHEME)
elif v.get("portnum", 0) == 70: #traceroute
v["decoded_payload_object"] = pb.decode(v["payload"], ROUTE_DISCOVERY_SCHEME)
elif v.get("portnum", 0) == 4: #nodeinfo
v["decoded_payload_object"] = pb.decode(v["payload"], USER_SCHEMA)
else:
self.tryParseProtobuf(v)
#if "decoded_payload_object" in v:
# print(v["decoded_payload_object"])
except:
logger.error("Cannot decode protobuf: " + v.get("portnum", 0))
self.tryParseProtobuf(v)
traceback.print_exc()
if "user" in v:
v.update(v["user"])
del v["user"]
v["device_uuid"] = from_radio["device_uuid"]
try:
await self.dbStore[k].insert_one(v)
except Exception as e:
logger.error("Cannot save packet to db", str(e), v)
def tryParseProtobuf(self, v):
try:
if v.get("portnum", 0) in []:
pass
else:
to_parse = ' '.join(f'{byte:02x}' for byte in v["payload"])
res = Parser().parse(to_parse)
logger.debug(v["portnum"], res.to_dict())
except:
pass
class MeshCenter(MeshMultiListener, MeshApi, MongoDriver, MeshArgsParse):
queue: asyncio.Queue = asyncio.Queue()
def __init__(self, args):
MeshMultiListener.__init__(self, args)
MeshArgsParse.__init__(self, args)
MeshApi.__init__(self, args)
MongoDriver.__init__(self, args)
self.buildBackgroundTasks()
self.bot = BotManager(self)
self.extensionLoader(['webExtensions'])
async def queueHandler(self):
logger.info("Start queue handler")
run = True
while run:
try:
from_radio = await self.queue.get()
if from_radio:
yield from_radio
except asyncio.exceptions.CancelledError:
run = False
except:
traceback.print_exc()
def buildBackgroundTasks(self):
#input queue #[{"func": func, "args":[]}]
for device in self.devices:
logger.debug("Append mesh listener", device)
self.listeners.append({"func": self.meshListener, "args":[device, self.queue]})
#output
async def handlerTask():
async for from_radio in self.queueHandler():
#logger.debug(from_radio)
asyncio.create_task(self.dbSaveRadio(from_radio))
asyncio.create_task(self.bot.handleMessage(from_radio))
self.tasks.append(handlerTask)
self.tasks.append(self.authManager.storeCleaner)
def extensionLoader(self, search_paths = []):
logger.info("Search fastapiExt")
os.chdir(os.path.dirname(os.path.abspath(__file__)))
if type(search_paths) == str:
search_paths = [search_paths]
for path in search_paths:
logger.info(f"Try found extensions in {path}")
if not os.path.exists(path) or not os.path.isdir(path):
logger.info(f"Directory is not exists or not directory, skip")
continue
sys.path.insert(0, path)
for extension in os.listdir(path):
extension, ext = os.path.splitext(extension)
if ext != ".py":
continue
logger.info(f"Found web ext: {extension}")
__import__(extension).WebExtension(self)
sys.path.pop(0)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
#mesh
parser.add_argument("--change-workdir", default=True, action="store_true")
parser.add_argument("--mesh-config", default="./config/mesh.json")
parser.add_argument("--disable-mesh", action="store_true", default=False)
parser.add_argument("--enable-mesh", action="store_true", default=False, help="Need to run in docker if git is bullshit updates")
'''
parser.add_argument("--transport", default="tcp")
#serial transport
parser.add_argument("--serial-port", default="/dev/tty.usbmodemD0CF1309DC141")
parser.add_argument("--serial-baudrate", default=115200)
parser.add_argument("--serial-alive-pool-seconds", default=60)
#ble transport
parser.add_argument("--ble-adapter", default=None)
parser.add_argument("--ble-mesh-mac", default="22AC1D28-5345-465E-2E82-18CDE5857A45")
#tcp trasponse
parser.add_argument("--tcp-address", default="192.168.3.26:8886")'''
#fastapi
parser.add_argument("--web-host", default="0.0.0.0")
parser.add_argument("--web-port", default=8680)
parser.add_argument("--web-salt", default=generate_random_string(32))
parser.add_argument("--web-auth-enable", default=False, action="store_true")
#mongodb
parser.add_argument("--mongo-url")
parser.add_argument("--mongo-host", default="192.168.3.2")
parser.add_argument("--mongo-port", default=27017)
parser.add_argument("--mongo-db", default="meshtastic")
a = MeshCenter(parser.parse_args())
a.run()