mirror of
https://github.com/smittix/intercept.git
synced 2026-07-08 01:28:13 -07:00
feat: Enhance Meshtastic mode with QR code support
Add QR code generation for sharing Meshtastic channel configurations. Add qrcode[pil] dependency for QR code generation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -607,3 +607,407 @@ def get_traceroute_results():
|
||||
'results': [r.to_dict() for r in results],
|
||||
'count': len(results)
|
||||
})
|
||||
|
||||
|
||||
@meshtastic_bp.route('/position/request', methods=['POST'])
|
||||
def request_position():
|
||||
"""
|
||||
Request position from a specific node.
|
||||
|
||||
JSON body:
|
||||
{
|
||||
"node_id": "!a1b2c3d4" // Required: target node ID
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON with request status.
|
||||
"""
|
||||
if not is_meshtastic_available():
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Meshtastic SDK not installed'
|
||||
}), 400
|
||||
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device'
|
||||
}), 400
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
node_id = data.get('node_id')
|
||||
|
||||
if not node_id:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Node ID is required'
|
||||
}), 400
|
||||
|
||||
success, error = client.request_position(node_id)
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
'status': 'sent',
|
||||
'node_id': node_id
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': error or 'Failed to request position'
|
||||
}), 500
|
||||
|
||||
|
||||
@meshtastic_bp.route('/firmware/check')
|
||||
def check_firmware():
|
||||
"""
|
||||
Check current firmware version and compare to latest release.
|
||||
|
||||
Returns:
|
||||
JSON with current_version, latest_version, update_available, release_url.
|
||||
"""
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device'
|
||||
}), 400
|
||||
|
||||
result = client.check_firmware()
|
||||
result['status'] = 'ok'
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@meshtastic_bp.route('/channels/<int:index>/qr')
|
||||
def get_channel_qr(index: int):
|
||||
"""
|
||||
Generate QR code for a channel configuration.
|
||||
|
||||
Args:
|
||||
index: Channel index (0-7)
|
||||
|
||||
Returns:
|
||||
PNG image of QR code.
|
||||
"""
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device'
|
||||
}), 400
|
||||
|
||||
if not 0 <= index <= 7:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Channel index must be 0-7'
|
||||
}), 400
|
||||
|
||||
png_data = client.generate_channel_qr(index)
|
||||
|
||||
if png_data:
|
||||
return Response(png_data, mimetype='image/png')
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Failed to generate QR code. Make sure qrcode library is installed.'
|
||||
}), 500
|
||||
|
||||
|
||||
@meshtastic_bp.route('/telemetry/history')
|
||||
def get_telemetry_history():
|
||||
"""
|
||||
Get telemetry history for a node.
|
||||
|
||||
Query parameters:
|
||||
node_id: Node ID or number (required)
|
||||
hours: Number of hours of history (default: 24)
|
||||
|
||||
Returns:
|
||||
JSON with telemetry data points.
|
||||
"""
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device',
|
||||
'data': []
|
||||
}), 400
|
||||
|
||||
node_id = request.args.get('node_id')
|
||||
hours = request.args.get('hours', 24, type=int)
|
||||
|
||||
if not node_id:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'node_id is required',
|
||||
'data': []
|
||||
}), 400
|
||||
|
||||
# Parse node ID to number
|
||||
try:
|
||||
if node_id.startswith('!'):
|
||||
node_num = int(node_id[1:], 16)
|
||||
else:
|
||||
node_num = int(node_id)
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Invalid node_id: {node_id}',
|
||||
'data': []
|
||||
}), 400
|
||||
|
||||
history = client.get_telemetry_history(node_num, hours=hours)
|
||||
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'node_id': node_id,
|
||||
'hours': hours,
|
||||
'data': [p.to_dict() for p in history],
|
||||
'count': len(history)
|
||||
})
|
||||
|
||||
|
||||
@meshtastic_bp.route('/neighbors')
|
||||
def get_neighbors():
|
||||
"""
|
||||
Get neighbor information for mesh topology visualization.
|
||||
|
||||
Query parameters:
|
||||
node_id: Specific node ID (optional, returns all if not provided)
|
||||
|
||||
Returns:
|
||||
JSON with neighbor relationships.
|
||||
"""
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device',
|
||||
'neighbors': {}
|
||||
}), 400
|
||||
|
||||
node_id = request.args.get('node_id')
|
||||
node_num = None
|
||||
|
||||
if node_id:
|
||||
try:
|
||||
if node_id.startswith('!'):
|
||||
node_num = int(node_id[1:], 16)
|
||||
else:
|
||||
node_num = int(node_id)
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Invalid node_id: {node_id}',
|
||||
'neighbors': {}
|
||||
}), 400
|
||||
|
||||
neighbors = client.get_neighbors(node_num)
|
||||
|
||||
# Convert to JSON-serializable format
|
||||
result = {}
|
||||
for num, neighbor_list in neighbors.items():
|
||||
node_key = f"!{num:08x}"
|
||||
result[node_key] = [n.to_dict() for n in neighbor_list]
|
||||
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'neighbors': result,
|
||||
'node_count': len(result)
|
||||
})
|
||||
|
||||
|
||||
@meshtastic_bp.route('/pending')
|
||||
def get_pending_messages():
|
||||
"""
|
||||
Get messages waiting for ACK.
|
||||
|
||||
Returns:
|
||||
JSON with pending messages and their status.
|
||||
"""
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device',
|
||||
'messages': []
|
||||
}), 400
|
||||
|
||||
pending = client.get_pending_messages()
|
||||
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'messages': [m.to_dict() for m in pending.values()],
|
||||
'count': len(pending)
|
||||
})
|
||||
|
||||
|
||||
@meshtastic_bp.route('/range-test/start', methods=['POST'])
|
||||
def start_range_test():
|
||||
"""
|
||||
Start a range test.
|
||||
|
||||
JSON body:
|
||||
{
|
||||
"count": 10, // Number of packets to send (default 10)
|
||||
"interval": 5 // Seconds between packets (default 5)
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON with start status.
|
||||
"""
|
||||
if not is_meshtastic_available():
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Meshtastic SDK not installed'
|
||||
}), 400
|
||||
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device'
|
||||
}), 400
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
count = data.get('count', 10)
|
||||
interval = data.get('interval', 5)
|
||||
|
||||
# Validate
|
||||
if not isinstance(count, int) or count < 1 or count > 100:
|
||||
count = 10
|
||||
if not isinstance(interval, int) or interval < 1 or interval > 60:
|
||||
interval = 5
|
||||
|
||||
success, error = client.start_range_test(count=count, interval=interval)
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
'status': 'started',
|
||||
'count': count,
|
||||
'interval': interval
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': error or 'Failed to start range test'
|
||||
}), 500
|
||||
|
||||
|
||||
@meshtastic_bp.route('/range-test/stop', methods=['POST'])
|
||||
def stop_range_test():
|
||||
"""
|
||||
Stop an ongoing range test.
|
||||
|
||||
Returns:
|
||||
JSON confirmation.
|
||||
"""
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if client:
|
||||
client.stop_range_test()
|
||||
|
||||
return jsonify({'status': 'stopped'})
|
||||
|
||||
|
||||
@meshtastic_bp.route('/range-test/status')
|
||||
def get_range_test_status():
|
||||
"""
|
||||
Get range test status and results.
|
||||
|
||||
Returns:
|
||||
JSON with running status and results.
|
||||
"""
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device',
|
||||
'running': False,
|
||||
'results': []
|
||||
}), 400
|
||||
|
||||
status = client.get_range_test_status()
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
**status
|
||||
})
|
||||
|
||||
|
||||
@meshtastic_bp.route('/store-forward/status')
|
||||
def get_store_forward_status():
|
||||
"""
|
||||
Check if Store & Forward router is available.
|
||||
|
||||
Returns:
|
||||
JSON with availability status and router info.
|
||||
"""
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device',
|
||||
'available': False
|
||||
}), 400
|
||||
|
||||
sf_status = client.check_store_forward_available()
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
**sf_status
|
||||
})
|
||||
|
||||
|
||||
@meshtastic_bp.route('/store-forward/request', methods=['POST'])
|
||||
def request_store_forward():
|
||||
"""
|
||||
Request missed messages from Store & Forward router.
|
||||
|
||||
JSON body:
|
||||
{
|
||||
"window_minutes": 60 // Minutes of history to request (default 60)
|
||||
}
|
||||
|
||||
Returns:
|
||||
JSON with request status.
|
||||
"""
|
||||
if not is_meshtastic_available():
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Meshtastic SDK not installed'
|
||||
}), 400
|
||||
|
||||
client = get_meshtastic_client()
|
||||
|
||||
if not client or not client.is_running:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Not connected to Meshtastic device'
|
||||
}), 400
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
window_minutes = data.get('window_minutes', 60)
|
||||
|
||||
if not isinstance(window_minutes, int) or window_minutes < 1 or window_minutes > 1440:
|
||||
window_minutes = 60
|
||||
|
||||
success, error = client.request_store_forward(window_minutes=window_minutes)
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
'status': 'sent',
|
||||
'window_minutes': window_minutes
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': error or 'Failed to request S&F history'
|
||||
}), 500
|
||||
|
||||
Reference in New Issue
Block a user