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()