You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

243 lines
8.1 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden 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.

# -----------------------
# File: components/uart/pgkomm2.py
#
# PGKomm2 framing + send logic (NON-BLOCKING)
# -------------------------------------------
# Frame format:
# [0xDD][0x22][ADR1][ADR2][LEN][DATA...][BCC]
# Total length = 6 + LEN (LEN at index 4)
#
# Goals:
# - Zero blocking: no sleep, no flush, no read timeouts.
# - Parse using a rolling buffer fed by ser.in_waiting.
# - Stop as soon as a COMPLETE frame is available.
# - Prefer HP; fall back to the last complete frame (often echo PH).
# - One concise log line per exchange: "→ TX ← RX"
# -----------------------
from __future__ import annotations
from components.scheduler.timebase import now_ns
from components.scheduler.coordinator import claim_uart_capture, release_uart_capture
import config.config as config
MAGIC = 0xDD
INVMAGIC = 0x22
ADR_PH = (0x50, 0x48) # 'P','H' (typical TX echo)
ADR_HP = (0x48, 0x50) # 'H','P' (slave reply we want)
# ---------------------------
# Utils
# ---------------------------
def fmt_hex_ascii(data: bytes) -> str:
"""HEX + ASCII, but hide leading DD 22 if present (constant PGKomm2 header)."""
hex_part = data.hex(" ").upper()
ascii_part = "".join(chr(b) if 32 <= b <= 126 else "." for b in data)
return f"{hex_part} | '{ascii_part}'"
def view_hex_no_magic(data: bytes) -> str:
"""HEX view, but skip the leading DD 22 if present."""
return data.hex(" ").upper()
def view_ascii_no_magic(data: bytes) -> str:
"""ASCII view, but skip the leading DD 22 if present."""
if len(data) >= 2 and data[0] == MAGIC and data[1] == INVMAGIC:
data = data[2:]
return "".join(chr(b) if 32 <= b <= 126 else "." for b in data)
def _is_ph_echo(frame: bytes) -> bool:
return len(frame) >= 6 and frame[2] == ADR_PH[0] and frame[3] == ADR_PH[1]
def _is_hp_reply(frame: bytes) -> bool:
return len(frame) >= 6 and frame[2] == ADR_HP[0] and frame[3] == ADR_HP[1]
# ---------------------------
# Non-blocking frame parser
# ---------------------------
def _extract_one_frame_from(buf: bytearray) -> bytes | None:
"""
Non-blocking parser: try to cut ONE complete PGKomm2 frame out of 'buf'.
If a full frame is found, it is REMOVED from 'buf' and returned (bytes).
If not enough bytes yet, returns None and leaves 'buf' as-is.
"""
# Hunt for header DD 22
i = 0
blen = len(buf)
while i + 1 < blen:
if buf[i] == MAGIC and buf[i + 1] == INVMAGIC:
# Have header; check if we have at least up to LEN
if i + 5 >= blen:
# Need more bytes for ADR1/ADR2/LEN
break
adr1 = buf[i + 2]
adr2 = buf[i + 3]
length = buf[i + 4]
total = 6 + length # full frame size
if i + total <= blen:
# We have the whole frame
frame = bytes(buf[i : i + total])
# Drop consumed bytes from buffer
del buf[: i + total]
return frame
else:
# Header found but incomplete body — wait for more bytes
break
else:
i += 1
# If we advanced i without finding a header, drop garbage to i to avoid re-scanning
if i > 0 and (
i >= blen or not (blen >= 2 and buf[0] == MAGIC and buf[1] == INVMAGIC)
):
del buf[:i]
return None
# ---------------------------
# Public API (non-blocking)
# ---------------------------
# --- replace your current send_pgkomm2 with this version ---
def send_pgkomm2(ser, hex_command: str, log, capture_max_ms: int = 15):
"""
Non-blocking PGKomm2 TX/RX with the exact log style requested:
[..] → <TX_HEX> | '<TX_ASCII>'
[..] ← <ECHO_HEX> - <REPLY_HEX> | '<REPLY_ASCII>'
or, if no reply:
[..] ← <ECHO_HEX> | '<ECHO_ASCII>'
- Uses a rolling buffer + LEN-based framing (no blocking).
- Prefers HP; falls back to echo if no reply before the guard ends.
"""
# Basic checks & parse TX
if not config.DEBUG_MODE:
if not ser or not ser.is_open:
log("error", "⚠️ Not connected.")
return
try:
tx = bytes.fromhex(hex_command.strip())
except ValueError:
log("error", "❌ Invalid hex string format.")
return
if not (
len(tx) >= 6 and tx[0] == MAGIC and tx[1] == INVMAGIC and len(tx) == 6 + tx[4]
):
log("warning", " TX length pattern mismatch (expected 6+LEN). Sending anyway.")
# Exclusive capture window
deadline_ns = now_ns() + int(capture_max_ms) * 1_000_000
if not claim_uart_capture(deadline_ns):
log("warning", "⛔ Busy: could not acquire capture window.")
return
old_timeout = None
try:
# TX (non-blocking; no flush, no sleep)
if not config.DEBUG_MODE:
try:
ser.reset_input_buffer()
except Exception:
pass
ser.write(tx)
# Line 1: TX view (hide magic in both hex + ascii)
tx_hex = view_hex_no_magic(tx)
tx_ascii = view_ascii_no_magic(tx)
log("success", f"{tx_hex} | '{tx_ascii}'")
# DEBUG path: fabricate echo + reply
if config.DEBUG_MODE:
echo = tx
reply = bytes.fromhex("DD 22 48 50 04 70 67 33 31 09")
# Line 2 (with reply): ECHO_HEX - REPLY_HEX | 'REPLY_ASCII'
line_left = view_hex_no_magic(echo)
line_right = view_hex_no_magic(reply)
right_ascii = view_ascii_no_magic(reply)
log("info", f"{line_left} - {line_right} | '{right_ascii}'")
return reply
# Non-blocking receive
old_timeout = ser.timeout
ser.timeout = 0 # non-blocking read()
rx_buf = bytearray()
echo_frame: bytes | None = None
reply_frame: bytes | None = None
while now_ns() < deadline_ns:
n = ser.in_waiting or 0
if n:
rx_buf += ser.read(n)
# Try to cut out complete frames as they arrive
while True:
frame = _extract_one_frame_from(rx_buf)
if frame is None:
break
# First complete frame we see is typically PH echo
if echo_frame is None and _is_ph_echo(frame):
echo_frame = frame
continue
# Prefer HP and stop looking once we have it
if _is_hp_reply(frame):
reply_frame = frame
break
# If it's neither PH nor HP and we have no echo yet, use it as "echo-like"
if echo_frame is None:
echo_frame = frame
if reply_frame is not None:
break # we have the answer; stop immediately
# If nothing waiting, just spin (no sleeps)
# Line 2: compose exactly like your examples
if reply_frame is not None:
# With answer: show echo hex (if any) then reply hex, ASCII only for reply
left_hex = view_hex_no_magic(echo_frame) if echo_frame else ""
right_hex = view_hex_no_magic(reply_frame)
right_ascii = view_ascii_no_magic(reply_frame)
if left_hex:
log("info", f"{left_hex} - {right_hex} | '{right_ascii}'")
else:
log("info", f"{right_hex} | '{right_ascii}'")
return reply_frame
if echo_frame is not None:
# Only echo: show echo hex and its ASCII
left_hex = view_hex_no_magic(echo_frame)
left_ascii = view_ascii_no_magic(echo_frame)
log("info", f"{left_hex} | '{left_ascii}'")
return echo_frame
# Nothing at all
log("info", "← <no response>")
return
except Exception as e:
log("error", f"❌ UART send (PGKomm2) error: {e}")
finally:
try:
if old_timeout is not None and ser:
ser.timeout = old_timeout
except Exception:
pass
release_uart_capture()