From 4ce36f82222ee5d50dc6508b4d13b24f1a2ae8a1 Mon Sep 17 00:00:00 2001 From: Jeremiah K Date: Mon, 17 Apr 2023 20:24:51 -0500 Subject: [PATCH 1/6] Added serial connection support --- main.py | 18 ++++++++++++++---- meshtastic.sqlite | Bin 0 -> 16384 bytes sample_config.yaml | 14 ++++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 meshtastic.sqlite diff --git a/main.py b/main.py index 7c0cb94..49b1b9b 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ import logging import yaml import re import meshtastic.tcp_interface +import meshtastic.serial_interface from nio import AsyncClient, AsyncClientConfig, MatrixRoom, RoomMessageText, RoomMessage from pubsub import pub from meshtastic import mesh_pb2 @@ -28,14 +29,23 @@ elif relay_config["logging"]["level"] == "warn": elif relay_config["logging"]["level"] == "error": logger.setLevel(logging.ERROR) -target_host = relay_config["meshtastic"]["host"] # Connect to the Meshtastic device logger.info(f"Starting Meshtastic <==> Matrix Relay...") -logger.info(f"Connecting to radio at {target_host} ...") -meshtastic_interface = meshtastic.tcp_interface.TCPInterface(hostname=target_host) -logger.info(f"Connected to radio at {target_host}.") +# Add a new configuration option to select between serial and network connections +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) + logger.info(f"Connected to radio using serial port {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) + logger.info(f"Connected to radio at {target_host}.") matrix_client = None diff --git a/meshtastic.sqlite b/meshtastic.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..9ac2eb195fbb81cca163af9e0dd599f80222f05a GIT binary patch literal 16384 zcmeI%K}*9h7=YoV8&ibQ-Fh12=wJxqS#U~_=_XnWx>Knvj>6jNT45px`+@!+kEVeh z=E18l-azs-4awI$?Ira7Zjx3;Jr}Disnm(+2`R;yQbGvN*0!xp`Q4Yzz#q{QM-NwB z|68>DmheCQFB>QbAbMpwzx(qyJAlZ7dp*uDt$ zDAFn#olmrC?yAFduEsZ!zSN<*4ad__I8)bpHdLj_=Vle#;YdG5Lsgk|)m)}_dX;1^ z&CJ16c7h;~A1-rV%uVSUZ!4#}J=w>} zTUEGz?AD2oU8qmt`!#mAZhCb?K>z^+5I_I{1Q0*~0R#|00D)Z Date: Tue, 18 Apr 2023 00:44:31 -0500 Subject: [PATCH 2/6] Optimized code, removed DMs for now --- .gitignore | 3 +- main.py | 145 +++++++++++------------------------------------------ 2 files changed, 30 insertions(+), 118 deletions(-) diff --git a/.gitignore b/.gitignore index d46ffab..90ab8a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .pyenv -config.yaml \ No newline at end of file +config.yaml +meshtastic.sqlite \ No newline at end of file diff --git a/main.py b/main.py index 49b1b9b..63cdfdf 100644 --- a/main.py +++ b/main.py @@ -1,51 +1,35 @@ import asyncio -import threading -import json -import sqlite3 +import time import logging -import yaml import re +import yaml import meshtastic.tcp_interface import meshtastic.serial_interface -from nio import AsyncClient, AsyncClientConfig, MatrixRoom, RoomMessageText, RoomMessage +from nio import AsyncClient, AsyncClientConfig, MatrixRoom, RoomMessageText from pubsub import pub -from meshtastic import mesh_pb2 from yaml.loader import SafeLoader +bot_start_time = int(time.time() * 1000) + logging.basicConfig() logger = logging.getLogger(name="meshtastic.matrix.relay") -# Collect configuration -relay_config = None +# Load configuration with open("config.yaml", "r") as f: relay_config = yaml.load(f, Loader=SafeLoader) -if relay_config["logging"]["level"] == "debug": - logger.setLevel(logging.DEBUG) -elif relay_config["logging"]["level"] == "info": - logger.setLevel(logging.INFO) -elif relay_config["logging"]["level"] == "warn": - logger.setLevel(logging.WARN) -elif relay_config["logging"]["level"] == "error": - logger.setLevel(logging.ERROR) +logger.setLevel(getattr(logging, relay_config["logging"]["level"].upper())) - -# Connect to the Meshtastic device -logger.info(f"Starting Meshtastic <==> Matrix Relay...") - -# Add a new configuration option to select between serial and network connections +# Initialize Meshtastic interface 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) - logger.info(f"Connected to radio using serial port {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) - logger.info(f"Connected to radio at {target_host}.") matrix_client = None @@ -55,43 +39,7 @@ matrix_access_token = relay_config["matrix"]["access_token"] bot_user_id = relay_config["matrix"]["bot_user_id"] matrix_room_id = relay_config["matrix"]["room_id"] -# SQLite configuration -db_file = "meshtastic.sqlite" -db = sqlite3.connect(db_file) - -# Initialize the database -db.execute("CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, longname TEXT)") -db.execute( - "CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, sender_id TEXT, text TEXT, timestamp INTEGER)" -) -db.commit() - - -# Function to insert or update a node -def upsert_node(id, longname): - db.execute( - "INSERT OR REPLACE INTO nodes (id, longname) VALUES (?, ?)", (id, longname) - ) - db.commit() - - -# Function to insert a message -def insert_message(sender_id, text, timestamp): - db.execute( - "INSERT INTO messages (sender_id, text, timestamp) VALUES (?, ?, ?)", - (sender_id, text, timestamp), - ) - db.commit() - - -# Function to get the node's longname -def get_node_longname(sender): - cursor = db.cursor() - cursor.execute("SELECT longname FROM nodes WHERE id = ?", (sender,)) - row = cursor.fetchone() - return row[0] if row else sender - - +# Send message to the Matrix room async def matrix_relay(matrix_client, message): try: await asyncio.wait_for( @@ -105,86 +53,50 @@ async def matrix_relay(matrix_client, message): logger.info(f"Sent inbound radio message to matrix room: {matrix_room_id}") except asyncio.TimeoutError: - logger.error( - f"Timed out while waiting for Matrix response - room {matrix_room_id}: {e}" - ) + logger.error(f"Timed out while waiting for Matrix response") except Exception as e: - logger.error( - f"Error sending radio message to matrix room {matrix_room_id}: {e}" - ) - + logger.error(f"Error sending radio message to matrix room {matrix_room_id}: {e}") # Callback for new messages from Meshtastic def on_meshtastic_message(packet, loop=None): - global matrix_client sender = packet["fromId"] if "text" in packet["decoded"] and packet["decoded"]["text"]: text = packet["decoded"]["text"] - # timestamp = packet["received"] - if logger.level == logging.DEBUG: - logger.debug(f"Processing radio message from {sender}: {text}") - elif logger.level == logging.INFO: - logger.info(f"Processing inbound radio message from {sender}") + logger.info(f"Processing inbound radio message from {sender}") formatted_message = f"{sender}: {text}" - # create an event loop asyncio.run_coroutine_threadsafe( matrix_relay(matrix_client, formatted_message), loop=loop, ) - # insert_message(sender, text, timestamp) - - # Callback for new messages in Matrix room async def on_room_message(room: MatrixRoom, event: RoomMessageText) -> None: - logger.info( - f"Detected inbound matrix message from {event.sender} in room {room.room_id}" - ) - if room.room_id == matrix_room_id and event.sender != bot_user_id: - target_node = None + message_timestamp = event.server_timestamp - if event.formatted_body: - text = event.formatted_body.strip() - else: + # Only process messages with a timestamp greater than the bot's start time + if message_timestamp > bot_start_time: text = event.body.strip() - logger.debug(f"Processing matrix message from {event.sender}: {text}") + logger.info(f"Processing matrix message from {event.sender}: {text}") - # Opportunistically detect node in message text !124abcd: - match = re.search(r"(![\da-z]+):", text) - if match: - target_node = match.group(1) + display_name_response = await matrix_client.get_displayname(event.sender) + display_name = display_name_response.displayname or event.sender - text = re.sub(r".*?", "", text) - text = f"{event.source['sender']}: {text}" - text = text[0:80] + text = f"{display_name}: {text}" + text = text[0:80] + + if relay_config["meshtastic"]["broadcast_enabled"]: + logger.info(f"Sending radio message from {display_name} to radio broadcast") + meshtastic_interface.sendText( + text=text, channelIndex=relay_config["meshtastic"]["channel"] + ) + else: + logger.debug(f"Broadcast not supported: Message from {display_name} dropped.") - if target_node: - logger.debug( - f"Sending radio message from {event.sender} to {target_node} ..." - ) - meshtastic_interface.sendText( - text=text, - channelIndex=relay_config["meshtastic"]["channel"], - destinationId=target_node, - ) - logger.info(f"Sent radio message from {event.sender} to {target_node}") - elif relay_config["meshtastic"]["broadcast_enabled"]: - logger.debug( - f"Sending radio message from {event.sender} to radio broadcast ..." - ) - meshtastic_interface.sendText( - text=text, channelIndex=relay_config["meshtastic"]["channel"] - ) - logger.info(f"Sent radio message from {event.sender} to radio broadcast") - elif not relay_config["meshtastic"]["broadcast_enabled"]: - logger.debug( - f"Broadcast not supported: Message from {event.sender} dropped." - ) async def main(): @@ -207,5 +119,4 @@ async def main(): # Start the Matrix client await matrix_client.sync_forever(timeout=30000) - asyncio.run(main()) From 5a8a9193cfcbb0627ca2d5db707522ce63a5f60f Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 18 Apr 2023 10:24:00 -0500 Subject: [PATCH 3/6] Delete meshtastic.sqlite --- meshtastic.sqlite | Bin 16384 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 meshtastic.sqlite diff --git a/meshtastic.sqlite b/meshtastic.sqlite deleted file mode 100644 index 9ac2eb195fbb81cca163af9e0dd599f80222f05a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI%K}*9h7=YoV8&ibQ-Fh12=wJxqS#U~_=_XnWx>Knvj>6jNT45px`+@!+kEVeh z=E18l-azs-4awI$?Ira7Zjx3;Jr}Disnm(+2`R;yQbGvN*0!xp`Q4Yzz#q{QM-NwB z|68>DmheCQFB>QbAbMpwzx(qyJAlZ7dp*uDt$ zDAFn#olmrC?yAFduEsZ!zSN<*4ad__I8)bpHdLj_=Vle#;YdG5Lsgk|)m)}_dX;1^ z&CJ16c7h;~A1-rV%uVSUZ!4#}J=w>} zTUEGz?AD2oU8qmt`!#mAZhCb?K>z^+5I_I{1Q0*~0R#|00D)Z Date: Tue, 18 Apr 2023 10:24:44 -0500 Subject: [PATCH 4/6] Update .gitignore Ignore meshtastic.sqlite --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d46ffab..5939b04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .pyenv -config.yaml \ No newline at end of file +config.yaml +meshtastic.sqlite From 1f28aaa40129b6bb2c73f970dc9c2a17e884852d Mon Sep 17 00:00:00 2001 From: Jeremiah K Date: Tue, 18 Apr 2023 11:25:42 -0500 Subject: [PATCH 5/6] Removed DMs for now. Fixed longnames --- main.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 63cdfdf..0f74608 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import asyncio import time import logging import re +import sqlite3 import yaml import meshtastic.tcp_interface import meshtastic.serial_interface @@ -20,6 +21,48 @@ with open("config.yaml", "r") as f: logger.setLevel(getattr(logging, relay_config["logging"]["level"].upper())) +# Initialize SQLite database +def initialize_database(): + conn = sqlite3.connect("meshtastic.sqlite") + cursor = conn.cursor() + cursor.execute( + "CREATE TABLE IF NOT EXISTS longnames (meshtastic_id TEXT PRIMARY KEY, longname TEXT)" + ) + conn.commit() + conn.close() + +# Get the longname for a given Meshtastic ID +def get_longname(meshtastic_id): + conn = sqlite3.connect("meshtastic.sqlite") + cursor = conn.cursor() + cursor.execute( + "SELECT longname FROM longnames WHERE meshtastic_id=?", (meshtastic_id,) + ) + result = cursor.fetchone() + conn.close() + return result[0] if result else None + +# Save the longname for a given Meshtastic ID +def save_longname(meshtastic_id, longname): + conn = sqlite3.connect("meshtastic.sqlite") + cursor = conn.cursor() + cursor.execute( + "INSERT OR REPLACE INTO longnames (meshtastic_id, longname) VALUES (?, ?)", + (meshtastic_id, longname), + ) + conn.commit() + conn.close() + +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) + + # Initialize Meshtastic interface connection_type = relay_config["meshtastic"]["connection_type"] if connection_type == "serial": @@ -66,12 +109,14 @@ def on_meshtastic_message(packet, loop=None): logger.info(f"Processing inbound radio message from {sender}") - formatted_message = f"{sender}: {text}" + longname = get_longname(sender) or sender + formatted_message = f"{longname}: {text}" asyncio.run_coroutine_threadsafe( matrix_relay(matrix_client, formatted_message), loop=loop, ) + # Callback for new messages in Matrix room async def on_room_message(room: MatrixRoom, event: RoomMessageText) -> None: if room.room_id == matrix_room_id and event.sender != bot_user_id: @@ -84,10 +129,10 @@ async def on_room_message(room: MatrixRoom, event: RoomMessageText) -> None: logger.info(f"Processing matrix message from {event.sender}: {text}") display_name_response = await matrix_client.get_displayname(event.sender) - display_name = display_name_response.displayname or event.sender + display_name = (display_name_response.displayname or event.sender)[:8] text = f"{display_name}: {text}" - text = text[0:80] + text = text[0:218] # 218 = 228 (max message length) - 8 (max display name length) - 1 (colon + space) if relay_config["meshtastic"]["broadcast_enabled"]: logger.info(f"Sending radio message from {display_name} to radio broadcast") @@ -99,9 +144,13 @@ async def on_room_message(room: MatrixRoom, event: RoomMessageText) -> None: + async def main(): global matrix_client + # Initialize the SQLite database + initialize_database() + config = AsyncClientConfig(encryption_enabled=False) matrix_client = AsyncClient(matrix_homeserver, bot_user_id, config=config) matrix_client.access_token = matrix_access_token @@ -117,6 +166,11 @@ async def main(): matrix_client.add_event_callback(on_room_message, RoomMessageText) # Start the Matrix client - await matrix_client.sync_forever(timeout=30000) + while True: + # Update longnames + update_longnames() + + await matrix_client.sync_forever(timeout=30000) + await asyncio.sleep(60) # Update longnames every 60 seconds asyncio.run(main()) From f132cf94ff2c8c9bf66fb1579da0ca616f341e39 Mon Sep 17 00:00:00 2001 From: Jeremiah K Date: Tue, 18 Apr 2023 11:45:19 -0500 Subject: [PATCH 6/6] Revert "Merge branch 'channel-logic-fix'" This reverts commit 44a0de44c3f919933efdd14a2fbd4cdee4138f60, reversing changes made to ea451b0eecda36b672108d8582a01ddecb5506f2. --- main.py | 147 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 29 deletions(-) diff --git a/main.py b/main.py index 63cdfdf..49b1b9b 100644 --- a/main.py +++ b/main.py @@ -1,35 +1,51 @@ import asyncio -import time +import threading +import json +import sqlite3 import logging -import re import yaml +import re import meshtastic.tcp_interface import meshtastic.serial_interface -from nio import AsyncClient, AsyncClientConfig, MatrixRoom, RoomMessageText +from nio import AsyncClient, AsyncClientConfig, MatrixRoom, RoomMessageText, RoomMessage from pubsub import pub +from meshtastic import mesh_pb2 from yaml.loader import SafeLoader -bot_start_time = int(time.time() * 1000) - logging.basicConfig() logger = logging.getLogger(name="meshtastic.matrix.relay") -# Load configuration +# Collect configuration +relay_config = None with open("config.yaml", "r") as f: relay_config = yaml.load(f, Loader=SafeLoader) -logger.setLevel(getattr(logging, relay_config["logging"]["level"].upper())) +if relay_config["logging"]["level"] == "debug": + logger.setLevel(logging.DEBUG) +elif relay_config["logging"]["level"] == "info": + logger.setLevel(logging.INFO) +elif relay_config["logging"]["level"] == "warn": + logger.setLevel(logging.WARN) +elif relay_config["logging"]["level"] == "error": + logger.setLevel(logging.ERROR) -# Initialize Meshtastic interface + +# Connect to the Meshtastic device +logger.info(f"Starting Meshtastic <==> Matrix Relay...") + +# Add a new configuration option to select between serial and network connections 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) + logger.info(f"Connected to radio using serial port {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) + logger.info(f"Connected to radio at {target_host}.") matrix_client = None @@ -39,7 +55,43 @@ matrix_access_token = relay_config["matrix"]["access_token"] bot_user_id = relay_config["matrix"]["bot_user_id"] matrix_room_id = relay_config["matrix"]["room_id"] -# Send message to the Matrix room +# SQLite configuration +db_file = "meshtastic.sqlite" +db = sqlite3.connect(db_file) + +# Initialize the database +db.execute("CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, longname TEXT)") +db.execute( + "CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, sender_id TEXT, text TEXT, timestamp INTEGER)" +) +db.commit() + + +# Function to insert or update a node +def upsert_node(id, longname): + db.execute( + "INSERT OR REPLACE INTO nodes (id, longname) VALUES (?, ?)", (id, longname) + ) + db.commit() + + +# Function to insert a message +def insert_message(sender_id, text, timestamp): + db.execute( + "INSERT INTO messages (sender_id, text, timestamp) VALUES (?, ?, ?)", + (sender_id, text, timestamp), + ) + db.commit() + + +# Function to get the node's longname +def get_node_longname(sender): + cursor = db.cursor() + cursor.execute("SELECT longname FROM nodes WHERE id = ?", (sender,)) + row = cursor.fetchone() + return row[0] if row else sender + + async def matrix_relay(matrix_client, message): try: await asyncio.wait_for( @@ -53,50 +105,86 @@ async def matrix_relay(matrix_client, message): logger.info(f"Sent inbound radio message to matrix room: {matrix_room_id}") except asyncio.TimeoutError: - logger.error(f"Timed out while waiting for Matrix response") + logger.error( + f"Timed out while waiting for Matrix response - room {matrix_room_id}: {e}" + ) except Exception as e: - logger.error(f"Error sending radio message to matrix room {matrix_room_id}: {e}") + logger.error( + f"Error sending radio message to matrix room {matrix_room_id}: {e}" + ) + # Callback for new messages from Meshtastic def on_meshtastic_message(packet, loop=None): + global matrix_client sender = packet["fromId"] if "text" in packet["decoded"] and packet["decoded"]["text"]: text = packet["decoded"]["text"] + # timestamp = packet["received"] - logger.info(f"Processing inbound radio message from {sender}") + if logger.level == logging.DEBUG: + logger.debug(f"Processing radio message from {sender}: {text}") + elif logger.level == logging.INFO: + logger.info(f"Processing inbound radio message from {sender}") formatted_message = f"{sender}: {text}" + # create an event loop asyncio.run_coroutine_threadsafe( matrix_relay(matrix_client, formatted_message), loop=loop, ) + # insert_message(sender, text, timestamp) + + # Callback for new messages in Matrix room async def on_room_message(room: MatrixRoom, event: RoomMessageText) -> None: - if room.room_id == matrix_room_id and event.sender != bot_user_id: - message_timestamp = event.server_timestamp + logger.info( + f"Detected inbound matrix message from {event.sender} in room {room.room_id}" + ) - # Only process messages with a timestamp greater than the bot's start time - if message_timestamp > bot_start_time: + if room.room_id == matrix_room_id and event.sender != bot_user_id: + target_node = None + + if event.formatted_body: + text = event.formatted_body.strip() + else: text = event.body.strip() - logger.info(f"Processing matrix message from {event.sender}: {text}") + logger.debug(f"Processing matrix message from {event.sender}: {text}") - display_name_response = await matrix_client.get_displayname(event.sender) - display_name = display_name_response.displayname or event.sender + # Opportunistically detect node in message text !124abcd: + match = re.search(r"(![\da-z]+):", text) + if match: + target_node = match.group(1) - text = f"{display_name}: {text}" - text = text[0:80] - - if relay_config["meshtastic"]["broadcast_enabled"]: - logger.info(f"Sending radio message from {display_name} to radio broadcast") - meshtastic_interface.sendText( - text=text, channelIndex=relay_config["meshtastic"]["channel"] - ) - else: - logger.debug(f"Broadcast not supported: Message from {display_name} dropped.") + text = re.sub(r".*?", "", text) + text = f"{event.source['sender']}: {text}" + text = text[0:80] + if target_node: + logger.debug( + f"Sending radio message from {event.sender} to {target_node} ..." + ) + meshtastic_interface.sendText( + text=text, + channelIndex=relay_config["meshtastic"]["channel"], + destinationId=target_node, + ) + logger.info(f"Sent radio message from {event.sender} to {target_node}") + elif relay_config["meshtastic"]["broadcast_enabled"]: + logger.debug( + f"Sending radio message from {event.sender} to radio broadcast ..." + ) + meshtastic_interface.sendText( + text=text, channelIndex=relay_config["meshtastic"]["channel"] + ) + logger.info(f"Sent radio message from {event.sender} to radio broadcast") + elif not relay_config["meshtastic"]["broadcast_enabled"]: + logger.debug( + f"Broadcast not supported: Message from {event.sender} dropped." + ) async def main(): @@ -119,4 +207,5 @@ async def main(): # Start the Matrix client await matrix_client.sync_forever(timeout=30000) + asyncio.run(main())