Refactor radar system

This commit is contained in:
Ayzen
2026-10-05 16:46:28 +03:00
parent 515049d2ed
commit 8f097dfa2e
179 changed files with 8212 additions and 6676 deletions
@@ -20,6 +20,8 @@ from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Union
from python_app.file_io import atomic_write_json
_PathLike = Union[str, os.PathLike[str]]
# Default IPC locations. Both are overridable via CLI/API so several runs can use
@@ -46,12 +48,7 @@ class LaserVariationSession:
Writes to a temp file in the same directory then renames, so a concurrent
checker never observes a half-written file.
"""
dest = Path(path)
dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_name(f"{dest.name}.{os.getpid()}.tmp")
tmp.write_text(json.dumps(asdict(self), indent=2), encoding="utf-8")
os.replace(tmp, dest)
return dest
return atomic_write_json(Path(path), asdict(self))
@classmethod
def load(cls, path: _PathLike = DEFAULT_SESSION_PATH) -> "LaserVariationSession":
@@ -196,6 +196,8 @@ class Protocol:
the bus. Note the lock is advisory (``flock``): every program that opens
this port must also use ``exclusive=True`` for the guarantee to hold.
"""
if self.is_connected:
return
port = self._port_name or self._detect_port()
try:
self._serial = serial.Serial(
@@ -219,9 +221,10 @@ class Protocol:
def disconnect(self) -> None:
"""Close the serial port if open."""
if self._serial and self._serial.is_open:
connection, self._serial = self._serial, None
if connection is not None and connection.is_open:
logger.debug("Closing laser serial port")
self._serial.close()
connection.close()
@property
def is_connected(self) -> bool:
@@ -246,37 +249,6 @@ class Protocol:
raise CommunicationError("Serial port is not connected")
return self._serial.read(length)
# ---- Static encoding helpers (no connection required) ---------------
@staticmethod
def flipfour(value: int) -> int:
"""Byte-swap a 16-bit integer (little-endian word swap)."""
return ((value & 0xFF) << 8) | ((value >> 8) & 0xFF)
@staticmethod
def pack_float(value: float) -> bytes:
return struct.pack('<f', value)
@staticmethod
def pack_uint16(value: int) -> bytes:
return struct.pack('<H', value)
@staticmethod
def calculate_crc(data: bytes) -> int:
"""Return the XOR CRC over all 16-bit words except word 0 and the CRC field.
The command-code word (word 0) is excluded, matching the firmware's CRC
expectation.
"""
hex_str = data.hex()
words = [hex_str[i:i+4] for i in range(0, len(hex_str), 4)]
# Word 0 (command code) is excluded from the CRC.
crc_words = words[1:]
result = int(crc_words[0], 16)
for w in crc_words[1:]:
result ^= int(w, 16)
return result
# ---- Command encoders -----------------------------------------------
@staticmethod
@@ -404,11 +376,6 @@ class Protocol:
"""Build DEFAULT_ENABLE command (0x2222) — reset device."""
return bytearray.fromhex(_flipfour(_int_to_hex4(CMD_DEFAULT_ENABLE)))
@staticmethod
def encode_remove_file() -> bytes:
"""Build REMOVE_FILE command (0x5555) — delete saved data."""
return bytearray.fromhex(_flipfour(_int_to_hex4(CMD_REMOVE_FILE)))
# ---- Response decoders -----------------------------------------------
@staticmethod
@@ -477,4 +444,4 @@ class Protocol:
@staticmethod
def state_to_description(state_hex_str: str) -> str:
"""Return human-readable description for a state hex string."""
return STATE_DESCRIPTIONS.get(state_hex_str, "Unknown or reserved error.")
return STATE_DESCRIPTIONS.get(state_hex_str, "Unknown or reserved error.")