mirror of
https://github.com/smittix/intercept.git
synced 2026-04-25 23:29:59 -07:00
fix: harden DSC decoder against noise-induced false decodes
Stricter dot pattern detection (200 bits/100 alternations), bounded phasing strip (max 7), symbol check bit parity validation, EOS minimum position check, strict MMSI decode (reject out-of-range symbols), format-aware telecommand extraction, and expanded critical category detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,8 @@ from .constants import (
|
||||
FORMAT_CODES,
|
||||
DISTRESS_NATURE_CODES,
|
||||
VALID_EOS,
|
||||
TELECOMMAND_FORMATS,
|
||||
MIN_SYMBOLS_FOR_FORMAT,
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
@@ -222,13 +224,14 @@ class DSCDecoder:
|
||||
Detect DSC dot pattern for synchronization.
|
||||
|
||||
The dot pattern is at least 200 alternating bits (1010101...).
|
||||
We look for at least 20 consecutive alternations.
|
||||
We require at least 100 consecutive alternations to avoid
|
||||
false sync triggers from noise.
|
||||
"""
|
||||
if len(self.bit_buffer) < 40:
|
||||
if len(self.bit_buffer) < 200:
|
||||
return False
|
||||
|
||||
# Check last 40 bits for alternating pattern
|
||||
last_bits = self.bit_buffer[-40:]
|
||||
# Check last 200 bits for alternating pattern
|
||||
last_bits = self.bit_buffer[-200:]
|
||||
alternations = 0
|
||||
|
||||
for i in range(1, len(last_bits)):
|
||||
@@ -237,7 +240,7 @@ class DSCDecoder:
|
||||
else:
|
||||
alternations = 0
|
||||
|
||||
if alternations >= 20:
|
||||
if alternations >= 100:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -263,27 +266,37 @@ class DSCDecoder:
|
||||
if end <= len(self.message_bits):
|
||||
symbol_bits = self.message_bits[start:end]
|
||||
symbol_value = self._bits_to_symbol(symbol_bits)
|
||||
if symbol_value == -1:
|
||||
logger.debug("DSC symbol check bit failure, aborting decode")
|
||||
return None
|
||||
symbols.append(symbol_value)
|
||||
|
||||
# Strip phasing sequence (RX/DX symbols 120-126) from the
|
||||
# start of the message. Per ITU-R M.493, after the dot pattern
|
||||
# there are 7 phasing symbols before the format specifier.
|
||||
# Bound to max 7 — if more are present, this is a bad sync.
|
||||
msg_start = 0
|
||||
for i, sym in enumerate(symbols):
|
||||
if 120 <= sym <= 126:
|
||||
msg_start = i + 1
|
||||
else:
|
||||
break
|
||||
if msg_start > 7:
|
||||
logger.debug("DSC bad sync: >7 phasing symbols stripped")
|
||||
return None
|
||||
symbols = symbols[msg_start:]
|
||||
|
||||
if len(symbols) < 5:
|
||||
return None
|
||||
|
||||
# Look for EOS (End of Sequence) - symbols 117, 122, or 127
|
||||
# EOS must appear after at least MIN_SYMBOLS_FOR_FORMAT symbols
|
||||
eos_found = False
|
||||
eos_index = -1
|
||||
for i, sym in enumerate(symbols):
|
||||
if sym in VALID_EOS:
|
||||
if i < MIN_SYMBOLS_FOR_FORMAT:
|
||||
continue # Too early — not a real EOS
|
||||
eos_found = True
|
||||
eos_index = i
|
||||
break
|
||||
@@ -300,7 +313,9 @@ class DSCDecoder:
|
||||
Convert 10 bits to symbol value.
|
||||
|
||||
DSC uses 10-bit symbols: 7 information bits + 3 error bits.
|
||||
We extract the 7-bit value.
|
||||
The 3 check bits provide parity such that the total number of
|
||||
'1' bits across all 10 bits should be even (even parity).
|
||||
Returns -1 if the check bits are invalid.
|
||||
"""
|
||||
if len(bits) != 10:
|
||||
return -1
|
||||
@@ -311,6 +326,11 @@ class DSCDecoder:
|
||||
if bits[i]:
|
||||
value |= (1 << i)
|
||||
|
||||
# Validate check bits: total number of 1s should be even
|
||||
ones = sum(bits)
|
||||
if ones % 2 != 0:
|
||||
return -1
|
||||
|
||||
return value
|
||||
|
||||
def _decode_symbols(self, symbols: list[int]) -> dict | None:
|
||||
@@ -356,9 +376,13 @@ class DSCDecoder:
|
||||
|
||||
# Decode MMSI from symbols 1-5 (destination/address)
|
||||
dest_mmsi = self._decode_mmsi(symbols[1:6])
|
||||
if dest_mmsi is None:
|
||||
return None
|
||||
|
||||
# Decode self-ID from symbols 6-10 (source)
|
||||
source_mmsi = self._decode_mmsi(symbols[6:11])
|
||||
if source_mmsi is None:
|
||||
return None
|
||||
|
||||
message = {
|
||||
'type': 'dsc',
|
||||
@@ -387,8 +411,9 @@ class DSCDecoder:
|
||||
if position:
|
||||
message['position'] = position
|
||||
|
||||
# Telecommand fields (usually last two before EOS)
|
||||
if len(remaining) >= 2:
|
||||
# Telecommand fields (last two before EOS) — only for formats
|
||||
# that carry telecommand fields per ITU-R M.493
|
||||
if format_code in TELECOMMAND_FORMATS and len(remaining) >= 2:
|
||||
message['telecommand1'] = remaining[-2]
|
||||
message['telecommand2'] = remaining[-1]
|
||||
|
||||
@@ -402,20 +427,21 @@ class DSCDecoder:
|
||||
logger.warning(f"DSC decode error: {e}")
|
||||
return None
|
||||
|
||||
def _decode_mmsi(self, symbols: list[int]) -> str:
|
||||
def _decode_mmsi(self, symbols: list[int]) -> str | None:
|
||||
"""
|
||||
Decode MMSI from 5 DSC symbols.
|
||||
|
||||
Each symbol represents 2 BCD digits (00-99).
|
||||
5 symbols = 10 digits, but MMSI is 9 digits (first symbol has leading 0).
|
||||
Returns None if any symbol is out of valid BCD range.
|
||||
"""
|
||||
if len(symbols) < 5:
|
||||
return '000000000'
|
||||
return None
|
||||
|
||||
digits = []
|
||||
for sym in symbols:
|
||||
if sym < 0 or sym > 99:
|
||||
sym = 0
|
||||
return None
|
||||
# Each symbol is 2 BCD digits
|
||||
digits.append(f'{sym:02d}')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user