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>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<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>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<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" />
|
||||
</component>
|
||||
<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")
|
||||