85 lines
3.4 KiB
Python
85 lines
3.4 KiB
Python
"""Расчётная длительность пакета VIDEO-002; PHY и радиоприёмник не меняются."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import asdict, dataclass
|
||
import math
|
||
|
||
from protocol.video_stream_transport import HEADER_SIZE
|
||
|
||
|
||
PROTOCOL_OVERHEAD_BYTES = 12 # Заголовок protocol.packet и CRC-32.
|
||
PREAMBLE_SYMBOLS = 1280
|
||
PHY_HEADER_SYMBOLS = 144
|
||
TRANSIENT_SYMBOLS = 42 # Два защитных интервала и хвост RRC.
|
||
FEC_TAIL_BITS = 6
|
||
RRC_ROLLOFF = 0.35
|
||
CURRENT_MAX_PACKET_BYTES = 1024
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class RadioEstimate:
|
||
modulation: str
|
||
symbols_per_second: int
|
||
phy_payload_bytes: int
|
||
gap_ms: float
|
||
video_bytes_per_packet: int
|
||
packet_bytes_before_fec: int
|
||
coded_body_bits: int
|
||
airtime_ms: float
|
||
useful_video_bps: float
|
||
overhead_fraction: float
|
||
raw_bps: int
|
||
occupied_bandwidth_hz: float
|
||
symbol_duration_us: float
|
||
within_current_packet_limit: bool
|
||
implemented_in_current_phy: bool
|
||
|
||
def as_dict(self) -> dict:
|
||
return asdict(self)
|
||
|
||
|
||
def estimate(modulation: str, symbols_per_second: int,
|
||
phy_payload_bytes: int, gap_ms: float) -> RadioEstimate:
|
||
"""QPSK: преамбула/PHY-заголовок BPSK, тело FEC по два бита на символ."""
|
||
if modulation not in ("BPSK", "QPSK") or symbols_per_second <= 0:
|
||
raise ValueError("Неизвестная модуляция или символьная скорость")
|
||
if phy_payload_bytes <= HEADER_SIZE or gap_ms < 0:
|
||
raise ValueError("Недопустимые нагрузка или пауза")
|
||
packet_bytes = phy_payload_bytes + PROTOCOL_OVERHEAD_BYTES
|
||
coded_bits = 2 * (packet_bytes * 8 + FEC_TAIL_BITS)
|
||
bits_per_symbol = 1 if modulation == "BPSK" else 2
|
||
body_symbols = math.ceil(coded_bits / bits_per_symbol)
|
||
symbols = PREAMBLE_SYMBOLS + PHY_HEADER_SYMBOLS + body_symbols + TRANSIENT_SYMBOLS
|
||
airtime_ms = 1000 * symbols / symbols_per_second
|
||
video_bytes = phy_payload_bytes - HEADER_SIZE
|
||
useful_bps = video_bytes * 8000 / (airtime_ms + gap_ms)
|
||
# Вся ёмкость интервала, кроме байтов изображения, относится к накладным расходам.
|
||
overhead = 1 - useful_bps / (symbols_per_second * bits_per_symbol)
|
||
return RadioEstimate(
|
||
modulation, symbols_per_second, phy_payload_bytes, gap_ms,
|
||
video_bytes, packet_bytes, coded_bits, airtime_ms, useful_bps,
|
||
overhead, symbols_per_second * bits_per_symbol,
|
||
symbols_per_second * (1 + RRC_ROLLOFF),
|
||
1_000_000 / symbols_per_second,
|
||
packet_bytes <= CURRENT_MAX_PACKET_BYTES,
|
||
modulation == "BPSK" and symbols_per_second == 500_000
|
||
and phy_payload_bytes == 256 and gap_ms == 5,
|
||
)
|
||
|
||
|
||
def matrix() -> list[RadioEstimate]:
|
||
return [estimate(modulation, speed, payload, gap)
|
||
for modulation, speed in (("BPSK", 500_000),
|
||
("QPSK", 250_000),
|
||
("QPSK", 500_000))
|
||
for payload in (256, 512, 1024)
|
||
for gap in (5, 2, 1, 0)]
|
||
|
||
|
||
def margin_fits(rate_bps: float, peak_bps: float, margin: float) -> bool:
|
||
"""Запас 20%: предельная скорость >= пик * 1.2."""
|
||
if not 0 <= margin <= 1:
|
||
raise ValueError("Запас вне диапазона 0–1")
|
||
return rate_bps >= peak_bps * (1 + margin)
|