add new sfp_transport

This commit is contained in:
VALLONGOL 2025-10-16 09:59:59 +02:00
parent 754d67b914
commit 075a4fda1c
5 changed files with 708 additions and 1 deletions

View File

@ -3,7 +3,7 @@
"scan_limit": 60,
"max_range": 100,
"geometry": "1200x1024+463+195",
"last_selected_scenario": "scenario2",
"last_selected_scenario": "scenario_9g",
"connection": {
"target": {
"type": "tftp",
@ -316,6 +316,63 @@
"use_spline": true
}
]
},
"scenario_9g": {
"name": "scenario2",
"targets": [
{
"target_id": 2,
"active": true,
"traceable": true,
"trajectory": [
{
"maneuver_type": "Fly to Point",
"duration_s": 1.0,
"target_range_nm": 28.0,
"target_azimuth_deg": 0.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 180.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 300.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 506.343,
"target_heading_deg": 180.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Dynamic Maneuver",
"duration_s": 9.0,
"maneuver_speed_fps": 1519.0289999999995,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 9.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
},
{
"maneuver_type": "Fly for Duration",
"duration_s": 100.0,
"target_altitude_ft": 10000.0,
"target_velocity_fps": 1012.686,
"target_heading_deg": -90.0,
"longitudinal_acceleration_g": 0.0,
"lateral_acceleration_g": 0.0,
"vertical_acceleration_g": 0.0,
"turn_direction": "Right"
}
],
"use_spline": false
}
]
}
}
}

View File

@ -0,0 +1,90 @@
# core/payload_router.py
"""
Defines the PayloadRouter class, responsible for directing incoming,
reassembled SFP payloads to the correct application-level handler.
"""
import logging
import json
from typing import Dict, Callable
# Definisci il tipo per un handler, se vuoi essere più esplicito
PayloadHandler = Callable[[bytearray], None]
class PayloadRouter:
"""
Inspects SFP flow IDs and routes payloads to registered handlers.
Also contains the implementations for each specific payload handler.
"""
def __init__(self):
self._log_prefix = "[PayloadRouter]"
logging.info(f"{self._log_prefix} Initializing...")
def get_handlers(self) -> Dict[int, PayloadHandler]:
"""
Returns a dictionary mapping SFP_FLOW IDs to their handler methods.
This is the primary method used to connect this router to the transport layer.
Returns:
Dict[int, PayloadHandler]: The dictionary of flow IDs and handlers.
"""
handlers = {
ord('M'): self.handle_mfd_image_payload,
ord('S'): self.handle_sar_image_payload,
ord('B'): self.handle_binary_data_payload,
ord('J'): self.handle_json_payload,
# Aggiungi qui nuovi flussi e handler
}
logging.debug(f"{self._log_prefix} Providing handlers for flows: {[chr(k) if 32<=k<=126 else k for k in handlers.keys()]}")
return handlers
# --- Handler Implementations ---
def handle_mfd_image_payload(self, payload: bytearray):
"""Handler for MFD image payloads."""
log_prefix = f"{self._log_prefix} MFD"
logging.info(f"{log_prefix} Received payload. Size: {len(payload)} bytes. Processing...")
# Qui la logica per interpretare il payload dell'immagine MFD
# Ad esempio, potresti usare ctypes per mappare una struttura dati:
# from .sfp_structures import ImageLeaderData
# image_leader = ImageLeaderData.from_buffer(payload)
# logging.debug(f"{log_prefix} Frame counter: {image_leader.HEADER_DATA.FCOUNTER}")
pass
def handle_sar_image_payload(self, payload: bytearray):
"""Handler for SAR image payloads."""
log_prefix = f"{self._log_prefix} SAR"
logging.info(f"{log_prefix} Received payload. Size: {len(payload)} bytes. Processing...")
# Qui la logica per interpretare il payload dell'immagine SAR
pass
def handle_binary_data_payload(self, payload: bytearray):
"""Handler for generic binary data payloads."""
log_prefix = f"{self._log_prefix} BIN"
logging.info(f"{log_prefix} Received payload. Size: {len(payload)} bytes.")
try:
# Esempio: leggi un ID (2 byte), una lunghezza (4 byte) e dei dati
if len(payload) >= 6:
message_id = int.from_bytes(payload[0:2], 'little')
data_length = int.from_bytes(payload[2:6], 'little')
actual_data = payload[6:6+data_length]
logging.info(f"{log_prefix} Parsed binary message: ID={message_id}, Length={data_length}, Data size={len(actual_data)}")
else:
logging.warning(f"{log_prefix} Payload too small for expected binary format.")
except Exception as e:
logging.error(f"{log_prefix} Error parsing binary payload: {e}")
def handle_json_payload(self, payload: bytearray):
"""Handler for JSON payloads."""
log_prefix = f"{self._log_prefix} JSON"
logging.info(f"{log_prefix} Received payload. Size: {len(payload)} bytes.")
try:
json_string = payload.decode('utf-8')
data = json.loads(json_string)
message_type = data.get('type', 'Unknown')
logging.info(f"{log_prefix} Decoded JSON message of type '{message_type}'.")
# Qui la logica per processare l'oggetto JSON
# if message_type == 'status_update':
# handle_status_update(data)
except (UnicodeDecodeError, json.JSONDecodeError) as e:
logging.error(f"{log_prefix} Error decoding JSON payload: {e}")

View File

@ -0,0 +1,162 @@
# core/sfp_structures.py
"""
Defines the ctypes Structures for the Simple Fragmentation Protocol (SFP)
and the application-specific image data format.
This centralizes data structure definitions, allowing them to be shared
between the transport layer (which uses SFPHeader) and the application
layer (which interprets the image-specific structures).
"""
import ctypes
# --- SFP Transport Layer Structures ---
class SFPHeader(ctypes.Structure):
"""Structure representing the SFP header (32 bytes)."""
_pack_ = 1
_fields_ = [
("SFP_MARKER", ctypes.c_uint8),
("SFP_DIRECTION", ctypes.c_uint8),
("SFP_PROT_VER", ctypes.c_uint8),
("SFP_PT_SPARE", ctypes.c_uint8),
("SFP_TAG", ctypes.c_uint8),
("SFP_SRC", ctypes.c_uint8),
("SFP_FLOW", ctypes.c_uint8),
("SFP_TID", ctypes.c_uint8),
("SFP_FLAGS", ctypes.c_uint8),
("SFP_WIN", ctypes.c_uint8),
("SFP_ERR", ctypes.c_uint8),
("SFP_ERR_INFO", ctypes.c_uint8),
("SFP_TOTFRGAS", ctypes.c_uint16),
("SFP_FRAG", ctypes.c_uint16),
("SFP_RECTYPE", ctypes.c_uint8),
("SFP_RECSPARE", ctypes.c_uint8),
("SFP_PLDAP", ctypes.c_uint8),
("SFP_PLEXT", ctypes.c_uint8),
("SFP_RECCOUNTER", ctypes.c_uint16),
("SFP_PLSIZE", ctypes.c_uint16),
("SFP_TOTSIZE", ctypes.c_uint32),
("SFP_PLOFFSET", ctypes.c_uint32),
]
@classmethod
def size(cls):
"""Returns the size of the structure in bytes."""
return ctypes.sizeof(cls)
@staticmethod
def get_field_offset(field_name: str) -> int:
"""Returns the byte offset of a specific field within the structure."""
try:
return getattr(SFPHeader, field_name).offset
except AttributeError:
return -1
# --- Application Layer (Image Format) Structures ---
class DataTag(ctypes.Structure):
"""Structure representing a generic data tag (8 bytes)."""
_pack_ = 1
_fields_ = [
("ID", ctypes.c_uint8 * 2),
("VALID", ctypes.c_uint8),
("VERSION", ctypes.c_uint8),
("SIZE", ctypes.c_uint32),
]
@classmethod
def size(cls):
"""Returns the size of the structure in bytes."""
return ctypes.sizeof(cls)
class HeaderData(ctypes.Structure):
"""Structure representing the HEADER_DATA block (~48 bytes)."""
_pack_ = 1
_fields_ = [
("TYPE", ctypes.c_uint8),
("SUBTYPE", ctypes.c_uint8),
("LOGCOLORS", ctypes.c_uint8),
("IMG_RESERVED", ctypes.c_uint8),
("PRODINFO", ctypes.c_uint32 * 2),
("TOD", ctypes.c_uint16 * 2),
("RESERVED", ctypes.c_uint32),
("FCOUNTER", ctypes.c_uint32),
("TIMETAG", ctypes.c_uint32),
("NOMINALFRATE", ctypes.c_uint16),
("FRAMETAG", ctypes.c_uint16),
("OX", ctypes.c_uint16),
("OY", ctypes.c_uint16),
("DX", ctypes.c_uint16),
("DY", ctypes.c_uint16),
("STRIDE", ctypes.c_uint16),
("BPP", ctypes.c_uint8),
("COMP", ctypes.c_uint8),
("SPARE", ctypes.c_uint16),
("PALTYPE", ctypes.c_uint8),
("GAP", ctypes.c_uint8),
]
@classmethod
def size(cls):
"""Returns the size of the structure in bytes."""
return ctypes.sizeof(cls)
class GeoData(ctypes.Structure):
"""Structure representing the GEO_DATA block (~80 bytes)."""
_pack_ = 1
_fields_ = [
("INVMASK", ctypes.c_uint32),
("ORIENTATION", ctypes.c_float),
("LATITUDE", ctypes.c_float),
("LONGITUDE", ctypes.c_float),
("REF_X", ctypes.c_uint16),
("REF_Y", ctypes.c_uint16),
("SCALE_X", ctypes.c_float),
("SCALE_Y", ctypes.c_float),
("POI_ORIENTATION", ctypes.c_float),
("POI_LATITUDE", ctypes.c_float),
("POI_LONGITUDE", ctypes.c_float),
("POI_ALTITUDE", ctypes.c_float),
("POI_X", ctypes.c_uint16),
("POI_Y", ctypes.c_uint16),
("SPARE", ctypes.c_uint32 * 7),
]
@classmethod
def size(cls):
"""Returns the size of the structure in bytes."""
return ctypes.sizeof(cls)
class ImageLeaderData(ctypes.Structure):
"""Represents the complete Image Leader data structure (~1320 bytes)."""
_pack_ = 1
_fields_ = [
("HEADER_TAG", DataTag),
("HEADER_DATA", HeaderData),
("GEO_TAG", DataTag),
("GEO_DATA", GeoData),
("RESERVED_TAG", DataTag),
("RESERVED_DATA", ctypes.c_uint8 * 128),
("CM_TAG", DataTag),
("COLOUR_MAP", ctypes.c_uint32 * 256),
("PIXEL_TAG", DataTag),
]
@classmethod
def size(cls):
"""Returns the size of the structure in bytes."""
return ctypes.sizeof(cls)
@staticmethod
def get_reserved_data_size() -> int:
"""Returns the static size of the RESERVED_DATA field."""
return 128
@staticmethod
def get_colour_map_size() -> int:
"""Returns the static size of the COLOUR_MAP field in bytes."""
return 1024

View File

@ -0,0 +1,240 @@
# core/sfp_transport.py
"""
Provides a reusable transport layer for the Simple Fragmentation Protocol (SFP).
This module handles UDP socket communication, SFP header parsing, fragment
reassembly, and ACK generation. It is application-agnostic and uses a
callback/handler system to pass fully reassembled payloads to the
application layer based on the SFP_FLOW identifier.
"""
import socket
import logging
import threading
import time
from typing import Dict, Callable, Optional
from controlpanel import config
from controlpanel.utils.network import create_udp_socket, close_udp_socket
from controlpanel.core.sfp_structures import SFPHeader
# Define a type hint for payload handlers
PayloadHandler = Callable[[bytearray], None]
class SfpTransport:
"""Manages SFP communication and payload reassembly."""
def __init__(self, host: str, port: int, payload_handlers: Dict[int, PayloadHandler]):
"""
Initializes the SFP Transport layer.
Args:
host (str): The local IP address to bind the UDP socket to.
port (int): The local port to listen on.
payload_handlers (Dict[int, PayloadHandler]): A dictionary mapping
SFP_FLOW IDs (as integers) to their corresponding handler functions.
Each handler will be called with the complete payload (bytearray).
"""
self._log_prefix = "[SfpTransport]"
logging.info(f"{self._log_prefix} Initializing for {host}:{port}...")
self._host = host
self._port = port
self._payload_handlers = payload_handlers
self._socket: Optional[socket.socket] = None
self._receiver_thread: Optional[threading.Thread] = None
self._stop_event = threading.Event()
# Reassembly state dictionaries, managed by this transport layer
self._fragments: Dict[tuple, Dict[int, int]] = {}
self._buffers: Dict[tuple, bytearray] = {}
logging.debug(
f"{self._log_prefix} Registered handlers for flows: "
f"{[chr(k) if 32 <= k <= 126 else k for k in self._payload_handlers.keys()]}"
)
def start(self) -> bool:
"""
Starts the transport layer by creating the socket and launching the receiver thread.
Returns:
bool: True if started successfully, False otherwise.
"""
if self._receiver_thread is not None and self._receiver_thread.is_alive():
logging.warning(f"{self._log_prefix} Start called, but receiver is already running.")
return True
self._socket = create_udp_socket(self._host, self._port)
if not self._socket:
logging.critical(f"{self._log_prefix} Failed to create and bind socket. Cannot start.")
return False
self._stop_event.clear()
self._receiver_thread = threading.Thread(
target=self._receive_loop, name="SfpTransportThread", daemon=True
)
self._receiver_thread.start()
logging.info(f"{self._log_prefix} Receiver thread started.")
return True
def shutdown(self):
"""Stops the receiver thread and closes the socket."""
logging.info(f"{self._log_prefix} Shutdown initiated.")
self._stop_event.set()
# The socket is closed here to interrupt the blocking recvfrom call
if self._socket:
close_udp_socket(self._socket)
self._socket = None
if self._receiver_thread and self._receiver_thread.is_alive():
logging.debug(f"{self._log_prefix} Waiting for receiver thread to join...")
self._receiver_thread.join(timeout=2.0)
if self._receiver_thread.is_alive():
logging.warning(f"{self._log_prefix} Receiver thread did not join cleanly.")
logging.info(f"{self._log_prefix} Shutdown complete.")
def _receive_loop(self):
"""The main loop that listens for UDP packets and processes them."""
log_prefix = f"{self._log_prefix} Loop"
logging.info(f"{log_prefix} Starting receive loop.")
while not self._stop_event.is_set():
if not self._socket:
logging.error(f"{log_prefix} Socket is not available. Stopping loop.")
break
try:
data, addr = self._socket.recvfrom(65535)
if not data:
continue
except socket.timeout:
continue
except OSError:
# This is expected when the socket is closed during shutdown
if not self._stop_event.is_set():
logging.error(f"{log_prefix} Socket error.", exc_info=True)
break
except Exception:
logging.exception(f"{log_prefix} Unexpected error in recvfrom.")
time.sleep(0.01)
continue
self._process_packet(data, addr)
logging.info(f"{log_prefix} Receive loop terminated.")
def _process_packet(self, data: bytes, addr: tuple):
"""Parses an SFP packet and handles fragment reassembly."""
header_size = SFPHeader.size()
if len(data) < header_size:
logging.warning(f"Packet from {addr} is too small for SFP header. Ignoring.")
return
try:
header = SFPHeader.from_buffer_copy(data)
except (ValueError, TypeError):
logging.error(f"Failed to parse SFP header from {addr}. Ignoring.")
return
flow, tid = header.SFP_FLOW, header.SFP_TID
frag, total_frags = header.SFP_FRAG, header.SFP_TOTFRGAS
pl_size, pl_offset = header.SFP_PLSIZE, header.SFP_PLOFFSET
total_size = header.SFP_TOTSIZE
key = (flow, tid)
# Handle ACK Request
if header.SFP_FLAGS & 0x01:
self._send_ack(addr, data[:header_size])
# Validate packet metadata
if total_frags == 0 or total_frags > 60000 or total_size <= 0:
logging.warning(f"Invalid metadata for {key}: total_frags={total_frags}, total_size={total_size}. Ignoring.")
return
# Start of a new transaction
if frag == 0:
self._cleanup_lingering_transactions(flow, tid)
logging.debug(f"New transaction started for key={key}. Total size: {total_size} bytes.")
self._fragments[key] = {}
try:
self._buffers[key] = bytearray(total_size)
except (MemoryError, ValueError):
logging.error(f"Failed to allocate {total_size} bytes for key={key}. Ignoring transaction.")
self._fragments.pop(key, None)
return
# Check if we are tracking this transaction
if key not in self._buffers or key not in self._fragments:
logging.debug(f"Ignoring fragment {frag} for untracked transaction key={key}.")
return
# Store fragment info and copy payload
self._fragments[key][frag] = total_frags
payload = data[header_size:]
bytes_to_copy = min(pl_size, len(payload))
if (pl_offset + bytes_to_copy) > len(self._buffers[key]):
logging.error(f"Payload for key={key}, frag={frag} would overflow buffer. Ignoring.")
return
self._buffers[key][pl_offset : pl_offset + bytes_to_copy] = payload[:bytes_to_copy]
# Check for completion
if len(self._fragments[key]) == total_frags:
#logging.info(f"Transaction complete for key={key}. Handing off to application layer.")
# Retrieve completed buffer and clean up state for this key
completed_payload = self._buffers.pop(key)
self._fragments.pop(key)
# Find and call the appropriate handler
handler = self._payload_handlers.get(flow)
if handler:
try:
handler(completed_payload)
except Exception:
logging.exception(f"Error executing payload handler for flow {flow}.")
else:
logging.warning(f"No payload handler registered for flow ID {flow}.")
def _send_ack(self, dest_addr: tuple, original_header_bytes: bytes):
"""Sends an SFP ACK packet back to the sender."""
log_prefix = f"{self._log_prefix} ACK"
if not self._socket: return
try:
ack_header = bytearray(original_header_bytes)
flow = ack_header[SFPHeader.get_field_offset("SFP_FLOW")]
# Determine window size based on flow
window_size = 0
if flow == ord('M'): # MFD
window_size = config.ACK_WINDOW_SIZE_MFD
elif flow == ord('S'): # SAR
window_size = config.ACK_WINDOW_SIZE_SAR
# Modify header for ACK response
ack_header[SFPHeader.get_field_offset("SFP_DIRECTION")] = 0x3C # '<'
ack_header[SFPHeader.get_field_offset("SFP_WIN")] = window_size
original_flags = ack_header[SFPHeader.get_field_offset("SFP_FLAGS")]
ack_header[SFPHeader.get_field_offset("SFP_FLAGS")] = (original_flags | 0x01) & ~0x02
self._socket.sendto(ack_header, dest_addr)
logging.debug(f"{log_prefix} Sent to {dest_addr} for flow {chr(flow) if 32<=flow<=126 else flow}.")
except Exception:
logging.exception(f"{log_prefix} Failed to send to {dest_addr}.")
def _cleanup_lingering_transactions(self, current_flow: int, current_tid: int):
"""Removes old, incomplete transactions for the same flow."""
# This is a simplified cleanup. The original was more complex for stats.
keys_to_remove = [
key for key in self._fragments
if key[0] == current_flow and key[1] != current_tid
]
for key in keys_to_remove:
logging.warning(f"Cleaning up lingering/incomplete transaction for key={key}.")
self._fragments.pop(key, None)
self._buffers.pop(key, None)

View File

@ -0,0 +1,158 @@
# network.py
"""
THIS SOFTWARE IS PROVIDED AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
Handles basic UDP socket operations like creation, configuration (buffer size),
and closing for the application. Uses standardized logging prefixes.
"""
# Standard library imports
import socket
import logging
import sys # Keep sys import if needed for platform-specific checks (not currently used)
# No local application imports needed in this module typically
def create_udp_socket(local_ip, local_port):
"""
Creates a UDP socket, attempts to increase its receive buffer size,
and binds it to the specified local IP address and port.
Args:
local_ip (str): The local IP address to bind to.
local_port (int): The local port to bind to.
Returns:
socket.socket: The created and bound UDP socket, or None on error.
"""
log_prefix = "[Network]" # Standard prefix for this module
logging.debug(
f"{log_prefix} Attempting to create UDP socket for {local_ip}:{local_port}"
)
try:
# Create the UDP socket (IPv4)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
logging.debug(f"{log_prefix} Socket object created.")
# --- Receive Buffer Size Adjustment ---
try:
# Get the default buffer size (INFO level is okay for this setup detail)
default_rcvbuf = sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
logging.info(
f"{log_prefix} Default socket receive buffer size: {default_rcvbuf} bytes"
)
# Set a larger buffer size (adjust value as needed)
desired_rcvbuf = 8 * 1024 * 1024 # Request 8MB
logging.debug(
f"{log_prefix} Requesting receive buffer size: {desired_rcvbuf} bytes"
)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, desired_rcvbuf)
# Verify the actual size set by the OS (INFO level okay)
actual_rcvbuf = sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
logging.info(
f"{log_prefix} Actual receive buffer size set: {actual_rcvbuf} bytes"
)
if actual_rcvbuf < desired_rcvbuf:
# WARNING if OS limited the size
logging.warning(
f"{log_prefix} OS capped the receive buffer size lower than requested."
)
except OSError as buf_e:
# ERROR if setting buffer fails (might indicate permissions or OS limits issue)
logging.error(
f"{log_prefix} Failed to set receive buffer size: {buf_e}. Check OS limits (e.g., sysctl net.core.rmem_max)."
)
except Exception as buf_e:
# Keep EXCEPTION for unexpected errors during buffer setting
logging.exception(
f"{log_prefix} Unexpected error setting receive buffer: {buf_e}"
)
# --- Bind the Socket ---
logging.debug(f"{log_prefix} Binding socket to ('{local_ip}', {local_port})...")
sock.bind((local_ip, local_port))
# INFO for successful bind confirmation
logging.info(
f"{log_prefix} UDP socket created and bound successfully to {local_ip}:{local_port}"
)
return sock
except socket.error as e:
# ERROR for critical socket creation/binding failure
logging.error(
f"{log_prefix} Error creating/binding UDP socket for {local_ip}:{local_port}: {e}"
)
return None
except Exception as e:
# Keep EXCEPTION for any other unexpected errors
logging.exception(
f"{log_prefix} Unexpected error during UDP socket creation for {local_ip}:{local_port}: {e}"
)
return None
def receive_udp_packet(sock, buffer_size=65535):
"""
Receives a single UDP packet from the specified socket.
Note: Currently unused by the UdpReceiver class which uses sock.recvfrom directly.
Args:
sock (socket.socket): The UDP socket to receive from.
buffer_size (int): The maximum buffer size for receiving data.
Returns:
tuple: A tuple containing (data, sender_address), or (None, None) on error.
"""
log_prefix = "[Network]" # Standard prefix
# DEBUG for function call (if used)
logging.debug(
f"{log_prefix} Attempting to receive UDP packet (buffer size: {buffer_size})."
)
try:
data, addr = sock.recvfrom(buffer_size)
logging.debug(f"{log_prefix} Received {len(data)} bytes from {addr}.")
return data, addr
except socket.timeout:
# DEBUG for expected timeout (if timeout is set on socket)
logging.debug(f"{log_prefix} Socket recvfrom timeout.")
return None, None
except socket.error as e:
# ERROR for socket errors during receive
logging.error(f"{log_prefix} Error receiving UDP packet: {e}")
return None, None
except Exception as e:
# Keep EXCEPTION for unexpected errors
logging.exception(f"{log_prefix} Unexpected error receiving UDP packet: {e}")
return None, None
def close_udp_socket(sock):
"""
Closes the specified UDP socket gracefully.
Args:
sock (socket.socket): The UDP socket object to close.
"""
log_prefix = "[Network]" # Standard prefix
# Check if socket is valid before attempting close
if sock and hasattr(sock, "fileno") and sock.fileno() != -1:
try:
# INFO for significant action: closing the main socket
logging.info(f"{log_prefix} Closing UDP socket (fd={sock.fileno()})...")
sock.close()
logging.info(f"{log_prefix} UDP socket closed successfully.")
except socket.error as e:
# ERROR if closing fails
logging.error(f"{log_prefix} Error closing UDP socket: {e}")
except Exception as e:
# Keep EXCEPTION for unexpected errors
logging.exception(f"{log_prefix} Unexpected error closing UDP socket: {e}")
else:
# DEBUG if socket is already closed or invalid
logging.debug(
f"{log_prefix} Socket close requested, but socket was already closed or invalid."
)