Browse Source

- Doku hinzugefügt
- DDE-AutoLLA - update mit Multi Job (TÖNS auslesen aus KOMBI für E46)

FloKra 3 years ago
parent
commit
464a0b6236

+ 114 - 29
DDE-AutoLLA/DDEAutoLLA_D50M57E1.ccpage

@@ -14,13 +14,15 @@
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_ZUHEIZER_ANSTEUERUNG_WERT"> Zuheizer PWM [%]</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LADEDRUCK_WERT"> Ladedruck Ist [mbar]</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LADEDRUCK_SOLL_WERT"> Ladedruck Soll [mbar]</string>
-      
-      <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LUFTMASSE_PRO_HUB_WERT"> Luftmasse [mg/Hub]</string>
+	  
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_UMGEBUNGSDRUCK_WERT"> Luftdruck [mbar]</string>
+      <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LUFTMASSE_PRO_HUB_WERT"> Luftmasse [mg/Hub]</string>
+	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_EINSPRITZMENGE_AKTUELL_WERT"> Einspritzmenge [mg]</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_RAILDRUCK_WERT"> Raildruck Ist [bar]</string>
-      <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_GESCHWINDIGKEIT_WERT"> Geschwindigkeit [km/h]</string>
+	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_RAILDRUCK_SOLL_WERT"> Raildruck Soll [bar]</string>
+      
+	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_GESCHWINDIGKEIT_WERT"> Geschwindigkeit [km/h]</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_FAHRZEUGBESCHLEUNIGUNG_WERT"> Beschleunigung [m/s²]</string>
-	  
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_KUPPLUNGSSCHALTER_ROH_WERT">  Kupplungsschalter</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_KLIMAKOMPRESSOR_STATUS_WERT">  Klimakompressor</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_BEREITSCHAFT_KLIMA_WERT">  Klima Bereitschaft</string>
@@ -39,14 +41,20 @@
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_UBATT_WERT"> voltage [V]</string>
 	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_GENERATORLAST_WERT"> alternator load [%]</string>
 	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_ZUHEIZER_ANSTEUERUNG_WERT"> aux heater PWM [%]</string>
-      <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LADEDRUCK_WERT"> act. boost pressure [mbar]</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LADEDRUCK_SOLL_WERT"> target boost pressure [mbar]</string>
+      <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LADEDRUCK_WERT"> act. boost pressure [mbar]</string>
 	  	  
-	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LUFTMASSE_PRO_HUB_WERT"> air mass [mg/stroke]</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_UMGEBUNGSDRUCK_WERT"> air pressure [mbar]</string>
+	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LUFTMASSE_PRO_HUB_WERT"> air mass [mg/stroke]</string>
+	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_EINSPRITZMENGE_AKTUELL_WERT"> Einspritzmenge [mg]</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_RAILDRUCK_WERT"> act. rail pressure [bar]</string>
+	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_RAILDRUCK_SOLL_WERT"> Raildruck Soll [bar]</string>
+	  
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_GESCHWINDIGKEIT_WERT"> speed [km/h]</string>
       <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_FAHRZEUGBESCHLEUNIGUNG_WERT"> acceleration [m/s²]</string>
+	  <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_KUPPLUNGSSCHALTER_ROH_WERT">  Clutch switch</string>
+      <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_KLIMAKOMPRESSOR_STATUS_WERT">  AC compressor</string>
+      <string name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_BEREITSCHAFT_KLIMA_WERT">  AC in standby</string>
       
 	  <string name="!JOB#STATUS_TOENS_IO#STAT_TOG_HIGH_WERT">  TÖNS heating time [ms]</string>
       <string name="!JOB#STATUS_TOENS_IO#STAT_TOG_LOW_WERT">  TÖNS cooling time [ms]</string>
@@ -61,16 +69,17 @@
     <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_UBATT_WERT" result="STAT_UBATT_WERT" grid-type="simple-gauge-round" min-value="0" max-value="15" log_tag="STAT_UBATT_WERT" />
 	<display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_GENERATORLAST_WERT" result="STAT_GENERATORLAST_WERT" grid-type="simple-gauge-round" min-value="0" max-value="100" log_tag="STAT_GENERATORLAST_WERT" />
     <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_ZUHEIZER_ANSTEUERUNG_WERT" result="STAT_ZUHEIZER_ANSTEUERUNG_WERT" grid-type="simple-gauge-round" min-value="0" max-value="100" log_tag="STAT_ZUHEIZER_ANSTEUERUNG_WERT" />
-    <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LADEDRUCK_WERT" result="STAT_LADEDRUCK_WERT" format="4.1R" grid-type="simple-gauge-round" min-value="0" max-value="2500" log_tag="STAT_LADEDRUCK_WERT" />
     <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LADEDRUCK_SOLL_WERT" result="STAT_LADEDRUCK_SOLL_WERT" format="4.1R" grid-type="simple-gauge-round" min-value="0" max-value="2500" log_tag="STAT_LADEDRUCK_SOLL_WERT" />
+    <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LADEDRUCK_WERT" result="STAT_LADEDRUCK_WERT" format="4.1R" grid-type="simple-gauge-round" min-value="0" max-value="2500" log_tag="STAT_LADEDRUCK_WERT" />
 	
-	<display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LUFTMASSE_PRO_HUB_WERT" result="STAT_LUFTMASSE_PRO_HUB_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="1500" log_tag="STAT_LUFTMASSE_PRO_HUB_WERT" />
     <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_UMGEBUNGSDRUCK_WERT" result="STAT_UMGEBUNGSDRUCK_WERT" format="4.1R" grid-type="simple-gauge-round" min-value="800" max-value="1300" log_tag="STAT_UMGEBUNGSDRUCK_WERT" />    
+	<display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_LUFTMASSE_PRO_HUB_WERT" result="STAT_LUFTMASSE_PRO_HUB_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="1500" log_tag="STAT_LUFTMASSE_PRO_HUB_WERT" />
+	<display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_EINSPRITZMENGE_AKTUELL_WERT" result="STAT_EINSPRITZMENGE_AKTUELL_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="100" log_tag="STAT_EINSPRITZMENGE_AKTUELL_WERT" />
     <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_RAILDRUCK_WERT" result="STAT_RAILDRUCK_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="1400" log_tag="STAT_RAILDRUCK_WERT" />
-    <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_GESCHWINDIGKEIT_WERT" result="STAT_GESCHWINDIGKEIT_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="250" log_tag="STAT_GESCHWINDIGKEIT_WERT" />
-    <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_FAHRZEUGBESCHLEUNIGUNG_WERT" result="STAT_FAHRZEUGBESCHLEUNIGUNG_WERT" format="2.3R" grid-type="simple-gauge-round" min-value="-15" max-value="15" log_tag="STAT_FAHRZEUGBESCHLEUNIGUNG_WERT" />
+    <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_RAILDRUCK_SOLL_WERT" result="STAT_RAILDRUCK_SOLL_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="1400" log_tag="STAT_RAILDRUCK_SOLL_WERT" />
 	
-    
+	<display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_GESCHWINDIGKEIT_WERT" result="STAT_GESCHWINDIGKEIT_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="250" log_tag="STAT_GESCHWINDIGKEIT_WERT" />
+    <display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_FAHRZEUGBESCHLEUNIGUNG_WERT" result="STAT_FAHRZEUGBESCHLEUNIGUNG_WERT" format="2.3R" grid-type="simple-gauge-round" min-value="-15" max-value="15" log_tag="STAT_FAHRZEUGBESCHLEUNIGUNG_WERT" />
 	<display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_KUPPLUNGSSCHALTER_ROH_WERT" result="STAT_KUPPLUNGSSCHALTER_ROH_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="1" log_tag="STAT_KUPPLUNGSSCHALTER_ROH_WERT" />
 	<display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_KLIMAKOMPRESSOR_STATUS_WERT" result="STAT_KLIMAKOMPRESSOR_STATUS_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="1" log_tag="STAT_KLIMAKOMPRESSOR_STATUS_WERT" />
 	<display name="!JOB#STATUS_MESSWERTBLOCK_LESEN#STAT_BEREITSCHAFT_KLIMA_WERT" result="STAT_BEREITSCHAFT_KLIMA_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="1" log_tag="STAT_BEREITSCHAFT_KLIMA_WERT" />
@@ -78,7 +87,7 @@
     <display name="!JOB#STATUS_TOENS_IO#STAT_TOG_HIGH_WERT" result="STAT_TOG_HIGH_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="1000" log_tag="STAT_TOG_HIGH_WERT" />
     <display name="!JOB#STATUS_TOENS_IO#STAT_TOG_LOW_WERT" result="STAT_TOG_LOW_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="1000" log_tag="STAT_TOG_LOW_WERT" />
 	
-	<jobs sgbd="D50M57E1" />
+	<jobs />
     <code show_warnings="true">
     <![CDATA[
     class PageClass
@@ -109,43 +118,67 @@
 		private int conf_LLA_KlimaBereitschaft = 90;
 		private int conf_LLA_Klimakompressor = 120;
 		private int conf_LLA_GeneratorlastHoch = 90;
-		private int conf_LLA_GeneratorlastSehrHoch = 120;
+		private int conf_LLA_GeneratorlastSehrHoch = 150;
 		
 		// Konfiguration für Zähler (Anzahl Durchläufe bis eine Aktion eintritt)
-		private int conf_countdown_ZuheizerAn_max = 5;
-		private int conf_countdown_Kupplung_max = 5;
-		private int conf_countdown_Generatorlast_max = 5;
+		// Noch genauer zu ermitteln wie viele Abfragen pro Sekunde bei der DDE5 mit dieser
+		// Konfiguration im Durchschnitt anfallen. 
+		// alle 5 Durchläufe ergibt ca. 1 Sekunde Verzögerung
+		private int conf_countdown_ZuheizerAn_max = 10;
+		private int conf_countdown_Kupplung_max = 10;
+		private int conf_countdown_Generatorlast_max = 10;
 		private int conf_countdown_LLA_aus = 15;
-		private int conf_countdown_Job2_max = 10;
+		
+		// wie häufig sollen zusätzliche Jobs ausgeführt werden?
+		private int conf_counter_Job2_max = 20;        // Job2 = am selben Steuergerät wie der Hauptjob
+		private int conf_counter_JobSgbd2_max = 98;    // JobSgbd2 = auf einem anderen Steuergerät
+
+		// Steuergeräte SGBD files für Multijob definieren
+		public static string[] conf_sgbds = {"D50M57E1", "kombi46r"};
+				
 		
 		// Statusvariablen
 		private bool status_MotorHeiss = false;
+		private bool status_MotorBetriebstemp = false;
 		private bool status_Kupplung = false;
 		private bool status_Zuheizer = false;
 		//private bool status_Klima = false;
 		private bool status_GeneratorlastHoch = false;
 		private bool status_GeneratorlastSehrHoch = false;
+		private int currSgbd = 0; // momentan aktiv abgefragtes Steuergerät, für switch zwischen mehreren 
 		
 		// Zähler
 		private int countdown_ZuheizerAn = 0;
 		private int countdown_Kupplung = 0;
 		private int countdown_Generatorlast = 0;
 		private int countdown_LLA_aus = 0;
-		private int countdown_Job2 = 0;
+		private int counter_Job2 = 0;
+		private int counter_JobSgbd2 = 0;
 		
 		// Globale Variablen für Leerlaufanhebung
 		private int LLA_momentan = 0;
 		
-						
+		// result objekte für die zusätzlichen Jobs, die nicht in jedem Durchlauf abgefragt werden
+        // muss global deklariert sein damit trotzdem in jedem Durchlauf die letzten Werte ausgegeben werden können
 		List<Dictionary<string, EdiabasNet.ResultData>> resultSets_Abgleich;
+		List<Dictionary<string, EdiabasNet.ResultData>> resultSets_JobSgbd2;
 			
 		public void ExecuteJob(EdiabasNet ediabas, ref MultiMap<string, EdiabasNet.ResultData> resultDict, bool firstCall)
         {
             List<Dictionary<string, EdiabasNet.ResultData>> resultSets;
+			
+			if(firstCall || currSgbd != 0) {
+				currSgbd = 0;
+				ediabas.ResolveSgbdFile(conf_sgbds[0]);
+				//ediabas.ArgString = string.Empty;
+				//ediabas.ArgBinaryStd = null;
+				//ediabas.ResultsRequests = string.Empty;
+				//ediabas.ExecuteJob("INITIALISIERUNG");
+			}
             
             
 			// Job 1 - ständig zu aktualisierende Werte
-            ediabas.ArgString = "JA;ILMMG;IPLAD;IPUMG;IUBAT;IMOAK;INMOT;SPLAD;ITKUM;ITLAL;IPRDR;IAFZG;IVKMH;ITZUH;IGENL;RSKUP;IKLIS;IBKLI";
+            ediabas.ArgString = "JA;ILMMG;IPLAD;IPUMG;IUBAT;IMOAK;INMOT;SPLAD;ITKUM;ITLAL;IPRDR;IAFZG;IVKMH;ITZUH;IGENL;RSKUP;IKLIS;IBKLI;SPRDR;IMEIA";
             ediabas.ArgBinaryStd = null;
             ediabas.ResultsRequests = string.Empty;
             ediabas.ExecuteJob("STATUS_MESSWERTBLOCK_LESEN");
@@ -157,19 +190,21 @@
 			
 			
 						
-			// Job 2 - wird nur bei jedem X-ten Durchlauf ausgeführt (definiert von [conf_countdown_Job2_max])
-			if(countdown_Job2 == 0) {
+			// Job 2 - wird nur bei jedem X-ten Durchlauf ausgeführt (definiert von [conf_counter_Job2_max])
+			// Ergebnisse werden in globaler Variable vorgehalten und die letzten Werte in jedem Durchlauf ausgegeben
+			if(counter_Job2 == 0) {
 				ediabas.ArgString = "lla";
 				ediabas.ArgBinaryStd = null;
 				ediabas.ResultsRequests = string.Empty;
 				ediabas.ExecuteJob("ABGLEICH_LESEN");
 				resultSets_Abgleich = ediabas.ResultSets;
+				counter_Job2++;
 			}
-			else if(countdown_Job2 >= conf_countdown_Job2_max) {
-				countdown_Job2 = 0;
+			else if(counter_Job2 >= conf_counter_Job2_max) {
+				counter_Job2 = 0;
 			}
 			else {
-				countdown_Job2++;
+				counter_Job2++;
 			}
 			
 			// da diese Werte nicht in jedem Durchlauf vom Steuergerät gelesen werden sind die Ergebnisdaten in einer 
@@ -181,7 +216,7 @@
 			
 			
 			
-			// Werte für automatische Leerlaufanhebung holen
+			// Eingangswerte für automatische Leerlaufanhebung holen
 			
 			EdiabasNet.ResultData resultData;
 			
@@ -271,6 +306,7 @@
 				countdown_Kupplung--;
 			}
 			else if (countdown_Kupplung == 0){
+				countdown_Kupplung = -1;
 				status_Kupplung = false;
 				if(conf_LLA_Basis > LLA_neu) LLA_neu = conf_LLA_Basis;
 			}
@@ -291,6 +327,7 @@
 				countdown_ZuheizerAn--;
 			}
 			else if (countdown_ZuheizerAn == 0) {
+				countdown_ZuheizerAn = -1;
 				status_Zuheizer = false;
 				if(conf_LLA_Basis > LLA_neu) LLA_neu = conf_LLA_Basis;
 			}
@@ -314,6 +351,7 @@
 				countdown_Generatorlast--;
 			}
 			else if (countdown_Generatorlast == 0) {
+				countdown_Generatorlast = -1;
 				status_GeneratorlastHoch = false;
 				status_GeneratorlastSehrHoch = false;
 				if(conf_LLA_Basis > LLA_neu) LLA_neu = conf_LLA_Basis;
@@ -334,35 +372,41 @@
 			// Drehzahlanhebung je nach Motortemperatur, 
 			if(mom_MotorTemp < conf_Temp_MotorSehrKaltBis) {
 				// Motor ist sehr kalt
+				status_MotorBetriebstemp = false;
 				if(conf_LLA_MotorSehrKalt > LLA_neu) LLA_neu = conf_LLA_MotorSehrKalt;
 			}
 			else if(mom_MotorTemp < conf_Temp_MotorKaltBis) {
 				// Motor ist kalt
+				status_MotorBetriebstemp = false;
 				if(conf_LLA_MotorKalt > LLA_neu) LLA_neu = conf_LLA_MotorKalt;
 			}
 			else if(mom_MotorTemp >= conf_Temp_MotorKaltBis && mom_MotorTemp < conf_Temp_MotorBetriebstempAb) {
 				// Motor ist nicht mehr kalt, aber noch nicht auf Betriebstemperatur
+				status_MotorBetriebstemp = false;
 				if(conf_LLA_MotorVorBetriebstemp > LLA_neu) LLA_neu = conf_LLA_MotorVorBetriebstemp;
 			}
 			else if(mom_MotorTemp >= conf_Temp_MotorBetriebstempAb && mom_MotorTemp < conf_Temp_MotorHeissAb && !status_MotorHeiss) {
 				// Motor ist auf Betriebstemperatur
+				status_MotorBetriebstemp = true;
 				if(conf_LLA_MotorBetriebstemp > LLA_neu) LLA_neu = conf_LLA_MotorBetriebstemp;
 			}
 			else if(mom_MotorTemp >= conf_Temp_MotorBetriebstempAb && mom_MotorTemp < conf_Temp_MotorBetriebstempBis && status_MotorHeiss) {
 				// Temperatur ist nachdem Status HEISS wieder weit genug gefallen -> Betriebstemperatur
 				status_MotorHeiss = false;
+				status_MotorBetriebstemp = true;
 				if(conf_LLA_MotorBetriebstemp > LLA_neu) LLA_neu = conf_LLA_MotorBetriebstemp;
 			}
 			else if(mom_MotorTemp >= conf_Temp_MotorHeissAb) {
 				// Motor ist HEISS -> weitere LL Anhebung um die Kühlung zu verbessern aktivieren 
 				// deaktivierung sobald die Temperatur wieder unter [conf_Temp_MotorBetriebstempBis] gefallen ist
 				status_MotorHeiss = true;
+				status_MotorBetriebstemp = false;
 				if(conf_LLA_MotorHeiss > LLA_neu) LLA_neu = conf_LLA_MotorHeiss;
 			}
 			
 			
-			// Motor auf Betriebstemperatur - reduziere LL auf minimum nach einiger Zeit
-			if(LLA_neu == conf_LLA_MotorBetriebstemp) {
+			// Motor auf Betriebstemperatur - reduziere LL auf minimum "conf_LLA_Basis" nach einiger Zeit
+			if(status_MotorBetriebstemp && LLA_neu == conf_LLA_MotorBetriebstemp) {
 				if(countdown_LLA_aus == 0) {
 					LLA_neu = conf_LLA_Basis;
 				}
@@ -391,9 +435,50 @@
                 
                 // Ablaufvariable wieder zurücksetzen, sonst würde diese funktion nun bei jedem folgenden durchlauf ausgeführt werden...
                 LLA_momentan = LLA_neu;
-				conf_countdown_Job2_max = 0;
+				conf_counter_Job2_max = 0;
 			}
 			
+			
+			
+			// Job SGBD 2
+			if(counter_JobSgbd2 == 0) {
+				counter_JobSgbd2++;
+				try {
+					// try/catch, damit nicht der ganze Durchlauf abgebrochen und 
+					// gar keine Daten zurückgeliefert werden, falls das Steuergerät nicht 
+					// antwortet. 
+					
+					currSgbd = 1;
+					ediabas.ResolveSgbdFile(conf_sgbds[currSgbd]);
+					
+					//ediabas.ArgString = string.Empty;
+					//ediabas.ArgBinaryStd = null;
+					//ediabas.ResultsRequests = string.Empty;
+					//ediabas.ExecuteJob("INITIALISIERUNG");
+								
+					ediabas.ArgString = string.Empty;
+					ediabas.ArgBinaryStd = null;
+					ediabas.ResultsRequests = string.Empty;
+					ediabas.ExecuteJob("STATUS_TOENS_IO");
+					resultSets_JobSgbd2 = ediabas.ResultSets;
+				}
+				catch {
+					// dann halt nicht
+				}
+			}
+			else if (counter_JobSgbd2 >= conf_counter_JobSgbd2_max) {
+				counter_JobSgbd2 = 0;
+			}
+			else {
+				counter_JobSgbd2++;
+			}
+			
+			if (resultSets_JobSgbd2 != null && resultSets_JobSgbd2.Count >= 2)
+				{
+					EdiabasThread.MergeResultDictionarys(ref resultDict, resultSets_JobSgbd2[1]);
+				}
+			
+			
         }
 		
 		

+ 21 - 16
DDE-AutoLLA/README.md

@@ -4,48 +4,53 @@
 
 **Automatische Leerlaufanhebung** 
 
-Hebt die Leerlaufdrehzahl automatisch per Regelwerk aufgrund verschiedener Parameter an.
+Hebt die Leerlaufdrehzahl automatisch per Regelwerk aufgrund verschiedener Parameter an.   
+Diese Konfiguration ruft Daten abwechselnd von mehreren Steuergeräten ab (DDE und KOMBI), wobei die Daten aus dem Kombiinstrument (TÖNS Heiz-/Kühlzeit) nur selten abgerufen werden um die restlichen Funktionen nicht unnötig auszubremsen.   
+
+Die Adaption Leerlaufanhebung wird hier **nicht programmiert** sondern nur **verstellt**, was sich rein im RAM der DDE abspielt und somit trotz sehr häufiger Änderung keine Folgeschäden befürchten lässt. Diese Änderungen verschwinden nach Zündung aus wieder. Ohne aktivem DeepOBD ist das ganze somit außer Funktion und der Serienstand aktiv. 
+
+Da dies immer automatisch aktiv sein soll muss diese Konfiguration die **Hauptseite** in DeepOBD sein und Deep OBD sollte automatisch gestartet werden, z.B. über die entsprechende Funktion in der IBUS App. Die Konfiguration enthält daher für die Daten-Broadcasts zur IBUS-App alle Werte aus der originalen I-BUS App Konfiguration (Ladedruck Soll/Ist, Einspritzmenge, Luftmasse, TÖNS Heiz-/Kühlzeit... ) für den M57TÜ sowie **farbige** Anzeigen. 
+
+
 
 #### Funktion
 
 - **Elektrischer Zuheizer**
 
-  wenn der Zuheizer aktiv ist wird die Leerlaufdrehzahl stärker angehoben, um Leerlaufschwankungen zu reduzieren
+  wenn der elektrische Zuheizer aktiv ist wird die Leerlaufdrehzahl um 200 u/min (auf 950 u/min) angehoben, um Leerlaufschwankungen zu reduzieren   
+  Anmerkung: beim M57 DDE4 war diese Anhebung Serienmäßig auf 900 u/min, beim M57TÜ DDE5 nur noch um 50 u/min, woraus das Problem mit dem schwankenden Leerlauf resultiert. Meine Tests haben als optimalen Wert bei meinem E46 330d M57TÜ (Schalter, EU4) eine Anhebung um 200 u/min als ideal ergeben. 
 
 - **Generatorlastsignal**
 
-  - bei hoher Generatorlast wird die LL-Drehzahl leicht angehoben
-  - bei sehr hoher Generatorlast noch höher
+  - bei hoher Generatorlast wird die LL-Drehzahl um 90 u/min angehoben
+  - bei sehr hoher Generatorlast um 150 u/min (z.B. beim Starthilfe geben sinnvoll)
 
 - **Motortemperatur**
 
   - verschiedene Stufen je nach Motortemperatur
   - ausgehend von sehr kalt wird bis Betriebstemperatur die Anhebung in Stufen reduziert
-  - bei heißem Motor (> 105°C) wird die Drehzahl für bessere Kühlung um 200 upm erhöht, bis die Temperatur auf 95 °C gesunken ist
+  - bei heißem Motor (> 105°C) wird die Drehzahl für bessere Kühlung um 200 u/min erhöht, bis die Temperatur auf 95 °C gesunken ist
 
 - **Kupplungsschalter**
 
-  - bei betätigter Kupplung Anhebung auf 900 upm für angenehmeres Anfahrverhalten
-  - einige Sekunden nachdem die Kupplung nicht mehr getreten ist Reduzierung auf Minimum (wenn keine andere LL-Intervention aktiv) 
+  - bei betätigter Kupplung: Anhebung auf 900 upm für angenehmeres Anfahrverhalten und einfachere Weiterfahrt im 2. Gang bei einem Rollstopp mit kurz schleifender Kupplung (Achtung auf Verschleiß!!)
+  - einige Sekunden nachdem die Kupplung nicht mehr getreten ist - Reduzierung der Drehzahlanhebung auf von der Motortemperatur abhängigen Wert (wenn keine andere LL-Intervention aktiv) 
+
+  Anmerkung: diese Funktion haben die Benziner wie M54B30 Serienmäßig. Dort wird ca. um 100 u/min angehoben - in meinen Tests hat sich für den M57TÜ im E46 eine Anhebung um 150 u/min (auf 900) als am Besten erwiesen. 
 
 - **Klimakompressor und Klimabereitschaft**
 
-  - Klimaanlage an aber Kompressor läuft nicht: moderate Anhebung um Drehzahleinbruch bei Kompressoranlauf zu reduzieren
+  - Klimaanlage in Bereitschaft, Kompressor läuft nicht: moderate Anhebung um Drehzahleinbruch bei Kompressoranlauf zu verhindern
   - Klimakompressor läuft: etwas höhere Anhebung
 
-- Reduktion auf Serienwert nur bei Betriebstemperatur, wenn keine andere Intervention aktiv ist, nach einigen Sekunden
-
-- Parameter sind im Code konfigurierbar
+  Anmerkung: eine gewisse Leerlaufanhebung bei aktiviertem Kompressor ist serienmäßig vorhanden, allerdings nicht bei Klima-Bereitschaft, weshalb man im Leerlauf beim M57TÜ jedesmal einen leichten "Verschlucker" bemerkt wenn der Klimakompressor einkuppelt. 
 
+- Reduktion auf Serienwert (Anhebung 0) bei Betriebstemperatur, wenn keine andere Intervention aktiv ist, nach einigen Sekunden
 
+- Alle Parameter sind im Code konfigurierbar
 
-Die Adaption Leerlaufanhebung wird hier **nicht programmiert** sondern nur **verstellt**, was sich rein im RAM der DDE abspielt und somit trotz sehr häufiger Änderung keine Folgeschäden befürchten lässt. Diese Änderungen verschwinden nach Zündung aus wieder. Ohne aktivem DeepOBD ist das ganze somit außer Funktion und der Serienstand aktiv. 
-
-Da dies immer automatisch aktiv sein soll muss die Konfiguration die "Hauptseite" in DeepOBD sein. 
 
 
 
-#### Bekannte Probleme
 
-- Werte für TÖNS wie in der IBUS_APP Konfiguration können derzeit nicht abgerufen werden, da ich noch keinen Weg gefunden habe über benutzerdefinierten Code mehrere ECUs nacheinander abzurufen. 
 

+ 8 - 0
README.md

@@ -28,9 +28,17 @@ Initialisierung der Reifen-Pannen-Anzeige (DSC-Funktion), z.B. wenn diese per Co
 
 Verstellung/Programmierung von Abgleichwerten in DDE Steuergeräten (Anhebung Frischluftrate, Leerlaufdrehzahl)
 
+##### [DDE-AutoLLA](DDE-AutoLLA/)
+
+Automatische temporäre Leerlaufanhebung in Abhängigkeit von Betriebswerten (Kupplungsschalter, Motortemperatur, Elektrischer Zuheizer, Generatorlast, Klimaanlage)
+
 
 ##### [SIA-Reset](SIA-Reset/)
 
 Service Intervall Anzeige - Daten auslesen und Service-Reset (Inspektion, Ölservice, Zeitinspektion) ausführen. 
 
 
+
+## Dokumentation
+
+Erweiterte Version der originalen Dokumentation von Deep OBD (in Englisch): [Page_specification.md](docs/Page_specification.md)

+ 1196 - 0
docs/Page_specification.md

@@ -0,0 +1,1196 @@
+# Defining pages for _Deep OBD for BMW and VAG_
+
+
+*This document is based on the original ediabaslib/DeepOBD documentation, with added comments and information I* 
+*found out when creating some extended Deep OBD pages with user defined code.* 
+
+*Original can be found here: https://github.com/uholeschak/ediabaslib/blob/master/docs/Page_specification.md* 
+
+
+
+Each page (tab) is defined in a single XML (`*.ccpage`) file. A general documentation of all XML tags could be found in the `BmwDeepObd.xsd` file. The documentation will be displayed in the XML editor when the `.xsd` is added as `xs:shema` in the XML file.  
+Table of contents:
+
+* [Simple jobs](#simple-jobs)
+* [Reading errors](#reading-errors)
+* [User defined code](#user-defined-code)
+	* [Formatting results (FormatResult)](#formatting-results-formatresult)
+	* [Formatting error results (FormatErrorResult)](#formatting-error-results-formaterrorresult)
+	* [Processing results (ProcessResults)](#processing-results-processresults)
+	* [Control output of the page (UpdateResultList)](#control-output-of-the-page-updateresultlist)
+	* [Executing own jobs (ExecuteJob)](#executing-own-jobs-executejob)
+	* [Adding controls to the layout](#adding-controls-to-the-layout)
+* [Grouping pages](#grouping-pages)
+* [The configuration file](#the-configuration-file)
+* [Broadcasts](#broadcasts)
+* [Special jobs](#special-jobs)
+
+## Simple jobs
+If only some EDIABAS jobs with fixed arguments are required for one display page, the XML code is relative simple. Below is the example code to display climate data for a E61 vehicle:
+``` xml
+<?xml version="1.0" encoding="utf-8" ?>
+<fragment xmlns="http://www.holeschak.de/BmwDeepObd"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://www.holeschak.de/BmwDeepObd ../BmwDeepObd.xsd">
+  <page name="tab_ihk" fontsize="medium" no_update="true">
+    <strings>
+      <string name="tab_ihk">Climate</string>
+      <string name="label_ihk_in_temp">Indoor temperature [°C](°C):</string>
+      <string name="label_ihk_in_temp_delay">Indoor temperature delayed [°C](°C):</string>
+      <string name="label_ihk_out_temp">Outdoor temperature [°C](°C):</string>
+      <string name="label_ihk_setpoint">Setpoint [°C](°C):</string>
+      <string name="label_ihk_heat_ex_temp">Heat exchanger temperature [°C](°C):</string>
+      <string name="label_ihk_heat_ex_setpoint">Heat exchanger setpoint [°C](°C):</string>
+      <string name="label_ihk_heat_ex_actuator">Heat exchanger actuator [%](%):</string>
+      <string name="label_ihk_main_actuator">Main actuator [%](%):</string>
+      <string name="label_ihk_evap_temp">Evaporator temperature [°C](°C):</string>
+      <string name="label_ihk_press_sense">Pressure sensor [bar](bar):</string>
+      <string name="label_ihk_circ_air_left">Circulating air left [%](%):</string>
+      <string name="label_ihk_circ_air_right">Circulating air right [%](%):</string>
+      <string name="label_ihk_defrost">Defrost [%](%):</string>
+      <string name="label_ihk_vent">Ventilation [%](%):</string>
+      <string name="label_ihk_cold_air">Cold air [%](%):</string>
+      <string name="label_ihk_legroom">Leg room [%](%):</string>
+      <string name="label_ihk_refrig_comp">Refrigerating compressor [%](%):</string>
+    </strings>
+    <strings lang="de">
+      <string name="tab_ihk">Klima</string>
+      <string name="label_ihk_in_temp">Innentemperatur [°C](°C):</string>
+      <string name="label_ihk_in_temp_delay">Innentemperatur verzögert [°C](°C):</string>
+      <string name="label_ihk_out_temp">Außentemperatur [°C](°C):</string>
+      <string name="label_ihk_setpoint">Sollwert [°C](°C):</string>
+      <string name="label_ihk_heat_ex_temp">Wärmetauschertemperatur [°C](°C):</string>
+      <string name="label_ihk_heat_ex_setpoint">Wärmetauschersollwert [°C](°C):</string>
+      <string name="label_ihk_heat_ex_actuator">Wärmetauscherstellgröße [%](%):</string>
+      <string name="label_ihk_main_actuator">Hauptstellgröße [%](%):</string>
+      <string name="label_ihk_evap_temp">Verdampfertemperatur [°C](°C):</string>
+      <string name="label_ihk_press_sense">Drucksensor [bar](bar):</string>
+      <string name="label_ihk_circ_air_left">Umluft links [%](%):</string>
+      <string name="label_ihk_circ_air_right">Umluft rechts [%](%):</string>
+      <string name="label_ihk_defrost">Abtauen [%](%):</string>
+      <string name="label_ihk_vent">Belüftung [%](%):</string>
+      <string name="label_ihk_cold_air">Kaltluft [%](%):</string>
+      <string name="label_ihk_legroom">Fußraum [%](%):</string>
+      <string name="label_ihk_refrig_comp">Kältemittelverdichter [%](%):</string>
+    </strings>
+    <jobs sgbd="d_klima">
+      <job name="STATUS_REGLERGROESSEN" results="STAT_TINNEN_WERT;STAT_TINNEN_VERZOEGERT_WERT;STAT_TAUSSEN_WERT;STAT_SOLL_LI_KORRIGIERT_WERT;STAT_WT_RE_WERT;STAT_WTSOLL_RE_WERT;STAT_YWT_RE_WERT;STAT_Y_RE_WERT">
+        <display name="label_ihk_in_temp" result="STAT_TINNEN_WERT" format="6.1R" />
+        <display name="label_ihk_in_temp_delay" result="STAT_TINNEN_VERZOEGERT_WERT" format="6.1R" />
+        <display name="label_ihk_out_temp" result="STAT_TAUSSEN_WERT" format="6.1R" />
+        <display name="label_ihk_setpoint" result="STAT_SOLL_LI_KORRIGIERT_WERT" format="6.1R" />
+        <display name="label_ihk_heat_ex_temp" result="STAT_WT_RE_WERT" format="6.1R" />
+        <display name="label_ihk_heat_ex_setpoint" result="STAT_WTSOLL_RE_WERT" format="6.1R" />
+        <display name="label_ihk_heat_ex_actuator" result="STAT_YWT_RE_WERT" format="3L" />
+        <display name="label_ihk_main_actuator" result="STAT_Y_RE_WERT" format="3L" />
+      </job>
+      <job name="STATUS_ANALOGEINGAENGE" results="STAT_TEMP_VERDAMFER_WERT;STAT_DRUCKSENSOR_WERT">
+        <display name="label_ihk_evap_temp" result="STAT_TEMP_VERDAMFER_WERT" format="6.1R" />
+        <display name="label_ihk_press_sense" result="STAT_DRUCKSENSOR_WERT" format="6.1R" />
+      </job>
+      <job name="STATUS_MOTOR_KLAPPENPOSITION" results="STAT_FRISCHLUFT_UMLUFT_LI_WERT;STAT_FRISCHLUFT_UMLUFT_RE_WERT;STAT_DEFROST_WERT;STAT_BELUEFTUNG_WERT;STAT_KALTLUFT_WERT;STAT_FUSSRAUM_WERT">
+        <display name="label_ihk_circ_air_left" result="STAT_FRISCHLUFT_UMLUFT_LI_WERT" format="3L" />
+        <display name="label_ihk_circ_air_right" result="STAT_FRISCHLUFT_UMLUFT_RE_WERT" format="3L" />
+        <display name="label_ihk_defrost" result="STAT_DEFROST_WERT" format="3L" />
+        <display name="label_ihk_vent" result="STAT_BELUEFTUNG_WERT" format="3L" />
+        <display name="label_ihk_cold_air" result="STAT_KALTLUFT_WERT" format="3L" />
+        <display name="label_ihk_legroom" result="STAT_FUSSRAUM_WERT" format="3L" />
+      </job>
+      <job name="STATUS_IO" results="STAT_STEUERUNG_KMV_WERT">
+        <display name="label_ihk_refrig_comp" result="STAT_STEUERUNG_KMV_WERT" format="3L" />
+      </job>
+    </jobs>
+  </page>
+</fragment>
+```
+The `page name` property specifies the title of the page and is a reference to the `strings` nodes.  
+With the attribute `display-mode` the type of the display could be specified (`list`: normal text list, `grid`: grid wiew for graphical gauge display).  
+The attribute `fontsize` allows to specify the font size of the display data in three steps (`small`, `medium` and `large`).  
+The attributes `gauges-portrait` and `gauges-landscape` specify the number of gauges per line in the corresponding display mode.  
+With `logfile` a log file name could be specified, that allows to log the display data. If the symbol `{D}` is used inside the log file name, it will be replaced by the current date and time.  
+Setting the `no_update` property prevents the configuration generator to update the file. This is helpful if changes have been made to an auto generated configuration.  
+The `strings` nodes contains the all the string used on this display page. If the current language is not matching the `lang` tag, the default language (without tag) is used. The `lang` property could be either the short form e.g. `'de'` or the long one `'de-DE'`.
+
+The `jobs` node groups all EDIABAS jobs to execute. The property `sgbd` specifies the name of the group (`.grp`) or the sgbd (`.prg`) file to use. In VAG mode the property `mwtab` could be used to store the file name of the associated mwtab file.  
+Within the `jobs` node multiple `job` nodes specify the EDIABAS jobs to execute. They contain the following properties:
+* `name`: Name of the job to execute
+* `id`: Id for job result identification. If this element is specified the result name will be: `[id]#[data set index]#[result]`, otherwise it's: `[name]#[result]`.
+* `sgbd`: Name of the SGBD file to load. This overrides the SGBD file from the `jobs` node. It's recommended to combine this with the id attribute.
+* `fixed_func_struct_id`: Fixed function structure id from the BMW database. This entry is only used by the configuration generator.
+* `args_first`: Allows to specify semicolon separated job arguments for the first job call.
+* `args`: Allows to specify semicolon separated job arguments. If this is the first call and `args_first` is present `args_first` will be used instead.
+* `result`: Allows to specify the required results. If omitted, all results will be generated, which may require more processing time.
+* `display-order`: Allows to specify the index of the display order. If omitted the default value is 0. If two values are identical, the original order is retained.
+* `grid-type`: If the `display-mode` ist switched to `grid`, the type of display element could be specified here:
+    * `hidden`: Nothing will be displayed.
+    * `text`: Text only, no gauge will be displayed.
+    * `simple-gauge-square`: A simple gauge with a square border will be displayed.
+    * `simple-gauge-round`: A simple gauge with a round border will be displayed.
+    * `simple-gauge-dot`: A simple gauge with a round border and a dot instead of a bar graph will be displayed.
+* `min-value`: For gauge views the minimum value is specified with this attribute.
+* `max-value`: For gauge views the maximim value is specified with this attribute.
+* `log_tag=<tag name>`: Adding this property allows to log the display data to a log file when activating the _Log data_ menu in the application. The `logfile` property in the `page` node has to be specified as well to activate logging.
+* Each `display` node specifies one line of the display output. `Name` is again a reference to the text translation in the _strings_ nodes. With `result` the EDIABAS job result name is selected, that contains the data. The `format` property allows to format the result with the EDIABAS aspiResultText format specification [EDIABAS result types and formats](EDIABAS_result_types_and_formats.md).
+The `page` node can optionally contain `display` nodes like the `job` node. They will be only used for [User defined code](#user-defined-code).  
+This is how the resulting page will look like:
+
+![Climate page](Page_specification_AppClimateSmall.png)
+
+## Reading errors
+With the `read_errors` node it's possible to read an error summary of all ECUs. Simply list all ECU names and the corresponding sgbd file names in a separate `ecu` node, like in the example below.  
+The `errors` node supports a property `sgbd_functional`, this specifies the SGBD for functional (gobal) error reset. If this property is specified, a button _Global Error Reset_ button will appear.  
+Setting the `no_update` property prevents the configuration generator to update the file. This is helpful if changes have been made to an auto generated configuration.  
+The error message is generated by the sgbd file and is in the language of the sgbd.  
+The page also allows to selectively reset ECU errors.
+``` xml
+<fragment xmlns="http://www.holeschak.de/BmwDeepObd"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://www.holeschak.de/BmwDeepObd ../BmwDeepObd.xsd">
+  <page name="tab_errors" fontsize="small" no_update="true">
+    <strings>
+      <string name="tab_errors">Errors</string>
+
+      <string name="ACSM">ACSM Crash security module</string>
+      <string name="CAS">CAS Car access system</string>
+      <string name="CCCBO">CCC-BO Front panel</string>
+      <string name="CCCGW">CCC-GW Gateway</string>
+      <string name="DDE">DDE Diesel elektronic</string>
+      <string name="DSC">DSC Dynamic stability control</string>
+      <string name="EKPS">EKPS Fuel pump control</string>
+      <string name="IHK">IHK Integrated heating climate automatic</string>
+      <string name="KBM">KBM Chassis basic module</string>
+      <string name="KGM">KGM Chassis gateway module</string>
+      <string name="KOMBI">KOMBI Combination instrument</string>
+      <string name="PDC">PDC Park distance control</string>
+      <string name="RLS">RLS Rain main beam sensor</string>
+      <string name="EPS">EPS Electric power steering</string>
+      <string name="ULF">ULF Universal hands-free system</string>
+      <string name="FZD">FZD Roof switch unit</string>
+    </strings>
+    <strings lang="de">
+      <string name="tab_errors">Fehler</string>
+
+      <string name="ACSM">ACSM Crash-Sicherheits-Modul</string>
+      <string name="CAS">CAS Car Access System</string>
+      <string name="CCCBO">CCC-BO Bedienoberfläche</string>
+      <string name="CCCGW">CCC-GW Gateway</string>
+      <string name="DDE">DDE Diesel Elektronik</string>
+      <string name="DSC">DSC Dynamische Stabilitätskontrolle</string>
+      <string name="EKPS">EKPS Kraftstoffpumpensteuerung</string>
+      <string name="IHK">IHK Integrierte Heiz-Klima-Automatik</string>
+      <string name="KBM">KBM Karosserie-Basismodul</string>
+      <string name="KGM">KGM Karosserie-Gateway-Modul</string>
+      <string name="KOMBI">KOMBI Instrumentenkombination</string>
+      <string name="PDC">PDC Park-Distance-Control</string>
+      <string name="RLS">RLS Regen-Fahrlicht-Sensor</string>
+      <string name="EPS">EPS Elektromechanische Servolenkung</string>
+      <string name="ULF">ULF Universale Ladefreisprechelektronik</string>
+      <string name="FZD">FZD Funtionszentrum Dach</string>
+    </strings>
+    <read_errors sgbd_functional="e60.prg">
+      <ecu name="CAS" sgbd="d_cas" />
+      <ecu name="DDE" sgbd="d_motor" />
+      <ecu name="EKPS" sgbd="d_ekp" />
+      <ecu name="DSC" sgbd="d_dsc" />
+      <ecu name="ACSM" sgbd="d_sim" />
+      <ecu name="CCCBO" sgbd="d_mmi" />
+      <ecu name="CCCGW" sgbd="d_mostgw" />
+      <ecu name="IHK" sgbd="d_klima" />
+      <ecu name="KBM" sgbd="d_kbm" />
+      <ecu name="KGM" sgbd="d_zgm" />
+      <ecu name="KOMBI" sgbd="d_kombi" />
+      <ecu name="PDC" sgbd="d_pdc" />
+      <ecu name="RLS" sgbd="d_rls" />
+      <ecu name="EPS" sgbd="d_eps" />
+      <ecu name="ULF" sgbd="d_ispb" />
+      <ecu name="FZD" sgbd="d_fzd" />
+    </read_errors>
+  </page>
+</fragment>
+```
+In the `ecu` node the property `name` is a link to a `string` node and `sgbd` is the name of the sgbd file. The output looks similar to this page:
+
+![Erros E90](Page_specification_AppReadAllErrorsSmall.png)
+
+# User defined code
+If the jobs and display output is getting more complex, user defined code will be required.  
+In this case a C# class could be added to a `code` node, which defines a set of optional callback functions. If the `show_warnings` property is set to true, also warnings will be reported during compilation of the code.  
+``` xml
+    <code show_warnings="true">
+      <![CDATA[
+    class PageClass
+    {
+        public void CreateLayout(ActivityMain activity, JobReader.PageInfo pageInfo, LinearLayout pageLayout)
+        {
+        }
+
+        public void DestroyLayout(JobReader.PageInfo pageInfo)
+        {
+        }
+
+        public void UpdateLayout(JobReader.PageInfo pageInfo, bool pageValid, bool threadActive)
+        {
+        }
+
+        public void ExecuteJob(EdiabasNet ediabas, ref Dictionary<string, EdiabasNet.ResultData> resultDict, bool firstCall)
+        {
+        }
+
+        public void ExecuteJob(EdiabasNet ediabas, ref MultiMap<string, EdiabasNet.ResultData> resultDict, bool firstCall)
+        {
+        }
+
+        public string FormatResult(JobReader.PageInfo pageInfo, Dictionary<string, EdiabasNet.ResultData> resultDict, string resultName)
+        {
+        }
+
+        public string FormatResult(JobReader.PageInfo pageInfo, Dictionary<string, EdiabasNet.ResultData> resultDict, string resultName, ref Android.Graphics.Color? textColor)
+        {
+        }
+
+        public string FormatResult(JobReader.PageInfo pageInfo, MultiMap<string, EdiabasNet.ResultData> resultDict, string resultName, ref Android.Graphics.Color? textColor)
+        {
+        }
+
+        public string FormatResult(JobReader.PageInfo pageInfo, MultiMap<string, EdiabasNet.ResultData> resultDict, string resultName, ref Android.Graphics.Color? textColor, ref double? dataValue)
+        {
+        }
+
+        public string FormatErrorResult(JobReader.PageInfo pageInfo, EdiabasThread.EdiabasErrorReport errorReport, string defaultMessage)
+        {
+        }
+
+        public void ProcessResults(Context context, JobReader.PageInfo pageInfo, MultiMap<string, EdiabasNet.ResultData> resultDict)
+        {
+        }
+
+        public void UpdateResultList(JobReader.PageInfo pageInfo, Dictionary<string, EdiabasNet.ResultData> resultDict, List<TableResultItem> resultList)
+        {
+        }
+
+        public void UpdateResultList(JobReader.PageInfo pageInfo, MultiMap<string, EdiabasNet.ResultData> resultDict, List<TableResultItem> resultList)
+        {
+        }
+    }
+      ]]>
+    </code>
+```
+
+
+## Formatting results (FormatResult)
+
+For special formatting of the result data, the callback `FormatResult` could be used. For each result of the EDIABAS results this function will be called with `resultName` set to the current result name. If the `display` node is a subnode of a `job` node the job name is prefixed with # as separator to the result name. If the `job` node contains an `id` attribute, the job name is `<id>#<result name>`. The function will be only called if there is **no** `format` property in the `display` node.   
+[FK] if the data is acquired using ExecuteJob, ```resultName```  normally does not contain a prefix but just the result name itself. 
+
+The `textColor` output could be also used for the gauge color.  
+Especially for gauges there is the possibility to return custom values in `dataValue`, if scaling is required or the result has to be converted in a double value first.   
+[FK] I experienced that scaling/calculation does only change the value, but not the input for the gauge itself - it still uses the unmodified original value. See second example below for details. 
+
+Here is an example from the motor page:
+
+``` cs
+        public string FormatResult(JobReader.PageInfo pageInfo, MultiMap<string, EdiabasNet.ResultData> resultDict, string resultName, ref Android.Graphics.Color? textColor, ref double? dataValue)
+        {
+            string result = string.Empty;
+            double value;
+            bool found;
+
+            switch (resultName)
+            {
+                case "STATUS_MESSWERTBLOCK_LESEN#STAT_UBATT_WERT":
+                    value = ActivityMain.GetResultDouble(resultDict, resultName, 0, out found);
+                    result = string.Format(ActivityMain.Culture, "{0,7:0.00}", value);
+                    if (found && value < 11.0)
+                    {
+                        textColor = Android.Graphics.Color.Red;
+                    }
+                    if (!found) result = string.Empty;
+                    break;
+
+                case "STATUS_MESSWERTBLOCK_LESEN#STAT_STRECKE_SEIT_ERFOLGREICHER_REGENERATION_WERT":
+                    result = string.Format(ActivityMain.Culture, "{0,6:0.0}", ActivityMain.GetResultDouble(resultDict, resultName, 0, out found) / 1000.0);
+                    if (!found) result = string.Empty;
+                    break;
+
+                case "STATUS_MESSWERTBLOCK_LESEN#STAT_OELDRUCKSCHALTER_EIN_WERT":
+                    result = ((ActivityMain.GetResultDouble (resultDict, resultName, 0, out found) > 0.5) && found) ? "1" : "0";
+                    if (found && result == "1")
+                    {
+                        textColor = Android.Graphics.Color.Red;
+                    }
+                    if (!found) result = string.Empty;
+                    break;
+
+                case "STATUS_MESSWERTBLOCK_LESEN#STAT_REGENERATIONSANFORDERUNG_WERT":
+                    result = ((ActivityMain.GetResultDouble (resultDict, resultName, 0, out found) < 0.5) && found) ? "1" : "0";
+                    if (!found) result = string.Empty;
+                    break;
+
+                case "STATUS_MESSWERTBLOCK_LESEN#STAT_EGT_st_WERT":
+                    result = ((ActivityMain.GetResultDouble (resultDict, resultName, 0, out found) > 1.5) && found) ? "1" : "0";
+                    if (!found) result = string.Empty;
+                    break;
+
+                case "STATUS_MESSWERTBLOCK_LESEN#STAT_REGENERATION_BLOCKIERUNG_UND_FREIGABE_WERT":
+                    result = ((ActivityMain.GetResultDouble (resultDict, resultName, 0, out found) < 0.5) && found) ? "1" : "0";
+                    if (!found) result = string.Empty;
+                    break;
+            }
+            return result;
+        }
+```
+
+   
+
+#### Colorized gauges/values depending on the value
+
+```cs
+public string FormatResult(JobReader.PageInfo pageInfo, MultiMap<string, EdiabasNet.ResultData> resultDict, string resultName, ref Android.Graphics.Color? textColor)
+{
+    string result = string.Empty;
+    double value;
+    bool found;
+
+    switch (resultName)
+    {
+        // engine speed gauge
+        case "STATUS_MESSWERTBLOCK_LESEN#STAT_MOTORDREHZAHL_WERT":
+            value = ActivityMain.GetResultDouble(resultDict, resultName, 0, out found);
+            
+            // result format: {0,[DIGITS TOTAL INCL COMMA],[0|0.0|0.00|0.000...]}
+            result = string.Format(ActivityMain.Culture, "{0,4:0}", value);
+            
+            // list of possible color names can be found here: 
+            // https://docs.microsoft.com/en-us/dotnet/api/android.graphics.color?view=xamarin-android-sdk-9
+            if (found && value <= 4000) textColor = Android.Graphics.Color.White;
+            else if (found && value <= 4250) textColor = Android.Graphics.Color.Yellow;
+            else if (found && value <= 4500) textColor = Android.Graphics.Color.Orange;
+            else if (found && value > 4500) textColor = Android.Graphics.Color.Red;
+            else textColor = Android.Graphics.Color.Gray;
+            
+            break;
+        
+        // battery voltage gauge		
+        case "STATUS_MESSWERTBLOCK_LESEN#STAT_UBATT_WERT":
+            value = ActivityMain.GetResultDouble(resultDict, resultName, 0, out found);
+            
+            // result format: {0,[DIGITS TOTAL INCL COMMA],[0|0.0|0.00|0.000...]}
+            result = string.Format(ActivityMain.Culture, "{0,4:0.0}", value);
+            
+            if (found && value < 10.5) textColor = Android.Graphics.Color.Red;
+            else if (found && value < 11.5) textColor = Android.Graphics.Color.Orange;
+            else if (found && value <= 12.5) textColor = Android.Graphics.Color.Yellow;
+            else if (found && value <= 13.2) textColor = Android.Graphics.Color.Gray;
+            else if (found && value <= 14.5) textColor = Android.Graphics.Color.White;
+            else if (found && value > 14.5) textColor = Android.Graphics.Color.Red;
+            else textColor = Android.Graphics.Color.Gray;
+            
+            break;
+
+        // coolant temperature gauge
+        case "STATUS_MESSWERTBLOCK_LESEN#STAT_KUEHLMITTELTEMPERATUR_WERT":
+            value = ActivityMain.GetResultDouble(resultDict, resultName, 0, out found);
+            result = string.Format(ActivityMain.Culture, "{0,3:0}", value);
+            
+            if (found && value < 75) textColor = Android.Graphics.Color.Blue;
+            else if (found && value <= 105) textColor = Android.Graphics.Color.White;
+            else if (found && value <= 110) textColor = Android.Graphics.Color.Yellow;
+            else if (found && value <= 115) textColor = Android.Graphics.Color.Orange;
+            else if (found && value > 115) textColor = Android.Graphics.Color.Red;
+            else textColor = Android.Graphics.Color.Gray;
+            
+            break;
+    }
+    return result;
+}
+```
+
+   
+
+#### Calculating result values
+
+Another example. Here we are using the DME MS43 after-cat O2 sensors inputs as analog inputs for custom sensors, namely a MAP sensor of type MPX4250AP and the analog output of a Innovate LC-2 wideband lambda controller.  
+Note: this is a track car with supercharger, modified exhaust and headers (under floor cat instead of original headers mounted, monitor O2 sensors stripped), so read O2 inputs are not used any more and also disabled in the ECU tune for their original purpose. 
+Wideband and MAP sensor are only added for monitoring/logging and display.  
+Values are acquired using ExecuteJob function in this example, so the ```resultName``` is not prefixed. In this case the gauge´s min/max in the display tag is 0 to 3.55 for the MAP sensor and 0 to 5 for the Wideband in order to fit display value and gauge. This is needed as the gauge itself continues to display the original value from the ECU, not the calculated one. 
+
+```cs
+public string FormatResult(JobReader.PageInfo pageInfo, MultiMap<string, EdiabasNet.ResultData> resultDict, string resultName, ref Android.Graphics.Color? textColor)
+    {
+        string result = string.Empty;
+        double value;
+        bool found;
+
+        switch (resultName)
+        {
+            // conversion for MAP Sensor type MPX4250AP
+            case "STAT_LS_NKAT_SIGNAL_1_WERT":
+                value = ActivityMain.GetResultDouble(resultDict, resultName, 0, out found);
+                value = ((( value / 4.9 ) + 0.04 ) / 0.004) * 10;
+                result = string.Format(ActivityMain.Culture, "{0,4:0}", value);
+                
+                if (found && value < 200) textColor = Android.Graphics.Color.Black;
+                else if (found && value < 1000) textColor = Android.Graphics.Color.White;
+                else if (found && value < 1500) textColor = Android.Graphics.Color.Orange;
+                else textColor = Android.Graphics.Color.Red;
+                
+                break;
+            
+            // conversion for Innovate LC-2 Wideband controller
+            case "STAT_LS_NKAT_SIGNAL_2_WERT":
+                value = ActivityMain.GetResultDouble(resultDict, resultName, 0, out found);
+                value = (1.523-0.5) / 5.0 * value + 0.5;
+                result = string.Format(ActivityMain.Culture, "{0,5:0.00}", value);
+                
+                if (found && value < 0.85) textColor = Android.Graphics.Color.Red;
+                else if (found && value < 0.90) textColor = Android.Graphics.Color.Orange;
+                else if (found && value < 0.95) textColor = Android.Graphics.Color.Yellow;
+                else if (found && value <= 1.05) textColor = Android.Graphics.Color.White;
+                else if (found && value <= 1.10) textColor = Android.Graphics.Color.Yellow;
+                else if (found && value <= 1.15) textColor = Android.Graphics.Color.Orange;
+                else if (found && value >1.15) textColor = Android.Graphics.Color.Red;
+                else textColor = Android.Graphics.Color.White;
+                break;
+                
+        }
+    }
+```
+
+
+
+
+
+
+## Formatting error results (FormatErrorResult)
+
+For special formatting of the error result data, the callback `FormatErrorResult` could be used. For each error entry this function will be called with `defaultMessage` set to the default error message output.  
+Here is an example from the errors page, that adds a RPM value to the error message. You have to add a `results` property to the `ecu` node specifying the results you want to be generated by the `FS_LESEN_DETAIL` job.
+``` xml
+    <read_errors>
+      <ecu name="CAS" sgbd="d_cas" />
+      <ecu name="DDE" sgbd="d_motor" results="F_UW_KM;F_UW_ANZ" />
+    </read_errors>
+    <code show_warnings="true">
+      <![CDATA[
+    class PageClass
+    {
+        public string FormatErrorResult(JobReader.PageInfo pageInfo, EdiabasThread.EdiabasErrorReport errorReport, string defaultMessage)
+        {
+            string message = defaultMessage;
+            switch (errorReport.EcuName)
+            {
+                case "DDE":
+                {
+                    string detailText = string.Empty;
+                    foreach (Dictionary<string, EdiabasNet.ResultData> errorDetail in errorReport.ErrorDetailSet)
+                    {
+                        string rpmText = ActivityMain.FormatResultDouble(errorDetail, "F_UW1_WERT", "{0,6:0.0}");
+                        if (rpmText.Length > 0)
+                        {
+                            if (detailText.Length == 0)
+                            {
+                                detailText += rpmText + " 1/min";
+                            }
+                        }
+                    }
+                    if (detailText.Length > 0)
+                    {
+                        message += "\r\n" + detailText;
+                    }
+                    break;
+                }
+            }
+            return message;
+        }
+    }
+      ]]>
+  </code>
+```
+
+
+
+## Processing results (ProcessResults)
+
+For general processing of the result data, the callback `ProcessResults` could be used.  
+In this example a notification is displayed if the battery voltage is too low.  
+It uses the following functions to display the notifications:
+``` cs
+public static bool ShowNotification(Context context, int id, int priority, string title, string message, bool update = false);
+public static bool HideNotification(Context context, int id);
+```
+The functions arguments are:
+* `context`: The current application context.
+* `id`: The notification id. It should be a value in the range of 0 and 9999. The same value must be used for `ShowNotification` and `HideNotification`.
+* `priority`: The notification priority in the range of -2 to 2.
+* `title`: The notification title.
+* `title`: The notification message.
+* `update`: If the notfication is already displayed, it will be not updated by default. When this value is `true` the current notification is updated.
+
+``` xml
+    <strings>
+      <string name="notification_title_battery">Battery</string>
+      <string name="notification_voltage_low">Voltage too low!</string>
+    </strings>
+    <strings lang="de">
+      <string name="notification_title_battery">Batterie</string>
+      <string name="notification_voltage_low">Spannung zu niedrig!</string>
+    </strings>
+
+    <code show_warnings="true">
+      <![CDATA[
+    class PageClass
+    {
+        public void ProcessResults(Context context, JobReader.PageInfo pageInfo, MultiMap<string, EdiabasNet.ResultData> resultDict)
+        {
+            double value;
+            bool found;
+
+            value = ActivityMain.GetResultDouble(resultDict, "STATUS_MESSWERTBLOCK_LESEN#STAT_UBATT_WERT", 0, out found);
+            if (found && value < 11.5)
+            {
+                ActivityMain.ShowNotification(context, 0, 2,
+                  ActivityMain.GetPageString(pageInfo, "notification_title_battery"),
+                  ActivityMain.GetPageString(pageInfo, "notification_voltage_low"));
+            }
+            else
+            {
+                ActivityMain.HideNotification(context, 0);
+            }
+        }
+    }
+      ]]>
+  </code>
+```
+
+
+
+## Control output of the page (UpdateResultList)
+
+Sometimes you want to dynamically control the number and the content of the output lines.  
+If the callback `UpdateResultList` is defined, you could directly fill the contents of the `resultListAdapter` which displays the results of the page. In this example from the (standard) _Adapter_ page the adapter configuration result will be displayed. Additionally only one column for output is used by setting the second argument of `resultListAdapter.Items.Add` to null. With `ActivityMain.GetPageString` it's possible to retrieve a string from the translation table.
+
+``` cs
+        public void UpdateResultList(JobReader.PageInfo pageInfo, MultiMap<string, EdiabasNet.ResultData> resultDict, List<TableResultItem> resultList)
+        {
+            int result = configResult;
+
+            if (result > 0)
+            {
+                resultList.Add(new TableResultItem(ActivityMain.GetPageString(pageInfo, "adapter_config_ok"), null));
+            }
+            else if (result == 0)
+            {
+                resultList.Add(new TableResultItem(ActivityMain.GetPageString(pageInfo, "adapter_config_error"), null));
+            }
+        }
+```
+
+
+
+## Executing own jobs (ExecuteJob)
+
+If more than one job has to be executed or the job requires special arguments, `ediabas.ExecuteJob` could be called in the `ExecuteJob` callback. Here is an example from the adapter page for calling a list of jobs. `EdiabasThread.MergeResultDictionarys` adds the results of the current job to the internal job list. The callback `ExecuteJob` will be executed in it's own thread. When using ExecuteJob it's recommended to add the `display` nodes to the `page` node because no `job` nodes will be present.   
+
+
+Note: using **ExecuteJob** the page should not contain nodes in the following format: 
+
+```xml
+<jobs sgbd="ms430ds0">
+    <job name="STATUS_MESSWERTEBLOCK">
+        <display name="!JOB#STAT_MOTORDREHZAHL_MWB_WERT" result="STAT_MOTORDREHZAHL_MWB_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="7000" log_tag="STAT_MOTORDREHZAHL_MWB_WERT" />
+        <display name="!JOB#STAT_GESCHWINDIGKEIT_MWB_WERT" result="STAT_GESCHWINDIGKEIT_MWB_WERT" format="L" grid-type="simple-gauge-round" min-value="0" max-value="250"  log_tag="STAT_GESCHWINDIGKEIT_MWB_WERT" />
+  </job>
+</jobs>
+```
+
+but instead only one <jobs> tag defining the SGBD file that is used, just before the <code>  tag: 
+
+```xml
+<jobs sgbd="ms430ds0" /> 
+<code show_warnings="true">
+```
+
+and as described the <display ...> tags directly below <page ...>   
+
+
+
+#### Example
+
+
+``` cs
+    class PageClass
+    {
+        private class EdiabasJob
+        {
+            private string jobName;
+            private string jobArgs;
+            private string resultRequests;
+
+            public EdiabasJob(string jobName, string jobArgs, string resultRequests)
+            {
+                this.jobName = jobName;
+                this.jobArgs = jobArgs;
+                this.resultRequests = resultRequests;
+            }
+
+            public string JobName
+            {
+                get
+                {
+                    return jobName;
+                }
+            }
+
+            public string JobArgs
+            {
+                get
+                {
+                    return jobArgs;
+                }
+            }
+
+            public string ResultRequests
+            {
+                get
+                {
+                    return resultRequests;
+                }
+            }
+        }
+
+        static private readonly EdiabasJob[]() jobArray =
+            {
+                new EdiabasJob("ADAPTER_CMD",
+                    "0xFE;0xFE",
+                    string.Empty
+                    ),
+                new EdiabasJob("ADAPTER_CMD",
+                    "0x80;0x00",
+                    string.Empty
+                    ),
+                new EdiabasJob("ADAPTER_CMD",
+                    "0x81;0x00",
+                    string.Empty
+                    ),
+                new EdiabasJob("ADAPTER_CMD",
+                    "0x82;0x00",
+                    string.Empty
+                    ),
+            };
+
+        public void ExecuteJob(EdiabasNet ediabas, ref MultiMap<string, EdiabasNet.ResultData> resultDict, bool firstCall)
+        {
+            int index = 0;
+            foreach (EdiabasJob job in jobArray)
+            {
+                ediabas.ArgString = job.JobArgs;
+                ediabas.ArgBinaryStd = null;
+                ediabas.ResultsRequests = job.ResultRequests;
+
+                ediabas.ExecuteJob(job.JobName);
+
+                List<Dictionary<string, EdiabasNet.ResultData>> resultSets = ediabas.ResultSets;
+                if (resultSets != null && resultSets.Count >= 2)
+                {
+                    EdiabasThread.MergeResultDictionarys(ref resultDict, resultSets[1](1), string.Format("{0}_", index));
+                }
+                index++;
+            }
+        }
+    }
+```
+
+
+
+#### Example using *firstCall*
+
+Simple **ExecuteJob** example making use of *firstCall*.  
+On the first run an initial job is performed - in this case it switches the DDE to *cylinder system check mode*.  
+After that the actual values are read on every iteration (which would not output valid data without first switching the DDE to this mode using the firstCall job):  
+
+```cs
+public void ExecuteJob(EdiabasNet ediabas, ref MultiMap<string, EdiabasNet.ResultData> resultDict, bool firstCall)
+    {   
+        if (firstCall) {
+            ediabas.ArgString = string.Empty;
+            ediabas.ArgBinaryStd = null;
+            ediabas.ResultsRequests = "JOB_STATUS";
+            ediabas.ExecuteJob("START_SYSTEMCHECK_ZYL");
+        }
+        else {
+            ediabas.ArgString = string.Empty;
+            ediabas.ArgBinaryStd = null;
+            ediabas.ResultsRequests = "STAT_LAUFUNRUHE_LLR_MENGE_ZYL1_WERT;STAT_LAUFUNRUHE_LLR_MENGE_ZYL2_WERT;STAT_LAUFUNRUHE_LLR_MENGE_ZYL3_WERT;STAT_LAUFUNRUHE_LLR_MENGE_ZYL4_WERT;STAT_LAUFUNRUHE_LLR_MENGE_ZYL5_WERT;STAT_LAUFUNRUHE_LLR_MENGE_ZYL6_WERT";
+            ediabas.ExecuteJob("STATUS_LAUFUNRUHE_LLR_MENGE");
+            
+            List<Dictionary<string, EdiabasNet.ResultData>> resultSets = ediabas.ResultSets;
+            if (resultSets != null && resultSets.Count >= 2)
+            {
+                EdiabasThread.MergeResultDictionarys(ref resultDict, resultSets[1]);
+            }
+        }
+    }
+```
+
+
+
+#### Multiple Jobs on the same ECU
+
+Another example that performs 2 different Jobs of the same SGBD.  
+Job 2 is less important and is only performed at every 20th iteration, in order to speed up the update rate of Job 1.  
+Note Job 2´s result data is stored globally and the last values are always added to the output (otherwise it would only be visible for a short time): 
+
+```cs
+<jobs sgbd="D50M57E1" />
+<code show_warnings="true">
+<![CDATA[
+class PageClass
+{   
+    // define interval for Job 2 - it will only be performed on every X iteration
+    private static int Job2_interval = 20;
+    
+    // counter for Job 2 handling
+    private int counter_Job2 = 0;
+    
+    // result objects for additional jobs that are not aquired on every iteration
+    // this is defined globally to save values between iterations and beeing able to output the 
+    // last values every time though the data is "old"
+    List<Dictionary<string, EdiabasNet.ResultData>> resultSets_Job2;
+    
+    public void ExecuteJob(EdiabasNet ediabas, ref MultiMap<string, EdiabasNet.ResultData> resultDict, bool firstCall)
+    {
+        // resultSets for Job 1
+        List<Dictionary<string, EdiabasNet.ResultData>> resultSets;
+        
+        // Job 1 - performed on every iteration
+        ediabas.ArgString = "JA;ILMMG;IPLAD;IPUMG;IUBAT;IMOAK;INMOT;SPLAD;ITKUM;ITLAL;IPRDR;IAFZG;IVKMH";
+        ediabas.ArgBinaryStd = null;
+        ediabas.ResultsRequests = string.Empty;
+        ediabas.ExecuteJob("STATUS_MESSWERTBLOCK_LESEN");
+        resultSets = ediabas.ResultSets;
+        if (resultSets != null && resultSets.Count >= 2)
+        {
+            EdiabasThread.MergeResultDictionarys(ref resultDict, resultSets[1]);
+        }
+        
+        // Job 2 - only performed on every X iteration (defined by Job2_interval)
+        // results are stored in a global variable so that the last values can be displayed every time
+        if(counter_Job2 == 0) {
+            ediabas.ArgString = "lla";
+            ediabas.ArgBinaryStd = null;
+            ediabas.ResultsRequests = string.Empty;
+            ediabas.ExecuteJob("ABGLEICH_LESEN");
+            resultSets_Job2 = ediabas.ResultSets;
+            counter_Job2++;
+        }
+        else if(counter_Job2 >= Job2_interval) {
+            counter_Job2 = 0;
+        }
+        else {
+            counter_Job2++;
+        }
+        
+        // Job 2 - merge last results to the output result list 
+        if (resultSets_Job2 != null && resultSets_Job2.Count >= 2)
+        {
+            // prefix the output result names of Job 2 with "LLA_" as result name of that job is not descriptive
+            EdiabasThread.MergeResultDictionarys(ref resultDict, resultSets_Job2[1], "LLA_");
+        }
+    }
+}
+  ]]>
+</code>
+```
+
+
+
+#### Multiple Jobs on different ECUs
+
+Even more advanced example, adding another Job to the last example which is using a different SGBD.   
+Note the now empty <jobs /> tag as the used SGBDs are loaded programmatically here.   
+The Job on the 2nd SGBD is much less important in this case and therefore only updated on every 98th iteration, so that it doesn´t slow down the update rate of the main job too much. Also there is some "error handling" if the Job on SGBD 2 fails. 
+
+```cs
+<jobs />
+<code show_warnings="true">
+<![CDATA[
+class PageClass
+{   
+    // define interval for Job 2 - it will only be performed on every X iteration
+    private int Job2_interval = 20;        // Job 2 = another job on the same ECU as Job 1
+    private int JobSgbd2_interval = 98;    // JobSgbd2 = another Job on a different ECU
+
+    // define ECU SGBD files used
+    public static string[] conf_sgbds = {"D50M57E1", "kombi46r"};
+    
+    private int currSgbd = 0; // currently active SGBD (index of conf_sgbds array)
+    
+    private int counter_Job2 = 0;
+    private int counter_JobSgbd2 = 0;
+    
+    // result objects for additional jobs that are not aquired on every iteration
+    // this is defined globally to save values between iterations and beeing able to output the 
+    // last values every time though the data is "old"
+    List<Dictionary<string, EdiabasNet.ResultData>> resultSets_Job2;
+    List<Dictionary<string, EdiabasNet.ResultData>> resultSets_JobSgbd2;
+        
+    public void ExecuteJob(EdiabasNet ediabas, ref MultiMap<string, EdiabasNet.ResultData> resultDict, bool firstCall)
+    {
+        // resultSets for Job 1 (updated every time)
+        List<Dictionary<string, EdiabasNet.ResultData>> resultSets;
+
+        // switch to SGBD number 1 (index 0) on first run or if last used SGBD was a different one
+        if(firstCall || currSgbd != 0) {
+            currSgbd = 0;
+            ediabas.ResolveSgbdFile(conf_sgbds[0]);
+            // possibly needed initialisation
+            //ediabas.ArgString = string.Empty;
+            //ediabas.ArgBinaryStd = null;
+            //ediabas.ResultsRequests = string.Empty;
+            //ediabas.ExecuteJob("INITIALISIERUNG");
+        }
+        
+        
+        // Job 1 - performed on every iteration
+        ediabas.ArgString = "JA;ILMMG;IPLAD;IPUMG;IUBAT;IMOAK;INMOT;SPLAD;ITKUM;ITLAL;IPRDR;IAFZG;IVKMH";
+        ediabas.ArgBinaryStd = null;
+        ediabas.ResultsRequests = string.Empty;
+        ediabas.ExecuteJob("STATUS_MESSWERTBLOCK_LESEN");
+        resultSets = ediabas.ResultSets;
+        if (resultSets != null && resultSets.Count >= 2)
+        {
+            EdiabasThread.MergeResultDictionarys(ref resultDict, resultSets[1]);
+        }
+        
+        
+        
+        // Job 2 - only performed on every X iteration (defined by Job2_interval)
+        // results are stored in a global variable so that the last values can be displayed every time
+        if(counter_Job2 == 0) {
+            ediabas.ArgString = "lla";
+            ediabas.ArgBinaryStd = null;
+            ediabas.ResultsRequests = string.Empty;
+            ediabas.ExecuteJob("ABGLEICH_LESEN");
+            resultSets_Job2 = ediabas.ResultSets;
+            counter_Job2++;
+        }
+        else if(counter_Job2 >= Job2_interval) {
+            counter_Job2 = 0;
+        }
+        else {
+            counter_Job2++;
+        }
+        
+        // Job 2 - merge last results to the output result list 
+        if (resultSets_Job2 != null && resultSets_Job2.Count >= 2)
+        {
+            // prefix the output result names of Job 2 with "LLA_" as result name of that job is not descriptive
+            EdiabasThread.MergeResultDictionarys(ref resultDict, resultSets_Job2[1], "LLA_");
+        }
+        
+        
+        
+        // Job on SGBD 2 - only run on every Y iteration (defined by JobSgbd2_interval)
+        if(counter_JobSgbd2 == 0) {
+            counter_JobSgbd2++;
+            try {
+                // try/catch, to prevent an error here destroys the whole results of all jobs 
+                // - otherwise NO data will be displayed if the SGBD 2 request fails 
+                
+                // switch to SGBD number 2 (index 1)
+                currSgbd = 1;
+                ediabas.ResolveSgbdFile(conf_sgbds[currSgbd]);
+                
+                // if needed - initialisation
+                //ediabas.ArgString = string.Empty;
+                //ediabas.ArgBinaryStd = null;
+                //ediabas.ResultsRequests = string.Empty;
+                //ediabas.ExecuteJob("INITIALISIERUNG");
+                            
+                ediabas.ArgString = string.Empty;
+                ediabas.ArgBinaryStd = null;
+                ediabas.ResultsRequests = string.Empty;
+                ediabas.ExecuteJob("STATUS_TOENS_IO");
+                resultSets_JobSgbd2 = ediabas.ResultSets;
+            }
+            catch {
+                // ok then not
+            }
+        }
+        else if (counter_JobSgbd2 >= JobSgbd2_interval) {
+            counter_JobSgbd2 = 0;
+        }
+        else {
+            counter_JobSgbd2++;
+        }
+        
+        // Job SGBD 2 - merge last results to the output result list 
+        if (resultSets_JobSgbd2 != null && resultSets_JobSgbd2.Count >= 2)
+            {
+                // in this case with NO prefix as result names are descriptive
+                EdiabasThread.MergeResultDictionarys(ref resultDict, resultSets_JobSgbd2[1]);
+            }        
+    }
+}
+  ]]>
+</code>
+```
+
+
+
+## Adding controls to the layout
+
+The standard layout only allows to display information, but there is no way to control outputs. With the callbacks `CreateLayout`, `DestroyLayout` and `UpdateLayout` there is a possibility to add own controls to the layout (in most cases buttons).  
+In this example from the AdapterCustom.ccpage buttons will be added to control the CAN block size, CAN separation time and the CAN mode. The `CreateLayout` adds the controls, `DestroyLayout` removes the controls and `UpdateLayout` is used to modify the state of the controls (depending form the connection state).  
+For every button there is a `Click` delegate that allows to set a global variable which is used for EDIABAS job control.
+``` cs
+        private Button buttonBlockSize;
+        private Button buttonSepTime0;
+        private Button buttonSepTime1;
+        private Button buttonCan500;
+        private Button buttonCan100;
+        private Button buttonCanOff;
+        private int adapterCmd = -1;
+        private int adapterValue;
+        private int blockSize = 0;
+
+        public void CreateLayout(ActivityMain activity, JobReader.PageInfo pageInfo, LinearLayout pageLayout)
+        {
+            LinearLayout buttonLayout = new LinearLayout(activity);
+            buttonLayout.Orientation = Orientation.Vertical;
+
+            LinearLayout.LayoutParams buttonLayoutParams = new LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MatchParent,
+                ViewGroup.LayoutParams.WrapContent);
+            buttonLayoutParams.Weight = 1;
+
+            buttonBlockSize = new Button(activity);
+            buttonBlockSize.Text = string.Format("{0}: {1}", ActivityMain.GetPageString(pageInfo, "button_adapter_config_block_size"), blockSize);
+            buttonBlockSize.Click += delegate
+            {
+                blockSize += 2;
+                if (blockSize > 8)
+                {
+                    blockSize = 0;
+                }
+                adapterCmd = 0x00;
+                adapterValue = blockSize;
+                buttonBlockSize.Text = string.Format("{0}: {1}", ActivityMain.GetPageString(pageInfo, "button_adapter_config_block_size"), blockSize);
+            };
+            buttonLayout.AddView(buttonBlockSize, buttonLayoutParams);
+
+            buttonSepTime0 = new Button(activity);
+            buttonSepTime0.Text = ActivityMain.GetPageString(pageInfo, "button_adapter_config_sep_time0");
+            buttonSepTime0.Click += delegate
+            {
+                adapterCmd = 0x01;
+                adapterValue = 0x00;
+            };
+            buttonLayout.AddView(buttonSepTime0, buttonLayoutParams);
+
+            buttonSepTime1 = new Button(activity);
+            buttonSepTime1.Text = ActivityMain.GetPageString(pageInfo, "button_adapter_config_sep_time1");
+            buttonSepTime1.Click += delegate
+            {
+                adapterCmd = 0x01;
+                adapterValue = 0x01;
+            };
+            buttonLayout.AddView(buttonSepTime1, buttonLayoutParams);
+
+            buttonCan500 = new Button(activity);
+            buttonCan500.Text = ActivityMain.GetPageString(pageInfo, "button_adapter_config_can_500");
+            buttonCan500.Click += delegate
+            {
+                adapterCmd = 0x02;
+                adapterValue = 0x01;
+            };
+            buttonLayout.AddView(buttonCan500, buttonLayoutParams);
+
+            buttonCan100 = new Button(activity);
+            buttonCan100.Text = ActivityMain.GetPageString(pageInfo, "button_adapter_config_can_100");
+            buttonCan100.Click += delegate
+            {
+                adapterCmd = 0x02;
+                adapterValue = 0x09;
+            };
+            buttonLayout.AddView(buttonCan100, buttonLayoutParams);
+
+            buttonCanOff = new Button(activity);
+            buttonCanOff.Text = ActivityMain.GetPageString(pageInfo, "button_adapter_config_can_off");
+            buttonCanOff.Click += delegate
+            {
+                adapterCmd = 0x02;
+                adapterValue = 0x00;
+            };
+            buttonLayout.AddView(buttonCanOff, buttonLayoutParams);
+
+            LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MatchParent,
+                ViewGroup.LayoutParams.WrapContent);
+            pageLayout.AddView(buttonLayout, layoutParams);
+
+            adapterCmd = -1;
+        }
+
+        public void DestroyLayout(JobReader.PageInfo pageInfo)
+        {
+            if (buttonBlockSize != null)
+            {
+                buttonBlockSize.Dispose();
+                buttonBlockSize = null;
+            }
+            if (buttonSepTime0 != null)
+            {
+                buttonSepTime0.Dispose();
+                buttonSepTime0 = null;
+            }
+            if (buttonSepTime1 != null)
+            {
+                buttonSepTime1.Dispose();
+                buttonSepTime1 = null;
+            }
+            if (buttonCan500 != null)
+            {
+                buttonCan500.Dispose();
+                buttonCan500 = null;
+            }
+            if (buttonCan100 != null)
+            {
+                buttonCan100.Dispose();
+                buttonCan100 = null;
+            }
+            if (buttonCanOff != null)
+            {
+                buttonCanOff.Dispose();
+                buttonCanOff = null;
+            }
+            //Android.Util.Log.Info("Custom", "Destroyed");
+        }
+
+        public void UpdateLayout(JobReader.PageInfo pageInfo, bool pageValid, bool threadActive)
+        {
+            if ((buttonCan500 == null) || (buttonCan100 == null) || (buttonCanOff == null))
+            {
+                return;
+            }
+
+            bool enabled = pageValid && threadActive;
+            buttonBlockSize.Enabled = enabled;
+            buttonSepTime0.Enabled = enabled;
+            buttonSepTime1.Enabled = enabled;
+            buttonCan500.Enabled = enabled;
+            buttonCan100.Enabled = enabled;
+            buttonCanOff.Enabled = enabled;
+        }
+    }
+```
+The resulting page will look like this:
+
+![Adapter page](Page_specification_AdapterConfigSmall.png)
+
+## Receiving broadcasts (BroadcastReceived)
+For interaction of the user code with other apps the broadcast `de.holeschak.bmw_deep_obd.Action.Command` could by processed by the function `BroadcastReceived`.  
+Here is an example from the axis page, that changes the axis direction with the broadcast.
+``` xml
+    <code show_warnings="true">
+      <![CDATA[
+    class PageClass
+    {
+        public void BroadcastReceived(JobReader.PageInfo pageInfo, Android.Content.Context context, Android.Content.Intent intent)
+        {
+            string request = intent.GetStringExtra("custom_action");
+            if (string.IsNullOrEmpty(request))
+            {
+                return;
+            }
+            request = request.ToLowerInvariant();
+            switch (request)
+            {
+                case "mode_status":
+                    opMode = OpModeStatus;
+                    break;
+
+                case "mode_up":
+                    opMode = OpModeUp;
+                    break;
+
+                case "mode_down":
+                    opMode = OpModeDown;
+                    break;
+            }
+        }
+    }
+      ]]>
+  </code>
+```
+
+# Grouping pages
+If the same of pages are required in multiple configuration, it's useful to group the together. This could be done with `*.ccpages` files. Simply include the `*.ccpage` files withing the `pages` node. The specifified path is relative to the `*.ccpages` file location. The file has the following layout:
+``` xml
+<?xml version="1.0" encoding="utf-8" ?>
+<fragment xmlns="http://www.holeschak.de/BmwDeepObd"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://www.holeschak.de/BmwDeepObd ../BmwDeepObd.xsd">
+  <pages>
+    <include filename="Axis.ccpage"/>
+    <include filename="Motor.ccpage"/>
+    <include filename="../AdapterCustom.ccpage"/>
+  </pages>
+</fragment>
+```
+# The configuration file
+Now all `*.page` or `*.pages` can be added to a configuration file `*.cccfg`. This file could be loaded by _[Deep OBD for BMW and VAG](Deep_OBD_for_BMW_and_VAG.md)_. In the `global` node of the file the following properties could be specified:
+* `ecu_path`: Directory of the ecu files (`*.grp` and `*.prg`) relative to the configuration file.
+* `log_path`: Directory for the data logging files. Logging could be enabled by adding a `log_tag` property to the `display` node of the `*.page` file. If the directory is not existing it will be created.
+* `append_log`: Setting this property to true will always append the log file.
+* `manufacturer`: Select the car manufacturer with this property. Possible values are `BWM`, `VW`, `Audi`, `Seat` and `Skoda`.
+* `interface`: Specify the communication interface in this property. Possible values are `BLUETOOTH` , `ENET`, `ELMWIFI`, `DEEPOBDWIFI` and `FTDI`. When using a manufacturer from the VAG group, only `BLUETOOTH` and `DEEPOBDWIFI` is allowed.
+``` xml
+<?xml version="1.0" encoding="utf-8"?>
+<fragment xmlns="http://www.holeschak.de/BmwDeepObd"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://www.holeschak.de/BmwDeepObd ../BmwDeepObd.xsd">
+  <global ecu_path="../Ecu" log_path="Log" append_log="true" manufacturer="BMW" interface="BLUETOOTH" />
+  <include filename="E61.ccpages"/>
+</fragment>
+```
+# Broadcasts
+The received OBD data could be broadcasted to other apps if broadcast sending is enabled in the [global settings](GlobalSettings.md). This way it's possible to display or process data individually.  
+The broadcast name is `de.holeschak.bmw_deep_obd.Notification.Info`. It contains the following intent data:
+* `action` (string): Change of operation status:
+  * `connect`: OBD connection is connected.
+  * `disconnect`: OBD connection is disconnected.
+  * `page_change`: The current display page changes.
+* `obd_data` (string): JSON object that contains the current OBD data. It has the following format:
+``` json5
+{
+    "PageName":"<tab_name>",
+    "ObdData":
+    [
+        {
+            "Name":"<label_name 1>",
+            "Result":"<job result name 1>",
+            "Value":"<display value 1>"
+        },
+        {
+            "Name":"<label_name 2>",
+            "Result":"<job result name 2>",
+            "Value":"<display value 2>"
+        },
+    ]
+}
+```
+Additionally the broadcast `de.holeschak.bmw_deep_obd.Action.Command` could be received by the App.  
+The following intent data is defined:
+* `action` (string): Action to be processed by the app:
+  * `new_page:<page_name>`: Switches to the new page (configuration) with the name `<page_name>`. `<page_name>` is the name in the node `<page name ="page_name">`.
+
+The broadcast could be also received by the [`BroadcastReceived`](#receiving-broadcasts-broadcastreceived) user function.
+
+# Special jobs
+There are some job, that require special handling:
+* With `STATUS_MESSWERTBLOCK_LESEN` or `STATUS_BLOCK_LESEN` multiples values could be requested simultanously.  
+  The job requires the entries from the table `MESSWERTETAB` or `SG_FUNKTIONEN` with column `ARG` as arument.  
+  The first argument `JA` or `NEIN` specifies if the last ECU request is reused.  
+  The number of arguments is limited by the ECU, the typical limit is 10.
+* For reading `STATUS_MESSWERTBLOCK_X` the corresponding `MESSWERTBLOCK_X_SCHREIBEN` is to be called first once (`MS450DS0.PRG` only).

BIN
docs/Page_specification_AdapterConfigSmall.png


BIN
docs/Page_specification_AppClimateSmall.png


BIN
docs/Page_specification_AppReadAllErrorsSmall.png