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"
|
"platformio.platformio-ide"
|
||||||
],
|
],
|
||||||
"unwantedRecommendations": [
|
"unwantedRecommendations": [
|
||||||
"ms-vscode.cpptools-extension-pack"
|
"ms-vscode.cpptools-extension-pack",
|
||||||
|
"pioarduino.pioarduino-ide"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,63 @@ STM-TX продолжает гнать SBUS на свой локальный п
|
|||||||
96-битного UID чипа (на случай неверно прошитого устройства, рассинхрона версий
|
96-битного UID чипа (на случай неверно прошитого устройства, рассинхрона версий
|
||||||
или чужого CDC с тем же VID/PID). Serial-дескриптор (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
|
## Кадр поверх USB CDC
|
||||||
|
|
||||||
Единственный источник истины — `src/link_protocol.h`, зеркало на Python —
|
Единственный источник истины — `src/link_protocol.h`, зеркало на Python —
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ handshake-кадром IDENT (роль + версия протокола). Дл
|
|||||||
import argparse
|
import argparse
|
||||||
import hmac
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
|
import select
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -128,7 +129,13 @@ def run_tx(args, key):
|
|||||||
log.info("tx: %s -> UDP %s:%d", ser.port, *peer)
|
log.info("tx: %s -> UDP %s:%d", ser.port, *peer)
|
||||||
sent = 0
|
sent = 0
|
||||||
while True:
|
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:
|
if ftype == lp.CONTROL:
|
||||||
sock.sendto(seal(payload, key), peer)
|
sock.sendto(seal(payload, key), peer)
|
||||||
sent += 1
|
sent += 1
|
||||||
@@ -149,7 +156,7 @@ def run_rx(args, key):
|
|||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sock.bind(bind)
|
sock.bind(bind)
|
||||||
sock.settimeout(lp.FRAME_PERIOD_MS / 1000.0)
|
sock.setblocking(False)
|
||||||
parser = lp.FrameParser()
|
parser = lp.FrameParser()
|
||||||
log.info("rx: UDP %s:%d -> %s", bind[0], bind[1], ser.port)
|
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
|
in_failsafe = False
|
||||||
rejects = 0 # подряд отброшенных по seq -> признак перезапуска источника
|
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:
|
while True:
|
||||||
try:
|
# Ждём пакет до 20 мс (пауза цикла), затем БЕЗ блокировки выгребаем всю
|
||||||
datagram, addr = sock.recvfrom(2048)
|
# накопившуюся очередь сокета и шлём на STM только самый свежий кадр
|
||||||
if allowed_host and addr[0] != allowed_host:
|
# (latest-wins): так задержка не растёт, даже если пакеты пришли пачкой.
|
||||||
log.warning("пакет с неожиданного адреса %s отброшен", addr[0])
|
newest = None
|
||||||
datagram = None
|
ready = select.select([sock], [], [], lp.FRAME_PERIOD_MS / 2000.0)[0]
|
||||||
payload = unseal(datagram, key) if datagram else None
|
while ready:
|
||||||
if payload and len(payload) == lp.CONTROL_LEN:
|
try:
|
||||||
c = lp.unpack_control(payload)
|
datagram, addr = sock.recvfrom(2048)
|
||||||
# Принять свежий seq ИЛИ новую сессию: источник (STM-TX) после
|
except BlockingIOError:
|
||||||
# ребута начинает seq заново с 0, и без сброса last_seq поток
|
break
|
||||||
# завис бы в failsafe навсегда. Новую сессию распознаём по пачке
|
p = accept(datagram, addr)
|
||||||
# подряд «несвежих» пакетов (устойчиво к одиночным reorder/dup).
|
if p is not None:
|
||||||
fresh = last_seq is None or seq_newer(c["seq"], last_seq)
|
newest = p
|
||||||
if not fresh:
|
ready = select.select([sock], [], [], 0)[0]
|
||||||
rejects += 1
|
if newest is not None:
|
||||||
if rejects >= SEQ_RESET_AFTER:
|
ser.write(lp.build_frame(lp.CONTROL, newest))
|
||||||
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
|
|
||||||
|
|
||||||
# сетевой failsafe: тишина -> активная нейтраль на STM в темпе 20 Гц
|
# сетевой failsafe: тишина -> активная нейтраль на STM в темпе 20 Гц
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
@@ -203,10 +228,11 @@ def run_rx(args, key):
|
|||||||
fs_seq = (fs_seq + 1) & 0xFFFFFFFF
|
fs_seq = (fs_seq + 1) & 0xFFFFFFFF
|
||||||
last_fs = now
|
last_fs = now
|
||||||
|
|
||||||
# дренируем входящие LOG/IDENT от STM-RX
|
# неблокирующий дренаж входящих LOG от STM-RX
|
||||||
for ftype, payload in parser.feed(ser.read(256)):
|
if ser.in_waiting:
|
||||||
if ftype == lp.LOG:
|
for ftype, payload in parser.feed(ser.read(ser.in_waiting)):
|
||||||
log.info("STM-RX: %s", payload.decode("ascii", "replace"))
|
if ftype == lp.LOG:
|
||||||
|
log.info("STM-RX: %s", payload.decode("ascii", "replace"))
|
||||||
|
|
||||||
|
|
||||||
# ---------------- общее ----------------
|
# ---------------- общее ----------------
|
||||||
|
|||||||
Reference in New Issue
Block a user