-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
958 additions
and
838 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[tool.poetry] | ||
name = "can-explorer" | ||
version = "0.1.8" | ||
version = "0.2.0" | ||
description = "Visualize CAN bus payloads in real time" | ||
license = "GPL-3.0-or-later" | ||
authors = ["TJ <[email protected]>"] | ||
|
@@ -10,14 +10,15 @@ repository = "https://github.com/Tbruno25/can-explorer" | |
keywords = ["can", "bus", "canbus", "can_bus", "dbc"] | ||
|
||
[tool.poetry.dependencies] | ||
python = ">=3.8, <4.0" | ||
python = ">=3.10, <4.0" | ||
pyserial = "^3.5" | ||
python-can = "^4.1.0" | ||
dearpygui = "^1.9.0" | ||
dearpygui-ext = "^0.9.5" | ||
|
||
[tool.poetry.group.dev.dependencies] | ||
pytest = "^7.2.2" | ||
pytest-order = "^1.2.1" | ||
mypy = "^1.2.0" | ||
pillow = "^10.3.0" | ||
pyautogui = "^0.9.54" | ||
|
@@ -28,13 +29,15 @@ pyvirtualdisplay = [ | |
] | ||
msvc-runtime = {version = "^14.34.31931", platform = "windows"} | ||
|
||
|
||
[build-system] | ||
requires = ["poetry-core>=1.0.0"] | ||
build-backend = "poetry.core.masonry.api" | ||
|
||
[tool.mypy] | ||
ignore_missing_imports = true | ||
|
||
[tool.ruff.per-file-ignores] | ||
"__init__.py" = ["F401"] | ||
|
||
[tool.poetry.scripts] | ||
can-explorer = "can_explorer.__main__:__main__" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from can_explorer.app import CanExplorer | ||
from can_explorer.controllers import Controller | ||
from can_explorer.models import PlotModel | ||
from can_explorer.views import MainView | ||
|
||
_all__ = ["PlotModel", "MainView", "Controller", "CanExplorer"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,18 @@ | ||
import argparse | ||
import sys | ||
|
||
from can_explorer import app | ||
from can_explorer import CanExplorer | ||
from can_explorer.resources.demo import demo_config | ||
|
||
parser = argparse.ArgumentParser() | ||
parser.add_argument("--demo", action="store_true") | ||
args = parser.parse_args() | ||
|
||
app = CanExplorer() | ||
|
||
if args.demo: | ||
app.main(demo_config) | ||
app.run(test_config=demo_config) | ||
else: | ||
app.main() | ||
app.run() | ||
|
||
sys.exit() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,187 +1,80 @@ | ||
import enum | ||
from __future__ import annotations | ||
|
||
import logging | ||
import sys | ||
import threading | ||
from typing import Callable, Optional | ||
from collections.abc import Callable | ||
|
||
import can | ||
import can.player | ||
import dearpygui.dearpygui as dpg | ||
|
||
from can_explorer import can_bus, layout, plotting | ||
from can_explorer.layout import Default | ||
|
||
|
||
class State(enum.Flag): | ||
ACTIVE = True | ||
STOPPED = False | ||
|
||
|
||
class MainApp: | ||
_rate = 0.05 | ||
_cancel = threading.Event() | ||
_state = State.STOPPED | ||
_worker: threading.Thread | ||
|
||
bus: Optional[can.bus.BusABC] = None | ||
can_recorder = can_bus.Recorder() | ||
plot_manager = plotting.PlotManager() | ||
|
||
@property | ||
def state(self) -> State: | ||
return self._state | ||
|
||
def is_active(self) -> bool: | ||
return bool(self._state) | ||
|
||
def repopulate(self) -> None: | ||
""" | ||
Repopulate all plots in ascending order. | ||
""" | ||
self.plot_manager.clear_all() | ||
for can_id, payload_buffer in sorted(self.can_recorder.items()): | ||
self.plot_manager.add(can_id, payload_buffer) | ||
|
||
def _get_worker(self) -> threading.Thread: | ||
""" | ||
Get the main loop worker thread. | ||
Returns: | ||
threading.Thread: Worker | ||
""" | ||
|
||
def loop() -> None: | ||
while not self._cancel.wait(self._rate): | ||
# Note: must convert can_recorder to avoid runtime error | ||
for can_id in tuple(self.can_recorder): | ||
if can_id not in self.plot_manager(): | ||
self.repopulate() | ||
break | ||
else: | ||
self.plot_manager.update(can_id) | ||
self._cancel.clear() | ||
|
||
return threading.Thread(target=loop, daemon=True) | ||
|
||
def start(self) -> None: | ||
""" | ||
Initialize and start app loop. | ||
Raises: | ||
Exception: If CAN bus does not exist. | ||
""" | ||
if self.bus is None: | ||
raise RuntimeError("Must apply settings before starting") | ||
|
||
self.can_recorder.set_bus(self.bus) | ||
self.can_recorder.start() | ||
|
||
self._worker = self._get_worker() | ||
self._worker.start() | ||
|
||
self._state = State.ACTIVE | ||
|
||
def stop(self) -> None: | ||
""" | ||
Stop the app loop. | ||
""" | ||
self.can_recorder.stop() | ||
self._cancel.set() | ||
self._worker.join() | ||
|
||
self._state = State.STOPPED | ||
|
||
def set_bus(self, bus: can.BusABC) -> None: | ||
""" | ||
Set CAN bus to use during app loop. | ||
""" | ||
self.bus = bus | ||
|
||
|
||
app = MainApp() | ||
|
||
|
||
def start_stop_button_callback(sender, app_data, user_data) -> None: | ||
app.stop() if app.is_active() else app.start() | ||
layout.set_main_button_label(app.state) | ||
|
||
|
||
def clear_button_callback(sender, app_data, user_data) -> None: | ||
app.plot_manager.clear_all() | ||
|
||
|
||
def plot_buffer_slider_callback(sender, app_data, user_data) -> None: | ||
app.plot_manager.set_limit(layout.get_settings_plot_buffer()) | ||
|
||
|
||
def plot_height_slider_callback(sender, app_data, user_data) -> None: | ||
app.plot_manager.set_height(layout.get_settings_plot_height()) | ||
|
||
|
||
def settings_apply_button_callback(sender, app_data, user_data) -> None: | ||
user_settings = dict( | ||
interface=layout.get_settings_interface(), | ||
channel=layout.get_settings_channel(), | ||
bitrate=layout.get_settings_baudrate(), | ||
) | ||
if app.is_active(): | ||
raise RuntimeError("App must be stopped before applying new settings") | ||
bus = can.Bus(**{k: v for k, v in user_settings.items() if v}) # type: ignore | ||
app.set_bus(bus) | ||
|
||
|
||
def settings_can_id_format_callback(sender, app_data, user_data) -> None: | ||
app.plot_manager.set_id_format(layout.get_settings_id_format()) | ||
app.repopulate() | ||
|
||
|
||
def setup(): | ||
dpg.create_context() | ||
from can_explorer import can_bus | ||
from can_explorer.configs import Default | ||
from can_explorer.controllers import Controller | ||
from can_explorer.models import PlotModel | ||
from can_explorer.views import MainView | ||
|
||
with dpg.window() as app_main: | ||
layout.create() | ||
|
||
layout.set_settings_interface_options(can_bus.INTERFACES) | ||
layout.set_settings_baudrate_options(can_bus.BAUDRATES) | ||
layout.set_settings_apply_button_callback(settings_apply_button_callback) | ||
layout.set_settings_can_id_format_callback(settings_can_id_format_callback) | ||
class CanExplorer: | ||
def __init__(self) -> None: | ||
self.model = PlotModel() | ||
self.view = MainView() | ||
self.controller = Controller(self.model, self.view) | ||
|
||
layout.set_main_button_label(app.state) | ||
layout.set_main_button_callback(start_stop_button_callback) | ||
layout.set_clear_button_callback(clear_button_callback) | ||
def setup(self): | ||
dpg.create_context() | ||
|
||
layout.set_plot_buffer_slider_callback(plot_buffer_slider_callback) | ||
layout.set_plot_height_slider_callback(plot_height_slider_callback) | ||
with dpg.window() as app: | ||
self.view.setup() | ||
|
||
dpg.create_viewport(title=Default.TITLE, width=Default.WIDTH, height=Default.HEIGHT) | ||
dpg.set_viewport_resize_callback(layout.resize) | ||
dpg.setup_dearpygui() | ||
layout.resize() | ||
self.view.settings.set_interface_options(can_bus.INTERFACES) | ||
self.view.settings.set_baudrate_options(can_bus.BAUDRATES) | ||
self.view.settings.set_apply_button_callback( | ||
self.controller.settings_apply_button_callback | ||
) | ||
self.view.settings.set_can_id_format_callback( | ||
self.controller.settings_can_id_format_callback | ||
) | ||
|
||
dpg.set_primary_window(app_main, True) | ||
self.view.set_main_button_label(self.controller.state) | ||
self.view.set_main_button_callback(self.controller.start_stop_button_callback) | ||
self.view.set_clear_button_callback(self.controller.clear_button_callback) | ||
|
||
self.view.set_plot_buffer_slider_callback( | ||
self.controller.plot_buffer_slider_callback | ||
) | ||
self.view.set_plot_height_slider_callback( | ||
self.controller.plot_height_slider_callback | ||
) | ||
|
||
def teardown(): | ||
dpg.destroy_context() | ||
dpg.create_viewport( | ||
title=Default.TITLE, width=Default.WIDTH, height=Default.HEIGHT | ||
) | ||
dpg.set_viewport_resize_callback(self.view.resize) | ||
dpg.setup_dearpygui() | ||
self.view.resize() | ||
|
||
dpg.set_primary_window(app, True) | ||
|
||
def exception_handler(exc_type, exc_value, exc_traceback): | ||
logging.debug(msg="ExceptionHandler", exc_info=(exc_type, exc_value, exc_traceback)) | ||
layout.popup_error(name=exc_type.__name__, info=exc_value) | ||
@staticmethod | ||
def teardown(): | ||
dpg.destroy_context() | ||
|
||
def exception_handler(self, exc_type, exc_value, exc_traceback): | ||
logging.debug( | ||
msg="ExceptionHandler", exc_info=(exc_type, exc_value, exc_traceback) | ||
) | ||
self.view.popup_error(name=exc_type.__name__, info=exc_value) | ||
|
||
def main(test_config: Optional[Callable] = None): | ||
setup() | ||
def run(self, test_config: Callable | None = None, show: bool = True): | ||
self.setup() | ||
|
||
if test_config: | ||
test_config() | ||
if test_config: | ||
test_config(self) | ||
|
||
sys.excepthook = exception_handler | ||
dpg.show_viewport() | ||
dpg.start_dearpygui() | ||
sys.excepthook = self.exception_handler | ||
|
||
teardown() | ||
if show: | ||
dpg.show_viewport() | ||
|
||
dpg.start_dearpygui() | ||
|
||
if __name__ == "__main__": | ||
main() | ||
self.teardown() |
Oops, something went wrong.