© 2012 Sandro Liebscher Der Hintere Teil des Autos nach dem Umbau

CAN-Bus mit JAVA auslesen

Dieser Artikel erklärt ausführlich, wie die Daten unseres Autos mit Hilfe eines Java-Programms gelesen und weitergenutzt werden können. Dabei wird vor allem darauf eingegangen, wie die CAN-Daten entschlüsselt werden.

Unser Auto überträgt seine Daten in Echtzeit per WLAN an Empfängerprogramme. Wie das Zusammenspiel der einzelnen Komponenten erfolgt haben wir bereits im Artikel Datenübertragung vom Auto in das Internet genauer erklärt. An dieser Stelle wollen wir nun einen genaueren Blick darauf werfen, wie man CAN-Botschaften mit Hilfe von JAVA entschlüsselt und nutzbar macht.

Der Weg der Daten vom Auto zum Programm

Der Hintere Teil des Autos nach dem Umbau

Am Aufbau des Autos sieht man die WLAN-Antenne, die zum Avisaro-Steuergerät führt und die CAN-Daten überträgt. (Klicken zum Vergrößern)

Doch zunächst eine kurze Einführung.  Der CAN-Bus dient dazu, Daten im Auto zwischen den Steuergeräten zu übertragen. Dies passiert sowohl in Realfahrzeugen, als auch in unserem Modellauto. Damit wir unser Modellauto nicht an einem langen Kabel fahren lassen müssen, übertragen wir alle CAN Daten mit Hilfe des Avisaro WLAN Moduls 2.0 direkt drahtlos. Dabei werden die gesamten Daten des CAN-Busses in WLAN-Pakete verpackt und dabei nicht verändert, so dass sie am Ziel “sauber” zu Verfügung stehen. Die Datenübertragung erfolgt bei uns als Broadcast,  also an keine bestimmte, sondern alle (lokalen) Adressen, so dass die Daten auch von mehreren Empfängern gleichzeitig mitgeschnitten und interpretiert werden können. In unserem Falle können die Daten also gleichzeitig am Laptop und am Android-Smartphone angezeigt werden.

Die Daten nun mit Hilfe eines Java-Programms zu empfangen stellt kein großes Problem dar. Wir haben dazu die Klasse CanReciever erstellt, die  das Interface Runnable implementiert. Das hat den Vorteil, dass der Empfang in einem Extra-Thread ablaufen kann und nicht das restliche Programm stört.

Hier ein Ausschnitt aus der Klasse, die zeigt, wie der Konstruktor aussieht.

public class CanReciever implements Runnable {
	private CarConnector carConnector;
	private DatagramSocket canRecieverSocket = null;

	public CanReciever(CarConnector carConnector, DatagramSocket socket) {
		this.carConnector = carConnector;
		this.canRecieverSocket = socket;
	}

Dabei ist der CarConnector eine Klasse, die sich um die Steuerung kümmert und als Schnittstelle zur grafischen Oberfläche dient. Wir wollen darauf aus Platzgründen nicht genauer eingehen. Wir zeigen aber kurz, wie vom CarConnector aus ein CanReciever erstellt und gestartet wird.

Daten emfpangen

// Aussschnitt aus dem CarConnector
// Der Port (Beispiel 1234) wurde bereits vom Programm übergeben
DatagramSocket carRecieverSocket = new DatagramSocket(recievingPort);
CanReciever canReciever = new CanReciever(this, this.carRecieverSocket);
Thread canRecieverThread = new Thread(canReciever);
canRecieverThread.setDaemon(true);
// Starten das Datenabrufens
canRecieverThread.start();

Der CanReciever benötigt nun die Methode run(), die dazu da ist, die Daten zu empfangen, zu sammeln und dann einzelne CAN-Pakete zur Verarbeitung weiterzureichen. Ein Ausschnitt (bei dem aus Gründen der Lesbarkeit auf Fehlerbehandlung verzichtet wurde):

while (true) {
	// eine unbestimmte Anzahl von CAN-Paketen abrufen
	DatagramPacket udpPacket = new DatagramPacket( new byte[1008], 1008 );
	// Dies blockiert, bis mindestens ein Paket anliegt
	// (dies werden nie weniger als 28 Byte sein, da immer mindestens ein komplettes Paket ankommt)
	canRecieverSocket.receive(udpPacket);
	// Die Daten werden als Byte-Array geliefert
	byte[] udpData = udpPacket.getData();

CAN-Pakete verstehen

In den ankommenden Daten haben wir nun eine unbestimmte Anzahl reiner CAN-Pakete. Avisaro gibt es eine sehr nützliche Übersicht, wie ein CAN-Paket aufgebaut ist. Es besteht  aus genau 28 Byte. Zuerst müssen wir die Pakete aus dem Datenstrom herausfiltern, so dass wir diese einzeln bearbeiten können. Dazu speichern wir jedes Byte in einer HashMap und geben mit einem Integer die Bytenummer an, so dass wir später einfach darauf zugreifen können. Entgegen der Konvention fangen wir nicht mit 0 sondern mit 1 an zu zählen, damit wir mit der Avisaro-Zählweise übereinstimmen.

int byteCounter = 1;
// In diesem Paket sammeln wir die einzelnen Bytes eines CAN-Paketes
HashMap packet = new HashMap();
for(int udpIterator = 0; udpIterator < udpPacket.getLength(); udpIterator++) {
	// Wir schreiben das Byte an die passende Stelle
	packet.put(byteCounter, udpData[udpIterator]);
	byteCounter++;
	// Paket "voll"
	if (29 == byteCounter) {
		// Das Paket wird in einer eigenen Funktion identifiziert und behandelt
		handlePackage(packet);
		// Das Auslesen kann nun wieder mit einem "neuen" Paket beginnen
		byteCounter = 1;
	}
}

Die Funktion handlePackage bekommt nun also die Aufgabe, ein Paket zu bearbeiten. Dazu bekommt es das Paket selbst übergeben. Schauen wir uns die Funktion nun im einzelnen an:

private void handlePackage(HashMap packet) {
	// die Message ID
	int messageID = getValueFromBytes(packet, 9, 12, true).intValue();

	// Datenlänge (Die letzten 4 Bit des 7. Bytes)
	int dataLength = packet.get(7) & 0xf;
	switch (messageID) {
		// hier behandelt man nun die Pakete je nach ID
		[...]
	}

Wir holen also die ID aus dem Bytestrom und fahren dann entsprechend weiter fort. Neu ist die Funktion getValueFromBytes, die ich weiter unten erkläre. Doch vorerst schauen wir uns noch einmal an, wie man solch ein Paket nun in Java nutzt. Die Daten sind (wie man auf der schon beschriebenen Avisaro-Seite nachlesen kann) in den Paketen 13 bis 20 verpackt. Je nach Datenformat kann man aber die Daten nicht direkt herauslesen, sondern muss mal mehrere Byte zu einer Zahl zusammenfassen oder auch nur einen Ausschnitt aus den Bytes nehmen. Das Datenformat ist abhängig vom Autohersteller. Wir haben uns ein speziell auf unser Auto zugeschnittenes Format erstellt. Ein paar Beispiele folgen.

Fall 1: Zahl steht im ganzen Byte

Der einfachste Fall ist, dass die Zahl in einem ganzen Byte steht. Nehmen wir als Beispiel die Drehzahl des Motors, die bei uns im 13. Datenbyte des CAN-Paketes mit der ID 53 steht. Das ist vor allem für kleine Ganzzahlen sinnvoll. Bei uns geht die Drehzahl nur von 0 bis 100. Direkt im nächsten Byte (14) befindet sich dann der Lenkwinkel. Diese Daten würde man folgendermaßen auslesen:

// Ausschnitt aus dem switch-Statement
case 53:
	carConnector.currentData.engine_rps = unsignedByteToInt(packet.get(13));
	carConnector.currentData.position_steering_out = new Integer(packet.get(14));
	break;

Wie man sieht, geht Java davon aus, das ein Integer in einem Byte vorzeichenbehaftet ist. Will man eine vorzeichenlose Zahl (also nur positiv) herauslesen benötigt man die Hilfsfunktion unsignedByteToInt, die unten abgebildet ist. Bei vorzeichenbehafteten Ganzzahlen wie dem Lenkwinkel (der kann auch negativ sein, da 0 die Mittelposition ist) können wir den in Java eingebauten Konstruktor Integer(Byte)nutzen.

Fall 2: Nur ein Teil des Bytes ist relevant

Eine CAN-Botschaft kann auch so aufgebaut sein, dass mehrere verschieden Informationen in einem Byte codiert wurden . So haben wir beispielsweise im 13. Datenbyte des Paketes 510 die Statusinformationen von vier verschiedenen Lichter untergebracht, wobei jedes Licht jeweils 2 Bit bekommt (steht eine 0, bleibt der Status, bei 1 geht das jeweilige Licht an, bei 2 aus). Dies ist sinnvoll, da damit die Datenmenge pro Paket recht hoch bleibt. Nur so ist es möglich, den Status aller 16 Lichter in nur einem CAN-Paket unterzubringen.

// Ausschnitt aus dem switch-Statement
case 53:
	// Wir müssen nun Bytes bitpaarweise auslesen (Mit "& 3" werden alle außer die letzten beiden Bits maskiert)
	// Wichtig: Es muss darauf geachtet werden, dass wir es mit vorzeichenlosen "Zahlen" zu tun haben
	// Erstes Bitpaar
	int temp = packet.get(13) & 3;
	// 0 heißt, es ändert sich nix, deswegen ändern wir auch nur bei ungleich 0
	if (0 != temp) {
		// bei 1 machen wir es an
		if (1 == temp) carConnector.currentData.flasher_left = true;
		// bei 2 machen wir es aus
		if (2 == temp) carConnector.currentData.flasher_left = false;
	}
	// Das zweite Bitpaar lesen wir nun folgendermaßen aus
	temp = (packet.get(13) >>> 2) & 3;
	[...] // Behandlung wie oben
	// drittes Bitpaar usw.
	temp = (packet.get(13) >>> 4) & 3;
	[...]
	break;

So kann man dann die einzelnen Bits und Bytes schrittweise auslesen. Das funktioniert natürlich mit “breiteren” und “schmaleren” Daten ähnlich, man muss nur die Weite des Shifts (>>>) und das Maskieren (&) anpassen.

Fall 3:Mehrere Bytes bilden eine Zahl

Für bestimmte große Zahlen oder genaue Floats kann es nötig sein, diese in mehrere Bytes zu verpacken. Wir zeigen es hier an Hand unserer Raddrehzahlen, die sich in jeweils zwei Byte des Paketes 25 verstecken.

// weiterer Ausschnitt aus dem switch-Statement
case 25:
	carConnector.currentData.wheel_rps_front_left  = new Float(getValueFromBytes(packet, 13, 14, true) * 0.01);
	carConnector.currentData.wheel_rps_front_right = new Float(getValueFromBytes(packet, 15, 16, true) * 0.01);
	carConnector.currentData.wheel_rps_back_left   = new Float(getValueFromBytes(packet, 17, 18, true) * 0.01);
	carConnector.currentData.wheel_rps_back_right  = new Float(getValueFromBytes(packet, 19, 20, true) * 0.01);

break;

Wie man sieht, nutzen wir erneut die Hilfsfunktion getValueFromBytes, die uns das Umwandeln der Bytedaten in Zahlen erleichtert. Auch diese wird später noch genauer erläutert. Außerdem sieht man, dass die tatsächlichen Werte zur genaueren Übertragung noch mit dem Faktor 100 multipliziert wurden, so dass wir nun die entsprechende Umkehroperation durchführen müssen. Dies ist allerdings eine Festlegung, die bei der Datenerzeugung für den CAN-Bus getroffen werden muss und je nach Anforderung an das System variieren kann.

Hilfsfunktionen

Den schwierigsten Teil — nämlich die Umrechnung von Bytes auf Zahlen — haben wir in den obigen Codebeispielen in extra Funktionen ausgelagert. Hier sind nun die vollständig kommentierten Hilfsfunktionen:

/**
 * Rechnet ein unsigned Byte in einen Integer um (da das Byte sonst von Java als signed interpretiert wird, mit einem Bereich -127 bis 127)
 * @param b das vorzeichenlose Byte
 * @return ein Integer zwischen 0 und 255
 */
private static int unsignedByteToInt(byte b) {
	return (int) b & 0xFF;
}
/**
 * Errechnet den Wert aus einer Bytefolge, wobei festgelegt werden kann, ob diese Signed oder Unsigned ist
 * Dazu wird das erste Byte so hergenommen wie es ist, das zweite mit 256 multipliziert, das dritte mit 2¹⁶ usw. ...
 * @param bytePacket Das Packet, welches die Bytefolge enthält.
 * @param startByte Ab diesem Byte wird summiert
 * @param endByte Bis zu diesem Byte wird summiert
 * @param byteIsUnsigned <code>true</code>, wenn ohne Vorzeichen (also nur positiv), <code>false</code>, wenn Vorzeichen wechseln kann
 * @return Der Zahlenwert der übergebenen Pakete
 */
public static Double getValueFromBytes(HashMap<Integer, Byte> bytePacket, int startByte, int endByte, Boolean byteIsUnsigned) {
	Double result = new Double(0);

	// Die Bytes werden noch mit dem richtigen Faktor multipliziert
	// für das erste 2⁰, dann 2⁸, 2¹⁶
	for (int i = startByte; i <= endByte ; i++) {
		if(!byteIsUnsigned && i == endByte) {
			result += new Double(bytePacket.get(i)) * Math.pow(2, 8 * (i - startByte));
		} else {
			result += new Double(unsignedByteToInt(bytePacket.get(i))) * Math.pow(2, 8* (i - startByte));
		}
	}
	return result;
}

Für das Verständnis dieser Funktionen ist es sehr nützlich, wenn man sich mit Bits und Byte sowie der Mathematik dahinter auskennt. Für wen das völliges Neuland ist, der kann sich im lesenswerten Kapitel Bits und Bytes und Mathematisches im Java ist auch eine Insel-Buch genauer informieren.

Fazit

Die Übertragung der Daten vom Auto erfolgt per WLANJava macht es nicht ganz so einfach, mit den CAN-Paketen umzugehen, da das auslesen einzelner Speicherbereiche und Bytes nicht gerade die Stärke dieser Programmiersprache ist. Man könnte auch sagen, dass sie dazu zu modern ist. Die Hilfsfunktionen (und auch die immer noch vorhandenen Möglichkeiten für binäre Rechenoperationen) nehmen uns allerdings die meiste Arbeit ab. Gerade beim Aufbau der Verbindung über WLAN zeigt Java seine Stärke. Hier nimmt uns die Nutzung von Sockets nahezu jegliche Arbeit ab.

One Comment

  1. Thorsten
    Posted 27. August 2013 at 12:18 | #

    danke für die tolle Anleitung. Ist ja garnicht so schwierig zu programmieren. Hatte mich zunächstr erstmal über den Aufbau des CAN-Bus hier http://can-adapter.de/can-bus/schichten-der-can-software-und-can-hardware/ informiert und lese mich derzeit immer mehr in das Thema rein

Post a Reply to Thorsten

Emailadresse wird niemals gezeigt oder weitergegeben. Obligatorisch *

*
*

Diese HTML tags funktionieren: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>