This commit is contained in:
awe
2026-10-19 16:12:36 +03:00
parent 5b70a9a4ce
commit 75185c7e36
3 changed files with 67 additions and 37 deletions
+2 -1
View File
@@ -5,6 +5,7 @@
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
"ms-vscode.cpptools-extension-pack",
"pioarduino.pioarduino-ide"
]
}
+62 -36
View File
@@ -21,6 +21,7 @@ handshake-кадром IDENT (роль + версия протокола). Дл
import argparse
import hmac
import logging
import select
import socket
import sys
import time
@@ -128,7 +129,13 @@ def run_tx(args, key):
log.info("tx: %s -> UDP %s:%d", ser.port, *peer)
sent = 0
while True:
for ftype, payload in parser.feed(ser.read(256)):
# read(1) блокирует до первого байта, затем забираем всё доступное —
# кадр уходит в сеть сразу, без ожидания таймаута чтения (иначе каждый
# кадр задерживался бы до таймаута порта).
data = ser.read(1)
if ser.in_waiting:
data += ser.read(ser.in_waiting)
for ftype, payload in parser.feed(data):
if ftype == lp.CONTROL:
sock.sendto(seal(payload, key), peer)
sent += 1
@@ -149,7 +156,7 @@ def run_rx(args, key):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(bind)
sock.settimeout(lp.FRAME_PERIOD_MS / 1000.0)
sock.setblocking(False)
parser = lp.FrameParser()
log.info("rx: UDP %s:%d -> %s", bind[0], bind[1], ser.port)
@@ -160,37 +167,55 @@ def run_rx(args, key):
in_failsafe = False
rejects = 0 # подряд отброшенных по seq -> признак перезапуска источника
def accept(datagram, addr):
"""Проверить пакет и обновить состояние; вернуть payload принятого
свежего кадра либо None."""
nonlocal last_seq, last_net, in_failsafe, rejects
if allowed_host and addr[0] != allowed_host:
log.warning("пакет с неожиданного адреса %s отброшен", addr[0])
return None
payload = unseal(datagram, key)
if not payload or len(payload) != lp.CONTROL_LEN:
log.warning("битый/неаутентичный пакет отброшен")
return None
c = lp.unpack_control(payload)
# Принять свежий seq ИЛИ новую сессию: источник (STM-TX) после ребута
# начинает seq заново с 0, и без сброса last_seq поток завис бы в
# failsafe навсегда. Новую сессию распознаём по пачке подряд «несвежих»
# пакетов (устойчиво к одиночным reorder/dup).
fresh = last_seq is None or seq_newer(c["seq"], last_seq)
if not fresh:
rejects += 1
if rejects >= SEQ_RESET_AFTER:
log.warning("seq откатился (%d -> %d): новая сессия", last_seq, c["seq"])
fresh = True
if not fresh:
return None # устаревший/повторный — anti-replay
rejects = 0
last_seq = c["seq"]
last_net = time.monotonic()
if in_failsafe:
log.info("связь восстановлена")
in_failsafe = False
return payload
while True:
try:
datagram, addr = sock.recvfrom(2048)
if allowed_host and addr[0] != allowed_host:
log.warning("пакет с неожиданного адреса %s отброшен", addr[0])
datagram = None
payload = unseal(datagram, key) if datagram else None
if payload and len(payload) == lp.CONTROL_LEN:
c = lp.unpack_control(payload)
# Принять свежий seq ИЛИ новую сессию: источник (STM-TX) после
# ребута начинает seq заново с 0, и без сброса last_seq поток
# завис бы в failsafe навсегда. Новую сессию распознаём по пачке
# подряд «несвежих» пакетов (устойчиво к одиночным reorder/dup).
fresh = last_seq is None or seq_newer(c["seq"], last_seq)
if not fresh:
rejects += 1
if rejects >= SEQ_RESET_AFTER:
log.warning("seq откатился (%d -> %d): новая сессия", last_seq, c["seq"])
fresh = True
if fresh:
rejects = 0
last_seq = c["seq"]
last_net = time.monotonic()
if in_failsafe:
log.info("связь восстановлена")
in_failsafe = False
ser.write(lp.build_frame(lp.CONTROL, payload))
elif datagram is not None:
log.warning("битый/неаутентичный пакет отброшен")
except socket.timeout:
pass
# Ждём пакет до 20 мс (пауза цикла), затем БЕЗ блокировки выгребаем всю
# накопившуюся очередь сокета и шлём на STM только самый свежий кадр
# (latest-wins): так задержка не растёт, даже если пакеты пришли пачкой.
newest = None
ready = select.select([sock], [], [], lp.FRAME_PERIOD_MS / 2000.0)[0]
while ready:
try:
datagram, addr = sock.recvfrom(2048)
except BlockingIOError:
break
p = accept(datagram, addr)
if p is not None:
newest = p
ready = select.select([sock], [], [], 0)[0]
if newest is not None:
ser.write(lp.build_frame(lp.CONTROL, newest))
# сетевой failsafe: тишина -> активная нейтраль на STM в темпе 20 Гц
now = time.monotonic()
@@ -203,10 +228,11 @@ def run_rx(args, key):
fs_seq = (fs_seq + 1) & 0xFFFFFFFF
last_fs = now
# дренируем входящие LOG/IDENT от STM-RX
for ftype, payload in parser.feed(ser.read(256)):
if ftype == lp.LOG:
log.info("STM-RX: %s", payload.decode("ascii", "replace"))
# неблокирующий дренаж входящих LOG от STM-RX
if ser.in_waiting:
for ftype, payload in parser.feed(ser.read(ser.in_waiting)):
if ftype == lp.LOG:
log.info("STM-RX: %s", payload.decode("ascii", "replace"))
# ---------------- общее ----------------