diff --git a/.gitignore b/.gitignore index 78ee506..e4d6245 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ __pycache__/ bulletins.db venv/ - +.venv diff --git a/README.md b/README.md index 9577e57..1697064 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,41 @@ If you're a Docker user, TC²-BBS Meshtastic is available on Docker Hub! Run the server with: ```sh -python server.py +python server.py ``` Be sure you've followed the Python virtual environment steps above and activated it before running. +## Command line arguments +``` +$ python server.py --help + +████████╗ ██████╗██████╗ ██████╗ ██████╗ ███████╗ +╚══██╔══╝██╔════╝╚════██╗ ██╔══██╗██╔══██╗██╔════╝ + ██║ ██║ █████╔╝█████╗██████╔╝██████╔╝███████╗ + ██║ ██║ ██╔═══╝ ╚════╝██╔══██╗██╔══██╗╚════██║ + ██║ ╚██████╗███████╗ ██████╔╝██████╔╝███████║ + ╚═╝ ╚═════╝╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ +Meshtastic Version + +usage: server.py [-h] [--config CONFIG] [--interface-type {serial,tcp}] [--port PORT] [--host HOST] [--mqtt-topic MQTT_TOPIC] + +Meshtastic BBS system + +options: + -h, --help show this help message and exit + --config CONFIG, -c CONFIG + System configuration file + --interface-type {serial,tcp}, -i {serial,tcp} + Node interface type + --port PORT, -p PORT Serial port + --host HOST TCP host address + --mqtt-topic MQTT_TOPIC, -t MQTT_TOPIC + MQTT topic to subscribe +``` + + + ## Automatically run at boot If you would like to have the script automatically run at boot, follow the steps below: diff --git a/command_handlers.py b/command_handlers.py index 9a96241..1863966 100644 --- a/command_handlers.py +++ b/command_handlers.py @@ -15,8 +15,6 @@ from utils import ( update_user_state ) -config, interface_type, hostname, port, bbs_nodes = initialize_config() - def get_node_name(node_id, interface): node_info = interface.nodes.get(node_id) diff --git a/config_init.py b/config_init.py index bf3ddd7..17820b7 100644 --- a/config_init.py +++ b/config_init.py @@ -1,12 +1,106 @@ import configparser import time +from typing import Any +import meshtastic.stream_interface import meshtastic.serial_interface import meshtastic.tcp_interface import serial.tools.list_ports +import argparse -def initialize_config(): + +def init_cli_parser() -> argparse.Namespace: + """Function build the CLI parser and parses the arguments. + + Returns: + argparse.ArgumentParser: Argparse namespace with processed CLI args + """ + parser = argparse.ArgumentParser(description="Meshtastic BBS system") + + parser.add_argument( + "--config", "-c", + action="store", + help="System configuration file", + default=None) + + parser.add_argument( + "--interface-type", "-i", + action="store", + choices=['serial', 'tcp'], + help="Node interface type", + default=None) + + parser.add_argument( + "--port", "-p", + action="store", + help="Serial port", + default=None) + + parser.add_argument( + "--host", + action="store", + help="TCP host address", + default=None) + + parser.add_argument( + "--mqtt-topic", '-t', + action="store", + help="MQTT topic to subscribe", + default='meshtastic.receive') + # + # Add extra arguments here + #... + + args = parser.parse_args() + + return args + + +def merge_config(system_config:dict[str, Any], args:argparse.Namespace) -> dict[str, Any]: + """Function merges configuration read from the config file and provided on the CLI. + + CLI arguments override values defined in the config file. + system_config argument is mutated by the function. + + Args: + system_config (dict[str, Any]): System config dict returned by initialize_config() + args (argparse.Namespace): argparse namespace with parsed CLI args + + Returns: + dict[str, Any]: system config dict with merged configurations + """ + + if args.interface_type is not None: + system_config['interface_type'] = args.interface_type + + if args.port is not None: + system_config['port'] = args.port + + if args.host is not None: + system_config['host'] = args.host + + return system_config + +def initialize_config(config_file:str = None) -> dict[str, Any]: + """Function reads and parses system configuration file + + Returns a dict with the following entries: + config - parsed config file + interface_type - type of the active interface + hostname - host name for TCP interface + port - serial port name for serial interface + bbs_nodes - list of peer nodes to sync with + + Args: + config_file (str, optional): Path to config file. Function reads from './config.ini' if this arg is set to None. Defaults to None. + + Returns: + dict: dict with system configuration, ad described above + """ config = configparser.ConfigParser() - config.read('config.ini') + + if config_file is None: + config_file = "config.ini" + config.read(config_file) interface_type = config['interface']['type'] hostname = config['interface'].get('hostname', None) @@ -16,14 +110,35 @@ def initialize_config(): if bbs_nodes == ['']: bbs_nodes = [] - return config, interface_type, hostname, port, bbs_nodes + return {'config':config, 'interface_type': interface_type, 'hostname': hostname, 'port': port, 'bbs_nodes': bbs_nodes, 'mqtt_topic': 'meshtastic.receive'} -def get_interface(interface_type, hostname=None, port=None): + +def get_interface(system_config:dict[str, Any]) -> meshtastic.stream_interface.StreamInterface: + """Function opens and returns an instance meshtastic interface of type specified by the configuration + + Function creates and returns an instance of a class inheriting from meshtastic.stream_interface.StreamInterface. + The type of the class depends on the type of the interface specified by the system configuration. + For 'serial' interfaces, function returns an instance of meshtastic.serial_interface.SerialInterface, + and for 'tcp' interface, an instance of meshtastic.tcp_interface.TCPInterface. + + Args: + system_config (dict[str, Any]): A dict with system configuration. See description of initialize_config() for details. + + Raises: + ValueError: Exception raised in the following cases: + - Type of interface not provided in the system config + - Multiple serial ports present in the system, and no port specified in the configuration + - Serial port interface requested, but no ports found in the system + - Hostname not provided for TCP interface + + Returns: + meshtastic.stream_interface.StreamInterface: An instance of StreamInterface + """ while True: try: - if interface_type == 'serial': - if port: - return meshtastic.serial_interface.SerialInterface(port) + if system_config['interface_type'] == 'serial': + if system_config['port']: + return meshtastic.serial_interface.SerialInterface(system_config['port']) else: ports = list(serial.tools.list_ports.comports()) if len(ports) == 1: @@ -33,10 +148,10 @@ def get_interface(interface_type, hostname=None, port=None): raise ValueError(f"Multiple serial ports detected: {port_list}. Specify one with the 'port' argument.") else: raise ValueError("No serial ports detected.") - elif interface_type == 'tcp': - if not hostname: + elif system_config['interface_type'] == 'tcp': + if not system_config['hostname']: raise ValueError("Hostname must be specified for TCP interface") - return meshtastic.tcp_interface.TCPInterface(hostname=hostname) + return meshtastic.tcp_interface.TCPInterface(hostname=system_config['hostname']) else: raise ValueError("Invalid interface type specified in config file") except PermissionError as e: diff --git a/server.py b/server.py index 36ba220..b590e63 100644 --- a/server.py +++ b/server.py @@ -14,7 +14,7 @@ other BBS servers listed in the config.ini file. import logging -from config_init import initialize_config, get_interface +from config_init import initialize_config, get_interface, init_cli_parser, merge_config from db_operations import initialize_database from message_processing import on_receive from pubsub import pub @@ -35,20 +35,32 @@ Meshtastic Version """ print(banner) + + def main(): display_banner() - config, interface_type, hostname, port, bbs_nodes = initialize_config() - interface = get_interface(interface_type, hostname, port) - interface.bbs_nodes = bbs_nodes + # config, interface_type, hostname, port, bbs_nodes = initialize_config() + args = init_cli_parser() + config_file = None + if args.config is not None: + config_file = args.config + system_config = initialize_config(config_file) + + merge_config(system_config, args) + + # print(f"{system_config=}") + + interface = get_interface(system_config) + interface.bbs_nodes = system_config['bbs_nodes'] - logging.info(f"TC²-BBS is running on {interface_type} interface...") + logging.info(f"TC²-BBS is running on {system_config['interface_type']} interface...") initialize_database() def receive_packet(packet): on_receive(packet, interface) - pub.subscribe(receive_packet, 'meshtastic.receive') + pub.subscribe(receive_packet, system_config['mqtt_topic']) try: while True: