mirror of
https://github.com/smittix/intercept.git
synced 2026-04-27 08:10:00 -07:00
Merge pull request #175 from thatsatechnique/fix/pager-display-classification
fix: improve pager message display and mute visibility
This commit is contained in:
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Force LF line endings for files that must run on Linux (Docker)
|
||||||
|
*.sh text eol=lf
|
||||||
|
Dockerfile text eol=lf
|
||||||
@@ -256,6 +256,9 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
# Copy application code
|
# Copy application code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Strip Windows CRLF from shell scripts (git autocrlf can re-introduce them)
|
||||||
|
RUN find . -name '*.sh' -exec sed -i 's/\r$//' {} +
|
||||||
|
|
||||||
# Create data directory for persistence
|
# Create data directory for persistence
|
||||||
RUN mkdir -p /app/data /app/data/weather_sat /app/data/radiosonde/logs
|
RUN mkdir -p /app/data /app/data/weather_sat /app/data/radiosonde/logs
|
||||||
|
|
||||||
|
|||||||
@@ -1,139 +1,139 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# INTERCEPT - Multi-architecture Docker image builder
|
# INTERCEPT - Multi-architecture Docker image builder
|
||||||
#
|
#
|
||||||
# Builds for both linux/amd64 and linux/arm64 using Docker buildx.
|
# Builds for both linux/amd64 and linux/arm64 using Docker buildx.
|
||||||
# Run this on your x64 machine to cross-compile the arm64 image
|
# Run this on your x64 machine to cross-compile the arm64 image
|
||||||
# instead of building natively on the RPi5.
|
# instead of building natively on the RPi5.
|
||||||
#
|
#
|
||||||
# Prerequisites (one-time setup):
|
# Prerequisites (one-time setup):
|
||||||
# docker run --privileged --rm tonistiigi/binfmt --install all
|
# docker run --privileged --rm tonistiigi/binfmt --install all
|
||||||
# docker buildx create --name intercept-builder --use --bootstrap
|
# docker buildx create --name intercept-builder --use --bootstrap
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./build-multiarch.sh # Build both platforms, load locally
|
# ./build-multiarch.sh # Build both platforms, load locally
|
||||||
# ./build-multiarch.sh --push # Build and push to registry
|
# ./build-multiarch.sh --push # Build and push to registry
|
||||||
# ./build-multiarch.sh --arm64-only # Build arm64 only (for RPi)
|
# ./build-multiarch.sh --arm64-only # Build arm64 only (for RPi)
|
||||||
# REGISTRY=ghcr.io/user ./build-multiarch.sh --push
|
# REGISTRY=ghcr.io/user ./build-multiarch.sh --push
|
||||||
#
|
#
|
||||||
# Environment variables:
|
# Environment variables:
|
||||||
# REGISTRY - Container registry (default: docker.io/library)
|
# REGISTRY - Container registry (default: docker.io/library)
|
||||||
# IMAGE_NAME - Image name (default: intercept)
|
# IMAGE_NAME - Image name (default: intercept)
|
||||||
# IMAGE_TAG - Image tag (default: latest)
|
# IMAGE_TAG - Image tag (default: latest)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="${IMAGE_NAME:-intercept}"
|
IMAGE_NAME="${IMAGE_NAME:-intercept}"
|
||||||
IMAGE_TAG="${IMAGE_TAG:-latest}"
|
IMAGE_TAG="${IMAGE_TAG:-latest}"
|
||||||
BUILDER_NAME="intercept-builder"
|
BUILDER_NAME="intercept-builder"
|
||||||
PLATFORMS="linux/amd64,linux/arm64"
|
PLATFORMS="linux/amd64,linux/arm64"
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
PUSH=false
|
PUSH=false
|
||||||
LOAD=false
|
LOAD=false
|
||||||
ARM64_ONLY=false
|
ARM64_ONLY=false
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case $arg in
|
case $arg in
|
||||||
--push) PUSH=true ;;
|
--push) PUSH=true ;;
|
||||||
--load) LOAD=true ;;
|
--load) LOAD=true ;;
|
||||||
--arm64-only)
|
--arm64-only)
|
||||||
ARM64_ONLY=true
|
ARM64_ONLY=true
|
||||||
PLATFORMS="linux/arm64"
|
PLATFORMS="linux/arm64"
|
||||||
;;
|
;;
|
||||||
--amd64-only)
|
--amd64-only)
|
||||||
PLATFORMS="linux/amd64"
|
PLATFORMS="linux/amd64"
|
||||||
;;
|
;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
echo "Usage: $0 [--push] [--load] [--arm64-only] [--amd64-only]"
|
echo "Usage: $0 [--push] [--load] [--arm64-only] [--amd64-only]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --push Push to container registry"
|
echo " --push Push to container registry"
|
||||||
echo " --load Load into local Docker (single platform only)"
|
echo " --load Load into local Docker (single platform only)"
|
||||||
echo " --arm64-only Build arm64 only (for RPi5 deployment)"
|
echo " --arm64-only Build arm64 only (for RPi5 deployment)"
|
||||||
echo " --amd64-only Build amd64 only"
|
echo " --amd64-only Build amd64 only"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Environment variables:"
|
echo "Environment variables:"
|
||||||
echo " REGISTRY Container registry (e.g. ghcr.io/username)"
|
echo " REGISTRY Container registry (e.g. ghcr.io/username)"
|
||||||
echo " IMAGE_NAME Image name (default: intercept)"
|
echo " IMAGE_NAME Image name (default: intercept)"
|
||||||
echo " IMAGE_TAG Image tag (default: latest)"
|
echo " IMAGE_TAG Image tag (default: latest)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 --push # Build both, push"
|
echo " $0 --push # Build both, push"
|
||||||
echo " REGISTRY=ghcr.io/myuser $0 --push # Push to GHCR"
|
echo " REGISTRY=ghcr.io/myuser $0 --push # Push to GHCR"
|
||||||
echo " $0 --arm64-only --load # Build arm64, load locally"
|
echo " $0 --arm64-only --load # Build arm64, load locally"
|
||||||
echo " $0 --arm64-only --push && ssh rpi docker pull # Build + deploy to RPi"
|
echo " $0 --arm64-only --push && ssh rpi docker pull # Build + deploy to RPi"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $arg"
|
echo "Unknown option: $arg"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Build full image reference
|
# Build full image reference
|
||||||
if [ -n "$REGISTRY" ]; then
|
if [ -n "$REGISTRY" ]; then
|
||||||
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
||||||
else
|
else
|
||||||
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
|
FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " INTERCEPT Multi-Architecture Builder"
|
echo " INTERCEPT Multi-Architecture Builder"
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " Image: ${FULL_IMAGE}"
|
echo " Image: ${FULL_IMAGE}"
|
||||||
echo " Platforms: ${PLATFORMS}"
|
echo " Platforms: ${PLATFORMS}"
|
||||||
echo " Push: ${PUSH}"
|
echo " Push: ${PUSH}"
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check if buildx builder exists, create if not
|
# Check if buildx builder exists, create if not
|
||||||
if ! docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
|
if ! docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
|
||||||
echo "Creating buildx builder: ${BUILDER_NAME}"
|
echo "Creating buildx builder: ${BUILDER_NAME}"
|
||||||
docker buildx create --name "$BUILDER_NAME" --use --bootstrap
|
docker buildx create --name "$BUILDER_NAME" --use --bootstrap
|
||||||
|
|
||||||
# Check for QEMU support
|
# Check for QEMU support
|
||||||
if ! docker run --rm --privileged tonistiigi/binfmt --install all >/dev/null 2>&1; then
|
if ! docker run --rm --privileged tonistiigi/binfmt --install all >/dev/null 2>&1; then
|
||||||
echo "WARNING: QEMU binfmt setup may have failed."
|
echo "WARNING: QEMU binfmt setup may have failed."
|
||||||
echo "Run: docker run --privileged --rm tonistiigi/binfmt --install all"
|
echo "Run: docker run --privileged --rm tonistiigi/binfmt --install all"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
docker buildx use "$BUILDER_NAME"
|
docker buildx use "$BUILDER_NAME"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build command
|
# Build command
|
||||||
BUILD_CMD="docker buildx build --platform ${PLATFORMS} --tag ${FULL_IMAGE}"
|
BUILD_CMD="docker buildx build --platform ${PLATFORMS} --tag ${FULL_IMAGE}"
|
||||||
|
|
||||||
if [ "$PUSH" = true ]; then
|
if [ "$PUSH" = true ]; then
|
||||||
BUILD_CMD="${BUILD_CMD} --push"
|
BUILD_CMD="${BUILD_CMD} --push"
|
||||||
echo "Will push to: ${FULL_IMAGE}"
|
echo "Will push to: ${FULL_IMAGE}"
|
||||||
elif [ "$LOAD" = true ]; then
|
elif [ "$LOAD" = true ]; then
|
||||||
# --load only works with single platform
|
# --load only works with single platform
|
||||||
if echo "$PLATFORMS" | grep -q ","; then
|
if echo "$PLATFORMS" | grep -q ","; then
|
||||||
echo "ERROR: --load only works with a single platform."
|
echo "ERROR: --load only works with a single platform."
|
||||||
echo "Use --arm64-only or --amd64-only with --load."
|
echo "Use --arm64-only or --amd64-only with --load."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
BUILD_CMD="${BUILD_CMD} --load"
|
BUILD_CMD="${BUILD_CMD} --load"
|
||||||
echo "Will load into local Docker"
|
echo "Will load into local Docker"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Building..."
|
echo "Building..."
|
||||||
echo "Command: ${BUILD_CMD} ."
|
echo "Command: ${BUILD_CMD} ."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
$BUILD_CMD .
|
$BUILD_CMD .
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " Build complete!"
|
echo " Build complete!"
|
||||||
if [ "$PUSH" = true ]; then
|
if [ "$PUSH" = true ]; then
|
||||||
echo " Image pushed to: ${FULL_IMAGE}"
|
echo " Image pushed to: ${FULL_IMAGE}"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Pull on RPi5:"
|
echo " Pull on RPi5:"
|
||||||
echo " docker pull ${FULL_IMAGE}"
|
echo " docker pull ${FULL_IMAGE}"
|
||||||
fi
|
fi
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Download sample NOAA APT recordings for testing the weather satellite
|
# Download sample NOAA APT recordings for testing the weather satellite
|
||||||
# test-decode feature. These are FM-demodulated audio WAV files.
|
# test-decode feature. These are FM-demodulated audio WAV files.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./download-weather-sat-samples.sh
|
# ./download-weather-sat-samples.sh
|
||||||
# docker exec intercept /app/download-weather-sat-samples.sh
|
# docker exec intercept /app/download-weather-sat-samples.sh
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SAMPLE_DIR="$(dirname "$0")/data/weather_sat/samples"
|
SAMPLE_DIR="$(dirname "$0")/data/weather_sat/samples"
|
||||||
mkdir -p "$SAMPLE_DIR"
|
mkdir -p "$SAMPLE_DIR"
|
||||||
|
|
||||||
echo "Downloading NOAA APT sample files to $SAMPLE_DIR ..."
|
echo "Downloading NOAA APT sample files to $SAMPLE_DIR ..."
|
||||||
|
|
||||||
# Full satellite pass recorded over Argentina (NOAA, 11025 Hz mono WAV)
|
# Full satellite pass recorded over Argentina (NOAA, 11025 Hz mono WAV)
|
||||||
# Source: https://github.com/martinber/noaa-apt
|
# Source: https://github.com/martinber/noaa-apt
|
||||||
if [ ! -f "$SAMPLE_DIR/noaa_apt_argentina.wav" ]; then
|
if [ ! -f "$SAMPLE_DIR/noaa_apt_argentina.wav" ]; then
|
||||||
echo " -> noaa_apt_argentina.wav (18 MB) ..."
|
echo " -> noaa_apt_argentina.wav (18 MB) ..."
|
||||||
curl -fSL -o "$SAMPLE_DIR/noaa_apt_argentina.wav" \
|
curl -fSL -o "$SAMPLE_DIR/noaa_apt_argentina.wav" \
|
||||||
"https://noaa-apt.mbernardi.com.ar/examples/argentina.wav"
|
"https://noaa-apt.mbernardi.com.ar/examples/argentina.wav"
|
||||||
else
|
else
|
||||||
echo " -> noaa_apt_argentina.wav (already exists)"
|
echo " -> noaa_apt_argentina.wav (already exists)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Done. Test decode with:"
|
echo "Done. Test decode with:"
|
||||||
echo " Satellite: NOAA-18"
|
echo " Satellite: NOAA-18"
|
||||||
echo " File path: data/weather_sat/samples/noaa_apt_argentina.wav"
|
echo " File path: data/weather_sat/samples/noaa_apt_argentina.wav"
|
||||||
echo " Sample rate: 11025 Hz"
|
echo " Sample rate: 11025 Hz"
|
||||||
|
|||||||
@@ -55,6 +55,20 @@ def parse_multimon_output(line: str) -> dict[str, str] | None:
|
|||||||
'message': pocsag_match.group(5).strip() or '[No Message]'
|
'message': pocsag_match.group(5).strip() or '[No Message]'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# POCSAG parsing - other content types (catch-all for non-Alpha/Numeric labels)
|
||||||
|
pocsag_other_match = re.match(
|
||||||
|
r'(POCSAG\d+):\s*Address:\s*(\d+)\s+Function:\s*(\d+)\s+(\w+):\s*(.*)',
|
||||||
|
line
|
||||||
|
)
|
||||||
|
if pocsag_other_match:
|
||||||
|
return {
|
||||||
|
'protocol': pocsag_other_match.group(1),
|
||||||
|
'address': pocsag_other_match.group(2),
|
||||||
|
'function': pocsag_other_match.group(3),
|
||||||
|
'msg_type': pocsag_other_match.group(4),
|
||||||
|
'message': pocsag_other_match.group(5).strip() or '[No Message]'
|
||||||
|
}
|
||||||
|
|
||||||
# POCSAG parsing - address only (no message content)
|
# POCSAG parsing - address only (no message content)
|
||||||
pocsag_addr_match = re.match(
|
pocsag_addr_match = re.match(
|
||||||
r'(POCSAG\d+):\s*Address:\s*(\d+)\s+Function:\s*(\d+)\s*$',
|
r'(POCSAG\d+):\s*Address:\s*(\d+)\s+Function:\s*(\d+)\s*$',
|
||||||
|
|||||||
@@ -1492,6 +1492,7 @@ const SignalCards = (function() {
|
|||||||
muted.push(address);
|
muted.push(address);
|
||||||
localStorage.setItem('mutedAddresses', JSON.stringify(muted));
|
localStorage.setItem('mutedAddresses', JSON.stringify(muted));
|
||||||
showToast(`Source ${address} hidden from view`);
|
showToast(`Source ${address} hidden from view`);
|
||||||
|
updateMutedIndicator();
|
||||||
|
|
||||||
// Hide existing cards with this address
|
// Hide existing cards with this address
|
||||||
document.querySelectorAll(`.signal-card[data-address="${address}"], .signal-card[data-callsign="${address}"], .signal-card[data-sensor-id="${address}"]`).forEach(card => {
|
document.querySelectorAll(`.signal-card[data-address="${address}"], .signal-card[data-callsign="${address}"], .signal-card[data-sensor-id="${address}"]`).forEach(card => {
|
||||||
@@ -1510,6 +1511,30 @@ const SignalCards = (function() {
|
|||||||
return muted.includes(address);
|
return muted.includes(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmute all addresses and refresh display
|
||||||
|
*/
|
||||||
|
function unmuteAll() {
|
||||||
|
localStorage.setItem('mutedAddresses', '[]');
|
||||||
|
updateMutedIndicator();
|
||||||
|
showToast('All sources unmuted');
|
||||||
|
// Reload to re-display previously muted messages
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the muted address count indicator in the sidebar
|
||||||
|
*/
|
||||||
|
function updateMutedIndicator() {
|
||||||
|
const muted = JSON.parse(localStorage.getItem('mutedAddresses') || '[]');
|
||||||
|
const info = document.getElementById('mutedAddressInfo');
|
||||||
|
const count = document.getElementById('mutedAddressCount');
|
||||||
|
if (info && count) {
|
||||||
|
count.textContent = muted.length;
|
||||||
|
info.style.display = muted.length > 0 ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show location on map (for APRS)
|
* Show location on map (for APRS)
|
||||||
*/
|
*/
|
||||||
@@ -2262,6 +2287,8 @@ const SignalCards = (function() {
|
|||||||
copyMessage,
|
copyMessage,
|
||||||
muteAddress,
|
muteAddress,
|
||||||
isAddressMuted,
|
isAddressMuted,
|
||||||
|
unmuteAll,
|
||||||
|
updateMutedIndicator,
|
||||||
showOnMap,
|
showOnMap,
|
||||||
showStationRawData,
|
showStationRawData,
|
||||||
showSignalDetails,
|
showSignalDetails,
|
||||||
|
|||||||
@@ -3611,7 +3611,7 @@
|
|||||||
|
|
||||||
// Pager message filter settings
|
// Pager message filter settings
|
||||||
let pagerFilters = {
|
let pagerFilters = {
|
||||||
hideToneOnly: true,
|
hideToneOnly: false,
|
||||||
keywords: []
|
keywords: []
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3663,7 +3663,11 @@
|
|||||||
const saved = localStorage.getItem('pagerFilters');
|
const saved = localStorage.getItem('pagerFilters');
|
||||||
if (saved) {
|
if (saved) {
|
||||||
try {
|
try {
|
||||||
pagerFilters = JSON.parse(saved);
|
const parsed = JSON.parse(saved);
|
||||||
|
// Only persist keywords across sessions.
|
||||||
|
// hideToneOnly defaults to false every session so users
|
||||||
|
// always see the full traffic stream unless they opt-in.
|
||||||
|
if (Array.isArray(parsed.keywords)) pagerFilters.keywords = parsed.keywords;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to load pager filters:', e);
|
console.warn('Failed to load pager filters:', e);
|
||||||
}
|
}
|
||||||
@@ -3964,6 +3968,7 @@
|
|||||||
|
|
||||||
// Load pager message filters
|
// Load pager message filters
|
||||||
loadPagerFilters();
|
loadPagerFilters();
|
||||||
|
if (typeof SignalCards !== 'undefined') SignalCards.updateMutedIndicator();
|
||||||
|
|
||||||
// Initialize dropdown nav active state
|
// Initialize dropdown nav active state
|
||||||
updateDropdownActiveState();
|
updateDropdownActiveState();
|
||||||
@@ -6953,20 +6958,21 @@
|
|||||||
return null; // Can't determine
|
return null; // Can't determine
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for high entropy (random-looking data)
|
// Check for non-printable characters (outside printable ASCII range)
|
||||||
const printableRatio = (message.match(/[a-zA-Z0-9\s.,!?-]/g) || []).length / message.length;
|
|
||||||
|
|
||||||
// Check for common encrypted patterns (hex strings, base64-like)
|
|
||||||
const hexPattern = /^[0-9A-Fa-f\s]+$/;
|
|
||||||
const hasNonPrintable = /[^\x20-\x7E]/.test(message);
|
const hasNonPrintable = /[^\x20-\x7E]/.test(message);
|
||||||
|
|
||||||
if (printableRatio > 0.8 && !hasNonPrintable) {
|
// Check for common encrypted patterns (hex strings)
|
||||||
return false; // Likely plaintext
|
const hexPattern = /^[0-9A-Fa-f\s]+$/;
|
||||||
} else if (hexPattern.test(message.replace(/\s/g, '')) || hasNonPrintable) {
|
|
||||||
return true; // Likely encrypted or encoded
|
if (hasNonPrintable) {
|
||||||
|
return true; // Contains non-printable chars — likely encrypted or encoded
|
||||||
|
}
|
||||||
|
if (hexPattern.test(message.replace(/\s/g, ''))) {
|
||||||
|
return true; // Pure hex data — likely encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
return null; // Unknown
|
// All printable ASCII (covers base64, structured data, punctuation, etc.)
|
||||||
|
return false; // Likely plaintext
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate device fingerprint
|
// Generate device fingerprint
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
<h3>Message Filters</h3>
|
<h3>Message Filters</h3>
|
||||||
<div class="checkbox-group" style="margin-bottom: 10px;">
|
<div class="checkbox-group" style="margin-bottom: 10px;">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="filterToneOnly" checked onchange="savePagerFilters()">
|
<input type="checkbox" id="filterToneOnly" onchange="savePagerFilters()">
|
||||||
Hide "Tone Only" messages
|
Hide "Tone Only" messages
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,6 +73,14 @@
|
|||||||
<div class="info-text" style="font-size: 10px; color: #666; margin-top: 5px;">
|
<div class="info-text" style="font-size: 10px; color: #666; margin-top: 5px;">
|
||||||
Messages matching these keywords will be hidden from display but still logged.
|
Messages matching these keywords will be hidden from display but still logged.
|
||||||
</div>
|
</div>
|
||||||
|
<div id="mutedAddressInfo" style="margin-top: 8px; display: none;">
|
||||||
|
<span style="font-size: 11px; color: var(--text-dim, #888);">
|
||||||
|
<span id="mutedAddressCount">0</span> muted source(s)
|
||||||
|
</span>
|
||||||
|
<button onclick="SignalCards.unmuteAll()" style="margin-left: 6px; font-size: 10px; padding: 2px 8px; cursor: pointer; background: transparent; border: 1px solid var(--border-color, #444); color: var(--text-secondary, #aaa); border-radius: 3px;">
|
||||||
|
Unmute All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Antenna Guide -->
|
<!-- Antenna Guide -->
|
||||||
|
|||||||
123
tests/test_pager_parser.py
Normal file
123
tests/test_pager_parser.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"""Tests for pager multimon-ng output parser."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from routes.pager import parse_multimon_output
|
||||||
|
|
||||||
|
|
||||||
|
class TestPocsagAlphaNumeric:
|
||||||
|
"""Standard POCSAG messages with Alpha or Numeric content."""
|
||||||
|
|
||||||
|
def test_alpha_message(self):
|
||||||
|
line = "POCSAG1200: Address: 1337 Function: 3 Alpha: Hello World"
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["protocol"] == "POCSAG1200"
|
||||||
|
assert result["address"] == "1337"
|
||||||
|
assert result["function"] == "3"
|
||||||
|
assert result["msg_type"] == "Alpha"
|
||||||
|
assert result["message"] == "Hello World"
|
||||||
|
|
||||||
|
def test_numeric_message(self):
|
||||||
|
line = "POCSAG1200: Address: 500 Function: 2 Numeric: 55512345"
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["msg_type"] == "Numeric"
|
||||||
|
assert result["message"] == "55512345"
|
||||||
|
|
||||||
|
def test_alpha_empty_content(self):
|
||||||
|
line = "POCSAG1200: Address: 200 Function: 3 Alpha: "
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["msg_type"] == "Alpha"
|
||||||
|
assert result["message"] == "[No Message]"
|
||||||
|
|
||||||
|
def test_pocsag512_baud(self):
|
||||||
|
line = "POCSAG512: Address: 12345 Function: 0 Alpha: test"
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["protocol"] == "POCSAG512"
|
||||||
|
assert result["message"] == "test"
|
||||||
|
|
||||||
|
def test_pocsag2400_baud(self):
|
||||||
|
line = "POCSAG2400: Address: 9999 Function: 1 Numeric: 0"
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["protocol"] == "POCSAG2400"
|
||||||
|
|
||||||
|
def test_alpha_with_special_characters(self):
|
||||||
|
"""Base64, colons, equals signs, and other punctuation should parse."""
|
||||||
|
line = "POCSAG1200: Address: 1337 Function: 3 Alpha: 0:U0tZLQ=="
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["msg_type"] == "Alpha"
|
||||||
|
assert result["message"] == "0:U0tZLQ=="
|
||||||
|
|
||||||
|
|
||||||
|
class TestPocsagCatchAll:
|
||||||
|
"""Catch-all pattern for non-standard content type labels."""
|
||||||
|
|
||||||
|
def test_unknown_content_label(self):
|
||||||
|
"""Future multimon-ng versions might emit new type labels."""
|
||||||
|
line = "POCSAG1200: Address: 1337 Function: 3 Skyper: some data"
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["msg_type"] == "Skyper"
|
||||||
|
assert result["message"] == "some data"
|
||||||
|
|
||||||
|
def test_char_content_label(self):
|
||||||
|
line = "POCSAG1200: Address: 1337 Function: 2 Char: ABCDEF"
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["msg_type"] == "Char"
|
||||||
|
assert result["message"] == "ABCDEF"
|
||||||
|
|
||||||
|
def test_catchall_empty_content(self):
|
||||||
|
line = "POCSAG1200: Address: 1337 Function: 2 Raw: "
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["msg_type"] == "Raw"
|
||||||
|
assert result["message"] == "[No Message]"
|
||||||
|
|
||||||
|
def test_alpha_still_matches_first(self):
|
||||||
|
"""Alpha/Numeric pattern should take priority over catch-all."""
|
||||||
|
line = "POCSAG1200: Address: 100 Function: 3 Alpha: priority"
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["msg_type"] == "Alpha"
|
||||||
|
assert result["message"] == "priority"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPocsagToneOnly:
|
||||||
|
"""Address-only lines with no message content."""
|
||||||
|
|
||||||
|
def test_tone_only(self):
|
||||||
|
line = "POCSAG1200: Address: 1977540 Function: 2"
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["msg_type"] == "Tone"
|
||||||
|
assert result["message"] == "[Tone Only]"
|
||||||
|
assert result["address"] == "1977540"
|
||||||
|
|
||||||
|
def test_tone_only_with_trailing_spaces(self):
|
||||||
|
line = "POCSAG1200: Address: 1337 Function: 1 "
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["msg_type"] == "Tone"
|
||||||
|
|
||||||
|
|
||||||
|
class TestFlexParsing:
|
||||||
|
"""FLEX protocol output parsing."""
|
||||||
|
|
||||||
|
def test_simple_flex(self):
|
||||||
|
line = "FLEX: Some flex message here"
|
||||||
|
result = parse_multimon_output(line)
|
||||||
|
assert result is not None
|
||||||
|
assert result["protocol"] == "FLEX"
|
||||||
|
assert result["message"] == "Some flex message here"
|
||||||
|
|
||||||
|
def test_no_match(self):
|
||||||
|
"""Unrecognized lines should return None."""
|
||||||
|
assert parse_multimon_output("multimon-ng 1.2.0") is None
|
||||||
|
assert parse_multimon_output("") is None
|
||||||
|
assert parse_multimon_output("Enabled decoders: POCSAG512") is None
|
||||||
Reference in New Issue
Block a user