Add graphical signal meter for APRS decoding

Backend changes (routes/aprs.py):
- Remove -q h flag from direwolf to enable audio level output
- Add parse_audio_level() to extract levels from direwolf output
- Add rate-limiting (max 10 updates/sec, min 2-level change)
- Push meter events to SSE queue as type='meter'

Frontend changes:
- Add signal meter widget to APRS sidebar
- Horizontal bar gauge with gradient (green->cyan->yellow->red)
- Numeric level display (0-100)
- "BURST" indicator for levels >70
- Status text (weak/moderate/strong signal)
- "No RF activity" state after 5 seconds of silence
- CSS styles in static/css/modes/aprs.css

Also added UK region to dropdown (same freq as Europe: 144.800)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-15 22:03:08 +00:00
parent f4282cb608
commit cd168da760
4 changed files with 273 additions and 5 deletions
+99 -1
View File
@@ -5949,6 +5949,9 @@
let isAprsRunning = false;
let aprsPacketCount = 0;
let aprsStationCount = 0;
let aprsMeterLastUpdate = 0;
let aprsMeterCheckInterval = null;
const APRS_METER_TIMEOUT = 5000; // 5 seconds for "no signal" state
function checkAprsTools() {
fetch('/aprs/tools')
@@ -6052,6 +6055,10 @@
updateAprsStatus('listening', data.frequency);
document.getElementById('aprsStatusStations').textContent = '0';
document.getElementById('aprsStatusPackets').textContent = '0';
// Show signal meter
document.getElementById('aprsSignalMeter').style.display = 'block';
resetAprsMeter();
startAprsMeterCheck();
startAprsStream();
} else {
alert('APRS Error: ' + data.message);
@@ -6071,10 +6078,13 @@
isAprsRunning = false;
document.getElementById('startAprsBtn').style.display = 'block';
document.getElementById('stopAprsBtn').style.display = 'none';
// Hide sidebar status bar
// Hide sidebar status bar and signal meter
document.getElementById('aprsStatusBar').style.display = 'none';
document.getElementById('aprsSignalMeter').style.display = 'none';
document.getElementById('aprsMapStatus').textContent = 'STANDBY';
document.getElementById('aprsMapStatus').style.color = '';
// Stop meter check interval
stopAprsMeterCheck();
if (aprsEventSource) {
aprsEventSource.close();
aprsEventSource = null;
@@ -6099,6 +6109,9 @@
updateAprsStatus('tracking');
}
processAprsPacket(data);
} else if (data.type === 'meter') {
// Update signal meter
updateAprsMeter(data.level);
}
};
@@ -6108,6 +6121,91 @@
};
}
// Signal Meter Functions
function resetAprsMeter() {
aprsMeterLastUpdate = 0;
const bar = document.getElementById('aprsMeterBar');
const value = document.getElementById('aprsMeterValue');
const burst = document.getElementById('aprsMeterBurst');
const status = document.getElementById('aprsMeterStatus');
if (bar) {
bar.style.width = '0%';
bar.classList.remove('no-signal');
}
if (value) value.textContent = '--';
if (burst) burst.style.display = 'none';
if (status) {
status.textContent = 'Waiting for signal...';
status.className = 'aprs-meter-status';
}
}
function updateAprsMeter(level) {
aprsMeterLastUpdate = Date.now();
const bar = document.getElementById('aprsMeterBar');
const value = document.getElementById('aprsMeterValue');
const burst = document.getElementById('aprsMeterBurst');
const status = document.getElementById('aprsMeterStatus');
if (bar) {
bar.style.width = level + '%';
bar.classList.remove('no-signal');
}
if (value) value.textContent = level;
// Show burst indicator for high levels (>70)
if (burst) {
if (level > 70) {
burst.style.display = 'inline';
// Remove and re-add to trigger animation
burst.style.animation = 'none';
burst.offsetHeight; // Trigger reflow
burst.style.animation = null;
} else {
burst.style.display = 'none';
}
}
// Update status text
if (status) {
status.className = 'aprs-meter-status active';
if (level < 10) {
status.textContent = 'Low signal / noise floor';
} else if (level < 30) {
status.textContent = 'Weak signal detected';
} else if (level < 60) {
status.textContent = 'Moderate signal';
} else {
status.textContent = 'Strong signal / packet burst';
}
}
}
function startAprsMeterCheck() {
// Check for no-signal state every second
aprsMeterCheckInterval = setInterval(function() {
if (aprsMeterLastUpdate > 0 && (Date.now() - aprsMeterLastUpdate) > APRS_METER_TIMEOUT) {
// No meter updates for 5 seconds - show no-signal state
const bar = document.getElementById('aprsMeterBar');
const status = document.getElementById('aprsMeterStatus');
const burst = document.getElementById('aprsMeterBurst');
if (bar) bar.classList.add('no-signal');
if (burst) burst.style.display = 'none';
if (status) {
status.textContent = 'No RF activity / silence';
status.className = 'aprs-meter-status no-signal';
}
}
}, 1000);
}
function stopAprsMeterCheck() {
if (aprsMeterCheckInterval) {
clearInterval(aprsMeterCheckInterval);
aprsMeterCheckInterval = null;
}
}
function processAprsPacket(packet) {
// Update packet log
const logEl = document.getElementById('aprsPacketLog');
+20
View File
@@ -17,6 +17,7 @@
<select id="aprsRegion">
<option value="north_america">North America (144.390)</option>
<option value="europe">Europe (144.800)</option>
<option value="uk">UK (144.800)</option>
<option value="australia">Australia (145.175)</option>
<option value="japan">Japan (144.640)</option>
</select>
@@ -48,4 +49,23 @@
<span class="aprs-stat"><span class="aprs-stat-label">PACKETS:</span> <span id="aprsStatusPackets">0</span></span>
</div>
</div>
<!-- Signal Meter -->
<div id="aprsSignalMeter" class="aprs-signal-meter" style="display: none;">
<div class="aprs-meter-header">
<span class="aprs-meter-label">SIGNAL</span>
<span class="aprs-meter-value" id="aprsMeterValue">--</span>
<span class="aprs-meter-burst" id="aprsMeterBurst" style="display: none;">BURST</span>
</div>
<div class="aprs-meter-bar-container">
<div class="aprs-meter-bar" id="aprsMeterBar"></div>
<div class="aprs-meter-ticks">
<span>0</span>
<span>25</span>
<span>50</span>
<span>75</span>
<span>100</span>
</div>
</div>
<div class="aprs-meter-status" id="aprsMeterStatus">Waiting for signal...</div>
</div>
</div>