diff --git a/routes/meshtastic.py b/routes/meshtastic.py
index 32c1e58..6e69925 100644
--- a/routes/meshtastic.py
+++ b/routes/meshtastic.py
@@ -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:
diff --git a/static/css/modes/meshtastic.css b/static/css/modes/meshtastic.css
index d39d459..3602dc0 100644
--- a/static/css/modes/meshtastic.css
+++ b/static/css/modes/meshtastic.css
@@ -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;
diff --git a/static/js/modes/meshtastic.js b/static/js/modes/meshtastic.js
index b271dfd..38ab43c 100644
--- a/static/js/modes/meshtastic.js
+++ b/static/js/modes/meshtastic.js
@@ -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,
diff --git a/templates/index.html b/templates/index.html
index 13700d2..668c213 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1629,9 +1629,14 @@
Disconnected
+
+
diff --git a/utils/meshtastic.py b/utils/meshtastic.py
index fc2ec67..f5b35eb 100644
--- a/utils/meshtastic.py
+++ b/utils/meshtastic.py
@@ -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: