diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 00000000..d75359b9 --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,336 @@ +name: Build CANgaroo + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + workflow_dispatch: + +jobs: + build-ubuntu: + name: Build CANgaroo (Ubuntu / Qt6) + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y build-essential git cmake \ + qt6-base-dev qt6-base-private-dev qt6-tools-dev \ + libqt6serialport6-dev libqt6charts6-dev \ + libnl-3-dev libnl-route-3-dev libgl1-mesa-dev fuse libfuse2 wget \ + python3-dev pybind11-dev python3-pybind11 + + echo "=== Qt6 Version ===" + qmake6 --version + + echo "=== Qt6 Plugin Path ===" + QT6_PLUGIN_PATH=$(qmake6 -query QT_INSTALL_PLUGINS) + echo "Qt6 Plugins: $QT6_PLUGIN_PATH" + + - name: Run qmake6 and make + run: | + qmake6 + make -j$(nproc) + + - name: Prepare AppDir + run: | + mkdir -p AppDir/usr/bin + mkdir -p AppDir/usr/share/applications + mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps + mkdir -p AppDir/usr/plugins/styles + mkdir -p AppDir/usr/plugins/platformthemes + + sed -i 's|Exec=/usr/bin/cangaroo %f|Exec=cangaroo %f|' cangaroo.desktop + + cp bin/cangaroo AppDir/usr/bin/ + cp cangaroo.desktop AppDir/usr/share/applications/ + cp src/assets/cangaroo.png AppDir/usr/share/icons/hicolor/256x256/apps/ + + - name: Create qt.conf + run: | + cat > AppDir/usr/bin/qt.conf << 'EOF' + [Paths] + Plugins = ../plugins + Libraries = ../lib + Prefix = ../ + EOF + + echo "=== qt.conf created ===" + cat AppDir/usr/bin/qt.conf + + - name: Download linuxdeploy + run: | + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage + chmod +x linuxdeploy*.AppImage + + - name: Build AppImage + env: + QMAKE: qmake6 + QML_SOURCES_PATHS: ./src + run: | + ./linuxdeploy-x86_64.AppImage \ + --appdir AppDir \ + --plugin qt \ + --output appimage + + - name: Upload AppImage + uses: actions/upload-artifact@v6 + with: + name: CANgaroo-Linux-AppImage + path: "CAN*.AppImage" + + - name: Upload Binary + uses: actions/upload-artifact@v6 + with: + name: CANgaroo-Linux-Binary + path: bin/cangaroo + + build-windows: + name: Build CANgaroo (Windows / MinGW / Qt6) + runs-on: windows-latest + + defaults: + run: + shell: msys2 {0} + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup MSYS2 (without install) + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + + - name: Cache MinGW and MSYS2 packages + uses: actions/cache@v4 + with: + path: | + C:/msys64/mingw64 + C:/msys64/var/cache/pacman/pkg + key: > + msys2-mingw64-qt6-${{ runner.os }}-${{ hashFiles('.github/workflows/cmake.yml') }} + restore-keys: | + msys2-mingw64-qt6-${{ runner.os }}- + + - name: Install MinGW + Qt6 (cached) + run: | + pacman -S --needed --noconfirm \ + mingw-w64-x86_64-toolchain \ + mingw-w64-x86_64-qt6 \ + mingw-w64-x86_64-qt6-tools \ + mingw-w64-x86_64-make \ + mingw-w64-x86_64-ntldd \ + mingw-w64-x86_64-python \ + mingw-w64-x86_64-pybind11 \ + git + + - name: Verify Qt6 installation + run: | + qmake6 --version + which qmake6 + + - name: Configure build (Qt6 qmake) + run: | + qmake6 CONFIG+=release + + - name: Build + run: | + mingw32-make -j$(nproc) + + - name: List build output + run: | + echo "==== Project root ====" + ls -lah + + echo "==== Executables ====" + find . -maxdepth 2 -name "*.exe" -type f -print + + - name: Prepare distribution directory + run: | + mkdir -p dist + cp ./bin/*.exe dist/ + + - name: Deploy Qt6 runtime (windeployqt) + run: | + EXE=$(ls dist/*.exe | head -n 1) + windeployqt6 \ + --release \ + --no-system-d3d-compiler \ + "$EXE" + + - name: Add MinGW runtime DLLs + run: | + cp /mingw64/bin/libgcc_s_seh-1.dll dist/ + cp /mingw64/bin/libstdc++-6.dll dist/ + cp /mingw64/bin/libwinpthread-1.dll dist/ + cp /mingw64/bin/libmd4c.dll dist/ + cp /mingw64/bin/libharfbuzz-0.dll dist/ + cp /mingw64/bin/libfreetype-6.dll dist/ + cp /mingw64/bin/libpng16-16.dll dist/ + cp /mingw64/bin/zlib1.dll dist/ + cp /mingw64/bin/libdouble-conversion.dll dist/ + cp /mingw64/bin/libb2-1.dll dist/ + cp /mingw64/bin/libicuin78.dll dist/ + cp /mingw64/bin/libbrotlidec.dll dist/ + cp /mingw64/bin/libzstd.dll dist/ + cp /mingw64/bin/libicuuc78.dll dist/ + cp /mingw64/bin/libpcre2-16-0.dll dist/ + cp /mingw64/bin/libpython3.12.dll dist/ || cp /mingw64/bin/libpython3.11.dll dist/ || true + + - name: Deploy MinGW runtime dependencies + run: | + EXE=$(ls dist/*.exe | head -n 1) + + echo "Resolving MinGW runtime dependencies for $EXE" + + ntldd -R "$EXE" \ + | grep mingw64 \ + | awk '{print $3}' \ + | sort -u \ + | while IFS= read -r file; do + # Convert Windows path to Linux path + linux_path=$(echo "$file" | sed -E 's|([A-Za-z]):\\|/\L\1/|; s|\\|/|g') + + echo "Copying $linux_path" + cp "$linux_path" dist/ || echo "Skipped $linux_path" + done + + - name: Verify missing DLLs + run: | + EXE=$(ls dist/*.exe | head -n 1) + ntldd "$EXE" || true + - name: Upload Windows Qt6 binaries + uses: actions/upload-artifact@v6 + with: + name: CANgaroo-Windows-MinGW-Qt6 + path: dist/* + + release: + name: Create GitHub Release + needs: [build-ubuntu, build-windows] + if: github.event_name == 'workflow_dispatch' || github.ref_name == 'main' || github.ref_name == 'master' + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine New Version + id: versioning + run: | + latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.4.0") + echo "Latest tag: $latest_tag" + + # Remove 'v' prefix if exists + clean_tag=${latest_tag#v} + + major=$(echo $clean_tag | cut -d. -f1) + minor=$(echo $clean_tag | cut -d. -f2) + patch=$(echo $clean_tag | cut -d. -f3) + + new_minor=$((minor+1)) + new_tag="v$major.$new_minor.0" + + echo "Next tag: $new_tag" + echo "version=$new_tag" >> $GITHUB_OUTPUT + + - name: Create Git Tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag ${{ steps.versioning.outputs.version }} + git push origin ${{ steps.versioning.outputs.version }} + + - name: Download Linux Binary + uses: actions/download-artifact@v4 + with: + name: CANgaroo-Linux-Binary + path: bin + + - name: Download Linux AppImage + uses: actions/download-artifact@v4 + with: + name: CANgaroo-Linux-AppImage + path: appimage + + - name: Download Windows Artifacts + uses: actions/download-artifact@v4 + with: + name: CANgaroo-Windows-MinGW-Qt6 + path: windows-bins + + - name: Package Release + run: | + VERSION=${{ steps.versioning.outputs.version }} + mkdir -p release-pkg + + # Linux packaging + LINUX_DIR="cangaroo-${VERSION}-linux-x86_64" + mkdir -p "$LINUX_DIR" + + cp bin/cangaroo "$LINUX_DIR/" + cp appimage/CAN*.AppImage "$LINUX_DIR/CANgaroo-x86_64.AppImage" + cp README.md "$LINUX_DIR/" + cp LICENSE "$LINUX_DIR/" + cp CONTRIBUTING.md "$LINUX_DIR/" + cp setup_release.sh "$LINUX_DIR/" + + tar -czf "release-pkg/${LINUX_DIR}.tar.gz" "$LINUX_DIR" + + # Windows packaging (Zip the dist folder) + WINDOWS_ZIP="cangaroo-${VERSION}-windows-x64.zip" + cd windows-bins + zip -r "../release-pkg/${WINDOWS_ZIP}" . + cd .. + + # Generate Checksums + cd release-pkg + for file in *; do + sha256sum "$file" > "${file}.sha256" + done + cd .. + + - name: Generate Changelog + id: changelog + run: | + latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$latest_tag" ]; then + log=$(git log --oneline -n 20) + else + log=$(git log ${latest_tag}..HEAD --oneline) + fi + + # Set output using multi-line delimiter + { + echo "body<> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.versioning.outputs.version }} + name: Release ${{ steps.versioning.outputs.version }} + body: ${{ steps.changelog.outputs.body }} + files: | + release-pkg/*.tar.gz + release-pkg/*.zip + release-pkg/*.sha256 + draft: true + prerelease: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 604ee40f..c17ac095 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,12 @@ Makefile* test.dbc + +.vscode/ + +build/ + +bin/ + +# Release artifacts +releases/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c5ebfeb8..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: cpp - -sudo: required - -services: - - docker - -compiler: - - clang - - gcc - -before_install: - - sudo apt-get update -qq - - sudo apt-get install --yes qtbase5-dev qtdeclarative5-dev - - sudo apt-get install --yes qt5-default qttools5-dev-tools libnl-route-3-dev - - qmake -version - -script: - - qmake cangaroo.pro - - make - -git: - depth: 3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..7c19d32a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Contributing to Cangaroo + +Thank you for your interest in contributing to Cangaroo! We welcome contributions from engineers, developers, and enthusiasts. + +## How Can I Contribute? + +### Reporting Bugs +- Check the [Issues Tab](https://github.com/OpenAutoDiagLabs/cangaroo/issues) to see if the bug has already been reported. +- If not, create a new issue. Include as much detail as possible, such as: + - Your OS and version. + - The CAN hardware you are using. + - Steps to reproduce the issue. + - Any error logs or screenshots. + +### Suggesting Features +- We love hearing new ideas! Please start a [Discussion](https://github.com/OpenAutoDiagLabs/cangaroo/discussions) to talk about your feature request. + +### Submitting Pull Requests +1. **Fork the repository** and create your branch from `master`. +2. **Ensure your code builds** and follows the existing coding style (Qt/C++). +3. **Document your changes**: Update the README or headers if necessary. +4. **Open a Pull Request**: Provide a clear description of the problem you are solving or the feature you are adding. + +## Coding Standards +- We use **Qt 6** (targeted at v6.10+). +- Prefer range-based `for` loops over `foreach`. +- Keep memory safety in mind (use smart pointers where appropriate). + +## Community +Join the conversation in the [GitHub Discussions](https://github.com/OpenAutoDiagLabs/cangaroo/discussions). + +--- + +By contributing, you agree that your contributions will be licensed under the project's **GPL-3.0+** license. diff --git a/README.md b/README.md index 015ca5b0..ff9ecaae 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,239 @@ -# cangaroo -open source can bus analyzer - -written by Hubert Denkmair - -## building on linux -* to install all required packages in a vanilla ubuntu 16.04: - * sudo apt-get install build-essential git qt5-qmake qtbase5-dev libnl-3-dev libnl-route-3-dev -* build with: - * qmake -qt=qt5 - * make - * make install - -## building on windows -* Qt Creator (Community Version is okay) brings everything you need -* except for the PCAN libraries. - * Get them from http://www.peak-system.com/fileadmin/media/files/pcan-basic.zip - * Extract to .zip to src/driver/PeakCanDriver/pcan-basic-api - * Make sure PCANBasic.dll (the one from pcan-basic-api/Win32 on a "normal" 32bit Windows build) - is found when running cangaroo, e.g. by putting it in the same folder as the .exe file. -* if you don't want Peak support, you can just disable the driver: - remove the line "win32:include($$PWD/driver/PeakCanDriver/PeakCanDriver.pri)" - from src/src.pro -* if you want to deploy the cangaroo app, make sure to also include the needed Qt Libraries. - for a normal release build, these are: Qt5Core.dll Qt5Gui.dll Qt5Widgets.dll Qt5Xml.dll - -## changelog - -### v0.2.1 unreleased -* make logging easier -* refactorings -* scroll trace view per pixel, not per item (always show last message when autoscroll is on) - -### v0.2.0 released 2016-01-24 -* docking windows system instead of MDI interface -* windows build -* windows PCAN-basic driver -* handle muxed signals in backend and trace window -* do not try to extract signals from messages when DLC too short -* can status window -* bugfixes in setup dialog -* show timestamps, log level etc. in log window - -### v0.1.3 released 2016-01-16 -* new can interface configuration GUI (missing a suid binary to actually set the config) -* use libnl-route-3 for socketcan device config read -* query socketcan interfaces for supported config options -* new logging subsystem, do not use QDebug any more -* some performance improvements when receiving lots of messages -* bugfix with time-delta view: timestamps not shown when no previous message avail - -### v0.1.2 released 2016-01-12 -* fix device re-scan ("could not bind" console message) -* fix some dbc parsing issues (signed signals, ...) -* implement big endian signals - -### v0.1.1 released 2016-01-11 -* change source structure to better fit debian packaging -* add debian packaging info - -### v0.1 released 2016-01-10 -initial release \o/ - - - -## todo - -### backend -* allow for canfd frames -* support non-message frames in traces (e.g. markers) -* implement plugin API -* embed python for scripting - -### can drivers -* allow socketcan interface config through suid binary -* socketcan: use hardware timestamps (SIOCSHWTSTAMP) if possible -* cannelloni support -* windows vector driver - -### import / export -* export to other file formats (e.g. Vector ASC, BLF, MDF) -* import CAN-Traces - -### general ui -* give some style to dock windows -* load/save docks from/to config - -### log window -* filter log messages by level - -### can status window -* display #warnings, #passive, #busoff, #restarts of socketcan devices - -### trace window -* message filtering -* assign colors to can interfaces / messages -* limit displayed number of messages -* show error frames and other non-message frames -* sort signals by startbit, name or position in candb - -### raw message generator -* provide a simple way to generate raw can messages - -### CanDB based generator -* generate can messages from candbs -* set signals according to value tables etc. -* provide generator functions for signal values -* allow scripting of signal values - -### replay window -* replay can traces -* map interfaces in traces to available networks - -### graph window -* test QCustomPlot -* allow for graphing of interface stats, message stats and signals - -### packaging / deployment -* provide clean debian package -* gentoo ebuild script -* provide static linked binary -* add windows installer +# Cangaroo +**Open-source cross-platform CAN bus analyzer for Automotive, Robotics, and Industrial Applications** +[![Try Cangaroo Now](https://img.shields.io/badge/Try-Cangaroo-blue?style=for-the-badge)](https://github.com/OpenAutoDiagLabs/cangaroo/releases/latest) + +[![Build CANgaroo](https://github.com/OpenAutoDiagLabs/CANgaroo/actions/workflows/cmake.yml/badge.svg)](https://github.com/OpenAutoDiagLabs/CANgaroo/actions/workflows/cmake.yml) +![GitHub stars](https://img.shields.io/github/stars/OpenAutoDiagLabs/cangaroo?style=social) +![GitHub forks](https://img.shields.io/github/forks/OpenAutoDiagLabs/cangaroo?style=social) +![GitHub issues](https://img.shields.io/github/issues/OpenAutoDiagLabs/cangaroo) +![License](https://img.shields.io/github/license/OpenAutoDiagLabs/cangaroo) +![Release](https://img.shields.io/github/v/release/OpenAutoDiagLabs/cangaroo) + +Cangaroo is a professional-grade CAN bus analyzer designed for engineers in **Automotive**, **Robotics**, and **Industrial Automation**. It facilitates real-time capture, decoding, and analysis of CAN and CAN‑FD traffic. + +> **Works natively with SocketCAN on Linux and WinUSB/CandleAPI on Windows for immediate real hardware connections.** + +--- + +## 🎥 Demo Gallery +*Real-time capture and decoding of CAN traffic using DBC databases.* +
![Cangaroo Trace View](img/trace_view.gif)
+ +*Simulate CAN traffic with customizable periodic and manual transmissions.* +
![Cangaroo Generator View](img/generator_view.gif)
+ +*Seamlessly reassemble and decode UDS (Unified Diagnostic Services) messages over the ISO-TP transport layer.* +
![Cangaroo UDS ISO-TP](img/uds_iso_tp.gif)
+ +*Robust analysis of J1939 heavy-duty protocols, supporting Multi-frame (BAM) reassembly and PGN identification.* +
![Cangaroo J1939](img/j1939.gif)
+ +*Visualize CAN signals in real-time with Time-series, Scatter charts, Text view, and interactive Gauges.* +
![Cangaroo Graph View](img/graph_view.gif)
+ +--- + +## 🚀 Key Features + +* **Real-time CAN/CAN-FD Decoding**: Support for standard and high-speed flexible data-rate frames. +* **Wide Hardware Compatibility**: Seamlessly works with **SocketCAN** on Linux (supports PCAN, Kvaser, etc.), **WinUSB/CandleAPI** on Windows (gs_usb/CandleLight adapters), **SLCAN**, and **CANblaster** (UDP). +* **DBC Database Support**: Load multiple `.dbc` files to instantly decode frames into human-readable signals. +* **Powerful Data Visualization**: Integrated Graphing tools supporting Time-series, Scatter charts, Text-based monitoring, and interactive Gauge views with zoom and live tooltips. +* **Python Scripting API**: Automate measurement and simulation via an embedded Python engine. View the [API Documentation](cangaroo_python_api.md). +* **Advanced Filtering & Logging**: Isolate critical data with live filters and export captures for offline analysis. +* **Modern Workspace**: A clean, dockable userinterface optimized for multi-monitor setups. + +--- + +## 🐍 Python Automation + +CANgaroo includes an embedded Python environment that allows you to script repetitive tasks, automate complex transmission sequences, or analyze incoming traffic programmatically. + +Check out the full [Python API Documentation](cangaroo_python_api.md) for examples and reference. + +--- + +## 🛠️ Installation & Setup (Linux) + +### Option 1: Build from Source +Getting started is as simple as running our automated setup script: + +```bash +git clone https://github.com/OpenAutoDiagLabs/CANgaroo.git +cd CANgaroo +./install_linux.sh +``` +Follow the interactive menu to install dependencies and build the project. + +### Option 2: Using Release Package +If you downloaded a pre-compiled release tarball, use the included setup script to prepare your environment: + +1. Extract the package: `tar -xzvf cangaroo-vX.Y.Z-linux-x86_64.tar.gz` +2. Run the setup script: `./setup_release.sh` +3. Select an option to install dependencies and/or install Cangaroo to `/usr/local/bin`. + +### Manual Dependency Installation + +| Distribution | Command | +| :--- | :--- | +| **Ubuntu / Debian** | `sudo apt install qt6-base-dev libqt6charts6-dev libqt6serialport6-dev build-essential libnl-3-dev libnl-route-3-dev` | +| **Fedora** | `sudo dnf install qt6-qtbase-devel qt6-qtcharts-devel qt6-qtserialport-devel libnl3-devel` | +| **Arch Linux** | `sudo pacman -S qt6-base qt6-charts qt6-serialport libnl` | + +--- + +## 🔌 Hardware Support + +Cangaroo is designed to be cross-platform, offering robust hardware support on both Linux and Windows. + +### Linux (SocketCAN) +Cangaroo leverages the standard Linux **SocketCAN** subsystem. This means it works with virtually any CAN interface supported by the Linux kernel. + +### Windows (WinUSB / CandleAPI) +On Windows, Cangaroo includes native support for adapters running the **gs_usb** protocol (such as CANable and CandleLight) via WinUSB and the CandleAPI driver. + +### Supported Hardware +* **PEAK-System (PCAN)**: + * PCAN-USB, PCAN-USB Pro, PCAN-PCIe, etc. (Native driver: `peak_usb`). +* **Native USB-CAN Adapters**: + * [CANable](https://canable.io/) (with Candlelight firmware) + * Kvaser USB/CAN Leaf + * Candlelight compatible devices (e.g., MKS CANable, cantact) +* **USB SLCAN Adapters**: + * CANable (with set-default SLCAN firmware) + * Arduino-based CAN shields (running SLCAN sketches) +* **Industrial / Embedded CAN**: + * PCIe/mPCIe CAN cards + * Embedded CAN controllers on SoCs (e.g., Raspberry Pi with MCP2515) +* **Remote / Network CAN**: + * [CANblaster](https://github.com/OpenAutoDiagLabs/CANblaster) (UDP) + * tcpcan / candlelight-over-ethernet + +### Setup Instructions + +#### 1. Native Drivers (gs_usb, pcan, etc.) +Most professional hardware is recognized automatically as `can0`, `can1`, etc. To bring up an interface at 500k bitrate: +```bash +sudo ip link set can0 up type can bitrate 500000 +``` + +#### 2. USB SLCAN (Adapters using Serial/COM) +If your device uses SLCAN (like original CANable firmware), use `slcand`: +```bash +# Connect device as /dev/ttyUSB0 and set bitrate (S6 = 500k) +sudo slcand -o -s6 -t hw -S 115200 /dev/ttyUSB0 slcan0 +sudo ip link set slcan0 up +``` + +--- + +## �🚦 Quick Start Guide + +### 1. Zero-Hardware Testing (Virtual CAN) +```bash +sudo modprobe vcan +sudo ip link add dev vcan0 type vcan +sudo ip link set up vcan0 +``` + +### 2. Remote CAN Monitoring (SSH Pipe) +![Remote CAN Monitoring](img/remote_can_monitoring.png) +Monitor traffic from a remote machine (e.g., a Raspberry Pi on your vehicle) on your local PC: +```bash +# On your local machine, setup vcan0 as shown above, then: +ssh user@remote-ip "candump -L can0" | canplayer vcan0=can0 -t +``` +*Now open Cangaroo and connect to `vcan0` to see the remote traffic.* + +#### Nested SSH Tunneling (Multi-hop) +If the target device is behind a jump host or firewall: +![Remote CAN Monitoring](img/nested_remote_can_monitoring.png) +1. **Create an SSH Tunnel**: Expose the remote device's SSH port to your local machine. +```bash +# local-pc -> jump-host -> target-device +sshpass ssh -N -L localhost:9999::22 user@jump-host-ip + +eg: ssh -N -L localhost:9999:10.66.201.60:22 root@10.147.17.225 +``` + +**Breakdown of the command:** +| Item | Description | +| :--- | :--- | +| `localhost:9999` | The local port on your **PC** that will map to the target device. | +| `10.66.201.60` | The Internal IP of the **Remote Linux Device** (Target). | +| `22` | The SSH port on the **Remote Linux Device**. | +| `root@10.147.17.225` | The login details for the **Jump Host / Remote PC** that has access to the target. | + +2. **Stream CAN over the Tunnel**: +```bash +ssh -p 9999 root@localhost "stdbuf -o0 candump -L can0" | canplayer vcan0=can0 -t +``` + +### 3. ARXML to DBC Conversion +Cangaroo natively supports DBC. If you have ARXML files, you can convert them using `canconvert`: +```bash +# Install canconvert +pip install canconvert + +# Convert ARXML to DBC +canconvert TCU.arxml TCU.dbc +``` + +--- + +## 📥 Downloads & Releases + +Download the latest release tarball from the [Releases Page](https://github.com/OpenAutoDiagLabs/cangaroo/releases). + +--- + +## 🤝 Contribution & Community + +### Verifying Your Download +All official releases include a SHA256 checksum. Verify your download using: + +```bash +sha256sum cangaroo-v0.4.3-linux-x86_64.tar.gz +# Expected output: +# abc123def456... cangaroo-v0.4.3-linux-x86_64.tar.gz +``` + +--- + +## 🤝 Contribution & Community + +We welcome contributions! +* **Report Bugs**: Open an issue on our [GitHub Tracker](https://github.com/OpenAutoDiagLabs/cangaroo/issues). +* **Suggest Features**: Start a discussion in the [Discussions Tab](https://github.com/OpenAutoDiagLabs/cangaroo/discussions). +* **Contribute Code**: See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +--- + +## 📜 Credits + +* **Original Author**: Hubert Denkmair ([hubert@denkmair.de](mailto:hubert@denkmair.de)) +* **Lead Maintainer**: [Jayachandran Dharuman](https://github.com/OpenAutoDiagLabs/cangaroo) + +--- + +## 📝 Changelog Summary (v0.4.5) +* **New Graph View Feature**: Added a versatile visualization suite including: + * **Time-series Graph**: Smooth real-time signal plotting with interactive cursors and tooltips. + * **Scatter Chart**: Visualize signal correlations and distributions. + * **Text View**: Compact, live-updating text representation of signal values. + * **Gauge View**: High-visibility analog/digital gauges with customizable column layouts. +* **Interactive Analysis Tools**: Integrated zooming (In/Out/Reset), signal color customization, and absolute timestamp cursors. + +## 📝 Changelog Summary (v0.4.4) +* **Unified Protocol Decoding**: Intelligent prioritization between J1939 (29-bit) and UDS/ISO-TP (11-bit) with robust Transport Protocol reassembly. +* **Enhanced J1939 Support**: Auto-labeling for common PGNs (VIN, EEC1) and reassembled multi-frame (BAM/CM) messages. +* **Generator Synchronization**: Global "Stop" now halts all background cyclic transmissions automatically for safe simulation teardown. +* **Responsive State Management**: Replaced unstable signal blocking with a "Safe Flag Pattern" to ensure responsive UI editing without data corruption. +* **Generator Loopback**: Transmitted frames are now visible in the Trace View (TX labels), providing a complete view of bus activity. + +--- + +**Keywords**: CAN bus analyzer Linux, SocketCAN GUI, CAN FD decoder, J1939 analyzer, UDS ISO-TP decoder, automotive diagnostic tool. + +**License**: [GPL-3.0+](LICENSE) diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..04a373ef --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.16.0 diff --git a/cangaroo.desktop b/cangaroo.desktop index f98c9e31..c52786e5 100644 --- a/cangaroo.desktop +++ b/cangaroo.desktop @@ -2,7 +2,7 @@ Version=1.0 Type=Application Terminal=false -Name=cangaroo +Name=CANgaroo Exec=/usr/bin/cangaroo %f Comment=CAN Bus Analyzer Icon=cangaroo diff --git a/cangaroo.pro b/cangaroo.pro index c414d160..5952e4a4 100644 --- a/cangaroo.pro +++ b/cangaroo.pro @@ -1,5 +1,8 @@ -SUBDIRS += src \ - canifconfig -TEMPLATE = subdirs +QT += charts + +# QT += network +SUBDIRS += src +TEMPLATE = subdirs CONFIG += ordered warn_on qt debug_and_release -CONFIG += c++11 +CONFIG += c++20 +LIBS += -lbsd diff --git a/cangaroo_python_api.md b/cangaroo_python_api.md new file mode 100644 index 00000000..a92987dc --- /dev/null +++ b/cangaroo_python_api.md @@ -0,0 +1,102 @@ +# CANgaroo Python API Reference + +The embedded `cangaroo` module allows you to interact with the CANgaroo measurement backend directly from your scripts. + +## `cangaroo.Message` Class + +The primary class for constructing and interpreting CAN messages. + +### Properties +- `id` (int): The CAN message ID. +- `dlc` (int): The length of the payload in bytes (Data Length Code). +- `extended` (bool): `True` if it is an extended (29-bit) ID, `False` for standard (11-bit). +- `fd` (bool): `True` if the frame is a CAN FD frame. +- `rtr` (bool): `True` if the frame is a Remote Transmission Request. +- `brs` (bool): `True` if the CAN FD frame has Bit Rate Switching enabled. +- `interface_id` (int, read-only): The ID of the interface that received this message. +- `timestamp` (float, read-only): The reception timestamp in seconds. +- `is_rx` (bool, read-only): `True` if the message was received from the bus (RX), `False` if it was transmitted locally (TX). +- `bustype` (str, read-only): The bus type (returns `"CAN"`). + +### Methods +- `get_byte(index: int) -> int`: Get the value of a specific data byte (0-63). +- `set_byte(index: int, value: int)`: Set the value of a specific data byte. +- `get_data() -> bytes`: Returns the entire payload as a Python `bytes` object. +- `set_data(data: bytes)`: Sets the payload to the provided bytes and automatically updates the `dlc` property based on the length of the bytes provided (capped at 64). + +--- + +## CAN Interfaces + +To send a message to a specific hardware interface, you must provide the `interface_id`. By default, functions use interface `0` (the first available interface). + +- **`cangaroo.interfaces() -> list[dict]`** + Returns a list of dictionaries representing the active interfaces. + Example Output: `[{'id': 0, 'name': 'can0'}, {'id': 1, 'name': 'vcan0'}]` + +- **`cangaroo.interface_name(id: int) -> str`** + Returns the human-readable string name of the given interface ID. + +--- + +## Transmission (TX) + +- **`cangaroo.send(msg: cangaroo.Message, interface_id: int = 0)`** + Sends a single CAN message over the specified interface. + +- **`cangaroo.send_periodic(msg: cangaroo.Message, interval_ms: int, interface_id: int = 0) -> int`** + Starts a background periodic transmission of the message at the requested interval. Returns a unique integer `handle` for the periodic task. + +- **`cangaroo.stop_periodic(handle: int)`** + Stops a running periodic background task using the `handle` returned by `send_periodic()`. + +--- + +## Reception (RX) + +- **`cangaroo.receive(timeout_sec: float = 1.0) -> list[cangaroo.Message]`** + Blocks execution until messages are received or the timeout is reached. Returns a list of `cangaroo.Message` objects. + +- **`cangaroo.set_filter(id: int, mask: int = 0xFFFFFFFF, extended: bool = None)`** + Applies a filter to the `receive()` queue. Only messages matching `(received_id & mask) == (id & mask)` will be enqueued. + +- **`cangaroo.clear_filter()`** + Removes the active receive filter, allowing all messages to be caught by `receive()`. + +- **`cangaroo.enable_tx_echo(enabled: bool = True)`** + By default, `receive()` only returns actual incoming (RX) messages. Calling this with `True` causes locally sent messages (TX echo) to also appear in `receive()`. + +--- + +## Trace Window Access + +- **`cangaroo.get_trace(count: int = 0) -> list[cangaroo.Message]`** + Retrieves the last `count` messages directly from the global CANgaroo Trace View. If `count` is 0 or omitted, it retrieves the entire trace buffer. + +- **`cangaroo.trace_size() -> int`** + Returns the total number of messages currently stored in the Trace View. + +- **`cangaroo.clear_trace()`** + Wipes the Trace View history clean. + +--- + +## Measurement Control & Logging + +- **`cangaroo.start_measurement() -> bool`** + Starts the CANgaroo measurement (equivalent to clicking the Play button). Returns `True` on success. + +- **`cangaroo.stop_measurement() -> bool`** + Stops the active CANgaroo measurement. + +- **`cangaroo.measurement_running() -> bool`** + Returns `True` if measurement is currently active. + +- **`cangaroo.log_info(text: str)`** + Logs an informational message to the CANgaroo Log view. + +- **`cangaroo.log_warning(text: str)`** + Logs a warning message (yellow) to the CANgaroo Log view. + +- **`cangaroo.log_error(text: str)`** + Logs an error message (red) to the CANgaroo Log view. diff --git a/canifconfig/canifconfig b/canifconfig/canifconfig new file mode 100755 index 00000000..fefcd476 Binary files /dev/null and b/canifconfig/canifconfig differ diff --git a/create_release.sh b/create_release.sh new file mode 100755 index 00000000..8d02a8d3 --- /dev/null +++ b/create_release.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# 1. DETECT VERSION +if [ -f "VERSION" ]; then + VERSION=$(cat VERSION | tr -d '\n') + echo "Detected Version: $VERSION" +else + echo "Error: VERSION file not found!" + exit 1 +fi +APP_NAME="cangaroo" +BINARY_PATH="./bin/${APP_NAME}" +ARCH=$(uname -m) +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +RELEASE_NAME="${APP_NAME}-v${VERSION}-${OS}-${ARCH}" +TAR_FILE="${RELEASE_NAME}.tar.gz" + +# 2. CHECK IF BINARY EXISTS +if [ ! -f "$BINARY_PATH" ]; then + echo "Error: Binary not found at $BINARY_PATH" + echo "Please build the project first." + exit 1 +fi + +echo "Packaging ${APP_NAME} version ${VERSION}..." + +# 3. CREATE STAGING AND RELEASE DIRECTORIES +mkdir -p release_stage +mkdir -p releases +cp "$BINARY_PATH" release_stage/ +cp README.md release_stage/ +cp LICENSE release_stage/ +cp CONTRIBUTING.md release_stage/ +cp setup_release.sh release_stage/ + +# 4. CREATE TARBALL +# -C changes directory so the tar doesn't include the 'release_stage' parent folder path +tar -czf "releases/$TAR_FILE" -C release_stage . + +# 5. GENERATE CHECKSUM +cd releases +sha256sum "$TAR_FILE" > "${TAR_FILE}.sha256" +cd .. + +# 6. CLEANUP & OUTPUT +rm -rf release_stage + +echo "-------------------------------------------------------" +echo "Release Created Successfully:" +echo "Location: releases/" +echo "Package: $TAR_FILE" +echo "Checksum: ${TAR_FILE}.sha256" +echo "-------------------------------------------------------" +echo "SHA256 Output:" +cat "releases/${TAR_FILE}.sha256" +echo "-------------------------------------------------------" diff --git a/examples/dbc_decode.py b/examples/dbc_decode.py new file mode 100644 index 00000000..c8c879f1 --- /dev/null +++ b/examples/dbc_decode.py @@ -0,0 +1,49 @@ +""" +Decode received CAN messages using loaded DBC files. + +Demonstrates: + - cangaroo.databases() — list all loaded DBC files and their messages + - cangaroo.decode(msg) — decode a message into physical signal values + - cangaroo.lookup(msg) — inspect the DBC definition (bit layout, scaling, etc.) + +Usage: Load one or more DBC files in the Measurement Setup, start the +measurement, then run this script. +""" +import cangaroo + +# ---- show loaded databases ---- +dbs = cangaroo.databases() +if not dbs: + print("No DBC files loaded. Add a DBC in Measurement Setup.") +else: + for db in dbs: + print(f"Database: {db['file']} (network: {db['network']})") + for m in db['messages']: + sig_names = ', '.join(m['signals']) if m['signals'] else '(none)' + print(f" 0x{m['id']:03X} {m['name']} [{m['dlc']}] {sig_names}") + print() + +# ---- receive and decode ---- +print("Waiting for messages...\n") + +while True: + messages = cangaroo.receive(timeout=1.0) + + for msg in messages: + decoded = cangaroo.decode(msg) + + if decoded is None: + # No DBC definition for this ID — print raw + print(f"0x{msg.id:03X} [{msg.dlc}] {msg.get_data().hex(' ')} (unknown)") + continue + + sender = decoded.get('sender', '?') + print(f"0x{msg.id:03X} {decoded['message']} (sender: {sender})") + + for name, sig in decoded['signals'].items(): + value_name = sig.get('value_name', '') + if value_name: + print(f" {name}: {value_name} ({sig['value']:.4g} {sig['unit']})") + else: + print(f" {name}: {sig['value']:.4g} {sig['unit']}" + f" [raw: {sig['raw']}]") diff --git a/examples/demo.dbc b/examples/demo.dbc new file mode 100644 index 00000000..77689b19 --- /dev/null +++ b/examples/demo.dbc @@ -0,0 +1,48 @@ +VERSION "" + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIG_UNIT_ + SG_MUL_VAL_ + +BS_: + +BU_: EngineECU TransmissionECU DisplayECU + +BO_ 256 EngineData: 8 EngineECU + SG_ EngineSpeed : 0|16@0+ (1,0) [0|8000] "rpm" DisplayECU + SG_ EngineTemp : 16|8@0+ (1,-40) [-40|215] "degC" DisplayECU + SG_ OilPressure : 24|8@0+ (0.1,0) [0|25] "bar" DisplayECU + +BO_ 512 TransmissionData: 4 TransmissionECU + SG_ GearPos : 0|4@0+ (1,0) [0|15] "" DisplayECU + SG_ VehicleSpeed : 4|12@0+ (0.1,0) [0|400] "km/h" DisplayECU + +BO_ 768 AmbientData: 2 SensorECU + SG_ OutsideTemp : 0|8@0+ (1,-40) [-40|100] "degC" DisplayECU + SG_ Humidity : 8|8@0+ (0.5,0) [0|100] "%" DisplayECU + +CM_ BO_ 256 "Engine status information"; +CM_ SG_ 256 EngineSpeed "Current engine rotations per minute"; +CM_ BO_ 512 "Transmission status and vehicle speed"; +CM_ BO_ 768 "Environment sensor data"; + +VAL_ 512 GearPos 0 "Neutral" 1 "First" 2 "Second" 3 "Third" 4 "Fourth" 5 "Fifth" 6 "Sixth" 7 "Reverse" 15 "Error" ; diff --git a/examples/demo.ldf b/examples/demo.ldf new file mode 100644 index 00000000..cdb8e691 --- /dev/null +++ b/examples/demo.ldf @@ -0,0 +1,194 @@ +LIN_description_file; +LIN_protocol_version = "2.0"; +LIN_language_version = "2.0"; +LIN_speed = 19.2 kbps; + +// Two-slave motor/lighting demo bus +// Master timebase 5 ms, jitter 0.2 ms + +Nodes { + Master: Master, 5 ms, 0.2 ms; + Slaves: Slave1, Slave2; +} + +// --------------------------------------------------------------------------- +// Signals +// --------------------------------------------------------------------------- + +Signals { + // Master → Slave1 (motor controller) + TargetSpeed_S1: 16, 0, Master, Slave1; // 0-8000 rpm, 0.5 rpm/bit + MotorEnable_S1: 8, 0, Master, Slave1; // 0=Off 1=Fwd 2=Rev 3=Brake + RampRate_S1: 8, 0, Master, Slave1; // 0-255, 50 rpm/s per bit + + // Slave1 → Master (motor controller status) + ActualSpeed_S1: 16, 0, Slave1, Master; // current speed, same encoding + ErrorFlags_S1: 8, 0, Slave1, Master; // error bitmask + Temperature_S1: 8, 0, Slave1, Master; // -40 … +87.5 °C, 0.5 °C/bit + + // Master → Slave2 (lighting controller) + LightIntensity_S2: 8, 0, Master, Slave2; // 0-100 % + LightMode_S2: 8, 0, Master, Slave2; // operating mode + + // Slave2 → Master (lighting controller status) + AmbientLight_S2: 16, 0, Slave2, Master; // 0-6553.5 lux, 0.1 lux/bit + ButtonState_S2: 8, 0, Slave2, Master; // 0=Released 1=Pressed + SupplyVoltage_S2: 8, 0, Slave2, Master; // 0-25.5 V, 0.1 V/bit +} + +// --------------------------------------------------------------------------- +// Frames +// --------------------------------------------------------------------------- + +Frames { + // Slave1 — motor controller (4 bytes each) + MasterToSlave1: 0x01, Master, 4 { + TargetSpeed_S1, 0; // bits 0-15 + MotorEnable_S1, 16; // bits 16-23 + RampRate_S1, 24; // bits 24-31 + } + Slave1ToMaster: 0x02, Slave1, 4 { + ActualSpeed_S1, 0; // bits 0-15 + ErrorFlags_S1, 16; // bits 16-23 + Temperature_S1, 24; // bits 24-31 + } + + // Slave2 — lighting controller (2 bytes TX, 4 bytes RX) + MasterToSlave2: 0x03, Master, 2 { + LightIntensity_S2, 0; // bits 0-7 + LightMode_S2, 8; // bits 8-15 + } + Slave2ToMaster: 0x04, Slave2, 4 { + AmbientLight_S2, 0; // bits 0-15 + ButtonState_S2, 16; // bits 16-23 + SupplyVoltage_S2,24; // bits 24-31 + } +} + +// --------------------------------------------------------------------------- +// Signal encoding types +// --------------------------------------------------------------------------- + +Signal_encoding_types { + MotorSpeedEncoding { + physical_value, 0, 8000, 0.5, 0, "rpm"; + } + MotorEnableEncoding { + logical_value, 0, "Off"; + logical_value, 1, "Forward"; + logical_value, 2, "Reverse"; + logical_value, 3, "Brake"; + } + RampRateEncoding { + physical_value, 0, 12750, 50, 0, "rpm/s"; + } + ErrorFlagsEncoding { + logical_value, 0, "OK"; + logical_value, 1, "OverCurrent"; + logical_value, 2, "OverTemp"; + logical_value, 4, "HallFault"; + } + TemperatureEncoding { + physical_value, 0, 255, 0.5, -40, "degC"; + } + PercentEncoding { + physical_value, 0, 100, 1, 0, "%"; + } + LightModeEncoding { + logical_value, 0, "Off"; + logical_value, 1, "Dim"; + logical_value, 2, "Normal"; + logical_value, 3, "Bright"; + logical_value, 4, "Auto"; + } + AmbientLightEncoding { + physical_value, 0, 6553.5, 0.1, 0, "lux"; + } + ButtonEncoding { + logical_value, 0, "Released"; + logical_value, 1, "Pressed"; + } + VoltageEncoding { + physical_value, 0, 25.5, 0.1, 0, "V"; + } +} + +// --------------------------------------------------------------------------- +// Signal representation +// --------------------------------------------------------------------------- + +Signal_representation { + MotorSpeedEncoding: TargetSpeed_S1, ActualSpeed_S1; + MotorEnableEncoding: MotorEnable_S1; + RampRateEncoding: RampRate_S1; + ErrorFlagsEncoding: ErrorFlags_S1; + TemperatureEncoding: Temperature_S1; + PercentEncoding: LightIntensity_S2; + LightModeEncoding: LightMode_S2; + AmbientLightEncoding:AmbientLight_S2; + ButtonEncoding: ButtonState_S2; + VoltageEncoding: SupplyVoltage_S2; +} + +// --------------------------------------------------------------------------- +// Node attributes +// --------------------------------------------------------------------------- + +Node_attributes { + Slave1 { + LIN_protocol = "2.0"; + configured_NAD = 0x01; + initial_NAD = 0x01; + product_id = 0x0001, 0x0001, 0; + response_error = ErrorFlags_S1; + P2_min = 50 ms; + ST_min = 0 ms; + N_As_timeout = 1000 ms; + N_Cr_timeout = 1000 ms; + configurable_frames { + MasterToSlave1; + Slave1ToMaster; + } + } + Slave2 { + LIN_protocol = "2.0"; + configured_NAD = 0x02; + initial_NAD = 0x02; + product_id = 0x0001, 0x0002, 0; + P2_min = 50 ms; + ST_min = 0 ms; + N_As_timeout = 1000 ms; + N_Cr_timeout = 1000 ms; + configurable_frames { + MasterToSlave2; + Slave2ToMaster; + } + } +} + +// --------------------------------------------------------------------------- +// Schedule tables +// --------------------------------------------------------------------------- + +Schedule_tables { + // Normal operation: all four frames polled at equal 10 ms intervals. + // Total cycle: 40 ms → Slave1 at 25 Hz, Slave2 at 25 Hz. + NormalOperation { + MasterToSlave1 delay 10 ms; + Slave1ToMaster delay 10 ms; + MasterToSlave2 delay 10 ms; + Slave2ToMaster delay 10 ms; + } + + // Fast motor control: Slave1 polled twice per cycle for tighter speed loop, + // Slave2 updated once at the end with a longer slot. + // Total cycle: 40 ms → Slave1 at 50 Hz, Slave2 at 25 Hz. + FastMotorControl { + MasterToSlave1 delay 5 ms; + Slave1ToMaster delay 5 ms; + MasterToSlave2 delay 10 ms; + Slave2ToMaster delay 10 ms; + MasterToSlave1 delay 5 ms; + Slave1ToMaster delay 5 ms; + } +} diff --git a/examples/encode_send.py b/examples/encode_send.py new file mode 100644 index 00000000..54c84f97 --- /dev/null +++ b/examples/encode_send.py @@ -0,0 +1,105 @@ +""" +Build and send CAN messages using DBC signal encoding. + +Demonstrates: + - cangaroo.encode() — build a Message from named signal values + - cangaroo.signal_value() — extract a single signal's physical value + - cangaroo.find_message() — look up a DBC message definition by name or ID + +Usage: Load demo.dbc in Measurement Setup, start the measurement, then run +this script. Adjust interface_id to match your setup. +""" +import cangaroo +import time + +INTERFACE_ID = 0 # change to match your setup + +# ---- inspect a message definition before encoding ---- +defn = cangaroo.find_message("EngineData") +if defn is None: + print("EngineData not found — is demo.dbc loaded?") +else: + print(f"Message: {defn['message']} ID=0x{defn['id']:03X} DLC={defn['dlc']}") + for sig in defn["signals"]: + print(f" {sig['name']:20s} [{sig['start_bit']}|{sig['length']}]" + f" factor={sig['factor']} offset={sig['offset']}" + f" unit={sig['unit']!r}") + print() + +# ---- encode and send EngineData ---- +engine_msg = cangaroo.encode("EngineData", { + "EngineSpeed": 3500.0, # rpm + "EngineTemp": 90.0, # degC + "OilPressure": 4.2, # bar +}) +cangaroo.send(engine_msg, interface_id=INTERFACE_ID) +print(f"Sent EngineData: {engine_msg}") + +# Verify round-trip: decode the just-sent message +speed = cangaroo.signal_value(engine_msg, "EngineSpeed") +temp = cangaroo.signal_value(engine_msg, "EngineTemp") +oil = cangaroo.signal_value(engine_msg, "OilPressure") +print(f" EngineSpeed={speed} rpm EngineTemp={temp} degC OilPressure={oil} bar") +print() + +# ---- encode and send TransmissionData ---- +# GearPos has value names: 0=Neutral 1=First … 7=Reverse +for gear_pos, speed_kmh in [(1, 15.0), (2, 35.0), (3, 60.0), (0, 0.0)]: + tx = cangaroo.encode("TransmissionData", { + "GearPos": float(gear_pos), + "VehicleSpeed": speed_kmh, + }) + cangaroo.send(tx, interface_id=INTERFACE_ID) + decoded = cangaroo.decode(tx) + gear_name = decoded["signals"]["GearPos"].get("value_name", str(gear_pos)) + print(f"Sent TransmissionData: gear={gear_name} speed={speed_kmh} km/h {tx}") + time.sleep(0.05) + +print() + +# ---- encode by raw ID instead of name ---- +ambient_msg = cangaroo.encode(768, { # 0x300 = AmbientData + "OutsideTemp": 21.5, # degC + "Humidity": 65.0, # % +}) +cangaroo.send(ambient_msg, interface_id=INTERFACE_ID) +print(f"Sent AmbientData (by ID): {ambient_msg}") +print(f" OutsideTemp={cangaroo.signal_value(ambient_msg, 'OutsideTemp')} degC" + f" Humidity={cangaroo.signal_value(ambient_msg, 'Humidity')} %") + +print("\nDone.") + +# ── AUTOSAR E2E Profile 2 ────────────────────────────────────────────────────── +# e2e_p2_protect(msg, data_id, counter) +# Writes the counter nibble into byte 1 and the CRC-8H2F into byte 0 in-place. +# +# e2e_p2_compute_crc(msg, data_id) -> int +# Returns the CRC byte only; useful when you need to inspect or place it manually. +# +# Message layout after protect(): +# Byte 0: CRC (all 8 bits) +# Byte 1: bits 7:4 = reserved (0), bits 3:0 = counter (0–14) +# Byte 2+: payload + +DATA_ID = 0x1234 +counter = 0 + +# Build the message from DBC signals (bytes 0 and 1 are reserved for E2E header) +e2e_msg = cangaroo.encode("EngineData", { + "EngineSpeed": 3500.0, + "EngineTemp": 90.0, +}) + +# One-shot: write counter + CRC and send +cangaroo.e2e_p2_protect(e2e_msg, data_id=DATA_ID, counter=counter) +cangaroo.send(e2e_msg, interface_id=INTERFACE_ID) +print(f"Sent E2E-protected EngineData: CRC=0x{e2e_msg.get_byte(0):02X} " + f"counter nibble=0x{e2e_msg.get_byte(1):02X} {e2e_msg}") + +# Fine-grained: compute CRC manually and place it yourself +e2e_msg2 = cangaroo.encode("EngineData", {"EngineSpeed": 1000.0, "EngineTemp": 25.0}) +e2e_msg2.set_byte(1, counter & 0x0F) # write counter nibble first +crc = cangaroo.e2e_p2_compute_crc(e2e_msg2, data_id=DATA_ID) # compute CRC +cangaroo.log(f"E2E P2 CRC: 0x{crc:02X}") +e2e_msg2.set_byte(0, crc) # place CRC +cangaroo.send(e2e_msg2, interface_id=INTERFACE_ID) diff --git a/examples/event_echo.py b/examples/event_echo.py new file mode 100644 index 00000000..84f28011 --- /dev/null +++ b/examples/event_echo.py @@ -0,0 +1,50 @@ +""" +Event-based message handler. + +Waits (without polling) for incoming CAN messages. Each time a message +arrives, the on_message callback is invoked. For every received message +a response with ID 0x123 and random data is sent back. + +Usage: Run while a measurement is active. Press Stop to end. +""" +import cangaroo +import os + +# ---- configuration ---- +RESPONSE_ID = 0x123 +RESPONSE_DLC = 8 +INTERFACE_ID = 513 # change to match your setup + +# Print available interfaces +for iface in cangaroo.interfaces(): + print(f"Interface {iface['id']}: {iface['name']}") + +def on_message(msg): + """Called for every received CAN message.""" + data_hex = msg.get_data().hex(' ') + print(f"RX 0x{msg.id:03X} [{msg.dlc}] {data_hex}") + + # Skip sending a response for our own response ID to avoid loops + if msg.id == RESPONSE_ID: + return + + # Build a response with random payload + resp = cangaroo.Message() + resp.id = RESPONSE_ID + resp.set_data(os.urandom(RESPONSE_DLC)) + cangaroo.send(resp, interface_id=INTERFACE_ID) + + resp_hex = resp.get_data().hex(' ') + print(f"TX 0x{resp.id:03X} [{resp.dlc}] {resp_hex}") + + +# ---- event loop ---- +print("Waiting for messages (event-based)...\n") + +while True: + # receive() blocks until at least one message arrives or timeout expires. + # No CPU is consumed while waiting (uses a condition variable internally). + messages = cangaroo.receive(timeout=1.0) + + for msg in messages: + on_message(msg) diff --git a/examples/filter_receive.py b/examples/filter_receive.py new file mode 100644 index 00000000..fb304c5d --- /dev/null +++ b/examples/filter_receive.py @@ -0,0 +1,62 @@ +""" +Receive only selected CAN IDs using set_filter() / clear_filter(). + +The filter is applied inside CANgaroo before messages enter the receive() +queue, so filtered-out traffic never reaches the script. + +Demonstrates: + - cangaroo.set_filter() — accept only messages whose (id & mask) == id + - cangaroo.clear_filter() — remove the filter, accept everything again + - cangaroo.trace_size() — total messages in the trace buffer + - cangaroo.clear_trace() — reset the trace buffer + +Usage: Start the measurement, then run. Works with or without a DBC loaded. +""" +import cangaroo +import time + +# ---- helper ---- +def receive_for(seconds, label): + """Drain the receive queue for `seconds`, printing each message.""" + print(f"\n--- {label} ---") + deadline = time.time() + seconds + count = 0 + while time.time() < deadline: + for msg in cangaroo.receive(timeout=0.1): + decoded = cangaroo.decode(msg) + if decoded: + sig_summary = ", ".join( + f"{n}={v['value']:.4g}{v['unit']}" + for n, v in decoded["signals"].items() + ) + print(f" 0x{msg.id:03X} {decoded['message']} {sig_summary}") + else: + print(f" 0x{msg.id:03X} [{msg.dlc}] {msg.get_data().hex(' ')}") + count += 1 + print(f" ({count} message(s) received)") + + +# ---- baseline: no filter ---- +cangaroo.clear_trace() +receive_for(1.0, "No filter — all traffic") +print(f"Trace size after 1 s: {cangaroo.trace_size()} messages") + +# ---- filter: accept only EngineData (ID 0x100 = 256) exactly ---- +cangaroo.set_filter(0x100, mask=0x7FF) +receive_for(1.0, "Filter: ID 0x100 only (EngineData)") + +# ---- filter: accept 0x100–0x1FF range (top 4 bits match 0x100) ---- +cangaroo.set_filter(0x100, mask=0x700) +receive_for(1.0, "Filter: 0x100–0x1FF range") + +# ---- filter: accept only extended frames ---- +cangaroo.set_filter(0x00000000, mask=0x00000000, extended=True) +receive_for(1.0, "Filter: extended frames only") + +# ---- remove filter ---- +cangaroo.clear_filter() +cangaroo.clear_trace() +receive_for(1.0, "Filter cleared — all traffic again") +print(f"Trace size after clear + 1 s: {cangaroo.trace_size()} messages") + +print("\nDone.") diff --git a/examples/lin_demo.py b/examples/lin_demo.py new file mode 100644 index 00000000..f3c7a9a7 --- /dev/null +++ b/examples/lin_demo.py @@ -0,0 +1,135 @@ +""" +LIN bus demonstration. + +Demonstrates: + - cangaroo.lin_databases() — list loaded LDF files and their frames + - cangaroo.find_lin_frame() — look up a LIN frame definition by name or ID + - cangaroo.make_lin_message() — build a LIN BusMessage + - cangaroo.send() — transmit the frame on a LIN interface + - cangaroo.decode_lin() — decode a received LIN frame using an LDF + - msg.bustype — distinguish LIN from CAN in a mixed trace + +Usage: + 1. Add your LDF file in Measurement Setup and connect a LIN-capable interface + (e.g. a GrIP LIN channel). + 2. Start the measurement. + 3. Paste this script into the Script window and click Run. + 4. Adjust INTERFACE_ID, FRAME_NAME, and signal values to match your LDF. +""" +import cangaroo +import time + +INTERFACE_ID = 0 # change to match your LIN interface +FRAME_NAME = "MasterToSlave1" # from demo.ldf +LISTEN_SECS = 5.0 # how long to listen for incoming LIN frames + +# --------------------------------------------------------------------------- +# 1. Show available interfaces +# --------------------------------------------------------------------------- +print("=== Interfaces ===") +for iface in cangaroo.interfaces(): + print(f" [{iface['id']}] {iface['name']}") +print() + +# --------------------------------------------------------------------------- +# 2. Show loaded LDF databases +# --------------------------------------------------------------------------- +print("=== LDF Databases ===") +ldfs = cangaroo.lin_databases() +if not ldfs: + print(" No LDF files loaded. Add one in Measurement Setup.") +else: + for db in ldfs: + print(f" {db['file']} (network: {db['network']}, speed: {db['speed']} bps)") + print(f" Master: {db['master']}") + for frame in db['frames']: + sig_names = ", ".join(s['name'] for s in frame['signals']) + print(f" ID=0x{frame['id']:02X} {frame['name']!r}" + f" [{frame['length']} B] signals: {sig_names or '(none)'}") +print() + +# --------------------------------------------------------------------------- +# 3. Inspect a specific frame definition +# --------------------------------------------------------------------------- +print(f"=== Frame definition: {FRAME_NAME!r} ===") +frame_def = cangaroo.find_lin_frame(FRAME_NAME) +if frame_def is None: + print(f" Frame {FRAME_NAME!r} not found — check your LDF and FRAME_NAME.") +else: + print(f" ID=0x{frame_def['id']:02X} length={frame_def['length']} B" + f" publisher={frame_def['publisher']!r}") + for sig in frame_def['signals']: + print(f" {sig['name']:20s} bit {sig['bit_offset']}+{sig['bit_length']}" + f" factor={sig['factor']} offset={sig['offset']}" + f" unit={sig['unit']!r}") +print() + +# Also look up by numeric ID (0x01 as an example) +frame_by_id = cangaroo.find_lin_frame(0x01) +if frame_by_id: + print(f"Frame at ID 0x01: {frame_by_id['name']!r}") +print() + +# --------------------------------------------------------------------------- +# 4. Build and send a LIN frame +# --------------------------------------------------------------------------- +print("=== Send LIN frame ===") +msg = cangaroo.make_lin_message(0x01, 4) +msg.set_data(bytes([0x11, 0x22, 0x33, 0x44])) +cangaroo.send(msg, interface_id=INTERFACE_ID) +print(f" Sent: {msg}") + +# Send a second frame using a known frame ID from the LDF +if frame_def is not None: + lin_id = frame_def['id'] + length = frame_def['length'] + tx = cangaroo.make_lin_message(lin_id, length) + # Fill with a simple incrementing pattern + tx.set_data(bytes(i & 0xFF for i in range(length))) + cangaroo.send(tx, interface_id=INTERFACE_ID) + print(f" Sent {FRAME_NAME!r}: {tx}") + + # Verify decode of the just-sent frame + decoded = cangaroo.decode_lin(tx) + if decoded: + print(f" Decoded {decoded['frame']!r} ID=0x{decoded['id']:02X}") + for name, sig in decoded['signals'].items(): + label = sig.get('value_name') or f"{sig['value']:.4g} {sig['unit']}" + print(f" {name}: {label} [raw={sig['raw']}]") +print() + +# --------------------------------------------------------------------------- +# 5. Receive and decode incoming frames (LIN + CAN mixed) +# --------------------------------------------------------------------------- +print(f"=== Listening for {LISTEN_SECS:.0f} s (LIN and CAN) ===") +deadline = time.time() + LISTEN_SECS +lin_count = 0 +can_count = 0 + +while time.time() < deadline: + for msg in cangaroo.receive(timeout=0.2): + if msg.bustype == "LIN": + lin_count += 1 + decoded = cangaroo.decode_lin(msg) + if decoded: + sig_str = " ".join( + f"{n}={v.get('value_name') or f\"{v['value']:.4g}{v['unit']}\"}" + for n, v in decoded['signals'].items() + ) + direction = "RX" if msg.is_rx else "TX" + print(f" LIN {direction} ID=0x{msg.id:02X} {decoded['frame']!r}" + f" [{msg.dlc} B] {sig_str}") + else: + direction = "RX" if msg.is_rx else "TX" + print(f" LIN {direction} ID=0x{msg.id:02X}" + f" [{msg.dlc} B] {msg.get_data().hex(' ')} (no LDF definition)") + else: + can_count += 1 + decoded = cangaroo.decode(msg) + if decoded: + print(f" CAN 0x{msg.id:03X} {decoded['message']!r}") + else: + print(f" CAN 0x{msg.id:03X} [{msg.dlc}] {msg.get_data().hex(' ')}") + +print(f"\nReceived {lin_count} LIN frame(s) and {can_count} CAN frame(s).") +print("Done.") diff --git a/examples/measurement_control.py b/examples/measurement_control.py new file mode 100644 index 00000000..db67bd31 --- /dev/null +++ b/examples/measurement_control.py @@ -0,0 +1,60 @@ +""" +Start / stop measurements and inspect the trace from a script. + +Demonstrates: + - cangaroo.measurement_running() — check whether a measurement is active + - cangaroo.start_measurement() — start the measurement programmatically + - cangaroo.stop_measurement() — stop the measurement programmatically + - cangaroo.trace_size() — number of messages currently in the trace + - cangaroo.clear_trace() — reset the trace buffer + - cangaroo.find_message() — inspect a DBC message definition by name + +Usage: Configure your interfaces in Measurement Setup (no need to start the +measurement manually), then run this script. +""" +import cangaroo +import time + +# ---- check initial state ---- +if cangaroo.measurement_running(): + print("Measurement is already running — stopping it first.") + cangaroo.stop_measurement() + +print(f"Measurement running: {cangaroo.measurement_running()}") +print(f"Trace size before start: {cangaroo.trace_size()} messages") + +# ---- start measurement ---- +ok = cangaroo.start_measurement() +print(f"\nstart_measurement() -> {ok}") +print(f"Measurement running: {cangaroo.measurement_running()}") + +# ---- collect traffic for a few seconds ---- +print("\nCollecting traffic for 3 seconds...") +time.sleep(3.0) +print(f"Trace size after 3 s: {cangaroo.trace_size()} messages") + +# ---- inspect DBC definitions (no live message needed) ---- +for name in ("EngineData", "TransmissionData", "AmbientData"): + defn = cangaroo.find_message(name) + if defn is None: + print(f"\n{name}: not found (is demo.dbc loaded?)") + else: + sig_names = [s["name"] for s in defn["signals"]] + print(f"\n{defn['message']} ID=0x{defn['id']:03X} DLC={defn['dlc']}") + print(f" Signals: {', '.join(sig_names)}") + +# ---- clear trace and collect again ---- +print("\nClearing trace...") +cangaroo.clear_trace() +print(f"Trace size after clear: {cangaroo.trace_size()} messages") + +time.sleep(1.0) +print(f"Trace size after 1 more second: {cangaroo.trace_size()} messages") + +# ---- stop measurement ---- +ok = cangaroo.stop_measurement() +print(f"\nstop_measurement() -> {ok}") +print(f"Measurement running: {cangaroo.measurement_running()}") +print(f"Final trace size: {cangaroo.trace_size()} messages") + +print("\nDone.") diff --git a/examples/periodic_send.py b/examples/periodic_send.py new file mode 100644 index 00000000..be186480 --- /dev/null +++ b/examples/periodic_send.py @@ -0,0 +1,66 @@ +""" +Send periodic CAN messages using send_periodic() / stop_periodic(). + +Each periodic task runs on its own background thread inside CANgaroo, so the +script can wait for incoming messages at the same time without any manual +timing loop. All tasks are automatically stopped when the script stops. + +Demonstrates: + - cangaroo.send_periodic() — start a repeating TX task, returns a handle + - cangaroo.stop_periodic() — cancel one task by handle + - cangaroo.receive() — wait for replies while periodic TX is running + +Usage: Load demo.dbc in Measurement Setup, start the measurement, then run. +Adjust INTERFACE_ID and intervals to match your setup. +""" +import cangaroo +import time + +INTERFACE_ID = 0 # change to match your setup + +# ---- show available interfaces ---- +for iface in cangaroo.interfaces(): + print(f"Interface {iface['id']}: {iface['name']}") + +# ---- build messages to send periodically ---- +heartbeat = cangaroo.Message() +heartbeat.id = 0x700 +heartbeat.set_data(bytes([0x01])) + +# Use encode() so we get the right DBC bit layout +engine_msg = cangaroo.encode("EngineData", { + "EngineSpeed": 2000.0, + "EngineTemp": 85.0, + "OilPressure": 3.8, +}) + +# ---- start periodic tasks ---- +h_heartbeat = cangaroo.send_periodic(heartbeat, interval_ms=100, interface_id=INTERFACE_ID) +h_engine = cangaroo.send_periodic(engine_msg, interval_ms=20, interface_id=INTERFACE_ID) + +print(f"Started heartbeat (handle={h_heartbeat}, every 100 ms)") +print(f"Started EngineData (handle={h_engine}, every 20 ms)") +print("Listening for incoming messages for 3 seconds...\n") + +# ---- receive loop while periodic TX runs in the background ---- +deadline = time.time() + 3.0 +while time.time() < deadline: + msgs = cangaroo.receive(timeout=0.1) + for msg in msgs: + decoded = cangaroo.decode(msg) + if decoded: + print(f"RX 0x{msg.id:03X} {decoded['message']}") + else: + print(f"RX 0x{msg.id:03X} [{msg.dlc}] {msg.get_data().hex(' ')}") + +# ---- stop individual tasks ---- +cangaroo.stop_periodic(h_engine) +print(f"\nStopped EngineData (handle={h_engine}).") +print("Heartbeat still running for 1 more second...") +time.sleep(1.0) + +# h_heartbeat is stopped automatically when the script exits, +# but we can also stop it explicitly: +cangaroo.stop_periodic(h_heartbeat) +print(f"Stopped heartbeat (handle={h_heartbeat}).") +print("Done.") diff --git a/examples/print_messages.py b/examples/print_messages.py new file mode 100644 index 00000000..2bfe1917 --- /dev/null +++ b/examples/print_messages.py @@ -0,0 +1,62 @@ +""" +Continuously print all received CAN messages and send a random +CAN message with ID 0x123 every 200 ms. + +Usage: Paste into the Script window and click Run while a measurement is active. +Press Stop to end the script. +""" +import cangaroo +import os +import time + +# ---- configuration ---- +TX_ID = 0x123 +TX_DLC = 8 +TX_INTERVAL = 0.2 # seconds +INTERFACE_ID = 513 # change to match your setup + +# Print available interfaces +for iface in cangaroo.interfaces(): + print(f"Interface {iface['id']}: {iface['name']}") + +print("Waiting for messages...\n") + +last_send = time.time() + +while True: + # receive() blocks up to timeout — use a short timeout so we can + # send at the desired interval even when no messages arrive. + messages = cangaroo.receive(timeout=0.02) + + for msg in messages: + iface = cangaroo.interface_name(msg.interface_id) + data_hex = msg.get_data().hex(' ') + + if msg.extended: + id_str = f"0x{msg.id:08X}" + else: + id_str = f"0x{msg.id:03X}" + + flags = [] + if msg.fd: + flags.append("FD") + if msg.brs: + flags.append("BRS") + if msg.rtr: + flags.append("RTR") + flag_str = f" [{','.join(flags)}]" if flags else "" + + direction = "RX" if msg.is_rx else "TX" + + print(f"{msg.timestamp:.6f} {iface} {direction} {id_str} " + f"[{msg.dlc}]{flag_str} {data_hex}") + + # Send a random message every TX_INTERVAL + now = time.time() + if now - last_send >= TX_INTERVAL: + tx = cangaroo.Message() + tx.id = TX_ID + tx.set_data(os.urandom(TX_DLC)) + cangaroo.send(tx, interface_id=INTERFACE_ID) + print(f"TX 0x{tx.id:03X} [{tx.dlc}] {tx.get_data().hex(' ')}") + last_send = now diff --git a/examples/respond_0x10.py b/examples/respond_0x10.py new file mode 100644 index 00000000..8fe5c0ad --- /dev/null +++ b/examples/respond_0x10.py @@ -0,0 +1,45 @@ +""" +React to CAN message 0x10 and send a response 0x321. + +- Byte 0 of 0x321 is copied from Byte 0 of the received 0x10 message. +- Byte 1 of 0x321 contains an incrementing counter (wraps at 255). + +Usage: Paste into the Script window and click Run while a measurement is active. +Adjust INTERFACE_ID to match your setup (see cangaroo.interfaces()). +""" +import cangaroo + +TRIGGER_ID = 0x010 +RESPONSE_ID = 0x321 +INTERFACE_ID = 513 # change to match your setup + +for iface in cangaroo.interfaces(): + print(f"Interface {iface['id']}: {iface['name']}") + +counter = 0 + +def on_message(msg): + global counter + + if msg.id != TRIGGER_ID: + return + + data = msg.get_data() + byte0 = data[0] if len(data) > 0 else 0x00 + + resp = cangaroo.Message() + resp.id = RESPONSE_ID + resp.set_data(bytes([byte0, counter & 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + cangaroo.send(resp, interface_id=INTERFACE_ID) + + print(f"RX 0x{msg.id:03X} [{msg.dlc}] {data.hex(' ')}") + print(f"TX 0x{resp.id:03X} [{resp.dlc}] byte0=0x{byte0:02X} counter={counter}") + + counter = (counter + 1) & 0xFF + + +print(f"Listening for 0x{TRIGGER_ID:03X}, responding with 0x{RESPONSE_ID:03X}...\n") + +while True: + for msg in cangaroo.receive(timeout=1.0): + on_message(msg) diff --git a/examples/send_messages.py b/examples/send_messages.py new file mode 100644 index 00000000..797a9094 --- /dev/null +++ b/examples/send_messages.py @@ -0,0 +1,50 @@ +""" +Send CAN messages on a given interface. + +Usage: Paste into the Script window and click Run while a measurement is active. +Adjust interface_id to match your setup (see cangaroo.interfaces()). +""" +import cangaroo +import time + +# Print available interfaces +for iface in cangaroo.interfaces(): + print(f"Interface {iface['id']}: {iface['name']}") + +# --- Send a single standard CAN message --- +msg = cangaroo.Message() +msg.id = 0x123 +msg.set_data(bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])) +cangaroo.send(msg, interface_id=0) +print(f"Sent: ID=0x{msg.id:03X} DLC={msg.dlc} data={msg.get_data().hex(' ')}") + +# --- Send an extended frame --- +msg2 = cangaroo.Message() +msg2.id = 0x18FEF100 +msg2.extended = True +msg2.set_data(bytes([0xAA, 0xBB, 0xCC, 0xDD])) +cangaroo.send(msg2, interface_id=0) +print(f"Sent: ID=0x{msg2.id:08X} (ext) DLC={msg2.dlc} data={msg2.get_data().hex(' ')}") + +# --- Send an RTR frame --- +msg3 = cangaroo.Message() +msg3.id = 0x200 +msg3.rtr = True +msg3.dlc = 8 +cangaroo.send(msg3, interface_id=0) +print(f"Sent: ID=0x{msg3.id:03X} RTR DLC={msg3.dlc}") + +# --- Send periodic messages --- +print("\nSending 0x100 every 100ms (10 times)...") +periodic = cangaroo.Message() +periodic.id = 0x100 +counter = 0 + +for i in range(10): + periodic.set_data(bytes([counter & 0xFF, (counter >> 8) & 0xFF, 0, 0, 0, 0, 0, 0])) + cangaroo.send(periodic, interface_id=0) + print(f" [{i+1}/10] counter={counter}") + counter += 1 + time.sleep(0.1) + +print("Done.") diff --git a/examples/test.py b/examples/test.py new file mode 100644 index 00000000..30faf536 --- /dev/null +++ b/examples/test.py @@ -0,0 +1,59 @@ +""" +Send 0x10 with a sine-wave on Byte 0 every 100 ms, and respond to every +received 0x10 with 0x321 (Byte 0 echoed, Byte 1 = counter). + +TX interface (0x10): interface_id = 256 +RX/TX interface (0x321): interface_id = 513 +""" +import cangaroo +import time +import math +import threading + +TX_ID = 0x010 +RESPONSE_ID = 0x321 +TX_IFACE = 0 +RX_IFACE = 512 + +for iface in cangaroo.interfaces(): + print(f"Interface {iface['id']}: {iface['name']}") + +# --- sine-wave sender thread --- +samples = 256 +sine_values = [int((math.sin(2 * math.pi * i / samples) + 1) * 127.5) for i in range(samples)] + +def sender(): + msg = cangaroo.Message() + msg.id = TX_ID + i = 0 + while True: + msg.set_data(bytes([sine_values[i % samples]])) + cangaroo.send(msg, interface_id=TX_IFACE) + print(f"TX 0x{TX_ID:03X} byte0={sine_values[i % samples]}") + i += 1 + time.sleep(0.1) + +threading.Thread(target=sender, daemon=True).start() + +# --- response loop --- +rx_counter = 0 + +print(f"Listening for 0x{TX_ID:03X}, responding with 0x{RESPONSE_ID:03X}...\n") + +while True: + for msg in cangaroo.receive(timeout=1.0): + if msg.id != TX_ID or not msg.is_rx: + continue + + data = msg.get_data() + byte0 = data[0] if len(data) > 0 else 0x00 + + resp = cangaroo.Message() + resp.id = RESPONSE_ID + resp.set_data(bytes([byte0, rx_counter & 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + cangaroo.send(resp, interface_id=RX_IFACE) + + print(f"RX 0x{msg.id:03X} [{msg.dlc}] {data.hex(' ')}") + print(f"TX 0x{resp.id:03X} [{resp.dlc}] byte0=0x{byte0:02X} counter={rx_counter}") + + rx_counter = (rx_counter + 1) & 0xFF diff --git a/img/generator_view.gif b/img/generator_view.gif new file mode 100644 index 00000000..ca67d73f Binary files /dev/null and b/img/generator_view.gif differ diff --git a/img/graph_view.gif b/img/graph_view.gif new file mode 100644 index 00000000..ce385213 Binary files /dev/null and b/img/graph_view.gif differ diff --git a/img/j1939.gif b/img/j1939.gif new file mode 100644 index 00000000..e9d0e951 Binary files /dev/null and b/img/j1939.gif differ diff --git a/img/nested_remote_can_monitoring.png b/img/nested_remote_can_monitoring.png new file mode 100644 index 00000000..a2c46277 Binary files /dev/null and b/img/nested_remote_can_monitoring.png differ diff --git a/img/remote_can_monitoring.png b/img/remote_can_monitoring.png new file mode 100644 index 00000000..e64120aa Binary files /dev/null and b/img/remote_can_monitoring.png differ diff --git a/img/trace_view.gif b/img/trace_view.gif new file mode 100644 index 00000000..f6d591a0 Binary files /dev/null and b/img/trace_view.gif differ diff --git a/img/uds_iso_tp copy.gif b/img/uds_iso_tp copy.gif new file mode 100644 index 00000000..718e7135 Binary files /dev/null and b/img/uds_iso_tp copy.gif differ diff --git a/img/uds_iso_tp.gif b/img/uds_iso_tp.gif new file mode 100644 index 00000000..718e7135 Binary files /dev/null and b/img/uds_iso_tp.gif differ diff --git a/install_linux.sh b/install_linux.sh new file mode 100755 index 00000000..04214d69 --- /dev/null +++ b/install_linux.sh @@ -0,0 +1,163 @@ +#!/bin/bash + +# CANgaroo Dependency Installer for Linux +# Supports Ubuntu/Debian, Fedora, and Arch Linux + +set -e + +# Setup colors +BLUE='\033[0;34m' +CYAN='\033[0;36m' +GREEN='\033[0;32m' +RED='\033[0;31m' +ORANGE='\033[38;5;208m' +NC='\033[0m' # No Color + +# Print ASCII Art +echo -e "${ORANGE}" +echo " ______ ___ _ __ " +echo " / ____// | / | / /____ _ ____ _ _____ ____ ____ " +echo " / / / /| | / |/ // __ \`// __ \`// ___// __ \ / __ \ " +echo "/ /___ / ___ |/ /| // /_/ // /_/ // / / /_/ // /_/ / " +echo "\____//_/ |_/_/ |_/ \__, / \__,_/_/ \____/ \____/ " +echo " /____/ " +echo -e "${NC}" +echo -e "${BLUE}Open-source CAN bus analyzer setup tool${NC}" +echo "-------------------------------------------------------" + +# Detect Distribution +if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$ID +else + echo -e "${RED}Error: Unsupported Linux distribution.${NC}" + exit 1 +fi + +echo -e "Detected OS: ${GREEN}$OS${NC}" + +install_deps() { + case $OS in + ubuntu|debian|linuxmint) + echo "Installing dependencies for $OS (using apt)..." + sudo apt update + sudo apt install -y \ + build-essential \ + qt6-base-dev \ + libqt6charts6-dev \ + libqt6serialport6-dev \ + libnl-3-dev \ + libnl-route-3-dev \ + libgl1-mesa-dev \ + pkg-config \ + git \ + python3-dev \ + pybind11-dev \ + python3-pybind11 + ;; + fedora) + echo "Installing dependencies for Fedora (using dnf)..." + sudo dnf install -y \ + gcc-c++ \ + make \ + qt6-qtbase-devel \ + qt6-qtcharts-devel \ + qt6-qtserialport-devel \ + libnl3-devel \ + mesa-libGL-devel \ + pkgconf-pkg-config \ + git \ + python3-devel \ + pybind11-devel + ;; + arch) + echo "Installing dependencies for Arch Linux (using pacman)..." + sudo pacman -S --needed --noconfirm \ + base-devel \ + qt6-base \ + qt6-charts \ + qt6-serialport \ + libnl \ + mesa \ + pkgconf \ + git \ + python \ + pybind11 + ;; + *) + echo -e "${RED}Error: Distribution $OS is not explicitly supported by this script.${NC}" + echo "Please install Qt6 (base, charts, serialport) and libnl3 manually." + exit 1 + ;; + esac +} + +build_cangaroo() { + echo -e "${BLUE}Building CANgaroo...${NC}" + cd src + # Try to find qmake6, fallback to qmake if not found or if it's already qt6 + QMAKE_CMD=$(command -v qmake6 || command -v qmake) + + if [ -z "$QMAKE_CMD" ]; then + echo -e "${RED}Error: qmake not found. Please ensure Qt6 is installed correctly.${NC}" + exit 1 + fi + + # Set PKG_CONFIG_PATH for ubuntu/debian if needed + if [[ "$OS" == "ubuntu" || "$OS" == "debian" || "$OS" == "linuxmint" ]]; then + export PKG_CONFIG_PATH=/usr/lib/$(uname -m)-linux-gnu/pkgconfig:$PKG_CONFIG_PATH + fi + + $QMAKE_CMD + make -j$(nproc) + cd .. +} + +install_to_bin() { + echo -e "${BLUE}Installing CANgaroo to /usr/local/bin...${NC}" + if [ -f "bin/cangaroo" ]; then + sudo cp bin/cangaroo /usr/local/bin/ + echo -e "${GREEN}Cangaroo installed to /usr/local/bin/cangaroo${NC}" + else + echo -e "${RED}Error: Binary not found at bin/cangaroo. Did the build fail?${NC}" + exit 1 + fi +} + +echo "Select an option:" +echo "1) Install only dependencies" +echo "2) Install dependencies and build Cangaroo" +echo "3) Install dependencies, build Cangaroo, and install to /usr/local/bin" +echo "4) Build Cangaroo only" +read -p "Enter choice [1-4]: " choice + +case $choice in + 1) + install_deps + ;; + 2) + install_deps + build_cangaroo + ;; + 3) + install_deps + build_cangaroo + install_to_bin + ;; + 4) + build_cangaroo + ;; + *) + echo -e "${RED}Invalid choice. Exiting.${NC}" + exit 1 + ;; +esac + +echo "-------------------------------------------------------" +echo -e "${GREEN}Setup completed successfully!${NC}" +if [[ "$choice" -eq 2 || "$choice" -eq 4 ]]; then + echo "You can run CANgaroo from: ./bin/cangaroo" +elif [[ "$choice" -eq 3 ]]; then + echo "You can now run CANgaroo by simply typing 'cangaroo' in your terminal." +fi +echo "-------------------------------------------------------" diff --git a/setup_release.sh b/setup_release.sh new file mode 100755 index 00000000..45860d3b --- /dev/null +++ b/setup_release.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# CANgaroo Release Setup Tool +# Use this script to install dependencies for the pre-compiled CANgaroo binary. + +set -e + +# Setup colors +BLUE='\033[0;34m' +CYAN='\033[0;36m' +GREEN='\033[0;32m' +RED='\033[0;31m' +ORANGE='\033[38;5;208m' +NC='\033[0m' # No Color + +# Print ASCII Art +echo -e "${ORANGE}" +echo " ______ ___ _ __ " +echo " / ____// | / | / /____ _ ____ _ _____ ____ ____ " +echo " / / / /| | / |/ // __ \`// __ \`// ___// __ \ / __ \ " +echo "/ /___ / ___ |/ /| // /_/ // /_/ // / / /_/ // /_/ / " +echo "\____//_/ |_/_/ |_/ \__, / \__,_|_| \___/ \___/ \___/ " +echo " /____/ " +echo -e "${NC}" +echo -e "${BLUE}CANgaroo Release Setup Tool${NC}" +echo "-------------------------------------------------------" + +# Detect Distribution +if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$ID +else + echo -e "${RED}Error: Unsupported Linux distribution.${NC}" + exit 1 +fi + +echo -e "Detected OS: ${GREEN}$OS${NC}" + +install_deps() { + case $OS in + ubuntu|debian|linuxmint) + echo "Installing dependencies for $OS (using apt)..." + sudo apt update + sudo apt install -y \ + qt6-base-dev \ + libqt6charts6-dev \ + libqt6serialport6-dev \ + libnl-3-dev \ + libnl-route-3-dev \ + libgl1-mesa-dev + ;; + fedora) + echo "Installing dependencies for Fedora (using dnf)..." + sudo dnf install -y \ + qt6-qtbase-devel \ + qt6-qtcharts-devel \ + qt6-qtserialport-devel \ + libnl3-devel \ + mesa-libGL-devel + ;; + arch) + echo "Installing dependencies for Arch Linux (using pacman)..." + sudo pacman -S --needed --noconfirm \ + qt6-base \ + qt6-charts \ + qt6-serialport \ + libnl \ + mesa + ;; + *) + echo -e "${RED}Error: Distribution $OS is not explicitly supported by this script.${NC}" + echo "Please install Qt6 (base, charts, serialport) and libnl3 manually." + exit 1 + ;; + esac +} + +install_to_bin() { + echo -e "${BLUE}Installing CANgaroo to /usr/local/bin...${NC}" + if [ -f "cangaroo" ]; then + sudo cp cangaroo /usr/local/bin/ + echo -e "${GREEN}Cangaroo installed to /usr/local/bin/cangaroo${NC}" + else + echo -e "${RED}Error: Binary 'cangaroo' not found in current directory.${NC}" + exit 1 + fi +} + +echo "Select an option:" +echo "1) Install only dependencies" +echo "2) Install dependencies and move CANgaroo to /usr/local/bin" +read -p "Enter choice [1-2]: " choice + +case $choice in + 1) + install_deps + ;; + 2) + install_deps + install_to_bin + ;; + *) + echo -e "${RED}Invalid choice. Exiting.${NC}" + exit 1 + ;; +esac + +echo "-------------------------------------------------------" +echo -e "${GREEN}Setup completed successfully!${NC}" +if [[ "$choice" -eq 1 ]]; then + echo "You can run CANgaroo from the current directory: ./cangaroo" +elif [[ "$choice" -eq 2 ]]; then + echo "You can now run CANgaroo by simply typing 'cangaroo' in your terminal." +fi +echo "-------------------------------------------------------" diff --git a/src/assets/auto-scroll.svg b/src/assets/auto-scroll.svg new file mode 100644 index 00000000..d97d50ce --- /dev/null +++ b/src/assets/auto-scroll.svg @@ -0,0 +1 @@ +auto-scroll \ No newline at end of file diff --git a/src/assets/dark_theme.qss b/src/assets/dark_theme.qss new file mode 100644 index 00000000..a46c5e42 --- /dev/null +++ b/src/assets/dark_theme.qss @@ -0,0 +1,156 @@ +/* Dark Theme QSS */ + +QMainWindow, QDialog, QDockWidget { + background-color: #2d2d30; + color: #dcdcdc; +} + +QMainWindow::separator { + background: #3e3e42; + width: 6px; + height: 6px; +} +QMainWindow::separator:hover { + background: #007acc; +} + +QSplitter::handle { + background: #3e3e42; +} +QSplitter::handle:hover { + background: #007acc; +} + +QMenuBar { + background-color: #2d2d30; + color: #dcdcdc; +} +QMenuBar::item:selected { + background: #3e3e42; +} + +QMenu { + background-color: #1b1b1c; + color: #f1f1f1; + border: 1px solid #434346; +} +QMenu::item:selected { + background-color: #333334; +} + +QToolBar { + background-color: #2d2d30; + border: 1px solid #3e3e42; + spacing: 3px; +} + +QTreeView, QListView, QTableView { + background-color: #1e1e1e; + color: #dcdcdc; + alternate-background-color: #252526; + gridline-color: #333337; + selection-background-color: #264f78; + selection-color: #ffffff; + border: 1px solid #3f3f46; +} + +QHeaderView::section { + background-color: #252526; + color: #dcdcdc; + padding: 4px; + border: 1px solid #333337; +} + +QPushButton { + background-color: #3e3e42; + color: #dcdcdc; + border: 1px solid #555555; + padding: 4px 8px; + border-radius: 2px; +} +QPushButton:hover { + background-color: #4e4e52; +} +QPushButton:pressed { + background-color: #007acc; +} +QPushButton:disabled { + color: #656565; + background-color: #2d2d30; +} + +QPushButton#btnStartMeasurement { + background-color: #2d813d; + color: white; + border-radius: 12px; + padding: 5px 15px; + font-weight: bold; + border: none; +} +QPushButton#btnStartMeasurement:disabled { + background-color: #1d4d28; +} + +QPushButton#btnStopMeasurement { + background-color: #a32b2b; + color: white; + border-radius: 12px; + padding: 5px 15px; + font-weight: bold; + border: none; +} +QPushButton#btnStopMeasurement:disabled { + background-color: #611e1e; +} + +QLineEdit, QComboBox, QSpinBox, QTextEdit { + background-color: #333337; + color: #f1f1f1; + border: 1px solid #434346; + padding: 2px; +} + +QToolTip { + color: #ffffff; + background-color: #4b4b4d; + border: 1px solid #767676; +} + +QScrollBar:vertical { + border: none; + background: #2d2d30; + width: 12px; + margin: 0px 0px 0px 0px; +} +QScrollBar::handle:vertical { + background: #3e3e42; + min-height: 20px; +} +QScrollBar::handle:vertical:hover { + background: #505050; +} +QScrollBar:horizontal { + border: none; + background: #2d2d30; + height: 12px; + margin: 0px 0px 0px 0px; +} +QScrollBar::handle:horizontal { + background: #3e3e42; + min-width: 20px; +} + +QTabWidget::pane { + border: 1px solid #3f3f46; +} +QTabBar::tab { + background: #2d2d30; + color: #dcdcdc; + padding: 6px 12px; + border: 1px solid #3f3f46; + border-bottom: none; +} +QTabBar::tab:selected { + background: #1e1e1e; + border-bottom: 2px solid #007acc; +} diff --git a/src/assets/graph.svg b/src/assets/graph.svg new file mode 100644 index 00000000..eb6233f3 --- /dev/null +++ b/src/assets/graph.svg @@ -0,0 +1 @@ + diff --git a/src/assets/light_theme.qss b/src/assets/light_theme.qss new file mode 100644 index 00000000..b809922d --- /dev/null +++ b/src/assets/light_theme.qss @@ -0,0 +1,46 @@ +/* Light Theme QSS */ + +QMainWindow::separator { + background: transparent; + width: 6px; + height: 6px; +} +QMainWindow::separator:hover { + background: #0078d7; +} +QSplitter::handle { + background: transparent; + width: 6px; + height: 6px; +} +QSplitter::handle:hover { + background: #0078d7; +} + +QPushButton#btnStartMeasurement { + background-color: #28a745; + color: white; + border-radius: 12px; + padding: 5px 15px; + font-weight: bold; +} +QPushButton#btnStartMeasurement:disabled { + background-color: #94d3a2; +} + +QPushButton#btnStopMeasurement { + background-color: #dc3545; + color: white; + border-radius: 12px; + padding: 5px 15px; + font-weight: bold; +} +QPushButton#btnStopMeasurement:disabled { + background-color: #f1aeb5; +} + +QToolTip { + color: black; + background-color: white; + border: 1px solid gray; +} diff --git a/src/cangaroo.ico b/src/cangaroo.ico new file mode 100644 index 00000000..0a380c01 Binary files /dev/null and b/src/cangaroo.ico differ diff --git a/src/cangaroo.qrc b/src/cangaroo.qrc index e1d055c7..24696d63 100644 --- a/src/cangaroo.qrc +++ b/src/cangaroo.qrc @@ -3,5 +3,12 @@ assets/mdibg.png assets/cangaroo.png assets/cangaroo.svg + translations/cangaroo_de_DE.qm + translations/i18n_en_us.qm + translations/i18n_zh_cn.qm + assets/auto-scroll.svg + assets/graph.svg + assets/light_theme.qss + assets/dark_theme.qss diff --git a/src/core/Backend.cpp b/src/core/Backend.cpp index e597d344..2f4f1b11 100644 --- a/src/core/Backend.cpp +++ b/src/core/Backend.cpp @@ -23,6 +23,7 @@ #include "LogModel.h" #include +#include #include #include @@ -44,8 +45,10 @@ Backend::Backend() _logModel = new LogModel(*this); setDefaultSetup(); - _trace = new CanTrace(*this, this, 100); + _trace = new CanTrace(*this, this, 50); + _conditionalLoggingManager = new ConditionalLoggingManager(*this, this); + connect(_trace, SIGNAL(messageEnqueued(int)), this, SLOT(onMessageEnqueued(int))); connect(&_setup, SIGNAL(onSetupChanged()), this, SIGNAL(onSetupChanged())); } @@ -70,7 +73,7 @@ void Backend::addCanDriver(CanDriver &driver) bool Backend::startMeasurement() { - log_info("Starting measurement"); + log_info(tr("Starting measurement")); _measurementStartTime = QDateTime::currentMSecsSinceEpoch(); _timerSinceStart.start(); @@ -84,9 +87,7 @@ bool Backend::startMeasurement() if (intf) { intf->applyConfig(*mi); - log_info(QString("Listening on interface: %1").arg(intf->getName())); - intf->open(); - + log_info(QString(tr("Listening on interface: %1")).arg(intf->getName())); CanListener *listener = new CanListener(0, *this, *intf); listener->startThread(); _listeners.append(listener); @@ -107,15 +108,14 @@ bool Backend::stopMeasurement() } foreach (CanListener *listener, _listeners) { + log_info(QString(tr("Closing interface: %1")).arg(getInterfaceName(listener->getInterfaceId()))); listener->waitFinish(); - log_info(QString("Closing interface: %1").arg(getInterfaceName(listener->getInterfaceId()))); - listener->getInterface().close(); } qDeleteAll(_listeners); _listeners.clear(); - log_info("Measurement stopped"); + log_info(tr("Measurement stopped")); _measurementRunning = false; @@ -138,11 +138,12 @@ void Backend::loadDefaultSetup(MeasurementSetup &setup) driver->update(); foreach (CanInterfaceId intf, driver->getInterfaceIds()) { MeasurementNetwork *network = setup.createNetwork(); - network->setName(QString().sprintf("Network %d", i++)); + network->setName(tr("Network ") + QString("%1").arg(i++)); MeasurementInterface *mi = new MeasurementInterface(); mi->setCanInterface(intf); mi->setBitrate(500000); + mi->setFdBitrate(2000000); network->addInterface(mi); } } @@ -151,6 +152,7 @@ void Backend::loadDefaultSetup(MeasurementSetup &setup) void Backend::setDefaultSetup() { loadDefaultSetup(_setup); + emit onSetupChanged(); } MeasurementSetup &Backend::getSetup() @@ -161,6 +163,7 @@ MeasurementSetup &Backend::getSetup() void Backend::setSetup(MeasurementSetup &new_setup) { _setup.cloneFrom(new_setup); + emit onSetupChanged(); } double Backend::currentTimeStamp() const @@ -176,6 +179,7 @@ CanTrace *Backend::getTrace() void Backend::clearTrace() { _trace->clear(); + emit onClearTraceRequested(); } CanDbMessage *Backend::findDbMessage(const CanMessage &msg) const @@ -247,13 +251,26 @@ CanInterface *Backend::getInterfaceByDriverAndName(QString driverName, QString d } -pCanDb Backend::loadDbc(QString filename) +pCanDb Backend::loadDbc(QString filename, QString *errorMsg) { DbcParser parser; + QFileInfo info(filename); + if (!info.exists() || !info.isReadable()) { + if (errorMsg) { + *errorMsg = tr("File not found or not readable."); + } + return pCanDb(); + } + QFile *dbc = new QFile(filename); + pCanDb candb(new CanDb()); - parser.parseFile(dbc, *candb); + if (!parser.parseFile(dbc, *candb)) { + if (errorMsg) { + *errorMsg = tr("Failed to parse DBC file. Please check the log for details."); + } + } delete dbc; return candb; @@ -293,3 +310,10 @@ void Backend::logMessage(const QDateTime dt, const log_level_t level, const QStr { emit onLogMessage(dt, level, msg); } + +void Backend::onMessageEnqueued(int idx) +{ + if (_conditionalLoggingManager->isEnabled()) { + _conditionalLoggingManager->processMessage(_trace->getMessage(idx)); + } +} diff --git a/src/core/Backend.h b/src/core/Backend.h index 9e2a40e1..70f02bd4 100644 --- a/src/core/Backend.h +++ b/src/core/Backend.h @@ -31,6 +31,7 @@ #include #include #include +#include class MeasurementNetwork; class CanTrace; @@ -71,6 +72,8 @@ class Backend : public QObject CanTrace *getTrace(); void clearTrace(); + ConditionalLoggingManager *getConditionalLoggingManager() const { return _conditionalLoggingManager; } + CanDbMessage *findDbMessage(const CanMessage &msg) const; CanInterfaceIdList getInterfaceList(); @@ -82,7 +85,7 @@ class Backend : public QObject CanDriver *getDriverByName(QString driverName); CanInterface *getInterfaceByDriverAndName(QString driverName, QString deviceName); - pCanDb loadDbc(QString filename); + pCanDb loadDbc(QString filename, QString *errorMsg = 0); void clearLog(); LogModel &getLogModel() const; @@ -93,11 +96,14 @@ class Backend : public QObject void onSetupChanged(); + void onClearTraceRequested(); + void onLogMessage(const QDateTime dt, const log_level_t level, const QString msg); void onSetupDialogCreated(SetupDialog &dlg); public slots: + void onMessageEnqueued(int idx); private: static Backend *_instance; @@ -111,4 +117,5 @@ public slots: QList _listeners; LogModel *_logModel; + ConditionalLoggingManager *_conditionalLoggingManager; }; diff --git a/src/core/CanDb.cpp b/src/core/CanDb.cpp index b8dd89ac..af40306b 100644 --- a/src/core/CanDb.cpp +++ b/src/core/CanDb.cpp @@ -54,6 +54,11 @@ CanDbNode *CanDb::getOrCreateNode(QString node_name) } } +size_t CanDb::getNumberOfMessages() +{ + return _messages.size(); +} + CanDbMessage *CanDb::getMessageById(uint32_t raw_id) { if (_messages.contains(raw_id)) { @@ -63,6 +68,11 @@ CanDbMessage *CanDb::getMessageById(uint32_t raw_id) } } +CanDbMessageList CanDb::getMessageList() +{ + return _messages; +} + void CanDb::addMessage(CanDbMessage *msg) { _messages[msg->getRaw_id()] = msg; @@ -87,3 +97,47 @@ bool CanDb::saveXML(Backend &backend, QDomDocument &xml, QDomElement &root) return true; } +void CanDb::updateFrom(CanDb *other) +{ + this->setVersion(other->getVersion()); + this->setComment(other->getComment()); + + for (CanDbMessage *otherMsg : other->getMessageList()) { + CanDbMessage *myMsg = this->getMessageById(otherMsg->getRaw_id()); + if (!myMsg) { + myMsg = new CanDbMessage(this); + myMsg->setName(otherMsg->getName()); + myMsg->setRaw_id(otherMsg->getRaw_id()); + myMsg->setDlc(otherMsg->getDlc()); + myMsg->setComment(otherMsg->getComment()); + this->addMessage(myMsg); + } else { + myMsg->setName(otherMsg->getName()); + myMsg->setDlc(otherMsg->getDlc()); + myMsg->setComment(otherMsg->getComment()); + } + + for (CanDbSignal *otherSig : otherMsg->getSignals()) { + CanDbSignal *mySig = myMsg->getSignalByName(otherSig->name()); + if (!mySig) { + mySig = new CanDbSignal(myMsg); + mySig->setName(otherSig->name()); + myMsg->addSignal(mySig); + } + mySig->setStartBit(otherSig->startBit()); + mySig->setLength(otherSig->length()); + mySig->setFactor(otherSig->getFactor()); + mySig->setOffset(otherSig->getOffset()); + mySig->setMinimumValue(otherSig->getMinimumValue()); + mySig->setMaximumValue(otherSig->getMaximumValue()); + mySig->setUnit(otherSig->getUnit()); + mySig->setUnsigned(otherSig->isUnsigned()); + mySig->setIsBigEndian(otherSig->isBigEndian()); + mySig->setIsMuxer(otherSig->isMuxer()); + mySig->setIsMuxed(otherSig->isMuxed()); + mySig->setMuxValue(otherSig->getMuxValue()); + mySig->setComment(otherSig->comment()); + } + } +} + diff --git a/src/core/CanDb.h b/src/core/CanDb.h index 48175f18..0e5d29d6 100644 --- a/src/core/CanDb.h +++ b/src/core/CanDb.h @@ -55,7 +55,11 @@ class CanDb CanDbNode *getOrCreateNode(QString node_name); + size_t getNumberOfMessages(); + CanDbMessage *getMessageById(uint32_t raw_id); + CanDbMessageList getMessageList(); + void addMessage(CanDbMessage *msg); QString getComment() const; @@ -63,6 +67,8 @@ class CanDb bool saveXML(Backend &backend, QDomDocument &xml, QDomElement &root); + void updateFrom(CanDb *other); + private: QString _path; QString _version; diff --git a/src/core/CanDbSignal.cpp b/src/core/CanDbSignal.cpp index 2ae83a28..04f72871 100644 --- a/src/core/CanDbSignal.cpp +++ b/src/core/CanDbSignal.cpp @@ -75,7 +75,7 @@ void CanDbSignal::setComment(const QString &comment) _comment = comment; } -QString CanDbSignal::getValueName(const uint32_t value) const +QString CanDbSignal::getValueName(const uint64_t value) const { if (_valueTable.contains(value)) { return _valueTable[value]; @@ -84,25 +84,26 @@ QString CanDbSignal::getValueName(const uint32_t value) const } } -void CanDbSignal::setValueName(const uint32_t value, const QString &name) +void CanDbSignal::setValueName(const uint64_t value, const QString &name) { _valueTable[value] = name; } -double CanDbSignal::convertRawValueToPhysical(const uint32_t rawValue) +double CanDbSignal::convertRawValueToPhysical(const uint64_t rawValue) const { - int v; if (isUnsigned()) { - v = rawValue; + uint64_t v = rawValue; + return v * _factor + _offset; } else { // TODO check with DBC that actually contains signed values?! - v = (int32_t)(rawValue<<(32-_length)); - v>>=(32-_length); + int64_t v = (int64_t)(rawValue<<(64-_length)); + v>>=(64-_length); + return v * _factor + _offset; + } - return v * _factor + _offset; } -double CanDbSignal::extractPhysicalFromMessage(const CanMessage &msg) +double CanDbSignal::extractPhysicalFromMessage(const CanMessage &msg) const { return convertRawValueToPhysical(extractRawDataFromMessage(msg)); } @@ -206,9 +207,25 @@ void CanDbSignal::setMuxValue(const uint32_t &muxValue) _muxValue = muxValue; } -bool CanDbSignal::isPresentInMessage(const CanMessage &msg) +bool CanDbSignal::isPresentInMessage(const CanMessage &msg) const { - if ((_startBit + _length)>(8*msg.getLength())) { + if (msg.getRawId() != _parent->getRaw_id()) { + return false; + } + + uint32_t max_byte = 0; + if (!_isBigEndian) { + max_byte = (_startBit + _length - 1) / 8; + } else { + int bits_in_first_byte = (_startBit % 8) + 1; + if (_length <= bits_in_first_byte) { + max_byte = _startBit / 8; + } else { + max_byte = (_startBit / 8) + 1 + (_length - bits_in_first_byte - 1) / 8; + } + } + + if (max_byte >= msg.getLength()) { return false; } @@ -220,9 +237,41 @@ bool CanDbSignal::isPresentInMessage(const CanMessage &msg) return _muxValue == muxer->extractRawDataFromMessage(msg); } -uint32_t CanDbSignal::extractRawDataFromMessage(const CanMessage &msg) +uint64_t CanDbSignal::extractRawDataFromMessage(const CanMessage &msg) const { return msg.extractRawSignal(startBit(), length(), isBigEndian()); } +void CanDbSignal::injectRawDataToMessage(uint64_t rawValue, CanMessage &msg) +{ + msg.setRawSignal(startBit(), length(), isBigEndian(), rawValue); +} +void CanDbSignal::applyPhysicalToMessage(double physicalValue, CanMessage &msg) +{ + if (_factor == 0.0) { + return; + } + + const double rawD = (physicalValue - _offset) / _factor; + uint64_t rawValue = 0; + + if (isUnsigned()) { + const double rounded = rawD < 0.0 ? 0.0 : rawD + 0.5; + rawValue = static_cast(rounded); + if (_length < 64) { + const uint64_t maxRaw = (1ULL << _length) - 1; + if (rawValue > maxRaw) { + rawValue = maxRaw; + } + } + } else { + int64_t v = static_cast(rawD < 0.0 ? rawD - 0.5 : rawD + 0.5); + uint64_t mask = 0xFFFFFFFFFFFFFFFFULL; + if (_length < 64) { + mask = (1ULL << _length) - 1; + } + rawValue = static_cast(v) & mask; + } + injectRawDataToMessage(rawValue, msg); +} diff --git a/src/core/CanDbSignal.h b/src/core/CanDbSignal.h index debdd9f3..d4903d94 100644 --- a/src/core/CanDbSignal.h +++ b/src/core/CanDbSignal.h @@ -28,7 +28,7 @@ class CanDbMessage; -typedef QMap CanDbValueTable; +typedef QMap CanDbValueTable; class CanDbSignal { @@ -36,6 +36,8 @@ class CanDbSignal CanDbSignal(CanDbMessage *parent); QString name() const; void setName(const QString &name); + + CanDbMessage* getParentMessage() const { return _parent; } uint8_t startBit() const; void setStartBit(uint8_t startBit); @@ -46,8 +48,8 @@ class CanDbSignal QString comment() const; void setComment(const QString &comment); - QString getValueName(const uint32_t value) const; - void setValueName(const uint32_t value, const QString &name); + QString getValueName(const uint64_t value) const; + void setValueName(const uint64_t value, const QString &name); double getFactor() const; void setFactor(double factor); @@ -79,11 +81,13 @@ class CanDbSignal uint32_t getMuxValue() const; void setMuxValue(const uint32_t &muxValue); - bool isPresentInMessage(const CanMessage &msg); - uint32_t extractRawDataFromMessage(const CanMessage &msg); + bool isPresentInMessage(const CanMessage &msg) const; + uint64_t extractRawDataFromMessage(const CanMessage &msg) const; - double convertRawValueToPhysical(const uint32_t rawValue); - double extractPhysicalFromMessage(const CanMessage &msg); + double convertRawValueToPhysical(const uint64_t rawValue) const; + double extractPhysicalFromMessage(const CanMessage &msg) const; + void applyPhysicalToMessage(double physicalValue, CanMessage &msg); + void injectRawDataToMessage(uint64_t rawValue, CanMessage &msg); private: diff --git a/src/core/CanLogParser.cpp b/src/core/CanLogParser.cpp new file mode 100644 index 00000000..617b452f --- /dev/null +++ b/src/core/CanLogParser.cpp @@ -0,0 +1,228 @@ +/* + + Copyright (c) 2026 Jayachandran Dharuman + + This file is part of CANgaroo. + + cangaroo is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + cangaroo is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with cangaroo. If not, see . + +*/ + +#include "CanLogParser.h" +#include +#include +#include +#include +#include + +bool CanLogParser::parseCanDump(const QString& filename, QVector& outMessages) +{ + QFile file(filename); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return false; + + QTextStream in(&file); + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) + continue; + + bool ok = false; + CanMessage msg = parseCanDumpLine(line, ok); + if (ok) { + outMessages.append(msg); + } + } + + return true; +} + +CanMessage CanLogParser::parseCanDumpLine(const QString& line, bool& ok) +{ + // candump format: (timestamp) interface ID#DATA + // e.g. (1710842000.123456) vcan0 123#1122334455667788 + // e.g. (1710842000.123456) vcan0 00000123#1122334455667788 + + CanMessage msg; + ok = false; + + QRegularExpression re("^\\((?\\d+\\.\\d+)\\)\\s+(?\\S+)\\s+(?[0-9A-Fa-f]+)#(?[0-9A-Fa-f]*)$"); + QRegularExpressionMatch match = re.match(line); + + if (match.hasMatch()) { + double timestamp = match.captured("timestamp").toDouble(); + + // Split timestamp into seconds and microseconds + uint64_t seconds = static_cast(timestamp); + uint32_t micro_seconds = static_cast((timestamp - seconds) * 1000000.0); + msg.setTimestamp(seconds, micro_seconds); + + QString idStr = match.captured("id"); + bool idOk; + uint32_t id = idStr.toUInt(&idOk, 16); + if (!idOk) return msg; + + // In candump, standard vs extended is determined implicitly by interface or ID length length, + // but typically 3 chars vs 8 chars. We will set extended if ID > 0x7FF or id string length > 3 + if (id > 0x7FF || idStr.length() > 3) { + msg.setExtended(true); + msg.setId(id & 0x1FFFFFFF); + } else { + msg.setExtended(false); + msg.setId(id & 0x7FF); + } + + QString dataStr = match.captured("data"); + uint8_t dlc = dataStr.length() / 2; + msg.setLength(dlc); + for (int i = 0; i < dlc && i < 64; ++i) { + QString byteStr = dataStr.mid(i * 2, 2); + msg.setByte(i, byteStr.toUInt(nullptr, 16)); + } + + ok = true; + } else { + // Try readable date format with DLC: (YYYY-MM-DD HH:MM:SS.uuuuuu) interface ID [DLC] DATA... + QRegularExpression re2("^\\s*\\((?\\d{4}-\\d{2}-\\d{2})\\s+(?