Table of Contents
Erstelle Deine eigenen OOBD Skripte
Die “nachladbare Intelligenz” von OOBD ist als Lua Skript realisiert. Was das bedeutet und wie es arbeitet, wird hier beschrieben.
Was ist Lua?
Einfach gesagt ist Lua “etwas” (ein so genannter Skript Interpretierer) der entwickelt wurde, um es einem Programm zu ermoeglichen intern in einem Programmaufruf eines anderen Programms gestartet zu werden. Der Trick dabei ist, das das “interne” Programm, zu dem Zeitpunkt an dem das äußere Programm erstellt wird, noch nicht existieren muss. Es kann also später geladen werden wenn Du es brauchst.
Das ermöglicht es ein Programm zu erstellen und das Verhalten so zu bestimmen wie es gebraucht wird. Dieses Konzept wurde in der OOBD “script engine” vielfach genutzt: Die “script engine” ist am Anfang aller Entwicklung, aber das was das Programm tun soll, wird mit dem geladenen Skripten zu Laufzeit des Programms festgelegt. Ein anderes Skript bedeutet eine andere Funktionalität…
Glücklicherweise wurde Lua ziemlich populär in den letzten Jahren und so ist es heute für alle wichtigen Plattformen verfügbar wie z.B. Desktop PC's, Smartphones und normale Mobiltelefone.
Es gibt eine Menge an Dokumentation über Lua im Internet, so das wir das Rad nicht noch mal neu erfinden wollen. Wir werden uns darauf konzentrieren, wie Lua innerhalb von OOBD funktioniert.
Die Lua - OOBD Schnittstelle
Wenn Lua innerhalb eines anderen Programms ohne weitere Hilfestellung gestartet wird, reagiert es wie eine Black Box: Es gibt keine Verbindung vom inneren zum äußeren Programm, keinerlei Eingabemöglichkeiten und keine Rückmeldungen an das aufrufende Programm. Das ist offensichtlich nicht besonders sinnvoll. Deshalb müssen, wenn Lua in ein Programm eingebaut wird, einige Schnittstellen erstellt werden, die Lua mit dem Host kommunizieren lassen.
Aus der Sicht von Lua arbeiten diese Schnittstellen als normale Lua Funktionsaufrufe. Aber wenn eine solche Funktion aufgerufen wird, verzweigt die Ausführung des Programms in das äußere Programm, macht dort irgendwas und kehrt letztlich von dieser Funktion zurück zu Lua.
Im Moment unterstützt OOBD zwei Arten von Schnittstellen für Lua: Einige die Menue's erstellen und andere die die Kommunikation über die serielle Schnittstelle ermöglichen.
Wichtig Um während der Fehlersuche die festeingebauten erweiterten Lua Funktionen gegen selbsterstellten Kode austauschen zu können, werden normalerweise die festeingebauten Lua Funktionen zuerst einigen Lua Variablen zugewiesen. Diese werden dann später in dem Skript benutzt.
local serFlush =serFlushCall serflush()
Mit dieser Benamungskonvention enden die festeingebauten erweiterten Funktionen mit xxx“Call”, während die Aufrufe Dieser nachher ohne “Call” getätigt werden.
Bitte behalte das im Gedächtnis, wenn Du dieses Dokument liest oder Deinen eigenen Kode schreibst.
Aber bevor wir einen Blick in den Programmablauf werfen, müssen wir verstehen wie der Lua Compiler (Luac) arbeitet und was das für die Programminitialisierung bedeutet:
Kompilierung und Aufstartkode
Um Speicherplatz zu sparen und die Aufstartzeit zu reduzieren, wird der Lua Interpretierer innerhalb OOBD nicht mit dem Quellcode ,der dann erst einmal kompiliert werden muss bevor er ausgeführt werden kann, versorgt. Stattdessen wird der fertig kompilierte Lua Programmkode eingebunden.
Der Kompilierungsprozess funktioniert folgendermassen: Der Lua Kompilierer (Ein Komandozeilenprogrammaufruf genannt luac) übersetzt den oder die Quellkode(s), mit der Endung .lua, in eine einzelne Datei. Diese eine Datei hat nun die Endung .lbc und beinhaltet das übersetzte Programm.
Der Kommandozeileneintrag sieht dann folgendermaßen aus:
luac -o Ausgabedateiname.lbc Quellkodename1.lua Quellkodename2.lua ...
In dem OOBD Quellkode Sammelverzeichnis sind einige Beispiele abgelegt, die zeigen wie es gemacht wird.
Wenn Du nun Deinen eigenen Übersetzungsprozess beginnen möchtest, gibt es zwei Dinge die Du wissen musst, da luac ein wenig anders arbeitet als ein gewöhnlicher Kompilierer.
- luac beurteilt nicht einige require() oder doFile() statements, sodaß diese Dateien nicht in der Ausgabedatei wiedergefunden werden können. Alle benötigten Dateien müssen stattdessen als Eingabedateien eingebunden werden, um in der Ausgabedatei enthalten zu sein.
- Auch die Reihenfolge der Eingabedateien ist wichtig: Luac bindet diese Dateien zusammen, in der Reihenfolge in der sie angefügt werden. In dieser Reihenfolge werden sie später auch ausgeführt. Hierbei musst Du darauf achten, das Variablen und Funktionen deklariert werden müssen bevor sie das erste Mal benutzt werden. Wenn das nicht passiert, bekommst Du eine Schutzverletzung.
Wegen des zweiten genannten Punktes, sollte die Lua Funktion die die kompletten Lua Strukturen initialisiert, das letzte und einzige Kommando sein, das in der letzen Datei steht die luac als Eingabedatei dient.
Das Start("","") Kommando
Um der OOBD Anwendung die Möglichkeit zu geben, neu initialisiert zu werden (wie z.B nach einem Verbindungsabbruch), wird der Name einer solchen Initialisierungsfunktion mit Start(“”,“”) bestimmt. Jedes OOBD Programm muss diese Funktion enthalten. Stelle also sicher, das Dein Programm dies enthält und das Deine Start() Funktion allen notwendigen Initialisierungskode enthält.
Die Menu Kommando's
OOBD benutzt nur drei Funktionen um die Menue Strukturen zu realisieren. Deshalb lass uns zuerst einen Blick in einen Beispielkode werfen:
function Start(oldvalue,id) identifyOOBDInterface() setSendID("$7E8") -- setzt nicht UDS kompatible Sender (=Antwort) Adresse für die OOBD firmware openPage("OOBD-ME Main") addElement("Sensor Data >", "createCMD01Menu",">>>",0x1, "") addElement("Snapshot Data >", "createCMD02Menu",">>>",0x1, "") addElement("Dynamic Menu3 >", "createCMD03Menu",">>>",0x1, "") addElement("Trouble Codes", "showdtcs","-",0x1, "") addElement("VIN Number", "vin","-",0x2, "") addElement("Clear Trouble Codes", "clearDTC","-",0x0, "") addElement("System Info >>>", "SysInfo_Menu",">>>",0x1, "") addElement("Greetings", "greet","",0x1, "") pageDone() return oldvalue end ----------------- Setzen der Startbedingungen -------------- Start("","") return
(Nebenbei gesagt: Hier siehst Du die start() Funktion in der Ausführung)
Also was finden wir hier? Zuerst haben wir den Aufruf der Funktion
openPage("Title")
Diese Funktion teilt OOBD mit, das ein neues Menue erstellt werden soll. Der Title wird benutzt, welch Überraschung, für den Namen des Titels dieses neuen Menü's.
Dann wird das Menü aufgebaut mit
addElement(Description, function, initialValue, Flagset, id)
Dieser Funktionsaufruf fügt ein einzelnes Element zu der Liste hinzu. Diese Funktion hat die folgenden Parameter
- Description(String): Dies erscheint als Beschreibung an diesem Menüeintrag.
- function: Jedes Menü ist einer Lua Funktion zugewiesen, wobei eine Funktion auf die diversen Menüeinträge aufgeteilt wird. Der Name dieser Funktion wird hier festgelegt ( Bitte achte auf die korrekte Groß- und Kleinschreibweise der Buchstaben )
- initialValue(String): Dies ist das, was angezeigt wird, wenn das Menü zum ersten Mal aufgeblendet wird.
- Flagset(Integer): Dies ist ein Integer Wert, wobei jedes Bit ein Flag darstellt. Die Bedeutung dieser Flags wird erklärt in der Benutzerschnittstellen Entwicklungsanleitung (Suche nach den System Flags)
- id(String): Diese id wird benutzt, um einen individuellen Marker an einen Menüeintrag zu binden. Dies ist weit verbreitet, wenn eine einzige Lua Funktion viele Menüoptionen unterstützen sollte. Es wird im Detail beschrieben in den Menü Funktionsaufrufe weiter unten.
pageDone()
Die pageDone() Funktion ist einfach, denn es teilt OOBD mit, daß das Menü nun komplett definiert und bereit zur Anzeige ist.
Das entgültige Ergebnis sieht dann ähnlich wie dieses aus. (Du wirst feststellen, das sich das Skript leicht verändert hat, nachdem der Screenshot gemacht wurde.)
Das Menü Funktionsaufruf
Wie oben gezeigt, haben wir nun die Menü Einträge aufgesetzt - aber nun wollen wir das etwas passiert, wenn der Benutzer einen Menue Eintrag auswählt, nicht wahr?
Lasst uns also annehmen, das ein Benutzer einen Eintrag auswählt. Was passiert jetzt?
- OOBD schaut nach, welche Lua Funktion mit diesem Menüeintrag verbunden ist
- dann wird diese Lua Funktion mit diesen beiden Parametern aufgerufen:
- der aktuell dargestellte Wert dieses Eintrags
- und die id, welche wir dem Menüeintrag gaben, während der Initialisierung
- dann macht die aufgerufene Lua Funktion etwas z.B. eine Berechnung oder sie startet die Erstellung eines weiteren Untermenüs
- wenn die Funktion abgearbeitet ist, gibt sie eine Zeichenfolge zurück
- Diese Zeichenfolge wird dann als neuer Wert dieses Menüeintrags dargestellt
Das ist erstmal alles, und es erfüllt den Zweck
Die Kommunikation Kommandos
OOBD benutzt gegenwärtig eine serielle Kommunikation, um mit dem Diagnoseadapter zu kommunizieren. Um es Lua zu ermöglichen, Daten zum seriellen Port zu senden und von Ihm zu empfangen, existieren einige wenige erweiterte Lua Funktionen:
serFlush()
Diese Funktion leert den Eingabespeicher (Von allem das, was möglicherweise in der Zwischenzeit empfangen wurde und nicht mehr benötigt wird)
serWrite(String)
Diese Funktion sendet einen String zum Ausgabespeicher (Das ist normalerweise der serielle Port)
serSleep(milliseconds)
Wartet milliseconds mit der Programmausführung
serReadLn(msTimeout)
Liest von der Eingabe bis ein Zeilenvorschub (dez. 10 hex 0x0A) auftaucht und gibt Diese als Zeichenfolge zurück.
serWait(OptionString, msTimeout)
Falls Du auf mehrere Zeichenfolgen wartest, von denen einige auftreten könnten, füllst Du den Optionstring mit string1|string2|..|stringN. Wenn dann eine dieser Zeichenfolgen in der Eingabe auftaucht, wird der Zählindex zurückgegeben (erste Zeichenfolge=1). Wenn die Zeichenfolge nicht innerhalb einer vorgegebenen msTimeout Zeitspanne eintrifft, gibt die Funktion den Wert 0 zurück.
Sonstige Kommandos
serDisplayWrite(String)
Schreibt eine String Zeichenfolge zu dem was schon in der Ausgabe steht.
dbLookup(db-File , searchstring)
Sucht in der db-file Datenbank nach allen Einträgen mit dem Index searchstring. Die db-file Datenbank muss sich im gleichen Verzeichnis befinden, wie wie das Lua-Skript. Die db-file Datenbank selber wird erzeugt durch oodbCreate.
dbLookup() gibt eine Lua Tabelle zurück
myTable = dbLookup("dtc.oodb", "0815")
myTable.len gibt den Erfolgswert zurück:
- if myTable.len < 0 dann ist ein Fehler aufgetaucht
- if myTable.len = 0 dann ist die Suchzeichenfolge nicht gefunden worden.
- if myTable.len > 0 dann zeigt myTable.len die Anzahl von Einträgen an, die gefunden wurden.
Wenn etwas gefunden wurde, enthält myTable zwei Bereiche, header Kopfzeile und data Daten.
Der “header” Bereich wird benötigt, wenn Du nicht weißt in welcher Spalte Dein gewünschtes Ergebnis gespeichert ist. Du kannst dann die Spalte erkennen, in dem Du über den Kopfzeilennamen Dich annäherst:
col= myTable.header["DTC-Text"] print (col) 2
myTable.header.size gibt die Anzahl der Spalten insgesamt zurück ohne die erste Index Spalte, welche immer unterdrückt wird.
Der data Daten Bereich enthält dann die gefundenen Daten, aufgebaut in einem zweidimensionalen Feld, sortiert nach Zeilen und Spalten.
result=myTable.data[row][column]
Achtung: Obwohl die Zeilen- und Spaltenindexe durch Zahlen dargestellt werden (1,2,3,4..), werden sie intern als Zeichenfolgen (“1”,“2”,“3”,“4”…) geführt. Um also das Ergebnis korrekt zu lesen, müssen die Zeilen- und Spaltenzähler zu Zeichenfolgen konvertiert werden, um das zweidimensionale Feld korrekt zu adressieren.
column=3 row=2 result=myTable.data[tostring(row)][tostring(column)])
Nach all den theoretischen Informationen ein kleines Stück Beispielkode:
myTable= dbLookupCall("dtc.oodb","005") print ("header") for k,v in pairs (myTable.header) do print (k,"=",v) end nrOfColumns = myTable.header.size nrOfRows = myTable.len print ("Rows x Columns:" ,nrOfRows, nrOfColumns) for row = 1 , nrOfRows , 1 do for column = 1 , nrOfColumns, 1 do print (cy, cx, myTable.data[tostring(row)][tostring(column)]) end end
openXCVehicleData(lua table)
OOBD kann als Fahrzeugdatenquelle für openXC arbeiten, was bedeutet das OOBD Datensätze an das openXC system sendet (welches dann nartürlich auch auf dem gleichen Android Gerät installiert werden muss).
Um dies zu erreichen, wird eine Lua Tabelle mit den richtigen Identifizierern und korrekt formatierten Werten wie im Beitrag OpenXC Message Format Spezifikationbeschrieben, immer ein Wert pro Aufruf.
Mithilfe dieser Tabelle wird openXCVehicleData() aufgerufen und die Daten werden zum openXC backbone Prozess übertragen, um eine weitere Verarbeitung zur ermöglichen.
openXCVehicleData({timestamp= 1332794087.675514, name= "longitude", value= -83.237427})
So kann alles was von openXC verstanden wird, mit Hilfe von OOBD lua Skript generiert werden.
Ein paar Programmiertricks
Wenn Du Kenntnisse aus den althergebrachten Programmiersprachen mitbringst, wirst Du wahrscheinlich einfache und statische Variablentypen verwenden, wie Zahlen und Zeichenfolgen. Aber Lua offeriert hier mehr Komfort, besonders mit der Hilfestellung von geschachtelten (assoziativen) Feldern. Mit diesen können einige Dinge sehr viel einfacher realisiert werden.
Wir wollen uns ein häufiges Szenario vorstellen: Du möchtest eine Anzahl von Menüeinträgen verwenden, welche alle das Gleiche tun, Einen Wert von einem Fahrzeug erhalten, nur mit verschiedenen Parametern.
In der Vergangenheit musstest Du eine Funktion für jeden einzelnen Wert und einen lange Liste von Menüeinträgen schreiben. Aber in Lua kannst Du alle Parameter, deren Bedeutungen und auch eine Funktionsreferenz, um Sie zu bekommen, in einem einzigen Feld speichern wie
local Menu2Data = { id0x0815 = { byte = 1 , size = 1 , mult = 0.392156862745 , offset = 0, unit = " %", title="Part Number", call = "readAscPid"} , id0x4711 = { byte = 1 , size = 1 , mult = 0.392156862745 , offset = 0, unit = " %", title="Software Level", call = "readAscPid"} , -- ... here are all the other parameter settings }
Dann, während Deiner Menü Initialisierung, kannst Du Lua das Feld durcharbeiten und das Menü daraus aufbauen lassen.
openPage("MyMenu") for key,value in pairs(Menu2Data) do res=_G[value.call]("-",key) --nice trick to call a function just by name :-) addElement(value.title, value.call,res,0x1, key) end pageDone()
Und wieder einmal kann man sagen: Das war's.
Wenn dann später eine Funktion aufgerufen wird, durch seinen Menü Eintrag, bekommt es einen Hash Key aus dem Menu2Data Feld als dessen id Parameter. Damit kann die Funktion, dann Ihre eigenen Parametersätze aus dem Menu2Data Feld auslesen um die korrekten Werte auszurechnen.
Bitte beachte den Gebrauch des _G array Feldes in dem obigen Beispiel: Lua speichert alle Funktionen in sein globales _G array Feld, wo sie dann referenziert über Ihren Namen, als normale Funktion benutzt werden können. So macht der res=_G[value.call](“-”,key)
Aufruf das folgende:
- es findet eine Funktion mit Hilfe seines Namens
- dann ruft es diese Funktion auf, wie auch der Nutzer es tun würde
- es speichert den aktuellen Rückgabewert ( welcher die aktuellen Messdaten repräsentiert ) in res
- res wird dann wie der initial dargestellte Wert, als das Menü konfiguriert wurde, benutzt
Durch diese Vorgehensweise ist es möglich, ohne großen Aufwand ein Menü mit den reellen Daten während der Initialisierung zu füllen. So sind die aktuellen Daten von Anfang an direkt sichtbar.