mirror of
https://github.com/controllerzz/carbus_lib.git
synced 2026-07-15 00:10:15 +03:00
update 0.1.9
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from .device import CarBusDevice
|
||||
from .device import CarBusDevice, CanTiming
|
||||
from .messages import CanMessage, MessageDirection
|
||||
from .exceptions import CarBusError, CommandError, SyncError
|
||||
from .can_router import CanIdRouter, RoutedCarBusCanTransport
|
||||
@@ -7,6 +7,7 @@ from .remote.client import open_remote_device
|
||||
|
||||
__all__ = [
|
||||
"CarBusDevice",
|
||||
"CanTiming",
|
||||
"CanMessage",
|
||||
"MessageDirection",
|
||||
"CarBusError",
|
||||
|
||||
@@ -49,6 +49,40 @@ class CanTiming:
|
||||
sjw: int
|
||||
|
||||
|
||||
DEFAULT_CAN_CLOCK_HZ = 120_000_000
|
||||
|
||||
# Проверенные bit-timing'и для CAN-модуля с тактовой 120 МГц (точка выборки ~80%).
|
||||
# open_can_channel() для FD-канала задаёт скорость именно таймингами, а не
|
||||
# одиночным индексом data-скорости (CC_BUS_SPEED_D): прошивка этих адаптеров
|
||||
# применяет явные тайминги надёжно, а индекс data-скорости не отрабатывает —
|
||||
# канал открывается, но FD/BRS-кадры не уходят в шину.
|
||||
_NOMINAL_TIMINGS_120M: Dict[int, CanTiming] = {
|
||||
1_000_000: CanTiming(prescaler=6, tq_seg1=15, tq_seg2=4, sjw=1), # 120/(6*20)
|
||||
500_000: CanTiming(prescaler=15, tq_seg1=12, tq_seg2=3, sjw=1), # 120/(15*16)
|
||||
250_000: CanTiming(prescaler=30, tq_seg1=12, tq_seg2=3, sjw=1), # 120/(30*16)
|
||||
125_000: CanTiming(prescaler=60, tq_seg1=12, tq_seg2=3, sjw=1), # 120/(60*16)
|
||||
}
|
||||
_DATA_TIMINGS_120M: Dict[int, CanTiming] = {
|
||||
5_000_000: CanTiming(prescaler=3, tq_seg1=5, tq_seg2=2, sjw=1), # 120/(3*8)
|
||||
4_000_000: CanTiming(prescaler=3, tq_seg1=7, tq_seg2=2, sjw=1), # 120/(3*10)
|
||||
2_000_000: CanTiming(prescaler=6, tq_seg1=7, tq_seg2=2, sjw=1), # 120/(6*10)
|
||||
1_000_000: CanTiming(prescaler=12, tq_seg1=7, tq_seg2=2, sjw=1), # 120/(12*10)
|
||||
500_000: CanTiming(prescaler=24, tq_seg1=7, tq_seg2=2, sjw=1), # 120/(24*10)
|
||||
}
|
||||
|
||||
|
||||
def _resolve_timing(
|
||||
table: Dict[int, CanTiming], bitrate: int, clock_hz: int, kind: str
|
||||
) -> CanTiming:
|
||||
if clock_hz == DEFAULT_CAN_CLOCK_HZ and bitrate in table:
|
||||
return table[bitrate]
|
||||
raise ValueError(
|
||||
f"Нет встроенного FD bit-timing для {kind}={bitrate} bps при тактовой "
|
||||
f"{clock_hz} Гц. Доступно (для 120 МГц): {sorted(table)}. "
|
||||
f"Задайте скорость явными таймингами через open_can_channel_custom(...)."
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceInfoParam:
|
||||
header: int
|
||||
@@ -823,8 +857,33 @@ class CarBusDevice:
|
||||
non_iso: bool = False,
|
||||
nominal_index: Optional[int] = None,
|
||||
data_index: Optional[int] = None,
|
||||
can_clock_hz: Optional[int] = None,
|
||||
) -> None:
|
||||
|
||||
# FD-канал настраиваем явными таймингами (рабочий путь): одиночный индекс
|
||||
# data-скорости прошивкой не применяется, канал открывается, но FD/BRS-кадры
|
||||
# не уходят в шину. Явное переопределение индексами/auto_detect не трогаем.
|
||||
if fd and not (auto_detect or nominal_index is not None or data_index is not None):
|
||||
if data_bitrate is None:
|
||||
raise ValueError("fd=True требует указать data_bitrate")
|
||||
clock = can_clock_hz or DEFAULT_CAN_CLOCK_HZ
|
||||
await self.open_can_channel_custom(
|
||||
channel,
|
||||
nominal_timing=_resolve_timing(
|
||||
_NOMINAL_TIMINGS_120M, nominal_bitrate, clock, "nominal"
|
||||
),
|
||||
data_timing=_resolve_timing(
|
||||
_DATA_TIMINGS_120M, data_bitrate, clock, "data"
|
||||
),
|
||||
fd=True,
|
||||
brs=brs,
|
||||
listen_only=listen_only,
|
||||
loopback=loopback,
|
||||
retransmit=retransmit,
|
||||
non_iso=non_iso,
|
||||
)
|
||||
return
|
||||
|
||||
if loopback:
|
||||
mode_val = 0x02
|
||||
elif listen_only:
|
||||
|
||||
@@ -72,6 +72,10 @@ class PeriodicJob:
|
||||
msg = CanMessage(
|
||||
can_id=self.can_id,
|
||||
data=out,
|
||||
extended=self.extended,
|
||||
rtr=self.rtr,
|
||||
fd=self.fd,
|
||||
brs=self.brs,
|
||||
)
|
||||
await dev.send_can(
|
||||
msg,
|
||||
|
||||
+115
-48
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
@@ -10,6 +11,15 @@ import serial_asyncio
|
||||
|
||||
log = logging.getLogger("carbus_remote.agent")
|
||||
|
||||
RECONNECT_MIN_S = 1.0
|
||||
RECONNECT_MAX_S = 15.0
|
||||
|
||||
CONNECT_TIMEOUT_S = 5.0
|
||||
AUTH_TIMEOUT_S = 10.0
|
||||
DATA_HELLO_TIMEOUT_S = 5.0
|
||||
|
||||
COM_REOPEN_DELAY_S = 0.25
|
||||
|
||||
|
||||
def parse_hostport(s: str) -> tuple[str, int]:
|
||||
if ":" not in s:
|
||||
@@ -38,10 +48,8 @@ async def pipe_bidirectional_streams(
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
dst.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
t1 = asyncio.create_task(pump(a_reader, b_writer), name="pipe_a_to_b")
|
||||
t2 = asyncio.create_task(pump(b_reader, a_writer), name="pipe_b_to_a")
|
||||
@@ -49,32 +57,35 @@ async def pipe_bidirectional_streams(
|
||||
await asyncio.wait({t1, t2}, return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
for w in (a_writer, b_writer):
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
w.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await asyncio.gather(t1, t2, return_exceptions=True)
|
||||
|
||||
for w in (a_writer, b_writer):
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
await w.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def open_serial_with_retry(port: str, baudrate: int, *, attempts: int = 3) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
||||
async def open_serial_with_retry(
|
||||
port: str,
|
||||
baudrate: int,
|
||||
*,
|
||||
attempts: int = 5,
|
||||
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
||||
|
||||
last: Optional[BaseException] = None
|
||||
for i in range(attempts):
|
||||
try:
|
||||
return await serial_asyncio.open_serial_connection(url=port, baudrate=baudrate)
|
||||
except Exception as e:
|
||||
last = e
|
||||
await asyncio.sleep(0.25 + 0.25 * i)
|
||||
await asyncio.sleep(0.25 + 0.35 * i)
|
||||
assert last is not None
|
||||
raise last
|
||||
|
||||
|
||||
async def agent_run(
|
||||
async def agent_run_once(
|
||||
*,
|
||||
port: str,
|
||||
baudrate: int,
|
||||
@@ -82,11 +93,15 @@ async def agent_run(
|
||||
serial: str,
|
||||
password: str,
|
||||
) -> None:
|
||||
|
||||
server_host, server_port = parse_hostport(server)
|
||||
|
||||
session_lock = asyncio.Lock()
|
||||
active_data_task: Optional[asyncio.Task] = None
|
||||
|
||||
async def open_data_session(session: str) -> None:
|
||||
nonlocal active_data_task
|
||||
|
||||
async with session_lock:
|
||||
net_r: Optional[asyncio.StreamReader] = None
|
||||
net_w: Optional[asyncio.StreamWriter] = None
|
||||
@@ -98,63 +113,71 @@ async def agent_run(
|
||||
|
||||
net_r, net_w = await asyncio.wait_for(
|
||||
asyncio.open_connection(server_host, server_port),
|
||||
timeout=5.0,
|
||||
timeout=CONNECT_TIMEOUT_S,
|
||||
)
|
||||
|
||||
net_w.write((json.dumps({"role": "agent_data", "session": session}) + "\n").encode("utf-8"))
|
||||
await net_w.drain()
|
||||
|
||||
line = await asyncio.wait_for(net_r.readline(), timeout=5.0)
|
||||
line = await asyncio.wait_for(net_r.readline(), timeout=DATA_HELLO_TIMEOUT_S)
|
||||
if not line:
|
||||
raise ConnectionError("relay closed data socket before hello")
|
||||
|
||||
resp = json.loads(line.decode("utf-8", errors="ignore") or "{}")
|
||||
if not resp.get("ok"):
|
||||
log.error("Data session refused %s: %s", session, resp)
|
||||
return
|
||||
|
||||
await asyncio.sleep(COM_REOPEN_DELAY_S)
|
||||
log.info("Opening COM %s @ %d for session %s", port, baudrate, session)
|
||||
dev_r, dev_w = await open_serial_with_retry(port, baudrate, attempts=3)
|
||||
dev_r, dev_w = await open_serial_with_retry(port, baudrate, attempts=5)
|
||||
|
||||
log.info("Session %s accepted. Piping bytes (COM <-> relay).", session)
|
||||
await pipe_bidirectional_streams(dev_r, dev_w, net_r, net_w)
|
||||
|
||||
log.info("Session %s finished.", session)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
log.info("Data session %s cancelled", session)
|
||||
raise
|
||||
except Exception:
|
||||
log.exception("Data session %s crashed", session)
|
||||
|
||||
finally:
|
||||
if net_w is not None:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
net_w.close()
|
||||
with contextlib.suppress(Exception):
|
||||
await net_w.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if dev_w is not None:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
dev_w.close()
|
||||
with contextlib.suppress(Exception):
|
||||
await dev_w.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- CONTROL CONNECT ---
|
||||
log.info("Connecting to relay %s:%d (control)", server_host, server_port)
|
||||
ctrl_reader, ctrl_writer = await asyncio.open_connection(server_host, server_port)
|
||||
|
||||
ctrl_writer.write((json.dumps({"role": "agent", "serial": serial, "password": password}) + "\n").encode("utf-8"))
|
||||
await ctrl_writer.drain()
|
||||
|
||||
line = await asyncio.wait_for(ctrl_reader.readline(), timeout=10.0)
|
||||
resp = json.loads(line.decode("utf-8", errors="ignore") or "{}")
|
||||
if not resp.get("ok"):
|
||||
ctrl_writer.close()
|
||||
try:
|
||||
await ctrl_writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
raise RuntimeError(f"relay refused agent: {resp}")
|
||||
|
||||
log.info("Agent registered OK. Waiting for sessions... (serial=%s)", serial)
|
||||
ctrl_reader, ctrl_writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(server_host, server_port),
|
||||
timeout=CONNECT_TIMEOUT_S,
|
||||
)
|
||||
|
||||
try:
|
||||
# AUTH
|
||||
ctrl_writer.write((json.dumps({"role": "agent", "serial": serial, "password": password}) + "\n").encode("utf-8"))
|
||||
await ctrl_writer.drain()
|
||||
|
||||
line = await asyncio.wait_for(ctrl_reader.readline(), timeout=AUTH_TIMEOUT_S)
|
||||
if not line:
|
||||
raise ConnectionError("relay closed control socket during auth")
|
||||
|
||||
resp = json.loads(line.decode("utf-8", errors="ignore") or "{}")
|
||||
if not resp.get("ok"):
|
||||
raise RuntimeError(f"relay refused agent: {resp}")
|
||||
|
||||
log.info("Agent registered OK. Waiting for sessions... (serial=%s)", serial)
|
||||
|
||||
# CONTROL LOOP
|
||||
while True:
|
||||
line = await ctrl_reader.readline()
|
||||
if not line:
|
||||
@@ -168,17 +191,62 @@ async def agent_run(
|
||||
|
||||
if msg.get("cmd") == "open_session":
|
||||
session = str(msg.get("session", "")).strip()
|
||||
if session:
|
||||
asyncio.create_task(open_data_session(session), name=f"agent_data_session_{session}")
|
||||
if not session:
|
||||
continue
|
||||
|
||||
# если уже идёт data-session — игнорируем новый запрос (или можно логировать busy)
|
||||
if active_data_task is not None and not active_data_task.done():
|
||||
log.warning("Data session already running; ignoring open_session=%s", session)
|
||||
continue
|
||||
|
||||
active_data_task = asyncio.create_task(
|
||||
open_data_session(session),
|
||||
name=f"agent_data_session_{session}",
|
||||
)
|
||||
|
||||
finally:
|
||||
try:
|
||||
ctrl_writer.close()
|
||||
await ctrl_writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
if active_data_task is not None and not active_data_task.done():
|
||||
active_data_task.cancel()
|
||||
with contextlib.suppress(Exception):
|
||||
await active_data_task
|
||||
|
||||
log.info("Agent stopped.")
|
||||
with contextlib.suppress(Exception):
|
||||
ctrl_writer.close()
|
||||
with contextlib.suppress(Exception):
|
||||
await ctrl_writer.wait_closed()
|
||||
|
||||
log.info("Agent control session ended.")
|
||||
|
||||
|
||||
async def agent_supervisor(
|
||||
*,
|
||||
port: str,
|
||||
baudrate: int,
|
||||
server: str,
|
||||
serial: str,
|
||||
password: str,
|
||||
) -> None:
|
||||
|
||||
delay = RECONNECT_MIN_S
|
||||
while True:
|
||||
try:
|
||||
await agent_run_once(
|
||||
port=port,
|
||||
baudrate=baudrate,
|
||||
server=server,
|
||||
serial=serial,
|
||||
password=password,
|
||||
)
|
||||
delay = RECONNECT_MIN_S
|
||||
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.exception("Agent crashed: %s", e)
|
||||
|
||||
log.info("Reconnect in %.1f sec...", delay)
|
||||
await asyncio.sleep(delay)
|
||||
delay = min(RECONNECT_MAX_S, delay * 1.7)
|
||||
|
||||
|
||||
async def main_async(
|
||||
@@ -188,7 +256,7 @@ async def main_async(
|
||||
serial: str,
|
||||
password: str,
|
||||
) -> None:
|
||||
await agent_run(
|
||||
await agent_supervisor(
|
||||
port=port,
|
||||
baudrate=baudrate,
|
||||
server=server,
|
||||
@@ -224,4 +292,3 @@ def cli() -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user