mirror of
https://github.com/controllerzz/carbus_lib.git
synced 2026-07-15 16:30:16 +03:00
save
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from .device import CarBusDevice
|
||||
from .messages import CanMessage, MessageDirection
|
||||
from .exceptions import CarBusError, CommandError, SyncError
|
||||
|
||||
__all__ = [
|
||||
"CarBusDevice",
|
||||
"CanMessage",
|
||||
"MessageDirection",
|
||||
"CarBusError",
|
||||
"CommandError",
|
||||
"SyncError",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
class CarBusError(Exception):
|
||||
...
|
||||
|
||||
|
||||
class SyncError(CarBusError):
|
||||
...
|
||||
|
||||
|
||||
class CommandError(CarBusError):
|
||||
...
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from .protocol import BusMessageFlags
|
||||
|
||||
|
||||
class MessageDirection(str, Enum):
|
||||
RX = "rx"
|
||||
TX = "tx"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CanMessage:
|
||||
can_id: int
|
||||
data: bytes = b""
|
||||
extended: bool = False
|
||||
rtr: bool = False
|
||||
fd: bool = False
|
||||
brs: bool = False
|
||||
timestamp_us: int = 0
|
||||
|
||||
@property
|
||||
def dlc(self) -> int:
|
||||
return len(self.data)
|
||||
|
||||
@classmethod
|
||||
def from_bus_payload(
|
||||
cls,
|
||||
*,
|
||||
flags: BusMessageFlags,
|
||||
timestamp_us: int,
|
||||
can_id: int,
|
||||
dlc: int,
|
||||
data: bytes,
|
||||
) -> "CanMessage":
|
||||
extended = bool(flags & BusMessageFlags.EXTID)
|
||||
rtr = bool(flags & BusMessageFlags.RTR)
|
||||
fd = bool(flags & BusMessageFlags.FDF)
|
||||
brs = bool(flags & BusMessageFlags.BRS)
|
||||
|
||||
payload = data[:dlc]
|
||||
|
||||
return cls(
|
||||
can_id=can_id,
|
||||
data=payload,
|
||||
extended=extended,
|
||||
rtr=rtr,
|
||||
fd=fd,
|
||||
brs=brs,
|
||||
timestamp_us=timestamp_us,
|
||||
)
|
||||
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum, IntFlag
|
||||
|
||||
|
||||
class Command(IntEnum):
|
||||
SYNC = 0xA5
|
||||
DEVICE_INFO = 0x06
|
||||
DEVICE_OPEN = 0x08
|
||||
DEVICE_CLOSE = 0x09
|
||||
CHANNEL_CONFIG = 0x11
|
||||
CHANNEL_OPEN = 0x18
|
||||
|
||||
FILTER_SET = 0x21 # COMMAND_FILTER_SET
|
||||
FILTER_CLEAR = 0x22 # COMMAND_FILTER_CLEAR
|
||||
|
||||
MESSAGE = 0x40
|
||||
BUS_ERROR = 0x48
|
||||
|
||||
ERROR = 0xFF
|
||||
|
||||
|
||||
class FilterType(IntEnum):
|
||||
STD_11BIT = 0x00
|
||||
EXT_29BIT = 0x01
|
||||
|
||||
|
||||
EXTENDED_HEADER_COMMANDS = {Command.MESSAGE, Command.BUS_ERROR}
|
||||
|
||||
|
||||
class HeaderFlags(IntFlag):
|
||||
NONE = 0x0000
|
||||
|
||||
CHANNEL_1 = 0x2000
|
||||
CHANNEL_2 = 0x4000
|
||||
CHANNEL_3 = 0x6000
|
||||
CHANNEL_4 = 0x8000
|
||||
|
||||
CONFIRM_REQUIRED = 0x0001
|
||||
|
||||
|
||||
class BusMessageFlags(IntFlag):
|
||||
NONE = 0x00000000
|
||||
|
||||
EXTID = 0x00000001
|
||||
RTR = 0x00000002
|
||||
FDF = 0x00000004
|
||||
BRS = 0x00000008
|
||||
ESI = 0x00000010
|
||||
|
||||
ERROR_FRAME = 0x01000000
|
||||
RX = 0x10000000
|
||||
TX = 0x20000000
|
||||
BLOCK_TX = 0x30000000
|
||||
|
||||
|
||||
CC_MULTIWORD = 0x80000000
|
||||
|
||||
DI_MULTIWORD = CC_MULTIWORD
|
||||
|
||||
DI_HARDWARE_ID = 0x01000000 # тип устройства (HWIdentifiers)
|
||||
DI_FIRMWARE_VERSION = 0x02000000 # строка версии прошивки (ASCII, MULTIWORD)
|
||||
DI_DEVICE_SERIAL = 0x03000000 # серийный номер (бинарно, MULTIWORD)
|
||||
DI_FEATURES = 0x11000000 # битовая маска общих фич
|
||||
DI_CHANNEL_MAP = 0x12000000 # карта каналов (тип канала)
|
||||
DI_CHANNEL_FEATURES = 0x13000000 # опции по каналам (ALC, TERMINATOR, ...)
|
||||
DI_FILTER = 0x14000000 # настройки фильтров по каналам
|
||||
DI_GATEWAY = 0x15000000 # возможные пробросы между каналами
|
||||
DI_CHANNEL_FREQUENCY = 0x16000000 # частота работы CAN/CAN-FD модуля
|
||||
DI_ISOTP = 0x21000000 # размер ISO-TP буфера
|
||||
DI_TX_BUFFER = 0x22000000 # размер буфера трейсера (в сообщениях)
|
||||
DI_TX_TASK = 0x23000000 # количество задач периодической отправки
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandHeader:
|
||||
command: int
|
||||
sequence: int
|
||||
flags: int
|
||||
dsize: int
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> "CommandHeader":
|
||||
if len(data) != 4:
|
||||
raise ValueError("CommandHeader требует ровно 4 байта")
|
||||
cmd, seq, flg, size = data
|
||||
return cls(cmd, seq, flg, size)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return bytes(
|
||||
(
|
||||
self.command & 0xFF,
|
||||
self.sequence & 0xFF,
|
||||
self.flags & 0xFF,
|
||||
self.dsize & 0xFF,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MsgCommandHeader:
|
||||
command: int
|
||||
sequence: int
|
||||
flags: int
|
||||
dsize: int
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> "MsgCommandHeader":
|
||||
if len(data) != 6:
|
||||
raise ValueError("MsgCommandHeader требует ровно 6 байт")
|
||||
cmd = data[0]
|
||||
seq = data[1]
|
||||
flags = int.from_bytes(data[2:4], "little")
|
||||
dsize = int.from_bytes(data[4:6], "little")
|
||||
return cls(cmd, seq, flags, dsize)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return (
|
||||
bytes(
|
||||
(
|
||||
self.command & 0xFF,
|
||||
self.sequence & 0xFF,
|
||||
)
|
||||
)
|
||||
+ self.flags.to_bytes(2, "little")
|
||||
+ self.dsize.to_bytes(2, "little")
|
||||
)
|
||||
|
||||
|
||||
def is_ack(cmd: int) -> bool:
|
||||
return 0x80 <= cmd < 0xFF
|
||||
|
||||
|
||||
def base_command_from_ack(cmd: int) -> int:
|
||||
return cmd & 0x7F
|
||||
|
||||
|
||||
def need_extended_header(command: int) -> bool:
|
||||
try:
|
||||
c = Command(command)
|
||||
except ValueError:
|
||||
return False
|
||||
return c in EXTENDED_HEADER_COMMANDS
|
||||
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import serial_asyncio
|
||||
|
||||
log = logging.getLogger("carbus_async.tcp_bridge")
|
||||
|
||||
|
||||
async def _pump(
|
||||
src: asyncio.StreamReader,
|
||||
dst: asyncio.StreamWriter,
|
||||
direction: str,
|
||||
chunk_size: int = 4096,
|
||||
) -> None:
|
||||
try:
|
||||
while True:
|
||||
data = await src.read(chunk_size)
|
||||
if not data:
|
||||
log.debug("%s: EOF", direction)
|
||||
break
|
||||
dst.write(data)
|
||||
await dst.drain()
|
||||
except asyncio.CancelledError:
|
||||
log.debug("%s: cancelled", direction)
|
||||
raise
|
||||
except Exception as e:
|
||||
log.exception("%s: exception: %s", direction, e)
|
||||
finally:
|
||||
try:
|
||||
dst.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def handle_client(
|
||||
tcp_reader: asyncio.StreamReader,
|
||||
tcp_writer: asyncio.StreamWriter,
|
||||
*,
|
||||
serial_port: str,
|
||||
baudrate: int = 115200,
|
||||
) -> None:
|
||||
peer = tcp_writer.get_extra_info("peername")
|
||||
log.info("Client connected: %s", peer)
|
||||
|
||||
try:
|
||||
serial_reader, serial_writer = await serial_asyncio.open_serial_connection(
|
||||
url=serial_port,
|
||||
baudrate=baudrate,
|
||||
)
|
||||
log.info("Opened local serial %s @ %d for %s", serial_port, baudrate, peer)
|
||||
|
||||
pump_tcp_to_serial = asyncio.create_task(
|
||||
_pump(tcp_reader, serial_writer, f"{peer} tcp->serial")
|
||||
)
|
||||
pump_serial_to_tcp = asyncio.create_task(
|
||||
_pump(serial_reader, tcp_writer, f"{peer} serial->tcp")
|
||||
)
|
||||
|
||||
done, pending = await asyncio.wait(
|
||||
[pump_tcp_to_serial, pump_serial_to_tcp],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
with asyncio.SuppressCancelledError if hasattr(asyncio, "SuppressCancelledError") else nullcontext():
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
log.exception("Error in handle_client %s: %s", peer, e)
|
||||
finally:
|
||||
try:
|
||||
tcp_writer.close()
|
||||
except Exception:
|
||||
pass
|
||||
log.info("Client disconnected: %s", peer)
|
||||
|
||||
|
||||
async def run_tcp_bridge(
|
||||
*,
|
||||
listen_host: str = "0.0.0.0",
|
||||
listen_port: int = 7000,
|
||||
serial_port: str = "COM6",
|
||||
baudrate: int = 115200,
|
||||
) -> None:
|
||||
server = await asyncio.start_server(
|
||||
lambda r, w: handle_client(r, w, serial_port=serial_port, baudrate=baudrate),
|
||||
listen_host,
|
||||
listen_port,
|
||||
)
|
||||
|
||||
addr = ", ".join(str(sock.getsockname()) for sock in server.sockets or [])
|
||||
log.info(
|
||||
"TCP bridge listening on %s -> serial %s @ %d",
|
||||
addr,
|
||||
serial_port,
|
||||
baudrate,
|
||||
)
|
||||
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="TCP bridge for CarBus device (raw byte forwarder)."
|
||||
)
|
||||
parser.add_argument("--host", default="0.0.0.0", help="Listen host (default 0.0.0.0)")
|
||||
parser.add_argument("--port", type=int, default=7000, help="Listen TCP port (default 7000)")
|
||||
parser.add_argument("--serial", required=True, help="Local serial port (e.g. COM6, /dev/ttyACM0)")
|
||||
parser.add_argument("--baudrate", type=int, default=115200, help="Serial baudrate (default 115200)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
async def _main() -> None:
|
||||
await run_tcp_bridge(
|
||||
listen_host=args.host,
|
||||
listen_port=args.port,
|
||||
serial_port=args.serial,
|
||||
baudrate=args.baudrate,
|
||||
)
|
||||
|
||||
asyncio.run(_main())
|
||||
Reference in New Issue
Block a user