Hallo,
vor einiger Zeit stand ich vor der Aufgabe, Bondrucker zu bedienen, die an einem USB-Port des Raspi angeschlossen werden sollten. Wenn man den Zugriff über Drucker-Treiber vermeiden möchte (oder muss aufgrund fehlender Treiber), ist es notwendig, direkt auf die USB-Ports zu schreiben. Ich möchte hiermit mein Wissen teilen und beschreiben, wie man das bewerkstelligen kann.
Es gibt unheimlich viele Wege, auf eine USB-Schnittstelle zuzugreifen. Ich habe den Weg über Java gewählt, weil mit der frei verfügbaren Bibliothek usb4java (LGPL-Lizenz) der Zugriff sehr schön programmierbar ist und ich mittlerweile mehr Erfahrung mit "gemanagten" Sprachen als mit purem C/C++ habe. Aber das ist Geschmackssache...
Wie bereits angeführt, habe ich die Library usb4java benutzt. Dieses Package kann man unter http://usb4java.org/ downloaden. Dort finden sich auch sehr gute Anleitungen, an denen ich mich orientiert habe.
usb4java erlaubt zwei verschiedene Zugriffsverfahren, einmal die harte Tour (die "low-level" libusb API) und einmal die warmherzig zuvorkommende Variante (die high-level "javax-usb" API). Letztere ist objektorientiert, ereignisgesteuert und feuert Exceptions im Fehlerfall, so dass die Abfrage negativer Rückgabewerte oft entfallen kann. Ich mags dekadent und nutze die high-level-API.
Meine wichtigste Einstiegsmethode heißt "sendBytesToDevice" und soll eine beliebe Menge an Bytes an ein USB-Device senden. Zusätzlich muss die Methode wissen, welches USB-Device ich denn nun ansprechen möchte. Dafür habe ich zwei Möglichkeiten vorgesehen:
1. Ich gebe die VendorID und die ProduktID des Devices an. Die Angaben liefert das Shellkommando "lsusb" als zwei per Doppelpunt getrennte Hex-Zahlen.
2. Ich weiß, dass nur genau ein Device mit genau meinem gewünschten Typ (in diesem Fall Printer) angeschlossen ist. In diesem Fall gebe ich use_first_found_printer = true an, so dass productId und vendorId ignoriert werden.
Das Programm soll an allen USBDevices nach dem gesuchten Gerät suchen (man könnte die Suche auch auf USBPorts/Interfaces einschränken - vielleicht interessant, wenn man zwei Drucker gleichen Modells unterscheiden möchte).
Sicherlich kann man den Quelltext noch anhübschen und wahrscheinlich auch eleganter und fehlertoleranter aufbauen, aber ich möchte in erster Linie zeigen, wie das Verfahren funktioniert. Ich habe versucht, den Quelltext so zu kommentieren, dass er leicht verständlich ist. Bei Fragen oder Fehlern einfach melden...
public static boolean sendBytesToDevice(byte[] content, String vendorIdHex, String productIdHex, boolean use_first_found_printer) throws Exception {
// wenn der Druck erfolgt ist, soll die Methode ein "true" zurückgeben.
boolean ok = true;
final UsbServices services = UsbHostManager.getUsbServices();
// der Einstiegspunkt - USB ist wie eine Baumstruktur - es gibt Hubs und Devices
final UsbHub device = services.getRootUsbHub();
// konvertiere die Hex-Angaben
short vendorId = Short.parseShort(vendorIdHex, 16);
short productId = Short.parseShort(productIdHex,16);
// UsbDevice: der Drucker, an den die Bytes gesendet werden sollen
UsbDevice usbDev = null;
if (use_first_found_printer) {
// finde den ersten Drucker, der am USB-Port hängt
usbDev = findPrintDevice(device);
} else {
// finde genau den Drucker mit der angegebenen vendorId und productId
usbDev = findDevice(device, vendorId, productId);
}
if (usbDev == null) {
// kein gesuchtes USB-Gerät gefunden
// gebe ein false zurück - die aufrufende Methode kann den Druckauftrag erneut ausführen
// sobald ein Drucker angeschlossen wurde und das in einer Loop testen
return false;
}
// man benötigt ein USBInterface, um auf das Gerät zuzugreifen
UsbInterface iface = getUsbInterface(usbDev);
// wenn es kein aktives Interface gibt, ist das USB-Device wahrscheinlich noch nicht bereit
if (iface == null) {
// kein USB-Interface auf dem USB-Device gefunden
System.err.println("Kein USB-Interace gefunden");
return false;
}
// bevor man das Interface benutzt, muss man es "claimen"
iface.claim(new UsbInterfacePolicy()
{
// falls der Kernel das Interacwe schon "besitzt", muss man ihn bitten, es freizugeben.
// (Bei Windows-Systemen soll das nicht immer funktionieren - habe ich nicht getestet)
@Override
public boolean forceClaim(UsbInterface usbInterface)
{
return true;
}
});
try
{
// das Interface ist bereit, die Bytes können an das UsbDevice gesendet werden.
sendToUsb(iface,content);
} catch (UsbNotOpenException e) {
ok = false;
} catch (UnsupportedEncodingException e) {
ok = false;
}
finally
{
iface.release();
}
return ok;
}
/**
* Methode, um ein UsbDevice mit einer vorgegebenen vendorId + productId zu finden
*/
public static UsbDevice findDevice(final UsbHub hub, short vendorId, short productId) {
for (UsbDevice device : (List<UsbDevice>) hub.getAttachedUsbDevices()) {
UsbDeviceDescriptor desc = device.getUsbDeviceDescriptor();
if (desc.idVendor() == vendorId && desc.idProduct() == productId)
return device;
if (device.isUsbHub()) {
// ein Device kann ein Hub sein, hinter dem weitere UsbDevices/Hubs stecken.
// Suche rekursiv weiter
device = findDevice((UsbHub) device, vendorId, productId);
if (device != null)
// ein Device wurde gefunden
return device;
}
}
// ein Device mit dem vorgegebenen vendorId und productId wurde nicht gefunden
return null;
}
/**
* Methode, um ein UsbDevice vom Typ "Printer" zu finden.
*/
public static UsbDevice findPrintDevice(final UsbHub hub) {
for (UsbDevice device : (List<UsbDevice>) hub.getAttachedUsbDevices()) {
if (device != null) {
UsbInterface usbInterf = getUsbInterface(device);
if ( usbInterf.getUsbInterfaceDescriptor().bInterfaceClass() == LibUsb.CLASS_PRINTER) {
return device;
}
if (device.isUsbHub()) {
// ein Device kann ein Hub sein, hinter dem weitere UsbDevices/Hubs stecken.
// Suche rekursiv weiter
device = findPrintDevice((UsbHub) device);
if (device != null)
return device;
}
}
}
// es hängt offenbar noch kein Device vom Typ "Drucker" am Raspi oder er wurde nicht eingeschaltet.
return null;
}
/**
* Finde das aktive Interface des UsbDevices
*/
private static UsbInterface getUsbInterface(UsbDevice usbDev) {
// jedes UsbDevice hat eine Configuration, über die man auf wichtige Eigenschaften des
// UsbDevices zugreifen kann. Unter anderem benötigt man ein aktives Interface, um
// Daten zu lesen/schreiben.
UsbConfiguration configuration = usbDev.getActiveUsbConfiguration();
List usbInterfaces = configuration.getUsbInterfaces();
for (UsbInterface aUsbInterface : (List<UsbInterface>) usbInterfaces) {
if (aUsbInterface.isActive()) {
return aUsbInterface;
}
}
return null;
}
/**
* Sende die Bytes an das UsbInterface
*/
private static void sendToUsb(UsbInterface iface, byte[] content) throws UsbNotActiveException, UsbNotOpenException, UsbDisconnectedException, UsbException, UnsupportedEncodingException {
byte outEndPointAddr = getOutEndpoint(iface);
// UsbEndpoints sind so etwas wie die Ports beim TCP/IP-Protokoll.
UsbEndpoint endpoint = iface.getUsbEndpoint(outEndPointAddr);
// über die Pipe werden die Daten gesendet
UsbPipe pipe = endpoint.getUsbPipe();
pipe.open();
try
{
// die Daten an das UsbDevice senden. Wenn man möchte, kann man über den Rückgabewert die Anzahl
// der gesendeten Bytes erfahren.
pipe.syncSubmit(content);
}
finally
{
pipe.close();
}
}
/**
* Finde genau den Endpoint, über den Daten ausgegeben werden können. Es gibt verschiedene Endpoint-Directions,
* und am besten vergleicht man mit den vordefinierten Bytewerten unter UsbConst.
* Je nach Gerät werden unterschiedliche Endpoints angeboten. Ein Drucker sollte immer ein
* Endpoint vom Typ OUT haben.
*/
private static byte getOutEndpoint(UsbInterface iface) {
for (UsbEndpoint endpoint : (List<UsbEndpoint>) iface.getUsbEndpoints()) {
if ( endpoint.getDirection() == UsbConst.ENDPOINT_DIRECTION_OUT ) {
UsbEndpointDescriptor descr = endpoint.getUsbEndpointDescriptor();
return descr.bEndpointAddress();
}
}
// graceful degradation - aber das könnte man sicher auch besser behandeln...
// (In der Regel passt 0 aber für den Ausgabe-Endpoint)
return 0;
}
Alles anzeigen
So, und jetzt noch ein Wort zu den Zugriffsrechten: Normalerweise darf nur Root auf die UsbDevices direkt zugreifen. Es macht aber durchaus Sinn, das Programm auch als "normaler" Benutzer ausführen zu können und diesem Benutzer oder einer Gruppe den Zugriff auf ein bestimmtes Usb-Gerät zu erlauben.
Dazu kann man im Verzeichnis /etc/udev/rules.d eine Datei anlegen, deren Namen mit einer Zahl beginnt (Priorität) und mit ".rules" endet, z.B. 50-usb-zugriff.rules. In diese gehört die Information, wer auf welches Usb_Device schreiben darf. Der Inhalt sieht beispielsweise so aus:
Damit haben die Benutzer der Gruppe "users" Lese- und Schreibrechte auf das Usb-Device mit der VendorId 4348 und der ProduktId 5584.
Ich hoffe, dieses Tutorial ist für den einen oder anderen hilfreich...
Stefan