feat: initialize Android project structure with Gradle and basic UI/data architecture
@@ -0,0 +1,15 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ESP32 ALDL Dashboard
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="17" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="app">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -5,7 +5,12 @@
|
|||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -1 +1,119 @@
|
|||||||
|
# ESP32 ALDL Dashboard
|
||||||
|
|
||||||
|
An Android application built using Jetpack Compose to display real-time engine telemetry from a **1986 Pontiac Fiero 2.8L V6 (1227170 ECM)**. The app connects to a Bluetooth serial device named **ESP32-ALDL** and decodes a raw binary stream of 27-byte frames containing ECM data packets under the `$24` mask specifications.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Specifications
|
||||||
|
|
||||||
|
### Data Stream Frame Structure
|
||||||
|
The ESP32 reads the raw 160-baud unidirectional ALDL stream from the ECM and encapsulates it in a 27-byte packet transmitted over Bluetooth SPP.
|
||||||
|
|
||||||
|
| Byte Index | Length | Value / Field | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **0 - 1** | 2 bytes | `AA 55` | Frame Sync Header (added by ESP32) |
|
||||||
|
| **2 - 26** | 25 bytes | Raw Data Payload | 1227170 ECM data stream (0-indexed indices 0 to 24) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parameter Offsets & Decoding Formulas
|
||||||
|
|
||||||
|
The telemetry parameters are parsed from the `$24` / `$24A` ECM mask definitions (`24-INT10.ads`). In TunerPro's `.ads` file, the byte numbers are 1-indexed (e.g. `btByteNumber = 4` maps to raw payload byte index `3`).
|
||||||
|
|
||||||
|
### 1. Primary Sensors & Measurements
|
||||||
|
|
||||||
|
| Parameter | Payload Index | Raw Size | Formula / Conversion | Units |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| **IAC Position** | Index 3 (Byte 4) | 8-bit | $Value = Raw$ | Steps |
|
||||||
|
| **Coolant Temp** | Index 4 (Byte 5) | 8-bit | $C = (Raw \times 0.75) - 40$<br>$F = (Raw \times 1.35) - 40$ | °C / °F |
|
||||||
|
| **Vehicle Speed** | Index 5 (Byte 6) | 8-bit | $Value = Raw$ (Operation 3) | MPH |
|
||||||
|
| **MAP** | Index 6 (Byte 7) | 8-bit | $Volts = Raw \times 0.019608$<br>$kPa = (Raw \times 0.369) + 10.354$ | Volts / kPa |
|
||||||
|
| **Engine Speed** | Index 7 (Byte 8) | 8-bit | $RPM = Raw \times 25$ | RPM |
|
||||||
|
| **TPS** | Index 8 (Byte 9) | 8-bit | $Volts = Raw \times 0.019608$ | Volts |
|
||||||
|
| **Integrator (INT)** | Index 9 (Byte 10) | 8-bit | $Value = Raw$ | — |
|
||||||
|
| **O2 Sensor** | Index 10 (Byte 11) | 8-bit | $mV = Raw \times 4.44$ | mV |
|
||||||
|
| **Battery Voltage** | Index 17 (Byte 18) | 8-bit | $Volts = Raw \times 0.1$ | Volts |
|
||||||
|
| **BLM** | Index 18 (Byte 19) | 8-bit | $Value = Raw$ | — |
|
||||||
|
| **Rich/Lean Crosses** | Index 19 (Byte 20) | 8-bit | $Value = Raw$ | Crosses |
|
||||||
|
| **Spark Advance** | Index 20 (Byte 21) | 8-bit | $Degrees = Raw \times 0.351563$ | Degrees |
|
||||||
|
| **EGR Duty Cycle** | Index 21 (Byte 22) | 8-bit | $Percent = Raw \times 0.392157$ | % |
|
||||||
|
| **Manifold Air Temp (MAT)** | Index 22 (Byte 23) | 8-bit | **Linear Table Interpolation** (see below) | °C / °F |
|
||||||
|
| **Base Pulse Width (BPW)** | Index 23-24 (Byte 24-25) | 16-bit | $Raw = (HighByte \ll 8) \vert LowByte$<br>$ms = Raw \times 0.015259$ | Milliseconds (ms) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. MAT Linear Interpolation Tables
|
||||||
|
The Manifold Air Temperature (MAT) is read from Index 22. It is mapped to degrees C or F using interpolation curves specified in tables 52 and 53:
|
||||||
|
|
||||||
|
* **Celsius (`MAT C` - Table 52):**
|
||||||
|
* `0` -> `200.0`, `12` -> `150.0`, `13` -> `145.0`, `14` -> `140.0`, `16` -> `135.0`, `18` -> `130.0`, `21` -> `125.0`, `23` -> `120.0`, `26` -> `115.0`, `30` -> `110.0`, `34` -> `105.0`, `39` -> `100.0`, `44` -> `95.0`, `50` -> `90.0`, `56` -> `85.0`, `64` -> `80.0`, `72` -> `75.0`, `81` -> `70.0`, `92` -> `65.0`, `102` -> `60.0`, `114` -> `55.0`, `126` -> `50.0`, `139` -> `45.0`, `152` -> `40.0`, `165` -> `35.0`, `177` -> `30.0`, `189` -> `25.0`, `199` -> `20.0`, `209` -> `15.0`, `218` -> `10.0`, `225` -> `5.0`, `231` -> `0.0`, `237` -> `-5.0`, `241` -> `-10.0`, `245` -> `-15.0`, `247` -> `-20.0`, `250` -> `-25.0`, `251` -> `-30.0`, `255` -> `-40.0`
|
||||||
|
* **Fahrenheit (`MAT F` - Table 53):**
|
||||||
|
* `0` -> `392.0`, `12` -> `302.0`, `13` -> `293.0`, `14` -> `284.0`, `16` -> `275.0`, `18` -> `266.0`, `21` -> `257.0`, `23` -> `248.0`, `26` -> `239.0`, `30` -> `230.0`, `34` -> `221.0`, `39` -> `212.0`, `44` -> `203.0`, `50` -> `194.0`, `56` -> `185.0`, `64` -> `176.0`, `72` -> `167.0`, `81` -> `158.0`, `92` -> `149.0`, `102` -> `140.0`, `114` -> `131.0`, `126` -> `122.0`, `139` -> `113.0`, `152` -> `104.0`, `165` -> `95.0`, `177` -> `86.0`, `189` -> `77.0`, `199` -> `68.0`, `209` -> `59.0`, `218` -> `50.0`, `225` -> `41.0`, `231` -> `32.0`, `237` -> `23.0`, `241` -> `14.0`, `245` -> `5.0`, `247` -> `-4.0`, `250` -> `-13.0`, `251` -> `-22.0`, `255` -> `-40.0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Stored Fault Trouble Codes
|
||||||
|
|
||||||
|
Malfunction Indicator Codes are mapped as bit flags across three specific status bytes:
|
||||||
|
|
||||||
|
#### Codes Byte 1 (Payload Index 11 / Byte 12)
|
||||||
|
* **Bit 7:** Code 12 - Crank Sensor / System Check
|
||||||
|
* **Bit 6:** Code 13 - O2 Sensor
|
||||||
|
* **Bit 5:** Code 14 - Coolant High Temp
|
||||||
|
* **Bit 4:** Code 15 - Coolant Low Temp
|
||||||
|
* **Bit 3:** Code 21 - TPS Voltage High
|
||||||
|
* **Bit 2:** Code 22 - TPS Voltage Low
|
||||||
|
* **Bit 1:** Code 23 - MAT Voltage Low
|
||||||
|
* **Bit 0:** Code 24 - Vehicle Speed Sensor (VSS)
|
||||||
|
|
||||||
|
#### Codes Byte 2 (Payload Index 12 / Byte 13)
|
||||||
|
* **Bit 7:** Code 25 - MAT Voltage High
|
||||||
|
* **Bit 5:** Code 32 - EGR System
|
||||||
|
* **Bit 4:** Code 33 - MAP Sensor High
|
||||||
|
* **Bit 3:** Code 34 - MAP Sensor Low
|
||||||
|
* **Bit 2:** Code 35 - IAC Position
|
||||||
|
* **Bit 0:** Code 42 - Electronic Spark Timing (EST)
|
||||||
|
|
||||||
|
#### Codes Byte 3 (Payload Index 13 / Byte 14)
|
||||||
|
* **Bit 7:** Code 43 - Electronic Spark Control (Knock Sensor)
|
||||||
|
* **Bit 6:** Code 44 - O2 Sensor Lean Exhaust
|
||||||
|
* **Bit 5:** Code 45 - O2 Sensor Rich Exhaust
|
||||||
|
* **Bit 4:** Code 51 - PROM Error
|
||||||
|
* **Bit 3:** Code 52 - Cal-Pack Error
|
||||||
|
* **Bit 2:** Code 53 - System Battery Over-Voltage
|
||||||
|
* **Bit 0:** Code 55 - ADU Error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Engine Status & Bit Flags
|
||||||
|
|
||||||
|
#### Misc Byte 1 (Payload Index 14 / Byte 15)
|
||||||
|
* **Bit 1:** BLM Enabled (1 = Yes)
|
||||||
|
* **Bit 3:** Quasi Pulse Mode (1 = Yes)
|
||||||
|
* **Bit 4:** Async Pulse Mode (1 = Yes)
|
||||||
|
* **Bit 6:** Rich/Lean Status (1 = Rich, 0 = Lean)
|
||||||
|
* **Bit 7:** Loop Status (1 = Closed, 0 = Open)
|
||||||
|
|
||||||
|
#### Misc Byte 2 (Payload Index 15 / Byte 16)
|
||||||
|
* **Bit 5:** A/C Status (0 = Enabled, 1 = Disabled/Idle)
|
||||||
|
* **Bit 7:** Park/Neutral Switch (1 = Park/Neutral, 0 = In Gear)
|
||||||
|
|
||||||
|
#### Misc Byte 3 (Payload Index 16 / Byte 17)
|
||||||
|
* **Bit 0:** A/C Clutch Command (1 = Enabled)
|
||||||
|
* **Bit 2:** Torque Converter Clutch (TCC) (1 = Locked)
|
||||||
|
* **Bit 5:** Power Steering Cramp Switch (1 = Active)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App Features & Architecture
|
||||||
|
|
||||||
|
1. **Bluetooth Thread Management:**
|
||||||
|
* Queries for paired devices and connects directly to `"ESP32-ALDL"` over standard SPP RFCOMM sockets.
|
||||||
|
* Robust frame synchronization: buffers incoming streams and aligns on the double-byte header `0xAA 0x55`.
|
||||||
|
2. **State Management:**
|
||||||
|
* State flows from the Bluetooth thread through Kotlin `StateFlow` to Compose UI.
|
||||||
|
3. **Modern Dash Dashboard UI:**
|
||||||
|
* Dynamic animations for RPM and TPS sweeps.
|
||||||
|
* Metrics displayed in responsive tiles with custom indicator colors (e.g. engine loop states, fuel trims, battery health).
|
||||||
|
* Live trouble code alert panel.
|
||||||
|
* Diagnostic pane displaying real-time raw hex buffers for physical signal verification.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.esp32aldldashboard"
|
||||||
|
compileSdk = 36
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.example.esp32aldldashboard"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
aidl = false
|
||||||
|
buildConfig = false
|
||||||
|
shaders = false
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources {
|
||||||
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
val composeBom = platform(libs.androidx.compose.bom)
|
||||||
|
implementation(composeBom)
|
||||||
|
androidTestImplementation(composeBom)
|
||||||
|
|
||||||
|
// Core Android dependencies
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|
||||||
|
// Arch Components
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
|
|
||||||
|
// Compose
|
||||||
|
implementation(libs.androidx.compose.ui)
|
||||||
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
implementation(libs.androidx.compose.material3)
|
||||||
|
// Tooling
|
||||||
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
|
// Instrumented tests
|
||||||
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||||
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
||||||
|
// Local tests: jUnit, coroutines, Android runner
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
|
|
||||||
|
// Instrumented tests: jUnit rules and runners
|
||||||
|
androidTestImplementation(libs.androidx.test.core)
|
||||||
|
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||||
|
androidTestImplementation(libs.androidx.test.runner)
|
||||||
|
androidTestImplementation(libs.androidx.test.espresso.core)
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
implementation(libs.androidx.navigation3.ui)
|
||||||
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.esp32aldldashboard.ui.main
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/** UI tests for [com.example.esp32aldldashboard.ui.main.MainScreen]. */
|
||||||
|
class MainScreenTest {
|
||||||
|
|
||||||
|
@get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
composeTestRule.setContent { MainScreen(FAKE_DATA) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun firstItem_exists() {
|
||||||
|
FAKE_DATA.forEach { composeTestRule.onNodeWithText("Hello $it!").assertExists() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val FAKE_DATA = listOf("Sample1", "Sample2", "Sample3")
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- Legacy Bluetooth permissions for Android 11 and lower -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
<!-- Bluetooth permissions for Android 12 and higher -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
|
<!-- Location permission (needed for Bluetooth scanning on Android 11 and lower) -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:theme="@style/Theme.ESP32ALDLDashboard">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.example.esp32aldldashboard
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.example.esp32aldldashboard.theme.ESP32ALDLDashboardTheme
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
ESP32ALDLDashboardTheme { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { MainNavigation() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.example.esp32aldldashboard
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation3.runtime.entryProvider
|
||||||
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
|
import androidx.navigation3.ui.NavDisplay
|
||||||
|
import com.example.esp32aldldashboard.ui.main.MainScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainNavigation() {
|
||||||
|
val backStack = rememberNavBackStack(Main)
|
||||||
|
|
||||||
|
NavDisplay(
|
||||||
|
backStack = backStack,
|
||||||
|
onBack = { backStack.removeLastOrNull() },
|
||||||
|
entryProvider =
|
||||||
|
entryProvider {
|
||||||
|
entry<Main> {
|
||||||
|
MainScreen(onItemClick = { navKey -> backStack.add(navKey) }, modifier = Modifier.safeDrawingPadding().padding(16.dp))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.example.esp32aldldashboard
|
||||||
|
|
||||||
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable data object Main : NavKey
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package com.example.esp32aldldashboard.bluetooth
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.example.esp32aldldashboard.parser.ALDLFrame
|
||||||
|
import com.example.esp32aldldashboard.parser.ALDLParser
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
enum class ConnectionState {
|
||||||
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
class BluetoothService(private val context: Context) {
|
||||||
|
|
||||||
|
private val TAG = "ALDLBluetoothService"
|
||||||
|
private val SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
|
||||||
|
|
||||||
|
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
|
||||||
|
val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||||
|
|
||||||
|
private val _latestFrame = MutableStateFlow<ALDLFrame?>(null)
|
||||||
|
val latestFrame: StateFlow<ALDLFrame?> = _latestFrame
|
||||||
|
|
||||||
|
private val _rawHexLog = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
val rawHexLog: StateFlow<List<String>> = _rawHexLog
|
||||||
|
|
||||||
|
private val _errorMessage = MutableStateFlow("")
|
||||||
|
val errorMessage: StateFlow<String> = _errorMessage
|
||||||
|
|
||||||
|
private var connectionJob: Job? = null
|
||||||
|
private var socket: BluetoothSocket? = null
|
||||||
|
private var isConnected = false
|
||||||
|
private var isSimulating = false
|
||||||
|
|
||||||
|
private val bluetoothAdapter: BluetoothAdapter? by lazy {
|
||||||
|
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
|
manager.adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addRawHexLog(hex: String) {
|
||||||
|
val currentList = _rawHexLog.value.toMutableList()
|
||||||
|
if (currentList.size >= 100) {
|
||||||
|
currentList.removeAt(0)
|
||||||
|
}
|
||||||
|
currentList.add(hex)
|
||||||
|
_rawHexLog.value = currentList
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startSimulation() {
|
||||||
|
if (_connectionState.value == ConnectionState.CONNECTED) {
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
isSimulating = true
|
||||||
|
_connectionState.value = ConnectionState.CONNECTED
|
||||||
|
_errorMessage.value = ""
|
||||||
|
|
||||||
|
connectionJob = CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
var simStep = 0
|
||||||
|
val basePayload = byteArrayOf(
|
||||||
|
0x20.toByte(), 0x00.toByte(), 0x2A.toByte(), 0x5F.toByte(), 0x59.toByte(),
|
||||||
|
0x00.toByte(), 0xF4.toByte(), 0x00.toByte(), 0x1E.toByte(), 0x80.toByte(),
|
||||||
|
0x65.toByte(), 0x08.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
|
||||||
|
0x25.toByte(), 0x18.toByte(), 0x7D.toByte(), 0x80.toByte(), 0x00.toByte(),
|
||||||
|
0x00.toByte(), 0x00.toByte(), 0xC9.toByte(), 0x02.toByte(), 0x62.toByte()
|
||||||
|
)
|
||||||
|
|
||||||
|
while (isActive && isSimulating) {
|
||||||
|
// Generate dynamic simulation data to show moving values on UI
|
||||||
|
val payload = basePayload.clone()
|
||||||
|
simStep++
|
||||||
|
|
||||||
|
// Simulate Engine Speed (Index 7), Coolant Temp (Index 4), Speed (Index 5), TPS (Index 8)
|
||||||
|
val rpmRaw: Int
|
||||||
|
val coolantRaw: Int
|
||||||
|
val speedRaw: Int
|
||||||
|
val tpsRaw: Int
|
||||||
|
val bpwHighRaw: Int
|
||||||
|
val bpwLowRaw: Int
|
||||||
|
val mapRaw: Int
|
||||||
|
val o2MvRaw: Int
|
||||||
|
val codesByte1: Int
|
||||||
|
val miscByte1: Int
|
||||||
|
|
||||||
|
when (simStep % 4) {
|
||||||
|
0 -> { // Key On Engine Off (Prompt data)
|
||||||
|
rpmRaw = 0
|
||||||
|
coolantRaw = 89 // ~80F / 26C
|
||||||
|
speedRaw = 0
|
||||||
|
tpsRaw = 30 // ~0.58V
|
||||||
|
mapRaw = 244 // ~100 kPa (Atmospheric)
|
||||||
|
o2MvRaw = 101 // ~448 mV
|
||||||
|
bpwHighRaw = 0x02
|
||||||
|
bpwLowRaw = 0x62 // 610 dec = 9.3ms
|
||||||
|
codesByte1 = 0x08 // Code 21 Active (TPS High)
|
||||||
|
miscByte1 = 0x00 // Open loop
|
||||||
|
}
|
||||||
|
1 -> { // Cranking
|
||||||
|
rpmRaw = 8 // 200 RPM
|
||||||
|
coolantRaw = 90
|
||||||
|
speedRaw = 0
|
||||||
|
tpsRaw = 35 // ~0.68V
|
||||||
|
mapRaw = 220 // ~91 kPa
|
||||||
|
o2MvRaw = 110 // ~488 mV
|
||||||
|
bpwHighRaw = 0x04
|
||||||
|
bpwLowRaw = 0x10 // 1040 dec = 15.8ms
|
||||||
|
codesByte1 = 0x00
|
||||||
|
miscByte1 = 0x00
|
||||||
|
}
|
||||||
|
2 -> { // Idle (Warmup)
|
||||||
|
rpmRaw = 30 // 750 RPM
|
||||||
|
coolantRaw = 140 // ~149F / 65C
|
||||||
|
speedRaw = 0
|
||||||
|
tpsRaw = 28 // ~0.54V
|
||||||
|
mapRaw = 80 // ~39 kPa (Vacuum)
|
||||||
|
o2MvRaw = (120 + 80 * Math.sin(simStep.toDouble())).toInt() // oscillating O2
|
||||||
|
bpwHighRaw = 0x00
|
||||||
|
bpwLowRaw = 0xD0 // 208 dec = 3.1ms
|
||||||
|
codesByte1 = 0x00
|
||||||
|
miscByte1 = 0x82 // Closed Loop, BLM Enable
|
||||||
|
}
|
||||||
|
else -> { // Cruising
|
||||||
|
rpmRaw = 88 // 2200 RPM
|
||||||
|
coolantRaw = 180 // ~203F / 95C
|
||||||
|
speedRaw = 45 // 45 MPH
|
||||||
|
tpsRaw = 62 // ~1.2V
|
||||||
|
mapRaw = 140 // ~62 kPa
|
||||||
|
o2MvRaw = (120 + 100 * Math.sin(simStep.toDouble())).toInt()
|
||||||
|
bpwHighRaw = 0x01
|
||||||
|
bpwLowRaw = 0x20 // 288 dec = 4.4ms
|
||||||
|
codesByte1 = 0x00
|
||||||
|
miscByte1 = 0xC2 // Closed Loop, BLM Enable, Rich
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload[4] = coolantRaw.toByte()
|
||||||
|
payload[5] = speedRaw.toByte()
|
||||||
|
payload[6] = mapRaw.toByte()
|
||||||
|
payload[7] = rpmRaw.toByte()
|
||||||
|
payload[8] = tpsRaw.toByte()
|
||||||
|
payload[10] = o2MvRaw.toByte()
|
||||||
|
payload[11] = codesByte1.toByte()
|
||||||
|
payload[14] = miscByte1.toByte()
|
||||||
|
payload[23] = bpwHighRaw.toByte()
|
||||||
|
payload[24] = bpwLowRaw.toByte()
|
||||||
|
|
||||||
|
val parsed = ALDLParser.parseFrame(payload)
|
||||||
|
if (parsed != null) {
|
||||||
|
_latestFrame.value = parsed
|
||||||
|
val hexString = payload.joinToString(" ") { String.format("%02X", it) }
|
||||||
|
addRawHexLog("AA 55 $hexString (SIMULATED)")
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun connect() {
|
||||||
|
if (isSimulating) {
|
||||||
|
isSimulating = false
|
||||||
|
connectionJob?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_connectionState.value == ConnectionState.CONNECTED || _connectionState.value == ConnectionState.CONNECTING) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapter = bluetoothAdapter
|
||||||
|
if (adapter == null) {
|
||||||
|
_connectionState.value = ConnectionState.ERROR
|
||||||
|
_errorMessage.value = "Bluetooth is not supported on this device"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adapter.isEnabled) {
|
||||||
|
_connectionState.value = ConnectionState.ERROR
|
||||||
|
_errorMessage.value = "Bluetooth is turned off"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectionState.value = ConnectionState.CONNECTING
|
||||||
|
_errorMessage.value = ""
|
||||||
|
|
||||||
|
connectionJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val pairedDevices = adapter.bondedDevices
|
||||||
|
val targetDevice: BluetoothDevice? = pairedDevices.find { it.name == "ESP32-ALDL" }
|
||||||
|
|
||||||
|
if (targetDevice == null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_connectionState.value = ConnectionState.ERROR
|
||||||
|
_errorMessage.value = "Device named 'ESP32-ALDL' is not paired. Please pair in system settings first."
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = targetDevice.createRfcommSocketToServiceRecord(SPP_UUID)
|
||||||
|
adapter.cancelDiscovery() // cancel discovery to speed up connection
|
||||||
|
|
||||||
|
socket?.connect()
|
||||||
|
isConnected = true
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_connectionState.value = ConnectionState.CONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
readDataStream(socket!!.inputStream)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Connection failed: ${e.message}", e)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_connectionState.value = ConnectionState.ERROR
|
||||||
|
_errorMessage.value = e.message ?: "Failed to connect"
|
||||||
|
}
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun readDataStream(inputStream: InputStream) {
|
||||||
|
val readBuffer = ByteArray(128)
|
||||||
|
val syncBuffer = ArrayList<Byte>()
|
||||||
|
|
||||||
|
while (currentCoroutineContext().isActive && isConnected) {
|
||||||
|
try {
|
||||||
|
val bytesRead = withContext(Dispatchers.IO) {
|
||||||
|
inputStream.read(readBuffer)
|
||||||
|
}
|
||||||
|
if (bytesRead <= 0) {
|
||||||
|
break // Stream closed
|
||||||
|
}
|
||||||
|
|
||||||
|
for (j in 0 until bytesRead) {
|
||||||
|
syncBuffer.add(readBuffer[j])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for frame matches in buffer
|
||||||
|
while (syncBuffer.size >= 27) {
|
||||||
|
var foundHeader = false
|
||||||
|
for (idx in 0 until syncBuffer.size - 1) {
|
||||||
|
if ((syncBuffer[idx].toInt() and 0xFF) == 0xAA && (syncBuffer[idx + 1].toInt() and 0xFF) == 0x55) {
|
||||||
|
// Discard garbage preceding header
|
||||||
|
if (idx > 0) {
|
||||||
|
for (d in 0 until idx) {
|
||||||
|
syncBuffer.removeAt(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundHeader = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundHeader) {
|
||||||
|
if (syncBuffer.size >= 27) {
|
||||||
|
val payload = ByteArray(25)
|
||||||
|
for (p in 0 until 25) {
|
||||||
|
payload[p] = syncBuffer[p + 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the 27 bytes from buffer
|
||||||
|
for (r in 0 until 27) {
|
||||||
|
syncBuffer.removeAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsed = ALDLParser.parseFrame(payload)
|
||||||
|
if (parsed != null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_latestFrame.value = parsed
|
||||||
|
val hexString = payload.joinToString(" ") { String.format("%02X", it) }
|
||||||
|
addRawHexLog("AA 55 $hexString")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Header found, but waiting for full 27-byte frame
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Header sequence not found, purge all but last byte if it is part of a potential header
|
||||||
|
val lastByte = syncBuffer.last()
|
||||||
|
syncBuffer.clear()
|
||||||
|
if ((lastByte.toInt() and 0xFF) == 0xAA) {
|
||||||
|
syncBuffer.add(lastByte)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Read stream error: ${e.message}")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_connectionState.value = ConnectionState.ERROR
|
||||||
|
_errorMessage.value = "Connection lost"
|
||||||
|
}
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnect() {
|
||||||
|
isConnected = false
|
||||||
|
isSimulating = false
|
||||||
|
connectionJob?.cancel()
|
||||||
|
connectionJob = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket?.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Error closing socket: ${e.message}")
|
||||||
|
}
|
||||||
|
socket = null
|
||||||
|
|
||||||
|
_connectionState.value = ConnectionState.DISCONNECTED
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.example.esp32aldldashboard.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
interface DataRepository {
|
||||||
|
val data: Flow<List<String>>
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultDataRepository : DataRepository {
|
||||||
|
override val data: Flow<List<String>> = flow { emit(listOf("Android")) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package com.example.esp32aldldashboard.parser
|
||||||
|
|
||||||
|
data class ALDLFrame(
|
||||||
|
val rawBytes: ByteArray,
|
||||||
|
val iacPosition: Int,
|
||||||
|
val coolantTempC: Float,
|
||||||
|
val coolantTempF: Float,
|
||||||
|
val vehicleSpeedMPH: Int,
|
||||||
|
val mapVolts: Float,
|
||||||
|
val mapKpa: Float,
|
||||||
|
val engineSpeedRpm: Int,
|
||||||
|
val tpsVolts: Float,
|
||||||
|
val integrator: Int,
|
||||||
|
val o2SensorMv: Float,
|
||||||
|
val batteryVolts: Float,
|
||||||
|
val blm: Int,
|
||||||
|
val richLeanCrosses: Int,
|
||||||
|
val sparkAdvance: Float,
|
||||||
|
val egrDutyCycle: Float,
|
||||||
|
val matC: Float,
|
||||||
|
val matF: Float,
|
||||||
|
val bpwMs: Float,
|
||||||
|
val blmEnable: Boolean,
|
||||||
|
val quasiPulse: Boolean,
|
||||||
|
val asyncPulse: Boolean,
|
||||||
|
val isRich: Boolean,
|
||||||
|
val isClosedLoop: Boolean,
|
||||||
|
val isAcEnabled: Boolean,
|
||||||
|
val isParkNeutral: Boolean,
|
||||||
|
val isAcClutchEnabled: Boolean,
|
||||||
|
val isTccLocked: Boolean,
|
||||||
|
val isPowerSteeringCrampActive: Boolean,
|
||||||
|
val activeFaultCodes: List<Int>,
|
||||||
|
val timestamp: Long = System.currentTimeMillis()
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as ALDLFrame
|
||||||
|
return rawBytes.contentEquals(other.rawBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return rawBytes.contentHashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ALDLParser {
|
||||||
|
// MAT C interpolation table: key is raw value, value is Temp in C
|
||||||
|
private val matTableC = listOf(
|
||||||
|
0 to 200.0f,
|
||||||
|
12 to 150.0f,
|
||||||
|
13 to 145.0f,
|
||||||
|
14 to 140.0f,
|
||||||
|
16 to 135.0f,
|
||||||
|
18 to 130.0f,
|
||||||
|
21 to 125.0f,
|
||||||
|
23 to 120.0f,
|
||||||
|
26 to 115.0f,
|
||||||
|
30 to 110.0f,
|
||||||
|
34 to 105.0f,
|
||||||
|
39 to 100.0f,
|
||||||
|
44 to 95.0f,
|
||||||
|
50 to 90.0f,
|
||||||
|
56 to 85.0f,
|
||||||
|
64 to 80.0f,
|
||||||
|
72 to 75.0f,
|
||||||
|
81 to 70.0f,
|
||||||
|
92 to 65.0f,
|
||||||
|
102 to 60.0f,
|
||||||
|
114 to 55.0f,
|
||||||
|
126 to 50.0f,
|
||||||
|
139 to 45.0f,
|
||||||
|
152 to 40.0f,
|
||||||
|
165 to 35.0f,
|
||||||
|
177 to 30.0f,
|
||||||
|
189 to 25.0f,
|
||||||
|
199 to 20.0f,
|
||||||
|
209 to 15.0f,
|
||||||
|
218 to 10.0f,
|
||||||
|
225 to 5.0f,
|
||||||
|
231 to 0.0f,
|
||||||
|
237 to -5.0f,
|
||||||
|
241 to -10.0f,
|
||||||
|
245 to -15.0f,
|
||||||
|
247 to -20.0f,
|
||||||
|
250 to -25.0f,
|
||||||
|
251 to -30.0f,
|
||||||
|
255 to -40.0f
|
||||||
|
)
|
||||||
|
|
||||||
|
// MAT F interpolation table: key is raw value, value is Temp in F
|
||||||
|
private val matTableF = listOf(
|
||||||
|
0 to 392.0f,
|
||||||
|
12 to 302.0f,
|
||||||
|
13 to 293.0f,
|
||||||
|
14 to 284.0f,
|
||||||
|
16 to 275.0f,
|
||||||
|
18 to 266.0f,
|
||||||
|
21 to 257.0f,
|
||||||
|
23 to 248.0f,
|
||||||
|
26 to 239.0f,
|
||||||
|
30 to 230.0f,
|
||||||
|
34 to 221.0f,
|
||||||
|
39 to 212.0f,
|
||||||
|
44 to 203.0f,
|
||||||
|
50 to 194.0f,
|
||||||
|
56 to 185.0f,
|
||||||
|
64 to 176.0f,
|
||||||
|
72 to 167.0f,
|
||||||
|
81 to 158.0f,
|
||||||
|
92 to 149.0f,
|
||||||
|
102 to 140.0f,
|
||||||
|
114 to 131.0f,
|
||||||
|
126 to 122.0f,
|
||||||
|
139 to 113.0f,
|
||||||
|
152 to 104.0f,
|
||||||
|
165 to 95.0f,
|
||||||
|
177 to 86.0f,
|
||||||
|
189 to 77.0f,
|
||||||
|
199 to 68.0f,
|
||||||
|
209 to 59.0f,
|
||||||
|
218 to 50.0f,
|
||||||
|
225 to 41.0f,
|
||||||
|
231 to 32.0f,
|
||||||
|
237 to 23.0f,
|
||||||
|
241 to 14.0f,
|
||||||
|
245 to 5.0f,
|
||||||
|
247 to -4.0f,
|
||||||
|
250 to -13.0f,
|
||||||
|
251 to -22.0f,
|
||||||
|
255 to -40.0f
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun interpolate(raw: Int, table: List<Pair<Int, Float>>): Float {
|
||||||
|
if (raw <= table.first().first) return table.first().second
|
||||||
|
if (raw >= table.last().first) return table.last().second
|
||||||
|
for (i in 0 until table.size - 1) {
|
||||||
|
val current = table[i]
|
||||||
|
val next = table[i + 1]
|
||||||
|
if (raw >= current.first && raw <= next.first) {
|
||||||
|
val span = next.first - current.first
|
||||||
|
if (span == 0) return current.second
|
||||||
|
val t = (raw - current.first).toFloat() / span
|
||||||
|
return current.second + t * (next.second - current.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0f
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a 25-byte raw data payload.
|
||||||
|
*/
|
||||||
|
fun parseFrame(data: ByteArray): ALDLFrame? {
|
||||||
|
if (data.size != 25) return null
|
||||||
|
|
||||||
|
val u = IntArray(25) { data[it].toInt() and 0xFF }
|
||||||
|
|
||||||
|
// Mappings based on 1-indexed btByteNumber in 24-INT10.ads (index = byteNumber - 1)
|
||||||
|
val iacPosition = u[3] // Byte 4
|
||||||
|
val coolantTempC = u[4] * 0.75f - 40.0f // Byte 5
|
||||||
|
val coolantTempF = u[4] * 1.35f - 40.0f // Byte 5
|
||||||
|
val vehicleSpeedMPH = u[5] // Byte 6
|
||||||
|
val mapVolts = u[6] * 0.019608f // Byte 7
|
||||||
|
val mapKpa = u[6] * 0.369f + 10.354f // Byte 7
|
||||||
|
val engineSpeedRpm = u[7] * 25 // Byte 8
|
||||||
|
val tpsVolts = u[8] * 0.019608f // Byte 9
|
||||||
|
val integrator = u[9] // Byte 10
|
||||||
|
val o2SensorMv = u[10] * 4.44f // Byte 11
|
||||||
|
|
||||||
|
val codesByte1 = u[11] // Byte 12
|
||||||
|
val codesByte2 = u[12] // Byte 13
|
||||||
|
val codesByte3 = u[13] // Byte 14
|
||||||
|
val miscByte1 = u[14] // Byte 15
|
||||||
|
val miscByte2 = u[15] // Byte 16
|
||||||
|
val miscByte3 = u[16] // Byte 17
|
||||||
|
|
||||||
|
val batteryVolts = u[17] * 0.1f // Byte 18
|
||||||
|
val blm = u[18] // Byte 19
|
||||||
|
val richLeanCrosses = u[19] // Byte 20
|
||||||
|
val sparkAdvance = u[20] * 0.351563f // Byte 21
|
||||||
|
val egrDutyCycle = u[21] * 0.392157f // Byte 22
|
||||||
|
|
||||||
|
// MAT (Air Temp) Interpolation
|
||||||
|
val matC = interpolate(u[22], matTableC) // Byte 23
|
||||||
|
val matF = interpolate(u[22], matTableF) // Byte 23
|
||||||
|
|
||||||
|
// BPW (Base Pulse Width) 16-bit
|
||||||
|
val rawBpw = (u[23] shl 8) or u[24] // Byte 24 (High), Byte 25 (Low)
|
||||||
|
val bpwMs = rawBpw * 0.015259f
|
||||||
|
|
||||||
|
// Status Flags Decoding
|
||||||
|
// Misc Byte 1 (Byte 15)
|
||||||
|
val blmEnable = (miscByte1 and 0x02) != 0 // bit 1
|
||||||
|
val quasiPulse = (miscByte1 and 0x08) != 0 // bit 3
|
||||||
|
val asyncPulse = (miscByte1 and 0x10) != 0 // bit 4
|
||||||
|
val isRich = (miscByte1 and 0x40) != 0 // bit 6 (1=RICH, 0=LEAN)
|
||||||
|
val isClosedLoop = (miscByte1 and 0x80) != 0 // bit 7 (1=CLOSED, 0=OPEN)
|
||||||
|
|
||||||
|
// Misc Byte 2 (Byte 16)
|
||||||
|
val isAcEnabled = (miscByte2 and 0x20) == 0 // bit 5 (0=ENABLED, 1=DISABLED/IDLE)
|
||||||
|
val isParkNeutral = (miscByte2 and 0x80) != 0 // bit 7 (1=PARK/NEUTRAL, 0=IN GEAR)
|
||||||
|
|
||||||
|
// Misc Byte 3 (Byte 17)
|
||||||
|
val isAcClutchEnabled = (miscByte3 and 0x01) != 0 // bit 0 (1=ENABLED)
|
||||||
|
val isTccLocked = (miscByte3 and 0x04) != 0 // bit 2 (1=LOCKED)
|
||||||
|
val isPowerSteeringCrampActive = (miscByte3 and 0x20) != 0 // bit 5 (1=ACTIVE)
|
||||||
|
|
||||||
|
// Active Fault Codes list based on code bits
|
||||||
|
val activeCodes = mutableListOf<Int>()
|
||||||
|
// Byte 12
|
||||||
|
if ((codesByte1 and 0x80) != 0) activeCodes.add(12) // bit 7
|
||||||
|
if ((codesByte1 and 0x40) != 0) activeCodes.add(13) // bit 6
|
||||||
|
if ((codesByte1 and 0x20) != 0) activeCodes.add(14) // bit 5
|
||||||
|
if ((codesByte1 and 0x10) != 0) activeCodes.add(15) // bit 4
|
||||||
|
if ((codesByte1 and 0x08) != 0) activeCodes.add(21) // bit 3
|
||||||
|
if ((codesByte1 and 0x04) != 0) activeCodes.add(22) // bit 2
|
||||||
|
if ((codesByte1 and 0x02) != 0) activeCodes.add(23) // bit 1
|
||||||
|
if ((codesByte1 and 0x01) != 0) activeCodes.add(24) // bit 0
|
||||||
|
// Byte 13
|
||||||
|
if ((codesByte2 and 0x80) != 0) activeCodes.add(25) // bit 7
|
||||||
|
if ((codesByte2 and 0x20) != 0) activeCodes.add(32) // bit 5
|
||||||
|
if ((codesByte2 and 0x10) != 0) activeCodes.add(33) // bit 4
|
||||||
|
if ((codesByte2 and 0x08) != 0) activeCodes.add(34) // bit 3
|
||||||
|
if ((codesByte2 and 0x04) != 0) activeCodes.add(35) // bit 2
|
||||||
|
if ((codesByte2 and 0x01) != 0) activeCodes.add(42) // bit 0
|
||||||
|
// Byte 14
|
||||||
|
if ((codesByte3 and 0x80) != 0) activeCodes.add(43) // bit 7
|
||||||
|
if ((codesByte3 and 0x40) != 0) activeCodes.add(44) // bit 6
|
||||||
|
if ((codesByte3 and 0x20) != 0) activeCodes.add(45) // bit 5
|
||||||
|
if ((codesByte3 and 0x10) != 0) activeCodes.add(51) // bit 4
|
||||||
|
if ((codesByte3 and 0x08) != 0) activeCodes.add(52) // bit 3
|
||||||
|
if ((codesByte3 and 0x04) != 0) activeCodes.add(53) // bit 2
|
||||||
|
if ((codesByte3 and 0x01) != 0) activeCodes.add(55) // bit 0
|
||||||
|
|
||||||
|
return ALDLFrame(
|
||||||
|
rawBytes = data,
|
||||||
|
iacPosition = iacPosition,
|
||||||
|
coolantTempC = coolantTempC,
|
||||||
|
coolantTempF = coolantTempF,
|
||||||
|
vehicleSpeedMPH = vehicleSpeedMPH,
|
||||||
|
mapVolts = mapVolts,
|
||||||
|
mapKpa = mapKpa,
|
||||||
|
engineSpeedRpm = engineSpeedRpm,
|
||||||
|
tpsVolts = tpsVolts,
|
||||||
|
integrator = integrator,
|
||||||
|
o2SensorMv = o2SensorMv,
|
||||||
|
batteryVolts = batteryVolts,
|
||||||
|
blm = blm,
|
||||||
|
richLeanCrosses = richLeanCrosses,
|
||||||
|
sparkAdvance = sparkAdvance,
|
||||||
|
egrDutyCycle = egrDutyCycle,
|
||||||
|
matC = matC,
|
||||||
|
matF = matF,
|
||||||
|
bpwMs = bpwMs,
|
||||||
|
blmEnable = blmEnable,
|
||||||
|
quasiPulse = quasiPulse,
|
||||||
|
asyncPulse = asyncPulse,
|
||||||
|
isRich = isRich,
|
||||||
|
isClosedLoop = isClosedLoop,
|
||||||
|
isAcEnabled = isAcEnabled,
|
||||||
|
isParkNeutral = isParkNeutral,
|
||||||
|
isAcClutchEnabled = isAcClutchEnabled,
|
||||||
|
isTccLocked = isTccLocked,
|
||||||
|
isPowerSteeringCrampActive = isPowerSteeringCrampActive,
|
||||||
|
activeFaultCodes = activeCodes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.esp32aldldashboard.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val Purple80 = Color(0xFFD0BCFF)
|
||||||
|
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||||
|
val Pink80 = Color(0xFFEFB8C8)
|
||||||
|
|
||||||
|
val Purple40 = Color(0xFF6650a4)
|
||||||
|
val PurpleGrey40 = Color(0xFF625b71)
|
||||||
|
val Pink40 = Color(0xFF7D5260)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.example.esp32aldldashboard.theme
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80)
|
||||||
|
|
||||||
|
private val LightColorScheme =
|
||||||
|
lightColorScheme(
|
||||||
|
primary = Purple40,
|
||||||
|
secondary = PurpleGrey40,
|
||||||
|
tertiary = Pink40,
|
||||||
|
|
||||||
|
/* Other default colors to override
|
||||||
|
background = Color(0xFFFFFBFE),
|
||||||
|
surface = Color(0xFFFFFBFE),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onSecondary = Color.White,
|
||||||
|
onTertiary = Color.White,
|
||||||
|
onBackground = Color(0xFF1C1B1F),
|
||||||
|
onSurface = Color(0xFF1C1B1F),
|
||||||
|
*/
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ESP32ALDLDashboardTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
// Dynamic color is available on Android 12+
|
||||||
|
dynamicColor: Boolean = true,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val colorScheme =
|
||||||
|
when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
darkTheme -> DarkColorScheme
|
||||||
|
else -> LightColorScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.example.esp32aldldashboard.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
// Set of Material typography styles to start with
|
||||||
|
val Typography =
|
||||||
|
Typography(
|
||||||
|
bodyLarge =
|
||||||
|
TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp,
|
||||||
|
)
|
||||||
|
/* Other default text styles to override
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
||||||
@@ -0,0 +1,712 @@
|
|||||||
|
package com.example.esp32aldldashboard.ui.main
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import com.example.esp32aldldashboard.bluetooth.ConnectionState
|
||||||
|
import com.example.esp32aldldashboard.parser.ALDLFrame
|
||||||
|
|
||||||
|
// Theme Colors
|
||||||
|
private val DarkBg = Color(0xFF0F0F12)
|
||||||
|
private val CardBg = Color(0xFF1B1B22)
|
||||||
|
private val BorderColor = Color(0xFF2E2E38)
|
||||||
|
private val NeonCyan = Color(0xFF00E5FF)
|
||||||
|
private val NeonRed = Color(0xFFFF3D00)
|
||||||
|
private val NeonGreen = Color(0xFF00E676)
|
||||||
|
private val NeonOrange = Color(0xFFFF9100)
|
||||||
|
private val TextWhite = Color(0xFFEEEEEE)
|
||||||
|
private val TextMuted = Color(0xFF9E9EAF)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(
|
||||||
|
onItemClick: (NavKey) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val viewModel: MainScreenViewModel = viewModel { MainScreenViewModel(context) }
|
||||||
|
|
||||||
|
val connState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||||
|
val frame by viewModel.latestFrame.collectAsStateWithLifecycle()
|
||||||
|
val rawLog by viewModel.rawHexLog.collectAsStateWithLifecycle()
|
||||||
|
val errorMsg by viewModel.errorMessage.collectAsStateWithLifecycle()
|
||||||
|
val isCelsius by viewModel.isCelsius.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val permissionsLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissions ->
|
||||||
|
val allGranted = permissions.values.all { it }
|
||||||
|
if (allGranted) {
|
||||||
|
viewModel.connect()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Bluetooth and Location permissions are required to connect", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onConnectClick = {
|
||||||
|
val requiredPermissions = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allGranted = requiredPermissions.all { perm ->
|
||||||
|
ContextCompat.checkSelfPermission(context, perm) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allGranted) {
|
||||||
|
viewModel.connect()
|
||||||
|
} else {
|
||||||
|
permissionsLauncher.launch(requiredPermissions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MainScreenContent(
|
||||||
|
connState = connState,
|
||||||
|
frame = frame,
|
||||||
|
rawLog = rawLog,
|
||||||
|
errorMsg = errorMsg,
|
||||||
|
isCelsius = isCelsius,
|
||||||
|
onConnect = onConnectClick,
|
||||||
|
onDisconnect = { viewModel.disconnect() },
|
||||||
|
onSimulate = { viewModel.startSimulation() },
|
||||||
|
onToggleUnit = { viewModel.toggleTemperatureUnit() },
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainScreenContent(
|
||||||
|
connState: ConnectionState,
|
||||||
|
frame: ALDLFrame?,
|
||||||
|
rawLog: List<String>,
|
||||||
|
errorMsg: String,
|
||||||
|
isCelsius: Boolean,
|
||||||
|
onConnect: () -> Unit,
|
||||||
|
onDisconnect: () -> Unit,
|
||||||
|
onSimulate: () -> Unit,
|
||||||
|
onToggleUnit: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(DarkBg)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
// App Title Banner
|
||||||
|
Text(
|
||||||
|
text = "PONTIAC FIERO ALDL DASHBOARD",
|
||||||
|
color = NeonOrange,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
letterSpacing = 1.5.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Connection Action Card
|
||||||
|
ConnectionCard(
|
||||||
|
connState = connState,
|
||||||
|
errorMsg = errorMsg,
|
||||||
|
isCelsius = isCelsius,
|
||||||
|
onConnect = onConnect,
|
||||||
|
onDisconnect = onDisconnect,
|
||||||
|
onSimulate = onSimulate,
|
||||||
|
onToggleUnit = onToggleUnit
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Telemetry Panels
|
||||||
|
if (frame != null) {
|
||||||
|
// Trouble Codes Card (Flashing if active)
|
||||||
|
if (frame.activeFaultCodes.isNotEmpty()) {
|
||||||
|
TroubleCodesCard(activeCodes = frame.activeFaultCodes)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key Metrics
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
MetricCard(
|
||||||
|
title = "ENGINE SPEED",
|
||||||
|
value = "${frame.engineSpeedRpm}",
|
||||||
|
unit = "RPM",
|
||||||
|
progress = frame.engineSpeedRpm / 6000f,
|
||||||
|
progressColor = NeonCyan,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
MetricCard(
|
||||||
|
title = "VEHICLE SPEED",
|
||||||
|
value = "${frame.vehicleSpeedMPH}",
|
||||||
|
unit = "MPH",
|
||||||
|
progress = frame.vehicleSpeedMPH / 120f,
|
||||||
|
progressColor = NeonGreen,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Grid of Minor Telemetry Parameters
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Coolant and Intake Temp Row
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
val coolantVal = if (isCelsius) frame.coolantTempC else frame.coolantTempF
|
||||||
|
val coolantUnit = if (isCelsius) "°C" else "°F"
|
||||||
|
val coolantProgress = (coolantVal + 40) / 290f // normalized range
|
||||||
|
|
||||||
|
val matVal = if (isCelsius) frame.matC else frame.matF
|
||||||
|
val matProgress = (matVal + 40) / 290f
|
||||||
|
|
||||||
|
GridItemCard(
|
||||||
|
title = "COOLANT TEMP",
|
||||||
|
value = String.format("%.1f", coolantVal) + coolantUnit,
|
||||||
|
progress = coolantProgress,
|
||||||
|
progressColor = if (coolantVal > 210) NeonRed else NeonGreen,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
GridItemCard(
|
||||||
|
title = "MAT (AIR TEMP)",
|
||||||
|
value = String.format("%.1f", matVal) + coolantUnit,
|
||||||
|
progress = matProgress,
|
||||||
|
progressColor = NeonCyan,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuel control row
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
GridItemCard(
|
||||||
|
title = "BPW (INJECTOR)",
|
||||||
|
value = String.format("%.3f ms", frame.bpwMs),
|
||||||
|
progress = frame.bpwMs / 15f,
|
||||||
|
progressColor = NeonCyan,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
GridItemCard(
|
||||||
|
title = "O2 SENSOR",
|
||||||
|
value = "${frame.o2SensorMv.toInt()} mV",
|
||||||
|
progress = frame.o2SensorMv / 1000f,
|
||||||
|
progressColor = if (frame.isRich) NeonGreen else NeonOrange,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Air flow & Throttle Position
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
GridItemCard(
|
||||||
|
title = "TPS (THROTTLE)",
|
||||||
|
value = String.format("%.2f V", frame.tpsVolts),
|
||||||
|
progress = frame.tpsVolts / 5.0f,
|
||||||
|
progressColor = NeonGreen,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
GridItemCard(
|
||||||
|
title = "MAP (VACUUM)",
|
||||||
|
value = String.format("%.1f kPa", frame.mapKpa),
|
||||||
|
progress = frame.mapKpa / 105f,
|
||||||
|
progressColor = NeonCyan,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuel trims (BLM & INT) & Battery
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
GridItemCard(
|
||||||
|
title = "BLM / INT",
|
||||||
|
value = "${frame.blm} / ${frame.integrator}",
|
||||||
|
progress = frame.blm / 256f,
|
||||||
|
progressColor = if (frame.blm in 120..136) NeonGreen else NeonOrange,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
GridItemCard(
|
||||||
|
title = "BATTERY VOLTS",
|
||||||
|
value = String.format("%.1f V", frame.batteryVolts),
|
||||||
|
progress = (frame.batteryVolts - 8) / 8f,
|
||||||
|
progressColor = if (frame.batteryVolts < 12.0f) NeonRed else NeonGreen,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IAC, Spark & EGR
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
GridItemCard(
|
||||||
|
title = "IAC POSITION",
|
||||||
|
value = "${frame.iacPosition} Steps",
|
||||||
|
progress = frame.iacPosition / 160f,
|
||||||
|
progressColor = NeonCyan,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
GridItemCard(
|
||||||
|
title = "SPARK / EGR",
|
||||||
|
value = String.format("%.1f° / %.0f%%", frame.sparkAdvance, frame.egrDutyCycle),
|
||||||
|
progress = frame.egrDutyCycle / 100f,
|
||||||
|
progressColor = NeonCyan,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Status Badges Section
|
||||||
|
StatusFlagsPanel(frame = frame)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
} else {
|
||||||
|
// Empty / Waiting display
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(260.dp)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.background(CardBg, shape = RoundedCornerShape(12.dp))
|
||||||
|
.border(1.dp, BorderColor, shape = RoundedCornerShape(12.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Waiting for ALDL Stream data...\nSelect Connection or Simulation above.",
|
||||||
|
color = TextMuted,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live Diagnostic Console log
|
||||||
|
DiagnosticConsole(rawLog = rawLog)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConnectionCard(
|
||||||
|
connState: ConnectionState,
|
||||||
|
errorMsg: String,
|
||||||
|
isCelsius: Boolean,
|
||||||
|
onConnect: () -> Unit,
|
||||||
|
onDisconnect: () -> Unit,
|
||||||
|
onSimulate: () -> Unit,
|
||||||
|
onToggleUnit: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = CardBg),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.border(1.dp, BorderColor, shape = RoundedCornerShape(12.dp))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Status bar
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "STATUS: ",
|
||||||
|
color = TextMuted,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
val (statusText, statusColor) = when (connState) {
|
||||||
|
ConnectionState.DISCONNECTED -> "DISCONNECTED" to TextMuted
|
||||||
|
ConnectionState.CONNECTING -> "CONNECTING..." to NeonOrange
|
||||||
|
ConnectionState.CONNECTED -> "CONNECTED" to NeonGreen
|
||||||
|
ConnectionState.ERROR -> "ERROR" to NeonRed
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = statusText,
|
||||||
|
color = statusColor,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperature Toggle Button
|
||||||
|
Button(
|
||||||
|
onClick = onToggleUnit,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = BorderColor),
|
||||||
|
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
modifier = Modifier.height(28.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isCelsius) "USE °F" else "USE °C",
|
||||||
|
color = TextWhite,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMsg.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = errorMsg,
|
||||||
|
color = NeonRed,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(14.dp))
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
if (connState != ConnectionState.CONNECTED && connState != ConnectionState.CONNECTING) {
|
||||||
|
Button(
|
||||||
|
onClick = onConnect,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = NeonOrange),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text("CONNECT BT", fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = onDisconnect,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = BorderColor),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text("DISCONNECT", fontWeight = FontWeight.Bold, color = TextWhite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onSimulate,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = BorderColor),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text("SIMULATE", fontWeight = FontWeight.Bold, color = NeonCyan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MetricCard(
|
||||||
|
title: String,
|
||||||
|
value: String,
|
||||||
|
unit: String,
|
||||||
|
progress: Float,
|
||||||
|
progressColor: Color,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = CardBg),
|
||||||
|
modifier = modifier
|
||||||
|
.border(1.dp, BorderColor, shape = RoundedCornerShape(12.dp))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(14.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = TextMuted,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
color = TextWhite,
|
||||||
|
fontSize = 28.sp,
|
||||||
|
fontWeight = FontWeight.Black
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = unit,
|
||||||
|
color = TextMuted,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = progress.coerceIn(0f, 1f),
|
||||||
|
color = progressColor,
|
||||||
|
trackColor = BorderColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GridItemCard(
|
||||||
|
title: String,
|
||||||
|
value: String,
|
||||||
|
progress: Float,
|
||||||
|
progressColor: Color,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = CardBg),
|
||||||
|
modifier = modifier
|
||||||
|
.border(1.dp, BorderColor, shape = RoundedCornerShape(10.dp))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(10.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = TextMuted,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
color = TextWhite,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.ExtraBold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = progress.coerceIn(0f, 1f),
|
||||||
|
color = progressColor,
|
||||||
|
trackColor = BorderColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TroubleCodesCard(activeCodes: List<Int>) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = CardBg),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.border(1.dp, NeonRed, shape = RoundedCornerShape(12.dp))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "⚠️ ACTIVE ECM FAULT CODES",
|
||||||
|
color = NeonRed,
|
||||||
|
fontWeight = FontWeight.Black,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
activeCodes.forEach { code ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(NeonRed, shape = RoundedCornerShape(4.dp))
|
||||||
|
.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "CODE $code",
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Refer to Fiero shop manual for diagnosis.",
|
||||||
|
color = TextMuted,
|
||||||
|
fontSize = 11.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun StatusFlagsPanel(frame: ALDLFrame) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = CardBg),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.border(1.dp, BorderColor, shape = RoundedCornerShape(12.dp))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(14.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "LOOP STATUS & SYSTEM SWITCHES",
|
||||||
|
color = TextMuted,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
StatusBadge(label = "Closed Loop", active = frame.isClosedLoop, activeColor = NeonGreen)
|
||||||
|
StatusBadge(label = "Rich Mixture", active = frame.isRich, activeColor = NeonGreen)
|
||||||
|
StatusBadge(label = "BLM Enabled", active = frame.blmEnable, activeColor = NeonGreen)
|
||||||
|
StatusBadge(label = "TCC Locked", active = frame.isTccLocked, activeColor = NeonGreen)
|
||||||
|
StatusBadge(label = "AC Clutch", active = frame.isAcClutchEnabled, activeColor = NeonGreen)
|
||||||
|
StatusBadge(label = "Park/Neutral", active = frame.isParkNeutral, activeColor = NeonCyan)
|
||||||
|
StatusBadge(label = "A/C Request", active = frame.isAcEnabled, activeColor = NeonCyan)
|
||||||
|
StatusBadge(label = "PS Cramp", active = frame.isPowerSteeringCrampActive, activeColor = NeonOrange)
|
||||||
|
StatusBadge(label = "Async Pulse", active = frame.asyncPulse, activeColor = NeonOrange)
|
||||||
|
StatusBadge(label = "Quasi Pulse", active = frame.quasiPulse, activeColor = NeonOrange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StatusBadge(label: String, active: Boolean, activeColor: Color) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = if (active) activeColor.copy(alpha = 0.15f) else Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = if (active) activeColor else BorderColor,
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = if (active) activeColor else TextMuted,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DiagnosticConsole(rawLog: List<String>) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = CardBg),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.border(1.dp, BorderColor, shape = RoundedCornerShape(12.dp))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(14.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "DIAGNOSTIC TELEMETRY STREAM LOG",
|
||||||
|
color = TextMuted,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(100.dp)
|
||||||
|
.background(Color.Black, shape = RoundedCornerShape(6.dp))
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
if (rawLog.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Console Idle. Connect to view hex dump...",
|
||||||
|
color = TextMuted,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 10.sp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
rawLog.forEach { logLine ->
|
||||||
|
Text(
|
||||||
|
text = logLine,
|
||||||
|
color = NeonGreen,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 10.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.example.esp32aldldashboard.ui.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.example.esp32aldldashboard.bluetooth.BluetoothService
|
||||||
|
import com.example.esp32aldldashboard.bluetooth.ConnectionState
|
||||||
|
import com.example.esp32aldldashboard.parser.ALDLFrame
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
class MainScreenViewModel(context: Context) : ViewModel() {
|
||||||
|
private val bluetoothService = BluetoothService(context.applicationContext)
|
||||||
|
|
||||||
|
val connectionState: StateFlow<ConnectionState> = bluetoothService.connectionState
|
||||||
|
val latestFrame: StateFlow<ALDLFrame?> = bluetoothService.latestFrame
|
||||||
|
val rawHexLog: StateFlow<List<String>> = bluetoothService.rawHexLog
|
||||||
|
val errorMessage: StateFlow<String> = bluetoothService.errorMessage
|
||||||
|
|
||||||
|
private val _isCelsius = MutableStateFlow(false) // Default to Fahrenheit for standard 80s GM telemetry
|
||||||
|
val isCelsius: StateFlow<Boolean> = _isCelsius
|
||||||
|
|
||||||
|
fun toggleTemperatureUnit() {
|
||||||
|
_isCelsius.value = !_isCelsius.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connect() {
|
||||||
|
bluetoothService.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnect() {
|
||||||
|
bluetoothService.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startSimulation() {
|
||||||
|
bluetoothService.startSimulation()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
bluetoothService.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#121212"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group
|
||||||
|
android:scaleX="0.1735766"
|
||||||
|
android:scaleY="0.18666106"
|
||||||
|
android:translateX="21.42"
|
||||||
|
android:translateY="21.42">
|
||||||
|
<!-- Outer Border -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M188.2,41.2L88.1,0c0,0,100.2,346.8,99.7,348.7L288.4,0.4L188.2,41.2z M100.4,12.8l88.1,35.8l88.1-35.4l-88.5,301.9 C188.5,313.5,100.4,12.8,100.4,12.8z" />
|
||||||
|
<!-- Red Arrowhead Body -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#D32F2F"
|
||||||
|
android:pathData="M188.5,56.8l-78.7-31.6c0,0,78.7,265.6,78.3,267l79.1-266.7L188.5,56.8z" />
|
||||||
|
<!-- Inner White Star -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M187.8,126.8l-6.7-23.8l-36.6-6.1l37.3-5.3 l5.4-21.9l6.6,22.3l38.6,5.1l-39.3,5.4L187.8,126.8z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">ESP32 ALDL Dashboard</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.ESP32ALDLDashboard" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample backup rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/guide/topics/data/autobackup
|
||||||
|
for details.
|
||||||
|
Note: This file is ignored for devices older than API 31
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore
|
||||||
|
-->
|
||||||
|
<full-backup-content>
|
||||||
|
<!--
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
|
-->
|
||||||
|
</full-backup-content>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
for details.
|
||||||
|
-->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
-->
|
||||||
|
</cloud-backup>
|
||||||
|
<!--
|
||||||
|
<device-transfer>
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
</device-transfer>
|
||||||
|
-->
|
||||||
|
</data-extraction-rules>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.example.esp32aldldashboard
|
||||||
|
|
||||||
|
import com.example.esp32aldldashboard.parser.ALDLParser
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ALDLParserTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testParseSampleFrame() {
|
||||||
|
// Sample hex stream from user request:
|
||||||
|
// AA 55 20 00 2A 5F 59 00 F4 00 1E 80 65 08 00 00 00 25 18 7D 80 00 00 00 C9 02 62
|
||||||
|
// AA 55 is the header, followed by 25 bytes of payload.
|
||||||
|
val rawPayload = byteArrayOf(
|
||||||
|
0x20.toByte(), 0x00.toByte(), 0x2A.toByte(), 0x5F.toByte(), 0x59.toByte(),
|
||||||
|
0x00.toByte(), 0xF4.toByte(), 0x00.toByte(), 0x1E.toByte(), 0x80.toByte(),
|
||||||
|
0x65.toByte(), 0x08.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
|
||||||
|
0x25.toByte(), 0x18.toByte(), 0x7D.toByte(), 0x80.toByte(), 0x00.toByte(),
|
||||||
|
0x00.toByte(), 0x00.toByte(), 0xC9.toByte(), 0x02.toByte(), 0x62.toByte()
|
||||||
|
)
|
||||||
|
|
||||||
|
val frame = ALDLParser.parseFrame(rawPayload)
|
||||||
|
assertNotNull(frame)
|
||||||
|
frame!!
|
||||||
|
|
||||||
|
// Assert values based on 24-INT10.ads specifications:
|
||||||
|
assertEquals(95, frame.iacPosition) // u[3] (Byte 4)
|
||||||
|
|
||||||
|
// coolantTemp = 89 * 0.75 - 40 = 26.75
|
||||||
|
assertEquals(26.75f, frame.coolantTempC, 0.001f)
|
||||||
|
assertEquals(80.15f, frame.coolantTempF, 0.001f)
|
||||||
|
|
||||||
|
assertEquals(0, frame.vehicleSpeedMPH) // u[5] (Byte 6)
|
||||||
|
|
||||||
|
// MAP: u[6] = 244
|
||||||
|
assertEquals(244 * 0.019608f, frame.mapVolts, 0.001f)
|
||||||
|
assertEquals(244 * 0.369f + 10.354f, frame.mapKpa, 0.001f)
|
||||||
|
|
||||||
|
assertEquals(0, frame.engineSpeedRpm) // u[7] (Byte 8)
|
||||||
|
|
||||||
|
// TPS: u[8] = 30
|
||||||
|
assertEquals(30 * 0.019608f, frame.tpsVolts, 0.001f)
|
||||||
|
|
||||||
|
assertEquals(128, frame.integrator) // u[9] (Byte 10)
|
||||||
|
assertEquals(101 * 4.44f, frame.o2SensorMv, 0.001f) // u[10] (Byte 11)
|
||||||
|
|
||||||
|
assertEquals(12.5f, frame.batteryVolts, 0.001f) // u[17] (Byte 18)
|
||||||
|
assertEquals(128, frame.blm) // u[18] (Byte 19)
|
||||||
|
assertEquals(0, frame.richLeanCrosses) // u[19] (Byte 20)
|
||||||
|
assertEquals(0f, frame.sparkAdvance, 0.001f) // u[20] (Byte 21)
|
||||||
|
assertEquals(0f, frame.egrDutyCycle, 0.001f) // u[21] (Byte 22)
|
||||||
|
|
||||||
|
// MAT Temp: u[22] = 201 (decimal)
|
||||||
|
// Table 52 interpolation:
|
||||||
|
// Key 199 -> 20.0 C
|
||||||
|
// Key 209 -> 15.0 C
|
||||||
|
// value = 20.0 - (201 - 199) / 10.0 * 5.0 = 19.0 C
|
||||||
|
assertEquals(19.0f, frame.matC, 0.001f)
|
||||||
|
assertEquals(66.2f, frame.matF, 0.001f)
|
||||||
|
|
||||||
|
// BPW: u[23]=0x02, u[24]=0x62 => 0x0262 = 610 decimal
|
||||||
|
// bpwMs = 610 * 0.015259 = 9.30799
|
||||||
|
assertEquals(610 * 0.015259f, frame.bpwMs, 0.001f)
|
||||||
|
|
||||||
|
// Active fault codes check (0x08 on codesByte1 = Code 21 active)
|
||||||
|
assertTrue(frame.activeFaultCodes.contains(21))
|
||||||
|
assertEquals(1, frame.activeFaultCodes.size)
|
||||||
|
|
||||||
|
// Misc flags
|
||||||
|
assertFalse(frame.isClosedLoop) // bit 7 of u[14] is 0
|
||||||
|
assertFalse(frame.isRich) // bit 6 of u[14] is 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.example.esp32aldldashboard.ui.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import com.example.esp32aldldashboard.bluetooth.ConnectionState
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import junit.framework.TestCase.assertFalse
|
||||||
|
import junit.framework.TestCase.assertTrue
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MainScreenViewModelTest {
|
||||||
|
|
||||||
|
private class FakeContext : ContextWrapper(null) {
|
||||||
|
override fun getApplicationContext(): Context {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
override fun getSystemService(name: String): Any? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInitialStates() = runTest {
|
||||||
|
val context = FakeContext()
|
||||||
|
val viewModel = MainScreenViewModel(context)
|
||||||
|
|
||||||
|
assertEquals(ConnectionState.DISCONNECTED, viewModel.connectionState.value)
|
||||||
|
assertEquals(null, viewModel.latestFrame.value)
|
||||||
|
assertFalse(viewModel.isCelsius.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testToggleTemperatureUnit() = runTest {
|
||||||
|
val context = FakeContext()
|
||||||
|
val viewModel = MainScreenViewModel(context)
|
||||||
|
|
||||||
|
assertFalse(viewModel.isCelsius.value)
|
||||||
|
viewModel.toggleTemperatureUnit()
|
||||||
|
assertTrue(viewModel.isCelsius.value)
|
||||||
|
viewModel.toggleTemperatureUnit()
|
||||||
|
assertFalse(viewModel.isCelsius.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.compose.compiler) apply false
|
||||||
|
alias(libs.plugins.kotlin.serialization) apply false
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# Enables Gradle Build Cache.
|
||||||
|
# See https://docs.gradle.org/current/userguide/build_cache.html
|
||||||
|
org.gradle.caching=true
|
||||||
|
# Enables Gradle Configuration Cache, the preferred Gradle execution mode.
|
||||||
|
# See https://docs.gradle.org/current/userguide/configuration_cache.html
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#This file is generated by updateDaemonJvm
|
||||||
|
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||||
|
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||||
|
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||||
|
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||||
|
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
|
||||||
|
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
|
||||||
|
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||||
|
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||||
|
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
|
||||||
|
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
|
||||||
|
toolchainVendor=JETBRAINS
|
||||||
|
toolchainVersion=21
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
[versions]
|
||||||
|
androidGradlePlugin = "9.2.1"
|
||||||
|
androidxCore = "1.18.0"
|
||||||
|
androidxLifecycle = "2.10.0"
|
||||||
|
androidxActivity = "1.13.0"
|
||||||
|
androidxComposeBom = "2026.03.01"
|
||||||
|
androidxTest = "1.7.0"
|
||||||
|
androidxTestExt = "1.3.0"
|
||||||
|
androidxTestRunner = "1.7.0"
|
||||||
|
androidxTestEspresso = "3.7.0"
|
||||||
|
coroutines = "1.10.2"
|
||||||
|
junit = "4.13.2"
|
||||||
|
kotlin = "2.3.20"
|
||||||
|
nav3Core = "1.0.1"
|
||||||
|
lifecycleViewmodelNav3 = "2.10.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
|
||||||
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
|
||||||
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
|
||||||
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3"}
|
||||||
|
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui"}
|
||||||
|
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview"}
|
||||||
|
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4"}
|
||||||
|
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling"}
|
||||||
|
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest"}
|
||||||
|
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
|
||||||
|
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
|
||||||
|
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
|
||||||
|
androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTest" }
|
||||||
|
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExt" }
|
||||||
|
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspresso" }
|
||||||
|
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" }
|
||||||
|
junit = { module = "junit:junit", version.ref = "junit" }
|
||||||
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||||
|
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
|
||||||
|
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
|
||||||
|
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||||
|
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "ESP32 ALDL Dashboard"
|
||||||
|
include(":app")
|
||||||