diff --git a/routes/wefax.py b/routes/wefax.py
index d5c58e1..ccce86d 100644
--- a/routes/wefax.py
+++ b/routes/wefax.py
@@ -11,11 +11,17 @@ 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 get_current_broadcasts, get_station, load_stations
+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,
+)
logger = get_logger('intercept.wefax')
@@ -69,15 +75,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_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()
@@ -115,17 +122,36 @@ 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))
-
- # 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()
+ 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
@@ -146,28 +172,34 @@ def start_decoder():
'message': error,
}), 409
- # Set callback and start
- decoder.set_callback(_progress_callback)
- success = decoder.start(
- frequency_khz=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,
)
if success:
wefax_active_device = device_int
- return jsonify({
- 'status': 'started',
- 'frequency_khz': frequency_khz,
- '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({
@@ -290,15 +322,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
- }
+ {
+ "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.
@@ -332,32 +365,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))
-
- scheduler = get_wefax_scheduler()
- scheduler.set_callbacks(_progress_callback, _scheduler_event_callback)
-
- try:
- result = scheduler.enable(
- station=station,
- frequency_khz=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})
+ 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 a5a1902..3499af4 100644
--- a/static/js/modes/wefax.js
+++ b/static/js/modes/wefax.js
@@ -148,12 +148,20 @@ var WeFax = (function () {
opt.textContent = f.khz + ' kHz — ' + f.description;
sel.appendChild(opt);
});
- }
-
- // ---- Start / Stop ----
-
- 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;
@@ -176,27 +184,34 @@ var WeFax = (function () {
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,
- };
+ 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') {
- state.running = true;
- updateButtons(true);
- setStatus('Scanning ' + freqKhz + ' kHz...');
- setStripFreq(freqKhz);
- 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);
}
@@ -1047,19 +1062,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,
- }),
- })
- .then(function (r) { return r.json(); })
- .then(function (data) {
- if (data.status === 'ok') {
- setStatus('Auto-capture enabled — ' + (data.scheduled_count || 0) + ' broadcasts scheduled');
- 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 319e6a0..e0dd170 100644
--- a/templates/partials/modes/wefax.html
+++ b/templates/partials/modes/wefax.html
@@ -44,11 +44,18 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Disable this if your source already provides USB dial frequencies.
+
+
Auto Capture
diff --git a/tests/test_wefax.py b/tests/test_wefax.py
index 2f23d1a..9a62192 100644
--- a/tests/test_wefax.py
+++ b/tests/test_wefax.py
@@ -54,16 +54,72 @@ class TestWeFaxStations:
from utils.wefax_stations import get_station
assert get_station('noj') is not None
- def test_get_station_not_found(self):
- """get_station() should return None for unknown callsign."""
- from utils.wefax_stations import get_station
- assert get_station('XXXXX') is None
-
- def test_station_frequencies_have_khz(self):
- """Each frequency entry must have 'khz' and 'description'."""
- from utils.wefax_stations import load_stations
- for station in load_stations():
- for freq in station['frequencies']:
+ def test_get_station_not_found(self):
+ """get_station() should return None for unknown callsign."""
+ from utils.wefax_stations import get_station
+ assert get_station('XXXXX') is None
+
+ def test_resolve_tuning_frequency_auto_uses_carrier_for_known_station(self):
+ """Known station frequencies default to carrier-list behavior in auto mode."""
+ from utils.wefax_stations import resolve_tuning_frequency_khz
+
+ tuned, reference, offset_applied = resolve_tuning_frequency_khz(
+ listed_frequency_khz=4298.0,
+ station_callsign='NOJ',
+ frequency_reference='auto',
+ )
+
+ assert math.isclose(tuned, 4296.1, abs_tol=1e-6)
+ assert reference == 'carrier'
+ assert offset_applied is True
+
+ def test_resolve_tuning_frequency_auto_preserves_unknown_station_input(self):
+ """Ad-hoc frequencies (no station metadata) should be treated as dial."""
+ from utils.wefax_stations import resolve_tuning_frequency_khz
+
+ tuned, reference, offset_applied = resolve_tuning_frequency_khz(
+ listed_frequency_khz=4298.0,
+ station_callsign='',
+ frequency_reference='auto',
+ )
+
+ assert math.isclose(tuned, 4298.0, abs_tol=1e-6)
+ assert reference == 'dial'
+ assert offset_applied is False
+
+ def test_resolve_tuning_frequency_dial_override(self):
+ """Explicit dial reference must bypass USB alignment."""
+ from utils.wefax_stations import resolve_tuning_frequency_khz
+
+ tuned, reference, offset_applied = resolve_tuning_frequency_khz(
+ listed_frequency_khz=4298.0,
+ station_callsign='NOJ',
+ frequency_reference='dial',
+ )
+
+ assert math.isclose(tuned, 4298.0, abs_tol=1e-6)
+ assert reference == 'dial'
+ assert offset_applied is False
+
+ def test_resolve_tuning_frequency_rejects_invalid_reference(self):
+ """Invalid frequency reference values should raise a validation error."""
+ from utils.wefax_stations import resolve_tuning_frequency_khz
+
+ try:
+ resolve_tuning_frequency_khz(
+ listed_frequency_khz=4298.0,
+ station_callsign='NOJ',
+ frequency_reference='invalid',
+ )
+ assert False, "Expected ValueError for invalid frequency_reference"
+ except ValueError as exc:
+ assert 'frequency_reference' in str(exc)
+
+ def test_station_frequencies_have_khz(self):
+ """Each frequency entry must have 'khz' and 'description'."""
+ from utils.wefax_stations import load_stations
+ for station in load_stations():
+ for freq in station['frequencies']:
assert 'khz' in freq, f"{station['callsign']} missing khz"
assert 'description' in freq, f"{station['callsign']} missing description"
assert isinstance(freq['khz'], (int, float))
@@ -334,11 +390,11 @@ class TestWeFaxRoutes:
data = response.get_json()
assert 'LPM' in data['message']
- def test_start_success(self, client):
- """POST /wefax/start with valid params should succeed."""
- _login_session(client)
- mock_decoder = MagicMock()
- mock_decoder.is_running = False
+ def test_start_success(self, client):
+ """POST /wefax/start with valid params should succeed."""
+ _login_session(client)
+ mock_decoder = MagicMock()
+ mock_decoder.is_running = False
mock_decoder.start.return_value = True
with patch('routes.wefax.get_wefax_decoder', return_value=mock_decoder), \
@@ -355,12 +411,46 @@ class TestWeFaxRoutes:
content_type='application/json',
)
- assert response.status_code == 200
- data = response.get_json()
- assert data['status'] == 'started'
- assert data['frequency_khz'] == 4298
- assert data['station'] == 'NOJ'
- mock_decoder.start.assert_called_once()
+ assert response.status_code == 200
+ data = response.get_json()
+ assert data['status'] == 'started'
+ assert data['frequency_khz'] == 4298
+ assert data['usb_offset_applied'] is True
+ assert math.isclose(data['tuned_frequency_khz'], 4296.1, abs_tol=1e-6)
+ assert data['frequency_reference'] == 'carrier'
+ assert data['station'] == 'NOJ'
+ mock_decoder.start.assert_called_once()
+ start_kwargs = mock_decoder.start.call_args.kwargs
+ assert math.isclose(start_kwargs['frequency_khz'], 4296.1, abs_tol=1e-6)
+
+ def test_start_respects_dial_reference_override(self, client):
+ """POST /wefax/start with dial reference should not apply USB offset."""
+ _login_session(client)
+ mock_decoder = MagicMock()
+ mock_decoder.is_running = False
+ mock_decoder.start.return_value = True
+
+ with patch('routes.wefax.get_wefax_decoder', return_value=mock_decoder), \
+ patch('routes.wefax.app_module.claim_sdr_device', return_value=None):
+ response = client.post(
+ '/wefax/start',
+ data=json.dumps({
+ 'frequency_khz': 4298,
+ 'station': 'NOJ',
+ 'device': 0,
+ 'frequency_reference': 'dial',
+ }),
+ content_type='application/json',
+ )
+
+ assert response.status_code == 200
+ data = response.get_json()
+ assert data['status'] == 'started'
+ assert data['usb_offset_applied'] is False
+ assert math.isclose(data['tuned_frequency_khz'], 4298.0, abs_tol=1e-6)
+ assert data['frequency_reference'] == 'dial'
+ start_kwargs = mock_decoder.start.call_args.kwargs
+ assert math.isclose(start_kwargs['frequency_khz'], 4298.0, abs_tol=1e-6)
def test_start_device_busy(self, client):
"""POST /wefax/start should return 409 when device is busy."""
@@ -429,6 +519,35 @@ class TestWeFaxRoutes:
assert response.status_code == 400
+ def test_schedule_enable_applies_usb_alignment(self, client):
+ """Scheduler should receive tuned USB dial frequency in auto mode."""
+ _login_session(client)
+ mock_scheduler = MagicMock()
+ mock_scheduler.enable.return_value = {
+ 'enabled': True,
+ 'scheduled_count': 2,
+ 'total_broadcasts': 2,
+ }
+
+ with patch('utils.wefax_scheduler.get_wefax_scheduler', return_value=mock_scheduler):
+ response = client.post(
+ '/wefax/schedule/enable',
+ data=json.dumps({
+ 'station': 'NOJ',
+ 'frequency_khz': 4298,
+ 'device': 0,
+ }),
+ content_type='application/json',
+ )
+
+ assert response.status_code == 200
+ data = response.get_json()
+ assert data['status'] == 'ok'
+ assert data['usb_offset_applied'] is True
+ assert math.isclose(data['tuned_frequency_khz'], 4296.1, abs_tol=1e-6)
+ enable_kwargs = mock_scheduler.enable.call_args.kwargs
+ assert math.isclose(enable_kwargs['frequency_khz'], 4296.1, abs_tol=1e-6)
+
class TestWeFaxProgressCallback:
"""Regression tests for WeFax route-level progress callback behavior."""
diff --git a/utils/wefax_stations.py b/utils/wefax_stations.py
index d80289c..15a3fdc 100644
--- a/utils/wefax_stations.py
+++ b/utils/wefax_stations.py
@@ -1,8 +1,8 @@
-"""WeFax station database loader.
-
-Loads and caches station data from data/wefax_stations.json. Provides
-lookup by callsign and current-broadcast filtering based on UTC time.
-"""
+"""WeFax station database loader.
+
+Loads and caches station data from data/wefax_stations.json. Provides
+lookup by callsign and current-broadcast filtering based on UTC time.
+"""
from __future__ import annotations
@@ -10,10 +10,12 @@ import json
from datetime import datetime, timezone
from pathlib import Path
-_stations_cache: list[dict] | None = None
-_stations_by_callsign: dict[str, dict] = {}
-
-_STATIONS_PATH = Path(__file__).resolve().parent.parent / 'data' / 'wefax_stations.json'
+_stations_cache: list[dict] | None = None
+_stations_by_callsign: dict[str, dict] = {}
+_VALID_FREQUENCY_REFERENCES = {'auto', 'carrier', 'dial'}
+WEFAX_USB_ALIGNMENT_OFFSET_KHZ = 1.9
+
+_STATIONS_PATH = Path(__file__).resolve().parent.parent / 'data' / 'wefax_stations.json'
def load_stations() -> list[dict]:
@@ -31,10 +33,79 @@ def load_stations() -> list[dict]:
return _stations_cache
-def get_station(callsign: str) -> dict | None:
- """Get a single station by callsign."""
- load_stations()
- return _stations_by_callsign.get(callsign.upper())
+def get_station(callsign: str) -> dict | None:
+ """Get a single station by callsign."""
+ load_stations()
+ return _stations_by_callsign.get(callsign.upper())
+
+
+def _normalize_frequency_reference(value: str | None) -> str:
+ """Normalize and validate frequency reference token."""
+ reference = str(value or 'auto').strip().lower()
+ if reference not in _VALID_FREQUENCY_REFERENCES:
+ choices = ', '.join(sorted(_VALID_FREQUENCY_REFERENCES))
+ raise ValueError(f'frequency_reference must be one of: {choices}')
+ return reference
+
+
+def _station_frequency_reference(station: dict, listed_frequency_khz: float) -> str:
+ """Infer whether a station frequency entry is carrier or already USB dial."""
+ for entry in station.get('frequencies', []):
+ try:
+ entry_khz = float(entry.get('khz'))
+ except (TypeError, ValueError):
+ continue
+ if abs(entry_khz - listed_frequency_khz) > 0.001:
+ continue
+ entry_ref = str(entry.get('reference', '')).strip().lower()
+ if entry_ref in ('carrier', 'dial'):
+ return entry_ref
+
+ station_ref = str(station.get('frequency_reference', '')).strip().lower()
+ if station_ref in ('carrier', 'dial'):
+ return station_ref
+
+ # Most published marine WeFax channel lists are carrier frequencies.
+ return 'carrier'
+
+
+def resolve_tuning_frequency_khz(
+ listed_frequency_khz: float,
+ station_callsign: str = '',
+ frequency_reference: str = 'auto',
+) -> tuple[float, str, bool]:
+ """Resolve listed frequency to the actual USB dial frequency.
+
+ Args:
+ listed_frequency_khz: Frequency value provided by UI/API.
+ station_callsign: Station callsign used for metadata lookup.
+ frequency_reference: One of auto/carrier/dial.
+
+ Returns:
+ (tuned_frequency_khz, resolved_reference, offset_applied)
+ """
+ listed = float(listed_frequency_khz)
+ if listed <= 0:
+ raise ValueError('frequency_khz must be greater than zero')
+
+ requested_ref = _normalize_frequency_reference(frequency_reference)
+ resolved_ref = requested_ref
+
+ if requested_ref == 'auto':
+ station = get_station(station_callsign) if station_callsign else None
+ if station:
+ resolved_ref = _station_frequency_reference(station, listed)
+ else:
+ # For ad-hoc frequencies (no station metadata), treat input as dial.
+ resolved_ref = 'dial'
+
+ if resolved_ref == 'carrier':
+ tuned = round(listed - WEFAX_USB_ALIGNMENT_OFFSET_KHZ, 3)
+ if tuned <= 0:
+ raise ValueError('frequency_khz too low after USB alignment offset')
+ return tuned, resolved_ref, True
+
+ return listed, resolved_ref, False
def get_current_broadcasts(callsign: str) -> list[dict]: