Files
SDR-Rover/protocol/link_controller.py

447 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Устойчивый контроллер режима радиоканала SDR Rover Link.
Контроллер устраняет частое переключение режимов
из-за шума измерения Eb/N0.
Реализованы:
1. Экспоненциальное усреднение Eb/N0.
2. Гистерезис порогов.
3. Подтверждение повышения режима.
4. Минимальное время удержания режима.
5. Быстрое аварийное понижение режима.
Доступные режимы:
0 — изображения отключены;
128 — фрагменты по 128 байт;
512 — фрагменты по 512 байт;
1024 — фрагменты по 1024 байта.
"""
from dataclasses import dataclass
# ============================================================
# Допустимые режимы
# ============================================================
IMAGE_OFF = 0
FRAGMENT_MODE_128 = 128
FRAGMENT_MODE_512 = 512
FRAGMENT_MODE_1024 = 1024
VALID_MODES = (
IMAGE_OFF,
FRAGMENT_MODE_128,
FRAGMENT_MODE_512,
FRAGMENT_MODE_1024,
)
# ============================================================
# Результат одного шага контроллера
# ============================================================
@dataclass(frozen=True)
class LinkControllerResult:
"""
Состояние контроллера после нового измерения Eb/N0.
"""
step: int
measured_eb_n0_db: float
filtered_eb_n0_db: float
previous_mode: int
selected_mode: int
mode_changed: bool
pending_upgrade_mode: int | None
pending_upgrade_count: int
reason: str
# ============================================================
# Контроллер
# ============================================================
class LinkModeController:
"""
Устойчивый адаптивный контроллер режима изображения.
Пороговые значения выбраны по результатам Lab015 и Lab016.
Гистерезис означает, что повышение и понижение режима
происходят при разных значениях Eb/N0.
Пример для режима 512 байт:
повышение 128 → 512: Eb/N0 >= 8,7 дБ
понижение 512 → 128: Eb/N0 < 8,1 дБ
Благодаря разнице 0,6 дБ небольшие колебания около
порога не вызывают постоянного переключения.
"""
def __init__(
self,
initial_mode: int = IMAGE_OFF,
filter_alpha: float = 0.35,
upgrade_confirmation_count: int = 3,
minimum_hold_steps: int = 2,
) -> None:
"""
Создать контроллер.
Параметры
----------
initial_mode:
Начальный режим.
filter_alpha:
Коэффициент экспоненциального фильтра.
Большое значение:
быстрее реакция;
слабее фильтрация шума.
Малое значение:
медленнее реакция;
сильнее фильтрация.
upgrade_confirmation_count:
Сколько последовательных измерений требуется
для повышения режима.
minimum_hold_steps:
Минимальное число шагов удержания режима
перед очередным повышением.
"""
if initial_mode not in VALID_MODES:
raise ValueError(
f"Недопустимый начальный режим: {initial_mode}"
)
if not 0.0 < filter_alpha <= 1.0:
raise ValueError(
"filter_alpha должен находиться в диапазоне 0...1"
)
if upgrade_confirmation_count <= 0:
raise ValueError(
"upgrade_confirmation_count должен быть положительным"
)
if minimum_hold_steps < 0:
raise ValueError(
"minimum_hold_steps не может быть отрицательным"
)
self.current_mode = initial_mode
self.filter_alpha = filter_alpha
self.upgrade_confirmation_count = (
upgrade_confirmation_count
)
self.minimum_hold_steps = (
minimum_hold_steps
)
self.filtered_eb_n0_db: float | None = None
self.step = 0
self.steps_in_mode = 0
self.pending_upgrade_mode: int | None = None
self.pending_upgrade_count = 0
# ========================================================
# Внутренние функции
# ========================================================
def _update_filter(
self,
measured_eb_n0_db: float,
) -> float:
"""
Обновить экспоненциальное среднее.
Формула:
filtered =
alpha * measured
+ (1 - alpha) * previous_filtered
"""
if self.filtered_eb_n0_db is None:
self.filtered_eb_n0_db = measured_eb_n0_db
else:
self.filtered_eb_n0_db = (
self.filter_alpha
* measured_eb_n0_db
+ (
1.0
- self.filter_alpha
)
* self.filtered_eb_n0_db
)
return self.filtered_eb_n0_db
def _emergency_safe_mode(
self,
measured_eb_n0_db: float,
) -> int:
"""
Определить максимально безопасный режим
по мгновенному измерению.
Используется для быстрого понижения режима,
не дожидаясь усреднения.
"""
if measured_eb_n0_db < 5.8:
return IMAGE_OFF
if measured_eb_n0_db < 7.7:
return FRAGMENT_MODE_128
if measured_eb_n0_db < 8.7:
return FRAGMENT_MODE_512
return FRAGMENT_MODE_1024
def _desired_mode_from_filtered(
self,
filtered_eb_n0_db: float,
) -> int:
"""
Определить желаемый режим с учётом гистерезиса.
Повышение выполняется только на один уровень за шаг.
"""
if self.current_mode == IMAGE_OFF:
if filtered_eb_n0_db >= 6.8:
return FRAGMENT_MODE_128
return IMAGE_OFF
if self.current_mode == FRAGMENT_MODE_128:
if filtered_eb_n0_db < 6.2:
return IMAGE_OFF
if filtered_eb_n0_db >= 8.7:
return FRAGMENT_MODE_512
return FRAGMENT_MODE_128
if self.current_mode == FRAGMENT_MODE_512:
if filtered_eb_n0_db < 8.1:
return FRAGMENT_MODE_128
if filtered_eb_n0_db >= 9.8:
return FRAGMENT_MODE_1024
return FRAGMENT_MODE_512
if self.current_mode == FRAGMENT_MODE_1024:
if filtered_eb_n0_db < 9.2:
return FRAGMENT_MODE_512
return FRAGMENT_MODE_1024
raise RuntimeError(
f"Неизвестный текущий режим: {self.current_mode}"
)
def _reset_pending_upgrade(
self,
) -> None:
"""
Сбросить накопленное подтверждение повышения.
"""
self.pending_upgrade_mode = None
self.pending_upgrade_count = 0
def _change_mode(
self,
new_mode: int,
) -> None:
"""
Немедленно установить новый режим.
"""
if new_mode not in VALID_MODES:
raise ValueError(
f"Недопустимый режим: {new_mode}"
)
self.current_mode = new_mode
self.steps_in_mode = 0
self._reset_pending_upgrade()
# ========================================================
# Основной шаг
# ========================================================
def update(
self,
measured_eb_n0_db: float,
) -> LinkControllerResult:
"""
Обработать новое измерение Eb/N0.
Логика:
1. Обновить фильтр.
2. Проверить аварийное понижение.
3. Проверить обычное понижение по фильтру.
4. Для повышения потребовать несколько подтверждений.
5. Вернуть результат.
"""
measured_eb_n0_db = float(
measured_eb_n0_db
)
previous_mode = self.current_mode
filtered_eb_n0_db = self._update_filter(
measured_eb_n0_db
)
reason = "Режим удерживается"
# ----------------------------------------------------
# Аварийное быстрое понижение
# ----------------------------------------------------
emergency_mode = self._emergency_safe_mode(
measured_eb_n0_db
)
if emergency_mode < self.current_mode:
self._change_mode(
emergency_mode
)
reason = (
"Аварийное понижение по мгновенному Eb/N0"
)
else:
desired_mode = self._desired_mode_from_filtered(
filtered_eb_n0_db
)
# ------------------------------------------------
# Обычное понижение
# ------------------------------------------------
if desired_mode < self.current_mode:
self._change_mode(
desired_mode
)
reason = (
"Понижение по фильтрованному Eb/N0"
)
# ------------------------------------------------
# Возможное повышение
# ------------------------------------------------
elif desired_mode > self.current_mode:
if (
self.steps_in_mode
< self.minimum_hold_steps
):
self._reset_pending_upgrade()
reason = (
"Повышение запрещено: "
"не выдержано минимальное время режима"
)
else:
if (
self.pending_upgrade_mode
== desired_mode
):
self.pending_upgrade_count += 1
else:
self.pending_upgrade_mode = (
desired_mode
)
self.pending_upgrade_count = 1
reason = (
"Накопление подтверждений "
"для повышения режима"
)
if (
self.pending_upgrade_count
>= self.upgrade_confirmation_count
):
self._change_mode(
desired_mode
)
reason = (
"Режим повышен после "
"нескольких подтверждений"
)
else:
self._reset_pending_upgrade()
mode_changed = (
self.current_mode != previous_mode
)
result = LinkControllerResult(
step=self.step,
measured_eb_n0_db=measured_eb_n0_db,
filtered_eb_n0_db=filtered_eb_n0_db,
previous_mode=previous_mode,
selected_mode=self.current_mode,
mode_changed=mode_changed,
pending_upgrade_mode=(
self.pending_upgrade_mode
),
pending_upgrade_count=(
self.pending_upgrade_count
),
reason=reason,
)
self.step += 1
self.steps_in_mode += 1
return result