Add multi-SDR support to WeFax decoder (HackRF, LimeSDR, Airspy, SDRPlay)

Replace hardcoded rtl_fm with SDRFactory abstraction layer so WeFax works
with any supported SDR hardware, matching the pattern used by APRS and
other modes. RTL-SDR direct sampling flag preserved for HF reception.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-25 16:45:07 +00:00
parent 2202e3ed98
commit f3158cbb69
4 changed files with 295 additions and 269 deletions
+76 -75
View File
@@ -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'));