From 6e6e9340479fd9ebc52142e3ce52cdbcaee7e4bf Mon Sep 17 00:00:00 2001 From: Jules Atchoglo Date: Wed, 16 Oct 2024 19:41:51 +0200 Subject: [PATCH] Initial commit: Basic UI + Added basic UI + Added parsing and sending of ISOCan frames with FD support + Basic project structure --- .devcontainer/devcontainer.json | 63 +++++ .github/workflows/build.yaml | 60 +++++ .github/workflows/installer.yaml | 0 .github/workflows/publish.yaml | 0 .github/workflows/release.yaml | 0 .github/workflows/security.yaml | 0 .gitignore | 135 ++++++++++ .vscode/settings.json | 1 + .vscode/tasks.json | 12 + Dockerfile | 12 + LICENSE.md | 0 Pipfile | 31 +++ README.md | 16 ++ can_explorer/__init__.py | 8 + can_explorer/__main__.py | 40 +++ can_explorer/__version__.py | 27 ++ can_explorer/database/dbc.py | 1 + can_explorer/database/dbf.py | 0 can_explorer/gui/about_dialog.py | 50 ++++ can_explorer/gui/base_worker.py | 38 +++ can_explorer/gui/can_raw_viewer.py | 70 ++++++ can_explorer/gui/can_worker.py | 39 +++ can_explorer/gui/main_window.py | 76 ++++++ can_explorer/gui/new_connection_dialog.py | 54 ++++ can_explorer/gui/qt/about_dialog.ui | 151 ++++++++++++ can_explorer/gui/qt/main_window.ui | 245 +++++++++++++++++++ can_explorer/gui/qt/new_connection_dialog.ui | 228 +++++++++++++++++ can_explorer/gui/qt/stylesheet.qss | 11 + can_explorer/transport/base_protocol.py | 4 + can_explorer/transport/can_connection.py | 30 +++ can_explorer/transport/canopen.py | 0 can_explorer/transport/fdcan.py | 0 can_explorer/transport/isocan.py | 88 +++++++ can_explorer/transport/isotp.py | 91 +++++++ can_explorer/transport/j1939.py | 0 can_explorer/util/canutils.py | 56 +++++ can_explorer/util/gui.py | 9 + can_explorer/util/version.py | 41 ++++ can_server/__init__.py | 1 + can_server/__main__.py | 30 +++ can_server/cli/cli.py | 12 + can_server/core/heart.py | 43 ++++ can_server/core/server.py | 28 +++ docs/chapters/references.adoc | 4 + docs/full_doc.adoc | 92 +++++++ docs/todo.md | 9 + setup.cfg | 64 +++++ tests/unit/test_demo.py | 5 + 48 files changed, 1975 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/installer.yaml create mode 100644 .github/workflows/publish.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/security.yaml create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 Pipfile create mode 100644 README.md create mode 100644 can_explorer/__init__.py create mode 100644 can_explorer/__main__.py create mode 100644 can_explorer/__version__.py create mode 100644 can_explorer/database/dbc.py create mode 100644 can_explorer/database/dbf.py create mode 100644 can_explorer/gui/about_dialog.py create mode 100644 can_explorer/gui/base_worker.py create mode 100644 can_explorer/gui/can_raw_viewer.py create mode 100644 can_explorer/gui/can_worker.py create mode 100644 can_explorer/gui/main_window.py create mode 100644 can_explorer/gui/new_connection_dialog.py create mode 100644 can_explorer/gui/qt/about_dialog.ui create mode 100644 can_explorer/gui/qt/main_window.ui create mode 100644 can_explorer/gui/qt/new_connection_dialog.ui create mode 100644 can_explorer/gui/qt/stylesheet.qss create mode 100644 can_explorer/transport/base_protocol.py create mode 100644 can_explorer/transport/can_connection.py create mode 100644 can_explorer/transport/canopen.py create mode 100644 can_explorer/transport/fdcan.py create mode 100644 can_explorer/transport/isocan.py create mode 100644 can_explorer/transport/isotp.py create mode 100644 can_explorer/transport/j1939.py create mode 100644 can_explorer/util/canutils.py create mode 100644 can_explorer/util/gui.py create mode 100644 can_explorer/util/version.py create mode 100644 can_server/__init__.py create mode 100644 can_server/__main__.py create mode 100644 can_server/cli/cli.py create mode 100644 can_server/core/heart.py create mode 100644 can_server/core/server.py create mode 100644 docs/chapters/references.adoc create mode 100644 docs/full_doc.adoc create mode 100644 docs/todo.md create mode 100644 setup.cfg create mode 100644 tests/unit/test_demo.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..a713626 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,63 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile +{ + "name": "CanExplorer Container", + "build": { + // Sets the run context to one level up instead of the .devcontainer folder. + "context": "..", + // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. + "dockerfile": "../Dockerfile", + "args": { + "network": "host", + "xhost": "+" + } + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined", + "--privileged", + "--network=host" + ], + "appPort": [ + "69:69" + ], + "settings" : { + "git.path": "/usr/bin/git" + }, + "mounts": [ + "type=bind,source=/dev/bus/usb,target=/dev/bus/usb", // Make Debugger available. + "source=${localEnv:HOME}/.ssh,target=/home/devuser/.ssh,type=bind,consistency=cached", // Make ssh key available + "source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind,consistency=cached" // Mount graphic inside container to run GUIs + ], + "containerEnv": { + "DISPLAY": "${localEnv:DISPLAY}" + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "cat /etc/os-release", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "can_explorer", + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.vscode-docker", + "ms-python.python", + "ms-python.debugpy", + "donjayamanne.python-environment-manager", + "asciidoctor.asciidoctor-vscode", + "FedericoVarela.pipenv-scripts" + ] + } + } +} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..9d71f62 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,60 @@ +name: build + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + prettier: + name: Format code using black + runs-on: ubuntu-20.04 + timeout-minutes: 5 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Run black + uses: psf/back@stable + with: + options: "--check --verbose" + doctoc: + name: Build docs + runs-on: ubuntu-20.04 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + linter-py: + name: Lint code using PyLint + runs-on: ubuntu-20.04 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + build-gui-linux: + name: Build GUI for Linux (Ubuntu) + runs-on: ubuntu-20.04 + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + build-gui-for-windows: + name: Build GUI for Windows + runs-on: windows-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + build-gui-for-mac: + name: Build GUI for Mac + runs-on: macos-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 \ No newline at end of file diff --git a/.github/workflows/installer.yaml b/.github/workflows/installer.yaml new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f3e4ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,135 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Pipe +Pipfile.lock + +# Jetbrains +.idea/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..6c28385 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "echo", + "type": "shell", + "command": "echo Hello" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c16f080 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:noble + +ENV TZ="Europe/Berlin" +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt -y update +RUN apt -y install git vim python3 python3-pip pipenv + +RUN apt -y update +RUN apt -y install libglib2.0-0 libglu1-mesa-dev libxkbcommon-x11-0 build-essential libgl1-mesa-dev libdbus-1-dev libxcb-* + +RUN useradd -ms /bin/bash can_explorer \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e69de29 diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..098c5b4 --- /dev/null +++ b/Pipfile @@ -0,0 +1,31 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pyqt6 = "*" +pyinstaller = "*" +python-can = "*" +cantools = "*" +python-decouple = "*" +click = "*" +aiofile = "*" +aiohttp = "*" +numpy = "*" +matplotlib = "*" +schema = "*" + +[dev-packages] +black = "*" +mypy = "*" +pytest = "*" +pylint = "*" +pytest-schema = "*" +pre-commit = "*" + +[requires] +python_version = "3.12" + +[scripts] +main = "python -m can_explorer" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5f1c70 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# CANExplorer +A CANbus-Analyse Tool for all platform. + + +## Roadmap +1. Project Setup + - [x] Initialize git project + - [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 diff --git a/can_explorer/__init__.py b/can_explorer/__init__.py new file mode 100644 index 0000000..8b48a4a --- /dev/null +++ b/can_explorer/__init__.py @@ -0,0 +1,8 @@ +from decouple import config + +LOG_FORMAT = config('CANEXPLORER_LOG_FORMAT', cast=str) +LOG_LEVEL = config('CANEXPLORER_LOG_LEVEL', cast=str) +PROJECT_NAME = config('CANEXPLORER_PROJECT_NAME', cast=str) +PROJECT_BUILD_DATE = '12 October 2024' +PROJECT_PLATFORM = 'Linux' + diff --git a/can_explorer/__main__.py b/can_explorer/__main__.py new file mode 100644 index 0000000..2a9c892 --- /dev/null +++ b/can_explorer/__main__.py @@ -0,0 +1,40 @@ +import sys +import asyncio +import logging + +from PyQt6.QtWidgets import QApplication +from can_explorer import LOG_FORMAT, LOG_LEVEL, PROJECT_NAME +from can_explorer.gui.main_window import MainWindow +from can_explorer.transport.can_connection import create_can_connection +from qasync import QEventLoop + + +logger = logging.getLogger(__name__) + + +async def main() -> None: + loop = asyncio.get_running_loop() + transport, protocol = await create_can_connection(loop=loop, protocol_factory=None, url=None, channel='vcan0', interface='socketcan', fd=True) + # protocol.write(bytearray([1, 2, 3]), arbitration_id=232) + logger.info(f'{transport=}, {protocol=}') + await asyncio.Future() + +def run_app() -> int: + app = QApplication(sys.argv) + event_loop = QEventLoop(app) + asyncio.set_event_loop(event_loop) + async def show_window(): + window = MainWindow(app) + window.show() + result = app.exec() + result = asyncio.run(show_window()) + return result + + +if __name__ == "__main__": + logging.basicConfig(format=LOG_FORMAT, level=LOG_LEVEL) + logger.info(f'Starting: {PROJECT_NAME}') + # asyncio.run(main=main()) + # asyncio.run(main=run_app()) + run_app() + logger.info(f"Exiting: {PROJECT_NAME} ") diff --git a/can_explorer/__version__.py b/can_explorer/__version__.py new file mode 100644 index 0000000..3fa7858 --- /dev/null +++ b/can_explorer/__version__.py @@ -0,0 +1,27 @@ +import asyncio +from can_explorer.util.version import SemanticVersion +import logging + +logger = logging.getLogger(__name__) + + +async def get_project_git_version() -> SemanticVersion: + cmd = 'git describe --dirty --tags' + proc = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() + error_msg = stderr.decode().strip() + out_msg = stdout.decode().strip() + if error_msg != '': + logger.info(f'Could not read version. Got: {error_msg}') + version_str = '' + else: + version_str = out_msg + version = SemanticVersion.from_str(version_str) + return version + + +def get_version(loop: asyncio.AbstractEventLoop): + version = loop.run_until_complete(get_project_git_version()) + return version + +__version__ = get_version(asyncio.get_event_loop()) diff --git a/can_explorer/database/dbc.py b/can_explorer/database/dbc.py new file mode 100644 index 0000000..380f77f --- /dev/null +++ b/can_explorer/database/dbc.py @@ -0,0 +1 @@ +from cantools.database import Database diff --git a/can_explorer/database/dbf.py b/can_explorer/database/dbf.py new file mode 100644 index 0000000..e69de29 diff --git a/can_explorer/gui/about_dialog.py b/can_explorer/gui/about_dialog.py new file mode 100644 index 0000000..33d17b5 --- /dev/null +++ b/can_explorer/gui/about_dialog.py @@ -0,0 +1,50 @@ +from PyQt6.uic import loadUi +from PyQt6.QtWidgets import QDialog, QLabel, QApplication, QPushButton +import logging +from can_explorer.util.gui import get_res_path +from can_explorer import PROJECT_NAME, PROJECT_PLATFORM, PROJECT_BUILD_DATE +from PyQt6.QtCore import QT_VERSION, PYQT_VERSION +from can_explorer.__version__ import __version__ +import sys + +logger = logging.getLogger(__name__) + + +class AboutDialog(QDialog): + def __init__(self, parent=None, app=None): + super(AboutDialog, self).__init__(parent) + ui_path = get_res_path('about_dialog.ui') + loadUi(ui_path, self) + self._parent = parent + self._app: QApplication = app + self._configure() + self._connect_signals() + + def _configure(self): + info = AboutDialog._get_program_info() + self.setWindowTitle(info['about']) + self.about_program_name.setText(info['version']) + self.about_build_info.setText(info['build']) + self.about_runtime_info.setText(info['runtime']) + self.about_copyright.setText(info['copyright']) + + def _connect_signals(self): + self.about_copy_to_clipboard.released.connect(self._copy_info_to_clipboard) + + def _copy_info_to_clipboard(self): + info = AboutDialog._get_program_info() + build_info = '\n\n'.join([f'{key}: {value}'for key, value in info.items()]) + logger.info(f'Copy to clipboard {build_info=}') + clipboard = self._app.clipboard() + clipboard.setText(build_info) + + @staticmethod + def _get_program_info(): + return { + 'about': f'About {PROJECT_NAME}', + 'version': f'{PROJECT_NAME} ({__version__})', + 'build': f'{PROJECT_PLATFORM}-{__version__}, built on {PROJECT_BUILD_DATE}', + 'runtime': f'Python Runtime version: {sys.version}\nQt Version {QT_VERSION}, PyQt Version: {PYQT_VERSION}', + 'copyright': f'Copyright @2024-2024 {PROJECT_NAME}' + } + diff --git a/can_explorer/gui/base_worker.py b/can_explorer/gui/base_worker.py new file mode 100644 index 0000000..451b090 --- /dev/null +++ b/can_explorer/gui/base_worker.py @@ -0,0 +1,38 @@ +from PyQt6.QtCore import QRunnable, QObject, pyqtSignal, pyqtSlot +import logging +import traceback + +logger = logging.getLogger(__name__) + + +# Borrowed from https://www.pythonguis.com/tutorials/multithreading-pyqt-applications-qthreadpool/ +class WorkerSignals(QObject): + finished = pyqtSignal() + error = pyqtSignal(Exception) + result = pyqtSignal(object) + progress = pyqtSignal(int) + + +class Worker(QRunnable): + def __init__(self, func, *args, **kwargs): + super().__init__() + self._func = func + self._args = args + self._kwargs = kwargs + self._signals = WorkerSignals() + self._kwargs['progress_callback'] = self._signals.progress + + @pyqtSlot() + def run(self): + # 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() + diff --git a/can_explorer/gui/can_raw_viewer.py b/can_explorer/gui/can_raw_viewer.py new file mode 100644 index 0000000..700a892 --- /dev/null +++ b/can_explorer/gui/can_raw_viewer.py @@ -0,0 +1,70 @@ +import logging +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 PyQt6.QtWidgets import QHeaderView + +from can_explorer.gui.can_worker import CanWorker +from can_explorer.util.canutils import CanConfiguration + +logger = logging.getLogger(__name__) + + +class RawCanViewerModel(QtCore.QAbstractTableModel): + HEADER_ROWS = ('Time', 'Tx/RX', 'Message Type', 'Arbitration ID', 'DLC', 'Data Bytes') + + def __init__(self): + super(RawCanViewerModel, self).__init__() + 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) + + def columnCount(self, parent): + return len(self.HEADER_ROWS) + + def data(self, index: QModelIndex, role): + return range(len(self.HEADER_ROWS)) + + +class RawCanViewerView(QtWidgets.QTableView): + data_received_signal = pyqtSignal(Message) + + def __init__(self, configuration: CanConfiguration): + super().__init__() + self._configuration = configuration + self._can_handler = CanWorker(self._configuration) + self._model = self._configure() + self._connect_signals() + + @property + def configuration_data(self) -> CanConfiguration: + return self._configuration + + def start_listening(self, threadpool: QThreadPool): + threadpool.start(self._can_handler) + + def _configure(self): + model = RawCanViewerModel() + self.setModel(model) + self.horizontalHeader().setStretchLastSection(True) + self.resizeColumnsToContents() + return model + + def _connect_signals(self): + pass + + @pyqtSlot(Message) + def add_can_raw_message(self, message: Message): + logger.info(f'Received {message=}') + # self._model.insertRow() \ No newline at end of file diff --git a/can_explorer/gui/can_worker.py b/can_explorer/gui/can_worker.py new file mode 100644 index 0000000..b09463c --- /dev/null +++ b/can_explorer/gui/can_worker.py @@ -0,0 +1,39 @@ +import asyncio +from can_explorer.gui.base_worker import Worker +from can_explorer.transport.can_connection import create_can_connection +from can_explorer.util.canutils import CanConfiguration +import logging + +logger = logging.getLogger(__name__) + + +class CanWorker(Worker): + def __init__(self, config: CanConfiguration): + self._config = config + self._protocol, self._transport = None, None + self._progress_callback = None + self._configure() + super().__init__(self.start_listening) + + def _configure(self): + pass + + def start_listening(self, progress_callback): + try: + running_loop = None + self._progress_callback = progress_callback + 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() + except Exception as e: + logger.error(f'Error while listening to can frame: {e}') + self._signals.error.emit(e) + + def send(self): + pass diff --git a/can_explorer/gui/main_window.py b/can_explorer/gui/main_window.py new file mode 100644 index 0000000..319ee52 --- /dev/null +++ b/can_explorer/gui/main_window.py @@ -0,0 +1,76 @@ +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 +from can_explorer.gui.new_connection_dialog import NewConnectionDialog +from can_explorer.util.canutils import CanConfiguration +from can_explorer.util.gui import get_res_path +from can_explorer.gui.about_dialog import AboutDialog +import signal +import logging +from can_explorer.gui.can_raw_viewer import RawCanViewerModel, RawCanViewerView +from can_explorer.transport.can_connection import create_can_connection +from can_explorer.util import canutils + +logger = logging.getLogger(__name__) + + +class MainWindow(QMainWindow): + def __init__(self, app: QApplication): + super().__init__() + loadUi(get_res_path('main_window.ui'), self) + self._app = app + self._pool = QThreadPool.globalInstance() + self._configure() + self._connect_signal_slots() + + def _configure(self): + sheet_path = get_res_path('stylesheet.qss') + with open(sheet_path, mode='r') as sheet_file: + sheet_content = sheet_file.read() + assert sheet_content is not None, 'Sheet content empty!' + self._app.setStyleSheet(str(sheet_content)) + max_thread_count = self._pool.maxThreadCount() + logger.info(f'Using global threadpool instance with max: {max_thread_count=}') + + def _connect_signal_slots(self): + signal.signal(signal.SIGINT, signal.SIG_DFL) + self.actionAbout.triggered.connect(self._show_about_dialog) + self.actionNew_Connection.triggered.connect(self._show_new_connection_dialog) + self.connect_button.released.connect(self._connect_to_bus) + + def _show_about_dialog(self) -> int: + dialog = AboutDialog(self, self._app) + status = dialog.exec() + return status + + def _show_new_connection_dialog(self) -> int: + dialog = NewConnectionDialog(self, self._app) + dialog.on_connection_added.connect(self._add_new_can_connection) + status = dialog.exec() + return status + + @pyqtSlot(CanConfiguration) + def _add_new_can_connection(self, data: CanConfiguration): + logger.info(f'Adding new connection: {data=}') + can_raw_viewer = RawCanViewerView(data) + self.tab_widget.addTab(can_raw_viewer, data.connection_name) + + def _connect_to_bus(self): + try: + widget = self.tab_widget.currentWidget() + logger.info(f'Connecting to selected bus: {widget}') + if isinstance(widget, RawCanViewerView): + channel = widget.configuration_data.channel + interface = canutils.get_interface_name(widget.configuration_data.interface) + is_fd = widget.configuration_data.fd + widget.start_listening(self._pool) + # protocol, transport = create_can_connection(asyncio.get_running_loop(), protocol_factory=None, url=None, channel=channel, interface=interface, fd=is_fd) + # protocol.on_data_received.connect(widget.add_can_raw_message) + # logger.info(f'Connection to protocol: {protocol} successful') + 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 diff --git a/can_explorer/gui/new_connection_dialog.py b/can_explorer/gui/new_connection_dialog.py new file mode 100644 index 0000000..ffca108 --- /dev/null +++ b/can_explorer/gui/new_connection_dialog.py @@ -0,0 +1,54 @@ +import logging + +from PyQt6.QtCore import pyqtSignal +from PyQt6.QtWidgets import QDialog, QComboBox, QLineEdit, QCheckBox +from PyQt6.uic import loadUi +from matplotlib.pyplot import connect + +from can_explorer.util.canutils import CanConfiguration +from can_explorer.util.gui import get_res_path +from can_explorer.util import canutils + +logger = logging.getLogger(__name__) + + +class NewConnectionDialog(QDialog): + on_connection_added = pyqtSignal(CanConfiguration) + + def __init__(self, parent=None, app=None): + super(NewConnectionDialog, self).__init__(parent) + self._parent = parent + self._app = app + ui_path = get_res_path('new_connection_dialog.ui') + loadUi(ui_path, self) + self._configure() + self._connect_signals() + + def _configure(self): + self.connection_name_box.setText('Connection 1') + supported_bitrates = canutils.SupportedProtocols.IsoCAN.get_supported_baudrates() + 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]))) + # TODO: Extend of proper interfaces + # available_channels = get_available_channels([name for name, description in supported_interfaces]) + available_channels = canutils.get_available_channels(['socketcan']) + self.channel_box.addItems([available_channel['channel'] for available_channel in available_channels]) + self.protocol_box.addItems(['IsoCan']) + logger.info(f'Loaded config: {canutils.load_config()}') + + def _connect_signals(self): + pass + + def accept(self): + QComboBox().currentText() + can_configuration = CanConfiguration( + connection_name=self.connection_name_box.text(), + bitrate=int(self.bitrate_box.currentText(), base=10), + 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() + ) + self.on_connection_added.emit(can_configuration) + super().accept() \ No newline at end of file diff --git a/can_explorer/gui/qt/about_dialog.ui b/can_explorer/gui/qt/about_dialog.ui new file mode 100644 index 0000000..5a78bf1 --- /dev/null +++ b/can_explorer/gui/qt/about_dialog.ui @@ -0,0 +1,151 @@ + + + Dialog + + + + 0 + 0 + 595 + 266 + + + + Dialog + + + + + + Qt::Vertical + + + + 20 + 10 + + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + TextLabel + + + + + + + PointingHandCursor + + + Copy to Clipboard + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + QDialogButtonBox::Close + + + + + + + + 20 + 75 + true + + + + TextLabel + + + + + + + Icon + + + Qt::AlignCenter + + + + + + + + + buttonBox + accepted() + Dialog + close() + + + 538 + 244 + + + 297 + 132 + + + + + buttonBox + rejected() + Dialog + close() + + + 538 + 244 + + + 297 + 132 + + + + + diff --git a/can_explorer/gui/qt/main_window.ui b/can_explorer/gui/qt/main_window.ui new file mode 100644 index 0000000..95e058a --- /dev/null +++ b/can_explorer/gui/qt/main_window.ui @@ -0,0 +1,245 @@ + + + MainWindow + + + + 0 + 0 + 1516 + 800 + + + + CANExplorer + + + + + + false + + + + + + + Connect + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 0 + + + + Tab 1 + + + + + + + Time + + + + + Tx/Rx + + + + + Msg Type + + + + + Arbitration ID + + + + + Message Type + + + + + DLC + + + + + Data Bytes + + + + + + + + + + + + Record + + + + + + + + + 0 + 0 + 1516 + 22 + + + + + File + + + + + + + + + + + + Help + + + + + + + + + + + + + + + + + + + + + + + Help + + + + + Check For Updates + + + + + About + + + + + Exit + + + + + What's new in CANExplorer + + + + + Getting Started + + + + + Contact Support ... + + + + + Submit Bug Report ... + + + + + Submit Feedback ... + + + + + Show logs in Files + + + + + Show Current Logs + + + + + New Connection + + + + + Recent Connections + + + + + Close Connection + + + + + Settings + + + + + + + actionExit + triggered() + MainWindow + close() + + + -1 + -1 + + + 487 + 399 + + + + + diff --git a/can_explorer/gui/qt/new_connection_dialog.ui b/can_explorer/gui/qt/new_connection_dialog.ui new file mode 100644 index 0000000..7171c2b --- /dev/null +++ b/can_explorer/gui/qt/new_connection_dialog.ui @@ -0,0 +1,228 @@ + + + Dialog + + + + 0 + 0 + 423 + 359 + + + + + 0 + 0 + + + + Add New Connection + + + + + + + 0 + 0 + + + + Connection Name + + + false + + + false + + + + + + 50 + + + Connection 1 + + + + + + + + + + + 0 + 0 + + + + Device Configuration + + + + + + + 0 + 0 + + + + QTabWidget::West + + + QTabWidget::Rounded + + + 0 + + + Qt::ElideNone + + + true + + + false + + + true + + + + + 0 + 0 + + + + Iso CAN + + + + + + Interface + + + + + + + Protocol + + + + + + + Channel + + + + + + + Bitrate + + + + + + + Self-Reception + + + + + + + + + + + + + + + + + + + Flexible Data (fd-can) + + + true + + + false + + + + + + + + IsoTP + + + + + + + + + + + PointingHandCursor + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/can_explorer/gui/qt/stylesheet.qss b/can_explorer/gui/qt/stylesheet.qss new file mode 100644 index 0000000..2beeff4 --- /dev/null +++ b/can_explorer/gui/qt/stylesheet.qss @@ -0,0 +1,11 @@ +QTabWidget::tab-bar { +} + +QTabBar::tab { +} + +QTabBar::tab:selected { +} + +QTabWidget::pane { +} \ No newline at end of file diff --git a/can_explorer/transport/base_protocol.py b/can_explorer/transport/base_protocol.py new file mode 100644 index 0000000..9d3f202 --- /dev/null +++ b/can_explorer/transport/base_protocol.py @@ -0,0 +1,4 @@ +from abc import ABC + +class BaseCanProtocol(ABC): + pass \ No newline at end of file diff --git a/can_explorer/transport/can_connection.py b/can_explorer/transport/can_connection.py new file mode 100644 index 0000000..836bf1d --- /dev/null +++ b/can_explorer/transport/can_connection.py @@ -0,0 +1,30 @@ +import urllib +import asyncio +import logging +from typing import Union, Tuple, Optional +import urllib.parse +from functools import partial +from can_explorer.transport.canopen import * +from can_explorer.transport.fdcan import * +from can_explorer.transport.isocan import * +from can_explorer.transport.isotp import * +from can_explorer.transport.j1939 import * +from can_explorer.transport.base_protocol import BaseCanProtocol + +logger = logging.getLogger(__name__) + +def connection_for_can(loop: asyncio.AbstractEventLoop, protocol_factory: BaseCanProtocol, can_bus: can.BusABC): + # protocol = protocol_factory() + transport = IsoCanTransport(bus=can_bus) + protocol = transport.get_protocol() + return protocol, transport + +def create_can_connection(loop: asyncio.AbstractEventLoop, protocol_factory: BaseCanProtocol, url: Optional[str], *args, **kwargs): + logger.info(f'Creating can connection with: {args}, {kwargs}') + parsed_url = urllib.parse.urlparse(url=url) + bus_instance = partial(can.Bus, *args, **kwargs) + if parsed_url.scheme == 'socket': + transport, protocol = loop.run_until_complete(loop.create_connection(protocol_factory, parsed_url.hostname, parsed_url.port)) + else: + transport, protocol = connection_for_can(loop, protocol_factory, bus_instance()) + return transport, protocol \ No newline at end of file diff --git a/can_explorer/transport/canopen.py b/can_explorer/transport/canopen.py new file mode 100644 index 0000000..e69de29 diff --git a/can_explorer/transport/fdcan.py b/can_explorer/transport/fdcan.py new file mode 100644 index 0000000..e69de29 diff --git a/can_explorer/transport/isocan.py b/can_explorer/transport/isocan.py new file mode 100644 index 0000000..594b276 --- /dev/null +++ b/can_explorer/transport/isocan.py @@ -0,0 +1,88 @@ +from PyQt6.QtCore import pyqtSignal + +import can +import logging +import asyncio +from PyQt6.QtCore import pyqtSignal +from PyQt6.QtWidgets import QWidget + +logger = logging.getLogger(__name__) + + +class IsoCanProtocol(asyncio.Protocol, QWidget): + # __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: + 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: + logger.debug(f'Connection made') + return super().connection_made(transport) + + def data_received(self, data: bytes) -> None: + logger.info(f'Received: {data}') + self._data_received_queue.put_nowait(data) + self.on_data_received.emit(data) + + def connection_lost(self, exc: Exception | None) -> None: + self._on_con_lost.set_result(True) + return super().connection_lost(exc) + + +class IsoCanTransport(asyncio.Transport): + __slot__ = ('_bus', '_protocol', '_loop', '_parsing_task') + + def __init__(self, bus: can.BusABC) -> None: + self._bus: can = bus + self._protocol = IsoCanProtocol(lambda x: logger.info(x)) + # self._loop = asyncio.get_running_loop() + self._loop = None + # self._start_message_polling() + super().__init__() + + def is_reading(self) -> bool: + return self._bus.state == can.BusState.ACTIVE + + def pause_reading(self) -> None: + if self._parsing_task is not None: + self._parsing_task.cancel() + self._parsing_task = None + else: + logger.warning(f'Parsing task not present!') + + def resume_reading(self) -> None: + self._start_message_polling() + + def get_protocol(self) -> asyncio.BaseProtocol: + return self._protocol + + def close(self) -> None: + self._bus.shutdown() + + def write(self, data: bytearray, arbitration_id: int) -> None: + message = can.Message(data=data, arbitration_id=arbitration_id) + self._bus.send(message) + + def _parse_can_frames(self) -> None: + try: + + logger.info('Waiting for CAN messages') + while True: + message = self._bus.recv() + self._protocol.data_received(message) + except Exception as e: + logger.error(e) + + def _start_message_polling(self): + logger.info('Starting message polling') + + async def a_parse_can_frames(): + self._parse_can_frames() + + self._parsing_task = asyncio.create_task(a_parse_can_frames()) diff --git a/can_explorer/transport/isotp.py b/can_explorer/transport/isotp.py new file mode 100644 index 0000000..415e7f7 --- /dev/null +++ b/can_explorer/transport/isotp.py @@ -0,0 +1,91 @@ +import logging +import asyncio +from dataclasses import dataclass +from typing import Optional +import time +import can + +logger = logging.getLogger(__name__) + +@dataclass(slots=True) +class Timer: + start: Optional[int] = 0 + timeout: Optional[int] = 0 + timeout_event = asyncio.Future() + + @staticmethod + def now() -> int: + return time.perf_counter_ns() + + def run(self, start: Optional[int]): + self.start = start or self.now() + + async def process(self): + async with asyncio.Timeout(self.timeout / 1_000_000) as tm: + await asyncio.sleep(self.timeout / 1E6) + + def stop(self): + self.start = None + + +class IsoTpCanProtocol(asyncio.Protocol): + + __slot__ = ('_transport', '_on_con_lost', '_data_received_queue', '_error_queue') + + def __init__(self, on_con_lost) -> None: + self._transport = None + self._on_con_lost = on_con_lost + self._data_received_queue = asyncio.Queue() + self._error_queue = asyncio.Queue() + super().__init__() + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + logger.info('Connection made') + return super().connection_made(transport) + + def data_received(self, data: bytes) -> None: + logger.info(f'Received: {data}') + return super().data_received(data) + + def connection_lost(self, exc: Exception | None) -> None: + self._on_con_lost.set_result(True) + return super().connection_lost(exc) + + +class IsoTpTransport(asyncio.Transport): + __slot__ = ('_bus', '_protocol', '_loop', '_parsing_task') + + def __init__(self, bus: can.BusABC) -> None: + self._bus = bus + self._protocol = IsoTpCanProtocol(lambda x: logger.info(x)) + self._loop = asyncio.get_running_loop() + self._start_message_polling() + super().__init__() + + def is_reading(self) -> bool: + return self._bus.state == can.BusState.ACTIVE + + # def write(self, data: bytes | bytearray | memoryview[int], arbitration_id: int) -> None: + def write(self, data: bytes | bytearray, arbitration_id: int) -> None: + message = can.Message(data=data, arbitration_id=arbitration_id) + self._bus.send(message) + + def pause_reading(self) -> None: + if self._parsing_task is not None: + self._parsing_task.cancel() + self._parsing_task = None + else: + logger.warning(f'Parsing task not present!') + + def close(self) -> None: + self._bus.shutdown() + return super().close() + + async def _parse_can_frames(self) -> None: + logger.info(f'Waiting for CAN messages') + while True: + message = self._bus.recv() + self._protocol.data_received(message) + + def _start_message_polling(self): + self._parsing_task = asyncio.create_task(self._parse_can_frames()) diff --git a/can_explorer/transport/j1939.py b/can_explorer/transport/j1939.py new file mode 100644 index 0000000..e69de29 diff --git a/can_explorer/util/canutils.py b/can_explorer/util/canutils.py new file mode 100644 index 0000000..e065114 --- /dev/null +++ b/can_explorer/util/canutils.py @@ -0,0 +1,56 @@ +import can +import logging +import enum +from typing import List, Tuple, Dict, Optional +from dataclasses import dataclass + +from attr.setters import frozen + +logger = logging.getLogger(__name__) + + +class SupportedProtocols(enum.IntEnum): + IsoCAN = enum.auto() + fdCAN = enum.auto() + isoTp = enum.auto() + j1939 = enum.auto() + canopen = enum.auto() + + def get_supported_baudrates(self) -> List[int]: + rates = None + match self.value: + case self.IsoCAN: + rates = [10000, 20000, 50000, 100000, 125000, 250000, 500000, + 800000, 1000000] + 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)] + return supported_interfaces + +def get_available_channels(interfaces: List[str]) -> List[Dict]: + 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: + return interface_name + return None + + +@dataclass(frozen=True) +class CanConfiguration: + interface: str + connection_name: str + bitrate: int + channel: str + protocol: str + fd: bool + diff --git a/can_explorer/util/gui.py b/can_explorer/util/gui.py new file mode 100644 index 0000000..17a3ec0 --- /dev/null +++ b/can_explorer/util/gui.py @@ -0,0 +1,9 @@ +import logging +from os import PathLike +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def get_res_path(path: PathLike | str) -> Path: + return Path('.').absolute() / 'gui' / 'qt' / path diff --git a/can_explorer/util/version.py b/can_explorer/util/version.py new file mode 100644 index 0000000..9e81180 --- /dev/null +++ b/can_explorer/util/version.py @@ -0,0 +1,41 @@ +import re +import logging +from dataclasses import dataclass + +logger = logging.getLogger(__name__) +INVALID_VERSION = 0xFF + + +@dataclass(frozen=True, slots=True) +class SemanticVersion: + major: int + minor: int + patch: int + + @classmethod + def from_str(cls, version: str) -> 'Version': + matches = re.match(r'([0-9]+)\.([0-9]+)\.?([0-9])*', version) + if matches is not None: + parts = matches.group(0).split('.', 3) + else: + parts = '' + parts = list(map(int, parts)) + major, minor, patch = (lambda x=INVALID_VERSION, y=INVALID_VERSION, z=INVALID_VERSION: (x, y, z))(*parts) + return cls(major=major, minor=minor, patch=patch) + + def validate(self) -> bool: + is_byte = lambda x: 0x00 <= x <= 0xFF + try: + for part, name in zip([self.major, self.minor, self.patch], ['major', 'minor', 'patch']): + assert is_byte(part), f'Semantic version part: "{name}" is not a byte. Got: {part}' + except AssertionError as e: + logger.error(e) + return False + return True + + def stringify(self, include_dots: True): + parts = list(map(str, [self.major, self.minor, self.patch])) + return '.'.join(parts) if include_dots else ''.join(parts) + + def __str__(self): + return self.stringify(include_dots=True) diff --git a/can_server/__init__.py b/can_server/__init__.py new file mode 100644 index 0000000..150efee --- /dev/null +++ b/can_server/__init__.py @@ -0,0 +1 @@ +PROJECT_NAME = 'CAN server' diff --git a/can_server/__main__.py b/can_server/__main__.py new file mode 100644 index 0000000..a612459 --- /dev/null +++ b/can_server/__main__.py @@ -0,0 +1,30 @@ +import sys +import asyncio +import logging +from can_server import PROJECT_NAME +from can_server.cli.cli import cli +from can_explorer import LOG_FORMAT, LOG_LEVEL +from can_server.core.heart import Heart +from can_server.core.server import create_server +from aiohttp import web +import pathlib + +logger = logging.getLogger(__name__) + + +async def is_alive(): + return True + + +async def main(): + heart = Heart(pathlib.Path('test.txt'), is_active=is_alive) + await heart.beat() + + +if __name__ == '__main__': + logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG) + logger.info(f'Starting "{PROJECT_NAME}" App') + # cli() + # asyncio.run(main()) + web.run_app(create_server()) + logger.info(f'Exiting "{PROJECT_NAME}" App. Have a nice day!') diff --git a/can_server/cli/cli.py b/can_server/cli/cli.py new file mode 100644 index 0000000..97acd55 --- /dev/null +++ b/can_server/cli/cli.py @@ -0,0 +1,12 @@ +import click + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option('--server-ip', default='localhost', help='IP of the CAN server') +def setup_server(server_ip: str): + click.echo(f'Received server IP: {server_ip}') diff --git a/can_server/core/heart.py b/can_server/core/heart.py new file mode 100644 index 0000000..b27de21 --- /dev/null +++ b/can_server/core/heart.py @@ -0,0 +1,43 @@ +import asyncio +import logging +from pathlib import Path +from typing import Callable, Coroutine +from aiofile import async_open +import time + +logger = logging.getLogger(__name__) + + +class Heart: + + def __init__( + self, + heartbeat_interval, + heartbeat_path: Path, + is_active: Callable[[], Coroutine], + ): + self._heartbeat_path = heartbeat_path + self._is_active = is_active + self._heartbeat_interval = heartbeat_interval + self._last_heartbeat = 0 + + @property + async def is_alive(self) -> bool: + now = time.perf_counter() + alive_state = now - self._last_heartbeat < self._heartbeat_interval + return alive_state + + async def beat(self): + while True: + if await self.is_alive: + return + logger.debug('Running Heartbeat') + self._last_heartbeat = time.perf_counter() + try: + async with async_open(self._heartbeat_path, 'w+') as heartbeat_file: + await heartbeat_file.write('') + except Exception as e: + logger.warning(e) + await asyncio.sleep(self._heartbeat_interval) + if not await self._is_active(): + return diff --git a/can_server/core/server.py b/can_server/core/server.py new file mode 100644 index 0000000..b6b82c8 --- /dev/null +++ b/can_server/core/server.py @@ -0,0 +1,28 @@ +from aiohttp import web + + +async def handle(request): + name = request.match_info.get('name', 'Anonymous') + text = 'Hello, ' + name + return web.Response(text=text) + + +async def ws_handle(request): + ws = web.WebSocketResponse() + await ws.prepare(request) + async for msg in ws: + if msg.type == web.WSMsgType.text: + await ws.send_str(f'Hello: {msg.data}') + elif msg.type == web.WSMsgType.binary: + await ws.send_bytes(msg.data) + elif msg.type == web.WSMsgType.close: + break + return ws + + +async def create_server(): + app = web.Application() + app.add_routes( + [web.get('/', handle), web.get('/echo', ws_handle), web.get('/{name}', handle)] + ) + return app diff --git a/docs/chapters/references.adoc b/docs/chapters/references.adoc new file mode 100644 index 0000000..b6a747c --- /dev/null +++ b/docs/chapters/references.adoc @@ -0,0 +1,4 @@ +== References +=== Useful links +- Setup VCAN: https://python-can.readthedocs.io/en/stable/interfaces/socketcan.html +- Setup FDCAN: https://munich.dissec.to/kb/chapters/can/canfd.html diff --git a/docs/full_doc.adoc b/docs/full_doc.adoc new file mode 100644 index 0000000..5d0a8b7 --- /dev/null +++ b/docs/full_doc.adoc @@ -0,0 +1,92 @@ += AsciiDoc Article Title +Firstname Lastname +3.0, July 29, 2022: AsciiDoc article template +:toc: +:sectnums: +:icons: font +:url-quickref: https://docs.asciidoctor.org/asciidoc/latest/syntax-quick-reference/ + +Content entered directly below the header but before the first section heading is called the preamble. + +== First level heading + +This is a paragraph with a *bold* word and an _italicized_ word. + +.Image caption +image::image-file-name.png[I am the image alt text.] + +This is another paragraph.footnote:[I am footnote text and will be displayed at the bottom of the article.] + +=== Second level heading + +.Unordered list title +* list item 1 +** nested list item +*** nested nested list item 1 +*** nested nested list item 2 +* list item 2 + +This is a paragraph. + +.Example block title +==== +Content in an example block is subject to normal substitutions. +==== + +.Sidebar title +**** +Sidebars contain aside text and are subject to normal substitutions. +**** + +==== Third level heading + +[#id-for-listing-block] +.Listing block title +---- +Content in a listing block is subject to verbatim substitutions. +Listing block content is commonly used to preserve code input. +---- + +===== Fourth level heading + +.Table title +|=== +|Column heading 1 |Column heading 2 + +|Column 1, row 1 +|Column 2, row 1 + +|Column 1, row 2 +|Column 2, row 2 +|=== + +====== Fifth level heading + +[quote, firstname lastname, movie title] +____ +I am a block quote or a prose excerpt. +I am subject to normal substitutions. +____ + +[verse, firstname lastname, poem title and more] +____ +I am a verse block. + Indents and endlines are preserved in verse blocks. +____ + +== First level heading + +TIP: There are five admonition labels: Tip, Note, Important, Caution and Warning. + +// I am a comment and won't be rendered. + +. ordered list item +.. nested ordered list item +. ordered list item + +The text at the end of this sentence is cross referenced to <<_third_level_heading,the third level heading>> + +== First level heading + +This is a link to the https://docs.asciidoctor.org/home/[Asciidoctor documentation]. +This is an attribute reference {url-quickref}[that links this text to the AsciiDoc Syntax Quick Reference]. diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 0000000..1127ce7 --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,9 @@ +## TODOs: +List of things to implements: + + +- [ ] Add CI Pipeline +- [ ] Design GUI Interface +- [ ] Set list of core functions +- [ ] Design website +- [ ] Implement release system \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f592aba --- /dev/null +++ b/setup.cfg @@ -0,0 +1,64 @@ +[metadata] +name = CANExplorer +version = attr: my_package.__version__ +author = AKJ7 +author_email = akj123429@gmail.com +description = A CAN-Analyse tool for all platform +long_description = file: README.md, CHANGELOG.md, LICENSE.md +keywords = CAN, can, canbus, analyse, explorer, decode, encode, dbc, gui +license = BSD-3-Clause +classifiers = + Framework :: PyQt6 + Programming Language :: Python :: 3 + +[options] +zip_safe = False +include_package_data = True +packages = find: +python_requires = >=3.10 +install_requires = + requests + importlib-metadata; python_version<"3.10" + +[options.package_data] +* = *.txt, *.rst +hello = *.msg + +[options.entry_points] +console_scripts = + executable-name = my_package.module:function + +[options.extras_require] +pdf = ReportLab>=1.2; RXP +rest = docutils>=0.3; pack ==1.1, ==1.3 + +[tool.pytest] +minversion = 6.0 +addopts = +testpaths = + tests/unit + test/integration + +[tool:black] +line-length = 120 +include = \.pyi?$ +exclude = \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +skip-string-normalization = True + +[tools:pylint] +disable = C0111, C0114, C0115, C0116б, W1203, missing-module-docstring, missing-class-docstring, disable=missing-function-docstring + +[options.packages.find] +exclude = + examples* + tools* + docs* + my_package.tests* diff --git a/tests/unit/test_demo.py b/tests/unit/test_demo.py new file mode 100644 index 0000000..31acc14 --- /dev/null +++ b/tests/unit/test_demo.py @@ -0,0 +1,5 @@ +import pytest + + +def test_works(): + assert 1 == 2 // 2, 'Test failed'