mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 17:18:11 -07:00
Add CW/Morse code decoder mode
New signal mode for decoding Morse code (CW) transmissions via SDR. Includes route blueprint, utility decoder, frontend UI, and tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+33
-1
@@ -80,7 +80,8 @@
|
||||
subghz: "{{ url_for('static', filename='css/modes/subghz.css') }}?v={{ version }}&r=subghz_layout9",
|
||||
bt_locate: "{{ url_for('static', filename='css/modes/bt_locate.css') }}?v={{ version }}&r=btlocate4",
|
||||
spaceweather: "{{ url_for('static', filename='css/modes/space-weather.css') }}",
|
||||
wefax: "{{ url_for('static', filename='css/modes/wefax.css') }}"
|
||||
wefax: "{{ url_for('static', filename='css/modes/wefax.css') }}",
|
||||
morse: "{{ url_for('static', filename='css/modes/morse.css') }}"
|
||||
};
|
||||
window.INTERCEPT_MODE_STYLE_LOADED = {};
|
||||
window.INTERCEPT_MODE_STYLE_PROMISES = {};
|
||||
@@ -271,6 +272,10 @@
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h4l3-8 3 16 3-8h4"/><path d="M2 18h20" opacity="0.5"/><path d="M2 21h20" opacity="0.3"/></svg></span>
|
||||
<span class="mode-name">Waterfall</span>
|
||||
</button>
|
||||
<button class="mode-card mode-card-sm" onclick="selectMode('morse')">
|
||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="2" y1="12" x2="5" y2="12"/><line x1="7" y1="12" x2="13" y2="12"/><line x1="15" y1="12" x2="18" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/></svg></span>
|
||||
<span class="mode-name">Morse</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -675,6 +680,8 @@
|
||||
|
||||
{% include 'partials/modes/wefax.html' %}
|
||||
|
||||
{% include 'partials/modes/morse.html' %}
|
||||
|
||||
{% include 'partials/modes/space-weather.html' %}
|
||||
|
||||
{% include 'partials/modes/tscm.html' %}
|
||||
@@ -3001,6 +3008,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Morse Code Decoder Visuals -->
|
||||
<div id="morseVisuals" style="display: none; flex-direction: column; gap: 12px; padding: 16px; height: 100%;">
|
||||
<div class="morse-toolbar">
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.exportTxt()">Export TXT</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.exportCsv()">Export CSV</button>
|
||||
<button class="btn btn-sm btn-outline" id="morseCopyBtn" onclick="MorseMode.copyToClipboard()">Copy</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.clearText()">Clear</button>
|
||||
</div>
|
||||
<div class="morse-scope-container">
|
||||
<canvas id="morseScopeCanvas"></canvas>
|
||||
</div>
|
||||
<div id="morseDecodedText" class="morse-decoded-panel"></div>
|
||||
<div class="morse-status-bar">
|
||||
<span class="status-item" id="morseStatusBarWpm">15 WPM</span>
|
||||
<span class="status-item" id="morseStatusBarTone">700 Hz</span>
|
||||
<span class="status-item" id="morseStatusBarChars">0 chars decoded</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Intelligence Dashboard (above waterfall for prominence) -->
|
||||
<div class="recon-panel collapsed" id="reconPanel">
|
||||
<div class="recon-header" onclick="toggleReconCollapse()" style="cursor: pointer;">
|
||||
@@ -3125,6 +3151,7 @@
|
||||
<script src="{{ url_for('static', filename='js/modes/subghz.js') }}?v={{ version }}&r=subghz_layout9"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/bt_locate.js') }}?v={{ version }}&r=btlocate4"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/wefax.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/morse.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/modes/space-weather.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix2"></script>
|
||||
<script src="{{ url_for('static', filename='js/core/keyboard-shortcuts.js') }}"></script>
|
||||
@@ -3278,6 +3305,7 @@
|
||||
spystations: { label: 'Spy Stations', indicator: 'SPY STATIONS', outputTitle: 'Spy Stations', group: 'intel' },
|
||||
websdr: { label: 'WebSDR', indicator: 'WEBSDR', outputTitle: 'HF/Shortwave WebSDR', group: 'intel' },
|
||||
waterfall: { label: 'Waterfall', indicator: 'WATERFALL', outputTitle: 'Spectrum Waterfall', group: 'signals' },
|
||||
morse: { label: 'Morse', indicator: 'MORSE', outputTitle: 'CW/Morse Decoder', group: 'signals' },
|
||||
};
|
||||
const validModes = new Set(Object.keys(modeCatalog));
|
||||
window.interceptModeCatalog = Object.assign({}, modeCatalog);
|
||||
@@ -4077,6 +4105,7 @@
|
||||
const wefaxVisuals = document.getElementById('wefaxVisuals');
|
||||
const spaceWeatherVisuals = document.getElementById('spaceWeatherVisuals');
|
||||
const waterfallVisuals = document.getElementById('waterfallVisuals');
|
||||
const morseVisuals = document.getElementById('morseVisuals');
|
||||
if (wifiLayoutContainer) wifiLayoutContainer.style.display = mode === 'wifi' ? 'flex' : 'none';
|
||||
if (btLayoutContainer) btLayoutContainer.style.display = mode === 'bluetooth' ? 'flex' : 'none';
|
||||
if (satelliteVisuals) satelliteVisuals.style.display = mode === 'satellite' ? 'block' : 'none';
|
||||
@@ -4094,6 +4123,7 @@
|
||||
if (wefaxVisuals) wefaxVisuals.style.display = mode === 'wefax' ? 'flex' : 'none';
|
||||
if (spaceWeatherVisuals) spaceWeatherVisuals.style.display = mode === 'spaceweather' ? 'flex' : 'none';
|
||||
if (waterfallVisuals) waterfallVisuals.style.display = mode === 'waterfall' ? 'flex' : 'none';
|
||||
if (morseVisuals) morseVisuals.style.display = mode === 'morse' ? 'flex' : 'none';
|
||||
|
||||
// Prevent Leaflet heatmap redraws on hidden BT Locate map containers.
|
||||
if (typeof BtLocate !== 'undefined' && BtLocate.setActiveMode) {
|
||||
@@ -4252,6 +4282,8 @@
|
||||
SpaceWeather.init();
|
||||
} else if (mode === 'waterfall') {
|
||||
if (typeof Waterfall !== 'undefined') Waterfall.init();
|
||||
} else if (mode === 'morse') {
|
||||
MorseMode.init();
|
||||
}
|
||||
|
||||
// Destroy Waterfall WebSocket when leaving SDR receiver modes
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<!-- MORSE CODE MODE -->
|
||||
<div id="morseMode" class="mode-content">
|
||||
<div class="section">
|
||||
<h3>CW/Morse Decoder</h3>
|
||||
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-bottom: 12px;">
|
||||
Decode CW (continuous wave) Morse code from amateur radio HF bands using USB demodulation
|
||||
and Goertzel tone detection.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Frequency</h3>
|
||||
<div class="form-group">
|
||||
<label>Frequency (MHz)</label>
|
||||
<input type="number" id="morseFrequency" value="14.060" step="0.001" min="1" max="30">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Band Presets</label>
|
||||
<div class="morse-presets" style="display: flex; flex-wrap: wrap; gap: 4px;">
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.setFreq(3.560)">80m</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.setFreq(7.030)">40m</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.setFreq(10.116)">30m</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.setFreq(14.060)">20m</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.setFreq(18.080)">17m</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.setFreq(21.060)">15m</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.setFreq(24.910)">12m</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="MorseMode.setFreq(28.060)">10m</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Settings</h3>
|
||||
<div class="form-group">
|
||||
<label>Gain (dB)</label>
|
||||
<input type="number" id="morseGain" value="40" step="1" min="0" max="50">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>PPM Correction</label>
|
||||
<input type="number" id="morsePPM" value="0" step="1" min="-100" max="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>SDR Type</label>
|
||||
<select id="morseSdrType">
|
||||
<option value="rtlsdr" selected>RTL-SDR</option>
|
||||
<option value="hackrf">HackRF</option>
|
||||
<option value="limesdr">LimeSDR</option>
|
||||
<option value="airspy">Airspy</option>
|
||||
<option value="sdrplay">SDRPlay</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Device Index</label>
|
||||
<input type="number" id="morseDevice" value="0" step="1" min="0" max="9">
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="checkbox" id="morseBiasT">
|
||||
<label for="morseBiasT" style="margin: 0; cursor: pointer;">Bias-T Power</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>CW Settings</h3>
|
||||
<div class="form-group">
|
||||
<label>Tone Frequency: <span id="morseToneFreqLabel">700</span> Hz</label>
|
||||
<input type="range" id="morseToneFreq" value="700" min="300" max="1200" step="10"
|
||||
oninput="document.getElementById('morseToneFreqLabel').textContent = this.value">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Speed: <span id="morseWpmLabel">15</span> WPM</label>
|
||||
<input type="range" id="morseWpm" value="15" min="5" max="50" step="1"
|
||||
oninput="document.getElementById('morseWpmLabel').textContent = this.value">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Morse Reference -->
|
||||
<div class="section">
|
||||
<h3 style="cursor: pointer;" onclick="this.parentElement.querySelector('.morse-ref-grid').classList.toggle('collapsed')">
|
||||
Morse Reference <span style="font-size: 10px; color: var(--text-dim);">(click to toggle)</span>
|
||||
</h3>
|
||||
<div class="morse-ref-grid collapsed" style="font-family: var(--font-mono); font-size: 10px; line-height: 1.8; columns: 2; column-gap: 12px; color: var(--text-dim);">
|
||||
<div>A .-</div><div>B -...</div><div>C -.-.</div><div>D -..</div>
|
||||
<div>E .</div><div>F ..-.</div><div>G --.</div><div>H ....</div>
|
||||
<div>I ..</div><div>J .---</div><div>K -.-</div><div>L .-..</div>
|
||||
<div>M --</div><div>N -.</div><div>O ---</div><div>P .--.</div>
|
||||
<div>Q --.-</div><div>R .-.</div><div>S ...</div><div>T -</div>
|
||||
<div>U ..-</div><div>V ...-</div><div>W .--</div><div>X -..-</div>
|
||||
<div>Y -.--</div><div>Z --..</div>
|
||||
<div style="margin-top: 4px; border-top: 1px solid var(--border-color); padding-top: 4px;">0 -----</div>
|
||||
<div style="margin-top: 4px; border-top: 1px solid var(--border-color); padding-top: 4px;">1 .----</div>
|
||||
<div>2 ..---</div><div>3 ...--</div><div>4 ....-</div>
|
||||
<div>5 .....</div><div>6 -....</div><div>7 --...</div>
|
||||
<div>8 ---..</div><div>9 ----.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="section">
|
||||
<div class="morse-status" style="display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-dim);">
|
||||
<span id="morseStatusIndicator" class="status-dot" style="width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim);"></span>
|
||||
<span id="morseStatusText">Standby</span>
|
||||
<span style="margin-left: auto;" id="morseCharCount">0 chars</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="section">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="btn btn-primary" id="morseStartBtn" onclick="MorseMode.start()" style="flex: 1;">
|
||||
Start Decoder
|
||||
</button>
|
||||
<button class="btn btn-danger" id="morseStopBtn" onclick="MorseMode.stop()" style="flex: 1; display: none;">
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HF Antenna Note -->
|
||||
<div class="section">
|
||||
<p class="info-text" style="font-size: 11px; color: #ffaa00; line-height: 1.5;">
|
||||
CW operates on HF bands (1-30 MHz). Requires an HF-capable SDR with direct sampling
|
||||
or an upconverter, plus an appropriate HF antenna (dipole, end-fed, or random wire).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,6 +67,7 @@
|
||||
{{ mode_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||
{{ mode_item('subghz', 'SubGHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h6l3-9 3 18 3-9h5"/></svg>') }}
|
||||
{{ mode_item('waterfall', 'Waterfall', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h4l3-8 3 16 3-8h4"/><path d="M2 18h20" opacity="0.4"/><path d="M2 21h20" opacity="0.2"/></svg>') }}
|
||||
{{ mode_item('morse', 'Morse', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="2" y1="12" x2="5" y2="12"/><line x1="7" y1="12" x2="13" y2="12"/><line x1="15" y1="12" x2="18" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/></svg>') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -201,6 +202,7 @@
|
||||
{{ mobile_item('sensor', '433MHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg>') }}
|
||||
{{ mobile_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||
{{ mobile_item('subghz', 'SubGHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h6l3-9 3 18 3-9h5"/></svg>') }}
|
||||
{{ mobile_item('morse', 'Morse', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="2" y1="12" x2="5" y2="12"/><line x1="7" y1="12" x2="13" y2="12"/><line x1="15" y1="12" x2="18" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/></svg>') }}
|
||||
{# Tracking #}
|
||||
{{ mobile_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
||||
{{ mobile_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
||||
|
||||
Reference in New Issue
Block a user