From 05a275995e5fe6ecb78cae0aa7eefa7bb1775df8 Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:38:25 +0100 Subject: [PATCH 1/7] feat: add FT2 mode support to PSKReporter/WSJT-X filter panels - Add 'FT2' to MODES arrays in PSKFilterManager and ActivateFilterManager - Add FT2 to callsign.js MODES, detectMode comment detection, and DIGITAL_ISLANDS frequencies - Add FT2 to VOACAP heatmap mode selector --- src/components/ActivateFilterManager.jsx | 1 + src/components/PSKFilterManager.jsx | 1 + src/plugins/layers/useVOACAPHeatmap.js | 2 +- src/utils/callsign.js | 14 +++++++++++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/ActivateFilterManager.jsx b/src/components/ActivateFilterManager.jsx index 7d594d78..3b21fbfe 100644 --- a/src/components/ActivateFilterManager.jsx +++ b/src/components/ActivateFilterManager.jsx @@ -22,6 +22,7 @@ const MODES = [ 'FST4', 'FST4W', 'FST4W-90', + 'FT2', 'FT4', 'FT8', 'HELL', diff --git a/src/components/PSKFilterManager.jsx b/src/components/PSKFilterManager.jsx index 951a08e2..5a113f51 100644 --- a/src/components/PSKFilterManager.jsx +++ b/src/components/PSKFilterManager.jsx @@ -22,6 +22,7 @@ const MODES = [ 'FST4', 'FST4W', 'FST4W-90', + 'FT2', 'FT4', 'FT8', 'HELL', diff --git a/src/plugins/layers/useVOACAPHeatmap.js b/src/plugins/layers/useVOACAPHeatmap.js index 79802e46..cbdb85b2 100644 --- a/src/plugins/layers/useVOACAPHeatmap.js +++ b/src/plugins/layers/useVOACAPHeatmap.js @@ -213,7 +213,7 @@ export function useLayer({ map, enabled, opacity, locator }) { .map((g) => ``) .join(''); - const modeOptions = ['SSB', 'CW', 'FT8', 'FT4', 'RTTY', 'AM', 'FM'] + const modeOptions = ['SSB', 'CW', 'FT8', 'FT4', 'FT2', 'RTTY', 'AM', 'FM'] .map((m) => ``) .join(''); diff --git a/src/utils/callsign.js b/src/utils/callsign.js index 6c0f87e1..aca4208f 100644 --- a/src/utils/callsign.js +++ b/src/utils/callsign.js @@ -43,7 +43,7 @@ export const CONTINENTS = [ /** * Digital/Voice Modes */ -export const MODES = ['CW', 'SSB', 'FT8', 'FT4', 'RTTY', 'PSK', 'AM', 'FM']; +export const MODES = ['CW', 'SSB', 'FT8', 'FT4', 'FT2', 'RTTY', 'PSK', 'AM', 'FM']; /** * Get band from frequency (in kHz) @@ -93,6 +93,7 @@ export const detectMode = (comment, freq) => { const upper = comment.toUpperCase(); if (upper.includes('FT8')) return 'FT8'; if (upper.includes('FT4')) return 'FT4'; + if (upper.includes('FT2')) return 'FT2'; if (upper.includes('CW')) return 'CW'; if (upper.includes('SSB') || upper.includes('LSB') || upper.includes('USB')) return 'SSB'; if (upper.includes('RTTY')) return 'RTTY'; @@ -126,6 +127,17 @@ export const detectMode = (comment, freq) => { { mhz: 24.915, mode: 'FT8' }, { mhz: 28.074, mode: 'FT8' }, { mhz: 50.313, mode: 'FT8' }, + // FT2 calling frequencies + { mhz: 1.842, mode: 'FT2' }, + { mhz: 3.577, mode: 'FT2' }, + { mhz: 7.077, mode: 'FT2' }, + { mhz: 10.141, mode: 'FT2' }, + { mhz: 14.077, mode: 'FT2' }, + { mhz: 18.107, mode: 'FT2' }, + { mhz: 21.077, mode: 'FT2' }, + { mhz: 24.92, mode: 'FT2' }, + { mhz: 28.077, mode: 'FT2' }, + { mhz: 50.317, mode: 'FT2' }, // FT4 calling frequencies { mhz: 3.575, mode: 'FT4' }, { mhz: 7.0475, mode: 'FT4' }, From cae50f901eb1c85497f59e24fb5916172f5f9d79 Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:34:44 +0100 Subject: [PATCH 2/7] fix: correct FT2 calling frequencies in callsign and band health modules Use the official FT2 calling frequencies: 160m: 1.843, 80m: 3.578, 60m: 5.360, 40m: 7.052, 30m: 10.144, 20m: 14.084, 17m: 18.108, 15m: 21.144, 12m: 24.923, 10m: 28.184 --- src/hooks/useBandHealth.js | 11 +++++++++++ src/utils/callsign.js | 20 ++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/hooks/useBandHealth.js b/src/hooks/useBandHealth.js index 0d7cda77..d477350d 100644 --- a/src/hooks/useBandHealth.js +++ b/src/hooks/useBandHealth.js @@ -62,6 +62,17 @@ const ISLANDS = [ { mhz: 21.074, mode: 'FT8' }, { mhz: 24.915, mode: 'FT8' }, { mhz: 28.074, mode: 'FT8' }, + // FT2 + { mhz: 1.843, mode: 'FT2' }, + { mhz: 3.578, mode: 'FT2' }, + { mhz: 5.36, mode: 'FT2' }, + { mhz: 7.052, mode: 'FT2' }, + { mhz: 10.144, mode: 'FT2' }, + { mhz: 14.084, mode: 'FT2' }, + { mhz: 18.108, mode: 'FT2' }, + { mhz: 21.144, mode: 'FT2' }, + { mhz: 24.923, mode: 'FT2' }, + { mhz: 28.184, mode: 'FT2' }, // FT4 { mhz: 3.575, mode: 'FT4' }, { mhz: 7.0475, mode: 'FT4' }, diff --git a/src/utils/callsign.js b/src/utils/callsign.js index aca4208f..62fb5408 100644 --- a/src/utils/callsign.js +++ b/src/utils/callsign.js @@ -128,16 +128,16 @@ export const detectMode = (comment, freq) => { { mhz: 28.074, mode: 'FT8' }, { mhz: 50.313, mode: 'FT8' }, // FT2 calling frequencies - { mhz: 1.842, mode: 'FT2' }, - { mhz: 3.577, mode: 'FT2' }, - { mhz: 7.077, mode: 'FT2' }, - { mhz: 10.141, mode: 'FT2' }, - { mhz: 14.077, mode: 'FT2' }, - { mhz: 18.107, mode: 'FT2' }, - { mhz: 21.077, mode: 'FT2' }, - { mhz: 24.92, mode: 'FT2' }, - { mhz: 28.077, mode: 'FT2' }, - { mhz: 50.317, mode: 'FT2' }, + { mhz: 1.843, mode: 'FT2' }, + { mhz: 3.578, mode: 'FT2' }, + { mhz: 5.36, mode: 'FT2' }, + { mhz: 7.052, mode: 'FT2' }, + { mhz: 10.144, mode: 'FT2' }, + { mhz: 14.084, mode: 'FT2' }, + { mhz: 18.108, mode: 'FT2' }, + { mhz: 21.144, mode: 'FT2' }, + { mhz: 24.923, mode: 'FT2' }, + { mhz: 28.184, mode: 'FT2' }, // FT4 calling frequencies { mhz: 3.575, mode: 'FT4' }, { mhz: 7.0475, mode: 'FT4' }, From 741913c0f78375f5f129356044a380fd8a115ccb Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:41:19 +0100 Subject: [PATCH 3/7] feat: add FT2 to DX Cluster filter modes and rig band plan digital modes --- src/components/DXFilterManager.jsx | 2 +- src/utils/bandPlan.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/DXFilterManager.jsx b/src/components/DXFilterManager.jsx index 293c5e5a..70c6d651 100644 --- a/src/components/DXFilterManager.jsx +++ b/src/components/DXFilterManager.jsx @@ -45,7 +45,7 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose, onCl '2m', '70cm', ]; - const modes = ['CW', 'SSB', 'FT8', 'FT4', 'RTTY', 'PSK', 'JT65', 'JS8', 'SSTV', 'AM', 'FM']; + const modes = ['CW', 'SSB', 'FT8', 'FT4', 'FT2', 'RTTY', 'PSK', 'JT65', 'JS8', 'SSTV', 'AM', 'FM']; // noinspection DuplicatedCode const toggleArrayItem = (key, item) => { diff --git a/src/utils/bandPlan.js b/src/utils/bandPlan.js index 839df5ca..eccb1753 100644 --- a/src/utils/bandPlan.js +++ b/src/utils/bandPlan.js @@ -74,7 +74,7 @@ export const mapModeToRig = (mode, freq) => { if (m === 'DATA-USB' || m === 'DATA-LSB') return m; // Digital/data modes → DATA-USB or DATA-LSB based on band convention - const digitalModes = ['DATA', 'FT8', 'FT4', 'JS8', 'WSPR', 'JT65', 'JT9', 'PSK31', 'PSK63', 'RTTY', 'PKT']; + const digitalModes = ['DATA', 'FT8', 'FT4', 'FT2', 'JS8', 'WSPR', 'JT65', 'JT9', 'PSK31', 'PSK63', 'RTTY', 'PKT']; if (digitalModes.includes(m)) { return sb === 'USB' ? 'DATA-USB' : 'DATA-LSB'; } From 29e93c71c3aa7d3f619fb0896fc8b4aefed6c769 Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:53:55 +0100 Subject: [PATCH 4/7] move to asymmetric window for spot mode calc --- src/utils/callsign.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/callsign.js b/src/utils/callsign.js index 62fb5408..017d8ca3 100644 --- a/src/utils/callsign.js +++ b/src/utils/callsign.js @@ -114,7 +114,6 @@ export const detectMode = (comment, freq) => { // DX cluster spots may report slightly off-frequency depending on the spotter's // rig readout or the audio offset of the specific signal they clicked on. // 3 kHz was too tight — 24.911 for 12m FT8 (dial 24.915) was being missed. - const TOLERANCE = 0.005; const DIGITAL_ISLANDS = [ // FT8 calling frequencies { mhz: 1.84, mode: 'FT8' }, @@ -157,9 +156,8 @@ export const detectMode = (comment, freq) => { ]; for (const island of DIGITAL_ISLANDS) { - if (Math.abs(mhz - island.mhz) <= TOLERANCE) { - return island.mode; - } + const offset = mhz - island.mhz; + if (offset >= -0.0005 && offset <= 0.0031) return island.mode; } // Band plan segments — CW vs SSB by frequency range From 00428f4fd15487410ba88b20b3a443feb41a38ae Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:37:51 +0100 Subject: [PATCH 5/7] fix: add FT2=32 dB to MODE_ADVANTAGE_DB in both propagation engines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, FT2 heatmaps rendered identically to SSB (0 dB advantage). FT2 gets 32 dB — between FT4 (30) and FT8 (34). --- server/utils/propagationPhysics.js | 1 + src/utils/propagationAdjust.js | 1 + 2 files changed, 2 insertions(+) diff --git a/server/utils/propagationPhysics.js b/server/utils/propagationPhysics.js index e3bf9048..9e54ee26 100644 --- a/server/utils/propagationPhysics.js +++ b/server/utils/propagationPhysics.js @@ -59,6 +59,7 @@ const MODE_ADVANTAGE_DB = { PSK31: 10, FT8: 34, FT4: 30, + FT2: 32, WSPR: 41, JS8: 37, OLIVIA: 20, diff --git a/src/utils/propagationAdjust.js b/src/utils/propagationAdjust.js index 630fe752..6c871562 100644 --- a/src/utils/propagationAdjust.js +++ b/src/utils/propagationAdjust.js @@ -13,6 +13,7 @@ export const MODE_ADVANTAGE_DB = { PSK31: 10, FT8: 34, FT4: 30, + FT2: 32, WSPR: 41, JS8: 37, OLIVIA: 20, From b25b93531a2f822aa664475fb17c346dfa8a04cd Mon Sep 17 00:00:00 2001 From: Laura Batalha <5883822+lbatalha@users.noreply.github.com> Date: Tue, 28 Jul 2026 22:53:05 +0100 Subject: [PATCH 6/7] fix: add FT2 to balancePathsByMode regex in dxcluster.js Change /\bFT[84]\b/ to /\bFT[842]\b/ so FT2 spots are classified as digital skimmer traffic instead of voice/human spots. Without this, FT2 skimmer spots consumed the human-spot reserve (25% of the display window) instead of the digital skimmer bucket (50%). --- server/routes/dxcluster.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/dxcluster.js b/server/routes/dxcluster.js index 52c1d610..c8a19ab5 100644 --- a/server/routes/dxcluster.js +++ b/server/routes/dxcluster.js @@ -1044,7 +1044,7 @@ module.exports = function (app, ctx) { const other = []; for (const p of paths) { const c = String(p.comment || '').toUpperCase(); - if (/\bFT[84]\b/.test(c)) ft8ft4.push(p); + if (/\bFT[842]\b/.test(c)) ft8ft4.push(p); else if (/\b(CW|RTTY|PSK)/.test(c)) other.push(p); else voice.push(p); // voice keywords or no mode marker — human spots } From 301b44f6db4bb3c985ee102d0edcc0d451b36383 Mon Sep 17 00:00:00 2001 From: accius Date: Tue, 28 Jul 2026 19:25:46 -0400 Subject: [PATCH 7/7] fix(ft2): correct sensitivity value, retire stale comment, pin boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MODE_ADVANTAGE_DB: FT2 32 → 25 in both mirror engines. The "better than FT4" claim is about speed (3.75 s periods), not sensitivity — the ft2.it technical document specs FT2's decode threshold at -12/-13 dB vs FT4's -17.5 and FT8's -21, making it the least sensitive FTx mode. - Replace the stale "24.911 was being missed" comment that argued for the old ±5 kHz window: FTx audio passbands sit entirely above the dial, so that spot was busted, and the asymmetric window is correct. - Document the inherent 160m FT8/FT2 dial adjacency (1.843 = 1.840 + 3 kHz) at the island entry. - Add detectMode boundary tests: FT4/FT2 separation at 4 kHz spacing, below-dial rejection, 160m adjacency, comment precedence, kHz input. Co-Authored-By: Claude Fable 5 --- server/utils/propagationPhysics.js | 5 +++- src/utils/callsign.js | 20 ++++++++++---- src/utils/callsign.test.js | 43 ++++++++++++++++++++++++++++++ src/utils/propagationAdjust.js | 5 +++- 4 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 src/utils/callsign.test.js diff --git a/server/utils/propagationPhysics.js b/server/utils/propagationPhysics.js index 9e54ee26..4fad1f15 100644 --- a/server/utils/propagationPhysics.js +++ b/server/utils/propagationPhysics.js @@ -59,7 +59,10 @@ const MODE_ADVANTAGE_DB = { PSK31: 10, FT8: 34, FT4: 30, - FT2: 32, + // FT2 trades sensitivity for speed: decode threshold is -12/-13 dB vs + // FT4's -17.5 and FT8's -21 (ft2.it technical document) — the LEAST + // sensitive of the FTx modes despite being the fastest. + FT2: 25, WSPR: 41, JS8: 37, OLIVIA: 20, diff --git a/src/utils/callsign.js b/src/utils/callsign.js index 017d8ca3..77fe297d 100644 --- a/src/utils/callsign.js +++ b/src/utils/callsign.js @@ -110,10 +110,14 @@ export const detectMode = (comment, freq) => { // Normalize to MHz (spots may arrive in kHz or MHz) const mhz = f > 1000 ? f / 1000 : f; - // Digital islands — narrow ±5 kHz windows around known calling frequencies - // DX cluster spots may report slightly off-frequency depending on the spotter's - // rig readout or the audio offset of the specific signal they clicked on. - // 3 kHz was too tight — 24.911 for 12m FT8 (dial 24.915) was being missed. + // Digital islands — asymmetric windows around known calling (dial) frequencies. + // FTx signals occupy dial + 0–3 kHz (audio passband sits ABOVE the dial), so + // legitimate spots land in [dial, dial + ~3.1 kHz]; the small negative + // allowance covers rounded-down spot frequencies. A symmetric window cannot + // work here: FT2 dials sit only 4 kHz above FT4's, so ±5 kHz would swallow + // the neighbouring island. (An old note here defended ±5 kHz because a + // "24.911 FT8" spot was being missed — that was a busted spot; real FTx + // signals are never below the dial.) const DIGITAL_ISLANDS = [ // FT8 calling frequencies { mhz: 1.84, mode: 'FT8' }, @@ -126,7 +130,13 @@ export const detectMode = (comment, freq) => { { mhz: 24.915, mode: 'FT8' }, { mhz: 28.074, mode: 'FT8' }, { mhz: 50.313, mode: 'FT8' }, - // FT2 calling frequencies + // FT2 calling frequencies (tentative community QRGs — experimental mode, + // Decodium / WSJT-X Improved, not official WSJT-X) + // 160m: 1.843 sits exactly at the top of FT8 1.840's passband. FT8 is + // checked first, so a bare spot at exactly 1.8430 classifies as FT8 — + // inherent to the frequency plan. Real FT2 auto-spots carry "FT2" in the + // comment (matched before the islands) and audio offsets that push them + // past 1.8431, so only comment-less dial quotes hit the ambiguity. { mhz: 1.843, mode: 'FT2' }, { mhz: 3.578, mode: 'FT2' }, { mhz: 5.36, mode: 'FT2' }, diff --git a/src/utils/callsign.test.js b/src/utils/callsign.test.js new file mode 100644 index 00000000..d4a01c34 --- /dev/null +++ b/src/utils/callsign.test.js @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; + +import { detectMode } from './callsign.js'; + +// FTx signals occupy dial + 0–3 kHz (audio passband above the dial), so the +// island window is asymmetric: [dial − 0.5 kHz, dial + 3.1 kHz]. These tests +// pin the boundaries — especially FT4 vs FT2, whose dials are only 4 kHz +// apart, which is why a symmetric ±5 kHz window can never work. +describe('detectMode frequency islands (asymmetric window)', () => { + it('classifies spots in the audio passband above the dial', () => { + expect(detectMode(null, '14.074')).toBe('FT8'); // 20m FT8 dial itself + expect(detectMode(null, '14.077')).toBe('FT8'); // near top of passband + expect(detectMode(null, '14.081')).toBe('FT4'); // FT4 dial + 1 kHz + expect(detectMode(null, '14.085')).toBe('FT2'); // FT2 dial + 1 kHz + expect(detectMode(null, '24.916')).toBe('FT8'); // 12m FT8 dial + 1 kHz + expect(detectMode(null, '24.924')).toBe('FT2'); // 12m FT2 dial + 1 kHz + }); + + it('does not classify below-dial spots as FTx — signals are never below the dial', () => { + // 24.911 is 4 kHz below the 12m FT8 dial (24.915). The old ±5 kHz window + // matched it (see the retired comment in callsign.js), but such spots are + // busted: the FT8 audio passband sits entirely above the dial. + expect(detectMode(null, '24.911')).not.toBe('FT8'); + }); + + it('separates FT4 and FT2 islands only 4 kHz apart', () => { + expect(detectMode(null, '14.083')).toBe('FT4'); // FT4 dial + 3.0 kHz + expect(detectMode(null, '14.0835')).toBe('FT2'); // FT2 dial − 0.5 kHz (rounded-down allowance) + }); + + it('160m FT8/FT2 dial adjacency: FT8 wins bare dial quotes, comments win everything', () => { + // FT2's 1.843 dial sits exactly at the top of FT8 1.840's passband — the + // documented, inherent ambiguity. A bare spot at exactly the FT2 dial + // classifies FT8; anything above it is FT2; a comment overrides both. + expect(detectMode(null, '1.8430')).toBe('FT8'); + expect(detectMode(null, '1.8432')).toBe('FT2'); + expect(detectMode('FT2 -12dB from JN44 1489Hz', '1.843')).toBe('FT2'); + }); + + it('normalizes spots arriving in kHz', () => { + expect(detectMode(null, '14085')).toBe('FT2'); + }); +}); diff --git a/src/utils/propagationAdjust.js b/src/utils/propagationAdjust.js index 6c871562..28853799 100644 --- a/src/utils/propagationAdjust.js +++ b/src/utils/propagationAdjust.js @@ -13,7 +13,10 @@ export const MODE_ADVANTAGE_DB = { PSK31: 10, FT8: 34, FT4: 30, - FT2: 32, + // FT2 trades sensitivity for speed: decode threshold is -12/-13 dB vs + // FT4's -17.5 and FT8's -21 (ft2.it technical document) — the LEAST + // sensitive of the FTx modes despite being the fastest. + FT2: 25, WSPR: 41, JS8: 37, OLIVIA: 20,