Skip to content

Commit

Permalink
Complete Milestone 0.1.0
Browse files Browse the repository at this point in the history
+ IsoCAN Frames can now be received and displayed in the GUI
AKJ7 committed Oct 21, 2024
1 parent a8d624b commit 06d8fac
Showing 13 changed files with 109 additions and 47 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -102,7 +102,6 @@ celerybeat.pid
*.sage.py

# Environments
.env
.venv
env/
venv/
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -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"
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# CANExplorer
A CANbus-Analyse Tool for all platform.
A CANbus-Analyse Tool for all platform.
<span style="color:red">This is a WIP Project!</span>


## 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
- [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
1 change: 0 additions & 1 deletion can_explorer/__init__.py
Original file line number Diff line number Diff line change
@@ -5,4 +5,3 @@
PROJECT_NAME = config('CANEXPLORER_PROJECT_NAME', cast=str)
PROJECT_BUILD_DATE = '12 October 2024'
PROJECT_PLATFORM = 'Linux'

19 changes: 9 additions & 10 deletions can_explorer/gui/base_worker.py
Original file line number Diff line number Diff line change
@@ -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()
65 changes: 57 additions & 8 deletions can_explorer/gui/can_raw_viewer.py
Original file line number Diff line number Diff line change
@@ -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,29 +15,73 @@


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:
return self.HEADER_ROWS[section]
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()
# self._model.insertRow()
13 changes: 9 additions & 4 deletions can_explorer/gui/can_worker.py
Original file line number Diff line number Diff line change
@@ -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,15 +26,16 @@ 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,
channel=self._config.channel,
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)
4 changes: 1 addition & 3 deletions can_explorer/gui/main_window.py
Original file line number Diff line number Diff line change
@@ -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}')
logger.error(f'Could not connect to bus: {e}')
6 changes: 3 additions & 3 deletions can_explorer/gui/new_connection_dialog.py
Original file line number Diff line number Diff line change
@@ -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()
super().accept()
4 changes: 4 additions & 0 deletions can_explorer/gui/qt/stylesheet.qss
Original file line number Diff line number Diff line change
@@ -8,4 +8,8 @@ QTabBar::tab:selected {
}

QTabWidget::pane {
}

QTableView::item {
text-align: right;
}
4 changes: 2 additions & 2 deletions can_explorer/transport/isocan.py
Original file line number Diff line number Diff line change
@@ -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:
17 changes: 9 additions & 8 deletions can_explorer/util/canutils.py
Original file line number Diff line number Diff line change
@@ -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,28 +14,32 @@ 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


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

0 comments on commit 06d8fac

Please sign in to comment.