Skip to content

Commit

Permalink
Add can id format setting (#14)
Browse files Browse the repository at this point in the history
* Minor fixes

* Update github actions

* Modify to use simple lambda callbacks

* Add CAN id format setting
  • Loading branch information
Tbruno25 authored Apr 29, 2023
1 parent 66b8d48 commit df770ff
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 35 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/test.yml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: CI

on:
pull_request:
push:
branches: ['main', 'dev']

jobs:
pre-commit:
Expand All @@ -13,7 +11,7 @@ jobs:
- uses: actions/setup-python@v3
- uses: pre-commit/[email protected]

test:
check:
runs-on: ubuntu-latest
strategy:
matrix:
Expand All @@ -31,7 +29,9 @@ jobs:
- name: Setup container display
# https://arbitrary-but-fixed.net/2022/01/21/headless-gui-github-actions.html
run: Xvfb :1 -screen 0 1600x1200x24 &
- name: Test with pytest
- name: Pytest
run: poetry run pytest -v
env:
DISPLAY: :1
- name: Mypy
run: poetry run mypy src
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@

`can-explorer` is a CAN bus visualization tool designed to aid in reverse engineering.

<br>
<div align="center">
<img src="https://github.com/Tbruno25/can-explorer/raw/main/docs/images/demo.gif" alt="Demo">
</div>
<br>
</details>
![Demo](https://github.com/Tbruno25/can-explorer/raw/main/docs/images/demo.gif)

### How does this help me?
By continuously plotting all payloads for each CAN id, spotting trends that correspond to a specific action can become signicantly easier to identify.
Expand Down
6 changes: 6 additions & 0 deletions src/can_explorer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ def settings_apply_button_callback(sender, app_data, user_data) -> None:
layout.popup_error(name=type(e).__name__, info=e)


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 main():
dpg.create_context()

Expand All @@ -145,6 +150,7 @@ def main():
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)

layout.set_main_button_callback(start_stop_button_callback)
layout.set_clear_button_callback(clear_button_callback)
Expand Down
4 changes: 2 additions & 2 deletions src/can_explorer/can_bus.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from collections import defaultdict, deque
from typing import Final, Optional
from typing import Final

from can.bus import BusABC
from can.interfaces import VALID_INTERFACES
Expand Down Expand Up @@ -43,7 +43,7 @@ class Recorder(defaultdict):
_active = False
_notifier: Notifier
_listener: _Listener
_bus: Optional[BusABC] = None
_bus: BusABC

def __init__(self):
super().__init__(PayloadBuffer)
Expand Down
44 changes: 31 additions & 13 deletions src/can_explorer/layout.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from enum import Enum, auto, unique
from pathlib import Path
from typing import Callable, Final, Iterable, Union
from typing import Callable, Final, Iterable, Union, cast

import dearpygui.dearpygui as dpg
from dearpygui_ext.themes import create_theme_imgui_light
Expand All @@ -17,6 +17,7 @@ class Default:
FONT_HEIGHT: Final = 14
PLOT_HEIGHT: Final = 100
BUFFER_SIZE: Final = 100
ID_FORMAT: Final = hex
FONT: Final = RESOURCES_DIR / "Inter-Medium.ttf"


Expand All @@ -28,7 +29,6 @@ class Font:
class Theme:
DEFAULT: int
LIGHT: int
MIDNIGHT: int


@unique
Expand All @@ -48,6 +48,7 @@ class Tag(str, Enum):
SETTINGS_CHANNEL = auto()
SETTINGS_BAUDRATE = auto()
SETTINGS_APPLY = auto()
SETTINGS_ID_FORMAT = auto()


class PercentageWidthTableRow:
Expand Down Expand Up @@ -207,13 +208,22 @@ def _settings_tab() -> None:
dpg.add_spacer(height=5)

with dpg.collapsing_header(label="GUI"):

def light_theme_calllback(sender, app_data, user_data):
dpg.bind_theme(Theme.LIGHT if dpg.get_value(sender) else Theme.DEFAULT)

with dpg.group(horizontal=True):
dpg.add_text("Light Theme")
dpg.add_checkbox(callback=light_theme_calllback)
dpg.add_text("ID Format")
dpg.add_radio_button(
["Hex", "Dec"],
tag=Tag.SETTINGS_ID_FORMAT,
horizontal=True,
)
with dpg.group(horizontal=True):
dpg.add_text("Theme")
dpg.add_radio_button(
["Default", "Light"],
horizontal=True,
callback=lambda sender: dpg.bind_theme(
getattr(Theme, dpg.get_value(sender).upper())
),
)

dpg.add_button(
label="Launch Font Manager", width=-1, callback=dpg.show_font_manager
Expand Down Expand Up @@ -242,10 +252,6 @@ def resize() -> None:
def popup_error(name: Union[str, Exception], info: Union[str, Exception]) -> None:
# https://github.com/hoffstadt/DearPyGui/discussions/1308

def on_selection(sender, unused, user_data):
# delete window
dpg.delete_item(user_data[0])

# guarantee these commands happen in the same frame
with dpg.mutex():
viewport_width = dpg.get_viewport_client_width()
Expand All @@ -260,7 +266,9 @@ def on_selection(sender, unused, user_data):
label="Close",
width=-1,
user_data=(modal_id, True),
callback=on_selection,
callback=lambda sender, app_data, user_data: dpg.delete_item(
user_data[0]
),
)

# guarantee these commands happen in another frame
Expand Down Expand Up @@ -301,6 +309,12 @@ def get_settings_baudrate() -> int:
return dpg.get_value(Tag.SETTINGS_BAUDRATE)


def get_settings_id_format() -> Callable:
return cast(
Callable, hex if dpg.get_value(Tag.SETTINGS_ID_FORMAT).lower() == "hex" else int
)


def set_main_button_callback(callback: Callable) -> None:
button_labels = ("Stop", "Start")

Expand Down Expand Up @@ -333,6 +347,10 @@ def set_settings_apply_button_callback(callback: Callable) -> None:
dpg.configure_item(Tag.SETTINGS_APPLY, callback=callback)


def set_settings_can_id_format_callback(callback: Callable) -> None:
dpg.configure_item(Tag.SETTINGS_ID_FORMAT, callback=callback)


def set_settings_interface_options(iterable: Iterable[str]) -> None:
dpg.configure_item(Tag.SETTINGS_INTERFACE, items=iterable)

Expand Down
43 changes: 33 additions & 10 deletions src/can_explorer/plotting.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from typing import Dict, Iterable
import uuid
from typing import Callable, Dict, Iterable

import dearpygui.dearpygui as dpg

Expand Down Expand Up @@ -31,9 +32,7 @@ class Plot(str):
series: str

def __new__(cls, x: Iterable, y: Iterable) -> Plot:
with dpg.plot(
tag=f"{Tag.PLOT_ITEM}{dpg.generate_uuid()}", **Config.PLOT
) as plot:
with dpg.plot(tag=f"{Tag.PLOT_ITEM}{uuid.uuid4()}", **Config.PLOT) as plot:
plot = super().__new__(cls, plot)
plot.x_axis = dpg.add_plot_axis(**Config.X_AXIS)
plot.y_axis = dpg.add_plot_axis(**Config.Y_AXIS)
Expand All @@ -48,10 +47,9 @@ def update(self, x: Iterable, y: Iterable) -> None:


class Label(str):
def __new__(cls, can_id: int) -> Label:
def __new__(cls) -> Label:
label = dpg.add_button(
tag=f"{Tag.PLOT_LABEL}{dpg.generate_uuid()}",
label=hex(can_id),
tag=f"{Tag.PLOT_LABEL}{uuid.uuid4()}",
**Config.LABEL,
)
dpg.bind_item_font(label, Font.LABEL)
Expand All @@ -63,21 +61,31 @@ class Row:
table: PlotTable
label: Label
plot: Plot
height: int
label_format: Callable

def __init__(self, can_id: int, height: int, x: Iterable, y: Iterable) -> None:
def __init__(
self, can_id: int, id_format: Callable, height: int, x: Iterable, y: Iterable
) -> None:
self._can_id = can_id
self.table = PlotTable()
self.label = Label(can_id)
self.label = Label()
self.plot = Plot(x, y)
self.table.add_label(self.label)
self.table.add_plot(self.plot)
self.table.submit()
self.set_label(id_format)
self.set_height(height)

def set_height(self, height: int) -> None:
dpg.set_item_height(self.label, height)
dpg.set_item_height(self.plot, height)
self.height = height

def set_label(self, id_format: Callable) -> None:
dpg.set_item_label(self.label, id_format(self._can_id))
self.label_format = id_format

def delete(self) -> None:
dpg.delete_item(self.table.table_id)

Expand All @@ -96,6 +104,7 @@ class PlotManager:
row: Dict[int, Row] = {}
_height = Default.PLOT_HEIGHT
_x_limit = Default.BUFFER_SIZE
_id_format: Callable = Default.ID_FORMAT

def __call__(self) -> dict[int, Row]:
"""
Expand Down Expand Up @@ -134,7 +143,9 @@ def add(self, can_id: int, payloads: PayloadBuffer) -> None:
if can_id in self.row:
raise Exception(f"Error: id {can_id} already exists")

row = Row(can_id, self._height, **AxisData(self._slice(payloads)))
row = Row(
can_id, self._id_format, self._height, **AxisData(self._slice(payloads))
)
self.row[can_id] = row

def delete(self, can_id: int) -> None:
Expand Down Expand Up @@ -162,6 +173,9 @@ def update(self, can_id: int, payloads: PayloadBuffer) -> None:
if row.height != self._height:
row.set_height(self._height)

if row.label_format != self._id_format:
row.set_label(self._id_format)

row.plot.update(**AxisData(self._slice(payloads)))

def clear_all(self) -> None:
Expand All @@ -182,6 +196,15 @@ def set_height(self, height: int) -> None:
"""
self._height = height

def set_id_format(self, id_format: Callable) -> None:
"""
Set the format CAN id's will be displayed as.
Args:
id_format (Callable)
"""
self._id_format = id_format

def set_limit(self, x_limit: int) -> None:
"""
Set the number of payloads to plot on the x axis.
Expand Down

0 comments on commit df770ff

Please sign in to comment.