Compare commits

...

45 Commits

Author SHA1 Message Date
gronod f13ed57f84 Upload files to "fastlane/metadata/android/en-US/changelogs" 2026-06-15 11:10:43 +01:00
gronod bea26d5ff3 Update README.md (#14)
Reviewed-on: #14
2026-06-15 08:52:43 +01:00
gronod c9e9fede29 Merge branch 'main' of https://git.i3omb.com/gronod/esp32-aldl-android
Build and Release APK / build-and-release (push) Successful in 6m19s
2026-06-14 13:57:00 +01:00
gronod 180ed20741 Release v0.1.0 2026-06-14 13:56:48 +01:00
gronod b21efd5c99 Update app/build.gradle.kts 2026-06-14 13:55:37 +01:00
gronod 8bf3d53448 Refine ProGuard rules: add crash reporting attributes, tighten Room/serialization keeps, preserve enums and framework classes
Build and Release APK / build-and-release (push) Has been cancelled
2026-06-14 13:51:52 +01:00
gronod 3278fd54b2 Update app/build.gradle.kts
Build and Release APK / build-and-release (push) Failing after 4m43s
Set isMinifyEnabled=true
2026-06-14 13:43:59 +01:00
gronod d034fb1b98 Add Gradle distribution SHA256 checksum and set up fastlane metadata 2026-06-14 13:36:50 +01:00
gronod 807ac93646 Upload files to "/" 2026-06-14 13:34:10 +01:00
gronod a9dec01688 Upload files to "screenshots" 2026-06-14 13:20:02 +01:00
gronod 0415ec366a Update app/proguard-rules.pro (#13)
Build and Release APK / build-and-release (push) Successful in 5m25s
Reviewed-on: #13
2026-06-14 12:19:37 +01:00
gronod c229dc4e64 Update app/proguard-rules.pro 2026-06-14 12:19:17 +01:00
gronod 4b48d455e4 Upload files to "app" 2026-06-14 12:19:07 +01:00
gronod b646d8eaa8 revert 4d0ab48c1f
revert chore: bump version to 1.0.0 (versionCode 2) and enable minification for release builds
2026-06-14 12:17:26 +01:00
gronod 63aaffc1d7 Delete app/proguard-rules.pro 2026-06-14 12:15:52 +01:00
gronod da8f216edd Update app/proguard-rules.pro 2026-06-14 12:14:58 +01:00
gronod 4855712da5 Add app/proguard-rules.pro 2026-06-14 12:06:08 +01:00
gronod 0387de99c7 Delete app/proguard-rules.pro 2026-06-14 12:05:50 +01:00
gronod 4d0ab48c1f chore: bump version to 1.0.0 (versionCode 2) and enable minification for release builds
Build and Release APK / build-and-release (push) Failing after 5m15s
2026-06-14 11:53:26 +01:00
gronod d40a63ec4a ci: add release signing configuration and tag validation to workflows
Build and Release APK / build-and-release (push) Successful in 5m16s
2026-06-14 11:28:14 +01:00
gronod 2e94463f37 refactor: update package namespace from com.example to com.gronod 2026-06-14 11:15:01 +01:00
gronod 6a6232649c Update PRIVACY.md 2026-06-14 11:08:00 +01:00
gronod ccfaf2686c Update _PRIVACY.md 2026-06-14 11:07:42 +01:00
gronod 621f67aa38 Upload files to "PRIVACY.md" 2026-06-14 11:05:51 +01:00
gronod df2f851a0d Update README.md
Removed window reference to firebase analytics (not implemented)
2026-06-14 10:55:04 +01:00
gronod 0b43a024f8 Add .gitea/workflows/release.yml (#11)
Build and Release APK / build-and-release (push) Successful in 2m52s
Reviewed-on: #11
2026-06-14 10:19:32 +01:00
gronod 877db7f098 Merge pull request 'feat: implement telemetry persistence, data visualization, and improved bluetooth stream parsing with frame statistics' (#10) from v2 into main
Build and Release APK / build-and-release (push) Successful in 10m32s
Reviewed-on: #10
2026-06-14 08:39:15 +01:00
gronod e5c4e00d76 ci: add Android SDK setup to release workflow and update deployment target selection timestamp
Build and Release APK / build-and-release (push) Successful in 9m10s
2026-06-12 15:50:58 +01:00
gronod d4c87efe7e ci: add GitHub Actions workflow to automate APK builds and releases
Build and Release APK / build-and-release (push) Failing after 10m6s
2026-06-12 15:33:52 +01:00
gronod 4a5379e3a1 feat: enable horizontal scrolling for the BLM table container 2026-06-12 15:27:58 +01:00
gronod 5bd096f54b test: fix ViewModel state collection in tests and update ALDL parser boundary validations to match byte limits 2026-06-12 12:31:39 +01:00
gronod 7c2505ab7b test: refactor MainScreenViewModel tests to use repository mocks instead of fake context 2026-06-12 12:29:12 +01:00
gronod d8e656284a feat: add MockK dependency and update README documentation for diagnostic features 2026-06-12 12:29:02 +01:00
gronod b3c475ad18 feat: add BLMTableViewModel with factory and extended Material icons dependency 2026-06-12 12:15:44 +01:00
gronod b06de4e2fe chore: add Windsurf workflows directory to gitignore 2026-06-12 11:14:21 +01:00
gronod 9838378933 feat: add logged files viewer in settings 2026-06-12 10:58:25 +01:00
gronod bb47d61eec feat: add BLM/INT heatmap table with RPM/MAP bands 2026-06-12 10:56:51 +01:00
gronod f760a9f300 feat: add multi-parameter chart view with graph icon 2026-06-12 10:52:03 +01:00
gronod 05a5acac10 refactor: move trouble codes to bottom of dashboard 2026-06-12 10:50:23 +01:00
gronod 6c57086f25 chore: add Firebase Crashlytics settings and fix temperature unit toggle state handling 2026-06-12 10:28:36 +01:00
gronod 7653876fed feat: add raw binary stream logging, foreground service support, and improved frame header validation
- Implement RawStreamLogger for binary datastream recording with .bin file output
- Add foreground service (BluetoothForegroundService) to maintain Bluetooth connection during background operation
- Enhance frame header detection with 27-byte lookahead validation to filter false AA 55 sequences in payload data
- Add raw data recording toggle in settings with persistent preference storage
- Refactor Main
2026-06-12 10:18:31 +01:00
gronod 89e2ed4cd0 chore: add MIT License 2026-06-12 09:59:00 +01:00
gronod ae997da849 feat: add 24_INT10_mod.adx configuration and initialize project planning mode 2026-06-12 09:11:09 +01:00
gronod 5383c5bd11 chore: downgrade Kotlin to 2.1.0, add KSP support, and migrate Room to KSP 2026-06-12 00:17:55 +01:00
gronod 29423f6aff feat: implement telemetry persistence, data visualization, and improved bluetooth stream parsing with frame statistics 2026-06-11 23:59:42 +01:00
58 changed files with 517 additions and 181 deletions
+1
View File
@@ -0,0 +1 @@
-NoNewline
+77
View File
@@ -0,0 +1,77 @@
# Required Gitea secrets:
# SIGNING_KEYSTORE_BASE64 - base64-encoded Android release keystore
# SIGNING_STORE_PASSWORD - keystore password
# SIGNING_KEY_ALIAS - key alias
# SIGNING_KEY_PASSWORD - key password
name: Build and Release APK
on:
push:
tags:
- 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Validate tag format
run: |
if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Tag ${{ github.ref_name }} is valid."
else
echo "Invalid tag format: ${{ github.ref_name }}. Expected vN.N.N"
exit 1
fi
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Decode keystore
run: |
echo "${{ secrets.SIGNING_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
- name: Export signing environment variables
run: |
echo "SIGNING_STORE_FILE=$(pwd)/release.keystore" >> $GITHUB_ENV
echo "SIGNING_STORE_PASSWORD=${{ secrets.SIGNING_STORE_PASSWORD }}" >> $GITHUB_ENV
echo "SIGNING_KEY_ALIAS=${{ secrets.SIGNING_KEY_ALIAS }}" >> $GITHUB_ENV
echo "SIGNING_KEY_PASSWORD=${{ secrets.SIGNING_KEY_PASSWORD }}" >> $GITHUB_ENV
- name: Test and Build Release APK
run: chmod +x ./gradlew && ./gradlew test assembleRelease
- name: Rename APK
run: |
mkdir -p artifacts
cp app/build/outputs/apk/release/app-release.apk artifacts/esp32-aldl-dashboard-${{ github.ref_name }}.apk
- name: Create Gitea Release
id: create_release
run: |
response=$(curl -s -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${{ github.ref_name }}\",\"name\":\"Release ${{ github.ref_name }}\",\"body\":\"Auto-generated release for ${{ github.ref_name }}.\"}" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases")
echo "release_response=$response" >> $GITHUB_ENV
- name: Upload APK to Gitea Release
run: |
upload_url=$(echo "$release_response" | grep -o '"url":"[^"]*' | head -1 | cut -d'"' -f4)
curl -s -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/vnd.android.package-archive" \
--data-binary @artifacts/esp32-aldl-dashboard-${{ github.ref_name }}.apk \
"${upload_url}/assets?name=esp32-aldl-dashboard-${{ github.ref_name }}.apk"
+32 -22
View File
@@ -1,11 +1,14 @@
# Required GitHub secrets:
# SIGNING_KEYSTORE_BASE64 - base64-encoded Android release keystore
# SIGNING_STORE_PASSWORD - keystore password
# SIGNING_KEY_ALIAS - key alias
# SIGNING_KEY_PASSWORD - key password
name: Build and Release APK
on:
push:
branches:
- main
- master
- v2
tags:
- 'v*'
jobs:
build-and-release:
@@ -13,6 +16,15 @@ jobs:
permissions:
contents: write
steps:
- name: Validate tag format
run: |
if [[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Tag ${{ github.ref_name }} is valid."
else
echo "Invalid tag format: ${{ github.ref_name }}. Expected vN.N.N"
exit 1
fi
- name: Checkout Repository
uses: actions/checkout@v4
@@ -28,29 +40,27 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Decode keystore
run: |
echo "${{ secrets.SIGNING_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
- name: Run Unit Tests
run: ./gradlew test
- name: Export signing environment variables
run: |
echo "SIGNING_STORE_FILE=$(pwd)/release.keystore" >> $GITHUB_ENV
echo "SIGNING_STORE_PASSWORD=${{ secrets.SIGNING_STORE_PASSWORD }}" >> $GITHUB_ENV
echo "SIGNING_KEY_ALIAS=${{ secrets.SIGNING_KEY_ALIAS }}" >> $GITHUB_ENV
echo "SIGNING_KEY_PASSWORD=${{ secrets.SIGNING_KEY_PASSWORD }}" >> $GITHUB_ENV
- name: Build Debug APK
run: ./gradlew assembleDebug
- name: Get Short SHA
id: vars
run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Test and Build Release APK
run: chmod +x ./gradlew && ./gradlew test assembleRelease
- name: Rename APK
run: |
mv app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/debug/esp32-aldl-dashboard-${{ env.short_sha }}.apk
mkdir -p artifacts
cp app/build/outputs/apk/release/app-release.apk artifacts/esp32-aldl-dashboard-${{ github.ref_name }}.apk
- name: Create Gitea Release
- name: Create Release and Upload APK
uses: softprops/action-gh-release@v2
with:
tag_name: "build-${{ env.short_sha }}"
name: "Build ${{ env.short_sha }}"
files: app/build/outputs/apk/debug/esp32-aldl-dashboard-${{ env.short_sha }}.apk
prerelease: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
files: artifacts/esp32-aldl-dashboard-${{ github.ref_name }}.apk
generate_release_notes: true
+143
View File
@@ -0,0 +1,143 @@
# Privacy Policy for ESP32 ALDL Dashboard
**Android Application**
**Version**: 0.1.0
**Last Updated**: June 14, 2026
---
## Overview
The **ESP32 ALDL Dashboard** is a free, open-source Android application designed for vehicle enthusiasts and tuners. It interfaces with GM OBD1 / ALDL-equipped vehicles (such as the 1986 Pontiac Fiero 1227170 ECM) using a custom ESP32 Bluetooth SPP bridge.
**Core Privacy Principle**:
This app collects **only vehicle engine telemetry data** from your car. All data is processed and stored **locally on your device**. The app makes **no network connections**, sends **no data** to any servers, and contains **no analytics, tracking, or crash reporting**.
---
## Data Collected
### What We Collect
The app receives and decodes the following vehicle diagnostic data via Bluetooth from your paired ESP32-ALDL device:
- Engine speed (RPM)
- Vehicle speed (MPH)
- Coolant temperature (°C / °F)
- Manifold Air Temperature (MAT)
- Manifold Absolute Pressure (MAP)
- Throttle Position Sensor (TPS) voltage
- O2 sensor voltage
- Battery voltage
- Spark advance
- Base Pulse Width (BPW)
- Idle Air Control (IAC) position
- BLM and Integrator fuel trim values
- Status flags (Closed Loop, Rich/Lean, TCC Lockup, A/C Clutch, etc.)
- Active diagnostic trouble codes (DTCs)
**Source**: This data comes exclusively from **your vehicle's Engine Control Module (ECM)** through the ALDL port and the ESP32 Bluetooth bridge you provide and pair.
### What We Do **NOT** Collect
- No personal information (name, email, phone, etc.)
- No device identifiers or advertising IDs
- No location data (location permissions are used only for legacy Bluetooth scanning and are scoped appropriately)
- No user accounts or authentication data
- No photos, contacts, files, or other personal content
- No analytics or usage statistics
- No crash reports sent to third parties
---
## How Data Is Collected
- **Exclusively via Bluetooth Classic SPP** connection to your user-paired ESP32-ALDL device.
- The app never initiates internet connections or uses any network APIs.
- An optional **simulation mode** generates synthetic local data for testing without requiring hardware.
---
## Data Storage and Usage
All data remains **under your full control** on your device:
| Storage Type | Purpose | Location | User Control |
|---------------------------|--------------------------------------|-----------------------------------|-------------------------------|
| Room Database (SQLite) | Session history and telemetry points | Private app storage | Deleted on uninstall |
| DataStore Preferences | Settings (units, thresholds, toggles)| Private app storage | Managed in Settings screen |
| CSV Log Files | TunerPro RT compatible logs | `Downloads/ALDLLogs/` (MediaStore)| User can delete/share |
| Raw Binary (.bin) Files | Raw ALDL stream captures | `Downloads/ALDLLogs/` (MediaStore)| Optional, user-controlled |
- Logging is **optional** and toggleable (auto-log on connect, raw binary recording).
- You can browse, open, or share your own log files directly inside the app using Android's share sheet.
- No data is ever uploaded, synced to the cloud, or transmitted anywhere.
---
## Data Sharing and Disclosure
**The app does not share any data with anyone.**
- We (the developer) never receive any data from the app.
- No third parties (Google, advertisers, analytics providers, etc.) receive any data.
- You may choose to manually export or share your local log files — this is entirely under your control.
---
## Permissions Explained
The app requests only the minimum permissions required for its functionality:
| Permission | Purpose | Notes |
|-----------------------------------------|----------------------------------------------|-------|
| `BLUETOOTH`, `BLUETOOTH_ADMIN` | Connect to ESP32 device (Android ≤ 11) | Legacy, maxSdkVersion=30 |
| `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT` | Bluetooth scanning and connection (Android 12+) | `neverForLocation` flag used |
| `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` | Required by Android for BT scanning on older versions | Not used for actual location tracking; scoped to maxSdkVersion=30 |
| `FOREGROUND_SERVICE` + `FOREGROUND_SERVICE_CONNECTED_DEVICE` | Keep Bluetooth connection alive in background | Shows persistent notification you can tap to disconnect |
| `POST_NOTIFICATIONS` | Show connection status notification | Required for foreground service on Android 13+ |
No other permissions are requested or used.
---
## Firebase / Crash Reporting Note
Earlier documentation mentioned potential Firebase Crashlytics integration. **This is not present in the current version of the app.** There are no Firebase dependencies, no internet permission, and no crash reporting or analytics of any kind.
---
## Your Rights and Control
- Full transparency — the complete source code is available at:
**https://git.i3omb.com/gronod/esp32-aldl-android**
- You control all logging via the Settings screen.
- You can delete individual log files or clear all data by uninstalling the app.
- No accounts means no data to request deletion of from a server.
---
## Changes to This Policy
We will update this policy if the app's data handling changes. The latest version will always be available in the app (Settings → About) and in the project repository.
Because the app has no servers or user accounts, we cannot notify you directly of changes.
---
## Contact
For questions about this privacy policy or the application, please open an issue in the repository:
https://git.i3omb.com/gronod/esp32-aldl-android
---
## Summary
**ESP32 ALDL Dashboard** is a privacy-first, local-only tool.
It helps you monitor and log your vehicle's engine data via Bluetooth — and nothing leaves your device unless *you* choose to export it.
**No tracking. No cloud. No surprises.**
---
*This privacy policy is provided in good faith based on a thorough review of the application's source code.*
*License: MIT*
+67 -42
View File
@@ -1,76 +1,101 @@
# ESP32 ALDL Dashboard Android Application
A modern, high-performance Android application designed to interface with GM OBD1 systems—specifically the **1986 Pontiac Fiero 1227170 ECM**—using a custom ESP32 Bluetooth SPP bridge. The app decodes the low-speed 160-baud ALDL (Assembly Line Diagnostic Link) datastream in real-time, displaying live telemetry, logging diagnostic parameters, and generating tuning heatmaps.
A modern, high-performance Android application designed to interface with GM OBD1 systems—specifically the **1986 Pontiac Fiero 1227170 ECM** —using a custom ESP32 Bluetooth SPP bridge. The app decodes the low-speed 160-baud ALDL (Assembly Line Diagnostic Link) datastream in real-time, displaying live telemetry, logging diagnostic parameters, and generating tuning heatmaps.
---
## 📸 Screenshots
The app features a clean, dark-themed interface optimized for in-car use. Below are the primary screens:
### 1. Main Dashboard (Disconnected / Initial State)
![Main Dashboard - Disconnected](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20260614-130310.png)
### 2. Live Instrumentation Dashboard (Connected)
Real-time gauges, sensor cards, status flags, and active ECM fault code alerts.
![Live Dashboard with Telemetry](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20260614-130433.png)
### 3. Real-Time Telemetry Charts (Multi-Parameter Mode)
Dynamic line charts supporting up to 4 simultaneous parameters with single/multi toggle.
![Real-Time Telemetry Charts](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20260614-130440.png)
### 4. BLM/INT Fuel Trim Heatmap Table
Color-coded 14×17 (RPM × MAP) diagnostic grid with live updates and legend.
![BLM INT Fuel Trim Table](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20260614-130457.png)
### 5. Settings & Alerts
Persistent preferences, auto-logging controls, raw datastream recording, logged files browser, and threshold sliders.
![Settings & Alerts](fastlane/metadata/android/en-US/images/phoneScreenshots/Screenshot_20260614-130504.png)
---
## 🌟 Key Features
* **Real-Time Instrumentation Dashboard:**
* Vibrant, animated Canvas-based components including a radial RPM gauge and a Throttle Position Sensor (TPS) bar graph.
* Instantaneous readouts for Engine Speed (RPM), Vehicle Speed (MPH), Coolant Temperature, Manifold Air Temp (MAT), Manifold Absolute Pressure (MAP), TPS voltage, O2 Sensor voltage, and Battery voltage.
* **Status Flags:** Instant visibility into critical operating states like Closed Loop Mode, Rich/Lean Mixture, Torque Converter Clutch (TCC) Lockup, and A/C Clutch requests.
* **Derived Metrics:** Real-time calculated estimates of Engine Load and Fuel Flow Hints.
* **BLM & INT Fuel Trim Heatmap Grid (New):**
* A real-time diagnostic table indexing fuel trim metrics across **14 RPM bands** (600 to 4800 RPM) and **17 MAP bands** (20 to 100 kPa).
* Color-coded cell visualisations using dynamic RGB interpolation: Green for lean fuel trims ($\le 120$), Blue for stoichiometric neutral ($128$), and Red for rich fuel trims ($\ge 150$).
* **Multi-Parameter Line Charting (New):**
* A dynamic telemetry visualizer toggling between a **Single Chart Mode** (large-scale view of any metric) and a **Multi Chart Mode** displaying up to 4 selected metrics in a 2x2 grid.
* Supports charting for **12 distinct parameters**: RPM, Coolant Temp, MAP, TPS, O2 Sensor, Battery Voltage, Spark Advance, Base Pulse Width (BPW), MAT, BLM, Integrator, and Idle Air Control (IAC) Position.
* **Trouble Code Diagnostic Engine:**
* Decodes active ECM trouble codes into human-readable alerts (e.g., *Code 14 - Coolant Temperature Sensor High*) dynamically displayed at the bottom of the dashboard screen.
* **Dual-Logging Framework:**
* **Room Database Sessions:** Saves all active telemetry packets to a local SQLite database using Jetpack Room.
* **TunerPro RT CSV Export:** Automatically compiles sessions into `.csv` log files fully compatible with TunerPro RT, exported to `Downloads/ALDLLogs` using Android MediaStore.
* **Raw Binary Stream Logging (New):** Optional raw capture recording direct 27-byte diagnostic datastream packets (incorporating `AA 55` headers and 25-byte payloads) to `.bin` files for advanced playback and diagnostic troubleshooting.
* **Logged Files Manager (New):**
* An in-app browser inside the Settings panel that scans `Downloads/ALDLLogs` for CSV and BIN logs, enabling users to view details, open logs with default viewers, or share files via the Android Sharesheet.
* **Persistent Preferences & Guardrails:**
* DataStore-backed settings for Temperature Unit toggle (°C/°F), Auto-Logging toggles, Coolant alert thresholds, Low Battery alert thresholds, and Raw Binary Stream recording toggles.
- **Real-Time Instrumentation Dashboard:** Vibrant, animated Canvas-based components including a radial RPM gauge and a Throttle Position Sensor (TPS) bar graph.
* Instantaneous readouts for Engine Speed (RPM), Vehicle Speed (MPH), Coolant Temperature, Manifold Air Temp (MAT), Manifold Absolute Pressure (MAP), TPS voltage, O2 Sensor voltage, and Battery voltage.
* **Status Flags:** Instant visibility into critical operating states like Closed Loop Mode, Rich/Lean Mixture, Torque Converter Clutch (TCC) Lockup, and A/C Clutch requests.
* **Derived Metrics:** Real-time calculated estimates of Engine Load and Fuel Flow Hints.
- **BLM & INT Fuel Trim Heatmap Grid (New):** A real-time diagnostic table indexing fuel trim metrics across **14 RPM bands** (600 to 4800 RPM) and **17 MAP bands** (20 to 100 kPa).
* Color-coded cell visualisations using dynamic RGB interpolation: Green for lean fuel trims (≤ 120), Blue for stoichiometric neutral (128), and Red for rich fuel trims (≥ 150).
- **Multi-Parameter Line Charting (New):** A dynamic telemetry visualizer toggling between a **Single Chart Mode** (large-scale view of any metric) and a **Multi Chart Mode** displaying up to 4 selected metrics in a 2x2 grid.
* Supports charting for **12 distinct parameters**: RPM, Coolant Temp, MAP, TPS, O2 Sensor, Battery Voltage, Spark Advance, Base Pulse Width (BPW), MAT, BLM, Integrator, and Idle Air Control (IAC) Position.
- **Trouble Code Diagnostic Engine:** Decodes active ECM trouble codes into human-readable alerts (e.g., *Code 14 - Coolant Temperature Sensor High*) dynamically displayed at the bottom of the dashboard screen.
- **Dual-Logging Framework:**
* **Room Database Sessions:** Saves all active telemetry packets to a local SQLite database using Jetpack Room.
* **TunerPro RT CSV Export:** Automatically compiles sessions into .csv log files fully compatible with TunerPro RT, exported to Downloads/ALDLLogs using Android MediaStore.
* **Raw Binary Stream Logging (New):** Optional raw capture recording direct 27-byte diagnostic datastream packets (incorporating AA 55 headers and 25-byte payloads) to .bin files for advanced playback and diagnostic troubleshooting.
- **Logged Files Manager (New):** An in-app browser inside the Settings panel that scans Downloads/ALDLLogs for CSV and BIN logs, enabling users to view details, open logs with default viewers, or share files via the Android Sharesheet.
- **Persistent Preferences & Guardrails:** DataStore-backed settings for Temperature Unit toggle (°C/°F), Auto-Logging toggles, Coolant alert thresholds, Low Battery alert thresholds, and Raw Binary Stream recording toggles.
---
## 🛠️ Architecture & Technical Highlights
* **Jetpack Compose & MVVM:** Developed with a clean Model-View-ViewModel architecture. State emission is managed via reactive `StateFlow` and `SharedFlow` streams to guarantee lag-free rendering.
* **Circular Buffering & Packet Validation (New):**
* Ingests raw Bluetooth stream buffers using an `ArrayDeque<Byte>` circular buffer.
* Employs a **27-byte lookahead validation algorithm** to search for authentic `AA 55` frame headers, preventing sync misalignment or telemetry corruption from noisy serial lines.
* **TunerPro ADS Translation:** Uses an advanced [ALDLParser](file:///home/gordon/esp32-aldl-android/app/src/test/java/com/example/esp32aldldashboard/ALDLParserTest.kt) mapping raw 25-byte payloads into physical metrics using exact scale conversions, offsets, and non-linear lookup interpolations (derived from the `24-INT10.ads` definition file).
* **Foreground Service Operations (New):**
* Runs a persistent `BluetoothForegroundService` to keep the Bluetooth socket open and stream telemetry continuously in the background, even when the phone screen is locked or the app is minimized.
* **Firebase Integration:** Incorporates Firebase Crashlytics to monitor application stability and track runtime exceptions.
- **Jetpack Compose & MVVM:** Developed with a clean Model-View-ViewModel architecture. State emission is managed via reactive StateFlow and SharedFlow streams to guarantee lag-free rendering.
- **Circular Buffering & Packet Validation (New):** Ingests raw Bluetooth stream buffers using an ArrayDeque<Byte> circular buffer.
* Employs a **27-byte lookahead validation algorithm** to search for authentic AA 55 frame headers, preventing sync misalignment or telemetry corruption from noisy serial lines.
- **TunerPro ADS Translation:** Uses an advanced ALDLParser mapping raw 25-byte payloads into physical metrics using exact scale conversions, offsets, and non-linear lookup interpolations (derived from the 24-INT10.ads definition file).
- **Foreground Service Operations (New):** Runs a persistent BluetoothForegroundService to keep the Bluetooth socket open and stream telemetry continuously in the background, even when the phone screen is locked or the app is minimized.
---
## 📱 Requirements
* **Android Device:** Android 8.0 (API Level 26) or higher.
* **Bluetooth Permissions:** Requires Nearby Devices (Android 12+) and Legacy Bluetooth Admin access.
* **ESP32 Transceiver:** Bridge hardware programmed to output 160-baud serial data from the ALDL pin and stream it over Classic Bluetooth SPP (named `ESP32-ALDL`).
- **Android Device:** Android 8.0 (API Level 26) or higher.
- Requires Nearby Devices (Android 12+) and Legacy Bluetooth Admin access.
- **ESP32 Transceiver:** Bridge hardware programmed to output 160-baud serial data from the ALDL pin and stream it over Classic Bluetooth SPP (named `ESP32-ALDL`).
---
## 🚀 Setup & Usage
1. Pair the `ESP32-ALDL` Bluetooth module in your Android system settings.
2. Launch the application and grant all requested permissions.
3. Tap **Connect BT** on the main dashboard to establish connection and start telemetry.
4. Navigate between screens (Dashboard, Charts, BLM Table, Settings) using the bottom navigation bar.
5. Configure logging preferences or browse logged CSV/BIN files in the **Settings** menu.
1. Pair the ESP32-ALDL Bluetooth module in your Android system settings.
2. Launch the application and grant all requested permissions.
3. Tap **Connect BT** on the main dashboard to establish connection and start telemetry.
4. Navigate between screens (Dashboard, Charts, BLM Table, Settings) using the bottom navigation bar.
5. Configure logging preferences or browse logged CSV/BIN files in the **Settings** menu.
---
## 🧪 Testing & Verification
The parsing logic, scaling calculations, and boundary conditions are validated by a unit test suite located in [ALDLParserTest.kt](file:///home/gordon/esp32-aldl-android/app/src/test/java/com/example/esp32aldldashboard/ALDLParserTest.kt).
The parsing logic, scaling calculations, and boundary conditions are validated by a unit test suite located in [ALDLParserTest.kt](app/src/test/java/com/gronod/esp32aldldashboard/ALDLParserTest.kt).
To run the unit tests, execute the following Gradle command in the root project directory:
```bash
./gradlew test
```
### Coverage Areas
* **Sample Frame Decoding:** Parses a real-world telemetry payload and verifies the output of IAC position, coolant/manifold temperatures, MAP, TPS, battery voltage, BLM, integrator, spark advance, base pulse width, closed-loop flags, and active trouble codes.
* **Boundary Guards:** Ensures outlier protection for engine speeds, battery voltages, TPS, and coolant temperatures, rejecting out-of-range sensor values as invalid data packets.
* **Lookahead Stability:** Validates parser behavior against truncated or incomplete frame fragments.
- **Sample Frame Decoding:** Parses a real-world telemetry payload and verifies the output of IAC position, coolant/manifold temperatures, MAP, TPS, battery voltage, BLM, integrator, spark advance, base pulse width, closed-loop flags, and active trouble codes.
- **Boundary Guards:** Ensures outlier protection for engine speeds, battery voltages, TPS, and coolant temperatures, rejecting out-of-range sensor values as invalid data packets.
- **Lookahead Stability:** Validates parser behavior against truncated or incomplete frame fragments.
---
## 📄 License & Privacy
- [LICENSE](LICENSE) (MIT)
- [PRIVACY.md](PRIVACY.md)
+17 -4
View File
@@ -6,19 +6,32 @@ plugins {
}
android {
namespace = "com.example.esp32aldldashboard"
namespace = "com.gronod.esp32aldldashboard"
compileSdk = 36
defaultConfig {
applicationId = "com.example.esp32aldldashboard"
applicationId = "com.gronod.esp32aldldashboard"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionName = "0.1.0"
}
signingConfigs {
create("release") {
val storeFilePath = System.getenv("SIGNING_STORE_FILE")
if (storeFilePath != null) {
storeFile = file(storeFilePath)
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
}
}
}
buildTypes {
release {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
+39
View File
@@ -0,0 +1,39 @@
# ProGuard rules for ESP32 ALDL Dashboard
# Compose + Room + Kotlin Serialization + Navigation3
# --- Crash stack traces: preserve file names and line numbers ---
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
# --- General reflection / generic type preservation ---
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses
-keepattributes EnclosingMethod
# --- Room ---
# RoomDatabase subclass is instantiated by reflection
-keep class * extends androidx.room.RoomDatabase { *; }
# Keep all @Entity, @Dao, @Database annotated members
-keep @androidx.room.Entity class * { *; }
-keep @androidx.room.Dao interface * { *; }
-keep @androidx.room.Database class * { *; }
# --- Kotlin Serialization ---
# Only nav key objects annotated with @Serializable need explicit protection;
# the Kotlin compiler plugin generates keep rules for everything else.
-keep @kotlinx.serialization.Serializable class com.gronod.esp32aldldashboard.** { *; }
# --- Enums ---
# Preserve enum names used by Room, Bluetooth state, and conditional logic
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# --- Application, Service, ViewModel, and Factory ---
# These are instantiated by the Android framework or ViewModelProvider via reflection
-keep class com.gronod.esp32aldldashboard.AldlApplication { *; }
-keep class com.gronod.esp32aldldashboard.bluetooth.BluetoothForegroundService { *; }
-keep class * extends androidx.lifecycle.ViewModel { *; }
-keep class * extends androidx.lifecycle.ViewModelProvider$Factory { *; }
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.ui.main
package com.gronod.esp32aldldashboard.ui.main
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -7,7 +7,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
/** UI tests for [com.example.esp32aldldashboard.ui.main.MainScreen]. */
/** UI tests for [com.gronod.esp32aldldashboard.ui.main.MainScreen]. */
class MainScreenTest {
@get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@@ -1,14 +1,14 @@
package com.example.esp32aldldashboard
package com.gronod.esp32aldldashboard
import android.app.Application
import com.example.esp32aldldashboard.bluetooth.BluetoothService
import com.example.esp32aldldashboard.repository.BLMTableRepository
import com.example.esp32aldldashboard.repository.ChartPreferencesRepository
import com.example.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository
import com.example.esp32aldldashboard.logging.CsvLogger
import com.example.esp32aldldashboard.logging.RawStreamLogger
import com.example.esp32aldldashboard.data.database.TelemetryDatabase
import com.gronod.esp32aldldashboard.bluetooth.BluetoothService
import com.gronod.esp32aldldashboard.repository.BLMTableRepository
import com.gronod.esp32aldldashboard.repository.ChartPreferencesRepository
import com.gronod.esp32aldldashboard.repository.SettingsRepository
import com.gronod.esp32aldldashboard.repository.TelemetryRepository
import com.gronod.esp32aldldashboard.logging.CsvLogger
import com.gronod.esp32aldldashboard.logging.RawStreamLogger
import com.gronod.esp32aldldashboard.data.database.TelemetryDatabase
class AldlApplication : Application() {
lateinit var bluetoothService: BluetoothService
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard
package com.gronod.esp32aldldashboard
import android.os.Bundle
import androidx.activity.ComponentActivity
@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.esp32aldldashboard.theme.ESP32ALDLDashboardTheme
import com.gronod.esp32aldldashboard.theme.ESP32ALDLDashboardTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard
package com.gronod.esp32aldldashboard
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
@@ -8,7 +8,7 @@ import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.example.esp32aldldashboard.ui.main.MainScreen
import com.gronod.esp32aldldashboard.ui.main.MainScreen
@Composable
fun MainNavigation() {
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard
package com.gronod.esp32aldldashboard
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.bluetooth
package com.gronod.esp32aldldashboard.bluetooth
import android.app.Notification
import android.app.NotificationChannel
@@ -9,9 +9,9 @@ import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.example.esp32aldldashboard.AldlApplication
import com.example.esp32aldldashboard.MainActivity
import com.example.esp32aldldashboard.R
import com.gronod.esp32aldldashboard.AldlApplication
import com.gronod.esp32aldldashboard.MainActivity
import com.gronod.esp32aldldashboard.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.bluetooth
package com.gronod.esp32aldldashboard.bluetooth
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
@@ -7,10 +7,10 @@ import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.content.Context
import android.util.Log
import com.example.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.parser.ALDLParser
import com.example.esp32aldldashboard.logging.RawStreamLogger
import com.example.esp32aldldashboard.repository.SettingsRepository
import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.gronod.esp32aldldashboard.parser.ALDLParser
import com.gronod.esp32aldldashboard.logging.RawStreamLogger
import com.gronod.esp32aldldashboard.repository.SettingsRepository
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
@@ -234,7 +234,7 @@ class BluetoothService(
payload[24] = bpwLowRaw.toByte()
val parsed = ALDLParser.parseFrame(payload)
if (parsed is com.example.esp32aldldashboard.parser.ALDLParseResult.Success) {
if (parsed is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success) {
_latestFrame.value = parsed.frame
val hexString = payload.joinToString(" ") { String.format("%02X", it) }
addRawHexLog("AA 55 $hexString (SIMULATED)")
@@ -380,7 +380,7 @@ class BluetoothService(
val parsed = ALDLParser.parseFrame(payload)
when (parsed) {
is com.example.esp32aldldashboard.parser.ALDLParseResult.Success -> {
is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success -> {
_framesReceived.value += 1
framesInCurrentSecond++
@@ -397,11 +397,11 @@ class BluetoothService(
addRawHexLog("AA 55 $hexString")
}
}
is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData -> {
is com.gronod.esp32aldldashboard.parser.ALDLParseResult.InvalidData -> {
_parseErrors.value += 1
Log.w(TAG, "Invalid frame: ${parsed.reason}")
}
com.example.esp32aldldashboard.parser.ALDLParseResult.Incomplete -> {
com.gronod.esp32aldldashboard.parser.ALDLParseResult.Incomplete -> {
// Handled by size check
}
}
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.data
package com.gronod.esp32aldldashboard.data
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.data.database
package com.gronod.esp32aldldashboard.data.database
import androidx.room.Entity
import androidx.room.PrimaryKey
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.data.database
package com.gronod.esp32aldldashboard.data.database
import androidx.room.Dao
import androidx.room.Insert
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.data.database
package com.gronod.esp32aldldashboard.data.database
import androidx.room.Entity
import androidx.room.ForeignKey
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.data.database
package com.gronod.esp32aldldashboard.data.database
import android.content.Context
import androidx.room.Database
@@ -1,11 +1,11 @@
package com.example.esp32aldldashboard.logging
package com.gronod.esp32aldldashboard.logging
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import com.example.esp32aldldashboard.parser.ALDLFrame
import com.gronod.esp32aldldashboard.parser.ALDLFrame
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Date
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.logging
package com.gronod.esp32aldldashboard.logging
import android.content.ContentValues
import android.content.Context
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.parser
package com.gronod.esp32aldldashboard.parser
data class ALDLFrame(
val rawBytes: ByteArray,
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.parser
package com.gronod.esp32aldldashboard.parser
// Approximate Engine Load (0.0 to 1.0)
// Very rough approximation: MAP (kPa) / ~100 kPa (atmospheric at sea level)
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.parser
package com.gronod.esp32aldldashboard.parser
object TroubleCodeDictionary {
val DTC_DESCRIPTIONS = mapOf(
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.repository
package com.gronod.esp32aldldashboard.repository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -1,10 +1,10 @@
package com.example.esp32aldldashboard.repository
package com.gronod.esp32aldldashboard.repository
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import com.example.esp32aldldashboard.ui.charts.ChartParameter
import com.gronod.esp32aldldashboard.ui.charts.ChartParameter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.repository
package com.gronod.esp32aldldashboard.repository
import android.content.ContentUris
import android.content.Context
@@ -1,15 +1,15 @@
package com.example.esp32aldldashboard.repository
package com.gronod.esp32aldldashboard.repository
import android.content.Context
import android.content.Intent
import com.example.esp32aldldashboard.bluetooth.BluetoothService
import com.example.esp32aldldashboard.bluetooth.BluetoothForegroundService
import com.example.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.data.database.SessionEntity
import com.example.esp32aldldashboard.data.database.TelemetryDao
import com.example.esp32aldldashboard.data.database.TelemetryDataPointEntity
import com.example.esp32aldldashboard.logging.CsvLogger
import com.gronod.esp32aldldashboard.bluetooth.BluetoothService
import com.gronod.esp32aldldashboard.bluetooth.BluetoothForegroundService
import com.gronod.esp32aldldashboard.bluetooth.ConnectionState
import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.gronod.esp32aldldashboard.data.database.SessionEntity
import com.gronod.esp32aldldashboard.data.database.TelemetryDao
import com.gronod.esp32aldldashboard.data.database.TelemetryDataPointEntity
import com.gronod.esp32aldldashboard.logging.CsvLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.theme
package com.gronod.esp32aldldashboard.theme
import androidx.compose.ui.graphics.Color
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.theme
package com.gronod.esp32aldldashboard.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.theme
package com.gronod.esp32aldldashboard.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.ui.blm
package com.gronod.esp32aldldashboard.ui.blm
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -18,7 +18,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.esp32aldldashboard.repository.BLMTableRepository
import com.gronod.esp32aldldashboard.repository.BLMTableRepository
@Composable
fun BLMTableScreen(
@@ -1,9 +1,9 @@
package com.example.esp32aldldashboard.ui.blm
package com.gronod.esp32aldldashboard.ui.blm
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.esp32aldldashboard.repository.BLMCellData
import com.example.esp32aldldashboard.repository.BLMTableRepository
import com.gronod.esp32aldldashboard.repository.BLMCellData
import com.gronod.esp32aldldashboard.repository.BLMTableRepository
import kotlinx.coroutines.flow.StateFlow
class BLMTableViewModel(private val repository: BLMTableRepository) : ViewModel() {
@@ -1,7 +1,7 @@
package com.example.esp32aldldashboard.ui.charts
package com.gronod.esp32aldldashboard.ui.charts
import androidx.compose.ui.graphics.Color
import com.example.esp32aldldashboard.parser.ALDLFrame
import com.gronod.esp32aldldashboard.parser.ALDLFrame
enum class ChartParameter(
val displayName: String,
@@ -1,5 +1,5 @@
@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
package com.example.esp32aldldashboard.ui.charts
package com.gronod.esp32aldldashboard.ui.charts
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.foundation.Canvas
@@ -16,8 +16,8 @@ import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.repository.ChartPreferencesRepository
import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.gronod.esp32aldldashboard.repository.ChartPreferencesRepository
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.ui.components
package com.gronod.esp32aldldashboard.ui.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.ui.main
package com.gronod.esp32aldldashboard.ui.main
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -16,11 +16,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.parser.TroubleCodeDictionary
import com.example.esp32aldldashboard.ui.components.RpmGauge
import com.example.esp32aldldashboard.ui.components.TpsBar
import com.gronod.esp32aldldashboard.bluetooth.ConnectionState
import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.gronod.esp32aldldashboard.parser.TroubleCodeDictionary
import com.gronod.esp32aldldashboard.ui.components.RpmGauge
import com.gronod.esp32aldldashboard.ui.components.TpsBar
// Theme Colors
val DarkBg = Color(0xFF0F0F12)
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.ui.main
package com.gronod.esp32aldldashboard.ui.main
import android.Manifest
import android.content.pm.PackageManager
@@ -18,11 +18,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.esp32aldldashboard.AldlApplication
import com.example.esp32aldldashboard.ui.blm.BLMTableScreen
import com.example.esp32aldldashboard.ui.blm.BLMTableViewModelFactory
import com.example.esp32aldldashboard.ui.charts.ChartsScreen
import com.example.esp32aldldashboard.ui.settings.SettingsScreen
import com.gronod.esp32aldldashboard.AldlApplication
import com.gronod.esp32aldldashboard.ui.blm.BLMTableScreen
import com.gronod.esp32aldldashboard.ui.blm.BLMTableViewModelFactory
import com.gronod.esp32aldldashboard.ui.charts.ChartsScreen
import com.gronod.esp32aldldashboard.ui.settings.SettingsScreen
@Composable
fun MainScreen(
@@ -131,7 +131,7 @@ fun MainScreen(
modifier = modifier.padding(paddingValues)
)
2 -> {
val blmViewModel: com.example.esp32aldldashboard.ui.blm.BLMTableViewModel = viewModel(
val blmViewModel: com.gronod.esp32aldldashboard.ui.blm.BLMTableViewModel = viewModel(
factory = BLMTableViewModelFactory(app.blmTableRepository)
)
BLMTableScreen(
@@ -1,11 +1,11 @@
package com.example.esp32aldldashboard.ui.main
package com.gronod.esp32aldldashboard.ui.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository
import com.gronod.esp32aldldashboard.bluetooth.ConnectionState
import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.gronod.esp32aldldashboard.repository.SettingsRepository
import com.gronod.esp32aldldashboard.repository.TelemetryRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
@@ -1,9 +1,9 @@
package com.example.esp32aldldashboard.ui.main
package com.gronod.esp32aldldashboard.ui.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository
import com.gronod.esp32aldldashboard.repository.SettingsRepository
import com.gronod.esp32aldldashboard.repository.TelemetryRepository
class MainScreenViewModelFactory(
private val telemetryRepository: TelemetryRepository,
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.ui.settings
package com.gronod.esp32aldldashboard.ui.settings
import android.content.Intent
import androidx.compose.foundation.layout.*
@@ -14,8 +14,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import com.example.esp32aldldashboard.repository.FileType
import com.example.esp32aldldashboard.repository.LoggedFile
import com.gronod.esp32aldldashboard.repository.FileType
import com.gronod.esp32aldldashboard.repository.LoggedFile
@Composable
fun LogFilesDialog(
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.ui.settings
package com.gronod.esp32aldldashboard.ui.settings
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
@@ -9,8 +9,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.esp32aldldashboard.repository.LoggedFile
import com.example.esp32aldldashboard.repository.SettingsRepository
import com.gronod.esp32aldldashboard.repository.LoggedFile
import com.gronod.esp32aldldashboard.repository.SettingsRepository
import kotlinx.coroutines.launch
@Composable
@@ -1,6 +1,6 @@
package com.example.esp32aldldashboard
package com.gronod.esp32aldldashboard
import com.example.esp32aldldashboard.parser.ALDLParser
import com.gronod.esp32aldldashboard.parser.ALDLParser
import org.junit.Assert.*
import org.junit.Test
@@ -20,8 +20,8 @@ class ALDLParserTest {
)
val result = ALDLParser.parseFrame(rawPayload)
assertTrue(result is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
val frame = (result as com.example.esp32aldldashboard.parser.ALDLParseResult.Success).frame
assertTrue(result is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success)
val frame = (result as com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success).frame
// Assert values based on 24-INT10.ads specifications:
assertEquals(95, frame.iacPosition) // u[3] (Byte 4)
@@ -78,7 +78,7 @@ class ALDLParserTest {
this[7] = 255.toByte() // 255 * 25 = 6375 RPM
}
val validResult = ALDLParser.parseFrame(validPayload)
assertTrue("Max representable RPM should be valid", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
assertTrue("Max representable RPM should be valid", validResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success)
}
@Test
@@ -88,28 +88,28 @@ class ALDLParserTest {
this[17] = 80.toByte()
}
val minResult = ALDLParser.parseFrame(minPayload)
assertTrue("Battery at 8V should be valid", minResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
assertTrue("Battery at 8V should be valid", minResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success)
// Max valid: 18V (180 * 0.1 = 18.0V)
val maxPayload = createBasePayload().apply {
this[17] = 180.toByte()
}
val maxResult = ALDLParser.parseFrame(maxPayload)
assertTrue("Battery at 18V should be valid", maxResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
assertTrue("Battery at 18V should be valid", maxResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success)
// Too low: 4.9V (49 * 0.1 = 4.9V, below 5V minimum)
val lowPayload = createBasePayload().apply {
this[17] = 49.toByte()
}
val lowResult = ALDLParser.parseFrame(lowPayload)
assertTrue("Battery below 5V should be rejected", lowResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
assertTrue("Battery below 5V should be rejected", lowResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
// Too high: 20.1V (201 * 0.1 = 20.1V, above 20V maximum)
val highPayload = createBasePayload().apply {
this[17] = 201.toByte()
}
val highResult = ALDLParser.parseFrame(highPayload)
assertTrue("Battery above 20V should be rejected", highResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
assertTrue("Battery above 20V should be rejected", highResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
}
@Test
@@ -119,7 +119,7 @@ class ALDLParserTest {
this[8] = 255.toByte()
}
val validResult = ALDLParser.parseFrame(validPayload)
assertTrue("Max representable TPS should be valid", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
assertTrue("Max representable TPS should be valid", validResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success)
}
@Test
@@ -127,21 +127,21 @@ class ALDLParserTest {
// Valid temperature: 89 * 0.75 - 40 = 26.75C (within -45 to 220 range)
val validPayload = createBasePayload()
val validResult = ALDLParser.parseFrame(validPayload)
assertTrue("Valid coolant temp should be accepted", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
assertTrue("Valid coolant temp should be accepted", validResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success)
// Max representable: 255 * 0.75 - 40 = 151.25C
val maxPayload = createBasePayload().apply {
this[4] = 255.toByte()
}
val maxResult = ALDLParser.parseFrame(maxPayload)
assertTrue("Max representable coolant temp should be accepted", maxResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
assertTrue("Max representable coolant temp should be accepted", maxResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success)
// Min representable: 0 * 0.75 - 40 = -40C
val minPayload = createBasePayload().apply {
this[4] = 0.toByte()
}
val minResult = ALDLParser.parseFrame(minPayload)
assertTrue("Min representable coolant temp should be accepted", minResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
assertTrue("Min representable coolant temp should be accepted", minResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success)
}
@Test
@@ -149,7 +149,7 @@ class ALDLParserTest {
// Payload with only 24 bytes (incomplete)
val incompletePayload = ByteArray(24) { 0x20.toByte() }
val result = ALDLParser.parseFrame(incompletePayload)
assertTrue("Incomplete payload should return Incomplete result", result is com.example.esp32aldldashboard.parser.ALDLParseResult.Incomplete)
assertTrue("Incomplete payload should return Incomplete result", result is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Incomplete)
}
private fun createBasePayload(): ByteArray {
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.parser
package com.gronod.esp32aldldashboard.parser
import org.junit.Assert.*
import org.junit.Test
@@ -1,9 +1,9 @@
@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
package com.example.esp32aldldashboard.ui.main
package com.gronod.esp32aldldashboard.ui.main
import com.example.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository
import com.gronod.esp32aldldashboard.bluetooth.ConnectionState
import com.gronod.esp32aldldashboard.repository.SettingsRepository
import com.gronod.esp32aldldashboard.repository.TelemetryRepository
import io.mockk.*
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
@@ -0,0 +1,11 @@
Initial release v0.1.0
• Real-time dashboard with RPM gauge, sensors, flags & trouble codes
• BLM/INT color heatmap (14 RPM × 17 MAP)
• Multi-parameter charts (single/2x2 grid)
• Room persistence + TunerPro CSV + raw .bin logging
• Logged files browser + background service
• Robust Bluetooth parsing (27-byte validation) + stats + simulation
• Settings, ProGuard, CI workflows & fastlane metadata
For classic GM OBD1 via ESP32-ALDL (Android 8.0+).
@@ -0,0 +1,14 @@
ESP32 ALDL Dashboard connects your Android phone to classic GM OBD1 vehicles (such as the 1986 Pontiac Fiero 1227170 ECM) via a custom ESP32 Bluetooth SPP bridge, decoding the 160-baud ALDL datastream in real time.
Features:
- Animated radial RPM gauge and live readouts for speed, coolant temp, MAP, TPS, O2 sensor, MAT, and battery voltage
- Status flags for closed-loop mode, rich/lean mixture, TCC lockup, and A/C clutch
- BLM and Integrator fuel-trim heatmap grid across 14 RPM and 17 MAP bands with dynamic colour coding
- Multi-parameter line charts in single or 2x2 grid mode for 12 selectable metrics
- Trouble code decoder with human-readable descriptions
- Dual logging: Room database sessions and TunerPro RT-compatible CSV export to Downloads/ALDLLogs
- Optional raw binary stream recording for advanced playback and diagnostics
- In-app log browser for viewing, opening, or sharing CSV and BIN files
- Persistent settings for temperature units, alert thresholds, and auto-logging preferences
Requirements: Android 7.0 or higher; an ESP32 device programmed to bridge the ALDL serial pin to Classic Bluetooth SPP (named ESP32-ALDL).
Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

@@ -0,0 +1 @@
Real-time GM OBD1 ALDL dashboard via ESP32 Bluetooth for classic cars.
@@ -0,0 +1 @@
ESP32 ALDL Dashboard
Binary file not shown.
+1
View File
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true