diff --git a/.env b/.env new file mode 100644 index 0000000..8c1502c --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +CANEXPLORER_LOG_FORMAT="%(asctime)s.%(msecs)03d [%(levelname)-7s] %(name)-35s: %(message)s" +CANEXPLORER_LOG_LEVEL=INFO +CANEXPLORER_PROJECT_NAME=CANExplorer +PIPENV_VERBOSITY=-1 diff --git a/.gitignore b/.gitignore index 4f3e4ac..597d6d1 100644 --- a/.gitignore +++ b/.gitignore @@ -102,7 +102,6 @@ celerybeat.pid *.sage.py # Environments -.env .venv env/ venv/ diff --git a/Pipfile b/Pipfile index 098c5b4..bddb808 100644 --- a/Pipfile +++ b/Pipfile @@ -29,3 +29,4 @@ python_version = "3.12" [scripts] main = "python -m can_explorer" +create-can = "sudo modprobe vcan && sudo ip link add dev vcan0 type vcan && sudo ip link set vcan0 up" diff --git a/README.md b/README.md index b5f1c70..3603ca7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # CANExplorer -A CANbus-Analyse Tool for all platform. +A CANbus-Analyse Tool for all platform. +This is a WIP Project! ## Roadmap @@ -8,9 +9,11 @@ A CANbus-Analyse Tool for all platform. - [x] Set basic project structure - [x] Set package manager and install packages - [x] Set docker and vscode devcontainer - - [x] Add basic working code -2. Transceive CAN messages - - [ ] Process IsoCan Frames - - [ ] Process ISoTp Frames - - [ ] Process J1939 Frames - - [ ] Process OpenCAN Frames \ No newline at end of file + - [x] Add parsing and transmission of CAN frame as proof of concept + - [x] Release 0.1.0 +2. Transception of CAN frames. + - [ ] Implement IsoCan transport and protocol + - [ ] Implement J1939 transport and protocol + - [ ] Implement ISOTP transport and protocol + - [ ] Implement CanOpen transport and protocol + - [ ] Release 0.2.0 diff --git a/can_explorer/__init__.py b/can_explorer/__init__.py index 8b48a4a..6b07d53 100644 --- a/can_explorer/__init__.py +++ b/can_explorer/__init__.py @@ -5,4 +5,3 @@ PROJECT_NAME = config('CANEXPLORER_PROJECT_NAME', cast=str) PROJECT_BUILD_DATE = '12 October 2024' PROJECT_PLATFORM = 'Linux' - diff --git a/can_explorer/gui/base_worker.py b/can_explorer/gui/base_worker.py index 451b090..3f222de 100644 --- a/can_explorer/gui/base_worker.py +++ b/can_explorer/gui/base_worker.py @@ -24,15 +24,14 @@ def __init__(self, func, *args, **kwargs): @pyqtSlot() def run(self): - # try: + try: logger.info(self._func) result = self._func(*self._args, **self._kwargs) - # except Exception as e: - # traceback.print_exc() - # logger.error(f'An error occurred while running task: {e}') - # self._signals.error.emit(e) - # else: - # self._signals.result.emit(result) - # finally: - # self._signals.finished.emit() - + except Exception as e: + traceback.print_exc() + logger.error(f'An error occurred while running task: {e}') + self._signals.error.emit(e) + else: + self._signals.result.emit(result) + finally: + self._signals.finished.emit() diff --git a/can_explorer/gui/can_raw_viewer.py b/can_explorer/gui/can_raw_viewer.py index 700a892..1aa4e24 100644 --- a/can_explorer/gui/can_raw_viewer.py +++ b/can_explorer/gui/can_raw_viewer.py @@ -1,9 +1,12 @@ import logging + +import can from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot, QModelIndex, QThreadPool from can.message import Message -from typing import Dict +from typing import Dict, List from PyQt6.QtWidgets import QHeaderView +import asyncio from can_explorer.gui.can_worker import CanWorker from can_explorer.util.canutils import CanConfiguration @@ -12,15 +15,15 @@ class RawCanViewerModel(QtCore.QAbstractTableModel): - HEADER_ROWS = ('Time', 'Tx/RX', 'Message Type', 'Arbitration ID', 'DLC', 'Data Bytes') + HEADER_ROWS = ('Time [s]', 'Tx/RX', 'Message Type', 'Arbitration ID [hex]', 'DLC [hex]', 'Data Bytes [hex]') def __init__(self): super(RawCanViewerModel, self).__init__() + self._data: List[can.Message] = [] self.configure() def configure(self): pass - # self.setHeaderData(0, Qt.Orientation.Horizontal, ['timestamp', 'DLC']) def headerData(self, section, orientation, role, *args, **kwargs): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: @@ -28,13 +31,57 @@ def headerData(self, section, orientation, role, *args, **kwargs): return super().headerData(section, orientation, role) def rowCount(self, parent) -> int: - return len(self.HEADER_ROWS) + return len(self._data) - def columnCount(self, parent): + def columnCount(self, parent) -> int: return len(self.HEADER_ROWS) + @staticmethod + def format_data(value): + match value: + case float(): + return f'{value: 8.5f}' + case int(): + return hex(value) + case bytearray(): + return ' '.join([f"{x:02X}" for x in value]) + return value + def data(self, index: QModelIndex, role): - return range(len(self.HEADER_ROWS)) + row = index.row() + col = index.column() + if role == QtCore.Qt.ItemDataRole.DisplayRole: + data = self._data[row] + row_data = ( + data.timestamp, + 'Rx' if data.is_rx else 'Tx', + 'F' if data.is_fd else 'S', + data.arbitration_id, + data.dlc, + data.data, + ) + return self.format_data(row_data[col]) + elif role == QtCore.Qt.ItemDataRole.TextAlignmentRole: + aligment = QtCore.Qt.AlignmentFlag + row_pos = ( + aligment.AlignRight, + aligment.AlignCenter, + aligment.AlignCenter, + aligment.AlignRight, + aligment.AlignRight, + aligment.AlignLeft, + ) + return row_pos[col] | aligment.AlignVCenter + + def flags(self, index: QModelIndex): + return QtCore.Qt.ItemFlag.ItemIsSelectable + + def insert(self, data: can.Message): + logger.info(f'Added {data=} to container') + self._data.append(data) + # self.dataChanged.emit() + # self.modelReset.emit() + self.layoutChanged.emit() class RawCanViewerView(QtWidgets.QTableView): @@ -43,8 +90,8 @@ class RawCanViewerView(QtWidgets.QTableView): def __init__(self, configuration: CanConfiguration): super().__init__() self._configuration = configuration - self._can_handler = CanWorker(self._configuration) self._model = self._configure() + self._can_handler = CanWorker(self._configuration, lambda x: self._model.insert(x)) self._connect_signals() @property @@ -53,6 +100,8 @@ def configuration_data(self) -> CanConfiguration: def start_listening(self, threadpool: QThreadPool): threadpool.start(self._can_handler) + # self._can_handler.protocol.on_data_received.connect(lambda x: logger.info(f'Received CAN message: {x}')) + logger.info('Signal connected') def _configure(self): model = RawCanViewerModel() @@ -67,4 +116,4 @@ def _connect_signals(self): @pyqtSlot(Message) def add_can_raw_message(self, message: Message): logger.info(f'Received {message=}') - # self._model.insertRow() \ No newline at end of file + # self._model.insertRow() diff --git a/can_explorer/gui/can_worker.py b/can_explorer/gui/can_worker.py index b09463c..3994af3 100644 --- a/can_explorer/gui/can_worker.py +++ b/can_explorer/gui/can_worker.py @@ -1,16 +1,20 @@ import asyncio from can_explorer.gui.base_worker import Worker from can_explorer.transport.can_connection import create_can_connection +from can_explorer.transport.isocan import IsoCanProtocol, IsoCanTransport from can_explorer.util.canutils import CanConfiguration +from typing import Optional import logging logger = logging.getLogger(__name__) class CanWorker(Worker): - def __init__(self, config: CanConfiguration): + def __init__(self, config: CanConfiguration, on_data_received): self._config = config - self._protocol, self._transport = None, None + self.protocol: Optional[IsoCanProtocol] = None + self.transport: Optional[IsoCanTransport] = None + self._on_data_received = on_data_received self._progress_callback = None self._configure() super().__init__(self.start_listening) @@ -22,7 +26,7 @@ def start_listening(self, progress_callback): try: running_loop = None self._progress_callback = progress_callback - self._protocol, self._transport = create_can_connection( + self.protocol, self.transport = create_can_connection( running_loop, protocol_factory=None, url=None, @@ -30,7 +34,8 @@ def start_listening(self, progress_callback): interface=self._config.interface, fd=self._config.fd, ) - self._transport._parse_can_frames() + self.protocol.on_data_received.connect(self._on_data_received) + self.transport._parse_can_frames() except Exception as e: logger.error(f'Error while listening to can frame: {e}') self._signals.error.emit(e) diff --git a/can_explorer/gui/main_window.py b/can_explorer/gui/main_window.py index 319ee52..5061ee4 100644 --- a/can_explorer/gui/main_window.py +++ b/can_explorer/gui/main_window.py @@ -1,6 +1,4 @@ import asyncio -from getpass import win_getpass - from PyQt6.uic import loadUi from PyQt6.QtCore import QSize, Qt, pyqtSlot, QFile, QStringEncoder, QThreadPool from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QTabWidget @@ -73,4 +71,4 @@ def _connect_to_bus(self): else: logger.warning(f'Connecting to an unexpected widget. Skipping ...') except Exception as e: - logger.error(f'Could not connect to bus: {e}') \ No newline at end of file + logger.error(f'Could not connect to bus: {e}') diff --git a/can_explorer/gui/new_connection_dialog.py b/can_explorer/gui/new_connection_dialog.py index ffca108..e4727fd 100644 --- a/can_explorer/gui/new_connection_dialog.py +++ b/can_explorer/gui/new_connection_dialog.py @@ -26,7 +26,7 @@ def __init__(self, parent=None, app=None): def _configure(self): self.connection_name_box.setText('Connection 1') - supported_bitrates = canutils.SupportedProtocols.IsoCAN.get_supported_baudrates() + supported_bitrates = canutils.SupportedProtocols.IsoCAN.supported_bitrates self.bitrate_box.addItems(list(map(str, supported_bitrates))) supported_interfaces = canutils.get_supported_interfaces() self.interface_box.addItems(sorted(list([description for name, description in supported_interfaces]))) @@ -48,7 +48,7 @@ def accept(self): interface=canutils.get_interface_name(self.interface_box.currentText()), channel=self.channel_box.currentText(), protocol=self.protocol_box.currentText(), - fd = self.flexible_data_checkbox.isChecked() + fd=self.flexible_data_checkbox.isChecked(), ) self.on_connection_added.emit(can_configuration) - super().accept() \ No newline at end of file + super().accept() diff --git a/can_explorer/gui/qt/stylesheet.qss b/can_explorer/gui/qt/stylesheet.qss index 2beeff4..0c4583a 100644 --- a/can_explorer/gui/qt/stylesheet.qss +++ b/can_explorer/gui/qt/stylesheet.qss @@ -8,4 +8,8 @@ QTabBar::tab:selected { } QTabWidget::pane { +} + +QTableView::item { + text-align: right; } \ No newline at end of file diff --git a/can_explorer/transport/isocan.py b/can_explorer/transport/isocan.py index 594b276..98c258d 100644 --- a/can_explorer/transport/isocan.py +++ b/can_explorer/transport/isocan.py @@ -10,15 +10,15 @@ class IsoCanProtocol(asyncio.Protocol, QWidget): - # __slot__ = ('_transport', '_on_con_lost', '_data_received_queue', '_error_queue', 'on_data_received') + __slot__ = ('_transport', '_on_con_lost', '_data_received_queue', '_error_queue', 'on_data_received') on_data_received = pyqtSignal(can.Message) def __init__(self, on_con_lost) -> None: + super().__init__() self._transport = None self._on_con_lost = on_con_lost self._data_received_queue = asyncio.Queue() self._error_queue = asyncio.Queue() - super().__init__() self.on_data_received.emit(can.Message()) def connection_made(self, transport: asyncio.BaseTransport) -> None: diff --git a/can_explorer/util/canutils.py b/can_explorer/util/canutils.py index e065114..3d76a17 100644 --- a/can_explorer/util/canutils.py +++ b/can_explorer/util/canutils.py @@ -4,8 +4,6 @@ from typing import List, Tuple, Dict, Optional from dataclasses import dataclass -from attr.setters import frozen - logger = logging.getLogger(__name__) @@ -16,21 +14,24 @@ class SupportedProtocols(enum.IntEnum): j1939 = enum.auto() canopen = enum.auto() - def get_supported_baudrates(self) -> List[int]: + @property + def supported_bitrates(self) -> List[int]: rates = None match self.value: case self.IsoCAN: - rates = [10000, 20000, 50000, 100000, 125000, 250000, 500000, - 800000, 1000000] + rates = [10_000, 20_000, 50_000, 100_000, 125_000, 250_000, 500_000, 800_000, 1_000_000] return rates def get_supported_interfaces() -> List[Tuple[str]]: - supported_interfaces = [(interface, can.interfaces.BACKENDS[interface][1]) for interface in list(can.interfaces.VALID_INTERFACES)] + supported_interfaces = [ + (interface, can.interfaces.BACKENDS[interface][1]) for interface in list(can.interfaces.VALID_INTERFACES) + ] return supported_interfaces + def get_available_channels(interfaces: List[str]) -> List[Dict]: - configs = can.interface.detect_available_configs(interfaces) + configs = can.interface.detect_available_configs(interfaces) logger.info(f'{configs=}') return configs @@ -38,6 +39,7 @@ def get_available_channels(interfaces: List[str]) -> List[Dict]: def load_config(): return {} + def get_interface_name(target_class_name: str) -> Optional[str]: for interface_name, (module_name, class_name) in can.interfaces.BACKENDS.items(): if class_name == target_class_name: @@ -53,4 +55,3 @@ class CanConfiguration: channel: str protocol: str fd: bool -