diff --git a/routes/wefax.py b/routes/wefax.py index ccce86d..497cdf0 100644 --- a/routes/wefax.py +++ b/routes/wefax.py @@ -11,17 +11,18 @@ import queue from flask import Blueprint, Response, jsonify, request, send_file import app as app_module -from utils.logging import get_logger -from utils.sse import sse_stream_fanout -from utils.validation import validate_frequency -from utils.wefax import get_wefax_decoder -from utils.wefax_stations import ( - WEFAX_USB_ALIGNMENT_OFFSET_KHZ, - get_current_broadcasts, - get_station, - load_stations, - resolve_tuning_frequency_khz, -) +from utils.logging import get_logger +from utils.sdr import SDRType +from utils.sse import sse_stream_fanout +from utils.validation import validate_frequency +from utils.wefax import get_wefax_decoder +from utils.wefax_stations import ( + WEFAX_USB_ALIGNMENT_OFFSET_KHZ, + get_current_broadcasts, + get_station, + load_stations, + resolve_tuning_frequency_khz, +) logger = get_logger('intercept.wefax') @@ -34,29 +35,29 @@ _wefax_queue: queue.Queue = queue.Queue(maxsize=100) wefax_active_device: int | None = None -def _progress_callback(data: dict) -> None: - """Callback to queue progress updates for SSE stream.""" - global wefax_active_device - - try: - _wefax_queue.put_nowait(data) - except queue.Full: - try: - _wefax_queue.get_nowait() - _wefax_queue.put_nowait(data) - except queue.Empty: - pass - - # Ensure manually claimed SDR devices are always released when a - # decode session ends on its own (complete/error/stopped). - if ( - isinstance(data, dict) - and data.get('type') == 'wefax_progress' - and data.get('status') in ('complete', 'error', 'stopped') - and wefax_active_device is not None - ): - app_module.release_sdr_device(wefax_active_device) - wefax_active_device = None +def _progress_callback(data: dict) -> None: + """Callback to queue progress updates for SSE stream.""" + global wefax_active_device + + try: + _wefax_queue.put_nowait(data) + except queue.Full: + try: + _wefax_queue.get_nowait() + _wefax_queue.put_nowait(data) + except queue.Empty: + pass + + # Ensure manually claimed SDR devices are always released when a + # decode session ends on its own (complete/error/stopped). + if ( + isinstance(data, dict) + and data.get('type') == 'wefax_progress' + and data.get('status') in ('complete', 'error', 'stopped') + and wefax_active_device is not None + ): + app_module.release_sdr_device(wefax_active_device) + wefax_active_device = None @wefax_bp.route('/status') @@ -75,16 +76,16 @@ def start_decoder(): """Start WeFax decoder. JSON body: - { - "frequency_khz": 4298, - "station": "NOJ", - "device": 0, - "gain": 40, - "ioc": 576, - "lpm": 120, - "direct_sampling": true, - "frequency_reference": "auto" // auto, carrier, or dial - } + { + "frequency_khz": 4298, + "station": "NOJ", + "device": 0, + "gain": 40, + "ioc": 576, + "lpm": 120, + "direct_sampling": true, + "frequency_reference": "auto" // auto, carrier, or dial + } """ decoder = get_wefax_decoder() @@ -122,36 +123,42 @@ def start_decoder(): 'message': f'Invalid frequency: {e}', }), 400 - station = str(data.get('station', '')).strip() - device_index = data.get('device', 0) - gain = float(data.get('gain', 40.0)) - ioc = int(data.get('ioc', 576)) - lpm = int(data.get('lpm', 120)) - direct_sampling = bool(data.get('direct_sampling', True)) - frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower() - if not frequency_reference: - frequency_reference = 'auto' - - try: - tuned_frequency_khz, resolved_reference, usb_offset_applied = ( - resolve_tuning_frequency_khz( - listed_frequency_khz=frequency_khz, - station_callsign=station, - frequency_reference=frequency_reference, - ) - ) - tuned_mhz = tuned_frequency_khz / 1000.0 - validate_frequency(tuned_mhz, min_mhz=2.0, max_mhz=30.0) - except ValueError as e: - return jsonify({ - 'status': 'error', - 'message': f'Invalid frequency settings: {e}', - }), 400 - - # Validate IOC and LPM - if ioc not in (288, 576): - return jsonify({ - 'status': 'error', + station = str(data.get('station', '')).strip() + device_index = data.get('device', 0) + gain = float(data.get('gain', 40.0)) + ioc = int(data.get('ioc', 576)) + lpm = int(data.get('lpm', 120)) + direct_sampling = bool(data.get('direct_sampling', True)) + frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower() + + sdr_type_str = str(data.get('sdr_type', 'rtlsdr')).lower() + try: + sdr_type = SDRType(sdr_type_str) + except ValueError: + sdr_type = SDRType.RTL_SDR + if not frequency_reference: + frequency_reference = 'auto' + + try: + tuned_frequency_khz, resolved_reference, usb_offset_applied = ( + resolve_tuning_frequency_khz( + listed_frequency_khz=frequency_khz, + station_callsign=station, + frequency_reference=frequency_reference, + ) + ) + tuned_mhz = tuned_frequency_khz / 1000.0 + validate_frequency(tuned_mhz, min_mhz=2.0, max_mhz=30.0) + except ValueError as e: + return jsonify({ + 'status': 'error', + 'message': f'Invalid frequency settings: {e}', + }), 400 + + # Validate IOC and LPM + if ioc not in (288, 576): + return jsonify({ + 'status': 'error', 'message': 'IOC must be 288 or 576', }), 400 @@ -172,34 +179,35 @@ def start_decoder(): 'message': error, }), 409 - # Set callback and start - decoder.set_callback(_progress_callback) - success = decoder.start( - frequency_khz=tuned_frequency_khz, - station=station, - device_index=device_int, - gain=gain, - ioc=ioc, - lpm=lpm, + # Set callback and start + decoder.set_callback(_progress_callback) + success = decoder.start( + frequency_khz=tuned_frequency_khz, + station=station, + device_index=device_int, + gain=gain, + ioc=ioc, + lpm=lpm, direct_sampling=direct_sampling, + sdr_type=sdr_type_str, ) if success: wefax_active_device = device_int - return jsonify({ - 'status': 'started', - 'frequency_khz': frequency_khz, - 'tuned_frequency_khz': tuned_frequency_khz, - 'frequency_reference': resolved_reference, - 'usb_offset_applied': usb_offset_applied, - 'usb_offset_khz': ( - WEFAX_USB_ALIGNMENT_OFFSET_KHZ if usb_offset_applied else 0.0 - ), - 'station': station, - 'ioc': ioc, - 'lpm': lpm, - 'device': device_int, - }) + return jsonify({ + 'status': 'started', + 'frequency_khz': frequency_khz, + 'tuned_frequency_khz': tuned_frequency_khz, + 'frequency_reference': resolved_reference, + 'usb_offset_applied': usb_offset_applied, + 'usb_offset_khz': ( + WEFAX_USB_ALIGNMENT_OFFSET_KHZ if usb_offset_applied else 0.0 + ), + 'station': station, + 'ioc': ioc, + 'lpm': lpm, + 'device': device_int, + }) else: app_module.release_sdr_device(device_int) return jsonify({ @@ -322,16 +330,16 @@ def enable_schedule(): """Enable auto-scheduling of WeFax broadcast captures. JSON body: - { - "station": "NOJ", - "frequency_khz": 4298, - "device": 0, - "gain": 40, - "ioc": 576, - "lpm": 120, - "direct_sampling": true, - "frequency_reference": "auto" // auto, carrier, or dial - } + { + "station": "NOJ", + "frequency_khz": 4298, + "device": 0, + "gain": 40, + "ioc": 576, + "lpm": 120, + "direct_sampling": true, + "frequency_reference": "auto" // auto, carrier, or dial + } Returns: JSON with scheduler status. @@ -365,61 +373,61 @@ def enable_schedule(): }), 400 device = int(data.get('device', 0)) - gain = float(data.get('gain', 40.0)) - ioc = int(data.get('ioc', 576)) - lpm = int(data.get('lpm', 120)) - direct_sampling = bool(data.get('direct_sampling', True)) - frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower() - if not frequency_reference: - frequency_reference = 'auto' - - try: - tuned_frequency_khz, resolved_reference, usb_offset_applied = ( - resolve_tuning_frequency_khz( - listed_frequency_khz=frequency_khz, - station_callsign=station, - frequency_reference=frequency_reference, - ) - ) - tuned_mhz = tuned_frequency_khz / 1000.0 - validate_frequency(tuned_mhz, min_mhz=2.0, max_mhz=30.0) - except ValueError as e: - return jsonify({ - 'status': 'error', - 'message': f'Invalid frequency settings: {e}', - }), 400 - - scheduler = get_wefax_scheduler() - scheduler.set_callbacks(_progress_callback, _scheduler_event_callback) - - try: - result = scheduler.enable( - station=station, - frequency_khz=tuned_frequency_khz, - device=device, - gain=gain, - ioc=ioc, - lpm=lpm, - direct_sampling=direct_sampling, + gain = float(data.get('gain', 40.0)) + ioc = int(data.get('ioc', 576)) + lpm = int(data.get('lpm', 120)) + direct_sampling = bool(data.get('direct_sampling', True)) + frequency_reference = str(data.get('frequency_reference', 'auto')).strip().lower() + if not frequency_reference: + frequency_reference = 'auto' + + try: + tuned_frequency_khz, resolved_reference, usb_offset_applied = ( + resolve_tuning_frequency_khz( + listed_frequency_khz=frequency_khz, + station_callsign=station, + frequency_reference=frequency_reference, + ) + ) + tuned_mhz = tuned_frequency_khz / 1000.0 + validate_frequency(tuned_mhz, min_mhz=2.0, max_mhz=30.0) + except ValueError as e: + return jsonify({ + 'status': 'error', + 'message': f'Invalid frequency settings: {e}', + }), 400 + + scheduler = get_wefax_scheduler() + scheduler.set_callbacks(_progress_callback, _scheduler_event_callback) + + try: + result = scheduler.enable( + station=station, + frequency_khz=tuned_frequency_khz, + device=device, + gain=gain, + ioc=ioc, + lpm=lpm, + direct_sampling=direct_sampling, ) except Exception: logger.exception("Failed to enable WeFax scheduler") - return jsonify({ - 'status': 'error', - 'message': 'Failed to enable scheduler', - }), 500 - - return jsonify({ - 'status': 'ok', - **result, - 'frequency_khz': frequency_khz, - 'tuned_frequency_khz': tuned_frequency_khz, - 'frequency_reference': resolved_reference, - 'usb_offset_applied': usb_offset_applied, - 'usb_offset_khz': ( - WEFAX_USB_ALIGNMENT_OFFSET_KHZ if usb_offset_applied else 0.0 - ), - }) + return jsonify({ + 'status': 'error', + 'message': 'Failed to enable scheduler', + }), 500 + + return jsonify({ + 'status': 'ok', + **result, + 'frequency_khz': frequency_khz, + 'tuned_frequency_khz': tuned_frequency_khz, + 'frequency_reference': resolved_reference, + 'usb_offset_applied': usb_offset_applied, + 'usb_offset_khz': ( + WEFAX_USB_ALIGNMENT_OFFSET_KHZ if usb_offset_applied else 0.0 + ), + }) @wefax_bp.route('/schedule/disable', methods=['POST']) diff --git a/static/js/modes/wefax.js b/static/js/modes/wefax.js index 3499af4..853bc72 100644 --- a/static/js/modes/wefax.js +++ b/static/js/modes/wefax.js @@ -148,20 +148,20 @@ var WeFax = (function () { opt.textContent = f.khz + ' kHz — ' + f.description; sel.appendChild(opt); }); - } - - // ---- Start / Stop ---- - - function selectedFrequencyReference() { - var alignCheckbox = document.getElementById('wefaxAutoUsbAlign'); - if (alignCheckbox && !alignCheckbox.checked) { - return 'dial'; - } - return 'auto'; - } - - function start() { - if (state.running) return; + } + + // ---- Start / Stop ---- + + function selectedFrequencyReference() { + var alignCheckbox = document.getElementById('wefaxAutoUsbAlign'); + if (alignCheckbox && !alignCheckbox.checked) { + return 'dial'; + } + return 'auto'; + } + + function start() { + if (state.running) return; var freqSel = document.getElementById('wefaxFrequency'); var freqKhz = freqSel ? parseFloat(freqSel.value) : 0; @@ -177,41 +177,42 @@ var WeFax = (function () { var gainInput = document.getElementById('wefaxGain'); var dsCheckbox = document.getElementById('wefaxDirectSampling'); - var deviceSel = document.getElementById('rtlDevice'); - var device = deviceSel ? parseInt(deviceSel.value, 10) || 0 : 0; + var device = (typeof getSelectedDevice === 'function') + ? parseInt(getSelectedDevice(), 10) || 0 : 0; var body = { frequency_khz: freqKhz, station: station, device: device, - gain: gainInput ? parseFloat(gainInput.value) || 40 : 40, - ioc: iocSel ? parseInt(iocSel.value, 10) : 576, - lpm: lpmSel ? parseInt(lpmSel.value, 10) : 120, - direct_sampling: dsCheckbox ? dsCheckbox.checked : true, - frequency_reference: selectedFrequencyReference(), - }; + sdr_type: (typeof getSelectedSDRType === 'function') ? getSelectedSDRType() : 'rtlsdr', + gain: gainInput ? parseFloat(gainInput.value) || 40 : 40, + ioc: iocSel ? parseInt(iocSel.value, 10) : 576, + lpm: lpmSel ? parseInt(lpmSel.value, 10) : 120, + direct_sampling: dsCheckbox ? dsCheckbox.checked : true, + frequency_reference: selectedFrequencyReference(), + }; fetch('/wefax/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) - .then(function (r) { return r.json(); }) - .then(function (data) { - if (data.status === 'started' || data.status === 'already_running') { - var tunedKhz = Number(data.tuned_frequency_khz); - if (isNaN(tunedKhz) || tunedKhz <= 0) tunedKhz = freqKhz; - state.running = true; - updateButtons(true); - if (data.usb_offset_applied) { - setStatus('Scanning ' + tunedKhz + ' kHz (USB aligned from ' + freqKhz + ' kHz)...'); - } else { - setStatus('Scanning ' + tunedKhz + ' kHz...'); - } - setStripFreq(tunedKhz); - connectSSE(); - } else { - var errMsg = data.message || 'unknown error'; + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.status === 'started' || data.status === 'already_running') { + var tunedKhz = Number(data.tuned_frequency_khz); + if (isNaN(tunedKhz) || tunedKhz <= 0) tunedKhz = freqKhz; + state.running = true; + updateButtons(true); + if (data.usb_offset_applied) { + setStatus('Scanning ' + tunedKhz + ' kHz (USB aligned from ' + freqKhz + ' kHz)...'); + } else { + setStatus('Scanning ' + tunedKhz + ' kHz...'); + } + setStripFreq(tunedKhz); + connectSSE(); + } else { + var errMsg = data.message || 'unknown error'; setStatus('Error: ' + errMsg); showStripError(errMsg); } @@ -342,25 +343,25 @@ var WeFax = (function () { if (idleEl) idleEl.style.display = 'none'; } - // Image complete - if (data.status === 'complete' && data.image) { - scopeImageBurst = 1.0; - loadImages(); - setStatus('Image decoded: ' + (data.line_count || '?') + ' lines'); - } - - if (data.status === 'complete') { - state.running = false; - updateButtons(false); - if (!state.schedulerEnabled) { - disconnectSSE(); - } - } - - if (data.status === 'error') { - state.running = false; - updateButtons(false); - showStripError(data.message || 'Decode error'); + // Image complete + if (data.status === 'complete' && data.image) { + scopeImageBurst = 1.0; + loadImages(); + setStatus('Image decoded: ' + (data.line_count || '?') + ' lines'); + } + + if (data.status === 'complete') { + state.running = false; + updateButtons(false); + if (!state.schedulerEnabled) { + disconnectSSE(); + } + } + + if (data.status === 'error') { + state.running = false; + updateButtons(false); + showStripError(data.message || 'Decode error'); } if (data.status === 'stopped') { @@ -1062,24 +1063,24 @@ var WeFax = (function () { station: station, frequency_khz: freqKhz, device: device, - gain: gainInput ? parseFloat(gainInput.value) || 40 : 40, - ioc: iocSel ? parseInt(iocSel.value, 10) : 576, - lpm: lpmSel ? parseInt(lpmSel.value, 10) : 120, - direct_sampling: dsCheckbox ? dsCheckbox.checked : true, - frequency_reference: selectedFrequencyReference(), - }), - }) - .then(function (r) { return r.json(); }) - .then(function (data) { - if (data.status === 'ok') { - var status = 'Auto-capture enabled — ' + (data.scheduled_count || 0) + ' broadcasts scheduled'; - if (data.usb_offset_applied && !isNaN(Number(data.tuned_frequency_khz))) { - status += ' (tuning ' + Number(data.tuned_frequency_khz) + ' kHz)'; - } - setStatus(status); - syncSchedulerCheckboxes(true); - state.schedulerEnabled = true; - connectSSE(); + gain: gainInput ? parseFloat(gainInput.value) || 40 : 40, + ioc: iocSel ? parseInt(iocSel.value, 10) : 576, + lpm: lpmSel ? parseInt(lpmSel.value, 10) : 120, + direct_sampling: dsCheckbox ? dsCheckbox.checked : true, + frequency_reference: selectedFrequencyReference(), + }), + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (data.status === 'ok') { + var status = 'Auto-capture enabled — ' + (data.scheduled_count || 0) + ' broadcasts scheduled'; + if (data.usb_offset_applied && !isNaN(Number(data.tuned_frequency_khz))) { + status += ' (tuning ' + Number(data.tuned_frequency_khz) + ' kHz)'; + } + setStatus(status); + syncSchedulerCheckboxes(true); + state.schedulerEnabled = true; + connectSSE(); startSchedulerPoll(); } else { setStatus('Scheduler error: ' + (data.message || 'unknown')); diff --git a/templates/partials/modes/wefax.html b/templates/partials/modes/wefax.html index e0dd170..55b3e50 100644 --- a/templates/partials/modes/wefax.html +++ b/templates/partials/modes/wefax.html @@ -44,18 +44,18 @@ -
- - -
-
- - -
-

- Disable this if your source already provides USB dial frequencies. -

- +
+ + +
+
+ + +
+

+ Disable this if your source already provides USB dial frequencies. +

+

Auto Capture

@@ -80,7 +80,7 @@
Requirements