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