2023-04-17 23:50:28 +00:00
import asyncio
2023-04-18 05:44:31 +00:00
import time
2023-04-17 23:50:28 +00:00
import logging
2023-04-18 16:45:19 +00:00
import re
2023-04-18 16:25:42 +00:00
import sqlite3
2023-04-18 05:44:31 +00:00
import yaml
2023-04-17 23:50:28 +00:00
import meshtastic . tcp_interface
2023-04-18 01:24:51 +00:00
import meshtastic . serial_interface
2023-04-18 05:44:31 +00:00
from nio import AsyncClient , AsyncClientConfig , MatrixRoom , RoomMessageText
2023-04-17 23:50:28 +00:00
from pubsub import pub
from yaml . loader import SafeLoader
2023-04-20 18:27:04 +00:00
from typing import List
2023-04-17 23:50:28 +00:00
2023-04-18 05:44:31 +00:00
bot_start_time = int ( time . time ( ) * 1000 )
2023-04-17 23:50:28 +00:00
logging . basicConfig ( )
logger = logging . getLogger ( name = " meshtastic.matrix.relay " )
2023-04-18 05:44:31 +00:00
# Load configuration
2023-04-17 23:50:28 +00:00
with open ( " config.yaml " , " r " ) as f :
relay_config = yaml . load ( f , Loader = SafeLoader )
2023-04-18 05:44:31 +00:00
logger . setLevel ( getattr ( logging , relay_config [ " logging " ] [ " level " ] . upper ( ) ) )
2023-04-18 16:45:19 +00:00
2023-04-18 16:25:42 +00:00
# Initialize SQLite database
def initialize_database ( ) :
2023-04-19 01:08:23 +00:00
with sqlite3 . connect ( " meshtastic.sqlite " ) as conn :
cursor = conn . cursor ( )
cursor . execute (
" CREATE TABLE IF NOT EXISTS longnames (meshtastic_id TEXT PRIMARY KEY, longname TEXT) "
)
conn . commit ( )
2023-04-18 16:25:42 +00:00
# Get the longname for a given Meshtastic ID
def get_longname ( meshtastic_id ) :
2023-04-19 01:08:23 +00:00
with sqlite3 . connect ( " meshtastic.sqlite " ) as conn :
cursor = conn . cursor ( )
cursor . execute (
" SELECT longname FROM longnames WHERE meshtastic_id=? " , ( meshtastic_id , )
)
result = cursor . fetchone ( )
2023-04-18 16:25:42 +00:00
return result [ 0 ] if result else None
2023-04-19 01:08:23 +00:00
2023-04-18 16:25:42 +00:00
def save_longname ( meshtastic_id , longname ) :
2023-04-19 01:08:23 +00:00
with sqlite3 . connect ( " meshtastic.sqlite " ) as conn :
cursor = conn . cursor ( )
cursor . execute (
" INSERT OR REPLACE INTO longnames (meshtastic_id, longname) VALUES (?, ?) " ,
( meshtastic_id , longname ) ,
)
conn . commit ( )
2023-04-17 23:50:28 +00:00
2023-04-18 16:25:42 +00:00
def update_longnames ( ) :
if meshtastic_interface . nodes :
for node in meshtastic_interface . nodes . values ( ) :
user = node . get ( " user " )
if user :
meshtastic_id = user [ " id " ]
longname = user . get ( " longName " , " N/A " )
save_longname ( meshtastic_id , longname )
2023-04-18 16:45:19 +00:00
2023-04-18 05:44:31 +00:00
# Initialize Meshtastic interface
2023-04-18 01:24:51 +00:00
connection_type = relay_config [ " meshtastic " ] [ " connection_type " ]
if connection_type == " serial " :
serial_port = relay_config [ " meshtastic " ] [ " serial_port " ]
logger . info ( f " Connecting to radio using serial port { serial_port } ... " )
meshtastic_interface = meshtastic . serial_interface . SerialInterface ( serial_port )
else :
target_host = relay_config [ " meshtastic " ] [ " host " ]
logger . info ( f " Connecting to radio at { target_host } ... " )
meshtastic_interface = meshtastic . tcp_interface . TCPInterface ( hostname = target_host )
2023-04-17 23:50:28 +00:00
matrix_client = None
# Matrix configuration
matrix_homeserver = relay_config [ " matrix " ] [ " homeserver " ]
matrix_access_token = relay_config [ " matrix " ] [ " access_token " ]
bot_user_id = relay_config [ " matrix " ] [ " bot_user_id " ]
2023-04-20 18:27:04 +00:00
matrix_rooms : List [ dict ] = relay_config [ " matrix_rooms " ]
2023-04-17 23:50:28 +00:00
2023-04-18 05:44:31 +00:00
# Send message to the Matrix room
2023-04-20 18:27:04 +00:00
async def matrix_relay ( matrix_client , room_id , message , longname , meshnet_name ) :
2023-04-17 23:50:28 +00:00
try :
2023-04-18 19:06:00 +00:00
content = {
" msgtype " : " m.text " ,
" body " : message ,
" meshtastic_longname " : longname ,
" meshtastic_meshnet " : meshnet_name ,
}
2023-04-17 23:50:28 +00:00
await asyncio . wait_for (
matrix_client . room_send (
2023-04-20 18:27:04 +00:00
room_id = room_id ,
2023-04-17 23:50:28 +00:00
message_type = " m.room.message " ,
2023-04-18 19:06:00 +00:00
content = content ,
2023-04-17 23:50:28 +00:00
) ,
timeout = 0.5 ,
)
2023-04-20 18:27:04 +00:00
logger . info ( f " Sent inbound radio message to matrix room: { room_id } " )
2023-04-17 23:50:28 +00:00
except asyncio . TimeoutError :
2023-04-18 05:44:31 +00:00
logger . error ( f " Timed out while waiting for Matrix response " )
2023-04-17 23:50:28 +00:00
except Exception as e :
2023-04-20 18:27:04 +00:00
logger . error ( f " Error sending radio message to matrix room { room_id } : { e } " )
2023-04-17 23:50:28 +00:00
# Callback for new messages from Meshtastic
def on_meshtastic_message ( packet , loop = None ) :
sender = packet [ " fromId " ]
if " text " in packet [ " decoded " ] and packet [ " decoded " ] [ " text " ] :
text = packet [ " decoded " ] [ " text " ]
2023-04-20 02:11:27 +00:00
if " channel " in packet :
channel = packet [ " channel " ]
else :
if packet [ " decoded " ] [ " portnum " ] == " TEXT_MESSAGE_APP " :
channel = 0
else :
logger . debug ( f " Unknown packet " )
return
2023-04-20 00:50:14 +00:00
2023-04-20 18:27:04 +00:00
# Check if the channel is mapped to a Matrix room in the configuration
channel_mapped = False
for room in matrix_rooms :
if room [ " meshtastic_channel " ] == channel :
channel_mapped = True
break
2023-04-20 00:50:14 +00:00
2023-04-20 18:27:04 +00:00
if not channel_mapped :
logger . debug ( f " Skipping message from unmapped channel { channel } " )
return
2023-04-20 00:50:14 +00:00
2023-04-20 18:27:04 +00:00
logger . info ( f " Processing inbound radio message from { sender } on channel { channel } " )
2023-04-20 02:11:27 +00:00
2023-04-20 18:27:04 +00:00
longname = get_longname ( sender ) or sender
meshnet_name = relay_config [ " meshtastic " ] [ " meshnet_name " ]
formatted_message = f " [ { longname } / { meshnet_name } ]: { text } "
logger . info ( f " Relaying Meshtastic message from { longname } to Matrix: { formatted_message } " )
for room in matrix_rooms :
if room [ " meshtastic_channel " ] == channel :
asyncio . run_coroutine_threadsafe (
matrix_relay ( matrix_client , room [ " id " ] , formatted_message , longname , meshnet_name ) ,
loop = loop ,
)
2023-04-20 02:11:27 +00:00
else :
2023-04-20 02:44:51 +00:00
portnum = packet [ " decoded " ] [ " portnum " ]
if portnum == " TELEMETRY_APP " :
logger . debug ( " Ignoring Telemetry packet " )
elif portnum == " POSITION_APP " :
logger . debug ( " Ignoring Position packet " )
elif portnum == " ADMIN_APP " :
logger . debug ( " Ignoring Admin packet " )
else :
logger . debug ( f " Ignoring Unknown packet " )
2023-04-20 02:11:27 +00:00
2023-04-17 23:50:28 +00:00
2023-04-19 21:06:07 +00:00
def truncate_message ( text , max_bytes = 234 ) : #234 is the maximum that we can run without an error. Trying it for awhile, otherwise lower this to 230 or less.
2023-04-19 01:08:23 +00:00
"""
Truncate the given text to fit within the specified byte size .
: param text : The text to truncate .
: param max_bytes : The maximum allowed byte size for the truncated text .
: return : The truncated text .
"""
2023-04-18 23:28:02 +00:00
truncated_text = text . encode ( ' utf-8 ' ) [ : max_bytes ] . decode ( ' utf-8 ' , ' ignore ' )
return truncated_text
2023-04-18 19:06:00 +00:00
2023-04-19 01:08:23 +00:00
2023-04-20 18:27:04 +00:00
# Callback for new messages in Matrix room
2023-04-17 23:50:28 +00:00
# Callback for new messages in Matrix room
async def on_room_message ( room : MatrixRoom , event : RoomMessageText ) - > None :
2023-04-19 01:08:23 +00:00
full_display_name = " Unknown user "
2023-04-20 18:27:04 +00:00
if event . sender != bot_user_id :
2023-04-18 05:44:31 +00:00
message_timestamp = event . server_timestamp
2023-04-17 23:50:28 +00:00
2023-04-18 05:44:31 +00:00
if message_timestamp > bot_start_time :
2023-04-17 23:50:28 +00:00
text = event . body . strip ( )
2023-04-19 01:49:37 +00:00
# Remove unnecessary part of the message content
split_content = text . split ( " ]: " , 1 )
if len ( split_content ) > 1 :
text = split_content [ 1 ]
2023-04-18 05:44:31 +00:00
logger . info ( f " Processing matrix message from { event . sender } : { text } " )
2023-04-18 19:06:00 +00:00
longname = event . source [ ' content ' ] . get ( " meshtastic_longname " )
meshnet_name = event . source [ ' content ' ] . get ( " meshtastic_meshnet " )
2023-04-19 01:08:23 +00:00
local_meshnet_name = relay_config [ " meshtastic " ] [ " meshnet_name " ]
2023-04-18 05:44:31 +00:00
2023-04-18 19:06:00 +00:00
if longname and meshnet_name :
2023-04-19 01:08:23 +00:00
if meshnet_name != local_meshnet_name :
short_longname = longname [ : 3 ]
short_meshnet_name = meshnet_name [ : 4 ]
text = f " { short_longname } / { short_meshnet_name } : { text } "
else :
logger . info ( " Ignoring message from the same meshnet. " )
return
2023-04-18 19:06:00 +00:00
else :
display_name_response = await matrix_client . get_displayname ( event . sender )
full_display_name = display_name_response . displayname or event . sender
short_display_name = full_display_name [ : 5 ]
text = f " { short_display_name } [M]: { text } "
2023-04-18 05:44:31 +00:00
2023-04-18 23:28:02 +00:00
text = truncate_message ( text )
2023-04-20 18:27:04 +00:00
# Find the corresponding room configuration
room_config = None
for config in matrix_rooms :
if config [ " id " ] == room . room_id :
room_config = config
break
if room_config :
meshtastic_channel = room_config [ " meshtastic_channel " ]
if relay_config [ " meshtastic " ] [ " broadcast_enabled " ] :
logger . info ( f " Sending radio message from { full_display_name } to radio broadcast " )
meshtastic_interface . sendText (
text = text , channelIndex = meshtastic_channel
)
else :
logger . debug ( f " Broadcast not supported: Message from { full_display_name } dropped. " )
2023-04-17 23:50:28 +00:00
async def main ( ) :
global matrix_client
2023-04-18 16:25:42 +00:00
# Initialize the SQLite database
initialize_database ( )
2023-04-17 23:50:28 +00:00
config = AsyncClientConfig ( encryption_enabled = False )
matrix_client = AsyncClient ( matrix_homeserver , bot_user_id , config = config )
matrix_client . access_token = matrix_access_token
2023-04-20 19:18:04 +00:00
logger . info ( " Connecting to Matrix server... " )
try :
login_response = await matrix_client . login ( matrix_access_token )
logger . info ( f " Login response: { login_response } " )
except Exception as e :
logger . error ( f " Error connecting to Matrix server: { e } " )
return
2023-04-17 23:50:28 +00:00
# Register the Meshtastic message callback
logger . info ( f " Listening for inbound radio messages ... " )
pub . subscribe (
on_meshtastic_message , " meshtastic.receive " , loop = asyncio . get_event_loop ( )
)
# Register the message callback
logger . info ( f " Listening for inbound matrix messages ... " )
matrix_client . add_event_callback ( on_room_message , RoomMessageText )
# Start the Matrix client
2023-04-18 16:25:42 +00:00
while True :
2023-04-20 19:18:04 +00:00
try :
# Update longnames
update_longnames ( )
logger . info ( " Syncing with Matrix server... " )
await matrix_client . sync_forever ( timeout = 30000 )
logger . info ( " Sync completed. " )
except Exception as e :
logger . error ( f " Error syncing with Matrix server: { e } " )
2023-04-17 23:50:28 +00:00
2023-04-18 16:25:42 +00:00
await asyncio . sleep ( 60 ) # Update longnames every 60 seconds
2023-04-18 16:45:19 +00:00
2023-04-18 17:31:03 +00:00
asyncio . run ( main ( ) )