import asyncio import random import pb import logging logger = logging.getLogger(__name__) from mesht_models import * from mesht_protobuf_extra import * from protobufs_extra.position_proto import * from protobufs_extra.telemetry_proto import * from protobufs_extra.logrecord_schema import * from protobufs_extra.queuestatus_proto import * from protobufs_extra.device_metadata_proto import * from protobufs_extra.file_info_proto import * from protobuf_decoder.protobuf_decoder import Parser from payload_decrypter import decrypt_aes128_ctr, CUSTOM_SCHEMA import base64 DATA_SCHEMA = [ ("varint", "portnum", 1), ("bytes", "payload", 2), ("bool", "want_response", 3), ("fixed32", "dest", 4), ("fixed32", "source", 5), ("fixed32", "request_id", 6), ("fixed32", "reply_id", 7), ("fixed32", "emoji", 8), ("uint32", "bitfield", 9), ] MESHPACKET_SCHEMA = [ ("fixed32", "from", 1), ("fixed32", "to", 2), ("uint32", "channel", 3), ("oneof", "payload_variant", [ (DATA_SCHEMA, "decoded", 4), ("bytes", "encrypted", 5), ]), ("fixed32", "id", 6), ("fixed32", "rx_time", 7), ("float", "rx_snr", 8), ("uint32", "hop_limit", 9), ("bool", "want_ack", 10), ("int32", "priority", 11), ("int32", "rx_rssi", 12), ("bool", "via_mqtt", 14), ("uint32", "hop_start", 15), ("bytes", "public_key", 16), ("bool", "pki_encrypted", 17), ("uint32", "next_hop", 18), ("uint32", "relay_node", 19), ("uint32", "tx_after", 20), ("int32", "transport_mechanism", 21) ] CHANNEL_SETTINGS_SCHEMA = [ ("uint32", "channel_num", 1), ("string", "name", 3), ] CHANNEL_SCHEMA = [ ("int32", "index", 1), (CHANNEL_SETTINGS_SCHEMA, "settings", 2), ("int32", "role", 3), ] USER_SCHEMA = [ ("string", "id", 1), ("string", "long_name", 2), ("string", "short_name", 3), ("bytes", "macaddr", 4), ("int32", "hw_model", 5), ("bool", "is_licensed", 6), ("int32", "role", 7), ("bytes", "public_key", 8), ("bool", "is_unmessagable", 9) ] MYNODEINFO_SCHEMA = [ ("uint32", "my_node_num", 1), ("uint32", "reboot_count", 8), ("uint32", "min_app_version", 11), ("bytes", "device_id", 12), ("string", "pio_env", 13), #firware edition ("uint32", "nodedb_count", 15) ] NODEINFO_SCHEMA = [ ("uint32", "num", 1), (USER_SCHEMA, "user", 2), (POSITION_SCHEME, "position", 3), ("float", "snr", 4), ("fixed32", "last_heard", 5), (DEVICE_METRICS_SCHEME, "device_metrics", 6), ("uint32", "channel", 7), ("bool", "via_mqtt", 8), ("scalar", "hops_away", 9, "uint32"), ("bool", "is_favorite", 10), ("bool", "is_ignored", 11), ("bool", "is_key_manually_verified", 12), ("bool", "is_muted", 13) ] LORACONFIG_SCHEMA = [ ("bool", "use_preset", 1), ("int32", "modem_preset", 2), ("int32", "region", 7), ] CONFIG_SCHEMA = [ ("oneof", "variant", [ (LORACONFIG_SCHEMA, "lora", 6), ]), ] #mesh.proto:2050 FROMRADIO_SCHEMA = [ ("uint32", "id", 1), ("oneof", "payload_variant", [ (MESHPACKET_SCHEMA, "packet", 2), (MYNODEINFO_SCHEMA, "my_info", 3), (NODEINFO_SCHEMA, "node_info", 4), (CONFIG_SCHEMA, "config", 5), (LOGRECORD_SCHEMA, "log_record", 6), ("uint32", "config_complete_id", 7), ("bool", "rebooted", 8), #(moduleConfig, 9)* (CHANNEL_SCHEMA, "channel", 10), (QUEUE_STATUS_SCHEMA, "queueStatus", 11), #(xmodem, 12) (DEVICE_METADATA_SCHEME, "metadata", 13), #(mqttproxy, 14) (FILE_INFO_SCHEME, "fileinfo", 15), #(clientnotifi, 16) #(deviceui, 17)* ]), ] TORADIO_SCHEMA = [ (MESHPACKET_SCHEMA, "packet", 1), ("uint32", "want_config_id", 3), ] PRESET_NAMES = { 0: "LongFast", 1: "LongSlow", 2: "VeryLongSlow", 3: "MediumSlow", 4: "MediumFast", 5: "ShortSlow", 6: "ShortFast", 7: "LongModerate", 8: "ShortTurbo", } REGION_NAMES = { 0: "UNSET", 1: "US", 2: "EU_433", 3: "EU_868", 4: "CN", 5: "JP", 6: "ANZ", 7: "KR", 8: "TW", 9: "RU", 10: "IN", 11: "NZ_865", 12: "TH", 13: "LORA_24", 14: "UA_433", 15: "UA_868", 16: "MY_433", 17: "MY_919", 18: "SG_923", 19: "PH_433", 20: "PH_868", 21: "PH_915", 22: "ANZ_433", 23: "KZ_433", 24: "KZ_863", 25: "NP_865", 26: "BR_902", } PORTNUMS = { 0: "UNKNOWN_APP", 1: "TEXT_MESSAGE_APP", 2: "REMOTE_HARDWARE_APP", 3: "POSITION_APP", 4: "NODEINFO_APP", 5: "ROUTING_APP", 6: "ADMIN_APP", 7: "TEXT_MESSAGE_COMPRESSED_APP", 8: "WAYPOINT_APP", 9: "AUDIO_APP", 10: "DETECTION_SENSOR_APP", 11: "ALERT_APP", 32: "REPLY_APP", 33: "IP_TUNNEL_APP", 34: "PAXCOUNTER_APP", 64: "SERIAL_APP", 65: "STORE_FORWARD_APP", 66: "RANGE_TEST_APP", 67: "TELEMETRY_APP", 68: "ZPS_APP", 69: "SIMULATOR_APP", 70: "TRACEROUTE_APP", 71: "NEIGHBORINFO_APP", 72: "ATAK_PLUGIN", 73: "MAP_REPORT_APP", 74: "POWERSTRESS_APP", 76: "RETICULUM_TUNNEL_APP", 256: "PRIVATE_APP", 257: "ATAK_FORWARDER", 511: "MAX", } NAMES_TO_PORTNUMS = {v: k for k, v in PORTNUMS.items()} def parseBytesToProtobuf(byte_s): try: to_parse = ' '.join(f'{byte:02x}' for byte in byte_s) res = Parser().parse(to_parse) print("TESTED CLIENT SEND PROTO: ", res.to_dict()) except: pass class Channel: def __init__(self, index, name, role): self.index = int(index) self.name = name self.role = int(role or 0) class MeshtDevice: def __init__(self, transport, device_uuid, skip_init = False, test_client = False, udp_packet = False): self.transport = transport self.channels = [] self.lora_config = None # Track local node number from MyNodeInfo self.my_node_id = "00000000" self.my_node_id_dec = 0 self.nid = None self.device_uuid = device_uuid self.skip_init = skip_init self.test_client = test_client self.udp_packet = udp_packet async def start(self): await self.transport.start() nonce = random.randint(1, 1_000_000_000) logger.debug("MeshtDevice.start: sending want_config_id nonce=%s", nonce) await self.transport.send(pb.encode({"want_config_id": nonce}, TORADIO_SCHEMA)) async def close(self): return await self.transport.close() async def sendMsgToChannel(self, text, channel_index = 0): return await self.send_text(text, channel_index= channel_index) async def sendMsgToDM(self, text, num): return await self.send_text(text, num) async def send_text(self, text, num = 0xFFFFFFFF, channel_index = 0): data = { "portnum": NAMES_TO_PORTNUMS["TEXT_MESSAGE_APP"], "payload": text.encode("utf-8"), "want_response": False, } meshpacket = { "id": random.randint(1, 0x7FFFFFFF), #https://github.com/meshtastic/python/blob/5cc0dae3947cd72f5a05d079a93751fc924afac6/meshtastic/mesh_interface.py#L935 "to": num, #3148365392,#0xFFFFFFFF, "channel": int(channel_index), "want_ack": True, "decoded": data, } payload = pb.encode({"packet": meshpacket}, TORADIO_SCHEMA) await self.transport.send(payload) # Return the meshpacket details for logging by callers return meshpacket async def recv(self): data = await self.transport.recv() if self.test_client: parseBytesToProtobuf(data) return {}, b"" if self.udp_packet: #mqtt или udp пакеты мы ЖЕСТКО перекодируем словно так и надо dec = pb.decode(data, MESHPACKET_SCHEMA) try: d = decrypt_aes128_ctr(base64.b64encode(dec["encrypted"]), "1PG7OiApB1nwvP+rz05pAQ==", dec["id"], dec["from"]) dec["decoded"] = pb.decode(d, CUSTOM_SCHEMA) del dec["encrypted"] except: print("cannot prepare payload") pass #пушим финальный пакет data = pb.encode({"packet": dec}, FROMRADIO_SCHEMA) fr = pb.decode(data, FROMRADIO_SCHEMA) fr["device_uuid"] = self.device_uuid logger.debug(f"FromRadio: {fr}") self._maybe_store_channel(fr) self._maybe_store_lora_config(fr) self._maybe_store_my_node(fr) return fr, data def get_channel_index(self, name): for ch in self.channels: if ch.name == name: return ch.index return None def get_channels(self): return list(self.channels) def _maybe_store_my_node(self, from_radio): mi = from_radio.get("my_info") if not mi: return self.nid = mi.get("my_node_num", 0) self.my_node_id_dec = int(self.nid) self.my_node_id = f"{self.nid & 0xFFFFFFFF:08x}" def _maybe_store_channel(self, from_radio): if not isinstance(from_radio, dict): return ch = from_radio.get("channel") if not isinstance(ch, dict): return # protobuf default for 0 means 'missing' -> treat as index 0 idx = int(ch.get("index") or 0) name = (ch.get("settings") or {}).get("name") or f"Channel {idx}" role = int(ch.get("role") or 0) for i, existing in enumerate(self.channels): if existing.index == idx: if role == 0: # Remove if disabled self.channels.pop(i) return existing.name = name existing.role = role break else: if role != 0: self.channels.append(Channel(idx, name, role)) self.channels.sort(key=lambda c: c.index) def _maybe_store_lora_config(self, from_radio): if not isinstance(from_radio, dict): return cfg = from_radio.get("config") if not isinstance(cfg, dict): return lora = cfg.get("lora") if isinstance(lora, dict): self.lora_config = lora