FreeDATA/freedata_server/norm/norm_transmission.py

174 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# base class for norm transmission
import structlog
import time
from enum import IntEnum
import hashlib
from freedata_server.codec2 import FREEDV_MODE
from freedata_server.data_frame_factory import DataFrameFactory
class NORMMsgType(IntEnum):
UNDEFINED = 0
MESSAGE = 1 # Generic text/data message
POSITION = 2 # GPS or grid locator info
SITREP = 3 # Situation report
METAR = 4 # METAR Message
class NORMMsgPriority(IntEnum):
LOW = 0
NORMAL = 1
HIGH = 2
CRITICAL = 3
ALERT = 4
EMERGENCY = 5
class NormTransmission:
def __init__(self, ctx):
self.logger = structlog.get_logger(type(self).__name__)
self.ctx = ctx
if not self.ctx.config_manager.config["EXP"]["enable_groupchat"]:
return
self.origin = None
self.domain = None
self.frame_factory = DataFrameFactory(self.ctx)
def log(self, message, isWarning=False):
"""Logs a message with session context.
Logs a message, including the class name, session ID, and current state,
using the appropriate log level (warning or info).
Args:
message: The message to be logged.
isWarning: A boolean indicating whether the message should be logged as a warning.
"""
msg = f"[{type(self).__name__}][origin={self.origin},domain={self.domain}][state={self.state.name}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def set_state(self, state):
self.last_state_change_timestamp = time.time()
if self.state == state:
self.log(f"{type(self).__name__} state {self.state.name} unchanged.")
else:
self.log(
f"{type(self).__name__} state change from {self.state.name} to {state.name} at {self.last_state_change_timestamp}"
)
self.state = state
def on_frame_received(self, frame):
"""Handles received frames based on the current session state.
This method processes incoming frames, triggering state transitions and
data handling based on the frame type and current session state.
It logs received frame types and ignores unknown state transitions.
Args:
frame: The received frame.
"""
self.event_frame_received.set()
self.log(f"Received {frame['frame_type']}")
frame_type = frame["frame_type_int"]
if self.state in self.STATE_TRANSITION and frame_type in self.STATE_TRANSITION[self.state]:
action_name = self.STATE_TRANSITION[self.state][frame_type]
received_data, type_byte = getattr(self, action_name)(frame)
if isinstance(received_data, bytearray) and isinstance(type_byte, int):
self.arq_data_type_handler.dispatch(
type_byte, received_data, self.update_histograms(len(received_data), len(received_data))
)
return
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
def encode_flags(self, msg_type, priority, is_last):
"""
Encodes message type, priority and 'last burst' flag into a single byte.
Supports msg_type as Enum or string (e.g., "MESSAGE").
"""
# Convert msg_type
if isinstance(msg_type, str):
try:
msg_type_enum = NORMMsgType[msg_type.upper()]
except KeyError:
raise ValueError(f"Invalid msg_type string: '{msg_type}'")
elif isinstance(msg_type, NORMMsgType):
msg_type_enum = msg_type
else:
raise TypeError("msg_type must be NORMMsgType or str")
msg_type_int = int(msg_type_enum)
# Convert priority
if isinstance(priority, IntEnum):
priority_int = int(priority)
else:
priority_int = int(priority)
assert 0 <= msg_type_int <= 15, "msg_type must be 015"
assert 0 <= priority_int <= 7, "priority must be 07"
return ((1 if is_last else 0) << 7) | ((msg_type_int & 0x0F) << 3) | (priority_int & 0x07)
def decode_flags(self, flags):
"""
Decodes a flags byte into (is_last, msg_type, priority).
Bit layout:
Bit 7 → is_last
Bits 63 → msg_type (015)
Bits 20 → priority (07)
Returns:
is_last (bool),
msg_type (NORMMsgType),
priority (int)
"""
is_last = bool((flags >> 7) & 0x01)
msg_type_int = (flags >> 3) & 0x0F
priority = flags & 0x07
try:
msg_type = NORMMsgType(msg_type_int)
except ValueError:
msg_type = NORMMsgType.UNDEFINED # Fallback bei unbekanntem Typ
return is_last, msg_type, priority
def encode_burst_info(self, burst_number, total_bursts):
return ((burst_number & 0x0F) << 4) | (total_bursts & 0x0F)
def decode_burst_info(self, burst_info):
burst_number = (burst_info >> 4) & 0x0F
burst_total = burst_info & 0x0F
return burst_number, burst_total
def create_broadcast_id(self, timestamp: int, domain: str, checksum: str, length: int = 10) -> str:
"""
Creates a deterministic broadcast ID using BLAKE2b.
Args:
timestamp (int): UNIX timestamp (int).
domain (str): Domain/context string.
checksum (str): Checksum string.
length (int): Number of hex characters in result (max 128).
Returns:
str: Broadcast ID, e.g., 'bc_8d12fa3b4e'
"""
base_str = f"{timestamp}:{domain}:{checksum}".encode("utf-8")
digest = hashlib.blake2b(base_str, digest_size=length // 2).hexdigest() # 1 hex char = 4 bits
return f"{digest}"
def create_and_transmit_nack_burst(self, origin, id, burst_numbers):
burst = self.frame_factory.build_norm_nack(origin, id, burst_numbers)
self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, burst)