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
  • Loading branch information
AKJ7 committed Oct 21, 2024
1 parent a8d624b commit 06d8fac
Show file tree
Hide file tree
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
Expand Up @@ -102,7 +102,6 @@ celerybeat.pid
*.sage.py

# Environments
.env
.venv
env/
venv/
Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Up @@ -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
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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)
Expand Down
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
Expand Down Expand Up @@ -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
Expand Up @@ -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])))
Expand All @@ -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
Expand Up @@ -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
Expand Up @@ -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:
Expand Down
17 changes: 9 additions & 8 deletions can_explorer/util/canutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass

from attr.setters import frozen

logger = logging.getLogger(__name__)


Expand All @@ -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:
Expand All @@ -53,4 +55,3 @@ class CanConfiguration:
channel: str
protocol: str
fd: bool

0 comments on commit 06d8fac

Please sign in to comment.