447 lines
13 KiB
Python
447 lines
13 KiB
Python
"""
|
||
Устойчивый контроллер режима радиоканала 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
|
||
|