Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09a2aa7932 | ||
|
|
75185c7e36 | ||
|
|
5b70a9a4ce |
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cmake.sourceDirectory": "/home/awe/Documents/radar_system/data_acq_and_processing/gpr_cpp"
|
||||
}
|
||||
+2
-1
@@ -5,6 +5,7 @@
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
"ms-vscode.cpptools-extension-pack",
|
||||
"pioarduino.pioarduino-ide"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -81,6 +81,63 @@ STM-TX продолжает гнать SBUS на свой локальный п
|
||||
96-битного UID чипа (на случай неверно прошитого устройства, рассинхрона версий
|
||||
или чужого CDC с тем же VID/PID). Serial-дескриптор (UID) различает экземпляры.
|
||||
|
||||
## Прошивка STM
|
||||
|
||||
Плата — WeAct STM32G431CB. Нужен PlatformIO Core (`pio`); если не установлен:
|
||||
`pip install platformio` (в репозитории он лежит в `~/.platformio/penv/bin/pio`).
|
||||
Все команды выполняются из каталога прошивки `device_firmware/cart_firmware`.
|
||||
|
||||
Две роли — две отдельные прошивки на две платы. По умолчанию загрузчик — ST-Link
|
||||
по SWD (`upload_protocol = stlink` в `platformio.ini`).
|
||||
|
||||
**1. Подключить ST-Link к плате по SWD** (4 провода):
|
||||
|
||||
| ST-Link | WeAct G431CB |
|
||||
|---|---|
|
||||
| SWDIO | PA13 (SWDIO) |
|
||||
| SWCLK | PA14 (SWCLK) |
|
||||
| GND | GND |
|
||||
| 3V3 | 3V3 |
|
||||
|
||||
**2. Собрать и прошить роль** (прошивать по одной плате за раз, один ST-Link):
|
||||
|
||||
```
|
||||
# плата-передатчик (к ней подключён пульт)
|
||||
pio run -e cart_tx -t upload
|
||||
# плата-приёмник (к ней подключён провод драйвера колёс)
|
||||
pio run -e cart_rx -t upload
|
||||
```
|
||||
|
||||
- Только собрать без заливки: `pio run -e cart_tx`. Артефакты —
|
||||
`.pio/build/<env>/firmware.{elf,bin}`.
|
||||
- Если подключено несколько ST-Link, указать конкретный:
|
||||
`pio run -e cart_tx -t upload --upload-port <serial-ST-Link>`.
|
||||
- Сменить роль платы — просто залить другой env, флеш перезаписывается целиком.
|
||||
|
||||
**3. Физические подключения по ролям** (распиновка общая, см. выше по документу):
|
||||
|
||||
| | CART-TX | CART-RX |
|
||||
|---|---|---|
|
||||
| Стик X / Y | PA0 / PA1 (АЦП) | — не используется |
|
||||
| Выход SBUS | PA9 → локальный драйвер (как раньше) | PA9 → провод драйвера колёс |
|
||||
| USB | → ПК (bridge --role tx) | → ПК (bridge --role rx) |
|
||||
|
||||
**4. Проверить роль после прошивки.** Подключить плату по USB к ПК и прочитать
|
||||
product string (роль зашита в неё):
|
||||
|
||||
```
|
||||
python3 -c "from serial.tools import list_ports; \
|
||||
[print(p.device, p.product, p.serial_number) for p in list_ports.comports()]"
|
||||
# ожидаемо: /dev/ttyACM0 CART-TX <UID> либо CART-RX
|
||||
```
|
||||
|
||||
Либо просто запустить `tools/bridge.py` соответствующей роли — на старте он
|
||||
делает handshake и логирует подтверждённые роль, версию прошивки и UID.
|
||||
|
||||
> USB CDC теперь несёт **бинарный** протокол, а не текстовый лог. `pio device
|
||||
> monitor` покажет «мусор» — это нормально. Отладочный текст идёт кадрами `LOG`,
|
||||
> которые печатает `bridge.py`.
|
||||
|
||||
## Кадр поверх USB CDC
|
||||
|
||||
Единственный источник истины — `src/link_protocol.h`, зеркало на Python —
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
# ---------------- общее ----------------
|
||||
|
||||
Reference in New Issue
Block a user