Der ADFC-Stormarn e.V. verwendet Klebeetiketten zur Codierung von Fahrrädern.
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.
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.
|
1.2 (2025-05-10) | Einige Methoden mit einem Try-Catch-Block versehen. |
Inhaltsverzeichnis
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:
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).
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.
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:
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.
Bei der Rückkehr von diesem Screen muss eine mögliche Druckerauswahl berücksichtigt werden.
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:
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:
Die Quellen der Extension zum Download.
Wie man Erweiterungskomponenten für den MIT App Inventor entwickelt ist hier beschrieben: Extensions entwickeln
![]() |
muss in ein Rasterformat mit 406 x 128 Pixel² umgesetzt werden. |
![]() |
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: |
![]() |
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.
Die einzelnen Blöcke kopiert man als Hexdump und fügt sie in einen Texteditor-Programm ein.
Für die Ermittlung der Steuersequenzen reicht es den ersten Block zu kopieren. Die Steuersequenzen beginnen beim ersten 0x1B und enden beim ersten 0x47:
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).
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:
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:
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:
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.
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.
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.
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;
}
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.
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
}