Files
meshcore-bot/modules/core.py
agessaman 04279c7322 Fix remaining errors and clean up print statements
- Remove problematic debug and log command attempts that cause 'Unknown command' errors
- Replace remaining print statement in meshcore_protocol.py with proper logging
- Add explanatory comment about debug mode availability
- This eliminates the ERROR messages seen in the logs during startup
2025-09-06 11:31:02 -07:00

422 lines
16 KiB
Python

#!/usr/bin/env python3
"""
Core MeshCore Bot functionality
Contains the main bot class and message processing logic
"""
import asyncio
import configparser
import logging
import colorlog
import time
import threading
import schedule
from pathlib import Path
from typing import Optional, List, Dict, Any
from dataclasses import dataclass
# Import the official meshcore package
import meshcore
from meshcore import EventType
# Import command functions from meshcore-cli
from meshcore_cli.meshcore_cli import send_cmd, send_chan_msg
# Import our modules
from .rate_limiter import RateLimiter, BotTxRateLimiter
from .message_handler import MessageHandler
from .command_manager import CommandManager
from .channel_manager import ChannelManager
from .scheduler import MessageScheduler
from .repeater_manager import RepeaterManager
class MeshCoreBot:
"""MeshCore Bot using official meshcore package"""
def __init__(self, config_file: str = "config.ini"):
self.config_file = config_file
self.config = configparser.ConfigParser()
self.load_config()
# Setup logging
self.setup_logging()
# Connection
self.meshcore = None
self.connected = False
# Initialize database manager first (needed by plugins)
db_path = self.config.get('Bot', 'db_path', fallback='meshcore_bot.db')
self.logger.info(f"Initializing database manager with database: {db_path}")
try:
from .db_manager import DBManager
self.db_manager = DBManager(self, db_path)
self.logger.info("Database manager initialized successfully")
except Exception as e:
self.logger.error(f"Failed to initialize database manager: {e}")
raise
# Initialize modules
self.rate_limiter = RateLimiter(
self.config.getint('Bot', 'rate_limit_seconds', fallback=10)
)
self.bot_tx_rate_limiter = BotTxRateLimiter(
self.config.getfloat('Bot', 'bot_tx_rate_limit_seconds', fallback=1.0)
)
self.message_handler = MessageHandler(self)
self.command_manager = CommandManager(self)
self.channel_manager = ChannelManager(self)
self.scheduler = MessageScheduler(self)
# Initialize repeater manager
self.logger.info("Initializing repeater manager")
try:
self.repeater_manager = RepeaterManager(self)
self.logger.info("Repeater manager initialized successfully")
except Exception as e:
self.logger.error(f"Failed to initialize repeater manager: {e}")
raise
# Initialize solar conditions configuration
from .solar_conditions import set_config
set_config(self.config)
# Advert tracking
self.last_advert_time = None
self.logger.info(f"MeshCore Bot initialized: {self.config.get('Bot', 'bot_name')}")
def load_config(self):
"""Load configuration from file"""
if not Path(self.config_file).exists():
self.create_default_config()
self.config.read(self.config_file)
def create_default_config(self):
"""Create default configuration file"""
default_config = """[Connection]
connection_type = ble
ble_device_name = MeshCore
serial_port = /dev/ttyUSB0
timeout = 30
[Bot]
bot_name = MeshCoreBot
node_id =
enabled = true
passive_mode = false
rate_limit_seconds = 10
# Send startup announcement when bot finishes initializing
# Send startup advert when bot finishes initializing
# Options: false (disabled), zero-hop, flood
startup_advert = false
# Path to repeater contacts database
repeater_db_path = repeater_contacts.db
[Keywords]
test = "Test message received! Hops: {hops}, Path: {path}, From: {sender}"
ping = "Pong! Response time: {timestamp}"
help = "Available commands: test, ping, help"
[Custom_Syntax]
# Custom syntax patterns for special message formats
# Format: pattern = "response_format"
# Available fields: {sender}, {phrase}, {connection_info}, {snr}, {timestamp}, {path}
t_phrase = "ack {sender} {phrase} | {connection_info}"
@_phrase = "ack {sender} {phrase} | {connection_info}"
[Channels]
monitor_channels = Testing
respond_to_dms = true
channel_public_key = 1321f3257ae4f7125204096e15b34c99
[Banned_Users]
banned_users =
[Scheduled_Messages]
0800 = Testing:Good morning! Testing channel is active.
1200 = Testing:Midday test - channel is working properly.
1800 = Testing:Evening test - channel status: Good
[Logging]
log_level = INFO
log_file = meshcore_bot.log
colored_output = true
[External_Data]
weather_api_key =
weather_update_interval = 3600
tide_api_key =
tide_update_interval = 1800
"""
with open(self.config_file, 'w') as f:
f.write(default_config)
# Note: Using print here since logger may not be initialized yet
print(f"Created default config file: {self.config_file}")
def setup_logging(self):
"""Setup logging configuration"""
log_level = getattr(logging, self.config.get('Logging', 'log_level', fallback='INFO'))
# Create formatter
if self.config.getboolean('Logging', 'colored_output', fallback=True):
formatter = colorlog.ColoredFormatter(
'%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
}
)
else:
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Setup logger
self.logger = logging.getLogger('MeshCoreBot')
self.logger.setLevel(log_level)
# Clear any existing handlers to prevent duplicates
self.logger.handlers.clear()
# Console handler
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
self.logger.addHandler(console_handler)
# File handler
log_file = self.config.get('Logging', 'log_file', fallback='meshcore_bot.log')
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
# Prevent propagation to root logger to avoid duplicate output
self.logger.propagate = False
# Configure meshcore library logging (separate from bot logging)
meshcore_log_level = getattr(logging, self.config.get('Logging', 'meshcore_log_level', fallback='INFO'))
# Configure all possible meshcore-related loggers
meshcore_loggers = [
'meshcore',
'meshcore_cli',
'meshcore.meshcore',
'meshcore_cli.meshcore_cli',
'meshcore_cli.commands',
'meshcore_cli.connection'
]
for logger_name in meshcore_loggers:
logger = logging.getLogger(logger_name)
logger.setLevel(meshcore_log_level)
# Remove any existing handlers to prevent duplicate output
logger.handlers.clear()
# Add our formatter
if not logger.handlers:
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
# Configure root logger to prevent other libraries from using DEBUG
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# Log the configuration for debugging
self.logger.info(f"Logging configured - Bot: {logging.getLevelName(log_level)}, MeshCore: {logging.getLevelName(meshcore_log_level)}")
async def connect(self) -> bool:
"""Connect to MeshCore node using official package"""
try:
self.logger.info("Connecting to MeshCore node...")
# Get connection type from config
connection_type = self.config.get('Connection', 'connection_type', fallback='ble').lower()
self.logger.info(f"Using connection type: {connection_type}")
if connection_type == 'serial':
# Create serial connection
serial_port = self.config.get('Connection', 'serial_port', fallback='/dev/ttyUSB0')
self.logger.info(f"Connecting via serial port: {serial_port}")
self.meshcore = await meshcore.MeshCore.create_serial(serial_port, debug=False)
else:
# Create BLE connection (default)
ble_device_name = self.config.get('Connection', 'ble_device_name', fallback=None)
self.logger.info(f"Connecting via BLE" + (f" to device: {ble_device_name}" if ble_device_name else ""))
self.meshcore = await meshcore.MeshCore.create_ble(device_name=ble_device_name, debug=False)
if self.meshcore.is_connected:
self.connected = True
self.logger.info(f"Connected to: {self.meshcore.self_info}")
# Wait for contacts to load
await self.wait_for_contacts()
# Fetch channels
await self.channel_manager.fetch_channels()
# Setup message event handlers
await self.setup_message_handlers()
return True
else:
self.logger.error("Failed to connect to MeshCore node")
return False
except Exception as e:
self.logger.error(f"Connection failed: {e}")
return False
async def wait_for_contacts(self):
"""Wait for contacts to be loaded"""
self.logger.info("Waiting for contacts to load...")
# Try to manually load contacts first
try:
from meshcore_cli.meshcore_cli import next_cmd
self.logger.info("Manually requesting contacts from device...")
result = await next_cmd(self.meshcore, ["contacts"])
self.logger.info(f"Contacts command result: {len(result) if result else 0} contacts")
except Exception as e:
self.logger.warning(f"Error manually loading contacts: {e}")
# Check if contacts are loaded (even if empty list)
if hasattr(self.meshcore, 'contacts'):
self.logger.info(f"Contacts loaded: {len(self.meshcore.contacts)} contacts")
return
# Wait up to 30 seconds for contacts to load
max_wait = 30
wait_time = 0
while wait_time < max_wait:
if hasattr(self.meshcore, 'contacts'):
self.logger.info(f"Contacts loaded: {len(self.meshcore.contacts)} contacts")
return
await asyncio.sleep(5)
wait_time += 5
self.logger.info(f"Still waiting for contacts... ({wait_time}s)")
self.logger.warning(f"Contacts not loaded after {max_wait} seconds, proceeding anyway")
async def setup_message_handlers(self):
"""Setup event handlers for messages"""
# Handle contact messages (DMs)
async def on_contact_message(event, metadata=None):
await self.message_handler.handle_contact_message(event, metadata)
# Handle channel messages
async def on_channel_message(event, metadata=None):
await self.message_handler.handle_channel_message(event, metadata)
# Handle RF log data for SNR information
async def on_rf_data(event, metadata=None):
await self.message_handler.handle_rf_log_data(event, metadata)
# Handle raw data events (full packet data)
async def on_raw_data(event, metadata=None):
await self.message_handler.handle_raw_data(event, metadata)
# Handle new contact events
async def on_new_contact(event, metadata=None):
await self.message_handler.handle_new_contact(event, metadata)
# Subscribe to events
self.meshcore.subscribe(EventType.CONTACT_MSG_RECV, on_contact_message)
self.meshcore.subscribe(EventType.CHANNEL_MSG_RECV, on_channel_message)
self.meshcore.subscribe(EventType.RX_LOG_DATA, on_rf_data)
# Subscribe to RAW_DATA events for full packet data
self.meshcore.subscribe(EventType.RAW_DATA, on_raw_data)
# Subscribe to NEW_CONTACT events for automatic contact management
self.meshcore.subscribe(EventType.NEW_CONTACT, on_new_contact)
# Note: Debug mode commands are not available in current meshcore-cli version
# The meshcore library handles debug output automatically when needed
# Start auto message fetching
await self.meshcore.start_auto_message_fetching()
self.logger.info("Message handlers setup complete")
async def start(self):
"""Start the bot"""
self.logger.info("Starting MeshCore Bot...")
# Connect to MeshCore node
if not await self.connect():
self.logger.error("Failed to connect to MeshCore node")
return
# Setup scheduled messages
self.scheduler.setup_scheduled_messages()
# Start scheduler thread
self.scheduler.start()
# Send startup advert if enabled
await self.send_startup_advert()
# Keep running
self.logger.info("Bot is running. Press Ctrl+C to stop.")
try:
while self.connected:
await asyncio.sleep(1)
except KeyboardInterrupt:
self.logger.info("Received interrupt signal")
finally:
await self.stop()
async def stop(self):
"""Stop the bot"""
self.logger.info("Stopping MeshCore Bot...")
self.connected = False
if self.meshcore:
await self.meshcore.disconnect()
self.logger.info("Bot stopped")
async def send_startup_advert(self):
"""Send a startup advert if enabled in config"""
try:
# Check if startup advert is enabled
startup_advert = self.config.get('Bot', 'startup_advert', fallback='false').lower()
if startup_advert == 'false':
self.logger.debug("Startup advert disabled")
return
self.logger.info(f"Sending startup advert: {startup_advert}")
# Add a small delay to ensure connection is fully established
await asyncio.sleep(2)
# Send the appropriate type of advert using meshcore.commands
if startup_advert == 'zero-hop':
self.logger.debug("Sending zero-hop advert")
await self.meshcore.commands.send_advert(flood=False)
elif startup_advert == 'flood':
self.logger.debug("Sending flood advert")
await self.meshcore.commands.send_advert(flood=True)
else:
self.logger.warning(f"Unknown startup_advert option: {startup_advert}")
return
# Update last advert time
import time
self.last_advert_time = time.time()
self.logger.info(f"Startup {startup_advert} advert sent successfully")
except Exception as e:
self.logger.error(f"Error sending startup advert: {e}")
import traceback
self.logger.error(traceback.format_exc())