meshtastic_mqtt- Unnamed repository; edit this file 'description' to name the repository. |
git clone git://git.acid.vegas/-c.git |
Log | Files | Refs | Archive | README | LICENSE |
meshtastic_mqtt.py (11074B)
1 #!/usr/bin/env python 2 # Meshtastic MQTT Interface - Developed by acidvegas in Python (https://acid.vegas/meshtastic) 3 4 import argparse 5 import base64 6 import json 7 8 try: 9 from cryptography.hazmat.backends import default_backend 10 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 11 except ImportError: 12 raise ImportError('missing the cryptography module (pip install cryptography)') 13 14 try: 15 from google.protobuf.json_format import MessageToJson 16 except ImportError: 17 raise ImportError('missing the google protobuf module (pip install protobuf)') 18 19 try: 20 from meshtastic import mesh_pb2, mqtt_pb2, portnums_pb2, telemetry_pb2 21 except ImportError: 22 raise ImportError('missing the meshtastic module (pip install meshtastic)') 23 24 try: 25 import paho.mqtt.client as mqtt 26 except ImportError: 27 raise ImportError('missing the paho-mqtt module (pip install paho-mqtt)') 28 29 class MeshtasticMQTT(object): 30 def __init__(self): 31 '''Initialize the Meshtastic MQTT client''' 32 33 self.broadcast_id = 4294967295 # Our channel ID 34 self.key = None 35 self.names = {} 36 self.filters = None 37 38 39 def connect(self, broker: str, port: int, root: str, channel: str, username: str, password: str, key: str): 40 ''' 41 Connect to the MQTT broker 42 43 :param broker: The MQTT broker address 44 :param port: The MQTT broker port 45 :param root: The root topic 46 :param channel: The channel name 47 :param username: The MQTT username 48 :param password: The MQTT password 49 :param key: The encryption key 50 ''' 51 52 # Initialize the MQTT client 53 client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id='', clean_session=True, userdata=None) 54 55 # Set the username and password for the MQTT broker 56 client.username_pw_set(username=username, password=password) 57 58 # Set the encryption key 59 self.key = '1PG7OiApB1nwvP+rz05pAQ==' if key == 'AQ==' else key 60 61 # Prepare the key for decryption 62 try: 63 padded_key = self.key.ljust(len(self.key) + ((4 - (len(self.key) % 4)) % 4), '=') 64 replaced_key = padded_key.replace('-', '+').replace('_', '/') 65 self.key_bytes = base64.b64decode(replaced_key.encode('ascii')) 66 except Exception as e: 67 print(f'Error decoding key: {e}') 68 raise 69 70 # Set the MQTT callbacks 71 client.on_connect = self.event_mqtt_connect 72 client.on_message = self.event_mqtt_recv 73 74 # Connect to the MQTT broker 75 client.connect(broker, port, 60) 76 77 # Set the subscribe topic 78 self.subscribe_topic = f'{root}{channel}/#' 79 80 # Keep-alive loop 81 client.loop_forever() 82 83 84 def decrypt_message_packet(self, mp): 85 ''' 86 Decrypt an encrypted message packet. 87 88 :param mp: The message packet to decrypt 89 ''' 90 91 try: 92 # Extract the nonce from the packet 93 nonce_packet_id = getattr(mp, 'id').to_bytes(8, 'little') 94 nonce_from_node = getattr(mp, 'from').to_bytes(8, 'little') 95 nonce = nonce_packet_id + nonce_from_node 96 97 # Decrypt the message 98 cipher = Cipher(algorithms.AES(self.key_bytes), modes.CTR(nonce), backend=default_backend()) 99 decryptor = cipher.decryptor() 100 decrypted_bytes = decryptor.update(getattr(mp, 'encrypted')) + decryptor.finalize() 101 102 # Parse the decrypted message 103 data = mesh_pb2.Data() 104 data.ParseFromString(decrypted_bytes) 105 mp.decoded.CopyFrom(data) 106 return mp 107 except Exception as e: 108 print(f'Error decrypting message: {e}') 109 print(mp) 110 return None 111 112 113 114 def event_mqtt_connect(self, client, userdata, flags, rc, properties): 115 ''' 116 Callback for when the client receives a CONNACK response from the server. 117 118 :param client: The client instance for this callback 119 :param userdata: The private user data as set in Client() or user_data_set() 120 :param flags: Response flags sent by the broker 121 :param rc: The connection result 122 :param properties: The properties returned by the broker 123 ''' 124 125 if rc == 0: 126 client.subscribe(self.subscribe_topic) 127 else: 128 print(f'Failed to connect to MQTT broker: {rc}') 129 130 131 def event_mqtt_recv(self, client, userdata, msg): 132 ''' 133 Callback for when a message is received from the server. 134 135 :param client: The client instance for this callback 136 :param userdata: The private user data as set in Client() or user_data_set() 137 :param msg: An instance of MQTTMessage. This is a 138 ''' 139 140 try: 141 # Define the service envelope 142 service_envelope = mqtt_pb2.ServiceEnvelope() 143 144 # Parse the message payload 145 service_envelope.ParseFromString(msg.payload) 146 147 # Extract the message packet from the service envelope 148 mp = service_envelope.packet 149 150 # Check if the message is encrypted before decrypting it 151 if mp.HasField('encrypted'): 152 decrypted_mp = self.decrypt_message_packet(mp) 153 if decrypted_mp: 154 mp = decrypted_mp 155 else: 156 return 157 158 portnum_name = portnums_pb2.PortNum.Name(mp.decoded.portnum) 159 160 # Skip if message type doesn't match filter 161 if self.filters and portnum_name not in self.filters: 162 return 163 164 json_packet = json.loads(MessageToJson(mp)) 165 166 # Process the message based on its type 167 if mp.decoded.portnum == portnums_pb2.ADMIN_APP: 168 data = mesh_pb2.Admin() 169 data.ParseFromString(mp.decoded.payload) 170 print(f'{MessageToJson(data)}') 171 172 elif mp.decoded.portnum == portnums_pb2.ATAK_FORWARDER: 173 data = mesh_pb2.AtakForwarder() 174 data.ParseFromString(mp.decoded.payload) 175 print(f'{MessageToJson(data)}') 176 177 elif mp.decoded.portnum == portnums_pb2.ATAK_PLUGIN: 178 data = mesh_pb2.AtakPlugin() 179 data.ParseFromString(mp.decoded.payload) 180 print(f'{MessageToJson(data)}') 181 182 elif mp.decoded.portnum == portnums_pb2.AUDIO_APP: 183 data = mesh_pb2.Audio() 184 data.ParseFromString(mp.decoded.payload) 185 print(f'{MessageToJson(data)}') 186 187 elif mp.decoded.portnum == portnums_pb2.DETECTION_SENSOR_APP: 188 data = mesh_pb2.DetectionSensor() 189 data.ParseFromString(mp.decoded.payload) 190 print(f'{MessageToJson(data)}') 191 192 elif mp.decoded.portnum == portnums_pb2.IP_TUNNEL_APP: 193 data = mesh_pb2.IPTunnel() 194 data.ParseFromString(mp.decoded.payload) 195 print(f'{MessageToJson(data)}') 196 197 elif mp.decoded.portnum == portnums_pb2.NEIGHBORINFO_APP: 198 neighborInfo = mesh_pb2.NeighborInfo() 199 neighborInfo.ParseFromString(mp.decoded.payload) 200 json_packet['decoded']['payload'] = json.loads(MessageToJson(neighborInfo)) 201 print(json.dumps(json_packet)) 202 203 elif mp.decoded.portnum == portnums_pb2.NODEINFO_APP: 204 from_id = getattr(mp, 'from') 205 node_info = mesh_pb2.User() 206 node_info.ParseFromString(mp.decoded.payload) 207 json_packet['decoded']['payload'] = json.loads(MessageToJson(node_info)) 208 print(json.dumps(json_packet)) 209 self.names[from_id] = node_info.long_name 210 211 elif mp.decoded.portnum == portnums_pb2.PAXCOUNTER_APP: 212 data = mesh_pb2.Paxcounter() 213 data.ParseFromString(mp.decoded.payload) 214 print(f'{MessageToJson(data)}') 215 216 elif mp.decoded.portnum == portnums_pb2.POSITION_APP: 217 position = mesh_pb2.Position() 218 position.ParseFromString(mp.decoded.payload) 219 json_packet['decoded']['payload'] = json.loads(MessageToJson(position)) 220 print(json.dumps(json_packet)) 221 222 elif mp.decoded.portnum == portnums_pb2.PRIVATE_APP: 223 data = mesh_pb2.Private() 224 data.ParseFromString(mp.decoded.payload) 225 print(f'{MessageToJson(data)}') 226 227 elif mp.decoded.portnum == portnums_pb2.RANGE_TEST_APP: 228 data = mesh_pb2.RangeTest() 229 data.ParseFromString(mp.decoded.payload) 230 json_packet['decoded']['payload'] = json.loads(MessageToJson(data)) 231 print(json.dumps(json_packet)) 232 233 elif mp.decoded.portnum == portnums_pb2.REMOTE_HARDWARE_APP: 234 data = mesh_pb2.RemoteHardware() 235 data.ParseFromString(mp.decoded.payload) 236 print(f'{MessageToJson(data)}') 237 238 elif mp.decoded.portnum == portnums_pb2.REPLY_APP: 239 data = mesh_pb2.Reply() 240 data.ParseFromString(mp.decoded.payload) 241 print(f'{MessageToJson(data)}') 242 243 elif mp.decoded.portnum == portnums_pb2.ROUTING_APP: 244 routing = mesh_pb2.Routing() 245 routing.ParseFromString(mp.decoded.payload) 246 json_packet['decoded']['payload'] = json.loads(MessageToJson(routing)) 247 print(json.dumps(json_packet)) 248 249 elif mp.decoded.portnum == portnums_pb2.SERIAL_APP: 250 data = mesh_pb2.Serial() 251 data.ParseFromString(mp.decoded.payload) 252 print(f'{MessageToJson(data)}') 253 254 elif mp.decoded.portnum == portnums_pb2.SIMULATOR_APP: 255 data = mesh_pb2.Simulator() 256 data.ParseFromString(mp.decoded.payload) 257 print(f'{MessageToJson(data)}') 258 259 elif mp.decoded.portnum == portnums_pb2.STORE_FORWARD_APP: 260 print(f'{MessageToJson(mp)}') 261 print(f'{mp.decoded.payload}') 262 263 elif mp.decoded.portnum == portnums_pb2.TELEMETRY_APP: 264 telemetry = telemetry_pb2.Telemetry() 265 telemetry.ParseFromString(mp.decoded.payload) 266 json_packet['decoded']['payload'] = json.loads(MessageToJson(telemetry)) 267 print(json.dumps(json_packet)) 268 269 elif mp.decoded.portnum == portnums_pb2.TEXT_MESSAGE_APP: 270 text_payload = mp.decoded.payload.decode('utf-8') 271 json_packet['decoded']['payload'] = text_payload 272 print(json.dumps(json_packet)) 273 274 elif mp.decoded.portnum == portnums_pb2.TEXT_MESSAGE_COMPRESSED_APP: 275 data = mesh_pb2.TextMessageCompressed() 276 data.ParseFromString(mp.decoded.payload) 277 print(f'{MessageToJson(data)}') 278 279 elif mp.decoded.portnum == portnums_pb2.TRACEROUTE_APP: 280 routeDiscovery = mesh_pb2.RouteDiscovery() 281 routeDiscovery.ParseFromString(mp.decoded.payload) 282 json_packet['decoded']['payload'] = json.loads(MessageToJson(routeDiscovery)) 283 print(json.dumps(json_packet)) 284 285 elif mp.decoded.portnum == portnums_pb2.WAYPOINT_APP: 286 data = mesh_pb2.Waypoint() 287 data.ParseFromString(mp.decoded.payload) 288 print(f'{MessageToJson(data)}') 289 290 elif mp.decoded.portnum == portnums_pb2.ZPS_APP: 291 data = mesh_pb2.Zps() 292 data.ParseFromString(mp.decoded.payload) 293 print(f'{MessageToJson(data)}') 294 295 else: 296 print(f'UNKNOWN: Received Portnum name: {portnum_name}') 297 print(f'UNKNOWN: {MessageToJson(mp)}') 298 299 except Exception as e: 300 print(f'Error processing message: {e}') 301 print(mp) 302 303 304 if __name__ == '__main__': 305 parser = argparse.ArgumentParser(description='Meshtastic MQTT Interface') 306 parser.add_argument('--broker', default='mqtt.meshtastic.org', help='MQTT broker address') 307 parser.add_argument('--port', default=1883, type=int, help='MQTT broker port') 308 parser.add_argument('--root', default='msh/US/2/e/', help='Root topic') 309 parser.add_argument('--channel', default='LongFast', help='Channel name') 310 parser.add_argument('--username', default='meshdev', help='MQTT username') 311 parser.add_argument('--password', default='large4cats', help='MQTT password') 312 parser.add_argument('--key', default='AQ==', help='Encryption key') 313 parser.add_argument('--filter', help='Filter message types (comma-separated). Example: NODEINFO,POSITION,TEXT_MESSAGE') 314 args = parser.parse_args() 315 316 client = MeshtasticMQTT() 317 if args.filter: 318 client.filters = [f'{f.strip()}_APP' for f in args.filter.upper().split(',')] 319 else: 320 client.filters = None 321 client.connect(args.broker, args.port, args.root, args.channel, args.username, args.password, args.key)