Lab037: evaluate control repetition on lossy link
This commit is contained in:
145
protocol/control_repetition.py
Normal file
145
protocol/control_repetition.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""Scheduling metadata and receiver de-duplication for command copies.
|
||||
|
||||
Lab037 deliberately keeps the Lab033 :class:`~protocol.link_packet.LinkPacket`
|
||||
wire representation unchanged. A repeated command is therefore the exact
|
||||
same encoded packet; only ``available_time_us`` and ``copy_index`` live in the
|
||||
local scheduler metadata defined here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Iterable
|
||||
|
||||
from protocol.link_packet import LinkPacket, TrafficClass
|
||||
|
||||
|
||||
class RepetitionMode(IntEnum):
|
||||
"""Command protection alternatives compared by Lab037."""
|
||||
|
||||
NONE = 1
|
||||
EMERGENCY_ONLY = 2
|
||||
EMERGENCY_AND_CONTROL = 3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandCopy:
|
||||
"""One scheduling instance of an otherwise unchanged link packet."""
|
||||
|
||||
packet: LinkPacket
|
||||
available_time_us: int
|
||||
copy_index: int
|
||||
arrival_order: int
|
||||
|
||||
@property
|
||||
def is_repeat(self) -> bool:
|
||||
return self.copy_index > 0
|
||||
|
||||
@property
|
||||
def command_key(self) -> tuple[int, int]:
|
||||
return self.packet.stream_id, self.packet.sequence_number
|
||||
|
||||
|
||||
def copy_offsets_ms(
|
||||
traffic_class: TrafficClass,
|
||||
mode: RepetitionMode,
|
||||
control_repeat_delay_ms: float = 22.5,
|
||||
) -> tuple[float, ...]:
|
||||
"""Return copy offsets for a command class and protection mode."""
|
||||
|
||||
traffic_class = TrafficClass(traffic_class)
|
||||
mode = RepetitionMode(mode)
|
||||
if traffic_class is TrafficClass.EMERGENCY:
|
||||
return (0.0, 15.0, 30.0) if mode >= RepetitionMode.EMERGENCY_ONLY else (0.0,)
|
||||
if traffic_class is TrafficClass.CONTROL:
|
||||
return (0.0, control_repeat_delay_ms) if mode is RepetitionMode.EMERGENCY_AND_CONTROL else (0.0,)
|
||||
raise ValueError("repetition is defined only for emergency and control commands")
|
||||
|
||||
|
||||
def build_command_copies(
|
||||
packets: Iterable[LinkPacket],
|
||||
mode: RepetitionMode,
|
||||
*,
|
||||
first_arrival_order: int = 0,
|
||||
control_repeat_delay_ms: float = 22.5,
|
||||
) -> tuple[CommandCopy, ...]:
|
||||
"""Expand original commands into stable, time-ordered scheduler copies."""
|
||||
|
||||
copies: list[CommandCopy] = []
|
||||
order = first_arrival_order
|
||||
for packet in packets:
|
||||
offsets = copy_offsets_ms(packet.traffic_class, mode, control_repeat_delay_ms)
|
||||
for copy_index, offset_ms in enumerate(offsets):
|
||||
copies.append(
|
||||
CommandCopy(
|
||||
packet=packet,
|
||||
available_time_us=packet.generation_time_us + int(round(offset_ms * 1000.0)),
|
||||
copy_index=copy_index,
|
||||
arrival_order=order,
|
||||
)
|
||||
)
|
||||
order += 1
|
||||
return tuple(
|
||||
sorted(
|
||||
copies,
|
||||
key=lambda item: (
|
||||
item.available_time_us,
|
||||
int(item.packet.traffic_class),
|
||||
item.arrival_order,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def cancel_superseded_control_copies(
|
||||
ready: Iterable[CommandCopy],
|
||||
newest: CommandCopy,
|
||||
) -> tuple[tuple[CommandCopy, ...], tuple[CommandCopy, ...]]:
|
||||
"""Remove only queued repeats belonging to older state commands.
|
||||
|
||||
A packet already selected for transmission is intentionally outside this
|
||||
function, which makes packet serialization non-preemptive by construction.
|
||||
Primary copies and emergency commands are never removed here.
|
||||
"""
|
||||
|
||||
if newest.packet.traffic_class is not TrafficClass.CONTROL:
|
||||
return tuple(ready), ()
|
||||
retained: list[CommandCopy] = []
|
||||
cancelled: list[CommandCopy] = []
|
||||
for item in ready:
|
||||
if (
|
||||
item.packet.traffic_class is TrafficClass.CONTROL
|
||||
and item.packet.stream_id == newest.packet.stream_id
|
||||
and item.packet.sequence_number < newest.packet.sequence_number
|
||||
and item.is_repeat
|
||||
):
|
||||
cancelled.append(item)
|
||||
else:
|
||||
retained.append(item)
|
||||
return tuple(retained), tuple(cancelled)
|
||||
|
||||
|
||||
class CommandReceiver:
|
||||
"""Apply only strictly newer sequence numbers independently per stream."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._last_sequence: dict[int, int] = {}
|
||||
self.accepted = 0
|
||||
self.suppressed = 0
|
||||
|
||||
def accept(self, packet: LinkPacket) -> bool:
|
||||
"""Return ``True`` only when this command advances its stream state."""
|
||||
|
||||
if packet.traffic_class not in (TrafficClass.EMERGENCY, TrafficClass.CONTROL):
|
||||
raise ValueError("CommandReceiver accepts command traffic only")
|
||||
previous = self._last_sequence.get(packet.stream_id, -1)
|
||||
if packet.sequence_number <= previous:
|
||||
self.suppressed += 1
|
||||
return False
|
||||
self._last_sequence[packet.stream_id] = packet.sequence_number
|
||||
self.accepted += 1
|
||||
return True
|
||||
|
||||
def last_sequence(self, stream_id: int) -> int | None:
|
||||
return self._last_sequence.get(stream_id)
|
||||
Reference in New Issue
Block a user