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)