⟵ zur Projektübericht

Motivation

Der ADFC-Stormarn e.V. verwendet Klebeetiketten zur Codierung von Fahrrädern.

EIN-Code Etikett

Auf diesen Etiketten ist ein EIN-Code (Eigentümer-Identifizierungs-Nummer) aufgedruckt, die es der Polizei, den Fundbüros und dem ADFC erlauben, den Eigentümer eines Fahrrads zu ermitteln. Eine über aufgeklebte Etikett angebrachte Plombierfolie schützt dieses vor dem Ablösen.

ADFC EIN-Code-Etikett aufgeklebt

Dieses Verfahren eignet sich auch für andere Gegenstände, wie Kameras, Fahrrad-Akkus, Kinderwagen, Pferdesättel, etc.

Der ADFC Stormarn benutzt einen Brother P-touch-Drucker zur Herstellung der Etiketten. Dazu muss der über eine Android-App ermittelte Code manuell über die Tastatur des Drucker erfasst werden. Das ist aufwändig und fehleranfällig.https://appinventor.mit.edu/

Seit kurzem steht ein neues Drucker-Modell (PT-E560BT) zur Verfügung, das über Bluetooth angesteuert werden kann. Es liegt also nahe, die App zur Ermittlung des EIN-Codes um eine Druckfunktion zu erweitern. Die App wurde mit Hilfe des MIT App Inventor entwickelt. App Inventor erlaubt es, in Java geschriebene Erweiterungen (Extension) einzubinden, um die App um nicht standardmäßig vorhandene Funktionalitäten zu erweitern.

Diese Seite beschreibt, die Entwicklung und Funktion einer speziellen Extension (UrsPte560) für den MIT App Inventor zum Ausdruck von EIN-Code-Etiketten auf einem Brother P-touch-E560BT-Drucker.


Version Anpassungen
1.0 2025-04-21) Initiale Version
1.1 (2025-05-08) Anpassungen zum Umgang mit mehr als einem gekoppelten Drucker.
  • Eigenschaften PrinterName und PrinterAddress hinzugefügt.
  • Designer-Eigenschaft BluetoothClient entfernt, dafür Methode SetBluetoothClient hinzugefügt.
  • Methode GetPairedPrinterList hinzugefügt
  • Event AfterDataPrinted hinzugefügt.
  • Methode Print überarbeitet:
    • Kopie von BluetoothConnectionBase.write mit eigenem Exceptionhandling übernimmt die Datenausgabe
    • löst Event AfterDataPrinted aus
  •  Referenzen auf BluetoothClient, PrinterName und PrinterAddress sind statisch
1.2 (2025-05-10) Einige Methoden mit einem Try-Catch-Block versehen.

 

In­halts­ver­zeich­nis

Verwendung

Über die App Inventor Erweiterung (Extension)

Referenz

Eigenschaften

Funktionen

Download

Implementierung

Aufbau eines Etiketts

Ansteuerung des Druckers

Ermittlung der Steuercodes

Ermittlung der Rastergrafik

Struktur der Extension


Über die App Inventor Erweiterung (Extension) UrsPte560

Viele Funktionen lassen sich mit den Standardkomponenten des App Inventor entwickeln. Häufig ist die dazu aufzubauende Blockstruktur sehr komplex. Die Formulierung der gleichen Lösung ist, wenn man sie in JAVA formuliert, meist deutlich einfacher und übersichtlicher. Über ein Extension-Programm kann man selbst geschriebenen JAVA-Code in eine App-Inventor-App integrieren.

Die hier beschriebene Extension UrsPte560 ist speziell für die Bedürfnisse des Allgemeinen Deutsche Fahrrad-Club (ADFC) zugeschnitten. Dennoch werden hier einige Verfahren aufgezeigt, die auch für andere Anwendungen nützlich sein können.

Die Herausforderungen, die mit dieser Extension gelöst wurden:

Verwendung

Persistenz

Die Referenz auf die verwendete BluetoothClient-Komponente und die Eigenschaften PrinterAddress und PrinterName (s.u.) sind als statische Variablen angelegt. D.h. die hier hinterlegten Werte sind für alle Instanzen Extension die selben, auch dann, wenn diese Instanzen zu verschiedenen Screen-Instanzen gehören. Die Zuweisung der BluetoothClient-Komponente darf nur einmal erfolgen. Am Besten geschieht dies gleich zu Beginn im Haupt-Screen Screen1 (siehe folgenden Abschnitt). Die Auswahl des Druckers (Eigenschaften PrinterAddress und PrinterName) kann an beliebiger Stelle erfolgen (z.B. in einem separatem Screen) und ist dann für alle Instanzen gültig (s. Abschnitt Drucker auswählen).

Initialisierung der Extension

Die Extension benötigt eine Instanz der BluetoothClient-Komponente. Diese sollte im Hauptfenster (Screen1) im Ereignis Initialize über die Methode SetBluetoothClient registriert werden. Die benutzte BluetoothClient-Instanz sollte durch das Programm nicht direkt angesprochen werden, sondern der Extension exklusiv zur Verfügung stehen.

Initialisierung der Extension

Drucker auswählen

Ein separate Druckerauswahl ist unnötig, wenn nur ein Drucker mit dem Smartphone gekoppelt ist. Dies sollte gleich zu Beginn der App geprüft werden:

Überprüfung der gekopplten Drucker

In der App überprüft die Prozedur CheckPrinters, ob nur ein einziger Drucker gekoppelt ist. Die Methode GetPairedPrinterList*** liefert eine Liste aller gekoppelten PT-E560BT-Drucker. Wird nur ein Element zurück geliefert, wird die Extension zur Verwendung dieses Elements initialisiert (PrinterAddress und PrinterName werden belegt). Der ausgewählte Drucker wird in einer Label-Komponente (lblPrinter) angezeigt.

***GetPairedPrinterList benutzt dir Funktion AddressesAndNames der BluetoothClient-Komponente.  Unter Android 12 oder später, wenn die Berechtigungen BLUETOOTH_CONNECT und BLUETOOTH_SCAN der App nicht gewährt wurden, liefert die Abfrage eine leere Liste zurück.

Vor dem Drucken muss geprüft werden, ob ein Drucker selektiert wurde. Ist dies nicht der Fall, wird in der App ein separater Screen (DruckerWahl) zur Druckerauswahl geöffnet.

Überprüfung auf selektierten Drucker

Bei der Rückkehr von diesem Screen muss eine mögliche Druckerauswahl berücksichtigt werden.

Etikettendruck auslösen

Leider ist es in der Android-Umgebung nicht möglich, valide zu prüfen, ob eine Bluetooth-Verbindung zu einem Gerät hergestellt werden kann. Man kann nur versuchen, eine Verbindung herzustellen. Die Verbindung zum Drucker wird über die Methode Connect aufgebaut. Connect blockiert die Benutzeroberfläche möglicherweise für über 10 Sekunden, wenn keine Verbindung hergestellt werden kann. Auch die Anpassung der Oberflächenelemente erfolgt nicht, da deren Aktualisierung asynchron erfolgt und zwar erst, wenn die Methode Connect beendet wurde. Es empfiehlt sich deshalb, nach Anpassung der Oberfläche den Verbindungsaufbau verzögert durch einen Timer zu starten. Die folgende Code-Sequenz zeigt ein Beispiel in Kombination mit der Methode Print:

Verzögerter Aufruf

Der Druck soll über die Schaltfläche cmdPrint ausgelöst werden. Im Ereignis cmdPrint.Click wird zunächst eine CircularProgess-Instanz sichtbar geschaltet. Diese soll anzeigen, dass das System arbeitet. Anschließend wird der Timer Clock1 gestartet (Clock1.TimerEnabled = true). Das Timerintervall ist auf 10 ms eingestellt. Die Verzögerung ist nicht wahrnehmbar, reicht aber für das System aus, die CircularProgess-Instanz zu rendern.

Im Timer-Ereignis wird zunächst der Timer wieder abgeschaltet (One-Shot-Timer) und dann die Methode Connect aufgerufen. Connect liefert true zurück, wenn die Verbindung erfolgreich aufgebaut werden konnte. Ist dies der Fall, wird das Etikett über die Methode Print gedruckt. Zuletzt wird in jedem Fall die CircularProgess-Instanz wieder unsichtbar geschaltet.

Connect meldet Fehlersituationen über das Ereignis Screen.ErrorOccured. Diese sollen dem Anwender mit Hilfe einer Label-Komponente angezeigt werden. UrsPte560 liefert für die Fehler, die beim Aufruf von Connect auftreten können, deutsche Texte. Ob solch ein Text vorhanden ist, kann über die Methode hasMessage abgefragt werden. Der Text kann über getMessage abgerufen werden:

Fehlerbehandlung

Referenz

Eigenschaften

PrinterAddress
Die Bluetooth-Adresse des Druckers, der benutzt werden soll.
PrinterName
Die Bluetooth-Name des Druckers, der benutzt werden soll.

Funktionen

Connect()
Versucht eine Verbindung zu einem PT-E560BT-Drucker herzustellen.
getMessage( errorNumber)
Liefert einen deutschen Text zu den Fehlernummern, bei denen hasMessage true zurück liefert. Ansonsten wird ein Leerstring ("") zurück geliefert.
GetPairedPrinterList()
Liefert eine Liste aller gekoppelten PT-E560BT-Drucker. Jedes Listenelement ist wiederum eine Liste mit zwei Elementen. Das erste enthält eine zur Anzeige geeigneten Bezeichnung des Druckers, das zweite die Bluetooth-Adresse, die zum Verbindungsaufbau benötigt wird. Zur Verwendung siehe Abschnitt Drucker auswählen.
GetPairedPrinterList benutzt dir Funktion AddressesAndNames der BluetoothClient-Komponente.  Unter Android 12 oder später, wenn die Berechtigungen BLUETOOTH_CONNECT und BLUETOOTH_SCAN der App nicht gewährt wurden, liefert die Abfrage eine leere Liste zurück.
hasMessage( errorNumber)
Gibt an, ob getMessage zu der angegeben Fehlernummer einen Text liefert.
Print( Code)
Druckt ein ADFC-EIN-Code-Etikett mit dem angegebenen Code.
SetBluetoothClient( BluetoothClient)
Druckt ein ADFC-EIN-Code-Etikett mit dem angegebenen Code.

Ereignisse

AfterDataPrinted (Success)
Wird ausgelöst, nachdem ein Etikett gedruckt wurde. Success ist false, wenn ein Fehler aufgetreten ist.

Download

Die Quellen der Extension zum Download.

Implementierung

Wie man Erweiterungskomponenten für den MIT App Inventor entwickelt ist hier beschrieben: Extensions entwickeln

Aufbau eines Etiketts

Elemente eines Etiketts   muss in ein Rasterformat mit 406 x 128 Pixel² umgesetzt werden. Rasterung des Eitiketts

Ansteuerung des Druckers

Herauszufinden, wie der Drucker angesteuert werden muss, um ein Etikett zu drucken, war eine echte Herausforderung. Während es zu anderen Druckertypen Dokumentationen zu den Steuersequenzen gibt, konnten für den PT-E560BT keine gefunden werden. Für den Druckertypen PT-E550W gibt es das Software Developer's Manual das wahrscheinlich in weiten Teilen auch für den PT-E560BT nützliche Informationen liefert.

Ein Druckauftrag besteht aus vier Teilen:  Aufbau eines Druckauftrags

Ermittlung der Steuercodes

Zur Ermittlung der Steuercodes habe ich die Brother App zur Ansteuerung der Drucker (Brother Pro Label Tool) auf meinem Smartphone installiert. Beim Druck eines Etiketts habe ich das Bluetooth HCI snoop log aktiviert und mitgeschnitten, was an den Drucker gesendet wird. Wie das geht ist bei medium.com ausführlich beschrieben: Bluetooth LE packet capture on Android. Die Log-Datei habe ich anschließend mit dem kostenlosen Tool WireShark analysiert.

Bluetooth Sniffer Log

Die einzelnen Blöcke kopiert man als Hexdump und fügt sie in einen Texteditor-Programm ein.

Hex-Dump

Für die Ermittlung der Steuersequenzen reicht es den ersten Block zu kopieren. Die Steuersequenzen beginnen beim ersten 0x1B und enden beim ersten 0x47:

Hex Steuersequenzen

Die folgende Tabelle zeigt die ermittelten Steuercodes. Nicht von allen ist die Bedeutung bekannt. Das ist aber nicht von Bedeutung, da die Codes ohne Änderungen übernommen werden können.

Code Bedeutung
0000...0000 100 x 0x00: Clear Buffer
1b40 ESC @: Initialize
1b692100 ESC i !: Switch automatic status notification mode, 0 = Do not notify
1b697001 ESC i p: ?
1b697ac4011800960100000200 ESC i z: Print information command
c4: Valid flag;
01: Media Type 01=Laminated tape
18: Media width (mm) 18=24 mm
00: Media length (mm) n4 is normally 00h, regardless of the paper length.
96010000: Raster number = n8*256*256*256 + n7*256*256 + n6*256 + n5 = 406 Rasterzeilen
02: ??? Starting page: 0, Other pages: 1
00: Fixed at 0
1b694b0c ESC i K: Advanced mode settings 0C: Half cut on, No chain printing
1b694d00 ESC i M: Various mode settings Does not automatically cut, No mirror printing
1b696b630100 ESC i k: ?
1b69640e00 ESC i d: Specify margin amount (feed amount)
4d00 M: Select c ompression mode 0 = uncompressed
1b694c000101 ESC i L: ?
1b694301ffffff ESC i C: ?
47..... Beginn der ersten Rasterzeile

Sollen andere Etikettenformate gedruckt werden, ist nur der Code mit der Angabe der Rasterzeilen zu ändern (in der Tabelle fett markiert).

Ermittlung der Rastergrafik

Natürlich kann man die Rastergrafik per manuell erstellen. Da eine Grafik integriert ist, ist das nicht leicht. Einfacher ist es, wenn man Rastergrafikcodes aufzeichnet und die Variablen Teile ersetzt.

Die Codes für die Rastergrafik sind beim Bluetooth-Transfer mit dem Smartphone komprimiert. Diese Komprimierung zu entschlüsseln und nach zu programmieren ist recht aufwändig. Der Gewinn ist nicht bedeutend und die Bluetooth-Übertragung ist sehr schnell. Einen praktischen Nutzen hat die Komprimierung nicht.

Weniger aufwändig und ohne Komprimierung kann man die Codes über Windows ermitteln. Dazu muss man den Druckertreiber für den PT-E560BT installieren. Dann kann man die Etiketten mit z.B. mit MS-Word drucken. Das Dokument muss entsprechend eingerichtet werden:

Word-Dokument

Die Mindestbreite für den oberen und unteren Rand ist jeweils 0,2 cm. Hier wurden größere Werte gewählt, um die Texte richtig zu platzieren. Die Höhe des Dokuments muss 2,4 cm sein. Die Breite des ADFC-Ein-Code-Etiketts ist 6,1 cm.

Das Dokument gibt man auf den Drucker aus und wählt dabei Ausgabe in Datei:

Dokument in Datei drucken

Zuerst druckt man den fixen Teil und dann alle benötigten weiteren Zeichen in den Positionen (Höhe) an denen sie später ausgegeben werden sollen:

Fixer und Variabler Teil

Die so erstellte Datei analysiert man mit einem Hex-Editor und/oder einem Text-Editor und separiert die Rasterzeilen. Jede Zeile mit mit "47 10 00". Anschließend folgen 16 Byte mit den Daten für die 128 Rasterpunkte.

Hex-Code

Struktur der Extension

Klasse Funktion
UrsPte560 Hauptklasse: Wrapper um eine BluetoothClient-Komponente. Stellt die öffentlichen Eigenschaften und Methoden der Extension bereit.
Label Fügt den fixen und den variablen Teil des Etiketts zusammen und erstellt die zu übertragende Byte-Folge.
Letter Repräsentiert ein einzelnes Zeichen. Die Instanzen werden von Letters bereit gestellt.
Letters Definiert die Rastercodes für die benötigten Zeichen (A-Z, 0-9, -, Leerzeichen).

Die folgenden Beschreibungen geben eine kurzen Überblick über die wesentlichen Methoden der Klassen.

Die Inhalte der benötigen Byte-Arrays sind als String mit Hexadezimalziffern hinterlegt und werden beim Initialisieren der Extension in reale Byte-Arrays (byte[]) mit der Methode fromHexString gewandelt.

static final String rasterFrameString = ""
     + "47100000000000000000000000000000000000"
     + "47100000000000000000000000000000000000"
     ...
     
static final byte[] rasterFrame = fromHexString(rasterFrameString);

Die Alternative, die Byte-Arrays direkt mit Hexadezimalwerten zu belegen, ist recht unübersichtlich. U.a. müssen Werte ≥ 0x80 als byte deklariert werden: (byte) 0x80. Da s sich um statische Daten handelt erfolgt die Wandlung nur einmal bei der Instanziierung der Extension.

Klasse UrsPte560

Die Klasse bedient sich des spezifizierten BluetoothClient-Komponente zum Verbindungsaufbau (Methode Connect) und Datentransfer (Methode Print). Die öffentliche Methode SendBytes des BluetoothClient ist unprakisch, da sie als Argument ein YailList erwartet. Die private Methode write der Superklasse ist besser geeignet. Der Zugriff darauf wird per Reflection ermittelt.

@SimpleFunction(description = "Print EIN-Code label.")
    public void Print(String Code) {
        Log.d(LOG_TAG, "Print " + Code);
        try {
            Label label = new Label();
            for (int i = 0; i < Code.length(); i++) {
                label.addChar(Code.charAt(i));
            }

            Method method = bluetoothClient.getClass().getSuperclass().getDeclaredMethod("write", String.class, byte[].class);
            method.setAccessible(true);
            Object r = method.invoke(bluetoothClient, "Print", label.getBytes());
        } catch (Exception e) {
            DebugUtil.logExecption(LOG_TAG, e);
        }
    }

Der Klasse Label wir die zu druckende Zeichenfolge zeichenweise übergeben. Dann wird per Reflection eine Zugriff auf write konstruiert. Die zum Druck notwendige Byte-Folge wird von Label abgerufen und übertragen.

Klasse Label

Die Klasse Label stellt die zu übertragende byte-Folge zusammen. Dort ist der fixe Teil und die Steuer-Codes abgelegt:

static final String rasterFrameString = ""
     + "47100000000000000000000000000000000000"
     + "47100000000000000000000000000000000000"
     + "47100000000000000000000000000000000000"
     + "47100000000000000000000000000000000000"
     + "47100000000000000000000000000000000000"
     + "47100000000000000000000000000000000000"
     + "471000000007FFFFFFFFFFFFFFFFFFFFE00000"
     + "47100000007FFFFFFFFFFFFFFFFFFFFFFE0000"
     + "4710000007F8000000000000000000001FE000"
     + "471000001F800000000000000000000001F800"
     + "47100000380000000000000000000000001C00"
     ...
static final String btHeaderString = ""
     + "00000000000000 ... 000000000000"
     + "1b696101" // ESC i a: Switch dynamic command mode, 01: raster graphics (PTCBP) mode
     + "1b40" // ESC @: Initialize
     ...

Die Methode fromHexString macht aus dem lesbaren String eine Byte-Array.

private static byte[] fromHexString(String src) {
  byte[] biBytes = new BigInteger("10" + src, 16).toByteArray();
  return Arrays.copyOfRange(biBytes, 1, biBytes.length);
}

Beim Anlegen einer neuen Instanz wird eine Kopie des fixen Teils angelegt. In diese Kopie werden dann nach und nach die variablen Codes eingefügt

private byte[] label;

Label() {
    label = rasterFrame.clone();
  }
  
  // ...
  
void addChar(char c) {
  Letter letter = Letters.getLetter(c);

  for (int i = 0; i < letter.getLength(); i++) { // Über alle Rasterlinien des Zeichens
    byte[] line = letter.getLine(i);   // line = die 16 Byte der Rasterlinie
    int rasterByte = rasterLine * 19 + 3; // Zeile Muster ist 19 Byte lang + '471000' der aktuellen Zeile
    for (int j = 0; j < line.length; j++) { // Über die 16 Byte der Rasterlinie
      label[rasterByte++] |= line[j];  // Musterlinie und Zeichenlinie mit OR verknüpfen
    }
    rasterLine++; // nächste Rasterlinie
  }
  rasterLine += rasterGap; // Zwischenraum zwischen zwei Zeichen
}

Beim Abruf der Byte-Folge werden die Steuercodes, die Rasterdaten und ein abschließendes "0x1A", das den Druck auslöst, zusammengefasst.

byte[] getBytes() {
  return  concatAll(header, label, fromHexString("1A"));
}

private static byte[] concatAll(byte[] first, byte[]... rest) {
  int totalLength = first.length;
  for (byte[] array : rest) {
    totalLength += array.length;
  }
  byte[] result = Arrays.copyOf(first, totalLength);
  int offset = first.length;
  for (byte[] array : rest) {
    System.arraycopy(array, 0, result, offset, array.length);
    offset += array.length;
  }
  return result;
}

Klasse Letter

Die Klasse Letter ist ein Wrapper um die Rastercodes der einzelnen Zeichen. Sie vereinfacht den Zugriff auf die Daten.

class Letter {
    byte[] data;

    Letter(byte[] data) {
        this.data = data;
    }

    public int getLength() {
        return data.length / 16; // 16 Byte pro Rasterzeile
    }

    public byte[] getLine(int line) {
        return Arrays.copyOfRange(data, line * 16, line * 16 + 16);
    }
}

Im Konstruktor werden die Rastercodes als Byte-Array für ein Zeichen übergeben.

Klasse Letters

Die Klasse Letters verwaltet die Rastercodes für die verschiedenen Zeichen:

// Die Breite einer Rahmenzeile sind 3 + 16 = 19 Byte
 // Die Breite einer Buchstabenzeile ist 16 Byte
 // Die Rahmenzeile kann ab der Position 3 mit der Buchstabenzeile mit "OR" verknüpft werden.
 // Der erste Buchstabe beginnt in Rasterzeile 35
 // Zwei Leerzeilen zwischen den Zeichen
 // Der Rahmen besitzt 406 Rasterzeilen
 private static final String letterAString = ""
                 + "00000000000000000000000000002000"
                 + "0000000000000000000000000001E000"
                 + "0000000001F0000001E00000000FE000"
                 + "0000000001F0000001E00000007FE000"
                 + "0000000001F0000000E0000003FFE000"
                 + "0000000001F00000000000001FFF0000"
                 + "0000000001F00000000000007FFC0000"
                 ...

Die Hexcode-Strings werden beim Instanziieren der Extension in Byte-Arrays gewandelt.

private static final byte[] letterA = fromHexString(letterAString);
private static final byte[] letterB = fromHexString(letterBString);
private static final byte[] letterC = fromHexString(letterCString);
...

static final byte[][] letters = { letterA, letterB, letterC, letterD, letterE, letterF, letterG, letterH,
                letterI, letterJ, letterK, letterL, letterM, letterN, letterO, letterP, letterQ, letterR,
                letterS, letterT, letterU, letterV, letterW, letterX, letterY, letterZ,
                digit0, digit1, digit2, digit3, digit4, digit5, digit6, digit7, digit8, digit9, symbolBlank,
                symbolMinus, unknown };

Die Methode getLetter erzeugt eine Instanz der Klasse Letter mit den Rasterdaten des angegebenen char.

static Letter getLetter(char c) {
        return new Letter(letters[charToIndex(c)]);
}

charToIndex ermittelt den Index des char im Array der hinterlegten Zeichenraster.

private static int charToIndex(char c) {
        if (c >= 'A' && c <= 'Z')
                return (int) (c - 'A');
        if (c == ' ')
                return 36;
        if (c == '-')
                return 37;
        if (c >= '0' && c <= '9')
                return (int) (c - '0' + 26);
        return 38; // unknown
}