2024-05-06 04:00:12 +00:00
#!/usr/bin/env python
2024-05-03 17:16:58 +00:00
# Meshtastic MQTT Interface - Developed by acidvegas in Python (https://acid.vegas/meshtastic)
2024-04-30 00:03:38 +00:00
2024-05-03 17:16:58 +00:00
import argparse
2024-04-30 00:03:38 +00:00
import base64
2024-05-03 17:16:58 +00:00
import logging
2024-04-30 00:03:38 +00:00
try :
2024-05-03 17:16:58 +00:00
from cryptography . hazmat . primitives . ciphers import Cipher , algorithms , modes
2024-05-06 04:00:12 +00:00
from cryptography . hazmat . backends import default_backend
2024-04-30 00:03:38 +00:00
except ImportError :
2024-05-03 17:16:58 +00:00
raise SystemExit ( ' missing the cryptography module (pip install cryptography) ' )
2024-04-30 00:03:38 +00:00
try :
2024-05-03 17:16:58 +00:00
from meshtastic import mesh_pb2 , mqtt_pb2 , portnums_pb2 , telemetry_pb2
2024-04-30 00:03:38 +00:00
except ImportError :
2024-05-03 17:16:58 +00:00
raise SystemExit ( ' missing the meshtastic module (pip install meshtastic) ' )
2024-04-30 00:03:38 +00:00
try :
2024-05-03 17:16:58 +00:00
import paho . mqtt . client as mqtt
2024-04-30 00:03:38 +00:00
except ImportError :
2024-05-03 17:16:58 +00:00
raise SystemExit ( ' missing the paho-mqtt module (pip install paho-mqtt) ' )
2024-04-30 00:03:38 +00:00
2024-05-07 00:53:19 +00:00
# Initialize the logging module
logging . basicConfig ( level = logging . INFO , format = ' %(asctime)s - %(levelname)s - %(message)s ' , datefmt = ' % Y- % m- %d % I: % M: % S ' )
2024-04-30 00:03:38 +00:00
2024-05-03 17:16:58 +00:00
def decode_encrypted ( message_packet ) :
'''
Decrypt an encrypted message packet .
2024-04-30 00:03:38 +00:00
2024-05-06 04:00:12 +00:00
: param message_packet : The message packet to decrypt
'''
2024-05-07 02:34:03 +00:00
try :
2024-05-07 01:34:25 +00:00
# Ensure the key is formatted and padded correctly before turning it into bytes
padded_key = args . key . ljust ( len ( args . key ) + ( ( 4 - ( len ( args . key ) % 4 ) ) % 4 ) , ' = ' )
key = padded_key . replace ( ' - ' , ' + ' ) . replace ( ' _ ' , ' / ' )
2024-05-03 17:16:58 +00:00
key_bytes = base64 . b64decode ( key . encode ( ' ascii ' ) )
2024-05-07 02:34:03 +00:00
2024-05-07 01:34:25 +00:00
# Extract the nonce from the packet
2024-05-03 17:16:58 +00:00
nonce_packet_id = getattr ( message_packet , ' id ' ) . to_bytes ( 8 , ' little ' )
nonce_from_node = getattr ( message_packet , ' from ' ) . to_bytes ( 8 , ' little ' )
nonce = nonce_packet_id + nonce_from_node
2024-05-07 01:34:25 +00:00
# Decrypt the message
2024-05-03 17:16:58 +00:00
cipher = Cipher ( algorithms . AES ( key_bytes ) , modes . CTR ( nonce ) , backend = default_backend ( ) )
decryptor = cipher . decryptor ( )
decrypted_bytes = decryptor . update ( getattr ( message_packet , ' encrypted ' ) ) + decryptor . finalize ( )
2024-05-07 01:34:25 +00:00
# Parse the decrypted message
2024-05-03 17:16:58 +00:00
data = mesh_pb2 . Data ( )
data . ParseFromString ( decrypted_bytes )
message_packet . decoded . CopyFrom ( data )
if message_packet . decoded . portnum == portnums_pb2 . TEXT_MESSAGE_APP :
text_payload = message_packet . decoded . payload . decode ( ' utf-8 ' )
2024-05-07 00:53:19 +00:00
text = {
' message ' : text_payload ,
' from ' : getattr ( message_packet , ' from ' ) ,
' id ' : getattr ( message_packet , ' id ' ) ,
' to ' : getattr ( message_packet , ' to ' )
}
logging . info ( ' Received text message: ' )
logging . info ( text )
2024-05-07 02:35:42 +00:00
2024-05-07 02:34:03 +00:00
elif message_packet . decoded . portnum == portnums_pb2 . MAP_REPORT_APP :
2024-05-07 02:35:42 +00:00
pos = mesh_pb2 . Position ( )
pos . ParseFromString ( message_packet . decoded . payload )
logging . info ( ' Received map report: ' )
2024-05-07 02:34:03 +00:00
logging . info ( pos )
2024-04-30 00:03:38 +00:00
2024-05-03 17:16:58 +00:00
elif message_packet . decoded . portnum == portnums_pb2 . NODEINFO_APP :
2024-05-07 01:34:25 +00:00
info = mesh_pb2 . User ( )
info . ParseFromString ( message_packet . decoded . payload )
logging . info ( ' Received node info: ' )
logging . info ( info )
2024-04-30 00:03:38 +00:00
2024-05-03 17:16:58 +00:00
elif message_packet . decoded . portnum == portnums_pb2 . POSITION_APP :
pos = mesh_pb2 . Position ( )
pos . ParseFromString ( message_packet . decoded . payload )
2024-05-07 00:53:19 +00:00
logging . info ( ' Received position: ' )
logging . info ( pos )
2024-04-30 00:03:38 +00:00
2024-05-03 17:16:58 +00:00
elif message_packet . decoded . portnum == portnums_pb2 . TELEMETRY_APP :
env = telemetry_pb2 . Telemetry ( )
env . ParseFromString ( message_packet . decoded . payload )
2024-05-07 00:53:19 +00:00
logging . info ( ' Received telemetry: ' )
logging . info ( env )
2024-04-30 00:03:38 +00:00
2024-05-03 17:16:58 +00:00
except Exception as e :
logging . error ( f ' Failed to decrypt message: { str ( e ) } ' )
2024-04-30 00:03:38 +00:00
2024-05-03 17:16:58 +00:00
def on_connect ( client , userdata , flags , rc , properties ) :
'''
Callback for when the client receives a CONNACK response from the server .
2024-05-06 04:00:12 +00:00
2024-05-03 17:16:58 +00:00
: param client : The client instance for this callback
: param userdata : The private user data as set in Client ( ) or user_data_set ( )
: param flags : Response flags sent by the broker
: param rc : The connection result
: param properties : The properties returned by the broker
'''
if rc == 0 :
2024-05-07 00:53:19 +00:00
logging . info ( ' Connected to MQTT broker ' )
2024-05-03 17:16:58 +00:00
else :
logging . error ( f ' Failed to connect to MQTT broker: { rc } ' )
def on_message ( client , userdata , msg ) :
'''
Callback for when a message is received from the server .
2024-05-06 04:00:12 +00:00
2024-05-03 17:16:58 +00:00
: param client : The client instance for this callback
: param userdata : The private user data as set in Client ( ) or user_data_set ( )
: param msg : An instance of MQTTMessage . This is a
'''
2024-05-07 02:34:03 +00:00
2024-05-07 01:34:25 +00:00
# Define the service envelope
2024-05-03 17:16:58 +00:00
service_envelope = mqtt_pb2 . ServiceEnvelope ( )
try :
2024-05-07 02:34:03 +00:00
# Parse the message payload
2024-05-03 17:16:58 +00:00
service_envelope . ParseFromString ( msg . payload )
2024-05-07 01:34:25 +00:00
2024-05-07 00:53:19 +00:00
logging . info ( ' Received a packet: ' )
logging . info ( service_envelope )
2024-05-07 01:34:25 +00:00
# Extract the message packet from the service envelope
2024-05-03 17:16:58 +00:00
message_packet = service_envelope . packet
2024-05-07 02:34:03 +00:00
except Exception as e :
2024-05-06 04:00:12 +00:00
#logging.error(f'Failed to parse message: {str(e)}')
2024-05-03 17:16:58 +00:00
return
2024-05-07 01:34:25 +00:00
# Check if the message is encrypted before decrypting it
2024-05-03 17:16:58 +00:00
if message_packet . HasField ( ' encrypted ' ) and not message_packet . HasField ( ' decoded ' ) :
decode_encrypted ( message_packet )
2024-05-07 01:34:25 +00:00
# If the message is not encrypted, log the payload (this should not happen)
2024-05-07 00:53:19 +00:00
else :
logging . warning ( ' Received an unencrypted message ' )
logging . info ( f ' Payload: { message_packet } ' )
2024-04-30 00:03:38 +00:00
2024-05-06 04:00:12 +00:00
def on_subscribe ( client , userdata , mid , reason_code_list , properties ) :
'''
Callback for when the client receives a SUBACK response from the server .
: param client : The client instance for this callback
: param userdata : The private user data as set in Client ( ) or user_data_set ( )
: param mid : The message ID of the subscribe request
: param reason_code_list : A list of SUBACK reason codes
: param properties : The properties returned by the broker
'''
2024-05-07 00:53:19 +00:00
# Since we subscribed only for a single channel, reason_code_list contains a single entry
2024-05-06 04:00:12 +00:00
if reason_code_list [ 0 ] . is_failure :
2024-05-07 00:53:19 +00:00
logging . error ( f ' Broker rejected you subscription: { reason_code_list [ 0 ] } ' )
2024-05-06 04:00:12 +00:00
else :
2024-05-07 00:53:19 +00:00
logging . info ( f ' Broker granted the following QoS: { reason_code_list [ 0 ] . value } ' )
2024-05-06 04:00:12 +00:00
def on_unsubscribe ( client , userdata , mid , reason_code_list , properties ) :
'''
Callback for when the client receives a UNSUBACK response from the server .
: param client : The client instance for this callback
: param userdata : The private user data as set in Client ( ) or user_data_set ( )
: param mid : The message ID of the unsubscribe request
: param reason_code_list : A list of UNSUBACK reason codes
: param properties : The properties returned by the broker
'''
2024-05-07 01:34:25 +00:00
# reason_code_list is only present in MQTTv5, it will always be empty in MQTTv3
2024-05-06 04:00:12 +00:00
if len ( reason_code_list ) == 0 or not reason_code_list [ 0 ] . is_failure :
2024-05-07 00:53:19 +00:00
logging . info ( ' Broker accepted the unsubscription(s) ' )
2024-05-06 04:00:12 +00:00
else :
2024-05-07 00:53:19 +00:00
logging . error ( f ' Broker replied with failure: { reason_code_list [ 0 ] } ' )
2024-05-07 01:34:25 +00:00
# Disconnect from the broker
2024-05-06 04:00:12 +00:00
client . disconnect ( )
2024-04-30 00:03:38 +00:00
if __name__ == ' __main__ ' :
2024-05-03 17:16:58 +00:00
parser = argparse . ArgumentParser ( description = ' Mesh MQTT ' )
parser . add_argument ( ' --broker ' , default = ' mqtt.meshtastic.org ' , help = ' MQTT broker address ' )
parser . add_argument ( ' --port ' , default = 1883 , type = int , help = ' MQTT broker port ' )
2024-05-06 04:00:12 +00:00
parser . add_argument ( ' --root ' , default = ' # ' , help = ' Root topic ' )
2024-05-03 17:16:58 +00:00
parser . add_argument ( ' --tls ' , action = ' store_true ' , help = ' Enable TLS/SSL ' )
parser . add_argument ( ' --username ' , default = ' meshdev ' , help = ' MQTT username ' )
parser . add_argument ( ' --password ' , default = ' large4cats ' , help = ' MQTT password ' )
parser . add_argument ( ' --key ' , default = ' AQ== ' , help = ' Encryption key ' )
args = parser . parse_args ( )
2024-05-07 01:34:25 +00:00
# Set the broadcast ID (Do we need to change this for a custom channel?)
broadcast_id = 4294967295
2024-05-07 02:34:03 +00:00
2024-05-06 04:00:12 +00:00
# Create the MQTT client
client = mqtt . Client ( mqtt . CallbackAPIVersion . VERSION2 , client_id = ' ' , clean_session = True , userdata = None ) # Defaults to mqtt.MQTTv311 (change with protocol=mqtt.MQTTv5)
2024-05-03 17:16:58 +00:00
2024-05-06 04:00:12 +00:00
# Set the authentication details
2024-05-03 17:16:58 +00:00
client . username_pw_set ( username = args . username , password = args . password )
2024-05-06 04:00:12 +00:00
# Enable TLS/SSL if the --tls flag is set
if args . tls :
import ssl
client . tls_set ( cert_reqs = ssl . CERT_REQUIRED , tls_version = ssl . PROTOCOL_TLSv1_2 )
client . tls_insecure_set ( False )
# Set the callbacks
client . on_connect = on_connect
client . on_message = on_message
client . on_subscribe = on_subscribe
client . on_unsubscribe = on_unsubscribe
# Connect to the broker
2024-05-03 17:16:58 +00:00
client . connect ( args . broker , args . port , 60 )
2024-05-06 04:00:12 +00:00
# Subscribe to the root topic
2024-05-03 17:16:58 +00:00
client . subscribe ( args . root , 0 )
2024-05-06 04:00:12 +00:00
# Start the keep-alive loop
client . loop_forever ( )