mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-05-14 19:35:18 +00:00
feat: Enhance web viewer functionality and configuration options
- Updated `config.ini.example` to include a new option for additional hashtag channels to decode in the packet stream. - Modified `BotDataViewer` to retrieve and display additional decode-only channels from the configuration. - Improved packet handling in `message_handler.py` to capture full packet data for web viewer integration. - Enhanced the web viewer's JavaScript to support detailed packet analysis and display, including color-coded hex breakdowns and improved user interface elements. - Added new styles and scripts to the web viewer templates for better visual representation of packet data and improved user experience.
This commit is contained in:
@@ -332,3 +332,4 @@ This project is licensed under the MIT License.
|
||||
- [MeshCore Project](https://github.com/meshcore-dev/MeshCore) for the mesh networking protocol
|
||||
- Some commands adapted from MeshingAround bot by K7MHI Kelly Keeton 2024
|
||||
- Packet capture service based on [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture) by agessaman
|
||||
- [meshcore-decoder](https://github.com/michaelhart/meshcore-decoder) by Michael Hart for client-side packet decoding and decryption in the web viewer
|
||||
|
||||
+14
-1
@@ -262,7 +262,7 @@ ping = "Pong!"
|
||||
pong = "Ping!"
|
||||
help = "Bot Help: test (or t), ping, help, hello, cmd, advert, @string, wx, aqi, sun, moon, solar, hfcond, satpass, prefix, path, sports, dice, roll, stats | More: 'help <command>'"
|
||||
|
||||
Override 'cmd' command output
|
||||
# Override 'cmd' command output
|
||||
# cmd = "Available commands: test (or t), ping, help, hello, cmd, advert, @string, wx, aqi, sun, moon, solar, hfcond, satpass, prefix, path, sports, dice, roll, stats"
|
||||
|
||||
[Channels]
|
||||
@@ -806,6 +806,19 @@ auto_start = false
|
||||
# Default: bot_data.db
|
||||
db_path = bot_data.db
|
||||
|
||||
# Additional hashtag channels to decode in the packet stream
|
||||
# The web viewer can decrypt GroupText messages from hashtag channels
|
||||
# without adding them to the radio. Enter channel names (with or without #)
|
||||
# as a comma-separated list. The Public channel is always included.
|
||||
#
|
||||
# Example: #CQ,#ARES,#EmComm
|
||||
# This will allow decryption of messages on #CQ, #ARES, and #EmComm channels
|
||||
# in addition to any channels configured on your radio.
|
||||
#
|
||||
# Note: Only hashtag channels work here - custom channels with private keys
|
||||
# must be added to the radio itself.
|
||||
decode_hashtag_channels =
|
||||
|
||||
####################################################################################################
|
||||
# #
|
||||
# Service Plugins Config #
|
||||
|
||||
@@ -668,6 +668,10 @@ class MessageHandler:
|
||||
if (hasattr(self.bot, 'web_viewer_integration') and
|
||||
self.bot.web_viewer_integration and
|
||||
self.bot.web_viewer_integration.bot_integration):
|
||||
# Use extracted_payload which is the full MeshCore packet
|
||||
# (header + path_len + path + payload, without RF wrapper)
|
||||
decoded_packet['raw_packet_hex'] = extracted_payload if extracted_payload else raw_hex
|
||||
decoded_packet['packet_hash'] = packet_hash
|
||||
self.bot.web_viewer_integration.bot_integration.capture_full_packet_data(decoded_packet)
|
||||
|
||||
# Process ADVERT packets for contact tracking (regardless of path length)
|
||||
|
||||
@@ -37,7 +37,12 @@ class BotDataViewer:
|
||||
# This is the directory containing the modules folder
|
||||
self.bot_root = Path(os.path.join(os.path.dirname(__file__), '..', '..')).resolve()
|
||||
|
||||
self.app = Flask(__name__, template_folder=os.path.join(os.path.dirname(__file__), 'templates'))
|
||||
self.app = Flask(
|
||||
__name__,
|
||||
template_folder=os.path.join(os.path.dirname(__file__), 'templates'),
|
||||
static_folder=os.path.join(os.path.dirname(__file__), 'static'),
|
||||
static_url_path='/static'
|
||||
)
|
||||
self.app.config['SECRET_KEY'] = 'meshcore_bot_viewer_secret'
|
||||
|
||||
# Flask-SocketIO configuration following 5.x best practices
|
||||
@@ -2933,33 +2938,57 @@ class BotDataViewer:
|
||||
return []
|
||||
|
||||
def _get_channels(self):
|
||||
"""Get all configured channels from database"""
|
||||
"""Get all configured channels from database plus additional decode-only channels"""
|
||||
import sqlite3
|
||||
conn = None
|
||||
try:
|
||||
conn = self._get_db_connection()
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
cursor.execute('''
|
||||
SELECT channel_idx, channel_name, channel_type, channel_key_hex, last_updated
|
||||
FROM channels
|
||||
ORDER BY channel_idx
|
||||
''')
|
||||
|
||||
|
||||
rows = cursor.fetchall()
|
||||
channels = []
|
||||
existing_names = set()
|
||||
|
||||
for row in rows:
|
||||
name = row['channel_name']
|
||||
channels.append({
|
||||
'channel_idx': row['channel_idx'],
|
||||
'index': row['channel_idx'], # Alias for compatibility
|
||||
'name': row['channel_name'],
|
||||
'channel_name': row['channel_name'], # Alias for compatibility
|
||||
'name': name,
|
||||
'channel_name': name, # Alias for compatibility
|
||||
'type': row['channel_type'] or 'hashtag',
|
||||
'key_hex': row['channel_key_hex'],
|
||||
'last_updated': row['last_updated']
|
||||
})
|
||||
|
||||
# Track names for deduplication (normalize to lowercase with #)
|
||||
normalized = name.lower() if name.startswith('#') else f'#{name.lower()}'
|
||||
existing_names.add(normalized)
|
||||
|
||||
# Add additional decode-only hashtag channels from config
|
||||
additional_channels = self._get_additional_decode_channels()
|
||||
for channel_name in additional_channels:
|
||||
# Normalize name
|
||||
normalized = channel_name.lower() if channel_name.startswith('#') else f'#{channel_name.lower()}'
|
||||
if normalized not in existing_names:
|
||||
channels.append({
|
||||
'channel_idx': None, # Not a real radio channel
|
||||
'index': None,
|
||||
'name': normalized,
|
||||
'channel_name': normalized,
|
||||
'type': 'hashtag',
|
||||
'key_hex': None, # Key will be derived client-side
|
||||
'last_updated': None,
|
||||
'decode_only': True # Flag to indicate this is decode-only
|
||||
})
|
||||
existing_names.add(normalized)
|
||||
|
||||
return channels
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting channels: {e}")
|
||||
@@ -2968,6 +2997,40 @@ class BotDataViewer:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
def _get_additional_decode_channels(self):
|
||||
"""Get additional hashtag channels to decode from config"""
|
||||
channels = set() # Use set for automatic deduplication
|
||||
|
||||
try:
|
||||
# 1. Get channels from decode_hashtag_channels in [Web_Viewer]
|
||||
if self.config and self.config.has_option('Web_Viewer', 'decode_hashtag_channels'):
|
||||
channels_str = self.config.get('Web_Viewer', 'decode_hashtag_channels', fallback='')
|
||||
if channels_str:
|
||||
for c in channels_str.split(','):
|
||||
c = c.strip().lower()
|
||||
if c:
|
||||
# Remove # prefix if present for normalization
|
||||
if c.startswith('#'):
|
||||
c = c[1:]
|
||||
channels.add(c)
|
||||
|
||||
# 2. Import channels from [Channels_List] section
|
||||
if self.config and self.config.has_section('Channels_List'):
|
||||
for key in self.config.options('Channels_List'):
|
||||
# Handle categorized channels like "sports.sounders" -> "sounders"
|
||||
if '.' in key:
|
||||
channel_name = key.split('.')[-1] # Get part after last dot
|
||||
else:
|
||||
channel_name = key
|
||||
|
||||
channel_name = channel_name.strip().lower()
|
||||
if channel_name:
|
||||
channels.add(channel_name)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reading decode channels config: {e}")
|
||||
|
||||
return list(channels)
|
||||
|
||||
def _get_channel_number(self, channel_name):
|
||||
"""Get channel number from channel name"""
|
||||
# This would use channel_manager
|
||||
|
||||
@@ -84,6 +84,7 @@ class BotIntegration:
|
||||
import sqlite3
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# Ensure packet_data is a dict (might be passed as dict already)
|
||||
if not isinstance(packet_data, dict):
|
||||
@@ -100,6 +101,10 @@ class BotIntegration:
|
||||
# If no path_len either, default to 0 hops
|
||||
packet_data['hops'] = 0
|
||||
|
||||
# Add datetime for frontend display
|
||||
if 'datetime' not in packet_data:
|
||||
packet_data['datetime'] = datetime.now().isoformat()
|
||||
|
||||
# Convert non-serializable objects to strings
|
||||
serializable_data = self._make_json_serializable(packet_data)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -444,7 +444,10 @@
|
||||
|
||||
<!-- Chart.js for graphs -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
|
||||
|
||||
<!-- SHA-256 for channel key derivation (works in non-HTTPS contexts) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-sha256/0.11.0/sha256.min.js"></script>
|
||||
|
||||
<!-- Base JavaScript -->
|
||||
<script>
|
||||
// Global connection management
|
||||
|
||||
@@ -164,7 +164,8 @@ class RadioManager {
|
||||
try {
|
||||
const response = await fetch('/api/channels');
|
||||
const data = await response.json();
|
||||
this.channels = data.channels || [];
|
||||
// Filter out decode-only channels (they're for packet decoding, not radio management)
|
||||
this.channels = (data.channels || []).filter(c => !c.decode_only);
|
||||
this.renderChannels();
|
||||
this.updateChannelIndexDisplay();
|
||||
} catch (error) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user