Create ASPM_system.py
Browse files- ASPM_system.py +898 -0
ASPM_system.py
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/Aur/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Advanced Signal Processing and Modulation System
|
| 4 |
+
===============================================
|
| 5 |
+
|
| 6 |
+
This module implements comprehensive digital signal processing including:
|
| 7 |
+
- Multiple modulation schemes (BFSK, BPSK, QPSK, QAM16, OFDM, DSSS)
|
| 8 |
+
- Forward Error Correction (FEC) coding
|
| 9 |
+
- Framing, security, and watermarking
|
| 10 |
+
- Audio and IQ signal generation
|
| 11 |
+
- Visualization and analysis tools
|
| 12 |
+
|
| 13 |
+
Author: Assistant
|
| 14 |
+
License: MIT
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import binascii
|
| 18 |
+
import hashlib
|
| 19 |
+
import math
|
| 20 |
+
import struct
|
| 21 |
+
import time
|
| 22 |
+
import wave
|
| 23 |
+
from dataclasses import dataclass
|
| 24 |
+
from enum import Enum, auto
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
| 27 |
+
|
| 28 |
+
import numpy as np
|
| 29 |
+
from scipy import signal as sp_signal
|
| 30 |
+
from scipy.fft import rfft, rfftfreq
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
import matplotlib.pyplot as plt
|
| 34 |
+
HAS_MATPLOTLIB = True
|
| 35 |
+
except ImportError:
|
| 36 |
+
HAS_MATPLOTLIB = False
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
import sounddevice as sd
|
| 40 |
+
HAS_AUDIO = True
|
| 41 |
+
except ImportError:
|
| 42 |
+
HAS_AUDIO = False
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
from Crypto.Cipher import AES
|
| 46 |
+
from Crypto.Random import get_random_bytes
|
| 47 |
+
from Crypto.Protocol.KDF import PBKDF2
|
| 48 |
+
HAS_CRYPTO = True
|
| 49 |
+
except ImportError:
|
| 50 |
+
HAS_CRYPTO = False
|
| 51 |
+
|
| 52 |
+
import logging
|
| 53 |
+
|
| 54 |
+
logging.basicConfig(level=logging.INFO)
|
| 55 |
+
logger = logging.getLogger(__name__)
|
| 56 |
+
|
| 57 |
+
# =========================================================
|
| 58 |
+
# Enums and Configuration
|
| 59 |
+
# =========================================================
|
| 60 |
+
|
| 61 |
+
class ModulationScheme(Enum):
|
| 62 |
+
BFSK = auto()
|
| 63 |
+
BPSK = auto()
|
| 64 |
+
QPSK = auto()
|
| 65 |
+
QAM16 = auto()
|
| 66 |
+
AFSK = auto()
|
| 67 |
+
OFDM = auto()
|
| 68 |
+
DSSS_BPSK = auto()
|
| 69 |
+
|
| 70 |
+
class FEC(Enum):
|
| 71 |
+
NONE = auto()
|
| 72 |
+
HAMMING74 = auto()
|
| 73 |
+
REED_SOLOMON = auto() # stub
|
| 74 |
+
LDPC = auto() # stub
|
| 75 |
+
TURBO = auto() # stub
|
| 76 |
+
|
| 77 |
+
@dataclass
|
| 78 |
+
class ModConfig:
|
| 79 |
+
sample_rate: int = 48000
|
| 80 |
+
symbol_rate: int = 1200
|
| 81 |
+
amplitude: float = 0.7
|
| 82 |
+
f0: float = 1200.0 # BFSK 0
|
| 83 |
+
f1: float = 2200.0 # BFSK 1
|
| 84 |
+
fc: float = 1800.0 # PSK/QAM audio carrier (for WAV)
|
| 85 |
+
clip: bool = True
|
| 86 |
+
# OFDM parameters
|
| 87 |
+
ofdm_subc: int = 64
|
| 88 |
+
cp_len: int = 16
|
| 89 |
+
# DSSS parameters
|
| 90 |
+
dsss_chip_rate: int = 4800
|
| 91 |
+
|
| 92 |
+
@dataclass
|
| 93 |
+
class FrameConfig:
|
| 94 |
+
use_crc32: bool = True
|
| 95 |
+
use_crc16: bool = False
|
| 96 |
+
preamble: bytes = b"\x55" * 8 # 01010101 * 8
|
| 97 |
+
version: int = 1
|
| 98 |
+
|
| 99 |
+
@dataclass
|
| 100 |
+
class SecurityConfig:
|
| 101 |
+
password: Optional[str] = None # AES-GCM if provided
|
| 102 |
+
watermark: Optional[str] = None # prepended SHA256[0:8]
|
| 103 |
+
hmac_key: Optional[str] = None # HMAC-SHA256 appended
|
| 104 |
+
|
| 105 |
+
@dataclass
|
| 106 |
+
class OutputPaths:
|
| 107 |
+
wav: Optional[Path] = None
|
| 108 |
+
iq: Optional[Path] = None
|
| 109 |
+
meta: Optional[Path] = None
|
| 110 |
+
png: Optional[Path] = None
|
| 111 |
+
|
| 112 |
+
# =========================================================
|
| 113 |
+
# Utility Functions
|
| 114 |
+
# =========================================================
|
| 115 |
+
|
| 116 |
+
def now_ms() -> int:
|
| 117 |
+
return int(time.time() * 1000)
|
| 118 |
+
|
| 119 |
+
def crc32_bytes(data: bytes) -> bytes:
|
| 120 |
+
return binascii.crc32(data).to_bytes(4, "big")
|
| 121 |
+
|
| 122 |
+
def crc16_ccitt(data: bytes) -> bytes:
|
| 123 |
+
poly, crc = 0x1021, 0xFFFF
|
| 124 |
+
for b in data:
|
| 125 |
+
crc ^= b << 8
|
| 126 |
+
for _ in range(8):
|
| 127 |
+
crc = ((crc << 1) ^ poly) & 0xFFFF if (crc & 0x8000) else ((crc << 1) & 0xFFFF)
|
| 128 |
+
return crc.to_bytes(2, "big")
|
| 129 |
+
|
| 130 |
+
def to_bits(data: bytes) -> List[int]:
|
| 131 |
+
return [(byte >> i) & 1 for byte in data for i in range(7, -1, -1)]
|
| 132 |
+
|
| 133 |
+
def from_bits(bits: Sequence[int]) -> bytes:
|
| 134 |
+
if len(bits) % 8 != 0:
|
| 135 |
+
bits = list(bits) + [0] * (8 - len(bits) % 8)
|
| 136 |
+
out = bytearray()
|
| 137 |
+
for i in range(0, len(bits), 8):
|
| 138 |
+
byte = 0
|
| 139 |
+
for b in bits[i:i+8]:
|
| 140 |
+
byte = (byte << 1) | (1 if b else 0)
|
| 141 |
+
out.append(byte)
|
| 142 |
+
return bytes(out)
|
| 143 |
+
|
| 144 |
+
def chunk_bits(bits: Sequence[int], n: int) -> List[List[int]]:
|
| 145 |
+
return [list(bits[i:i+n]) for i in range(0, len(bits), n)]
|
| 146 |
+
|
| 147 |
+
def safe_json(obj: Any) -> str:
|
| 148 |
+
import json
|
| 149 |
+
def enc(x):
|
| 150 |
+
if isinstance(x, (np.floating,)):
|
| 151 |
+
return float(x)
|
| 152 |
+
if isinstance(x, (np.integer,)):
|
| 153 |
+
return int(x)
|
| 154 |
+
if isinstance(x, (np.ndarray,)):
|
| 155 |
+
return x.tolist()
|
| 156 |
+
if isinstance(x, complex):
|
| 157 |
+
return {"real": float(x.real), "imag": float(x.imag)}
|
| 158 |
+
return str(x)
|
| 159 |
+
return json.dumps(obj, ensure_ascii=False, indent=2, default=enc)
|
| 160 |
+
|
| 161 |
+
# =========================================================
|
| 162 |
+
# FEC Implementation
|
| 163 |
+
# =========================================================
|
| 164 |
+
|
| 165 |
+
def hamming74_encode(data_bits: List[int]) -> List[int]:
|
| 166 |
+
"""Hamming (7,4) encoding"""
|
| 167 |
+
if len(data_bits) % 4 != 0:
|
| 168 |
+
data_bits = data_bits + [0] * (4 - len(data_bits) % 4)
|
| 169 |
+
|
| 170 |
+
out = []
|
| 171 |
+
for i in range(0, len(data_bits), 4):
|
| 172 |
+
d0, d1, d2, d3 = data_bits[i:i+4]
|
| 173 |
+
p1 = d0 ^ d1 ^ d3
|
| 174 |
+
p2 = d0 ^ d2 ^ d3
|
| 175 |
+
p3 = d1 ^ d2 ^ d3
|
| 176 |
+
out += [p1, p2, d0, p3, d1, d2, d3]
|
| 177 |
+
|
| 178 |
+
return out
|
| 179 |
+
|
| 180 |
+
def hamming74_decode(coded_bits: List[int]) -> Tuple[List[int], int]:
|
| 181 |
+
"""Hamming (7,4) decoding with error correction"""
|
| 182 |
+
if len(coded_bits) % 7 != 0:
|
| 183 |
+
coded_bits = coded_bits + [0] * (7 - len(coded_bits) % 7)
|
| 184 |
+
|
| 185 |
+
decoded = []
|
| 186 |
+
errors_corrected = 0
|
| 187 |
+
|
| 188 |
+
for i in range(0, len(coded_bits), 7):
|
| 189 |
+
r = coded_bits[i:i+7] # received codeword
|
| 190 |
+
p1, p2, d0, p3, d1, d2, d3 = r
|
| 191 |
+
|
| 192 |
+
# Calculate syndrome
|
| 193 |
+
s1 = p1 ^ d0 ^ d1 ^ d3
|
| 194 |
+
s2 = p2 ^ d0 ^ d2 ^ d3
|
| 195 |
+
s3 = p3 ^ d1 ^ d2 ^ d3
|
| 196 |
+
|
| 197 |
+
syndrome = s1 + 2*s2 + 4*s3
|
| 198 |
+
|
| 199 |
+
# Correct single-bit errors
|
| 200 |
+
if syndrome != 0:
|
| 201 |
+
errors_corrected += 1
|
| 202 |
+
if syndrome <= 7:
|
| 203 |
+
r[syndrome - 1] ^= 1 # flip the error bit
|
| 204 |
+
|
| 205 |
+
# Extract data bits
|
| 206 |
+
decoded.extend([r[2], r[4], r[5], r[6]]) # d0, d1, d2, d3
|
| 207 |
+
|
| 208 |
+
return decoded, errors_corrected
|
| 209 |
+
|
| 210 |
+
def fec_encode(bits: List[int], scheme: FEC) -> List[int]:
|
| 211 |
+
if scheme == FEC.NONE:
|
| 212 |
+
return list(bits)
|
| 213 |
+
elif scheme == FEC.HAMMING74:
|
| 214 |
+
return hamming74_encode(bits)
|
| 215 |
+
elif scheme in (FEC.REED_SOLOMON, FEC.LDPC, FEC.TURBO):
|
| 216 |
+
raise NotImplementedError(f"{scheme.name} encoding not implemented")
|
| 217 |
+
else:
|
| 218 |
+
raise ValueError("Unknown FEC scheme")
|
| 219 |
+
|
| 220 |
+
def fec_decode(bits: List[int], scheme: FEC) -> Tuple[List[int], Dict[str, Any]]:
|
| 221 |
+
if scheme == FEC.NONE:
|
| 222 |
+
return list(bits), {"errors_corrected": 0}
|
| 223 |
+
elif scheme == FEC.HAMMING74:
|
| 224 |
+
decoded, errors = hamming74_decode(bits)
|
| 225 |
+
return decoded, {"errors_corrected": errors}
|
| 226 |
+
else:
|
| 227 |
+
raise NotImplementedError(f"{scheme.name} decoding not implemented")
|
| 228 |
+
|
| 229 |
+
# =========================================================
|
| 230 |
+
# Security and Framing
|
| 231 |
+
# =========================================================
|
| 232 |
+
|
| 233 |
+
def aes_gcm_encrypt(plaintext: bytes, password: str) -> bytes:
|
| 234 |
+
if not HAS_CRYPTO:
|
| 235 |
+
raise RuntimeError("pycryptodome required for encryption")
|
| 236 |
+
|
| 237 |
+
salt = get_random_bytes(16)
|
| 238 |
+
key = PBKDF2(password, salt, dkLen=32, count=200_000)
|
| 239 |
+
nonce = get_random_bytes(12)
|
| 240 |
+
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
| 241 |
+
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
|
| 242 |
+
|
| 243 |
+
return b"AGCM" + salt + nonce + tag + ciphertext
|
| 244 |
+
|
| 245 |
+
def aes_gcm_decrypt(encrypted: bytes, password: str) -> bytes:
|
| 246 |
+
if not HAS_CRYPTO:
|
| 247 |
+
raise RuntimeError("pycryptodome required for decryption")
|
| 248 |
+
|
| 249 |
+
if not encrypted.startswith(b"AGCM"):
|
| 250 |
+
raise ValueError("Invalid encrypted format")
|
| 251 |
+
|
| 252 |
+
data = encrypted[4:] # skip "AGCM" header
|
| 253 |
+
salt = data[:16]
|
| 254 |
+
nonce = data[16:28]
|
| 255 |
+
tag = data[28:44]
|
| 256 |
+
ciphertext = data[44:]
|
| 257 |
+
|
| 258 |
+
key = PBKDF2(password, salt, dkLen=32, count=200_000)
|
| 259 |
+
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
| 260 |
+
|
| 261 |
+
return cipher.decrypt_and_verify(ciphertext, tag)
|
| 262 |
+
|
| 263 |
+
def apply_hmac(data: bytes, hkey: str) -> bytes:
|
| 264 |
+
import hmac
|
| 265 |
+
key = hashlib.sha256(hkey.encode("utf-8")).digest()
|
| 266 |
+
mac = hmac.new(key, data, hashlib.sha256).digest()
|
| 267 |
+
return data + b"HMAC" + mac
|
| 268 |
+
|
| 269 |
+
def verify_hmac(data: bytes, hkey: str) -> Tuple[bytes, bool]:
|
| 270 |
+
if not data.endswith(b"HMAC"):
|
| 271 |
+
return data, False
|
| 272 |
+
|
| 273 |
+
# Find HMAC marker
|
| 274 |
+
hmac_pos = data.rfind(b"HMAC")
|
| 275 |
+
if hmac_pos == -1 or len(data) - hmac_pos != 36: # 4 + 32 bytes
|
| 276 |
+
return data, False
|
| 277 |
+
|
| 278 |
+
payload = data[:hmac_pos]
|
| 279 |
+
received_mac = data[hmac_pos + 4:]
|
| 280 |
+
|
| 281 |
+
import hmac
|
| 282 |
+
key = hashlib.sha256(hkey.encode("utf-8")).digest()
|
| 283 |
+
expected_mac = hmac.new(key, payload, hashlib.sha256).digest()
|
| 284 |
+
|
| 285 |
+
return payload, hmac.compare_digest(received_mac, expected_mac)
|
| 286 |
+
|
| 287 |
+
def add_watermark(data: bytes, wm: str) -> bytes:
|
| 288 |
+
return hashlib.sha256(wm.encode("utf-8")).digest()[:8] + data
|
| 289 |
+
|
| 290 |
+
def check_watermark(data: bytes, wm: str) -> Tuple[bytes, bool]:
|
| 291 |
+
if len(data) < 8:
|
| 292 |
+
return data, False
|
| 293 |
+
|
| 294 |
+
expected = hashlib.sha256(wm.encode("utf-8")).digest()[:8]
|
| 295 |
+
received = data[:8]
|
| 296 |
+
payload = data[8:]
|
| 297 |
+
|
| 298 |
+
return payload, received == expected
|
| 299 |
+
|
| 300 |
+
def frame_payload(payload: bytes, fcfg: FrameConfig) -> bytes:
|
| 301 |
+
header = struct.pack(">BBI", 0xA5, fcfg.version, now_ms() & 0xFFFFFFFF)
|
| 302 |
+
core = header + payload
|
| 303 |
+
|
| 304 |
+
tail = b""
|
| 305 |
+
if fcfg.use_crc32:
|
| 306 |
+
tail += crc32_bytes(core)
|
| 307 |
+
if fcfg.use_crc16:
|
| 308 |
+
tail += crc16_ccitt(core)
|
| 309 |
+
|
| 310 |
+
return fcfg.preamble + core + tail
|
| 311 |
+
|
| 312 |
+
def unframe_payload(framed: bytes, fcfg: FrameConfig) -> Tuple[bytes, Dict[str, Any]]:
|
| 313 |
+
if len(framed) < len(fcfg.preamble) + 7: # minimum frame size
|
| 314 |
+
return b"", {"error": "Frame too short"}
|
| 315 |
+
|
| 316 |
+
# Check preamble
|
| 317 |
+
if not framed.startswith(fcfg.preamble):
|
| 318 |
+
return b"", {"error": "Invalid preamble"}
|
| 319 |
+
|
| 320 |
+
data = framed[len(fcfg.preamble):]
|
| 321 |
+
|
| 322 |
+
# Parse header
|
| 323 |
+
if len(data) < 7:
|
| 324 |
+
return b"", {"error": "Header too short"}
|
| 325 |
+
|
| 326 |
+
sync, version, timestamp = struct.unpack(">BBI", data[:7])
|
| 327 |
+
if sync != 0xA5:
|
| 328 |
+
return b"", {"error": "Invalid sync byte"}
|
| 329 |
+
|
| 330 |
+
# Calculate payload length
|
| 331 |
+
tail_len = 0
|
| 332 |
+
if fcfg.use_crc32:
|
| 333 |
+
tail_len += 4
|
| 334 |
+
if fcfg.use_crc16:
|
| 335 |
+
tail_len += 2
|
| 336 |
+
|
| 337 |
+
if len(data) < 7 + tail_len:
|
| 338 |
+
return b"", {"error": "Frame too short for CRC"}
|
| 339 |
+
|
| 340 |
+
payload = data[7:-tail_len] if tail_len > 0 else data[7:]
|
| 341 |
+
|
| 342 |
+
# Verify CRCs
|
| 343 |
+
info = {"version": version, "timestamp": timestamp}
|
| 344 |
+
|
| 345 |
+
if fcfg.use_crc32:
|
| 346 |
+
expected_crc32 = crc32_bytes(data[:-tail_len])
|
| 347 |
+
received_crc32 = data[-tail_len:-tail_len+4] if fcfg.use_crc16 else data[-4:]
|
| 348 |
+
info["crc32_ok"] = expected_crc32 == received_crc32
|
| 349 |
+
|
| 350 |
+
if fcfg.use_crc16:
|
| 351 |
+
expected_crc16 = crc16_ccitt(data[:-2])
|
| 352 |
+
received_crc16 = data[-2:]
|
| 353 |
+
info["crc16_ok"] = expected_crc16 == received_crc16
|
| 354 |
+
|
| 355 |
+
return payload, info
|
| 356 |
+
|
| 357 |
+
def encode_text(text: str, fcfg: FrameConfig, sec: SecurityConfig, fec_scheme: FEC) -> List[int]:
|
| 358 |
+
"""Complete encoding pipeline"""
|
| 359 |
+
data = text.encode("utf-8")
|
| 360 |
+
|
| 361 |
+
# Apply watermark
|
| 362 |
+
if sec.watermark:
|
| 363 |
+
data = add_watermark(data, sec.watermark)
|
| 364 |
+
|
| 365 |
+
# Apply encryption
|
| 366 |
+
if sec.password:
|
| 367 |
+
data = aes_gcm_encrypt(data, sec.password)
|
| 368 |
+
|
| 369 |
+
# Frame the data
|
| 370 |
+
framed = frame_payload(data, fcfg)
|
| 371 |
+
|
| 372 |
+
# Apply HMAC
|
| 373 |
+
if sec.hmac_key:
|
| 374 |
+
framed = apply_hmac(framed, sec.hmac_key)
|
| 375 |
+
|
| 376 |
+
# Convert to bits and apply FEC
|
| 377 |
+
bits = to_bits(framed)
|
| 378 |
+
bits = fec_encode(bits, fec_scheme)
|
| 379 |
+
|
| 380 |
+
return bits
|
| 381 |
+
|
| 382 |
+
def decode_bits(bits: List[int], fcfg: FrameConfig, sec: SecurityConfig, fec_scheme: FEC) -> Tuple[str, Dict[str, Any]]:
|
| 383 |
+
"""Complete decoding pipeline"""
|
| 384 |
+
info = {}
|
| 385 |
+
|
| 386 |
+
try:
|
| 387 |
+
# Apply FEC decoding
|
| 388 |
+
decoded_bits, fec_info = fec_decode(bits, fec_scheme)
|
| 389 |
+
info.update(fec_info)
|
| 390 |
+
|
| 391 |
+
# Convert bits to bytes
|
| 392 |
+
framed = from_bits(decoded_bits)
|
| 393 |
+
|
| 394 |
+
# Verify HMAC
|
| 395 |
+
if sec.hmac_key:
|
| 396 |
+
framed, hmac_ok = verify_hmac(framed, sec.hmac_key)
|
| 397 |
+
info["hmac_ok"] = hmac_ok
|
| 398 |
+
if not hmac_ok:
|
| 399 |
+
return "", {**info, "error": "HMAC verification failed"}
|
| 400 |
+
|
| 401 |
+
# Unframe
|
| 402 |
+
data, frame_info = unframe_payload(framed, fcfg)
|
| 403 |
+
info.update(frame_info)
|
| 404 |
+
|
| 405 |
+
if "error" in frame_info:
|
| 406 |
+
return "", info
|
| 407 |
+
|
| 408 |
+
# Decrypt
|
| 409 |
+
if sec.password:
|
| 410 |
+
data = aes_gcm_decrypt(data, sec.password)
|
| 411 |
+
info["decrypted"] = True
|
| 412 |
+
|
| 413 |
+
# Check watermark
|
| 414 |
+
if sec.watermark:
|
| 415 |
+
data, wm_ok = check_watermark(data, sec.watermark)
|
| 416 |
+
info["watermark_ok"] = wm_ok
|
| 417 |
+
if not wm_ok:
|
| 418 |
+
return "", {**info, "error": "Watermark verification failed"}
|
| 419 |
+
|
| 420 |
+
# Decode text
|
| 421 |
+
text = data.decode("utf-8", errors="replace")
|
| 422 |
+
return text, info
|
| 423 |
+
|
| 424 |
+
except Exception as e:
|
| 425 |
+
return "", {**info, "error": str(e)}
|
| 426 |
+
|
| 427 |
+
# =========================================================
|
| 428 |
+
# Modulation Schemes
|
| 429 |
+
# =========================================================
|
| 430 |
+
|
| 431 |
+
class Modulators:
|
| 432 |
+
@staticmethod
|
| 433 |
+
def bfsk(bits: Sequence[int], cfg: ModConfig) -> np.ndarray:
|
| 434 |
+
"""Binary Frequency Shift Keying"""
|
| 435 |
+
sr, rb = cfg.sample_rate, cfg.symbol_rate
|
| 436 |
+
spb = int(sr / rb) # samples per bit
|
| 437 |
+
t = np.arange(spb) / sr
|
| 438 |
+
|
| 439 |
+
signal_blocks = []
|
| 440 |
+
for bit in bits:
|
| 441 |
+
freq = cfg.f1 if bit else cfg.f0
|
| 442 |
+
signal_blocks.append(cfg.amplitude * np.sin(2 * np.pi * freq * t))
|
| 443 |
+
|
| 444 |
+
if not signal_blocks:
|
| 445 |
+
return np.zeros(0, dtype=np.float32)
|
| 446 |
+
|
| 447 |
+
signal = np.concatenate(signal_blocks)
|
| 448 |
+
|
| 449 |
+
if cfg.clip:
|
| 450 |
+
signal = np.clip(signal, -1, 1)
|
| 451 |
+
|
| 452 |
+
return signal.astype(np.float32)
|
| 453 |
+
|
| 454 |
+
@staticmethod
|
| 455 |
+
def bpsk(bits: Sequence[int], cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
|
| 456 |
+
"""Binary Phase Shift Keying"""
|
| 457 |
+
sr, rb, fc = cfg.sample_rate, cfg.symbol_rate, cfg.fc
|
| 458 |
+
spb = int(sr / rb)
|
| 459 |
+
t = np.arange(spb) / sr
|
| 460 |
+
|
| 461 |
+
audio_blocks = []
|
| 462 |
+
iq_blocks = []
|
| 463 |
+
|
| 464 |
+
for bit in bits:
|
| 465 |
+
phase = 0.0 if bit else np.pi
|
| 466 |
+
|
| 467 |
+
# Audio signal (upconverted)
|
| 468 |
+
audio_blocks.append(cfg.amplitude * np.sin(2 * np.pi * fc * t + phase))
|
| 469 |
+
|
| 470 |
+
# IQ signal (baseband)
|
| 471 |
+
iq_symbol = cfg.amplitude * (np.cos(phase) + 1j * np.sin(phase))
|
| 472 |
+
iq_blocks.append(iq_symbol * np.ones(spb, dtype=np.complex64))
|
| 473 |
+
|
| 474 |
+
audio = np.concatenate(audio_blocks) if audio_blocks else np.zeros(0, dtype=np.float32)
|
| 475 |
+
iq = np.concatenate(iq_blocks) if iq_blocks else np.zeros(0, dtype=np.complex64)
|
| 476 |
+
|
| 477 |
+
if cfg.clip:
|
| 478 |
+
audio = np.clip(audio, -1, 1)
|
| 479 |
+
|
| 480 |
+
return audio.astype(np.float32), iq
|
| 481 |
+
|
| 482 |
+
@staticmethod
|
| 483 |
+
def qpsk(bits: Sequence[int], cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
|
| 484 |
+
"""Quadrature Phase Shift Keying"""
|
| 485 |
+
pairs = chunk_bits(bits, 2)
|
| 486 |
+
symbols = []
|
| 487 |
+
|
| 488 |
+
# Gray mapping: 00→(1+1j), 01→(-1+1j), 11→(-1-1j), 10→(1-1j)
|
| 489 |
+
for pair in pairs:
|
| 490 |
+
b0, b1 = (pair + [0, 0])[:2]
|
| 491 |
+
if (b0, b1) == (0, 0):
|
| 492 |
+
symbol = 1 + 1j
|
| 493 |
+
elif (b0, b1) == (0, 1):
|
| 494 |
+
symbol = -1 + 1j
|
| 495 |
+
elif (b0, b1) == (1, 1):
|
| 496 |
+
symbol = -1 - 1j
|
| 497 |
+
else: # (1, 0)
|
| 498 |
+
symbol = 1 - 1j
|
| 499 |
+
|
| 500 |
+
symbols.append(symbol / math.sqrt(2)) # normalize for unit energy
|
| 501 |
+
|
| 502 |
+
return Modulators._psk_qam_to_audio_iq(np.array(symbols, dtype=np.complex64), cfg)
|
| 503 |
+
|
| 504 |
+
@staticmethod
|
| 505 |
+
def qam16(bits: Sequence[int], cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
|
| 506 |
+
"""16-QAM modulation"""
|
| 507 |
+
quads = chunk_bits(bits, 4)
|
| 508 |
+
|
| 509 |
+
def gray_map_2bit(b0, b1):
|
| 510 |
+
# Gray mapping for 2 bits to {-3, -1, 1, 3}
|
| 511 |
+
val = (b0 << 1) | b1
|
| 512 |
+
return [-3, -1, 1, 3][val]
|
| 513 |
+
|
| 514 |
+
symbols = []
|
| 515 |
+
for quad in quads:
|
| 516 |
+
b0, b1, b2, b3 = (quad + [0, 0, 0, 0])[:4]
|
| 517 |
+
I = gray_map_2bit(b0, b1)
|
| 518 |
+
Q = gray_map_2bit(b2, b3)
|
| 519 |
+
symbol = (I + 1j * Q) / math.sqrt(10) # normalize for unit average power
|
| 520 |
+
symbols.append(symbol)
|
| 521 |
+
|
| 522 |
+
return Modulators._psk_qam_to_audio_iq(np.array(symbols, dtype=np.complex64), cfg)
|
| 523 |
+
|
| 524 |
+
@staticmethod
|
| 525 |
+
def _psk_qam_to_audio_iq(symbols: np.ndarray, cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
|
| 526 |
+
"""Convert PSK/QAM symbols to audio and IQ signals"""
|
| 527 |
+
sr, rb, fc = cfg.sample_rate, cfg.symbol_rate, cfg.fc
|
| 528 |
+
spb = int(sr / rb)
|
| 529 |
+
|
| 530 |
+
# Upsample symbols (rectangular pulse shaping)
|
| 531 |
+
i_data = np.repeat(symbols.real.astype(np.float32), spb)
|
| 532 |
+
q_data = np.repeat(symbols.imag.astype(np.float32), spb)
|
| 533 |
+
|
| 534 |
+
# Generate time vector
|
| 535 |
+
t = np.arange(len(i_data)) / sr
|
| 536 |
+
|
| 537 |
+
# Generate audio signal (upconverted)
|
| 538 |
+
audio = cfg.amplitude * (i_data * np.cos(2 * np.pi * fc * t) -
|
| 539 |
+
q_data * np.sin(2 * np.pi * fc * t))
|
| 540 |
+
|
| 541 |
+
# Generate IQ signal (baseband)
|
| 542 |
+
iq = (cfg.amplitude * i_data) + 1j * (cfg.amplitude * q_data)
|
| 543 |
+
|
| 544 |
+
if cfg.clip:
|
| 545 |
+
audio = np.clip(audio, -1, 1)
|
| 546 |
+
|
| 547 |
+
return audio.astype(np.float32), iq.astype(np.complex64)
|
| 548 |
+
|
| 549 |
+
@staticmethod
|
| 550 |
+
def afsk(bits: Sequence[int], cfg: ModConfig) -> np.ndarray:
|
| 551 |
+
"""Audio Frequency Shift Keying (same as BFSK)"""
|
| 552 |
+
return Modulators.bfsk(bits, cfg)
|
| 553 |
+
|
| 554 |
+
@staticmethod
|
| 555 |
+
def dsss_bpsk(bits: Sequence[int], cfg: ModConfig) -> np.ndarray:
|
| 556 |
+
"""Direct Sequence Spread Spectrum BPSK"""
|
| 557 |
+
# Simple PN sequence for spreading
|
| 558 |
+
pn_sequence = np.array([1, -1, 1, 1, -1, 1, -1, -1], dtype=np.float32)
|
| 559 |
+
|
| 560 |
+
sr = cfg.sample_rate
|
| 561 |
+
chip_rate = cfg.dsss_chip_rate
|
| 562 |
+
samples_per_chip = int(sr / chip_rate)
|
| 563 |
+
|
| 564 |
+
baseband_signal = []
|
| 565 |
+
|
| 566 |
+
for bit in bits:
|
| 567 |
+
bit_value = 1.0 if bit else -1.0
|
| 568 |
+
|
| 569 |
+
# Spread with PN sequence
|
| 570 |
+
spread_chips = bit_value * pn_sequence
|
| 571 |
+
|
| 572 |
+
# Upsample chips
|
| 573 |
+
for chip in spread_chips:
|
| 574 |
+
baseband_signal.extend([chip] * samples_per_chip)
|
| 575 |
+
|
| 576 |
+
baseband = np.array(baseband_signal, dtype=np.float32)
|
| 577 |
+
|
| 578 |
+
# Upconvert to carrier frequency
|
| 579 |
+
t = np.arange(len(baseband)) / sr
|
| 580 |
+
audio = cfg.amplitude * baseband * np.sin(2 * np.pi * cfg.fc * t)
|
| 581 |
+
|
| 582 |
+
if cfg.clip:
|
| 583 |
+
audio = np.clip(audio, -1, 1)
|
| 584 |
+
|
| 585 |
+
return audio.astype(np.float32)
|
| 586 |
+
|
| 587 |
+
@staticmethod
|
| 588 |
+
def ofdm(bits: Sequence[int], cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
|
| 589 |
+
"""Orthogonal Frequency Division Multiplexing"""
|
| 590 |
+
N = cfg.ofdm_subc
|
| 591 |
+
cp_len = cfg.cp_len
|
| 592 |
+
|
| 593 |
+
# Group bits for QPSK mapping on each subcarrier
|
| 594 |
+
symbol_chunks = chunk_bits(bits, 2 * N)
|
| 595 |
+
|
| 596 |
+
audio_blocks = []
|
| 597 |
+
iq_blocks = []
|
| 598 |
+
|
| 599 |
+
for chunk in symbol_chunks:
|
| 600 |
+
# Map bits to QPSK symbols
|
| 601 |
+
qpsk_symbols = []
|
| 602 |
+
bit_pairs = chunk_bits(chunk, 2)
|
| 603 |
+
|
| 604 |
+
for pair in bit_pairs:
|
| 605 |
+
b0, b1 = (pair + [0, 0])[:2]
|
| 606 |
+
if (b0, b1) == (0, 0):
|
| 607 |
+
symbol = 1 + 1j
|
| 608 |
+
elif (b0, b1) == (0, 1):
|
| 609 |
+
symbol = -1 + 1j
|
| 610 |
+
elif (b0, b1) == (1, 1):
|
| 611 |
+
symbol = -1 - 1j
|
| 612 |
+
else:
|
| 613 |
+
symbol = 1 - 1j
|
| 614 |
+
qpsk_symbols.append(symbol / math.sqrt(2))
|
| 615 |
+
|
| 616 |
+
# Pad to N subcarriers
|
| 617 |
+
while len(qpsk_symbols) < N:
|
| 618 |
+
qpsk_symbols.append(0j)
|
| 619 |
+
|
| 620 |
+
# IFFT to get time domain signal
|
| 621 |
+
freq_domain = np.array(qpsk_symbols[:N], dtype=np.complex64)
|
| 622 |
+
time_domain = np.fft.ifft(freq_domain)
|
| 623 |
+
|
| 624 |
+
# Add cyclic prefix
|
| 625 |
+
cyclic_prefix = time_domain[-cp_len:]
|
| 626 |
+
ofdm_symbol = np.concatenate([cyclic_prefix, time_domain])
|
| 627 |
+
|
| 628 |
+
# Scale to fit symbol rate timing
|
| 629 |
+
symbol_duration = int(cfg.sample_rate / cfg.symbol_rate)
|
| 630 |
+
repeat_factor = max(1, symbol_duration // len(ofdm_symbol))
|
| 631 |
+
upsampled = np.repeat(ofdm_symbol, repeat_factor)
|
| 632 |
+
|
| 633 |
+
# Generate audio (upconverted)
|
| 634 |
+
t = np.arange(len(upsampled)) / cfg.sample_rate
|
| 635 |
+
audio = cfg.amplitude * (upsampled.real * np.cos(2 * np.pi * cfg.fc * t) -
|
| 636 |
+
upsampled.imag * np.sin(2 * np.pi * cfg.fc * t))
|
| 637 |
+
|
| 638 |
+
audio_blocks.append(audio.astype(np.float32))
|
| 639 |
+
iq_blocks.append((cfg.amplitude * upsampled).astype(np.complex64))
|
| 640 |
+
|
| 641 |
+
audio = np.concatenate(audio_blocks) if audio_blocks else np.zeros(0, dtype=np.float32)
|
| 642 |
+
iq = np.concatenate(iq_blocks) if iq_blocks else np.zeros(0, dtype=np.complex64)
|
| 643 |
+
|
| 644 |
+
if cfg.clip:
|
| 645 |
+
audio = np.clip(audio, -1, 1)
|
| 646 |
+
|
| 647 |
+
return audio, iq
|
| 648 |
+
|
| 649 |
+
def bits_to_signals(bits: List[int], scheme: ModulationScheme, cfg: ModConfig) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]:
|
| 650 |
+
"""Convert bits to modulated signals"""
|
| 651 |
+
if scheme == ModulationScheme.BFSK:
|
| 652 |
+
return Modulators.bfsk(bits, cfg), None
|
| 653 |
+
elif scheme == ModulationScheme.AFSK:
|
| 654 |
+
return Modulators.afsk(bits, cfg), None
|
| 655 |
+
elif scheme == ModulationScheme.BPSK:
|
| 656 |
+
return Modulators.bpsk(bits, cfg)
|
| 657 |
+
elif scheme == ModulationScheme.QPSK:
|
| 658 |
+
return Modulators.qpsk(bits, cfg)
|
| 659 |
+
elif scheme == ModulationScheme.QAM16:
|
| 660 |
+
return Modulators.qam16(bits, cfg)
|
| 661 |
+
elif scheme == ModulationScheme.OFDM:
|
| 662 |
+
return Modulators.ofdm(bits, cfg)
|
| 663 |
+
elif scheme == ModulationScheme.DSSS_BPSK:
|
| 664 |
+
return Modulators.dsss_bpsk(bits, cfg), None
|
| 665 |
+
else:
|
| 666 |
+
raise ValueError(f"Unknown modulation scheme: {scheme}")
|
| 667 |
+
|
| 668 |
+
# =========================================================
|
| 669 |
+
# File I/O and Visualization
|
| 670 |
+
# =========================================================
|
| 671 |
+
|
| 672 |
+
def write_wav_mono(path: Path, signal: np.ndarray, sample_rate: int):
|
| 673 |
+
"""Write mono WAV file"""
|
| 674 |
+
sig = np.clip(signal, -1.0, 1.0)
|
| 675 |
+
pcm = (sig * 32767.0).astype(np.int16)
|
| 676 |
+
|
| 677 |
+
with wave.open(str(path), "wb") as w:
|
| 678 |
+
w.setnchannels(1)
|
| 679 |
+
w.setsampwidth(2)
|
| 680 |
+
w.setframerate(sample_rate)
|
| 681 |
+
w.writeframes(pcm.tobytes())
|
| 682 |
+
|
| 683 |
+
def write_iq_f32(path: Path, iq: np.ndarray):
|
| 684 |
+
"""Write IQ data as interleaved float32"""
|
| 685 |
+
if iq.ndim != 1 or not np.iscomplexobj(iq):
|
| 686 |
+
raise ValueError("iq must be 1-D complex array")
|
| 687 |
+
|
| 688 |
+
interleaved = np.empty(iq.size * 2, dtype=np.float32)
|
| 689 |
+
interleaved[0::2] = iq.real.astype(np.float32)
|
| 690 |
+
interleaved[1::2] = iq.imag.astype(np.float32)
|
| 691 |
+
|
| 692 |
+
path.write_bytes(interleaved.tobytes())
|
| 693 |
+
|
| 694 |
+
def plot_wave_and_spectrum(path_png: Path, x: np.ndarray, sr: int, title: str):
|
| 695 |
+
"""Plot waveform and spectrum"""
|
| 696 |
+
if not HAS_MATPLOTLIB:
|
| 697 |
+
logger.warning("Matplotlib not available, skipping plot")
|
| 698 |
+
return
|
| 699 |
+
|
| 700 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
|
| 701 |
+
|
| 702 |
+
# Time domain plot (first 50ms)
|
| 703 |
+
samples_to_plot = min(len(x), int(0.05 * sr))
|
| 704 |
+
t = np.arange(samples_to_plot) / sr
|
| 705 |
+
ax1.plot(t, x[:samples_to_plot])
|
| 706 |
+
ax1.set_title(f"{title} - Time Domain (first 50ms)")
|
| 707 |
+
ax1.set_xlabel("Time (s)")
|
| 708 |
+
ax1.set_ylabel("Amplitude")
|
| 709 |
+
ax1.grid(True, alpha=0.3)
|
| 710 |
+
|
| 711 |
+
# Frequency domain plot
|
| 712 |
+
spectrum = np.abs(rfft(x)) + 1e-12
|
| 713 |
+
freqs = rfftfreq(len(x), 1.0 / sr)
|
| 714 |
+
ax2.semilogy(freqs, spectrum / spectrum.max())
|
| 715 |
+
ax2.set_xlim(0, min(8000, sr // 2))
|
| 716 |
+
ax2.set_title(f"{title} - Frequency Domain")
|
| 717 |
+
ax2.set_xlabel("Frequency (Hz)")
|
| 718 |
+
ax2.set_ylabel("Normalized |X(f)|")
|
| 719 |
+
ax2.grid(True, alpha=0.3)
|
| 720 |
+
|
| 721 |
+
plt.tight_layout()
|
| 722 |
+
fig.savefig(path_png, dpi=300, bbox_inches='tight')
|
| 723 |
+
plt.close(fig)
|
| 724 |
+
|
| 725 |
+
def plot_constellation(symbols: np.ndarray, title: str = "Constellation", save_path: Optional[str] = None):
|
| 726 |
+
"""Plot constellation diagram"""
|
| 727 |
+
if not HAS_MATPLOTLIB:
|
| 728 |
+
logger.warning("Matplotlib not available, skipping constellation plot")
|
| 729 |
+
return
|
| 730 |
+
|
| 731 |
+
plt.figure(figsize=(8, 8))
|
| 732 |
+
plt.scatter(np.real(symbols), np.imag(symbols), alpha=0.7, s=20)
|
| 733 |
+
plt.title(title)
|
| 734 |
+
plt.xlabel("In-phase (I)")
|
| 735 |
+
plt.ylabel("Quadrature (Q)")
|
| 736 |
+
plt.grid(True, alpha=0.3)
|
| 737 |
+
plt.axis('equal')
|
| 738 |
+
|
| 739 |
+
if save_path:
|
| 740 |
+
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
| 741 |
+
plt.close()
|
| 742 |
+
else:
|
| 743 |
+
plt.show()
|
| 744 |
+
|
| 745 |
+
def play_audio(x: np.ndarray, sr: int):
|
| 746 |
+
"""Play audio through soundcard"""
|
| 747 |
+
if not HAS_AUDIO:
|
| 748 |
+
logger.warning("sounddevice not installed; cannot play audio")
|
| 749 |
+
return
|
| 750 |
+
|
| 751 |
+
try:
|
| 752 |
+
sd.play(x, sr)
|
| 753 |
+
sd.wait()
|
| 754 |
+
except Exception as e:
|
| 755 |
+
logger.error(f"Audio playback failed: {e}")
|
| 756 |
+
|
| 757 |
+
# =========================================================
|
| 758 |
+
# Complete Processing Pipeline
|
| 759 |
+
# =========================================================
|
| 760 |
+
|
| 761 |
+
def full_process_and_save(
|
| 762 |
+
text: str,
|
| 763 |
+
outdir: Path,
|
| 764 |
+
scheme: ModulationScheme,
|
| 765 |
+
mcfg: ModConfig,
|
| 766 |
+
fcfg: FrameConfig,
|
| 767 |
+
sec: SecurityConfig,
|
| 768 |
+
fec_scheme: FEC,
|
| 769 |
+
want_wav: bool,
|
| 770 |
+
want_iq: bool,
|
| 771 |
+
title: str = "SignalProcessor"
|
| 772 |
+
) -> OutputPaths:
|
| 773 |
+
"""Complete processing pipeline from text to files"""
|
| 774 |
+
|
| 775 |
+
outdir.mkdir(parents=True, exist_ok=True)
|
| 776 |
+
timestamp = int(time.time())
|
| 777 |
+
base_name = f"signal_{scheme.name.lower()}_{timestamp}"
|
| 778 |
+
base_path = outdir / base_name
|
| 779 |
+
|
| 780 |
+
# Encode text to bits
|
| 781 |
+
bits = encode_text(text, fcfg, sec, fec_scheme)
|
| 782 |
+
logger.info(f"Encoded {len(text)} characters to {len(bits)} bits")
|
| 783 |
+
|
| 784 |
+
# Modulate bits to signals
|
| 785 |
+
audio, iq = bits_to_signals(bits, scheme, mcfg)
|
| 786 |
+
|
| 787 |
+
paths = OutputPaths()
|
| 788 |
+
|
| 789 |
+
# Save WAV file
|
| 790 |
+
if want_wav and audio is not None and len(audio) > 0:
|
| 791 |
+
paths.wav = base_path.with_suffix(".wav")
|
| 792 |
+
write_wav_mono(paths.wav, audio, mcfg.sample_rate)
|
| 793 |
+
logger.info(f"Saved WAV: {paths.wav}")
|
| 794 |
+
|
| 795 |
+
# Save IQ file
|
| 796 |
+
if want_iq:
|
| 797 |
+
if iq is None and audio is not None:
|
| 798 |
+
# Generate IQ from audio using Hilbert transform
|
| 799 |
+
try:
|
| 800 |
+
analytic = sp_signal.hilbert(audio)
|
| 801 |
+
iq = analytic.astype(np.complex64)
|
| 802 |
+
except Exception as e:
|
| 803 |
+
logger.warning(f"Failed to generate IQ from audio: {e}")
|
| 804 |
+
iq = audio.astype(np.float32) + 1j * np.zeros_like(audio, dtype=np.float32)
|
| 805 |
+
|
| 806 |
+
if iq is not None:
|
| 807 |
+
paths.iq = base_path.with_suffix(".iqf32")
|
| 808 |
+
write_iq_f32(paths.iq, iq)
|
| 809 |
+
logger.info(f"Saved IQ: {paths.iq}")
|
| 810 |
+
|
| 811 |
+
# Generate visualization
|
| 812 |
+
if audio is not None and len(audio) > 0:
|
| 813 |
+
paths.png = base_path.with_suffix(".png")
|
| 814 |
+
plot_wave_and_spectrum(paths.png, audio, mcfg.sample_rate, title)
|
| 815 |
+
logger.info(f"Saved plot: {paths.png}")
|
| 816 |
+
|
| 817 |
+
# Save metadata
|
| 818 |
+
metadata = {
|
| 819 |
+
"timestamp": timestamp,
|
| 820 |
+
"scheme": scheme.name,
|
| 821 |
+
"sample_rate": mcfg.sample_rate,
|
| 822 |
+
"symbol_rate": mcfg.symbol_rate,
|
| 823 |
+
"duration_sec": len(audio) / mcfg.sample_rate if audio is not None else 0,
|
| 824 |
+
"fec": fec_scheme.name,
|
| 825 |
+
"encrypted": bool(sec.password),
|
| 826 |
+
"watermark": bool(sec.watermark),
|
| 827 |
+
"hmac": bool(sec.hmac_key),
|
| 828 |
+
"text_length": len(text),
|
| 829 |
+
"bits_length": len(bits)
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
paths.meta = base_path.with_suffix(".json")
|
| 833 |
+
paths.meta.write_text(safe_json(metadata), encoding="utf-8")
|
| 834 |
+
logger.info(f"Saved metadata: {paths.meta}")
|
| 835 |
+
|
| 836 |
+
return paths
|
| 837 |
+
|
| 838 |
+
def demo_signal_processing():
|
| 839 |
+
"""Demonstration of signal processing capabilities"""
|
| 840 |
+
|
| 841 |
+
# Test configuration
|
| 842 |
+
text = "Hello, World! This is a test of the signal processing system. 🚀"
|
| 843 |
+
|
| 844 |
+
schemes_to_test = [
|
| 845 |
+
ModulationScheme.BFSK,
|
| 846 |
+
ModulationScheme.QPSK,
|
| 847 |
+
ModulationScheme.QAM16,
|
| 848 |
+
ModulationScheme.OFDM
|
| 849 |
+
]
|
| 850 |
+
|
| 851 |
+
mcfg = ModConfig(sample_rate=48000, symbol_rate=1200)
|
| 852 |
+
fcfg = FrameConfig()
|
| 853 |
+
sec = SecurityConfig(watermark="test_watermark")
|
| 854 |
+
fec_scheme = FEC.HAMMING74
|
| 855 |
+
|
| 856 |
+
results = []
|
| 857 |
+
|
| 858 |
+
for scheme in schemes_to_test:
|
| 859 |
+
logger.info(f"Testing {scheme.name}...")
|
| 860 |
+
|
| 861 |
+
try:
|
| 862 |
+
paths = full_process_and_save(
|
| 863 |
+
text=text,
|
| 864 |
+
outdir=Path("demo_output"),
|
| 865 |
+
scheme=scheme,
|
| 866 |
+
mcfg=mcfg,
|
| 867 |
+
fcfg=fcfg,
|
| 868 |
+
sec=sec,
|
| 869 |
+
fec_scheme=fec_scheme,
|
| 870 |
+
want_wav=True,
|
| 871 |
+
want_iq=True,
|
| 872 |
+
title=f"{scheme.name} Demo"
|
| 873 |
+
)
|
| 874 |
+
|
| 875 |
+
results.append({
|
| 876 |
+
"scheme": scheme.name,
|
| 877 |
+
"success": True,
|
| 878 |
+
"paths": paths
|
| 879 |
+
})
|
| 880 |
+
|
| 881 |
+
except Exception as e:
|
| 882 |
+
logger.error(f"Failed to process {scheme.name}: {e}")
|
| 883 |
+
results.append({
|
| 884 |
+
"scheme": scheme.name,
|
| 885 |
+
"success": False,
|
| 886 |
+
"error": str(e)
|
| 887 |
+
})
|
| 888 |
+
|
| 889 |
+
# Print summary
|
| 890 |
+
logger.info("=== Signal Processing Demo Complete ===")
|
| 891 |
+
for result in results:
|
| 892 |
+
status = "✓" if result["success"] else "✗"
|
| 893 |
+
logger.info(f"{status} {result['scheme']}")
|
| 894 |
+
|
| 895 |
+
return results
|
| 896 |
+
|
| 897 |
+
if __name__ == "__main__":
|
| 898 |
+
demo_signal_processing()
|