|
|
# -----------------------
|
|
|
# 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()
|