Compare commits
26 Commits
877db7f098
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f13ed57f84 | |||
| bea26d5ff3 | |||
| c9e9fede29 | |||
| 180ed20741 | |||
| b21efd5c99 | |||
| 8bf3d53448 | |||
| 3278fd54b2 | |||
| d034fb1b98 | |||
| 807ac93646 | |||
| a9dec01688 | |||
| 0415ec366a | |||
| c229dc4e64 | |||
| 4b48d455e4 | |||
| b646d8eaa8 | |||
| 63aaffc1d7 | |||
| da8f216edd | |||
| 4855712da5 | |||
| 0387de99c7 | |||
| 4d0ab48c1f | |||
| d40a63ec4a | |||
| 2e94463f37 | |||
| 6a6232649c | |||
| ccfaf2686c | |||
| 621f67aa38 | |||
| df2f851a0d | |||
| 0b43a024f8 |
@@ -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"
|
||||||
@@ -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 }}
|
|
||||||
@@ -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*
|
||||||
@@ -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)
|
||||||
|

|
||||||
|
|
||||||
|
### 2. Live Instrumentation Dashboard (Connected)
|
||||||
|
Real-time gauges, sensor cards, status flags, and active ECM fault code alerts.
|
||||||
|

|
||||||
|
|
||||||
|
### 3. Real-Time Telemetry Charts (Multi-Parameter Mode)
|
||||||
|
Dynamic line charts supporting up to 4 simultaneous parameters with single/multi toggle.
|
||||||
|

|
||||||
|
|
||||||
|
### 4. BLM/INT Fuel Trim Heatmap Table
|
||||||
|
Color-coded 14×17 (RPM × MAP) diagnostic grid with live updates and legend.
|
||||||
|

|
||||||
|
|
||||||
|
### 5. Settings & Alerts
|
||||||
|
Persistent preferences, auto-logging controls, raw datastream recording, logged files browser, and threshold sliders.
|
||||||
|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌟 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)
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
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
|
||||||
@@ -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
|
||||||
|
|||||||