146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
"""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)
|