feat: initialize Android project structure with Gradle and basic UI/data architecture

This commit is contained in:
2026-06-11 22:41:02 +01:00
parent 70ae9230ca
commit f8f3be36ee
54 changed files with 3961 additions and 3 deletions
+15
View File
@@ -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
Generated
+1
View File
@@ -0,0 +1 @@
ESP32 ALDL Dashboard
+123
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
+11
View File
@@ -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>
+6 -1
View File
@@ -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 -1
View File
@@ -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">
+1261
View File
File diff suppressed because it is too large Load Diff
+119 -1
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
/build
+84
View File
@@ -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")
+35
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">ESP32 ALDL Dashboard</string>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.ESP32ALDLDashboard" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
+13
View File
@@ -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)
}
}
+6
View File
@@ -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
}
+29
View File
@@ -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
+13
View File
@@ -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
+43
View File
@@ -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" }
Binary file not shown.
+7
View File
@@ -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
Vendored Executable
+172
View File
@@ -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" "$@"
Vendored
+84
View File
@@ -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
+33
View File
@@ -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")