From 5826c4ff679dce22ad7ae9ab16d3a76404e797aa Mon Sep 17 00:00:00 2001 From: awe Date: Thu, 22 Oct 2026 12:57:33 +0300 Subject: [PATCH] deploy examples --- .../cart_firmware/deploy/README.md | 53 +++++++ .../deploy/cart-bridge-rx.env.example | 19 +++ .../deploy/cart-bridge-rx.service | 18 +++ .../deploy/cart-bridge-tx.env.example | 15 ++ .../deploy/cart-bridge-tx.service | 20 +++ .../cart_firmware/deploy/install-bridge.sh | 79 +++++++++++ .../cart_firmware/docs/protocol.md | 33 +++++ .../cart_firmware/src/link_protocol.h | 2 +- device_firmware/cart_firmware/tools/bridge.py | 129 ++++++++++++++++-- .../cart_firmware/tools/link_proto.py | 2 +- 10 files changed, 357 insertions(+), 13 deletions(-) create mode 100644 device_firmware/cart_firmware/deploy/README.md create mode 100644 device_firmware/cart_firmware/deploy/cart-bridge-rx.env.example create mode 100644 device_firmware/cart_firmware/deploy/cart-bridge-rx.service create mode 100644 device_firmware/cart_firmware/deploy/cart-bridge-tx.env.example create mode 100644 device_firmware/cart_firmware/deploy/cart-bridge-tx.service create mode 100755 device_firmware/cart_firmware/deploy/install-bridge.sh diff --git a/device_firmware/cart_firmware/deploy/README.md b/device_firmware/cart_firmware/deploy/README.md new file mode 100644 index 0000000..a5d0c38 --- /dev/null +++ b/device_firmware/cart_firmware/deploy/README.md @@ -0,0 +1,53 @@ +# systemd-службы сетевого моста + +Автозапуск `tools/bridge.py` как служб: `cart-bridge-tx` (отправитель, рядом с +пультом) и `cart-bridge-rx` (приёмник, рядом с тележкой). На каждой машине +включается ровно одна роль. + +## Установка + +На машине-отправителе: + +```sh +sudo ./install-bridge.sh tx +``` + +На машине-приёмнике: + +```sh +sudo ./install-bridge.sh rx +``` + +Установщик подставляет в юнит реальные пути (python из `.venv` проекта, если +есть, иначе системный `python3`; `bridge.py`; рабочий каталог) и пользователя +(того, кто вызвал `sudo`). Юниты кладутся в `/etc/systemd/system/`, конфиги — +в `/etc/cart-bridge/{tx,rx}.env` (создаются из примеров, существующие не +перезаписываются). Без аргумента (`none`) — только установка, без включения. + +Интерпретатор можно задать явно: `sudo PYTHON=/usr/bin/python3 ./install-bridge.sh rx`. +В выбранном python должен быть `pyserial`. + +## Настройка + +Отредактировать конфиг под свою сеть и ключ: + +```sh +sudoedit /etc/cart-bridge/tx.env # peer = адрес приёмника, общий ключ +sudoedit /etc/cart-bridge/rx.env # bind, тот же ключ +sudo systemctl restart cart-bridge-tx # применить +``` + +Ключ `CART_BRIDGE_KEY` на обеих сторонах должен совпадать (HMAC-аутентификация +пакетов; обязателен для интернета). + +## Эксплуатация + +```sh +systemctl status cart-bridge-tx +journalctl -u cart-bridge-tx -f # логи; на отправителе видно «приёмник отвечает/НЕ отвечает» +sudo systemctl restart cart-bridge-tx +sudo systemctl disable --now cart-bridge-tx +``` + +Служба перезапускается при падении (`Restart=always`). Доступ к USB-порту STM +даётся группами `dialout`/`plugdev` (прописаны в юните). diff --git a/device_firmware/cart_firmware/deploy/cart-bridge-rx.env.example b/device_firmware/cart_firmware/deploy/cart-bridge-rx.env.example new file mode 100644 index 0000000..4c6b6e8 --- /dev/null +++ b/device_firmware/cart_firmware/deploy/cart-bridge-rx.env.example @@ -0,0 +1,19 @@ +# Конфигурация службы приёмника (cart-bridge-rx). Копируется в +# /etc/cart-bridge/rx.env установщиком. Переменные читает tools/bridge.py. + +# Локальный UDP-адрес для приёма HOST:PORT. +CART_BRIDGE_BIND=0.0.0.0:5005 + +# Общий ключ HMAC (ОБЯЗАТЕЛЕН для интернета). Должен совпадать с отправителем. +CART_BRIDGE_KEY=change-me-shared-secret + +# Необязательно: принимать пакеты только с этого адреса отправителя (фильтр по +# хосту). Для интернета за NAT обычно оставляют пустым и полагаются на HMAC. +#CART_BRIDGE_PEER=10.0.0.1 + +# Необязательно: путь к порту или подстрока product/serial. По умолчанию порт +# ищется по USB product string CART-RX. +#CART_BRIDGE_DEVICE=CART-RX + +# Необязательно: уровень логов (DEBUG/INFO/WARNING). +#CART_BRIDGE_LOG_LEVEL=INFO diff --git a/device_firmware/cart_firmware/deploy/cart-bridge-rx.service b/device_firmware/cart_firmware/deploy/cart-bridge-rx.service new file mode 100644 index 0000000..2fbb394 --- /dev/null +++ b/device_firmware/cart_firmware/deploy/cart-bridge-rx.service @@ -0,0 +1,18 @@ +[Unit] +Description=Cart bridge receiver (role rx): сеть -> USB тележки +After=network-online.target +Wants=network-online.target +StartLimitIntervalSec=0 + +[Service] +Type=simple +User=@USER@ +SupplementaryGroups=dialout plugdev +EnvironmentFile=/etc/cart-bridge/rx.env +WorkingDirectory=@WORKDIR@ +ExecStart=@PYTHON@ @BRIDGE@ --role rx +Restart=always +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/device_firmware/cart_firmware/deploy/cart-bridge-tx.env.example b/device_firmware/cart_firmware/deploy/cart-bridge-tx.env.example new file mode 100644 index 0000000..872f43a --- /dev/null +++ b/device_firmware/cart_firmware/deploy/cart-bridge-tx.env.example @@ -0,0 +1,15 @@ +# Конфигурация службы отправителя (cart-bridge-tx). Копируется в +# /etc/cart-bridge/tx.env установщиком. Переменные читает tools/bridge.py. + +# Адрес приёмника HOST:PORT — куда слать управление. +CART_BRIDGE_PEER=10.0.0.2:5005 + +# Общий ключ HMAC (ОБЯЗАТЕЛЕН для интернета). Должен совпадать с приёмником. +CART_BRIDGE_KEY=change-me-shared-secret + +# Необязательно: путь к порту или подстрока product/serial. По умолчанию порт +# ищется по USB product string CART-TX. +#CART_BRIDGE_DEVICE=CART-TX + +# Необязательно: уровень логов (DEBUG/INFO/WARNING). +#CART_BRIDGE_LOG_LEVEL=INFO diff --git a/device_firmware/cart_firmware/deploy/cart-bridge-tx.service b/device_firmware/cart_firmware/deploy/cart-bridge-tx.service new file mode 100644 index 0000000..f157ef5 --- /dev/null +++ b/device_firmware/cart_firmware/deploy/cart-bridge-tx.service @@ -0,0 +1,20 @@ +[Unit] +Description=Cart bridge sender (role tx): пульт USB -> сеть +After=network-online.target +Wants=network-online.target +# не упираться в лимит перезапусков при флапе сети/USB +StartLimitIntervalSec=0 + +[Service] +Type=simple +User=@USER@ +# доступ к последовательному порту STM (dialout) и к USB-дескрипторам (plugdev) +SupplementaryGroups=dialout plugdev +EnvironmentFile=/etc/cart-bridge/tx.env +WorkingDirectory=@WORKDIR@ +ExecStart=@PYTHON@ @BRIDGE@ --role tx +Restart=always +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/device_firmware/cart_firmware/deploy/install-bridge.sh b/device_firmware/cart_firmware/deploy/install-bridge.sh new file mode 100755 index 0000000..dd4aa86 --- /dev/null +++ b/device_firmware/cart_firmware/deploy/install-bridge.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Установка systemd-служб сетевого моста тележки. +# +# sudo ./install-bridge.sh [tx|rx|both|none] +# +# Аргумент выбирает, какую службу включить и запустить на ЭТОЙ машине +# (tx — рядом с пультом, rx — рядом с тележкой). Без аргумента юниты только +# ставятся, но не включаются. Файлы юнитов и шаблоны конфигов берутся из этого +# каталога; пути к python и bridge.py подставляются автоматически. +set -euo pipefail + +SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TOOLS="$(cd "$SELF/../tools" && pwd)" +REPO="$(cd "$SELF/../../.." && pwd)" +BRIDGE="$TOOLS/bridge.py" +WORKDIR="$TOOLS" +ENABLE="${1:-none}" + +case "$ENABLE" in tx|rx|both|none) ;; *) echo "аргумент: tx|rx|both|none" >&2; exit 2;; esac + +if [ "$(id -u)" -ne 0 ]; then + echo "нужны права root — перезапускаюсь через sudo" + exec sudo -E PYTHON="${PYTHON:-}" bash "$0" "$ENABLE" +fi + +TARGET_USER="${SUDO_USER:-root}" + +# интерпретатор: venv проекта, если есть, иначе системный python3 +PYTHON="${PYTHON:-}" +if [ -z "$PYTHON" ]; then + if [ -x "$REPO/.venv/bin/python" ]; then + PYTHON="$REPO/.venv/bin/python" + else + PYTHON="$(command -v python3)" + fi +fi + +echo "python : $PYTHON" +echo "bridge : $BRIDGE" +echo "user : $TARGET_USER" + +if ! "$PYTHON" -c 'import serial' 2>/dev/null; then + echo "ВНИМАНИЕ: в $PYTHON нет pyserial — поставь: $PYTHON -m pip install pyserial" >&2 +fi + +gen_unit() { # + local role="$1" + sed -e "s|@PYTHON@|$PYTHON|g" -e "s|@BRIDGE@|$BRIDGE|g" \ + -e "s|@WORKDIR@|$WORKDIR|g" -e "s|@USER@|$TARGET_USER|g" \ + "$SELF/cart-bridge-$role.service" > "/etc/systemd/system/cart-bridge-$role.service" + echo "установлен юнит /etc/systemd/system/cart-bridge-$role.service" +} + +install -d -m 755 /etc/cart-bridge +for role in tx rx; do + gen_unit "$role" + conf="/etc/cart-bridge/$role.env" + if [ ! -e "$conf" ]; then + install -m 600 "$SELF/cart-bridge-$role.env.example" "$conf" + echo "создан конфиг $conf (ОТРЕДАКТИРУЙ: peer/bind и ключ)" + else + echo "конфиг $conf уже есть — не трогаю" + fi +done + +systemctl daemon-reload + +enable_one() { # + systemctl enable --now "cart-bridge-$1.service" + echo "включена и запущена cart-bridge-$1.service" +} +case "$ENABLE" in + tx) enable_one tx ;; + rx) enable_one rx ;; + both) enable_one tx; enable_one rx ;; + none) echo "юниты установлены, не включены. Включить: systemctl enable --now cart-bridge-tx (или -rx)";; +esac + +echo "готово. Логи: journalctl -u cart-bridge-tx -f (или -rx)" diff --git a/device_firmware/cart_firmware/docs/protocol.md b/device_firmware/cart_firmware/docs/protocol.md index 142547d..619b0c8 100644 --- a/device_firmware/cart_firmware/docs/protocol.md +++ b/device_firmware/cart_firmware/docs/protocol.md @@ -179,6 +179,18 @@ Payload `CONTROL` (LE): `seq:u32`, `t_ms:u32`, `flags:u8`, `ch[16]:u16` = 41 б неаутентичные и принимает только с ожидаемого адреса (`--peer`). Рекомендуемый способ наружу — WireGuard-туннель, внутри которого идёт простой UDP. «Голый» неаутентифицированный UDP допустим только в доверенной локальной сети. +- **Обратный heartbeat.** RX шлёт на адрес источника последнего CONTROL + маленький служебный пакет (~5 раз/с): magic `CB 01`, флаг собственного + failsafe, последний принятый seq; при включённом ключе — тоже под HMAC. TX + принимает их на тот же сокет (сквозь NAT проходит, т.к. он сам инициировал + поток) и показывает в логе, **отвечает ли приёмник** и не ушёл ли тот в + failsafe. Это чисто служебный PC-трафик, STM в нём не участвует, поэтому он не + в зеркале протокола, а в самом `bridge.py`. На выдачу SBUS и на основной поток + управления heartbeat не влияет. + - **Индикация для оператора.** Если запустить tx в терминале, он показывает одну + живую строку, обновляемую на месте: зелёное `● СВЯЗЬ ЕСТЬ`, жёлтое + `● СВЯЗЬ ЕСТЬ (приёмник в failsafe)` или красное `● СВЯЗИ НЕТ`. Под systemd + (вывод не в терминал) вместо строки идут события в журнал по смене состояния. ## FAILSAFE @@ -206,3 +218,24 @@ Payload `CONTROL` (LE): `seq:u32`, `t_ms:u32`, `flags:u8`, `ch[16]:u16` = 41 б Локальная проверка без сети: оба экземпляра на одной машине с `--peer 127.0.0.1` и двумя STM по USB; SBUS с RX снять `tools/decode.py` и сверить с осями стика TX. + +Параметры можно задавать через env (`CART_BRIDGE_ROLE/PEER/BIND/DEVICE/KEY/ +LOG_LEVEL`), CLI их переопределяет. + +## Автозапуск: systemd-службы + +Для постоянной работы мост ставится службами: `cart-bridge-tx` (отправитель) и +`cart-bridge-rx` (приёмник), по одной роли на машину. Юниты, примеры конфигов и +установщик — в каталоге `deploy/`: + +```sh +cd deploy +sudo ./install-bridge.sh tx # на машине у пульта +sudo ./install-bridge.sh rx # на машине у тележки +``` + +Установщик подставляет реальные пути (python из `.venv`, `bridge.py`, рабочий +каталог) и пользователя, кладёт конфиги в `/etc/cart-bridge/{tx,rx}.env`. Там +задаются `CART_BRIDGE_PEER`/`BIND` и общий `CART_BRIDGE_KEY` (должен совпадать на +обеих сторонах). Логи и индикация «отвечает ли приёмник» — `journalctl -u +cart-bridge-tx -f`. Подробности — `deploy/README.md`. diff --git a/device_firmware/cart_firmware/src/link_protocol.h b/device_firmware/cart_firmware/src/link_protocol.h index 767a2f8..589a94e 100644 --- a/device_firmware/cart_firmware/src/link_protocol.h +++ b/device_firmware/cart_firmware/src/link_protocol.h @@ -56,4 +56,4 @@ static const uint16_t CART_FW_VERSION = 0x0100; // 1.00 // --- тайминги (мс) --- static const uint32_t FRAME_PERIOD_MS = 50; // 20 Гц, темп старого пульта -static const uint32_t RX_USB_TIMEOUT_MS = 150; // нет CONTROL дольше -> нейтраль на проводе +static const uint32_t RX_USB_TIMEOUT_MS = 450; // нет CONTROL дольше -> нейтраль на проводе diff --git a/device_firmware/cart_firmware/tools/bridge.py b/device_firmware/cart_firmware/tools/bridge.py index 6e5e959..8fd37bb 100644 --- a/device_firmware/cart_firmware/tools/bridge.py +++ b/device_firmware/cart_firmware/tools/bridge.py @@ -9,11 +9,17 @@ Роль rx: принимает UDP, пишет CONTROL в USB на STM-RX; при тишине из сети шлёт явную нейтраль с флагом failsafe (главный предохранитель — на самом STM). +Обратный канал: rx шлёт tx heartbeat, поэтому на стороне отправителя видно, +отвечает ли приёмник и не ушёл ли он сам в failsafe. + Устройство выбирается по USB product string (CART-TX / CART-RX) и подтверждается handshake-кадром IDENT (роль + версия протокола). Для интернета используй общий ключ (--key) — каждый UDP-пакет аутентифицируется HMAC-SHA256; без ключа трафик идёт в открытом виде (только для доверенной локальной сети). +Параметры можно задавать и через env (CART_BRIDGE_ROLE/PEER/BIND/DEVICE/KEY/ +LOG_LEVEL) — так настраиваются systemd-службы (см. deploy/). + Запуск (из каталога прошивки): .venv/bin/python tools/bridge.py --role tx --peer 10.0.0.2:5005 .venv/bin/python tools/bridge.py --role rx --bind 0.0.0.0:5005 @@ -21,8 +27,10 @@ handshake-кадром IDENT (роль + версия протокола). Дл import argparse import hmac import logging +import os import select import socket +import struct import sys import time from hashlib import sha256 @@ -33,13 +41,55 @@ from serial.tools import list_ports import link_proto as lp HMAC_LEN = 8 # усечённый HMAC-SHA256 на пакет -NET_TIMEOUT_S = 0.10 # нет UDP дольше -> rx начинает слать failsafe +NET_TIMEOUT_S = 0.50 # нет UDP дольше -> rx начинает слать failsafe HELLO_TIMEOUT_S = 1.0 SEQ_RESET_AFTER = 20 # столько подряд «несвежих» пакетов = перезапуск источника +# Обратный канал «жив ли приёмник»: rx шлёт heartbeat в сторону tx (на адрес +# источника последнего CONTROL), tx по ним показывает, отвечает ли приёмник и +# не ушёл ли он сам в failsafe. Это PC-only служебный трафик (к STM отношения не +# имеет), поэтому живёт здесь, а не в зеркале протокола link_proto. +HEARTBEAT_MAGIC = b"\xCB\x01" +HEARTBEAT_PERIOD_S = 0.2 # как часто rx шлёт heartbeat +HEARTBEAT_TIMEOUT_S = 1.0 # нет heartbeat дольше -> tx считает приёмник недоступным +HEARTBEAT_STATUS_S = 5.0 # период печати статуса приёмника на tx +HB_FLAG_FAILSAFE = 0x01 + log = logging.getLogger("bridge") +def pack_hb(failsafe: bool, last_seq: int) -> bytes: + flags = HB_FLAG_FAILSAFE if failsafe else 0 + return HEARTBEAT_MAGIC + struct.pack(" dict(failsafe, last_seq) либо None, если это не heartbeat.""" + if len(payload) != len(HEARTBEAT_MAGIC) + 5 or payload[:2] != HEARTBEAT_MAGIC: + return None + flags, last_seq = struct.unpack_from(" str: @@ -128,6 +178,14 @@ def run_tx(args, key): parser = lp.FrameParser() log.info("tx: %s -> UDP %s:%d", ser.port, *peer) sent = 0 + rx_alive = False # получаем ли heartbeat от приёмника + rx_failsafe = False # сообщил ли приёмник, что сам в failsafe + last_hb = 0.0 + last_status = 0.0 + # В терминале — живая цветная строка; под systemd (journald, не TTY) — обычные + # события по изменению состояния. + tty = sys.stdout.isatty() + color = tty and os.environ.get("NO_COLOR") is None while True: # read(1) блокирует до первого байта, затем забираем всё доступное — # кадр уходит в сеть сразу, без ожидания таймаута чтения (иначе каждый @@ -139,12 +197,41 @@ def run_tx(args, key): if ftype == lp.CONTROL: sock.sendto(seal(payload, key), peer) sent += 1 - if sent % 100 == 0: - c = lp.unpack_control(payload) - log.debug("tx seq=%d ch2=%d ch4=%d", c["seq"], c["ch"][1], c["ch"][3]) elif ftype == lp.LOG: log.info("STM-TX: %s", payload.decode("ascii", "replace")) + now = time.monotonic() + prev_alive, prev_fs = rx_alive, rx_failsafe + # обратный канал: heartbeat от приёмника (приходит на тот же сокет) + while select.select([sock], [], [], 0)[0]: + try: + datagram, _ = sock.recvfrom(2048) + except BlockingIOError: + break + hb = parse_hb(unseal(datagram, key) or b"") + if hb is None: + continue + last_hb = now + rx_alive = True + rx_failsafe = hb["failsafe"] + if rx_alive and now - last_hb > HEARTBEAT_TIMEOUT_S: + rx_alive, rx_failsafe = False, False + + if tty: + render_link_status(rx_alive, rx_failsafe, color) # живая строка + else: + if rx_alive != prev_alive: + (log.info if rx_alive else log.warning)( + "приёмник отвечает" if rx_alive else "приёмник НЕ отвечает") + elif rx_alive and rx_failsafe != prev_fs: + (log.warning if rx_failsafe else log.info)( + "приёмник в failsafe: наш поток не доходит" if rx_failsafe + else "приёмник принимает поток") + if now - last_status >= HEARTBEAT_STATUS_S: + last_status = now + log.info("приёмник: %s (отправлено %d)", + "отвечает" if rx_alive else "НЕ отвечает", sent) + # ---------------- роль RX: сеть -> USB ---------------- @@ -163,14 +250,16 @@ def run_rx(args, key): last_seq = None last_net = time.monotonic() last_fs = 0.0 + last_hb_sent = 0.0 fs_seq = 0 in_failsafe = False rejects = 0 # подряд отброшенных по seq -> признак перезапуска источника + tx_addr = None # куда слать heartbeat (источник последнего CONTROL) def accept(datagram, addr): """Проверить пакет и обновить состояние; вернуть payload принятого свежего кадра либо None.""" - nonlocal last_seq, last_net, in_failsafe, rejects + nonlocal last_seq, last_net, in_failsafe, rejects, tx_addr if allowed_host and addr[0] != allowed_host: log.warning("пакет с неожиданного адреса %s отброшен", addr[0]) return None @@ -178,6 +267,7 @@ def run_rx(args, key): if not payload or len(payload) != lp.CONTROL_LEN: log.warning("битый/неаутентичный пакет отброшен") return None + tx_addr = addr # аутентичный CONTROL -> знаем обратный адрес для heartbeat c = lp.unpack_control(payload) # Принять свежий seq ИЛИ новую сессию: источник (STM-TX) после ребута # начинает seq заново с 0, и без сброса last_seq поток завис бы в @@ -228,6 +318,11 @@ def run_rx(args, key): fs_seq = (fs_seq + 1) & 0xFFFFFFFF last_fs = now + # обратный heartbeat к tx: «приёмник жив», с признаком собственного failsafe + if tx_addr is not None and now - last_hb_sent >= HEARTBEAT_PERIOD_S: + sock.sendto(seal(pack_hb(in_failsafe, last_seq or 0), key), tx_addr) + last_hb_sent = now + # неблокирующий дренаж входящих LOG от STM-RX if ser.in_waiting: for ftype, payload in parser.feed(ser.read(ser.in_waiting)): @@ -245,17 +340,26 @@ def parse_hostport(s: str, what: str): def main(): + # Значения по умолчанию берутся из env (CART_BRIDGE_*), чтобы systemd-юнит + # мог задавать всё через EnvironmentFile; аргументы CLI их переопределяют. + env = os.environ.get ap = argparse.ArgumentParser(description="мост пульт<->тележка через сеть") - ap.add_argument("--role", required=True, choices=("tx", "rx")) - ap.add_argument("--peer", help="tx: куда слать HOST:PORT; rx: разрешённый источник (опц.)") - ap.add_argument("--bind", default="0.0.0.0:5005", help="rx: локальный UDP HOST:PORT") - ap.add_argument("--device", help="путь к порту или подстрока product/serial (иначе по роли)") - ap.add_argument("--key", help="общий ключ для HMAC-аутентификации пакетов (для интернета)") - ap.add_argument("--log-level", default="INFO") + ap.add_argument("--role", default=env("CART_BRIDGE_ROLE"), choices=("tx", "rx")) + ap.add_argument("--peer", default=env("CART_BRIDGE_PEER"), + help="tx: куда слать HOST:PORT; rx: разрешённый источник (опц.)") + ap.add_argument("--bind", default=env("CART_BRIDGE_BIND", "0.0.0.0:5005"), + help="rx: локальный UDP HOST:PORT") + ap.add_argument("--device", default=env("CART_BRIDGE_DEVICE"), + help="путь к порту или подстрока product/serial (иначе по роли)") + ap.add_argument("--key", default=env("CART_BRIDGE_KEY"), + help="общий ключ для HMAC-аутентификации пакетов (для интернета)") + ap.add_argument("--log-level", default=env("CART_BRIDGE_LOG_LEVEL", "INFO")) args = ap.parse_args() logging.basicConfig(level=args.log_level.upper(), format="%(asctime)s %(levelname)s %(message)s") + if args.role not in ("tx", "rx"): + raise SystemExit("нужна роль: --role tx|rx (или CART_BRIDGE_ROLE)") key = args.key.encode() if args.key else None if args.role == "tx" and not args.peer: raise SystemExit("--peer обязателен для роли tx") @@ -266,6 +370,9 @@ def main(): (run_tx if args.role == "tx" else run_rx)(args, key) except KeyboardInterrupt: log.info("остановлено") + finally: + if sys.stdout.isatty(): + sys.stdout.write("\n") # увести живую строку от приглашения shell if __name__ == "__main__": diff --git a/device_firmware/cart_firmware/tools/link_proto.py b/device_firmware/cart_firmware/tools/link_proto.py index 5e21e24..5fa6778 100644 --- a/device_firmware/cart_firmware/tools/link_proto.py +++ b/device_firmware/cart_firmware/tools/link_proto.py @@ -34,7 +34,7 @@ SBUS_MID = 1024 # тайминги (мс), зеркало link_protocol.h FRAME_PERIOD_MS = 50 -RX_USB_TIMEOUT_MS = 150 +RX_USB_TIMEOUT_MS = 400 def crc16(data: bytes) -> int: