mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
feat: Add TCP connection support for Meshtastic
Allow connecting to WiFi-enabled Meshtastic devices via TCP/IP in
addition to USB/Serial connections. This enables remote monitoring
of mesh nodes that have WiFi capability (T-Beam, Heltec WiFi LoRa, etc).
- Add connection_type parameter ('serial' or 'tcp') to /meshtastic/start
- Add hostname parameter for TCP connections
- Update UI with connection type dropdown and hostname input field
- Show connection type in status responses
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+49
-9
@@ -3,8 +3,9 @@
|
||||
Provides endpoints for connecting to Meshtastic devices, configuring
|
||||
channels with encryption keys, and streaming received messages.
|
||||
|
||||
Requires a physical Meshtastic device (Heltec, T-Beam, RAK, etc.)
|
||||
connected via USB/Serial.
|
||||
Supports multiple connection types:
|
||||
- USB/Serial: Physical device connected via USB
|
||||
- TCP: WiFi-enabled devices accessible via IP address
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -95,7 +96,7 @@ def get_status():
|
||||
Get Meshtastic connection status.
|
||||
|
||||
Returns:
|
||||
JSON with connection status, device info, and node information.
|
||||
JSON with connection status, device info, connection type, and node information.
|
||||
"""
|
||||
if not is_meshtastic_available():
|
||||
return jsonify({
|
||||
@@ -111,6 +112,7 @@ def get_status():
|
||||
'available': True,
|
||||
'running': False,
|
||||
'device': None,
|
||||
'connection_type': None,
|
||||
'node_info': None,
|
||||
})
|
||||
|
||||
@@ -120,6 +122,7 @@ def get_status():
|
||||
'available': True,
|
||||
'running': client.is_running,
|
||||
'device': client.device_path,
|
||||
'connection_type': client.connection_type,
|
||||
'error': client.error,
|
||||
'node_info': node_info.to_dict() if node_info else None,
|
||||
})
|
||||
@@ -131,13 +134,20 @@ def start_mesh():
|
||||
Start Meshtastic listener.
|
||||
|
||||
Connects to a Meshtastic device and begins receiving messages.
|
||||
The device must be connected via USB/Serial.
|
||||
Supports both USB/Serial and TCP connections.
|
||||
|
||||
JSON body (optional):
|
||||
{
|
||||
"device": "/dev/ttyUSB0" // Serial port path. Auto-discovers if not provided.
|
||||
"connection_type": "serial", // 'serial' (default) or 'tcp'
|
||||
"device": "/dev/ttyUSB0", // Serial port path. Auto-discovers if not provided.
|
||||
"hostname": "192.168.1.100" // IP address or hostname for TCP connections
|
||||
}
|
||||
|
||||
Examples:
|
||||
Serial (auto-discover): {}
|
||||
Serial (specific port): {"device": "/dev/ttyUSB0"}
|
||||
TCP: {"connection_type": "tcp", "hostname": "192.168.1.100"}
|
||||
|
||||
Returns:
|
||||
JSON with connection status.
|
||||
"""
|
||||
@@ -151,7 +161,8 @@ def start_mesh():
|
||||
if client and client.is_running:
|
||||
return jsonify({
|
||||
'status': 'already_running',
|
||||
'device': client.device_path
|
||||
'device': client.device_path,
|
||||
'connection_type': client.connection_type
|
||||
})
|
||||
|
||||
# Clear queue and history
|
||||
@@ -162,18 +173,46 @@ def start_mesh():
|
||||
break
|
||||
_recent_messages.clear()
|
||||
|
||||
# Get optional device path
|
||||
# Parse connection parameters
|
||||
data = request.get_json(silent=True) or {}
|
||||
connection_type = data.get('connection_type', 'serial').lower().strip()
|
||||
device = data.get('device')
|
||||
hostname = data.get('hostname')
|
||||
|
||||
# Validate device path if provided
|
||||
# Validate connection type
|
||||
if connection_type not in ('serial', 'tcp'):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f"Invalid connection_type: {connection_type}. Must be 'serial' or 'tcp'"
|
||||
}), 400
|
||||
|
||||
# Validate TCP parameters
|
||||
if connection_type == 'tcp':
|
||||
if not hostname:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'hostname is required for TCP connections'
|
||||
}), 400
|
||||
hostname = str(hostname).strip()
|
||||
if not hostname:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'hostname cannot be empty'
|
||||
}), 400
|
||||
|
||||
# Validate serial device path if provided
|
||||
if device:
|
||||
device = str(device).strip()
|
||||
if not device:
|
||||
device = None
|
||||
|
||||
# Start client
|
||||
success = start_meshtastic(device=device, callback=_message_callback)
|
||||
success = start_meshtastic(
|
||||
device=device,
|
||||
callback=_message_callback,
|
||||
connection_type=connection_type,
|
||||
hostname=hostname
|
||||
)
|
||||
|
||||
if success:
|
||||
client = get_meshtastic_client()
|
||||
@@ -181,6 +220,7 @@ def start_mesh():
|
||||
return jsonify({
|
||||
'status': 'started',
|
||||
'device': client.device_path if client else None,
|
||||
'connection_type': client.connection_type if client else None,
|
||||
'node_info': node_info.to_dict() if node_info else None,
|
||||
})
|
||||
else:
|
||||
|
||||
@@ -259,6 +259,27 @@
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.mesh-strip-input {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.mesh-strip-input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mesh-strip-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.mesh-strip-btn {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
|
||||
@@ -158,21 +158,64 @@ const Meshtastic = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle connection type change (serial vs TCP)
|
||||
*/
|
||||
function onConnectionTypeChange() {
|
||||
const connTypeSelect = document.getElementById('meshStripConnType');
|
||||
const deviceSelect = document.getElementById('meshStripDevice');
|
||||
const hostnameInput = document.getElementById('meshStripHostname');
|
||||
|
||||
if (!connTypeSelect) return;
|
||||
|
||||
const connType = connTypeSelect.value;
|
||||
|
||||
if (connType === 'tcp') {
|
||||
// Show hostname input, hide device select
|
||||
if (deviceSelect) deviceSelect.style.display = 'none';
|
||||
if (hostnameInput) hostnameInput.style.display = 'block';
|
||||
} else {
|
||||
// Show device select, hide hostname input
|
||||
if (deviceSelect) deviceSelect.style.display = 'block';
|
||||
if (hostnameInput) hostnameInput.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Meshtastic connection
|
||||
*/
|
||||
async function start() {
|
||||
// Try strip device select first, then sidebar
|
||||
const stripDeviceSelect = document.getElementById('meshStripDevice');
|
||||
const sidebarDeviceSelect = document.getElementById('meshDeviceSelect');
|
||||
let device = stripDeviceSelect?.value || sidebarDeviceSelect?.value || null;
|
||||
// Get connection type
|
||||
const connTypeSelect = document.getElementById('meshStripConnType');
|
||||
const connectionType = connTypeSelect?.value || 'serial';
|
||||
|
||||
// Check if auto-detect is selected but multiple ports exist
|
||||
if (!device && stripDeviceSelect && stripDeviceSelect.options.length > 2) {
|
||||
// Multiple ports available - prompt user to select one
|
||||
showStatusMessage('Multiple ports detected. Please select a specific device from the dropdown.', 'warning');
|
||||
updateStatusIndicator('disconnected', 'Select a device');
|
||||
return;
|
||||
// Get connection parameters based on type
|
||||
let device = null;
|
||||
let hostname = null;
|
||||
|
||||
if (connectionType === 'tcp') {
|
||||
// TCP connection - get hostname
|
||||
const hostnameInput = document.getElementById('meshStripHostname');
|
||||
hostname = hostnameInput?.value?.trim() || null;
|
||||
|
||||
if (!hostname) {
|
||||
showStatusMessage('Please enter a hostname or IP address for TCP connection', 'error');
|
||||
updateStatusIndicator('disconnected', 'Enter hostname');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Serial connection - get device
|
||||
const stripDeviceSelect = document.getElementById('meshStripDevice');
|
||||
const sidebarDeviceSelect = document.getElementById('meshDeviceSelect');
|
||||
device = stripDeviceSelect?.value || sidebarDeviceSelect?.value || null;
|
||||
|
||||
// Check if auto-detect is selected but multiple ports exist
|
||||
if (!device && stripDeviceSelect && stripDeviceSelect.options.length > 2) {
|
||||
// Multiple ports available - prompt user to select one
|
||||
showStatusMessage('Multiple ports detected. Please select a specific device from the dropdown.', 'warning');
|
||||
updateStatusIndicator('disconnected', 'Select a device');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusIndicator('connecting', 'Connecting...');
|
||||
@@ -184,17 +227,27 @@ const Meshtastic = (function() {
|
||||
if (stripStatus) stripStatus.textContent = 'Connecting...';
|
||||
|
||||
try {
|
||||
const requestBody = {
|
||||
connection_type: connectionType
|
||||
};
|
||||
|
||||
if (connectionType === 'tcp') {
|
||||
requestBody.hostname = hostname;
|
||||
} else if (device) {
|
||||
requestBody.device = device;
|
||||
}
|
||||
|
||||
const response = await fetch('/meshtastic/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device: device || undefined })
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'started' || data.status === 'already_running') {
|
||||
isConnected = true;
|
||||
updateConnectionUI(true, data.device);
|
||||
updateConnectionUI(true, data.device, data.connection_type);
|
||||
if (data.node_info) {
|
||||
updateNodeInfo(data.node_info);
|
||||
localNodeId = data.node_info.num;
|
||||
@@ -202,7 +255,8 @@ const Meshtastic = (function() {
|
||||
loadChannels();
|
||||
loadNodes();
|
||||
startStream();
|
||||
showNotification('Meshtastic', 'Connected to device');
|
||||
const connLabel = data.connection_type === 'tcp' ? 'TCP' : 'Serial';
|
||||
showNotification('Meshtastic', `Connected via ${connLabel}`);
|
||||
} else {
|
||||
updateStatusIndicator('disconnected', data.message || 'Connection failed');
|
||||
showStatusMessage(data.message || 'Failed to connect', 'error');
|
||||
@@ -232,7 +286,7 @@ const Meshtastic = (function() {
|
||||
/**
|
||||
* Update connection UI state
|
||||
*/
|
||||
function updateConnectionUI(connected, device) {
|
||||
function updateConnectionUI(connected, device, connectionType) {
|
||||
const connectBtn = document.getElementById('meshConnectBtn');
|
||||
const disconnectBtn = document.getElementById('meshDisconnectBtn');
|
||||
const nodeSection = document.getElementById('meshNodeSection');
|
||||
@@ -248,7 +302,9 @@ const Meshtastic = (function() {
|
||||
const stripStatus = document.getElementById('meshStripStatus');
|
||||
|
||||
if (connected) {
|
||||
updateStatusIndicator('connected', device ? `Connected to ${device}` : 'Connected');
|
||||
const connLabel = connectionType === 'tcp' ? 'TCP' : 'Serial';
|
||||
const statusText = device ? `${device} (${connLabel})` : `Connected (${connLabel})`;
|
||||
updateStatusIndicator('connected', statusText);
|
||||
if (connectBtn) connectBtn.style.display = 'none';
|
||||
if (disconnectBtn) disconnectBtn.style.display = 'block';
|
||||
if (nodeSection) nodeSection.style.display = 'block';
|
||||
@@ -263,7 +319,7 @@ const Meshtastic = (function() {
|
||||
if (stripDot) {
|
||||
stripDot.className = 'mesh-strip-dot connected';
|
||||
}
|
||||
if (stripStatus) stripStatus.textContent = device || 'Connected';
|
||||
if (stripStatus) stripStatus.textContent = statusText;
|
||||
} else {
|
||||
updateStatusIndicator('disconnected', 'Disconnected');
|
||||
if (connectBtn) connectBtn.style.display = 'block';
|
||||
@@ -2200,6 +2256,7 @@ const Meshtastic = (function() {
|
||||
init,
|
||||
start,
|
||||
stop,
|
||||
onConnectionTypeChange,
|
||||
loadPorts,
|
||||
refreshChannels,
|
||||
openChannelModal,
|
||||
|
||||
@@ -1629,9 +1629,14 @@
|
||||
<span class="mesh-strip-dot disconnected" id="meshStripDot"></span>
|
||||
<span class="mesh-strip-status-text" id="meshStripStatus">Disconnected</span>
|
||||
</div>
|
||||
<select id="meshStripConnType" class="mesh-strip-select" title="Connection Type" onchange="Meshtastic.onConnectionTypeChange()" style="width: 70px;">
|
||||
<option value="serial">Serial</option>
|
||||
<option value="tcp">TCP</option>
|
||||
</select>
|
||||
<select id="meshStripDevice" class="mesh-strip-select" title="Device">
|
||||
<option value="">Auto-detect</option>
|
||||
</select>
|
||||
<input type="text" id="meshStripHostname" class="mesh-strip-input" placeholder="IP address" title="Hostname/IP for TCP" style="display: none; width: 120px;">
|
||||
<button class="mesh-strip-btn connect" id="meshStripConnectBtn" onclick="Meshtastic.start()">Connect</button>
|
||||
<button class="mesh-strip-btn disconnect" id="meshStripDisconnectBtn" onclick="Meshtastic.stop()" style="display: none;">Disconnect</button>
|
||||
</div>
|
||||
|
||||
+42
-13
@@ -3,7 +3,10 @@
|
||||
This module provides integration with Meshtastic mesh networking devices,
|
||||
allowing INTERCEPT to receive and decode messages from LoRa mesh networks.
|
||||
|
||||
Requires a physical Meshtastic device connected via USB/Serial.
|
||||
Supports multiple connection types:
|
||||
- USB/Serial: Physical device connected via USB
|
||||
- TCP: WiFi-enabled devices (T-Beam, Heltec WiFi LoRa, etc.)
|
||||
|
||||
Install SDK with: pip install meshtastic
|
||||
"""
|
||||
|
||||
@@ -28,6 +31,7 @@ logger = get_logger('intercept.meshtastic')
|
||||
try:
|
||||
import meshtastic
|
||||
import meshtastic.serial_interface
|
||||
import meshtastic.tcp_interface
|
||||
from meshtastic import BROADCAST_ADDR
|
||||
from pubsub import pub
|
||||
HAS_MESHTASTIC = True
|
||||
@@ -278,6 +282,7 @@ class MeshtasticClient:
|
||||
self._lock = threading.Lock()
|
||||
self._nodes: dict[int, MeshNode] = {} # num -> MeshNode
|
||||
self._device_path: str | None = None
|
||||
self._connection_type: str | None = None # 'serial' or 'tcp'
|
||||
self._error: str | None = None
|
||||
self._traceroute_results: list[TracerouteResult] = []
|
||||
self._max_traceroute_results = 50
|
||||
@@ -309,6 +314,10 @@ class MeshtasticClient:
|
||||
def device_path(self) -> str | None:
|
||||
return self._device_path
|
||||
|
||||
@property
|
||||
def connection_type(self) -> str | None:
|
||||
return self._connection_type
|
||||
|
||||
@property
|
||||
def error(self) -> str | None:
|
||||
return self._error
|
||||
@@ -317,13 +326,16 @@ class MeshtasticClient:
|
||||
"""Set callback for received messages."""
|
||||
self._callback = callback
|
||||
|
||||
def connect(self, device: str | None = None) -> bool:
|
||||
def connect(self, device: str | None = None, connection_type: str = 'serial',
|
||||
hostname: str | None = None) -> bool:
|
||||
"""
|
||||
Connect to a Meshtastic device.
|
||||
|
||||
Args:
|
||||
device: Serial port path (e.g., /dev/ttyUSB0, /dev/ttyACM0).
|
||||
If None, auto-discovers first available device.
|
||||
Only used for serial connections. If None, auto-discovers.
|
||||
connection_type: Connection type - 'serial' or 'tcp' (default: 'serial')
|
||||
hostname: Hostname or IP address for TCP connections (e.g., '192.168.1.100')
|
||||
|
||||
Returns:
|
||||
True if connected successfully.
|
||||
@@ -342,18 +354,30 @@ class MeshtasticClient:
|
||||
pub.subscribe(self._on_connection, "meshtastic.connection.established")
|
||||
pub.subscribe(self._on_disconnect, "meshtastic.connection.lost")
|
||||
|
||||
# Connect to device
|
||||
if device:
|
||||
self._interface = meshtastic.serial_interface.SerialInterface(device)
|
||||
self._device_path = device
|
||||
# Connect based on connection type
|
||||
if connection_type == 'tcp':
|
||||
if not hostname:
|
||||
self._error = "Hostname is required for TCP connections"
|
||||
self._cleanup_subscriptions()
|
||||
return False
|
||||
self._interface = meshtastic.tcp_interface.TCPInterface(hostname=hostname)
|
||||
self._device_path = hostname
|
||||
self._connection_type = 'tcp'
|
||||
logger.info(f"Connected to Meshtastic device via TCP: {hostname}")
|
||||
else:
|
||||
# Auto-discover
|
||||
self._interface = meshtastic.serial_interface.SerialInterface()
|
||||
self._device_path = "auto"
|
||||
# Serial connection (default)
|
||||
if device:
|
||||
self._interface = meshtastic.serial_interface.SerialInterface(device)
|
||||
self._device_path = device
|
||||
else:
|
||||
# Auto-discover
|
||||
self._interface = meshtastic.serial_interface.SerialInterface()
|
||||
self._device_path = "auto"
|
||||
self._connection_type = 'serial'
|
||||
logger.info(f"Connected to Meshtastic device via serial: {self._device_path}")
|
||||
|
||||
self._running = True
|
||||
self._error = None
|
||||
logger.info(f"Connected to Meshtastic device: {self._device_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -375,6 +399,7 @@ class MeshtasticClient:
|
||||
self._cleanup_subscriptions()
|
||||
self._running = False
|
||||
self._device_path = None
|
||||
self._connection_type = None
|
||||
logger.info("Disconnected from Meshtastic device")
|
||||
|
||||
def _cleanup_subscriptions(self) -> None:
|
||||
@@ -1502,13 +1527,17 @@ def get_meshtastic_client() -> MeshtasticClient | None:
|
||||
|
||||
|
||||
def start_meshtastic(device: str | None = None,
|
||||
callback: Callable[[MeshtasticMessage], None] | None = None) -> bool:
|
||||
callback: Callable[[MeshtasticMessage], None] | None = None,
|
||||
connection_type: str = 'serial',
|
||||
hostname: str | None = None) -> bool:
|
||||
"""
|
||||
Start the Meshtastic client.
|
||||
|
||||
Args:
|
||||
device: Serial port path (optional, auto-discovers if not provided)
|
||||
callback: Function to call when messages are received
|
||||
connection_type: Connection type - 'serial' or 'tcp' (default: 'serial')
|
||||
hostname: Hostname or IP address for TCP connections
|
||||
|
||||
Returns:
|
||||
True if started successfully
|
||||
@@ -1522,7 +1551,7 @@ def start_meshtastic(device: str | None = None,
|
||||
if callback:
|
||||
_client.set_callback(callback)
|
||||
|
||||
return _client.connect(device)
|
||||
return _client.connect(device, connection_type=connection_type, hostname=hostname)
|
||||
|
||||
|
||||
def stop_meshtastic() -> None:
|
||||
|
||||
Reference in New Issue
Block a user