# ----------------------- # 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: [..] → | '' [..] ← - | '' or, if no reply: [..] ← | '' - 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", "← ") 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()