diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..47b3928
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+ESP32 ALDL Dashboard
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..7643783
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..ca16a99
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index b838237..cdbc250 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -5,7 +5,12 @@
-
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 3aec57f..3b0be22 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/24-INT10.ads b/24-INT10.ads
new file mode 100644
index 0000000..3adef74
--- /dev/null
+++ b/24-INT10.ads
@@ -0,0 +1,1261 @@
+//--------------------------------------------------------------------------------------
+// Created by TunerPro. Hand editing is *not* recommended or supported.
+//--------------------------------------------------------------------------------------
+
+
+//--------------------------------------------------------------------------------------
+//--------------------------------- HEADER ------------------------------------
+//--------------------------------------------------------------------------------------
+
+{
+ fDefFrmtVers =1.21;
+ strDefVersion =Version 1.0;
+ strDefTitle =A29 $24;
+ strAuthor =Robert Saar;
+ strEngine =2.8MPFIHO L44;
+ strYear =1985-1988;
+ strVINCode =9;
+ strCodeMask =$24 $24A;
+ strComments =use 10K resistor. questions comments should be directed to robertisaar@yahoo.com;
+ iBaud =160;
+ dwFlags =0x00000000;
+ dwCSID =0x0000BCE4;
+ btNumDumpRequests =1;
+
+ strCommandName =Data Transfer;
+ rgbtCommand =;
+ iTotalBytesInCommand =0;
+ bChecksumCommand =0;
+ iNumBytesInPayload =25;
+ iNumBytesBeforePayload =-1;
+ bMaster =1;
+ bMonitor =1;
+ iChainTo =-1;
+}
+
+//--------------------------------------------------------------------------------------
+//---------------------------------- DASH -------------------------------------
+//--------------------------------------------------------------------------------------
+
+{
+ dwItemType =6;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =0;
+
+ btNumGauges =6;
+ strIDsDisplayed =0,0,0,0,0,0,;
+ btNumMonitors =4;
+ strMonsDisplayed =0,0,0,0,;
+}
+
+//--------------------------------------------------------------------------------------
+//--------------------------------- VALUES ------------------------------------
+//--------------------------------------------------------------------------------------
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =1;
+ bVisible =1;
+ dwUniqueID =54;
+
+ btByteNumber =0;
+ btMessageNumber =0;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle ======Important=====;
+ strUnitLabel =Units;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =4;
+
+ btByteNumber =6;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =3;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle =Vehicle Speed;
+ strUnitLabel =MPH;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =7;
+
+ btByteNumber =8;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =25.000000;
+ dOffset =0.000000;
+ strItemTitle =Engine Speed;
+ strUnitLabel =RPM;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =8;
+
+ btByteNumber =9;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =0.019608;
+ dOffset =0.000000;
+ strItemTitle =TPS;
+ strUnitLabel =Volts;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =1;
+ bVisible =1;
+ dwUniqueID =55;
+
+ btByteNumber =0;
+ btMessageNumber =0;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle ======Temps=====;
+ strUnitLabel =Units;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =2;
+
+ btByteNumber =5;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =0.750000;
+ dOffset =-40.000000;
+ strItemTitle =Coolant Temp;
+ strUnitLabel =C;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =3;
+
+ btByteNumber =5;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =1.350000;
+ dOffset =-40.000000;
+ strItemTitle =Coolant Temp;
+ strUnitLabel =F;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =49;
+
+ btByteNumber =23;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =6;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle =MAT;
+ strUnitLabel =C;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =52;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =50;
+
+ btByteNumber =23;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =6;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle =MAT;
+ strUnitLabel =F;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =53;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =1;
+ bVisible =1;
+ dwUniqueID =56;
+
+ btByteNumber =0;
+ btMessageNumber =0;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle ======Air/Fuel=====;
+ strUnitLabel =Units;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =5;
+
+ btByteNumber =7;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =0.019608;
+ dOffset =0.000000;
+ strItemTitle =MAP;
+ strUnitLabel =Volts;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =6;
+
+ btByteNumber =7;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =0.369000;
+ dOffset =10.354000;
+ strItemTitle =MAP;
+ strUnitLabel =kPa;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =45;
+
+ btByteNumber =19;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =3;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle =BLM;
+ strUnitLabel =;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =9;
+
+ btByteNumber =10;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =3;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle =INT;
+ strUnitLabel =;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =10;
+
+ btByteNumber =11;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =4.440000;
+ dOffset =0.000000;
+ strItemTitle =O2 Sensor;
+ strUnitLabel =mV;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =46;
+
+ btByteNumber =20;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =3;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle =Rich/Lean Transitions;
+ strUnitLabel =Crosses;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =51;
+
+ btByteNumber =24;
+ btMessageNumber =1;
+ dwItemSizeBits =16;
+ dwOperation =0;
+ dFactor =0.015259;
+ dOffset =0.000000;
+ strItemTitle =BPW;
+ strUnitLabel =mS;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =65536;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =1;
+ bVisible =1;
+ dwUniqueID =57;
+
+ btByteNumber =0;
+ btMessageNumber =0;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle ======Misc=====;
+ strUnitLabel =Units;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =48;
+
+ btByteNumber =22;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =0.392157;
+ dOffset =0.000000;
+ strItemTitle =EGR Duty Cycle;
+ strUnitLabel =%;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =47;
+
+ btByteNumber =21;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =0.351563;
+ dOffset =0.000000;
+ strItemTitle =Spark Advance;
+ strUnitLabel =*;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =44;
+
+ btByteNumber =18;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =0;
+ dFactor =0.100000;
+ dOffset =0.000000;
+ strItemTitle =Battery Voltage;
+ strUnitLabel =Volts;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+{
+ dwItemType =1;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =1;
+
+ btByteNumber =4;
+ btMessageNumber =1;
+ dwItemSizeBits =8;
+ dwOperation =3;
+ dFactor =1.000000;
+ dOffset =0.000000;
+ strItemTitle =IAC Position;
+ strUnitLabel =Steps;
+ dwAlarmHigh =255;
+ bAlarmHighENable =0;
+ dwAlarmLow =0;
+ bAlarmLowEnable =0;
+ iRangeHigh =255;
+ iRangeLow =0;
+ iLookupTableIndex =-1;
+}
+
+//--------------------------------------------------------------------------------------
+//---------------------------------- BITS -------------------------------------
+//--------------------------------------------------------------------------------------
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =1;
+ bVisible =1;
+ dwUniqueID =11;
+
+ btByteNumber =0;
+ btMessageNumber =0;
+ btBitNumber =0;
+ strItemTitle ======Codes=====;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =1;
+ strBitClearTitle =0;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =19;
+
+ btByteNumber =12;
+ btMessageNumber =1;
+ btBitNumber =7;
+ strItemTitle =12 Crank Sensor/System Check;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =18;
+
+ btByteNumber =12;
+ btMessageNumber =1;
+ btBitNumber =6;
+ strItemTitle =13 O2 Sensor;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =17;
+
+ btByteNumber =12;
+ btMessageNumber =1;
+ btBitNumber =5;
+ strItemTitle =14 Coolant High;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =16;
+
+ btByteNumber =12;
+ btMessageNumber =1;
+ btBitNumber =4;
+ strItemTitle =15 Coolant Low;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =15;
+
+ btByteNumber =12;
+ btMessageNumber =1;
+ btBitNumber =3;
+ strItemTitle =21 TPS High;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =14;
+
+ btByteNumber =12;
+ btMessageNumber =1;
+ btBitNumber =2;
+ strItemTitle =22 TPS Low;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =13;
+
+ btByteNumber =12;
+ btMessageNumber =1;
+ btBitNumber =1;
+ strItemTitle =23 MAT Low;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =12;
+
+ btByteNumber =12;
+ btMessageNumber =1;
+ btBitNumber =0;
+ strItemTitle =24 VSS;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =25;
+
+ btByteNumber =13;
+ btMessageNumber =1;
+ btBitNumber =7;
+ strItemTitle =25 MAT High;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =24;
+
+ btByteNumber =13;
+ btMessageNumber =1;
+ btBitNumber =5;
+ strItemTitle =32 EGR;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =23;
+
+ btByteNumber =13;
+ btMessageNumber =1;
+ btBitNumber =4;
+ strItemTitle =33 MAP High;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =22;
+
+ btByteNumber =13;
+ btMessageNumber =1;
+ btBitNumber =3;
+ strItemTitle =34 MAP Low;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =21;
+
+ btByteNumber =13;
+ btMessageNumber =1;
+ btBitNumber =2;
+ strItemTitle =35 IAC;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =20;
+
+ btByteNumber =13;
+ btMessageNumber =1;
+ btBitNumber =0;
+ strItemTitle =42 EST;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =32;
+
+ btByteNumber =14;
+ btMessageNumber =1;
+ btBitNumber =7;
+ strItemTitle =43 Knock Sensor;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =31;
+
+ btByteNumber =14;
+ btMessageNumber =1;
+ btBitNumber =6;
+ strItemTitle =44 O2 Lean;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =30;
+
+ btByteNumber =14;
+ btMessageNumber =1;
+ btBitNumber =5;
+ strItemTitle =45 O2 Rich;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =29;
+
+ btByteNumber =14;
+ btMessageNumber =1;
+ btBitNumber =4;
+ strItemTitle =51 PROM;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =28;
+
+ btByteNumber =14;
+ btMessageNumber =1;
+ btBitNumber =3;
+ strItemTitle =52 CAL-PACK;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =27;
+
+ btByteNumber =14;
+ btMessageNumber =1;
+ btBitNumber =2;
+ strItemTitle =53 Battery Voltage High;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =26;
+
+ btByteNumber =14;
+ btMessageNumber =1;
+ btBitNumber =0;
+ strItemTitle =55 ADU;
+ bAlarmSetEnable =1;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ERROR;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =1;
+ bVisible =1;
+ dwUniqueID =33;
+
+ btByteNumber =0;
+ btMessageNumber =0;
+ btBitNumber =0;
+ strItemTitle ======Misc=====;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =1;
+ strBitClearTitle =0;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =34;
+
+ btByteNumber =15;
+ btMessageNumber =1;
+ btBitNumber =1;
+ strItemTitle =BLM Enable;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =YES;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =35;
+
+ btByteNumber =15;
+ btMessageNumber =1;
+ btBitNumber =3;
+ strItemTitle =Quasi Pulse;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =YES;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =36;
+
+ btByteNumber =15;
+ btMessageNumber =1;
+ btBitNumber =4;
+ strItemTitle =Async Pulse;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =YES;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =37;
+
+ btByteNumber =15;
+ btMessageNumber =1;
+ btBitNumber =6;
+ strItemTitle =Rich/Lean;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =RICH;
+ strBitClearTitle =LEAN;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =38;
+
+ btByteNumber =15;
+ btMessageNumber =1;
+ btBitNumber =7;
+ strItemTitle =Loop Status;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =1;
+ strBitSetTitle =CLOSED;
+ strBitClearTitle =OPEN;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =39;
+
+ btByteNumber =16;
+ btMessageNumber =1;
+ btBitNumber =5;
+ strItemTitle =A/C;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =;
+ strBitClearTitle =ENABLED;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =40;
+
+ btByteNumber =16;
+ btMessageNumber =1;
+ btBitNumber =7;
+ strItemTitle =P/N Switch;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =PARK/NEUTRAL;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =41;
+
+ btByteNumber =17;
+ btMessageNumber =1;
+ btBitNumber =0;
+ strItemTitle =A/C Clutch;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ENABLED;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =42;
+
+ btByteNumber =17;
+ btMessageNumber =1;
+ btBitNumber =2;
+ strItemTitle =TCC;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =LOCKED;
+ strBitClearTitle =;
+}
+
+{
+ dwItemType =2;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =43;
+
+ btByteNumber =17;
+ btMessageNumber =1;
+ btBitNumber =5;
+ strItemTitle =Power Steering Cramp;
+ bAlarmSetEnable =0;
+ bAlarmNotSetEnable =0;
+ strBitSetTitle =ACTIVE;
+ strBitClearTitle =;
+}
+
+//--------------------------------------------------------------------------------------
+//---------------------------- LOOKUP TABLES ----------------------------------
+//--------------------------------------------------------------------------------------
+
+{
+ dwItemType =5;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =52;
+
+ btDataType =2;
+ wTableSize =256;
+ wIndexSize =4;
+ strTableName =MAT C;
+ dwReserved =0;
+ dwReserved =0;
+ pbtData =0, 200.00
+ 12, 150.00
+ 13, 145.00
+ 14, 140.00
+ 16, 135.00
+ 18, 130.00
+ 21, 125.00
+ 23, 120.00
+ 26, 115.00
+ 30, 110.00
+ 34, 105.00
+ 39, 100.00
+ 44, 95.00
+ 50, 90.00
+ 56, 85.00
+ 64, 80.00
+ 72, 75.00
+ 81, 70.00
+ 92, 65.00
+ 102, 60.00
+ 114, 55.00
+ 126, 50.00
+ 139, 45.00
+ 152, 40.00
+ 165, 35.00
+ 177, 30.00
+ 189, 25.00
+ 199, 20.00
+ 209, 15.00
+ 218, 10.00
+ 225, 5.00
+ 231, 0.00
+ 237, -5.00
+ 241, -10.00
+ 245, -15.00
+ 247, -20.00
+ 250, -25.00
+ 251, -30.00
+ 255, -40.00;
+}
+
+{
+ dwItemType =5;
+ strItemComments =;
+ bSeparator =0;
+ bVisible =1;
+ dwUniqueID =53;
+
+ btDataType =2;
+ wTableSize =256;
+ wIndexSize =4;
+ strTableName =MAT F;
+ dwReserved =0;
+ dwReserved =0;
+ pbtData =0, 392.00
+ 12, 302.00
+ 13, 293.00
+ 14, 284.00
+ 16, 275.00
+ 18, 266.00
+ 21, 257.00
+ 23, 248.00
+ 26, 239.00
+ 30, 230.00
+ 34, 221.00
+ 39, 212.00
+ 44, 203.00
+ 50, 194.00
+ 56, 185.00
+ 64, 176.00
+ 72, 167.00
+ 81, 158.00
+ 92, 149.00
+ 102, 140.00
+ 114, 131.00
+ 126, 122.00
+ 139, 113.00
+ 152, 104.00
+ 165, 95.00
+ 177, 86.00
+ 189, 77.00
+ 199, 68.00
+ 209, 59.00
+ 218, 50.00
+ 225, 41.00
+ 231, 32.00
+ 237, 23.00
+ 241, 14.00
+ 245, 5.00
+ 247, -4.00
+ 250, -13.00
+ 251, -22.00
+ 255, -40.00;
+}
+
diff --git a/README.md b/README.md
index 0519ecb..6e2034c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,119 @@
-
\ No newline at end of file
+# 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$
$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$
$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$
$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.
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..0cf9aef
--- /dev/null
+++ b/app/build.gradle.kts
@@ -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)
+}
diff --git a/app/src/androidTest/java/com/example/esp32aldldashboard/ui/main/MainScreenTest.kt b/app/src/androidTest/java/com/example/esp32aldldashboard/ui/main/MainScreenTest.kt
new file mode 100644
index 0000000..de4066a
--- /dev/null
+++ b/app/src/androidTest/java/com/example/esp32aldldashboard/ui/main/MainScreenTest.kt
@@ -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()
+
+ @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")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..86887e4
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/example/esp32aldldashboard/MainActivity.kt b/app/src/main/java/com/example/esp32aldldashboard/MainActivity.kt
new file mode 100644
index 0000000..683206d
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/MainActivity.kt
@@ -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() } }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/esp32aldldashboard/Navigation.kt b/app/src/main/java/com/example/esp32aldldashboard/Navigation.kt
new file mode 100644
index 0000000..757ef4b
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/Navigation.kt
@@ -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 {
+ MainScreen(onItemClick = { navKey -> backStack.add(navKey) }, modifier = Modifier.safeDrawingPadding().padding(16.dp))
+ }
+ },
+ )
+}
diff --git a/app/src/main/java/com/example/esp32aldldashboard/NavigationKeys.kt b/app/src/main/java/com/example/esp32aldldashboard/NavigationKeys.kt
new file mode 100644
index 0000000..9ab7bd6
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/NavigationKeys.kt
@@ -0,0 +1,6 @@
+package com.example.esp32aldldashboard
+
+import androidx.navigation3.runtime.NavKey
+import kotlinx.serialization.Serializable
+
+@Serializable data object Main : NavKey
diff --git a/app/src/main/java/com/example/esp32aldldashboard/bluetooth/BluetoothService.kt b/app/src/main/java/com/example/esp32aldldashboard/bluetooth/BluetoothService.kt
new file mode 100644
index 0000000..b47cd23
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/bluetooth/BluetoothService.kt
@@ -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
+
+ private val _latestFrame = MutableStateFlow(null)
+ val latestFrame: StateFlow = _latestFrame
+
+ private val _rawHexLog = MutableStateFlow>(emptyList())
+ val rawHexLog: StateFlow> = _rawHexLog
+
+ private val _errorMessage = MutableStateFlow("")
+ val errorMessage: StateFlow = _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()
+
+ 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
+ }
+}
diff --git a/app/src/main/java/com/example/esp32aldldashboard/data/DataRepository.kt b/app/src/main/java/com/example/esp32aldldashboard/data/DataRepository.kt
new file mode 100644
index 0000000..96df2d5
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/data/DataRepository.kt
@@ -0,0 +1,12 @@
+package com.example.esp32aldldashboard.data
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+interface DataRepository {
+ val data: Flow>
+}
+
+class DefaultDataRepository : DataRepository {
+ override val data: Flow> = flow { emit(listOf("Android")) }
+}
diff --git a/app/src/main/java/com/example/esp32aldldashboard/parser/ALDLParser.kt b/app/src/main/java/com/example/esp32aldldashboard/parser/ALDLParser.kt
new file mode 100644
index 0000000..5cbe560
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/parser/ALDLParser.kt
@@ -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,
+ 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>): 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()
+ // 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
+ )
+ }
+}
diff --git a/app/src/main/java/com/example/esp32aldldashboard/theme/Color.kt b/app/src/main/java/com/example/esp32aldldashboard/theme/Color.kt
new file mode 100644
index 0000000..825610b
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/theme/Color.kt
@@ -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)
diff --git a/app/src/main/java/com/example/esp32aldldashboard/theme/Theme.kt b/app/src/main/java/com/example/esp32aldldashboard/theme/Theme.kt
new file mode 100644
index 0000000..7de22ff
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/theme/Theme.kt
@@ -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)
+}
diff --git a/app/src/main/java/com/example/esp32aldldashboard/theme/Type.kt b/app/src/main/java/com/example/esp32aldldashboard/theme/Type.kt
new file mode 100644
index 0000000..0aca8b7
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/theme/Type.kt
@@ -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
+ )
+ */
+ )
diff --git a/app/src/main/java/com/example/esp32aldldashboard/ui/main/MainScreen.kt b/app/src/main/java/com/example/esp32aldldashboard/ui/main/MainScreen.kt
new file mode 100644
index 0000000..7e68f17
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/ui/main/MainScreen.kt
@@ -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,
+ 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) {
+ 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) {
+ 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
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/esp32aldldashboard/ui/main/MainScreenViewModel.kt b/app/src/main/java/com/example/esp32aldldashboard/ui/main/MainScreenViewModel.kt
new file mode 100644
index 0000000..6ac861c
--- /dev/null
+++ b/app/src/main/java/com/example/esp32aldldashboard/ui/main/MainScreenViewModel.kt
@@ -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 = bluetoothService.connectionState
+ val latestFrame: StateFlow = bluetoothService.latestFrame
+ val rawHexLog: StateFlow> = bluetoothService.rawHexLog
+ val errorMessage: StateFlow = bluetoothService.errorMessage
+
+ private val _isCelsius = MutableStateFlow(false) // Default to Fahrenheit for standard 80s GM telemetry
+ val isCelsius: StateFlow = _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()
+ }
+}
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..423ed70
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..1f8b003
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..be88654
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ ESP32 ALDL Dashboard
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..bbc3ccb
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/example/esp32aldldashboard/ALDLParserTest.kt b/app/src/test/java/com/example/esp32aldldashboard/ALDLParserTest.kt
new file mode 100644
index 0000000..97fd9c9
--- /dev/null
+++ b/app/src/test/java/com/example/esp32aldldashboard/ALDLParserTest.kt
@@ -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
+ }
+}
diff --git a/app/src/test/java/com/example/esp32aldldashboard/ui/main/MainScreenViewModelTest.kt b/app/src/test/java/com/example/esp32aldldashboard/ui/main/MainScreenViewModelTest.kt
new file mode 100644
index 0000000..d4f1252
--- /dev/null
+++ b/app/src/test/java/com/example/esp32aldldashboard/ui/main/MainScreenViewModelTest.kt
@@ -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)
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..bb92a46
--- /dev/null
+++ b/build.gradle.kts
@@ -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
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..32d72a9
--- /dev/null
+++ b/gradle.properties
@@ -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
diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties
new file mode 100644
index 0000000..baa28d1
--- /dev/null
+++ b/gradle/gradle-daemon-jvm.properties
@@ -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
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..660fd3a
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -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" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..c61a118
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -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" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -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
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..b229c66
--- /dev/null
+++ b/settings.gradle.kts
@@ -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")