Extract reusable device protocols, ports and logic analyzers from SETGUI
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
"""WinUSB transport for candleLight/gs_usb CAN adapters.
|
||||
|
||||
The native library is the same Candle API used by CANgaroo. This module keeps
|
||||
all ctypes details out of the UI and exposes a small Qt-friendly connection.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes as ct
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import threading
|
||||
import re
|
||||
|
||||
from .qt_compat import QObject, Signal
|
||||
|
||||
|
||||
_EXTENDED_ID = 0x80000000
|
||||
_FRAME_RECEIVE = 1
|
||||
_MODE_LISTEN_ONLY = 0x0001
|
||||
|
||||
|
||||
class CandleError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CandleChannel:
|
||||
path: str
|
||||
channel: int
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
lower = self.path.lower()
|
||||
product = "CANnectivity" if "vid_1209&pid_ca01" in lower else "candle"
|
||||
return f"{product} — канал {self.channel}"
|
||||
|
||||
@property
|
||||
def adapter_kind(self) -> str:
|
||||
return "candle"
|
||||
|
||||
|
||||
class _Frame(ct.Structure):
|
||||
_pack_ = 1
|
||||
_fields_ = [
|
||||
("echo_id", ct.c_uint32), ("can_id", ct.c_uint32),
|
||||
("can_dlc", ct.c_uint8), ("channel", ct.c_uint8),
|
||||
("flags", ct.c_uint8), ("reserved", ct.c_uint8),
|
||||
("data", ct.c_uint8 * 8), ("timestamp_us", ct.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
def _library_path() -> Path:
|
||||
explicit = os.environ.get('CANDLE_LIBRARY')
|
||||
if explicit:
|
||||
return Path(explicit)
|
||||
bundled = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parents[1]))
|
||||
candidates = (
|
||||
Path(__file__).resolve().parents[1] / "native" / "candle.dll",
|
||||
bundled / "native" / "candle.dll",
|
||||
bundled / "candle.dll",
|
||||
)
|
||||
return next((path for path in candidates if path.exists()), candidates[0])
|
||||
|
||||
|
||||
class _Api:
|
||||
def __init__(self) -> None:
|
||||
path = _library_path()
|
||||
if sys.platform != "win32" or not path.exists():
|
||||
raise CandleError(f"Candle API недоступен: {path}")
|
||||
self.dll = ct.WinDLL(str(path))
|
||||
handle = ct.c_void_p
|
||||
self._fn("candle_list_scan", [ct.POINTER(handle)])
|
||||
self._fn("candle_list_free", [handle])
|
||||
self._fn("candle_list_length", [handle, ct.POINTER(ct.c_uint8)])
|
||||
self._fn("candle_dev_get", [handle, ct.c_uint8, ct.POINTER(handle)])
|
||||
self._fn("candle_dev_get_path", [handle], ct.c_wchar_p)
|
||||
self._fn("candle_dev_open", [handle])
|
||||
self._fn("candle_dev_close", [handle])
|
||||
self._fn("candle_dev_free", [handle])
|
||||
self._fn("candle_dev_last_error", [handle], ct.c_int)
|
||||
self._fn("candle_channel_count", [handle, ct.POINTER(ct.c_uint8)])
|
||||
self._fn("candle_channel_set_bitrate", [handle, ct.c_uint8, ct.c_uint32])
|
||||
self._fn("candle_channel_start", [handle, ct.c_uint8, ct.c_uint32])
|
||||
self._fn("candle_channel_stop", [handle, ct.c_uint8])
|
||||
self._fn("candle_frame_send", [handle, ct.c_uint8, ct.POINTER(_Frame)])
|
||||
self._fn("candle_frame_read", [handle, ct.POINTER(_Frame), ct.c_uint32])
|
||||
self._fn("candle_frame_type", [ct.POINTER(_Frame)], ct.c_int)
|
||||
self._fn("candle_frame_id", [ct.POINTER(_Frame)], ct.c_uint32)
|
||||
|
||||
def _fn(self, name: str, args: list, result=ct.c_bool) -> None:
|
||||
function = getattr(self.dll, name)
|
||||
function.argtypes = args
|
||||
function.restype = result
|
||||
|
||||
def scan(self) -> list[CandleChannel]:
|
||||
result: list[CandleChannel] = []
|
||||
physical_devices: set[str] = set()
|
||||
listing = ct.c_void_p()
|
||||
if not self.dll.candle_list_scan(ct.byref(listing)):
|
||||
raise CandleError("Не удалось выполнить поиск candle-адаптеров")
|
||||
try:
|
||||
count = ct.c_uint8()
|
||||
if not self.dll.candle_list_length(listing, ct.byref(count)):
|
||||
raise CandleError("Candle API не вернул список устройств")
|
||||
for index in range(count.value):
|
||||
device = ct.c_void_p()
|
||||
if not self.dll.candle_dev_get(listing, index, ct.byref(device)):
|
||||
continue
|
||||
try:
|
||||
if not self.dll.candle_dev_open(device):
|
||||
continue
|
||||
channels = ct.c_uint8()
|
||||
if self.dll.candle_channel_count(device, ct.byref(channels)):
|
||||
path = self.dll.candle_dev_get_path(device) or ""
|
||||
# Composite gs_usb devices expose MI_00, MI_02, ... as
|
||||
# separate Windows paths although each path reports all
|
||||
# CAN channels. CANgaroo folds them into one device.
|
||||
key = re.sub(r"&mi_[0-9a-f]+", "", path.casefold())
|
||||
key = re.sub(r"&[0-9a-f]{4}(?=#\{)", "", key)
|
||||
if key not in physical_devices:
|
||||
physical_devices.add(key)
|
||||
result.extend(CandleChannel(path, channel)
|
||||
for channel in range(channels.value))
|
||||
self.dll.candle_dev_close(device)
|
||||
finally:
|
||||
self.dll.candle_dev_free(device)
|
||||
finally:
|
||||
self.dll.candle_list_free(listing)
|
||||
return result
|
||||
|
||||
def acquire(self, wanted_path: str) -> ct.c_void_p:
|
||||
listing = ct.c_void_p()
|
||||
if not self.dll.candle_list_scan(ct.byref(listing)):
|
||||
raise CandleError("Не удалось обновить список candle-адаптеров")
|
||||
try:
|
||||
count = ct.c_uint8()
|
||||
self.dll.candle_list_length(listing, ct.byref(count))
|
||||
for index in range(count.value):
|
||||
device = ct.c_void_p()
|
||||
if not self.dll.candle_dev_get(listing, index, ct.byref(device)):
|
||||
continue
|
||||
path = self.dll.candle_dev_get_path(device) or ""
|
||||
if path.casefold() == wanted_path.casefold():
|
||||
return device
|
||||
self.dll.candle_dev_free(device)
|
||||
finally:
|
||||
self.dll.candle_list_free(listing)
|
||||
raise CandleError("Выбранный candle-адаптер больше не подключён")
|
||||
|
||||
|
||||
class CandleAdapter(QObject):
|
||||
"""One opened classic-CAN channel with a background RX loop."""
|
||||
|
||||
frame_received = Signal(int, bytes)
|
||||
capture_received = Signal(int, bytes, bool, bool)
|
||||
connection_lost = Signal(str)
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._api: _Api | None = None
|
||||
self._device: ct.c_void_p | None = None
|
||||
self._channel = 0
|
||||
self._stop = threading.Event()
|
||||
self._reader: threading.Thread | None = None
|
||||
self._send_lock = threading.Lock()
|
||||
self._listen_only = False
|
||||
|
||||
def _get_api(self) -> _Api:
|
||||
if self._api is None:
|
||||
self._api = _Api()
|
||||
return self._api
|
||||
|
||||
def scan(self) -> list[CandleChannel]:
|
||||
return self._get_api().scan()
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
return self._device is not None
|
||||
|
||||
def connect_channel(self, channel: CandleChannel, bitrate: int,
|
||||
listen_only: bool = False) -> None:
|
||||
self.disconnect_channel()
|
||||
api = self._get_api()
|
||||
device = api.acquire(channel.path)
|
||||
try:
|
||||
if not api.dll.candle_dev_open(device):
|
||||
raise CandleError("Не удалось открыть candle-адаптер")
|
||||
if not api.dll.candle_channel_set_bitrate(device, channel.channel, bitrate):
|
||||
raise CandleError(f"Адаптер не поддерживает {bitrate} бит/с")
|
||||
mode = _MODE_LISTEN_ONLY if listen_only else 0
|
||||
if not api.dll.candle_channel_start(device, channel.channel, mode):
|
||||
raise CandleError("Не удалось запустить CAN-канал")
|
||||
except Exception:
|
||||
api.dll.candle_dev_close(device)
|
||||
api.dll.candle_dev_free(device)
|
||||
raise
|
||||
self._device = device
|
||||
self._channel = channel.channel
|
||||
self._listen_only = listen_only
|
||||
self._stop.clear()
|
||||
self._reader = threading.Thread(target=self._read_loop,
|
||||
name="candle-rx", daemon=True)
|
||||
self._reader.start()
|
||||
|
||||
def disconnect_channel(self) -> None:
|
||||
device, self._device = self._device, None
|
||||
if device is None:
|
||||
return
|
||||
self._stop.set()
|
||||
if self._reader is not None:
|
||||
self._reader.join(timeout=0.3)
|
||||
api = self._get_api()
|
||||
api.dll.candle_channel_stop(device, self._channel)
|
||||
api.dll.candle_dev_close(device)
|
||||
api.dll.candle_dev_free(device)
|
||||
self._reader = None
|
||||
self._listen_only = False
|
||||
|
||||
def send(self, can_id: int, data: bytes) -> None:
|
||||
device = self._device
|
||||
if device is None:
|
||||
raise CandleError("Candle-адаптер не подключён")
|
||||
if self._listen_only:
|
||||
raise CandleError("Candle-адаптер открыт в режиме только приёма")
|
||||
if len(data) > 8:
|
||||
raise CandleError("Classic CAN поддерживает не более 8 байт")
|
||||
frame = _Frame()
|
||||
frame.can_id = (can_id & 0x1FFFFFFF) | _EXTENDED_ID
|
||||
frame.can_dlc = len(data)
|
||||
frame.channel = self._channel
|
||||
frame.data[:len(data)] = data
|
||||
with self._send_lock:
|
||||
if not self._get_api().dll.candle_frame_send(
|
||||
device, self._channel, ct.byref(frame)):
|
||||
raise CandleError("Candle API не передал CAN-кадр")
|
||||
|
||||
def _read_loop(self) -> None:
|
||||
device = self._device
|
||||
if device is None:
|
||||
return
|
||||
api = self._get_api()
|
||||
while not self._stop.is_set() and self._device is device:
|
||||
frame = _Frame()
|
||||
if not api.dll.candle_frame_read(device, ct.byref(frame), 50):
|
||||
continue
|
||||
if (api.dll.candle_frame_type(ct.byref(frame)) == _FRAME_RECEIVE
|
||||
and frame.channel == self._channel):
|
||||
size = min(frame.can_dlc, 8)
|
||||
self.capture_received.emit(api.dll.candle_frame_id(ct.byref(frame)),
|
||||
bytes(frame.data[:size]), bool(frame.can_id & 0x80000000), bool(frame.can_id & 0x40000000))
|
||||
self.frame_received.emit(api.dll.candle_frame_id(ct.byref(frame)),
|
||||
bytes(frame.data[:size]))
|
||||
|
||||
def close(self) -> None:
|
||||
self.disconnect_channel()
|
||||
Reference in New Issue
Block a user