Compare commits

...

26 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
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 name: Build and Release APK
on: on:
push: push:
branches: tags:
- main - 'v*'
- master
- v2
jobs: jobs:
build-and-release: build-and-release:
@@ -13,6 +16,15 @@ jobs:
permissions: permissions:
contents: write contents: write
steps: 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 - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -28,29 +40,27 @@ jobs:
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v3
- name: Make gradlew executable - name: Decode keystore
run: chmod +x ./gradlew run: |
echo "${{ secrets.SIGNING_KEYSTORE_BASE64 }}" | base64 -d > release.keystore
- name: Run Unit Tests - name: Export signing environment variables
run: ./gradlew test 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 - name: Test and Build Release APK
run: ./gradlew assembleDebug run: chmod +x ./gradlew && ./gradlew test assembleRelease
- name: Get Short SHA
id: vars
run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Rename APK - name: Rename APK
run: | 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 uses: softprops/action-gh-release@v2
with: with:
tag_name: "build-${{ env.short_sha }}" files: artifacts/esp32-aldl-dashboard-${{ github.ref_name }}.apk
name: "Build ${{ env.short_sha }}" generate_release_notes: true
files: app/build/outputs/apk/debug/esp32-aldl-dashboard-${{ env.short_sha }}.apk
prerelease: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+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 # 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 ## 🌟 Key Features
* **Real-Time Instrumentation Dashboard:** - **Real-Time Instrumentation Dashboard:** Vibrant, animated Canvas-based components including a radial RPM gauge and a Throttle Position Sensor (TPS) bar graph.
* 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.
* 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.
* **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.
* **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).
* **BLM & INT Fuel Trim Heatmap Grid (New):** * 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).
* 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). - **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.
* 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$). * 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.
* **Multi-Parameter Line Charting (New):** - **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.
* 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. - **Dual-Logging Framework:**
* 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. * **Room Database Sessions:** Saves all active telemetry packets to a local SQLite database using Jetpack Room.
* **Trouble Code Diagnostic Engine:** * **TunerPro RT CSV Export:** Automatically compiles sessions into .csv log files fully compatible with TunerPro RT, exported to Downloads/ALDLLogs using Android MediaStore.
* 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. * **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.
* **Dual-Logging Framework:** - **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.
* **Room Database Sessions:** Saves all active telemetry packets to a local SQLite database using Jetpack Room. - **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.
* **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 ## 🛠️ 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. - **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):** - **Circular Buffering & Packet Validation (New):** Ingests raw Bluetooth stream buffers using an ArrayDeque<Byte> circular buffer.
* 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.
* 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).
* **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.
* **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.
--- ---
## 📱 Requirements ## 📱 Requirements
* **Android Device:** Android 8.0 (API Level 26) or higher. - **Android Device:** Android 8.0 (API Level 26) or higher.
* **Bluetooth Permissions:** Requires Nearby Devices (Android 12+) and Legacy Bluetooth Admin access. - 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`). - **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 ## 🚀 Setup & Usage
1. Pair the `ESP32-ALDL` Bluetooth module in your Android system settings. 1. Pair the ESP32-ALDL Bluetooth module in your Android system settings.
2. Launch the application and grant all requested permissions. 2. Launch the application and grant all requested permissions.
3. Tap **Connect BT** on the main dashboard to establish connection and start telemetry. 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. 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. 5. Configure logging preferences or browse logged CSV/BIN files in the **Settings** menu.
--- ---
## 🧪 Testing & Verification ## 🧪 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: To run the unit tests, execute the following Gradle command in the root project directory:
```bash ```bash
./gradlew test ./gradlew test
``` ```
### Coverage Areas ### 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. - **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.
* **Lookahead Stability:** Validates parser behavior against truncated or incomplete frame fragments. - **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 { android {
namespace = "com.example.esp32aldldashboard" namespace = "com.gronod.esp32aldldashboard"
compileSdk = 36 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "com.example.esp32aldldashboard" applicationId = "com.gronod.esp32aldldashboard"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 1 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 { buildTypes {
release { release {
isMinifyEnabled = false signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 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.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -7,7 +7,7 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
/** UI tests for [com.example.esp32aldldashboard.ui.main.MainScreen]. */ /** UI tests for [com.gronod.esp32aldldashboard.ui.main.MainScreen]. */
class MainScreenTest { class MainScreenTest {
@get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>() @get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@@ -1,14 +1,14 @@
package com.example.esp32aldldashboard package com.gronod.esp32aldldashboard
import android.app.Application import android.app.Application
import com.example.esp32aldldashboard.bluetooth.BluetoothService import com.gronod.esp32aldldashboard.bluetooth.BluetoothService
import com.example.esp32aldldashboard.repository.BLMTableRepository import com.gronod.esp32aldldashboard.repository.BLMTableRepository
import com.example.esp32aldldashboard.repository.ChartPreferencesRepository import com.gronod.esp32aldldashboard.repository.ChartPreferencesRepository
import com.example.esp32aldldashboard.repository.SettingsRepository import com.gronod.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository import com.gronod.esp32aldldashboard.repository.TelemetryRepository
import com.example.esp32aldldashboard.logging.CsvLogger import com.gronod.esp32aldldashboard.logging.CsvLogger
import com.example.esp32aldldashboard.logging.RawStreamLogger import com.gronod.esp32aldldashboard.logging.RawStreamLogger
import com.example.esp32aldldashboard.data.database.TelemetryDatabase import com.gronod.esp32aldldashboard.data.database.TelemetryDatabase
class AldlApplication : Application() { class AldlApplication : Application() {
lateinit var bluetoothService: BluetoothService lateinit var bluetoothService: BluetoothService
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard package com.gronod.esp32aldldashboard
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.example.esp32aldldashboard.theme.ESP32ALDLDashboardTheme import com.gronod.esp32aldldashboard.theme.ESP32ALDLDashboardTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { 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.padding
import androidx.compose.foundation.layout.safeDrawingPadding 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.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import com.example.esp32aldldashboard.ui.main.MainScreen import com.gronod.esp32aldldashboard.ui.main.MainScreen
@Composable @Composable
fun MainNavigation() { fun MainNavigation() {
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard package com.gronod.esp32aldldashboard
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable 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.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
@@ -9,9 +9,9 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.example.esp32aldldashboard.AldlApplication import com.gronod.esp32aldldashboard.AldlApplication
import com.example.esp32aldldashboard.MainActivity import com.gronod.esp32aldldashboard.MainActivity
import com.example.esp32aldldashboard.R import com.gronod.esp32aldldashboard.R
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.bluetooth package com.gronod.esp32aldldashboard.bluetooth
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
@@ -7,10 +7,10 @@ import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket import android.bluetooth.BluetoothSocket
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.example.esp32aldldashboard.parser.ALDLFrame import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.parser.ALDLParser import com.gronod.esp32aldldashboard.parser.ALDLParser
import com.example.esp32aldldashboard.logging.RawStreamLogger import com.gronod.esp32aldldashboard.logging.RawStreamLogger
import com.example.esp32aldldashboard.repository.SettingsRepository import com.gronod.esp32aldldashboard.repository.SettingsRepository
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -234,7 +234,7 @@ class BluetoothService(
payload[24] = bpwLowRaw.toByte() payload[24] = bpwLowRaw.toByte()
val parsed = ALDLParser.parseFrame(payload) 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 _latestFrame.value = parsed.frame
val hexString = payload.joinToString(" ") { String.format("%02X", it) } val hexString = payload.joinToString(" ") { String.format("%02X", it) }
addRawHexLog("AA 55 $hexString (SIMULATED)") addRawHexLog("AA 55 $hexString (SIMULATED)")
@@ -380,7 +380,7 @@ class BluetoothService(
val parsed = ALDLParser.parseFrame(payload) val parsed = ALDLParser.parseFrame(payload)
when (parsed) { when (parsed) {
is com.example.esp32aldldashboard.parser.ALDLParseResult.Success -> { is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success -> {
_framesReceived.value += 1 _framesReceived.value += 1
framesInCurrentSecond++ framesInCurrentSecond++
@@ -397,11 +397,11 @@ class BluetoothService(
addRawHexLog("AA 55 $hexString") addRawHexLog("AA 55 $hexString")
} }
} }
is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData -> { is com.gronod.esp32aldldashboard.parser.ALDLParseResult.InvalidData -> {
_parseErrors.value += 1 _parseErrors.value += 1
Log.w(TAG, "Invalid frame: ${parsed.reason}") Log.w(TAG, "Invalid frame: ${parsed.reason}")
} }
com.example.esp32aldldashboard.parser.ALDLParseResult.Incomplete -> { com.gronod.esp32aldldashboard.parser.ALDLParseResult.Incomplete -> {
// Handled by size check // 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
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.Entity
import androidx.room.PrimaryKey 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.Dao
import androidx.room.Insert 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.Entity
import androidx.room.ForeignKey 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 android.content.Context
import androidx.room.Database 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.ContentValues
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import com.example.esp32aldldashboard.parser.ALDLFrame import com.gronod.esp32aldldashboard.parser.ALDLFrame
import java.io.OutputStream import java.io.OutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date 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.ContentValues
import android.content.Context import android.content.Context
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.parser package com.gronod.esp32aldldashboard.parser
data class ALDLFrame( data class ALDLFrame(
val rawBytes: ByteArray, 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) // Approximate Engine Load (0.0 to 1.0)
// Very rough approximation: MAP (kPa) / ~100 kPa (atmospheric at sea level) // 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 { object TroubleCodeDictionary {
val DTC_DESCRIPTIONS = mapOf( 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -1,10 +1,10 @@
package com.example.esp32aldldashboard.repository package com.gronod.esp32aldldashboard.repository
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.* import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore 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.Flow
import kotlinx.coroutines.flow.map 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.ContentUris
import android.content.Context 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.Context
import android.content.Intent import android.content.Intent
import com.example.esp32aldldashboard.bluetooth.BluetoothService import com.gronod.esp32aldldashboard.bluetooth.BluetoothService
import com.example.esp32aldldashboard.bluetooth.BluetoothForegroundService import com.gronod.esp32aldldashboard.bluetooth.BluetoothForegroundService
import com.example.esp32aldldashboard.bluetooth.ConnectionState import com.gronod.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.parser.ALDLFrame import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.data.database.SessionEntity import com.gronod.esp32aldldashboard.data.database.SessionEntity
import com.example.esp32aldldashboard.data.database.TelemetryDao import com.gronod.esp32aldldashboard.data.database.TelemetryDao
import com.example.esp32aldldashboard.data.database.TelemetryDataPointEntity import com.gronod.esp32aldldashboard.data.database.TelemetryDataPointEntity
import com.example.esp32aldldashboard.logging.CsvLogger import com.gronod.esp32aldldashboard.logging.CsvLogger
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -1,4 +1,4 @@
package com.example.esp32aldldashboard.theme package com.gronod.esp32aldldashboard.theme
import androidx.compose.ui.graphics.Color 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 android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme 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.material3.Typography
import androidx.compose.ui.text.TextStyle 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.background
import androidx.compose.foundation.border 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.esp32aldldashboard.repository.BLMTableRepository import com.gronod.esp32aldldashboard.repository.BLMTableRepository
@Composable @Composable
fun BLMTableScreen( 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.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.example.esp32aldldashboard.repository.BLMCellData import com.gronod.esp32aldldashboard.repository.BLMCellData
import com.example.esp32aldldashboard.repository.BLMTableRepository import com.gronod.esp32aldldashboard.repository.BLMTableRepository
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
class BLMTableViewModel(private val repository: BLMTableRepository) : ViewModel() { 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 androidx.compose.ui.graphics.Color
import com.example.esp32aldldashboard.parser.ALDLFrame import com.gronod.esp32aldldashboard.parser.ALDLFrame
enum class ChartParameter( enum class ChartParameter(
val displayName: String, val displayName: String,
@@ -1,5 +1,5 @@
@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) @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.material3.ExperimentalMaterial3Api
import androidx.compose.foundation.Canvas 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.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.esp32aldldashboard.parser.ALDLFrame import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.repository.ChartPreferencesRepository import com.gronod.esp32aldldashboard.repository.ChartPreferencesRepository
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch 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.Spring
import androidx.compose.animation.core.animateFloatAsState 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.background
import androidx.compose.foundation.border 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.example.esp32aldldashboard.bluetooth.ConnectionState import com.gronod.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.parser.ALDLFrame import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.parser.TroubleCodeDictionary import com.gronod.esp32aldldashboard.parser.TroubleCodeDictionary
import com.example.esp32aldldashboard.ui.components.RpmGauge import com.gronod.esp32aldldashboard.ui.components.RpmGauge
import com.example.esp32aldldashboard.ui.components.TpsBar import com.gronod.esp32aldldashboard.ui.components.TpsBar
// Theme Colors // Theme Colors
val DarkBg = Color(0xFF0F0F12) 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.Manifest
import android.content.pm.PackageManager import android.content.pm.PackageManager
@@ -18,11 +18,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.esp32aldldashboard.AldlApplication import com.gronod.esp32aldldashboard.AldlApplication
import com.example.esp32aldldashboard.ui.blm.BLMTableScreen import com.gronod.esp32aldldashboard.ui.blm.BLMTableScreen
import com.example.esp32aldldashboard.ui.blm.BLMTableViewModelFactory import com.gronod.esp32aldldashboard.ui.blm.BLMTableViewModelFactory
import com.example.esp32aldldashboard.ui.charts.ChartsScreen import com.gronod.esp32aldldashboard.ui.charts.ChartsScreen
import com.example.esp32aldldashboard.ui.settings.SettingsScreen import com.gronod.esp32aldldashboard.ui.settings.SettingsScreen
@Composable @Composable
fun MainScreen( fun MainScreen(
@@ -131,7 +131,7 @@ fun MainScreen(
modifier = modifier.padding(paddingValues) modifier = modifier.padding(paddingValues)
) )
2 -> { 2 -> {
val blmViewModel: com.example.esp32aldldashboard.ui.blm.BLMTableViewModel = viewModel( val blmViewModel: com.gronod.esp32aldldashboard.ui.blm.BLMTableViewModel = viewModel(
factory = BLMTableViewModelFactory(app.blmTableRepository) factory = BLMTableViewModelFactory(app.blmTableRepository)
) )
BLMTableScreen( BLMTableScreen(
@@ -1,11 +1,11 @@
package com.example.esp32aldldashboard.ui.main package com.gronod.esp32aldldashboard.ui.main
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.esp32aldldashboard.bluetooth.ConnectionState import com.gronod.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.parser.ALDLFrame import com.gronod.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.repository.SettingsRepository import com.gronod.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository import com.gronod.esp32aldldashboard.repository.TelemetryRepository
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn 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.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.example.esp32aldldashboard.repository.SettingsRepository import com.gronod.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository import com.gronod.esp32aldldashboard.repository.TelemetryRepository
class MainScreenViewModelFactory( class MainScreenViewModelFactory(
private val telemetryRepository: TelemetryRepository, 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 android.content.Intent
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -14,8 +14,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.example.esp32aldldashboard.repository.FileType import com.gronod.esp32aldldashboard.repository.FileType
import com.example.esp32aldldashboard.repository.LoggedFile import com.gronod.esp32aldldashboard.repository.LoggedFile
@Composable @Composable
fun LogFilesDialog( 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.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -9,8 +9,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.esp32aldldashboard.repository.LoggedFile import com.gronod.esp32aldldashboard.repository.LoggedFile
import com.example.esp32aldldashboard.repository.SettingsRepository import com.gronod.esp32aldldashboard.repository.SettingsRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @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.Assert.*
import org.junit.Test import org.junit.Test
@@ -20,8 +20,8 @@ class ALDLParserTest {
) )
val result = ALDLParser.parseFrame(rawPayload) val result = ALDLParser.parseFrame(rawPayload)
assertTrue(result is com.example.esp32aldldashboard.parser.ALDLParseResult.Success) assertTrue(result is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success)
val frame = (result as com.example.esp32aldldashboard.parser.ALDLParseResult.Success).frame val frame = (result as com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success).frame
// Assert values based on 24-INT10.ads specifications: // Assert values based on 24-INT10.ads specifications:
assertEquals(95, frame.iacPosition) // u[3] (Byte 4) assertEquals(95, frame.iacPosition) // u[3] (Byte 4)
@@ -78,7 +78,7 @@ class ALDLParserTest {
this[7] = 255.toByte() // 255 * 25 = 6375 RPM this[7] = 255.toByte() // 255 * 25 = 6375 RPM
} }
val validResult = ALDLParser.parseFrame(validPayload) 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 @Test
@@ -88,28 +88,28 @@ class ALDLParserTest {
this[17] = 80.toByte() this[17] = 80.toByte()
} }
val minResult = ALDLParser.parseFrame(minPayload) 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) // Max valid: 18V (180 * 0.1 = 18.0V)
val maxPayload = createBasePayload().apply { val maxPayload = createBasePayload().apply {
this[17] = 180.toByte() this[17] = 180.toByte()
} }
val maxResult = ALDLParser.parseFrame(maxPayload) 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) // Too low: 4.9V (49 * 0.1 = 4.9V, below 5V minimum)
val lowPayload = createBasePayload().apply { val lowPayload = createBasePayload().apply {
this[17] = 49.toByte() this[17] = 49.toByte()
} }
val lowResult = ALDLParser.parseFrame(lowPayload) 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) // Too high: 20.1V (201 * 0.1 = 20.1V, above 20V maximum)
val highPayload = createBasePayload().apply { val highPayload = createBasePayload().apply {
this[17] = 201.toByte() this[17] = 201.toByte()
} }
val highResult = ALDLParser.parseFrame(highPayload) 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 @Test
@@ -119,7 +119,7 @@ class ALDLParserTest {
this[8] = 255.toByte() this[8] = 255.toByte()
} }
val validResult = ALDLParser.parseFrame(validPayload) 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 @Test
@@ -127,21 +127,21 @@ class ALDLParserTest {
// Valid temperature: 89 * 0.75 - 40 = 26.75C (within -45 to 220 range) // Valid temperature: 89 * 0.75 - 40 = 26.75C (within -45 to 220 range)
val validPayload = createBasePayload() val validPayload = createBasePayload()
val validResult = ALDLParser.parseFrame(validPayload) 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 // Max representable: 255 * 0.75 - 40 = 151.25C
val maxPayload = createBasePayload().apply { val maxPayload = createBasePayload().apply {
this[4] = 255.toByte() this[4] = 255.toByte()
} }
val maxResult = ALDLParser.parseFrame(maxPayload) 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 // Min representable: 0 * 0.75 - 40 = -40C
val minPayload = createBasePayload().apply { val minPayload = createBasePayload().apply {
this[4] = 0.toByte() this[4] = 0.toByte()
} }
val minResult = ALDLParser.parseFrame(minPayload) 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 @Test
@@ -149,7 +149,7 @@ class ALDLParserTest {
// Payload with only 24 bytes (incomplete) // Payload with only 24 bytes (incomplete)
val incompletePayload = ByteArray(24) { 0x20.toByte() } val incompletePayload = ByteArray(24) { 0x20.toByte() }
val result = ALDLParser.parseFrame(incompletePayload) 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 { 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.Assert.*
import org.junit.Test import org.junit.Test
@@ -1,9 +1,9 @@
@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @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.gronod.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.repository.SettingsRepository import com.gronod.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository import com.gronod.esp32aldldashboard.repository.TelemetryRepository
import io.mockk.* import io.mockk.*
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true